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,129 +1,135 @@
<script lang="ts"> <script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization" import type { SpecialVisualizationState } from "../SpecialVisualization"
import LocationInput from "../InputElement/Helpers/LocationInput.svelte" import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import { Tiles } from "../../Models/TileRange" import { Tiles } from "../../Models/TileRange"
import { Map as MlMap } from "maplibre-gl" import { Map as MlMap } from "maplibre-gl"
import { BBox } from "../../Logic/BBox" import { BBox } from "../../Logic/BBox"
import type { MapProperties } from "../../Models/MapProperties" import type { MapProperties } from "../../Models/MapProperties"
import ShowDataLayer from "../Map/ShowDataLayer" import ShowDataLayer from "../Map/ShowDataLayer"
import type { import type { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource"
FeatureSource, import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource"
FeatureSourceForLayer, import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
} from "../../Logic/FeatureSource/FeatureSource" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource" import { Utils } from "../../Utils"
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger" import { createEventDispatcher } from "svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import Move_arrows from "../../assets/svg/Move_arrows.svelte"
import { Utils } from "../../Utils"
import { createEventDispatcher } from "svelte"
import Move_arrows from "../../assets/svg/Move_arrows.svelte"
/** /**
* An advanced location input, which has support to: * An advanced location input, which has support to:
* - Show more layers * - Show more layers
* - Snap to layers * - Snap to layers
* *
* This one is mostly used to insert new points, including when importing * This one is mostly used to insert new points, including when importing
*/ */
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
/** /**
* The start coordinate * The start coordinate
*/ */
export let coordinate: { lon: number; lat: number } export let coordinate: { lon: number; lat: number }
/** /**
* The center of the map at all times * The center of the map at all times
* If undefined at the beginning, 'coordinate' will be used * If undefined at the beginning, 'coordinate' will be used
*/ */
export let value: UIEventSource<{ lon: number; lat: number }> export let value: UIEventSource<{ lon: number; lat: number }>
if (value.data === undefined) { if (value.data === undefined) {
value.setData(coordinate) value.setData(coordinate)
}
if (coordinate === undefined) {
coordinate = value.data
}
export let snapToLayers: string[] | undefined
export let targetLayer: LayerConfig | undefined
export let maxSnapDistance: number = undefined
export let snappedTo: UIEventSource<string | undefined>
let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{
lon: number
lat: number
}>(undefined)
const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>()
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16)
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let initialMapProperties: Partial<MapProperties> = {
zoom: new UIEventSource<number>(19),
maxbounds: new UIEventSource(undefined),
/*If no snapping needed: the value is simply the map location;
* If snapping is needed: the value will be set later on by the snapping feature source
* */
location:
snapToLayers?.length > 0
? new UIEventSource<{ lon: number; lat: number }>(coordinate)
: value,
bounds: new UIEventSource<BBox>(undefined),
allowMoving: new UIEventSource<boolean>(true),
allowZooming: new UIEventSource<boolean>(true),
minzoom: new UIEventSource<number>(18),
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
}
if (targetLayer) {
const featuresForLayer = state.perLayer.get(targetLayer.id)
if (featuresForLayer) {
new ShowDataLayer(map, {
layer: targetLayer,
features: featuresForLayer,
})
} }
} if (coordinate === undefined) {
coordinate = value.data
if (snapToLayers?.length > 0) {
const snapSources: FeatureSource[] = []
for (const layerId of snapToLayers ?? []) {
const layer: FeatureSourceForLayer = state.perLayer.get(layerId)
snapSources.push(layer)
if (layer.features === undefined) {
continue
}
new ShowDataLayer(map, {
layer: layer.layer.layerDef,
zoomToFeatures: false,
features: layer,
})
} }
const snappedLocation = new SnappingFeatureSource( export let snapToLayers: string[] | undefined
new FeatureSourceMerger(...Utils.NoNull(snapSources)), export let targetLayer: LayerConfig | LayerConfig[] | undefined
// We snap to the (constantly updating) map location
initialMapProperties.location,
{
maxDistance: maxSnapDistance ?? 15,
allowUnsnapped: true,
snappedTo,
snapLocation: value,
}
)
new ShowDataLayer(map, { let targetLayers: LayerConfig[] | undefined
layer: targetLayer, if (Array.isArray(targetLayers)) {
features: snappedLocation, targetLayers = <LayerConfig[]>targetLayer
} else if (targetLayer) {
targetLayers = [<LayerConfig>targetLayer]
}
export let maxSnapDistance: number = undefined
export let snappedTo: UIEventSource<string | undefined>
let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{
lon: number
lat: number
}>(undefined)
const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>()
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16)
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let initialMapProperties: Partial<MapProperties> = {
zoom: new UIEventSource<number>(19),
maxbounds: new UIEventSource(undefined),
/*If no snapping needed: the value is simply the map location;
* If snapping is needed: the value will be set later on by the snapping feature source
* */
location:
snapToLayers?.length > 0
? new UIEventSource<{ lon: number; lat: number }>(coordinate)
: value,
bounds: new UIEventSource<BBox>(undefined),
allowMoving: new UIEventSource<boolean>(true),
allowZooming: new UIEventSource<boolean>(true),
minzoom: new UIEventSource<number>(18),
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
}
targetLayers?.forEach(layer => {
const featuresForLayer = state.perLayer.get(layer.id)
if (featuresForLayer) {
new ShowDataLayer(map, {
layer,
features: featuresForLayer,
})
}
}) })
}
if (snapToLayers?.length > 0) {
const snapSources: FeatureSource[] = []
for (const layerId of snapToLayers ?? []) {
const layer: FeatureSourceForLayer = state.perLayer.get(layerId)
snapSources.push(layer)
if (layer.features === undefined) {
continue
}
new ShowDataLayer(map, {
layer: layer.layer.layerDef,
zoomToFeatures: false,
features: layer,
})
}
const snappedLocation = new SnappingFeatureSource(
new FeatureSourceMerger(...Utils.NoNull(snapSources)),
// We snap to the (constantly updating) map location
initialMapProperties.location,
{
maxDistance: maxSnapDistance ?? 15,
allowUnsnapped: true,
snappedTo,
snapLocation: value,
},
)
targetLayers.forEach(layer => {
new ShowDataLayer(map, {
layer,
features: snappedLocation,
})
})
}
</script> </script>
<LocationInput <LocationInput
{map}
on:click={(data) => dispatch("click", data)}
mapProperties={initialMapProperties}
value={preciseLocation}
initialCoordinate={coordinate} initialCoordinate={coordinate}
maxDistanceInMeters="50" {map}
mapProperties={initialMapProperties}
maxDistanceInMeters={50}
on:click={(data) => dispatch("click", data)}
value={preciseLocation}
> >
<slot name="image" slot="image"> <slot name="image" slot="image">
<Move_arrows class="h-full max-h-24" /> <Move_arrows class="h-full max-h-24" />

