SpecialVis: allow import flows to work with multiple target layers

This commit is contained in:
Pieter Vander Vennet 2024-01-16 04:20:25 +01:00
parent 7872f22151
commit 915cad2253
6 changed files with 349 additions and 329 deletions

View file

@ -1,41 +1,57 @@
<script lang="ts">
/**
* The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ...
* They show some default components
*/
import ImportFlow from "./ImportFlow"
import LoginToggle from "../../Base/LoginToggle.svelte"
import BackButton from "../../Base/BackButton.svelte"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import NextButton from "../../Base/NextButton.svelte"
import { createEventDispatcher } from "svelte"
import Loading from "../../Base/Loading.svelte"
import { And } from "../../../Logic/Tags/And"
import TagHint from "../TagHint.svelte"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { Store } from "../../../Logic/UIEventSource"
import Svg from "../../../Svg"
import ToSvelte from "../../Base/ToSvelte.svelte"
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
import type { ImportFlowArguments } from "./ImportFlow"
/**
* The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ...
* They show some default components
*/
import ImportFlow from "./ImportFlow"
import LoginToggle from "../../Base/LoginToggle.svelte"
import BackButton from "../../Base/BackButton.svelte"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import NextButton from "../../Base/NextButton.svelte"
import { createEventDispatcher, onDestroy } from "svelte"
import Loading from "../../Base/Loading.svelte"
import { And } from "../../../Logic/Tags/And"
import TagHint from "../TagHint.svelte"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { Store } from "../../../Logic/UIEventSource"
import Svg from "../../../Svg"
import ToSvelte from "../../Base/ToSvelte.svelte"
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"
import FilteredLayer from "../../../Models/FilteredLayer"
export let importFlow: ImportFlow
let state = importFlow.state
export let importFlow: ImportFlow<ImportFlowArguments>
let state = importFlow.state
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start"
const isLoading = state.dataIsLoading
const dispatch = createEventDispatcher<{ confirm }>()
const canBeImported = importFlow.canBeImported()
const tags: Store<TagsFilter> = importFlow.tagsToApply.map((tags) => new And(tags))
const isLoading = state.dataIsLoading
let dispatch = createEventDispatcher<{ confirm }>()
let canBeImported = importFlow.canBeImported()
let tags: Store<TagsFilter> = importFlow.tagsToApply.map((tags) => new And(tags))
const isDisplayed = importFlow.targetLayer.isDisplayed
const hasFilter = importFlow.targetLayer.hasFilter
function abort() {
state.selectedElement.setData(undefined)
state.selectedLayer.setData(undefined)
}
let targetLayers = importFlow.targetLayer
let filteredLayer: FilteredLayer
let undisplayedLayer: FilteredLayer
function updateIsDisplayed() {
filteredLayer = targetLayers.find(tl => tl.hasFilter.data)
undisplayedLayer = targetLayers.find(tl => !tl.isDisplayed.data)
}
updateIsDisplayed()
for (const tl of targetLayers) {
onDestroy(
tl.isDisplayed.addCallback(updateIsDisplayed),
)
}
function abort() {
state.selectedElement.setData(undefined)
}
</script>
<LoginToggle {state}>
@ -44,13 +60,13 @@
{#if $canBeImported.extraHelp}
<Tr t={$canBeImported.extraHelp} />
{/if}
{:else if !$isDisplayed}
{:else if undisplayedLayer !== undefined}
<!-- Check that the layer is enabled, so that we don't add a duplicate -->
<div class="alert flex items-center justify-center">
<EyeOffIcon class="w-8" />
<Tr
t={Translations.t.general.add.layerNotEnabled.Subs({
layer: importFlow.targetLayer.layerDef.name,
layer: undisplayedLayer.layerDef.name,
})}
/>
</div>
@ -60,7 +76,7 @@
class="flex w-full gap-x-1"
on:click={() => {
abort()
state.guistate.openFilterView(importFlow.targetLayer.layerDef)
state.guistate.openFilterView(filteredLayer.layerDef)
}}
>
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />
@ -70,19 +86,19 @@
<button
class="primary flex w-full gap-x-1"
on:click={() => {
isDisplayed.setData(true)
undisplayedLayer.isDisplayed.setData(true)
abort()
}}
>
<EyeIcon class="w-12" />
<Tr
t={Translations.t.general.add.enableLayer.Subs({
name: importFlow.targetLayer.layerDef.name,
name: undisplayedLayer.layerDef.name,
})}
/>
</button>
</div>
{:else if $hasFilter}
{:else if filteredLayer !== undefined}
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
<div class="alert flex items-center justify-center">
<EyeOffIcon class="w-8" />
@ -93,7 +109,7 @@
class="primary flex w-full gap-x-1"
on:click={() => {
abort()
importFlow.targetLayer.disableAllFilters()
filteredLayer.disableAllFilters()
}}
>
<EyeOffIcon class="w-12" />
@ -103,7 +119,7 @@
class="flex w-full gap-x-1"
on:click={() => {
abort()
state.guistate.openFilterView(importFlow.targetLayer.layerDef)
state.guistate.openFilterView(filteredLayer.layerDef)
}}
>
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} />

View file

