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">
import type { SpecialVisualizationState } from "../SpecialVisualization"
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Tiles } from "../../Models/TileRange"
import { Map as MlMap } from "maplibre-gl"
import { BBox } from "../../Logic/BBox"
import type { MapProperties } from "../../Models/MapProperties"
import ShowDataLayer from "../Map/ShowDataLayer"
import type {
FeatureSource,
FeatureSourceForLayer,
} from "../../Logic/FeatureSource/FeatureSource"
import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource"
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../Utils"
import { createEventDispatcher } from "svelte"
import Move_arrows from "../../assets/svg/Move_arrows.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Tiles } from "../../Models/TileRange"
import { Map as MlMap } from "maplibre-gl"
import { BBox } from "../../Logic/BBox"
import type { MapProperties } from "../../Models/MapProperties"
import ShowDataLayer from "../Map/ShowDataLayer"
import type { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource"
import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource"
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
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:
* - Show more layers
* - Snap to layers
*
* This one is mostly used to insert new points, including when importing
*/
export let state: SpecialVisualizationState
/**
* The start coordinate
*/
export let coordinate: { lon: number; lat: number }
/**
* An advanced location input, which has support to:
* - Show more layers
* - Snap to layers
*
* This one is mostly used to insert new points, including when importing
*/
export let state: SpecialVisualizationState
/**
* The start coordinate
*/
export let coordinate: { lon: number; lat: number }
/**
* The center of the map at all times
* If undefined at the beginning, 'coordinate' will be used
*/
export let value: UIEventSource<{ lon: number; lat: number }>
if (value.data === undefined) {
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,
})
/**
* The center of the map at all times
* If undefined at the beginning, 'coordinate' will be used
*/
export let value: UIEventSource<{ lon: number; lat: number }>
if (value.data === undefined) {
value.setData(coordinate)
}
}
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,
})
if (coordinate === undefined) {
coordinate = value.data
}
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,
}
)
export let snapToLayers: string[] | undefined
export let targetLayer: LayerConfig | LayerConfig[] | undefined
new ShowDataLayer(map, {
layer: targetLayer,
features: snappedLocation,
let targetLayers: LayerConfig[] | undefined
if (Array.isArray(targetLayers)) {
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>
<LocationInput
{map}
on:click={(data) => dispatch("click", data)}
mapProperties={initialMapProperties}
value={preciseLocation}
initialCoordinate={coordinate}
maxDistanceInMeters="50"
{map}
mapProperties={initialMapProperties}
maxDistanceInMeters={50}
on:click={(data) => dispatch("click", data)}
value={preciseLocation}
>
<slot name="image" slot="image">
<Move_arrows class="h-full max-h-24" />

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>

View file

@ -3,11 +3,7 @@ import { FixedUiElement } from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement"
import Title from "./Base/Title"
import Table from "./Base/Table"
import {
RenderingSpecification,
SpecialVisualization,
SpecialVisualizationState,
} from "./SpecialVisualization"
import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz"
import { MinimapViz } from "./Popup/MinimapViz"
import { ShareLinkViz } from "./Popup/ShareLinkViz"
@ -110,7 +106,7 @@ class NearbyImageVis implements SpecialVisualization {
tags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
const isOpen = args[0] === "open"
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
@ -175,7 +171,7 @@ class StealViz implements SpecialVisualization {
selectedElement: otherFeature,
state,
layer,
})
}),
)
}
if (elements.length === 1) {
@ -183,8 +179,8 @@ class StealViz implements SpecialVisualization {
}
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>>,
args: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
const labels = args[0]
?.split(";")
@ -277,17 +273,17 @@ export default class SpecialVisualizations {
* templ.args[0] = "{email}"
*/
public static constructSpecification(
template: string,
extraMappings: SpecialVisualization[] = []
template: string | { special: Record<string, string | Record<string, string>> & { type: string } },
extraMappings: SpecialVisualization[] = [],
): RenderingSpecification[] {
if (template === "") {
return []
}
if (template["type"] !== undefined) {
if (typeof template !== "string") {
console.trace(
"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"
}
@ -295,20 +291,20 @@ export default class SpecialVisualizations {
for (const knownSpecial of allKnownSpecials) {
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
const matched = template.match(
new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s")
new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s"),
)
if (matched != null) {
// We found a special component that should be brought to live
const partBefore = SpecialVisualizations.constructSpecification(
matched[1],
extraMappings
extraMappings,
)
const argument =
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 partAfter = SpecialVisualizations.constructSpecification(
matched[4],
extraMappings
extraMappings,
)
const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "")
if (argument.length > 0) {
@ -347,31 +343,31 @@ export default class SpecialVisualizations {
viz.docs,
viz.args.length > 0
? new Table(
["name", "default", "description"],
viz.args.map((arg) => {
let defaultArg = arg.defaultValue ?? "_undefined_"
if (defaultArg == "") {
defaultArg = "_empty string_"
}
return [arg.name, defaultArg, arg.doc]
})
)
["name", "default", "description"],
viz.args.map((arg) => {
let defaultArg = arg.defaultValue ?? "_undefined_"
if (defaultArg == "") {
defaultArg = "_empty string_"
}
return [arg.name, defaultArg, arg.doc]
}),
)
: undefined,
new Title("Example usage of " + viz.funcName, 4),
new FixedUiElement(
viz.example ??
"`{" +
viz.funcName +
"(" +
viz.args.map((arg) => arg.defaultValue).join(",") +
")}`"
"`{" +
viz.funcName +
"(" +
viz.args.map((arg) => arg.defaultValue).join(",") +
")}`",
).SetClass("literal-code"),
])
}
public static HelpMessage() {
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
SpecialVisualizations.DocumentationFor(viz)
SpecialVisualizations.DocumentationFor(viz),
)
return new Combine([
@ -405,10 +401,10 @@ export default class SpecialVisualizations {
},
},
null,
" "
)
" ",
),
).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"),
...helpTexts,
]).SetClass("flex flex-col")
@ -417,20 +413,20 @@ export default class SpecialVisualizations {
// noinspection JSUnusedGlobalSymbols
public static renderExampleOfSpecial(
state: SpecialVisualizationState,
s: SpecialVisualization
s: SpecialVisualization,
): BaseUIElement {
const examples =
s.structuredExamples === undefined
? []
: s.structuredExamples().map((e) => {
return s.constr(
state,
new UIEventSource<Record<string, string>>(e.feature.properties),
e.args,
e.feature,
undefined
)
})
return s.constr(
state,
new UIEventSource<Record<string, string>>(e.feature.properties),
e.args,
e.feature,
undefined,
)
})
return new Combine([new Title(s.funcName), s.docs, ...examples])
}
@ -470,7 +466,7 @@ export default class SpecialVisualizations {
assignTo: state.userRelatedState.language,
availableLanguages: state.layout.language,
preferredLanguages: state.osmConnection.userDetails.map(
(ud) => ud.languages
(ud) => ud.languages,
),
})
},
@ -495,7 +491,7 @@ export default class SpecialVisualizations {
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>
tagSource: UIEventSource<Record<string, string>>,
): BaseUIElement {
return new VariableUiElement(
tagSource
@ -505,7 +501,7 @@ export default class SpecialVisualizations {
return new SplitRoadWizard(<WayId>id, state)
}
return undefined
})
}),
)
},
},
@ -519,7 +515,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
if (feature.geometry.type !== "Point") {
return undefined
@ -542,7 +538,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
if (!layer.deletion) {
return undefined
@ -570,7 +566,7 @@ export default class SpecialVisualizations {
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature
feature: Feature,
): BaseUIElement {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(CreateNewNote, {
@ -634,7 +630,7 @@ export default class SpecialVisualizations {
.map((tags) => tags[args[0]])
.map((wikidata) => {
wikidata = Utils.NoEmpty(
wikidata?.split(";")?.map((wd) => wd.trim()) ?? []
wikidata?.split(";")?.map((wd) => wd.trim()) ?? [],
)[0]
const entry = Wikidata.LoadWikidataEntry(wikidata)
return new VariableUiElement(
@ -644,9 +640,9 @@ export default class SpecialVisualizations {
}
const response = <WikidataResponse>e["success"]
return Translation.fromMap(response.labels)
})
}),
)
})
}),
),
},
new MapillaryLinkVis(),
@ -678,7 +674,7 @@ export default class SpecialVisualizations {
AllImageProviders.LoadImagesFor(tags, imagePrefixes),
tags,
state,
feature
feature,
)
},
},
@ -734,7 +730,7 @@ export default class SpecialVisualizations {
{
nameKey: nameKey,
fallbackName,
}
},
)
return new SvelteUIElement(StarsBarIcon, {
score: reviews.average,
@ -767,7 +763,7 @@ export default class SpecialVisualizations {
{
nameKey: nameKey,
fallbackName,
}
},
)
return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer })
},
@ -799,7 +795,7 @@ export default class SpecialVisualizations {
{
nameKey: nameKey,
fallbackName,
}
},
)
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
},
@ -857,7 +853,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): SvelteUIElement {
const keyToUse = args[0]
const prefix = args[1]
@ -894,10 +890,10 @@ export default class SpecialVisualizations {
return undefined
}
const allUnits: Unit[] = [].concat(
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? [])
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []),
)
const unit = allUnits.filter((unit) =>
unit.isApplicableToKey(key)
unit.isApplicableToKey(key),
)[0]
if (unit === undefined) {
return value
@ -905,7 +901,7 @@ export default class SpecialVisualizations {
const getCountry = () => tagSource.data._country
const [v, denom] = unit.findDenomination(value, getCountry)
return unit.asHumanLongValue(v, getCountry)
})
}),
)
},
},
@ -922,7 +918,7 @@ export default class SpecialVisualizations {
new Combine([
t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"),
t.downloadGeoJsonHelper.SetClass("subtle"),
]).SetClass("flex flex-col")
]).SetClass("flex flex-col"),
)
.onClick(() => {
console.log("Exporting as Geojson")
@ -935,7 +931,7 @@ export default class SpecialVisualizations {
title + "_mapcomplete_export.geojson",
{
mimetype: "application/vnd.geo+json",
}
},
)
})
.SetClass("w-full")
@ -971,7 +967,7 @@ export default class SpecialVisualizations {
constr: (state) => {
return new SubtleButton(
Svg.delete_icon_svg().SetStyle("height: 1.5rem"),
Translations.t.general.removeLocationHistory
Translations.t.general.removeLocationHistory,
).onClick(() => {
state.historicalUserLocations.features.setData([])
state.selectedElement.setData(undefined)
@ -1009,10 +1005,10 @@ export default class SpecialVisualizations {
.filter((c) => c.text !== "")
.map(
(c, i) =>
new NoteCommentElement(c, state, i, comments.length)
)
new NoteCommentElement(c, state, i, comments.length),
),
).SetClass("flex flex-col")
})
}),
),
},
{
@ -1053,9 +1049,9 @@ export default class SpecialVisualizations {
return undefined
}
return new SubstitutedTranslation(title, tagsSource, state).SetClass(
"px-1"
"px-1",
)
})
}),
),
},
{
@ -1071,8 +1067,8 @@ export default class SpecialVisualizations {
let challenge = Stores.FromPromise(
Utils.downloadJsonCached(
`${Maproulette.defaultEndpoint}/challenge/${parentId}`,
24 * 60 * 60 * 1000
)
24 * 60 * 60 * 1000,
),
)
return new VariableUiElement(
@ -1097,7 +1093,7 @@ export default class SpecialVisualizations {
} else {
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.",
@ -1111,15 +1107,15 @@ export default class SpecialVisualizations {
"\n" +
"```json\n" +
"{\n" +
' "id": "mark_duplicate",\n' +
' "render": {\n' +
' "special": {\n' +
' "type": "maproulette_set_status",\n' +
' "message": {\n' +
' "en": "Mark as not found or false positive"\n' +
" \"id\": \"mark_duplicate\",\n" +
" \"render\": {\n" +
" \"special\": {\n" +
" \"type\": \"maproulette_set_status\",\n" +
" \"message\": {\n" +
" \"en\": \"Mark as not found or false positive\"\n" +
" },\n" +
' "status": "2",\n' +
' "image": "close"\n' +
" \"status\": \"2\",\n" +
" \"image\": \"close\"\n" +
" }\n" +
" }\n" +
"}\n" +
@ -1185,8 +1181,8 @@ export default class SpecialVisualizations {
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
return new StatisticsPanel(fsBboxed)
},
[state.mapProperties.bounds]
)
[state.mapProperties.bounds],
),
)
},
},
@ -1252,7 +1248,7 @@ export default class SpecialVisualizations {
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[]
args: string[],
): BaseUIElement {
let [text, href, classnames, download, ariaLabel] = args
if (download === "") {
@ -1269,15 +1265,14 @@ export default class SpecialVisualizations {
download: Utils.SubstituteKeys(download, tags),
ariaLabel: Utils.SubstituteKeys(ariaLabel, tags),
newTab,
})
)
}),
),
)
},
},
{
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",
example:
"```json\n" +
JSON.stringify(
@ -1293,7 +1288,7 @@ export default class SpecialVisualizations {
},
},
null,
" "
" ",
) +
"\n```",
args: [
@ -1313,18 +1308,28 @@ export default class SpecialVisualizations {
const translation = new Translation({ "*": tr })
return new VariableUiElement(
featureTags.map((tags) => {
const properties: object[] = JSON.parse(tags[key])
const elements = []
for (const property of properties) {
const subsTr = new SubstitutedTranslation(
translation,
new UIEventSource<any>(property),
state
)
elements.push(subsTr)
try {
const data = tags[key]
const properties: object[] = typeof data === "string" ? JSON.parse(tags[key]) : data
const elements = []
for (const property of properties) {
const subsTr = new SubstitutedTranslation(
translation,
new UIEventSource<any>(property),
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>>,
argument: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
return new VariableUiElement(
tagSource.map((tags) => {
@ -1356,7 +1361,7 @@ export default class SpecialVisualizations {
console.error("Cannot create a translation for", v, "due to", e)
return JSON.stringify(v)
}
})
}),
)
},
},
@ -1376,7 +1381,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
const key = argument[0]
const validator = new FediverseValidator()
@ -1386,14 +1391,14 @@ export default class SpecialVisualizations {
.map((fediAccount) => {
fediAccount = validator.reformat(fediAccount)
const [_, username, host] = fediAccount.match(
FediverseValidator.usernameAtServer
FediverseValidator.usernameAtServer,
)
return new SvelteUIElement(Link, {
text: fediAccount,
url: "https://" + host + "/@" + username,
newTab: true,
})
})
}),
)
},
},
@ -1413,7 +1418,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
return new FixedUiElement("{" + args[0] + "}")
},
@ -1434,7 +1439,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
const key = argument[0] ?? "value"
return new VariableUiElement(
@ -1452,12 +1457,12 @@ export default class SpecialVisualizations {
} catch (e) {
return new FixedUiElement(
"Could not parse this tag: " +
JSON.stringify(value) +
" due to " +
e
JSON.stringify(value) +
" due to " +
e,
).SetClass("alert")
}
})
}),
)
},
},
@ -1478,7 +1483,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
const giggityUrl = argument[0]
return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl })
@ -1494,12 +1499,12 @@ export default class SpecialVisualizations {
_: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
const tags = (<ThemeViewState>(
state
)).geolocation.currentUserLocation.features.map(
(features) => features[0]?.properties
(features) => features[0]?.properties,
)
return new Combine([
new SvelteUIElement(OrientationDebugPanel, {}),
@ -1521,7 +1526,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
return new SvelteUIElement(MarkAsFavourite, {
tags: tagSource,
@ -1541,7 +1546,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
return new SvelteUIElement(MarkAsFavouriteMini, {
tags: tagSource,
@ -1561,7 +1566,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
return new SvelteUIElement(DirectionIndicator, { state, feature })
},
@ -1576,7 +1581,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
return new VariableUiElement(
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` +
`#${id}`
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>>,
args: string[],
feature: Feature,
layer: LayerConfig
layer: LayerConfig,
): BaseUIElement {
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
return new VariableUiElement(
@ -1631,41 +1636,55 @@ export default class SpecialVisualizations {
})
.mapD((value) => {
const dir = GeoOperations.bearingToHuman(
GeoOperations.parseBearing(value)
GeoOperations.parseBearing(value),
)
console.log("Human dir", dir)
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
})
}),
)
},
},
{
funcName: "compare_data",
needsUrls: (args) => args[1].split(";"),
args:[
args: [
{
name: "url",
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",
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 '*'. "
name: "host",
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 '*'. ",
},
{
name: "postprocessing",
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",
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
const url = args[0]
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))
@ -1677,7 +1696,7 @@ export default class SpecialVisualizations {
throw (
"Invalid special visualisation found: funcName is undefined for " +
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"
)
}