View file

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

View file

@ -6,7 +6,6 @@ import TagApplyButton from "../TagApplyButton"
import { PointImportFlowArguments } from "./PointImportFlowState" import { PointImportFlowArguments } from "./PointImportFlowState"
import { Translation } from "../../i18n/Translation" import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations" import Translations from "../../i18n/Translations"
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
import FilteredLayer from "../../../Models/FilteredLayer" import FilteredLayer from "../../../Models/FilteredLayer"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { LayerConfigJson } from "../../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../../Models/ThemeConfig/Json/LayerConfigJson"
@ -25,7 +24,7 @@ export class ImportFlowUtils {
public static readonly conflationLayer = new LayerConfig( public static readonly conflationLayer = new LayerConfig(
<LayerConfigJson>conflation_json, <LayerConfigJson>conflation_json,
"all_known_layers", "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. 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( public static getTagsToApply(
originalFeatureTags: UIEventSource<any>, originalFeatureTags: UIEventSource<any>,
args: { tags: string } args: { tags: string },
): Store<Tag[]> { ): Store<Tag[]> {
if (originalFeatureTags === undefined) { if (originalFeatureTags === undefined) {
return undefined return undefined
@ -83,9 +82,9 @@ ${Utils.special_visualizations_importRequirementDocs}
const items: string = originalFeatureTags.data[tags] const items: string = originalFeatureTags.data[tags]
console.debug( console.debug(
"The import button is using tags from properties[" + "The import button is using tags from properties[" +
tags + tags +
"] of this object, namely ", "] of this object, namely ",
items items,
) )
if (items.startsWith("{")) { if (items.startsWith("{")) {
@ -108,13 +107,12 @@ ${Utils.special_visualizations_importRequirementDocs}
* - targetLayer * - targetLayer
* *
* Others (e.g.: snapOnto-layers) are not to be handled here * 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>( const args: ImportFlowArguments = <any>(
Utils.ParseVisArgs(argSpec ?? ImportFlowUtils.generalArguments, argsRaw) Utils.ParseVisArgs(argSpec ?? ImportFlowUtils.generalArguments, argsRaw)
) )
return [args.targetLayer] return args.targetLayer.split(" ")
} }
public static getLayerDependenciesWithSnapOnto( public static getLayerDependenciesWithSnapOnto(
@ -122,7 +120,7 @@ ${Utils.special_visualizations_importRequirementDocs}
name: string name: string
defaultValue?: string defaultValue?: string
}[], }[],
argsRaw: string[] argsRaw: string[],
): string[] { ): string[] {
const deps = ImportFlowUtils.getLayerDependencies(argsRaw, argSpec) const deps = ImportFlowUtils.getLayerDependencies(argsRaw, argSpec)
const argsParsed: PointImportFlowArguments = <any>Utils.ParseVisArgs(argSpec, argsRaw) const argsParsed: PointImportFlowArguments = <any>Utils.ParseVisArgs(argSpec, argsRaw)
@ -130,30 +128,6 @@ ${Utils.special_visualizations_importRequirementDocs}
deps.push(...snapOntoLayers) deps.push(...snapOntoLayers)
return deps 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> { export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
public readonly state: SpecialVisualizationState public readonly state: SpecialVisualizationState
public readonly args: ArgT public readonly args: ArgT
public readonly targetLayer: FilteredLayer public readonly targetLayer: FilteredLayer[]
public readonly tagsToApply: Store<Tag[]> public readonly tagsToApply: Store<Tag[]>
protected readonly _originalFeatureTags: UIEventSource<Record<string, string>> protected readonly _originalFeatureTags: UIEventSource<Record<string, string>>
@ -172,13 +146,19 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
state: SpecialVisualizationState, state: SpecialVisualizationState,
args: ArgT, args: ArgT,
tagsToApply: Store<Tag[]>, tagsToApply: Store<Tag[]>,
originalTags: UIEventSource<Record<string, string>> originalTags: UIEventSource<Record<string, string>>,
) { ) {
this.state = state this.state = state
this.args = args this.args = args
this.tagsToApply = tagsToApply this.tagsToApply = tagsToApply
this._originalFeatureTags = originalTags 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 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 funcName: string
public readonly docs: string | BaseUIElement public readonly docs: string | BaseUIElement
public readonly example?: string 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 = [] public needsUrls = []
constructor() { constructor() {

View file

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

View file

@ -3,11 +3,7 @@ import { FixedUiElement } from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement" import BaseUIElement from "./BaseUIElement"
import Title from "./Base/Title" import Title from "./Base/Title"
import Table from "./Base/Table" import Table from "./Base/Table"
import { import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
RenderingSpecification,
SpecialVisualization,
SpecialVisualizationState,
} from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz" import { HistogramViz } from "./Popup/HistogramViz"
import { MinimapViz } from "./Popup/MinimapViz" import { MinimapViz } from "./Popup/MinimapViz"
import { ShareLinkViz } from "./Popup/ShareLinkViz" import { ShareLinkViz } from "./Popup/ShareLinkViz"
@ -110,7 +106,7 @@ class NearbyImageVis implements SpecialVisualization {
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const isOpen = args[0] === "open" const isOpen = args[0] === "open"
const [lon, lat] = GeoOperations.centerpointCoordinates(feature) const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
@ -175,7 +171,7 @@ class StealViz implements SpecialVisualization {
selectedElement: otherFeature, selectedElement: otherFeature,
state, state,
layer, layer,
}) }),
) )
} }
if (elements.length === 1) { if (elements.length === 1) {
@ -183,8 +179,8 @@ class StealViz implements SpecialVisualization {
} }
return new Combine(elements).SetClass("flex flex-col") return new Combine(elements).SetClass("flex flex-col")
}, },
[state.indexedFeatures.featuresById] [state.indexedFeatures.featuresById],
) ),
) )
} }
@ -223,7 +219,7 @@ export class QuestionViz implements SpecialVisualization {
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const labels = args[0] const labels = args[0]
?.split(";") ?.split(";")
@ -277,17 +273,17 @@ export default class SpecialVisualizations {
* templ.args[0] = "{email}" * templ.args[0] = "{email}"
*/ */
public static constructSpecification( public static constructSpecification(
template: string, template: string | { special: Record<string, string | Record<string, string>> & { type: string } },
extraMappings: SpecialVisualization[] = [] extraMappings: SpecialVisualization[] = [],
): RenderingSpecification[] { ): RenderingSpecification[] {
if (template === "") { if (template === "") {
return [] return []
} }
if (template["type"] !== undefined) { if (typeof template !== "string") {
console.trace( console.trace(
"Got a non-expanded template while constructing the specification, it still has a 'special-key':", "Got a non-expanded template while constructing the specification, it still has a 'special-key':",
template template,
) )
throw "Got a non-expanded template while constructing the specification" throw "Got a non-expanded template while constructing the specification"
} }
@ -295,20 +291,20 @@ export default class SpecialVisualizations {
for (const knownSpecial of allKnownSpecials) { for (const knownSpecial of allKnownSpecials) {
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way' // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
const matched = template.match( const matched = template.match(
new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s") new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s"),
) )
if (matched != null) { if (matched != null) {
// We found a special component that should be brought to live // We found a special component that should be brought to live
const partBefore = SpecialVisualizations.constructSpecification( const partBefore = SpecialVisualizations.constructSpecification(
matched[1], matched[1],
extraMappings extraMappings,
) )
const argument = const argument =
matched[2] /* .trim() // We don't trim, as spaces might be relevant, e.g. "what is ... of {title()}"*/ matched[2] /* .trim() // We don't trim, as spaces might be relevant, e.g. "what is ... of {title()}"*/
const style = matched[3]?.substring(1) ?? "" const style = matched[3]?.substring(1) ?? ""
const partAfter = SpecialVisualizations.constructSpecification( const partAfter = SpecialVisualizations.constructSpecification(
matched[4], matched[4],
extraMappings extraMappings,
) )
const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "") const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "")
if (argument.length > 0) { if (argument.length > 0) {
@ -347,31 +343,31 @@ export default class SpecialVisualizations {
viz.docs, viz.docs,
viz.args.length > 0 viz.args.length > 0
? new Table( ? new Table(
["name", "default", "description"], ["name", "default", "description"],
viz.args.map((arg) => { viz.args.map((arg) => {
let defaultArg = arg.defaultValue ?? "_undefined_" let defaultArg = arg.defaultValue ?? "_undefined_"
if (defaultArg == "") { if (defaultArg == "") {
defaultArg = "_empty string_" defaultArg = "_empty string_"
} }
return [arg.name, defaultArg, arg.doc] return [arg.name, defaultArg, arg.doc]
}) }),
) )
: undefined, : undefined,
new Title("Example usage of " + viz.funcName, 4), new Title("Example usage of " + viz.funcName, 4),
new FixedUiElement( new FixedUiElement(
viz.example ?? viz.example ??
"`{" + "`{" +
viz.funcName + viz.funcName +
"(" + "(" +
viz.args.map((arg) => arg.defaultValue).join(",") + viz.args.map((arg) => arg.defaultValue).join(",") +
")}`" ")}`",
).SetClass("literal-code"), ).SetClass("literal-code"),
]) ])
} }
public static HelpMessage() { public static HelpMessage() {
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) => const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
SpecialVisualizations.DocumentationFor(viz) SpecialVisualizations.DocumentationFor(viz),
) )
return new Combine([ return new Combine([
@ -405,10 +401,10 @@ export default class SpecialVisualizations {
}, },
}, },
null, null,
" " " ",
) ),
).SetClass("code"), ).SetClass("code"),
'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)', "In other words: use `{ \"before\": ..., \"after\": ..., \"special\": {\"type\": ..., \"argname\": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)",
]).SetClass("flex flex-col"), ]).SetClass("flex flex-col"),
...helpTexts, ...helpTexts,
]).SetClass("flex flex-col") ]).SetClass("flex flex-col")
@ -417,20 +413,20 @@ export default class SpecialVisualizations {
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
public static renderExampleOfSpecial( public static renderExampleOfSpecial(
state: SpecialVisualizationState, state: SpecialVisualizationState,
s: SpecialVisualization s: SpecialVisualization,
): BaseUIElement { ): BaseUIElement {
const examples = const examples =
s.structuredExamples === undefined s.structuredExamples === undefined
? [] ? []
: s.structuredExamples().map((e) => { : s.structuredExamples().map((e) => {
return s.constr( return s.constr(
state, state,
new UIEventSource<Record<string, string>>(e.feature.properties), new UIEventSource<Record<string, string>>(e.feature.properties),
e.args, e.args,
e.feature, e.feature,
undefined undefined,
) )
}) })
return new Combine([new Title(s.funcName), s.docs, ...examples]) return new Combine([new Title(s.funcName), s.docs, ...examples])
} }
@ -470,7 +466,7 @@ export default class SpecialVisualizations {
assignTo: state.userRelatedState.language, assignTo: state.userRelatedState.language,
availableLanguages: state.layout.language, availableLanguages: state.layout.language,
preferredLanguages: state.osmConnection.userDetails.map( preferredLanguages: state.osmConnection.userDetails.map(
(ud) => ud.languages (ud) => ud.languages,
), ),
}) })
}, },
@ -495,7 +491,7 @@ export default class SpecialVisualizations {
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>> tagSource: UIEventSource<Record<string, string>>,
): BaseUIElement { ): BaseUIElement {
return new VariableUiElement( return new VariableUiElement(
tagSource tagSource
@ -505,7 +501,7 @@ export default class SpecialVisualizations {
return new SplitRoadWizard(<WayId>id, state) return new SplitRoadWizard(<WayId>id, state)
} }
return undefined return undefined
}) }),
) )
}, },
}, },
@ -519,7 +515,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
if (feature.geometry.type !== "Point") { if (feature.geometry.type !== "Point") {
return undefined return undefined
@ -542,7 +538,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
if (!layer.deletion) { if (!layer.deletion) {
return undefined return undefined
@ -570,7 +566,7 @@ export default class SpecialVisualizations {
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature feature: Feature,
): BaseUIElement { ): BaseUIElement {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature) const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(CreateNewNote, { return new SvelteUIElement(CreateNewNote, {
@ -634,7 +630,7 @@ export default class SpecialVisualizations {
.map((tags) => tags[args[0]]) .map((tags) => tags[args[0]])
.map((wikidata) => { .map((wikidata) => {
wikidata = Utils.NoEmpty( wikidata = Utils.NoEmpty(
wikidata?.split(";")?.map((wd) => wd.trim()) ?? [] wikidata?.split(";")?.map((wd) => wd.trim()) ?? [],
)[0] )[0]
const entry = Wikidata.LoadWikidataEntry(wikidata) const entry = Wikidata.LoadWikidataEntry(wikidata)
return new VariableUiElement( return new VariableUiElement(
@ -644,9 +640,9 @@ export default class SpecialVisualizations {
} }
const response = <WikidataResponse>e["success"] const response = <WikidataResponse>e["success"]
return Translation.fromMap(response.labels) return Translation.fromMap(response.labels)
}) }),
) )
}) }),
), ),
}, },
new MapillaryLinkVis(), new MapillaryLinkVis(),
@ -678,7 +674,7 @@ export default class SpecialVisualizations {
AllImageProviders.LoadImagesFor(tags, imagePrefixes), AllImageProviders.LoadImagesFor(tags, imagePrefixes),
tags, tags,
state, state,
feature feature,
) )
}, },
}, },
@ -734,7 +730,7 @@ export default class SpecialVisualizations {
{ {
nameKey: nameKey, nameKey: nameKey,
fallbackName, fallbackName,
} },
) )
return new SvelteUIElement(StarsBarIcon, { return new SvelteUIElement(StarsBarIcon, {
score: reviews.average, score: reviews.average,
@ -767,7 +763,7 @@ export default class SpecialVisualizations {
{ {
nameKey: nameKey, nameKey: nameKey,
fallbackName, fallbackName,
} },
) )
return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer }) return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer })
}, },
@ -799,7 +795,7 @@ export default class SpecialVisualizations {
{ {
nameKey: nameKey, nameKey: nameKey,
fallbackName, fallbackName,
} },
) )
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer }) return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
}, },
@ -857,7 +853,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): SvelteUIElement { ): SvelteUIElement {
const keyToUse = args[0] const keyToUse = args[0]
const prefix = args[1] const prefix = args[1]
@ -894,10 +890,10 @@ export default class SpecialVisualizations {
return undefined return undefined
} }
const allUnits: Unit[] = [].concat( const allUnits: Unit[] = [].concat(
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []) ...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []),
) )
const unit = allUnits.filter((unit) => const unit = allUnits.filter((unit) =>
unit.isApplicableToKey(key) unit.isApplicableToKey(key),
)[0] )[0]
if (unit === undefined) { if (unit === undefined) {
return value return value
@ -905,7 +901,7 @@ export default class SpecialVisualizations {
const getCountry = () => tagSource.data._country const getCountry = () => tagSource.data._country
const [v, denom] = unit.findDenomination(value, getCountry) const [v, denom] = unit.findDenomination(value, getCountry)
return unit.asHumanLongValue(v, getCountry) return unit.asHumanLongValue(v, getCountry)
}) }),
) )
}, },
}, },
@ -922,7 +918,7 @@ export default class SpecialVisualizations {
new Combine([ new Combine([
t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"),
t.downloadGeoJsonHelper.SetClass("subtle"), t.downloadGeoJsonHelper.SetClass("subtle"),
]).SetClass("flex flex-col") ]).SetClass("flex flex-col"),
) )
.onClick(() => { .onClick(() => {
console.log("Exporting as Geojson") console.log("Exporting as Geojson")
@ -935,7 +931,7 @@ export default class SpecialVisualizations {
title + "_mapcomplete_export.geojson", title + "_mapcomplete_export.geojson",
{ {
mimetype: "application/vnd.geo+json", mimetype: "application/vnd.geo+json",
} },
) )
}) })
.SetClass("w-full") .SetClass("w-full")
@ -971,7 +967,7 @@ export default class SpecialVisualizations {
constr: (state) => { constr: (state) => {
return new SubtleButton( return new SubtleButton(
Svg.delete_icon_svg().SetStyle("height: 1.5rem"), Svg.delete_icon_svg().SetStyle("height: 1.5rem"),
Translations.t.general.removeLocationHistory Translations.t.general.removeLocationHistory,
).onClick(() => { ).onClick(() => {
state.historicalUserLocations.features.setData([]) state.historicalUserLocations.features.setData([])
state.selectedElement.setData(undefined) state.selectedElement.setData(undefined)
@ -1009,10 +1005,10 @@ export default class SpecialVisualizations {
.filter((c) => c.text !== "") .filter((c) => c.text !== "")
.map( .map(
(c, i) => (c, i) =>
new NoteCommentElement(c, state, i, comments.length) new NoteCommentElement(c, state, i, comments.length),
) ),
).SetClass("flex flex-col") ).SetClass("flex flex-col")
}) }),
), ),
}, },
{ {
@ -1053,9 +1049,9 @@ export default class SpecialVisualizations {
return undefined return undefined
} }
return new SubstitutedTranslation(title, tagsSource, state).SetClass( return new SubstitutedTranslation(title, tagsSource, state).SetClass(
"px-1" "px-1",
) )
}) }),
), ),
}, },
{ {
@ -1071,8 +1067,8 @@ export default class SpecialVisualizations {
let challenge = Stores.FromPromise( let challenge = Stores.FromPromise(
Utils.downloadJsonCached( Utils.downloadJsonCached(
`${Maproulette.defaultEndpoint}/challenge/${parentId}`, `${Maproulette.defaultEndpoint}/challenge/${parentId}`,
24 * 60 * 60 * 1000 24 * 60 * 60 * 1000,
) ),
) )
return new VariableUiElement( return new VariableUiElement(
@ -1097,7 +1093,7 @@ export default class SpecialVisualizations {
} else { } else {
return [title, new List(listItems)] return [title, new List(listItems)]
} }
}) }),
) )
}, },
docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.", docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.",
@ -1111,15 +1107,15 @@ export default class SpecialVisualizations {
"\n" + "\n" +
"```json\n" + "```json\n" +
"{\n" + "{\n" +
' "id": "mark_duplicate",\n' + " \"id\": \"mark_duplicate\",\n" +
' "render": {\n' + " \"render\": {\n" +
' "special": {\n' + " \"special\": {\n" +
' "type": "maproulette_set_status",\n' + " \"type\": \"maproulette_set_status\",\n" +
' "message": {\n' + " \"message\": {\n" +
' "en": "Mark as not found or false positive"\n' + " \"en\": \"Mark as not found or false positive\"\n" +
" },\n" + " },\n" +
' "status": "2",\n' + " \"status\": \"2\",\n" +
' "image": "close"\n' + " \"image\": \"close\"\n" +
" }\n" + " }\n" +
" }\n" + " }\n" +
"}\n" + "}\n" +
@ -1185,8 +1181,8 @@ export default class SpecialVisualizations {
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox) const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
return new StatisticsPanel(fsBboxed) return new StatisticsPanel(fsBboxed)
}, },
[state.mapProperties.bounds] [state.mapProperties.bounds],
) ),
) )
}, },
}, },
@ -1252,7 +1248,7 @@ export default class SpecialVisualizations {
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
args: string[] args: string[],
): BaseUIElement { ): BaseUIElement {
let [text, href, classnames, download, ariaLabel] = args let [text, href, classnames, download, ariaLabel] = args
if (download === "") { if (download === "") {
@ -1269,15 +1265,14 @@ export default class SpecialVisualizations {
download: Utils.SubstituteKeys(download, tags), download: Utils.SubstituteKeys(download, tags),
ariaLabel: Utils.SubstituteKeys(ariaLabel, tags), ariaLabel: Utils.SubstituteKeys(ariaLabel, tags),
newTab, newTab,
}) }),
) ),
) )
}, },
}, },
{ {
funcName: "multi", funcName: "multi",
docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering", docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering",
example: example:
"```json\n" + "```json\n" +
JSON.stringify( JSON.stringify(
@ -1293,7 +1288,7 @@ export default class SpecialVisualizations {
}, },
}, },
null, null,
" " " ",
) + ) +
"\n```", "\n```",
args: [ args: [
@ -1313,18 +1308,28 @@ export default class SpecialVisualizations {
const translation = new Translation({ "*": tr }) const translation = new Translation({ "*": tr })
return new VariableUiElement( return new VariableUiElement(
featureTags.map((tags) => { featureTags.map((tags) => {
const properties: object[] = JSON.parse(tags[key]) try {
const elements = [] const data = tags[key]
for (const property of properties) { const properties: object[] = typeof data === "string" ? JSON.parse(tags[key]) : data
const subsTr = new SubstitutedTranslation( const elements = []
translation, for (const property of properties) {
new UIEventSource<any>(property), const subsTr = new SubstitutedTranslation(
state translation,
) new UIEventSource<any>(property),
elements.push(subsTr) state,
)
elements.push(subsTr)
}
return new List(elements)
} catch (e) {
console.log("Something went wrong while generating the elements for a multi", {
e,
tags,
key,
loaded: tags[key],
})
} }
return new List(elements) }),
})
) )
}, },
}, },
@ -1344,7 +1349,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new VariableUiElement( return new VariableUiElement(
tagSource.map((tags) => { tagSource.map((tags) => {
@ -1356,7 +1361,7 @@ export default class SpecialVisualizations {
console.error("Cannot create a translation for", v, "due to", e) console.error("Cannot create a translation for", v, "due to", e)
return JSON.stringify(v) return JSON.stringify(v)
} }
}) }),
) )
}, },
}, },
@ -1376,7 +1381,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const key = argument[0] const key = argument[0]
const validator = new FediverseValidator() const validator = new FediverseValidator()
@ -1386,14 +1391,14 @@ export default class SpecialVisualizations {
.map((fediAccount) => { .map((fediAccount) => {
fediAccount = validator.reformat(fediAccount) fediAccount = validator.reformat(fediAccount)
const [_, username, host] = fediAccount.match( const [_, username, host] = fediAccount.match(
FediverseValidator.usernameAtServer FediverseValidator.usernameAtServer,
) )
return new SvelteUIElement(Link, { return new SvelteUIElement(Link, {
text: fediAccount, text: fediAccount,
url: "https://" + host + "/@" + username, url: "https://" + host + "/@" + username,
newTab: true, newTab: true,
}) })
}) }),
) )
}, },
}, },
@ -1413,7 +1418,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new FixedUiElement("{" + args[0] + "}") return new FixedUiElement("{" + args[0] + "}")
}, },
@ -1434,7 +1439,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const key = argument[0] ?? "value" const key = argument[0] ?? "value"
return new VariableUiElement( return new VariableUiElement(
@ -1452,12 +1457,12 @@ export default class SpecialVisualizations {
} catch (e) { } catch (e) {
return new FixedUiElement( return new FixedUiElement(
"Could not parse this tag: " + "Could not parse this tag: " +
JSON.stringify(value) + JSON.stringify(value) +
" due to " + " due to " +
e e,
).SetClass("alert") ).SetClass("alert")
} }
}) }),
) )
}, },
}, },
@ -1478,7 +1483,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const giggityUrl = argument[0] const giggityUrl = argument[0]
return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl }) return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl })
@ -1494,12 +1499,12 @@ export default class SpecialVisualizations {
_: UIEventSource<Record<string, string>>, _: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const tags = (<ThemeViewState>( const tags = (<ThemeViewState>(
state state
)).geolocation.currentUserLocation.features.map( )).geolocation.currentUserLocation.features.map(
(features) => features[0]?.properties (features) => features[0]?.properties,
) )
return new Combine([ return new Combine([
new SvelteUIElement(OrientationDebugPanel, {}), new SvelteUIElement(OrientationDebugPanel, {}),
@ -1521,7 +1526,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new SvelteUIElement(MarkAsFavourite, { return new SvelteUIElement(MarkAsFavourite, {
tags: tagSource, tags: tagSource,
@ -1541,7 +1546,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new SvelteUIElement(MarkAsFavouriteMini, { return new SvelteUIElement(MarkAsFavouriteMini, {
tags: tagSource, tags: tagSource,
@ -1561,7 +1566,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new SvelteUIElement(DirectionIndicator, { state, feature }) return new SvelteUIElement(DirectionIndicator, { state, feature })
}, },
@ -1576,7 +1581,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
return new VariableUiElement( return new VariableUiElement(
tagSource tagSource
@ -1598,9 +1603,9 @@ export default class SpecialVisualizations {
`${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` + `${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` +
`#${id}` `#${id}`
return new Img(new Qr(url).toImageElement(75)).SetStyle( return new Img(new Qr(url).toImageElement(75)).SetStyle(
"width: 75px" "width: 75px",
) )
}) }),
) )
}, },
}, },
@ -1620,7 +1625,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig layer: LayerConfig,
): BaseUIElement { ): BaseUIElement {
const key = args[0] === "" ? "_direction:centerpoint" : args[0] const key = args[0] === "" ? "_direction:centerpoint" : args[0]
return new VariableUiElement( return new VariableUiElement(
@ -1631,41 +1636,55 @@ export default class SpecialVisualizations {
}) })
.mapD((value) => { .mapD((value) => {
const dir = GeoOperations.bearingToHuman( const dir = GeoOperations.bearingToHuman(
GeoOperations.parseBearing(value) GeoOperations.parseBearing(value),
) )
console.log("Human dir", dir) console.log("Human dir", dir)
return Translations.t.general.visualFeedback.directionsAbsolute[dir] return Translations.t.general.visualFeedback.directionsAbsolute[dir]
}) }),
) )
}, },
}, },
{ {
funcName: "compare_data", funcName: "compare_data",
needsUrls: (args) => args[1].split(";"), needsUrls: (args) => args[1].split(";"),
args:[ args: [
{ {
name: "url", name: "url",
required: true, required: true,
doc: "The attribute containing the url where to fetch more data" doc: "The attribute containing the url where to fetch more data",
}, },
{ {
name:"host", name: "host",
required:true, required: true,
doc: "The domain name(s) where data might be fetched from - this is needed to set the CSP. A domain must include 'https', e.g. 'https://example.com'. For multiple domains, separate them with ';'. If you don't know the possible domains, use '*'. " doc: "The domain name(s) where data might be fetched from - this is needed to set the CSP. A domain must include 'https', e.g. 'https://example.com'. For multiple domains, separate them with ';'. If you don't know the possible domains, use '*'. ",
}, },
{ {
name: "postprocessing", name: "postprocessing",
required: false, required: false,
doc: "Apply some postprocessing. Currently, only 'velopark' is allowed as value" doc: "Apply some postprocessing. Currently, only 'velopark' is allowed as value",
},
{
name:"readonly",
required: false,
doc: "If 'yes', will not show 'apply'-buttons"
} }
], ],
docs: "Gives an interactive element which shows a tag comparison between the OSM-object and the upstream object. This allows to copy some or all tags into OSM", docs: "Gives an interactive element which shows a tag comparison between the OSM-object and the upstream object. This allows to copy some or all tags into OSM",
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement { constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
const url = args[0] const url = args[0]
const postprocessVelopark = args[2] === "velopark" const postprocessVelopark = args[2] === "velopark"
return new SvelteUIElement(ComparisonTool, {url, postprocessVelopark, state, tags: tagSource, layer, feature}) const readonly = args[3] === "yes"
} return new SvelteUIElement(ComparisonTool, {
} url,
postprocessVelopark,
state,
tags: tagSource,
layer,
feature,
readonly
})
},
},
] ]
specialVisualizations.push(new AutoApplyButton(specialVisualizations)) specialVisualizations.push(new AutoApplyButton(specialVisualizations))
@ -1677,7 +1696,7 @@ export default class SpecialVisualizations {
throw ( throw (
"Invalid special visualisation found: funcName is undefined for " + "Invalid special visualisation found: funcName is undefined for " +
invalid.map((sp) => sp.i).join(", ") + invalid.map((sp) => sp.i).join(", ") +
'. Did you perhaps type \n funcName: "funcname" // type declaration uses COLON\ninstead of:\n funcName = "funcName" // value definition uses EQUAL' ". Did you perhaps type \n funcName: \"funcname\" // type declaration uses COLON\ninstead of:\n funcName = \"funcName\" // value definition uses EQUAL"
) )
} }