@ -6,7 +6,6 @@ import TagApplyButton from "../TagApplyButton"
import { PointImportFlowArguments } from "./PointImportFlowState"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
import FilteredLayer from "../../../Models/FilteredLayer"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { LayerConfigJson } from "../../../Models/ThemeConfig/Json/LayerConfigJson"
@ -25,7 +24,7 @@ export class ImportFlowUtils {
public static readonly conflationLayer = new LayerConfig(
<LayerConfigJson>conflation_json,
"all_known_layers",
true
true,
)
public static readonly documentationGeneral = `\n\n\nNote that the contributor must zoom to at least zoomlevel 18 to be able to use this functionality.
@ -67,7 +66,7 @@ ${Utils.special_visualizations_importRequirementDocs}
*/
public static getTagsToApply(
originalFeatureTags: UIEventSource<any>,
args: { tags: string }
args: { tags: string },
): Store<Tag[]> {
if (originalFeatureTags === undefined) {
return undefined
@ -83,9 +82,9 @@ ${Utils.special_visualizations_importRequirementDocs}
const items: string = originalFeatureTags.data[tags]
console.debug(
"The import button is using tags from properties[" +
tags +
"] of this object, namely ",
items
tags +
"] of this object, namely ",
items,
)
if (items.startsWith("{")) {
@ -108,13 +107,12 @@ ${Utils.special_visualizations_importRequirementDocs}
* - targetLayer
*
* Others (e.g.: snapOnto-layers) are not to be handled here
* @param argsRaw
*/
public static getLayerDependencies(argsRaw: string[], argSpec?) {
public static getLayerDependencies(argsRaw: string[], argSpec?): string[] {
const args: ImportFlowArguments = <any>(
Utils.ParseVisArgs(argSpec ?? ImportFlowUtils.generalArguments, argsRaw)
)
return [args.targetLayer]
return args.targetLayer.split(" ")
}
public static getLayerDependenciesWithSnapOnto(
@ -122,7 +120,7 @@ ${Utils.special_visualizations_importRequirementDocs}
name: string
defaultValue?: string
}[],
argsRaw: string[]
argsRaw: string[],
): string[] {
const deps = ImportFlowUtils.getLayerDependencies(argsRaw, argSpec)
const argsParsed: PointImportFlowArguments = <any>Utils.ParseVisArgs(argSpec, argsRaw)
@ -130,30 +128,6 @@ ${Utils.special_visualizations_importRequirementDocs}
deps.push(...snapOntoLayers)
return deps
}
public static buildTagSpec(
args: ImportFlowArguments,
tagSource: Store<Record<string, string>>
): Store<string> {
let tagSpec = args.tags
return tagSource.mapD((tags) => {
if (
tagSpec.indexOf(" ") < 0 &&
tagSpec.indexOf(";") < 0 &&
tags[args.tags] !== undefined
) {
// This is probably a key
tagSpec = tags[args.tags]
console.debug(
"The import button is using tags from properties[" +
args.tags +
"] of this object, namely ",
tagSpec
)
}
return tagSpec
})
}
}
/**
@ -164,7 +138,7 @@ ${Utils.special_visualizations_importRequirementDocs}
export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
public readonly state: SpecialVisualizationState
public readonly args: ArgT
public readonly targetLayer: FilteredLayer
public readonly targetLayer: FilteredLayer[]
public readonly tagsToApply: Store<Tag[]>
protected readonly _originalFeatureTags: UIEventSource<Record<string, string>>
@ -172,13 +146,19 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
state: SpecialVisualizationState,
args: ArgT,
tagsToApply: Store<Tag[]>,
originalTags: UIEventSource<Record<string, string>>
originalTags: UIEventSource<Record<string, string>>,
) {
this.state = state
this.args = args
this.tagsToApply = tagsToApply
this._originalFeatureTags = originalTags
this.targetLayer = state.layerState.filteredLayers.get(args.targetLayer)
this.targetLayer = args.targetLayer.split(" ").map(tl => {
let found = state.layerState.filteredLayers.get(tl)
if (!found) {
throw "Layer " + tl + " not found"
}
return found
})
}
/**
@ -218,7 +198,7 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
return undefined
},
[state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags]
[state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags],
)
}
}

View file

@ -17,7 +17,7 @@ export class PointImportButtonViz implements SpecialVisualization {
public readonly funcName: string
public readonly docs: string | BaseUIElement
public readonly example?: string
public readonly args: { name: string; defaultValue?: string; doc: string }[]
public readonly args: { name: string; defaultValue?: string; doc: string, split?: boolean }[]
public needsUrls = []
constructor() {

View file

@ -13,7 +13,7 @@
const args = importFlow.args
// The following variables are used for the map
const targetLayer: LayerConfig = state.layout.layers.find((l) => l.id === args.targetLayer)
const targetLayers: LayerConfig[] = args.targetLayer.split(" ").map(tl => state.layout.layers.find((l) => l.id === tl))
const snapToLayers: string[] | undefined =
args.snap_onto_layers?.split(",")?.map((l) => l.trim()) ?? []
const maxSnapDistance: number = Number(args.max_snap_distance ?? 25) ?? 25
@ -33,21 +33,20 @@
async function onConfirm(): Promise<void> {
const importedId = await importFlow.onConfirm(value.data, snappedTo.data)
state.selectedLayer.setData(targetLayer)
state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(importedId))
}
</script>
<ImportFlow {importFlow} on:confirm={onConfirm}>
<div class="relative" slot="map">
<div class="h-32">
<div class="h-64">
<NewPointLocationInput
coordinate={startCoordinate}
{maxSnapDistance}
{snapToLayers}
{snappedTo}
{state}
{targetLayer}
targetLayer={targetLayers}
{value}
/>
</div>