MapComplete/src/UI/SpecialVisualizations.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1320 lines
60 KiB
TypeScript
Raw Normal View History

2020-10-14 12:15:09 +02:00
import Combine from "./Base/Combine"
import { FixedUiElement } from "./Base/FixedUiElement"
2021-06-11 22:51:45 +02:00
import BaseUIElement from "./BaseUIElement"
import { default as FeatureTitle } from "./Popup/Title.svelte"
2025-01-22 01:06:58 +01:00
import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz"
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
import { MultiApplyViz } from "./Popup/MultiApplyViz"
import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"
2022-11-02 13:47:34 +01:00
import TagApplyButton from "./Popup/TagApplyButton"
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
2024-01-31 12:09:15 +01:00
import { ImmutableStore, Store, Stores, UIEventSource } from "../Logic/UIEventSource"
2023-03-24 19:21:15 +01:00
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
import { VariableUiElement } from "./Base/VariableUIElement"
import { Utils } from "../Utils"
import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"
import { Translation } from "./i18n/Translation"
2022-11-02 13:47:34 +01:00
import Translations from "./i18n/Translations"
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
import { SubtleButton } from "./Base/SubtleButton"
2022-11-02 13:47:34 +01:00
import StatisticsPanel from "./BigComponents/StatisticsPanel"
import AutoApplyButton from "./Popup/AutoApplyButton"
import { LanguageElement } from "./Popup/LanguageElement/LanguageElement"
2023-02-15 18:24:08 +01:00
import SvelteUIElement from "./Base/SvelteUIElement"
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import { Feature, GeoJsonProperties, LineString } from "geojson"
import { GeoOperations } from "../Logic/GeoOperations"
2023-04-13 22:44:35 +02:00
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import ExportFeatureButton from "./Popup/ExportFeatureButton.svelte"
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"
import { PointImportButtonViz } from "./Popup/ImportButtons/PointImportButtonViz"
import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz"
import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import SendEmail from "./Popup/SendEmail.svelte"
2023-09-27 22:21:35 +02:00
import Constants from "../Models/Constants"
import Wikipedia from "../Logic/Web/Wikipedia"
2023-10-22 01:30:05 +02:00
import { TagUtils } from "../Logic/Tags/TagUtils"
import OpenJosm from "./Base/OpenJosm.svelte"
2023-12-04 03:32:25 +01:00
import NextChangeViz from "./OpeningHours/NextChangeViz.svelte"
import { Unit } from "../Models/Unit"
import DirectionIndicator from "./Base/DirectionIndicator.svelte"
2024-01-13 05:24:56 +01:00
import ComparisonTool from "./Comparison/ComparisonTool.svelte"
import SpecialTranslation from "./Popup/TagRendering/SpecialTranslation.svelte"
2024-01-25 13:41:33 +01:00
import SpecialVisualisationUtils from "./SpecialVisualisationUtils"
import Toggle from "./Input/Toggle"
import LinkedDataLoader from "../Logic/Web/LinkedDataLoader"
import DynLink from "./Base/DynLink.svelte"
import MarkdownUtils from "../Utils/MarkdownUtils"
2024-06-16 19:00:43 +02:00
import Trash from "@babeard/svelte-heroicons/mini/Trash"
import { And } from "../Logic/Tags/And"
import GroupedView from "./Popup/GroupedView.svelte"
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import NoteCommentElement from "./Popup/Notes/NoteCommentElement.svelte"
2024-10-29 23:46:52 +01:00
import FediverseLink from "./Popup/FediverseLink.svelte"
import { ImageVisualisations } from "./SpecialVisualisations/ImageVisualisations"
import { NoteVisualisations } from "./SpecialVisualisations/NoteVisualisations"
import { FavouriteVisualisations } from "./SpecialVisualisations/FavouriteVisualisations"
import { UISpecialVisualisations } from "./SpecialVisualisations/UISpecialVisualisations"
import { SettingsVisualisations } from "./SpecialVisualisations/SettingsVisualisations"
import { ReviewSpecialVisualisations } from "./SpecialVisualisations/ReviewSpecialVisualisations"
import { MapRouletteSpecialVisualisations } from "./SpecialVisualisations/MapRouletteSpecialVisualisations"
2023-04-13 22:44:35 +02:00
class StealViz implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
funcName = "steal"
docs = "Shows a tagRendering from a different object as if this was the object itself"
args = [
{
name: "featureId",
doc: "The key of the attribute which contains the id of the feature from which to use the tags",
required: true
2023-04-13 22:44:35 +02:00
},
{
name: "tagRenderingId",
doc: "The layer-id and tagRenderingId to render. Can be multiple value if ';'-separated (in which case every value must also contain the layerId, e.g. `layerId.tagRendering0; layerId.tagRendering1`). Note: this can cause layer injection",
required: true
}
2023-04-13 22:44:35 +02:00
]
2023-09-27 22:21:35 +02:00
needsUrls = []
svelteBased = true
2023-04-13 22:44:35 +02:00
constr(state: SpecialVisualizationState, featureTags, args) {
const [featureIdKey, layerAndtagRenderingIds] = args
const tagRenderings: [LayerConfig, TagRenderingConfig][] = []
for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) {
const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".")
const layer = state.theme.layers.find((l) => l.id === layerId)
2023-04-13 22:44:35 +02:00
const tagRendering = layer.tagRenderings.find((tr) => tr.id === tagRenderingId)
tagRenderings.push([layer, tagRendering])
}
if (tagRenderings.length === 0) {
throw "Could not create stolen tagrenddering: tagRenderings not found"
}
return new VariableUiElement(
featureTags.map(
(tags) => {
const featureId = tags[featureIdKey]
if (featureId === undefined) {
return undefined
}
const otherTags = state.featureProperties.getStore(featureId)
const otherFeature = state.indexedFeatures.featuresById.data.get(featureId)
const elements: BaseUIElement[] = []
for (const [layer, tagRendering] of tagRenderings) {
elements.push(
new SvelteUIElement(TagRenderingEditable, {
config: tagRendering,
tags: otherTags,
selectedElement: otherFeature,
state,
layer
2025-01-18 00:30:06 +01:00
})
)
}
if (elements.length === 1) {
return elements[0]
}
return new Combine(elements).SetClass("flex flex-col")
},
2025-01-18 00:30:06 +01:00
[state.indexedFeatures.featuresById]
)
2023-04-13 22:44:35 +02:00
)
}
getLayerDependencies(args): string[] {
2024-04-12 15:33:15 +02:00
const [, tagRenderingId] = args
2023-04-13 22:44:35 +02:00
if (tagRenderingId.indexOf(".") < 0) {
throw "Error: argument 'layerId.tagRenderingId' of special visualisation 'steal' should contain a dot"
}
2024-04-12 15:33:15 +02:00
const [layerId] = tagRenderingId.split(".")
2023-04-13 22:44:35 +02:00
return [layerId]
}
}
2023-10-06 03:34:26 +02:00
export default class SpecialVisualizations {
2022-11-02 13:47:34 +01:00
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
2024-08-14 13:53:56 +02:00
public static specialVisualisationsDict: Map<string, SpecialVisualization> = new Map<
string,
SpecialVisualization
>()
static {
for (const specialVisualization of SpecialVisualizations.specialVisualizations) {
2024-08-14 13:53:56 +02:00
SpecialVisualizations.specialVisualisationsDict.set(
specialVisualization.funcName,
2025-01-18 00:30:06 +01:00
specialVisualization
2024-08-14 13:53:56 +02:00
)
}
}
public static DocumentationFor(viz: string | SpecialVisualization): string {
if (typeof viz === "string") {
viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
}
if (viz === undefined) {
return ""
}
2024-06-16 16:06:26 +02:00
const example =
viz.example ??
"`{" + viz.funcName + "(" + viz.args.map((arg) => arg.defaultValue).join(",") + ")}`"
return [
"### " + viz.funcName,
viz.docs,
viz.args.length > 0
? MarkdownUtils.table(
["name", "default", "description"],
viz.args.map((arg) => {
let defaultArg = arg.defaultValue ?? "_undefined_"
if (defaultArg == "") {
defaultArg = "_empty string_"
}
return [arg.name, defaultArg, arg.doc]
})
)
: undefined,
"#### Example usage of " + viz.funcName,
"<code>" + example + "</code>"
].join("\n\n")
}
2024-01-25 13:41:33 +01:00
public static constructSpecification(
template: string,
2025-01-18 00:30:06 +01:00
extraMappings: SpecialVisualization[] = []
2024-01-25 13:41:33 +01:00
): RenderingSpecification[] {
2024-08-14 13:53:56 +02:00
return SpecialVisualisationUtils.constructSpecification(
template,
SpecialVisualizations.specialVisualisationsDict,
2025-01-18 00:30:06 +01:00
extraMappings
2024-08-14 13:53:56 +02:00
)
2024-01-25 13:41:33 +01:00
}
public static HelpMessage(): string {
const vis = [...SpecialVisualizations.specialVisualizations]
vis.sort((a, b) => {
return a.funcName < b.funcName ? -1 : 1
})
vis.sort((a, b) => {
if (a.group === b.group) {
return 0
}
return (a.group ?? "xxx") < (b.group ?? "xxx") ? -1 : 1
})
const groupExplanations: Record<string, string> = {
"default": "These special visualisations are (mostly) interactive components that most elements get by default. You'll normally won't need them in custom layers. There are also a few miscellaneous elements supporting the map UI.",
"favourites": "Elements relating to marking an object as favourite (giving it a heart). Default element",
"settings": "Elements part of the usersettings-ui",
"images": "Elements related to adding or manipulating images. Normally also added by default, but in some cases a tweaked version is needed",
"notes": "Elements relating to OpenStreetMap-notes, e.g. the component to close and/or add a comment",
"reviews": "Elements relating to seeing and adding ratings and reviews with Mangrove.reviews",
"maproulette": "Elements to close a maproulette task"
}
const helpTexts: string[] = []
let lastGroup: string = null
for (const viz of vis) {
if (viz.group !== lastGroup) {
lastGroup = viz.group
if (viz.group === undefined) {
helpTexts.push("## Unclassified elements\n\nVarious elements")
} else {
helpTexts.push("## " + viz.group)
if (!groupExplanations[viz.group]) {
throw "\n\n >>>> ERROR <<<< Unknown visualisation group type: " + viz.group + "\n\n\n"
}
helpTexts.push(groupExplanations[viz.group])
}
}
helpTexts.push(SpecialVisualizations.DocumentationFor(viz))
}
const example = JSON.stringify(
{
render: {
special: {
type: "some_special_visualisation",
argname: "some_arg",
message: {
en: "some other really long message",
nl: "een boodschap in een andere taal"
},
other_arg_name: "more args"
},
before: {
en: "Some text to prefix before the special element (e.g. a title)",
nl: "Een tekst om voor het element te zetten (bv. een titel)"
},
after: {
en: "Some text to put after the element, e.g. a footer"
}
}
},
null,
" "
)
const firstPart = [
"# Special tag renderings",
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
2024-06-17 04:27:08 +02:00
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssClasses}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
"#### Using expanded syntax",
`Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`,
"```\n" + example + "\n```\n",
"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)"
].join("\n\n")
return firstPart + "\n\n" + helpTexts.join("\n\n")
}
2022-11-02 13:47:34 +01:00
private static initList(): SpecialVisualization[] {
2022-01-08 04:22:50 +01:00
const specialVisualizations: SpecialVisualization[] = [
...ImageVisualisations.initList(),
...NoteVisualisations.initList(),
...FavouriteVisualisations.initList(),
...UISpecialVisualisations.initList(),
...SettingsVisualisations.initList(),
...ReviewSpecialVisualisations.initList(),
...MapRouletteSpecialVisualisations.initList(),
new HistogramViz(),
new StealViz(),
{
funcName: "export_as_gpx",
docs: "Exports the selected feature as GPX-file",
args: [],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
) {
if (feature.geometry.type !== "LineString") {
return undefined
}
const t = Translations.t.general.download
return new SvelteUIElement(ExportFeatureButton, {
2024-11-07 11:19:15 +01:00
tags,
feature,
layer,
mimetype: "{gpx=application/gpx+xml}",
extension: "gpx",
2024-11-07 11:19:15 +01:00
construct: (feature: Feature<LineString>, title: string) =>
GeoOperations.toGpx(feature, title),
helpertext: t.downloadGpxHelper,
maintext: t.downloadFeatureAsGpx
})
}
},
new UploadToOsmViz(),
new MultiApplyViz(),
new PlantNetDetectionViz(),
2023-03-28 05:13:48 +02:00
new TagApplyButton(),
2023-06-01 02:52:21 +02:00
new PointImportButtonViz(),
2023-05-30 02:52:22 +02:00
new WayImportButtonViz(),
2023-06-01 02:52:21 +02:00
new ConflateImportButtonViz(),
2023-03-28 05:13:48 +02:00
{
funcName: "wikipedia",
docs: "A box showing the corresponding wikipedia article(s) - based on the **wikidata** tag.",
args: [
{
name: "keyToShowWikipediaFor",
doc: "Use the wikidata entry from this key to show the wikipedia article for. Multiple keys can be given (separated by ';'), in which case the first matching value is used",
defaultValue: "wikidata;wikipedia"
}
],
2023-09-27 22:21:35 +02:00
needsUrls: [...Wikidata.neededUrls, ...Wikipedia.neededUrls],
example:
"`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height",
constr: (_, tagsSource, args) => {
const keys = args[0].split(";").map((k) => k.trim())
const wikiIds: Store<string[]> = tagsSource.map((tags) => {
const key = keys.find((k) => tags[k] !== undefined && tags[k] !== "")
return tags[key]?.split(";")?.map((id) => id.trim()) ?? []
})
return new SvelteUIElement(WikipediaPanel, {
wikiIds
})
}
2022-09-08 21:40:48 +02:00
},
{
funcName: "wikidata_label",
docs: "Shows the label of the corresponding wikidata-item",
args: [
{
name: "keyToShowWikidataFor",
doc: "Use the wikidata entry from this key to show the label",
defaultValue: "wikidata"
}
],
2023-09-27 22:21:35 +02:00
needsUrls: Wikidata.neededUrls,
example:
"`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself",
constr: (_, tagsSource, args) =>
new VariableUiElement(
tagsSource
.map((tags) => tags[args[0]])
.map((wikidata) => {
wikidata = Utils.NoEmpty(
2025-01-18 00:30:06 +01:00
wikidata?.split(";")?.map((wd) => wd.trim()) ?? []
)[0]
const entry = Wikidata.LoadWikidataEntry(wikidata)
return new VariableUiElement(
entry.map((e) => {
if (e === undefined || e["success"] === undefined) {
return wikidata
}
const response = <WikidataResponse>e["success"]
return Translation.fromMap(response.labels)
2025-01-18 00:30:06 +01:00
})
2022-09-08 21:40:48 +02:00
)
2025-01-18 00:30:06 +01:00
})
)
},
2023-03-28 05:13:48 +02:00
new MapillaryLinkVis(),
new LanguageElement(),
{
funcName: "all_tags",
docs: "Prints all key-value pairs of the object - used for debugging",
args: [],
constr: (
state,
tags: UIEventSource<Record<string, string>>,
_,
__,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
) => new SvelteUIElement(AllTagsPanel, { tags, layer })
2023-03-28 05:13:48 +02:00
},
2024-08-13 22:17:36 +02:00
{
2024-08-14 13:53:56 +02:00
funcName: "reviews",
group: "reviews",
2024-08-13 22:17:36 +02:00
example:
"`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used",
2024-08-14 13:53:56 +02:00
docs: "A pragmatic combination of `create_review` and `list_reviews`",
2024-08-13 22:17:36 +02:00
args: [
{
name: "subjectKey",
defaultValue: "name",
doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>"
2024-08-13 22:17:36 +02:00
},
{
name: "fallback",
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value"
2024-09-02 12:48:15 +02:00
},
{
name: "question",
doc: "The question to ask in the review form. Optional"
}
2024-08-13 22:17:36 +02:00
],
2024-08-14 13:53:56 +02:00
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
2024-08-14 13:53:56 +02:00
): BaseUIElement {
2024-08-13 22:17:36 +02:00
return new Combine([
2025-01-18 00:30:06 +01:00
SpecialVisualizations.specialVisualisationsDict
.get("create_review")
.constr(state, tagSource, args, feature, layer),
SpecialVisualizations.specialVisualisationsDict
.get("list_reviews")
.constr(state, tagSource, args, feature, layer)
2024-08-13 22:17:36 +02:00
])
}
2024-02-20 16:53:26 +01:00
},
{
funcName: "opening_hours_table",
docs: "Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.",
args: [
{
2021-06-20 03:09:55 +02:00
name: "key",
defaultValue: "opening_hours",
doc: "The tagkey from which the table is constructed."
},
{
name: "prefix",
defaultValue: "",
doc: "Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__"
},
{
name: "postfix",
defaultValue: "",
doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__"
}
],
needsUrls: [Constants.countryCoderEndpoint],
example:
"A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`",
constr: (state, tagSource: UIEventSource<any>, args) => {
const [key, prefix, postfix] = args
return new OpeningHoursVisualization(tagSource, key, prefix, postfix)
}
2022-09-08 21:40:48 +02:00
},
2023-12-04 03:32:25 +01:00
{
funcName: "opening_hours_state",
docs: "A small element, showing if the POI is currently open and when the next change is",
args: [
{
name: "key",
defaultValue: "opening_hours",
doc: "The tagkey from which the opening hours are read."
2023-12-04 03:32:25 +01:00
},
{
name: "prefix",
defaultValue: "",
doc: "Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__"
2023-12-04 03:32:25 +01:00
},
{
name: "postfix",
defaultValue: "",
doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__"
}
2023-12-04 03:32:25 +01:00
],
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
2025-01-18 00:30:06 +01:00
args: string[]
): SvelteUIElement {
2023-12-04 03:32:25 +01:00
const keyToUse = args[0]
const prefix = args[1]
const postfix = args[2]
return new SvelteUIElement(NextChangeViz, {
state,
keyToUse,
tags,
prefix,
postfix
2023-12-04 03:32:25 +01:00
})
}
2023-12-04 03:32:25 +01:00
},
{
funcName: "canonical",
2022-07-26 12:05:34 +02:00
docs: "Converts a short, canonical value into the long, translated text including the unit. This only works if a `unit` is defined for the corresponding value. The unit specification will be included in the text. ",
example:
"If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ...",
args: [
{
name: "key",
doc: "The key of the tag to give the canonical text for",
required: true
}
],
constr: (state, tagSource, args) => {
const key = args[0]
return new VariableUiElement(
tagSource
.map((tags) => tags[key])
.map((value) => {
if (value === undefined) {
return undefined
}
const allUnits: Unit[] = [].concat(
2025-01-18 00:30:06 +01:00
...(state?.theme?.layers?.map((lyr) => lyr.units) ?? [])
2022-09-08 21:40:48 +02:00
)
const unit = allUnits.filter((unit) =>
2025-01-18 00:30:06 +01:00
unit.isApplicableToKey(key)
)[0]
if (unit === undefined) {
return value
}
const getCountry = () => tagSource.data._country
2024-02-12 14:48:05 +01:00
return unit.asHumanLongValue(value, getCountry)
2025-01-18 00:30:06 +01:00
})
)
}
2022-09-08 21:40:48 +02:00
},
{
funcName: "export_as_geojson",
docs: "Exports the selected feature as GeoJson-file",
args: [],
constr: (state, tags, args, feature, layer) => {
const t = Translations.t.general.download
return new SvelteUIElement(ExportFeatureButton, {
2024-11-07 11:19:15 +01:00
tags,
feature,
layer,
mimetype: "application/vnd.geo+json",
extension: "geojson",
2024-11-07 11:19:15 +01:00
construct: (feature: Feature<LineString>) =>
JSON.stringify(feature, null, " "),
maintext: t.downloadFeatureAsGeojson,
helpertext: t.downloadGeoJsonHelper
})
}
2022-09-08 21:40:48 +02:00
},
{
funcName: "open_in_iD",
docs: "Opens the current view in the iD-editor",
args: [],
2022-01-08 04:22:50 +01:00
constr: (state, feature) => {
return new SvelteUIElement(OpenIdEditor, {
mapProperties: state.mapProperties,
objectId: feature.data.id
})
}
2022-09-08 21:40:48 +02:00
},
2022-06-08 12:53:04 +02:00
{
funcName: "open_in_josm",
docs: "Opens the current view in the JOSM-editor",
args: [],
needsUrls: ["http://127.0.0.1:8111/load_and_zoom"],
2023-09-27 22:21:35 +02:00
2023-03-28 05:13:48 +02:00
constr: (state) => {
return new SvelteUIElement(OpenJosm, { state })
}
2022-09-08 21:40:48 +02:00
},
{
funcName: "clear_location_history",
docs: "A button to remove the travelled track information from the device",
args: [],
constr: (state) => {
return new SubtleButton(
new SvelteUIElement(Trash),
2025-01-18 00:30:06 +01:00
Translations.t.general.removeLocationHistory
).onClick(() => {
state.historicalUserLocations.features.setData([])
2023-06-07 02:42:49 +02:00
state.selectedElement.setData(undefined)
})
}
2022-09-08 21:40:48 +02:00
},
2022-01-08 14:08:04 +01:00
{
2022-01-08 04:22:50 +01:00
funcName: "visualize_note_comments",
2022-01-12 02:31:51 +01:00
docs: "Visualises the comments for notes",
2022-09-08 21:40:48 +02:00
args: [
{
2022-01-08 04:22:50 +01:00
name: "commentsKey",
doc: "The property name of the comments, which should be stringified json",
defaultValue: "comments"
2022-09-08 21:40:48 +02:00
},
{
2022-01-12 02:31:51 +01:00
name: "start",
2022-01-26 21:40:38 +01:00
doc: "Drop the first 'start' comments",
defaultValue: "0"
}
2022-09-08 21:40:48 +02:00
],
2023-09-27 22:21:35 +02:00
needsUrls: [Constants.osmAuthConfig.url],
2022-01-08 14:08:04 +01:00
constr: (state, tags, args) =>
new VariableUiElement(
2022-09-08 21:40:48 +02:00
tags
.map((tags) => tags[args[0]])
2022-01-08 04:22:50 +01:00
.map((commentsStr) => {
const comments: { text: string }[] = JSON.parse(commentsStr)
2022-01-12 02:31:51 +01:00
const startLoc = Number(args[1] ?? 0)
2022-01-26 21:40:38 +01:00
if (!isNaN(startLoc) && startLoc > 0) {
2022-01-12 02:31:51 +01:00
comments.splice(0, startLoc)
2022-09-08 21:40:48 +02:00
}
return new Combine(
2022-09-08 21:40:48 +02:00
comments
2022-01-08 04:22:50 +01:00
.filter((c) => c.text !== "")
2023-12-26 22:30:27 +01:00
.map(
(comment) =>
2024-10-19 14:44:55 +02:00
new SvelteUIElement(NoteCommentElement, {
comment,
state
2025-01-18 00:30:06 +01:00
})
)
).SetClass("flex flex-col")
2025-01-18 00:30:06 +01:00
})
)
2022-09-08 21:40:48 +02:00
},
{
2022-03-10 23:20:50 +01:00
funcName: "title",
args: [],
2022-03-10 23:20:50 +01:00
docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'",
example:
"`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.",
constr: (
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
_: string[],
feature: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
) => {
return new SvelteUIElement(FeatureTitle, { state, tags, feature, layer })
}
},
{
funcName: "statistics",
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
args: [],
2023-03-29 17:21:20 +02:00
constr: (state) => {
2023-03-28 05:13:48 +02:00
return new Combine(
state.theme.layers
2024-04-13 02:40:21 +02:00
.filter(
(l) =>
l.name !== null &&
l.title &&
2025-01-18 00:30:06 +01:00
state.perLayer.get(l.id) !== undefined
2024-04-13 02:40:21 +02:00
)
2023-03-28 05:13:48 +02:00
.map(
(l) => {
const fs = state.perLayer.get(l.id)
2024-03-28 03:39:46 +01:00
console.log(">>>", l.id, fs)
const bbox = state.mapProperties.bounds
2023-03-28 05:13:48 +02:00
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
return new StatisticsPanel(fsBboxed)
},
2025-01-18 00:30:06 +01:00
[state.mapProperties.bounds]
)
2023-03-28 05:13:48 +02:00
)
}
2022-09-08 21:40:48 +02:00
},
{
funcName: "send_email",
docs: "Creates a `mailto`-link where some fields are already set and correctly escaped. The user will be promted to send the email",
args: [
2022-09-08 21:40:48 +02:00
{
name: "to",
doc: "Who to send the email to?",
required: true
2022-09-08 21:40:48 +02:00
},
{
name: "subject",
doc: "The subject of the email",
required: true
2022-09-08 21:40:48 +02:00
},
{
name: "body",
doc: "The text in the email",
required: true
2022-09-08 21:40:48 +02:00
},
2022-09-08 21:40:48 +02:00
{
name: "button_text",
doc: "The text shown on the button in the UI",
required: true
}
2022-09-08 21:40:48 +02:00
],
2023-09-27 22:21:35 +02:00
2023-03-28 05:13:48 +02:00
constr(__, tags, args) {
return new SvelteUIElement(SendEmail, { args, tags })
}
2022-09-08 21:40:48 +02:00
},
2022-07-28 09:16:19 +02:00
{
2023-04-07 02:13:57 +02:00
funcName: "link",
docs: "Construct a link. By using the 'special' visualisation notation, translations should be easier",
2023-04-07 02:13:57 +02:00
args: [
{
name: "text",
doc: "Text to be shown",
required: true
2023-04-07 02:13:57 +02:00
},
{
name: "href",
doc: "The URL to link to. Note that this will be URI-encoded before ",
required: true
2023-04-07 02:13:57 +02:00
},
{
name: "class",
doc: "CSS-classes to add to the element"
},
{
name: "download",
doc: "Expects a string which denotes the filename to download the contents of `href` into. If set, this link will act as a download-button."
},
{
name: "arialabel",
doc: "If set, this text will be used as aria-label"
},
{
name: "icon",
doc: "If set, show this icon next to the link. You might want to combine this with `class: button`"
}
2023-04-07 02:13:57 +02:00
],
2023-04-07 02:13:57 +02:00
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
2025-01-18 00:30:06 +01:00
args: string[]
): SvelteUIElement {
let [text, href, classnames, download, ariaLabel, icon] = args
2023-12-19 16:45:27 +01:00
if (download === "") {
download = undefined
}
const newTab = download === undefined && !href.startsWith("#")
2024-04-13 02:40:21 +02:00
const textStore = tagSource.map((tags) => Utils.SubstituteKeys(text, tags))
2024-09-02 12:48:15 +02:00
const hrefStore = tagSource.map((tags) => Utils.SubstituteKeys(href, tags))
return new SvelteUIElement(DynLink, {
2024-04-13 02:40:21 +02:00
text: textStore,
href: hrefStore,
classnames: new ImmutableStore(classnames),
2024-04-13 02:40:21 +02:00
download: tagSource.map((tags) => Utils.SubstituteKeys(download, tags)),
ariaLabel: tagSource.map((tags) => Utils.SubstituteKeys(ariaLabel, tags)),
2024-08-09 16:55:08 +02:00
newTab: new ImmutableStore(newTab),
icon: tagSource.map((tags) => Utils.SubstituteKeys(icon, tags))
}).setSpan()
}
2023-04-07 02:13:57 +02:00
},
{
2022-07-28 09:16:19 +02:00
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(
2022-07-28 09:16:19 +02:00
{
render: {
special: {
type: "multi",
key: "_doors_from_building_properties",
tagrendering: {
en: "The building containing this feature has a <a href='#{id}'>door</a> of width {entrance:width}"
}
}
}
},
2022-09-08 21:40:48 +02:00
null,
2025-01-18 00:30:06 +01:00
" "
2022-09-08 21:40:48 +02:00
) +
2022-10-11 01:39:09 +02:00
"\n```",
2022-09-08 21:40:48 +02:00
args: [
{
name: "key",
2022-08-22 19:16:37 +02:00
doc: "The property to read and to interpret as a list of properties",
required: true
2022-09-08 21:40:48 +02:00
},
{
name: "tagrendering",
doc: "An entire tagRenderingConfig",
required: true
2025-01-02 03:38:39 +01:00
},
{
name: "classes",
doc: "CSS-classes to apply on every individual item. Seperated by `space`"
}
2022-09-08 21:40:48 +02:00
],
constr(
state: SpecialVisualizationState,
featureTags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
) {
2025-01-02 03:38:39 +01:00
const [key, tr, classesRaw] = args
let classes = classesRaw ?? ""
const translation = new Translation({ "*": tr })
return new VariableUiElement(
featureTags.map((tags) => {
2024-01-31 12:09:15 +01:00
let properties: object[]
2024-02-02 13:37:05 +01:00
if (typeof tags[key] === "string") {
properties = JSON.parse(tags[key])
} else {
properties = <any>tags[key]
2024-01-31 12:09:15 +01:00
}
2024-04-13 02:40:21 +02:00
if (!properties) {
console.debug(
"Could not create a special visualization for multi(",
args.join(", ") + ")",
"no properties found for object",
2025-01-18 00:30:06 +01:00
feature.properties.id
2024-04-13 02:40:21 +02:00
)
return undefined
}
const elements = []
for (const property of properties) {
const subsTr = new SvelteUIElement(SpecialTranslation, {
t: translation,
2024-01-31 12:09:15 +01:00
tags: new ImmutableStore(property),
state,
feature,
layer
2025-01-02 05:34:38 +01:00
// clss: classes ?? "",
2025-01-02 15:34:59 +01:00
}).SetClass(classes)
elements.push(subsTr)
}
2024-01-28 03:27:17 +01:00
return elements
2025-01-18 00:30:06 +01:00
})
2022-09-08 21:40:48 +02:00
)
}
2022-09-08 21:40:48 +02:00
},
2023-08-08 13:52:58 +02:00
{
funcName: "translated",
docs: "If the given key can be interpreted as a JSON, only show the key containing the current language (or 'en'). This specialRendering is meant to be used by MapComplete studio and is not useful in map themes",
2023-08-08 13:52:58 +02:00
args: [
{
name: "key",
doc: "The attribute to interpret as json",
defaultValue: "value"
}
2023-08-08 13:52:58 +02:00
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
): BaseUIElement {
return new VariableUiElement(
tagSource.map((tags) => {
const v = tags[argument[0] ?? "value"]
try {
2023-10-17 00:32:54 +02:00
const tr = typeof v === "string" ? JSON.parse(v) : v
2023-08-08 13:52:58 +02:00
return new Translation(tr).SetClass("font-bold")
} catch (e) {
console.error("Cannot create a translation for", v, "due to", e)
2023-10-17 00:32:54 +02:00
return JSON.stringify(v)
2023-08-08 13:52:58 +02:00
}
2025-01-18 00:30:06 +01:00
})
2023-08-08 13:52:58 +02:00
)
}
2023-08-08 13:52:58 +02:00
},
{
funcName: "fediverse_link",
docs: "Converts a fediverse username or link into a clickable link",
2023-08-10 16:25:25 +02:00
args: [
{
name: "key",
doc: "The attribute-name containing the link",
required: true
}
2023-08-10 16:25:25 +02:00
],
2023-08-10 16:25:25 +02:00
constr(
state: SpecialVisualizationState,
2024-10-29 23:46:52 +01:00
tags: UIEventSource<Record<string, string>>,
2023-08-10 16:25:25 +02:00
argument: string[],
feature: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
2023-08-10 16:25:25 +02:00
): BaseUIElement {
const key = argument[0]
return new SvelteUIElement(FediverseLink, { key, tags, state })
}
2023-08-10 16:25:25 +02:00
},
2023-10-20 19:04:55 +02:00
{
funcName: "braced",
docs: "Show a literal text within braces",
2023-10-20 19:04:55 +02:00
args: [
{
name: "text",
required: true,
doc: "The value to show"
}
2023-10-20 19:04:55 +02:00
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
2023-10-20 19:04:55 +02:00
): BaseUIElement {
return new FixedUiElement("{" + args[0] + "}")
}
2023-10-20 19:04:55 +02:00
},
2023-10-22 01:30:05 +02:00
{
funcName: "tags",
docs: "Shows a (json of) tags in a human-readable way + links to the wiki",
2023-10-22 01:30:05 +02:00
args: [
{
name: "key",
defaultValue: "value",
doc: "The key to look for the tags"
}
2023-10-22 01:30:05 +02:00
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
2023-10-22 01:30:05 +02:00
): BaseUIElement {
const key = argument[0] ?? "value"
return new VariableUiElement(
tagSource.map((tags) => {
let value = tags[key]
if (!value) {
return new FixedUiElement("No tags found").SetClass("font-bold")
}
if (typeof value === "string" && value.startsWith("{")) {
value = JSON.parse(value)
}
try {
const parsed = TagUtils.Tag(value)
return parsed.asHumanString(true, false, {})
} catch (e) {
return new FixedUiElement(
"Could not parse this tag: " +
JSON.stringify(value) +
" due to " +
e
2023-10-22 01:30:05 +02:00
).SetClass("alert")
}
2025-01-18 00:30:06 +01:00
})
2023-10-22 01:30:05 +02:00
)
}
2023-10-22 01:30:05 +02:00
},
{
funcName: "direction_indicator",
args: [],
docs: "Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object",
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
): BaseUIElement {
return new SvelteUIElement(DirectionIndicator, { state, feature })
}
},
{
funcName: "direction_absolute",
docs: "Converts compass degrees (with 0° being north, 90° being east, ...) into a human readable, translated direction such as 'north', 'northeast'",
args: [
{
name: "key",
doc: "The attribute containing the degrees",
defaultValue: "_direction:centerpoint"
}
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
2025-01-18 00:30:06 +01:00
args: string[]
): BaseUIElement {
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
return new VariableUiElement(
tagSource
.map((tags) => {
console.log("Direction value", tags[key], key)
return tags[key]
})
.mapD((value) => {
const dir = GeoOperations.bearingToHuman(
2025-01-18 00:30:06 +01:00
GeoOperations.parseBearing(value)
)
console.log("Human dir", dir)
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
2025-01-18 00:30:06 +01:00
})
)
}
},
2024-01-13 05:24:56 +01:00
{
funcName: "compare_data",
2024-01-14 22:24:35 +01:00
needsUrls: (args) => args[1].split(";"),
args: [
2024-01-13 05:24:56 +01:00
{
name: "url",
required: true,
doc: "The attribute containing the url where to fetch more data"
2024-01-13 05:24:56 +01:00
},
2024-01-14 22:24:35 +01:00
{
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 '*'. "
2024-01-14 22:24:35 +01:00
},
{
name: "readonly",
required: false,
doc: "If 'yes', will not show 'apply'-buttons"
}
2024-01-13 05:24:56 +01:00
],
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,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
): BaseUIElement {
2024-01-13 05:24:56 +01:00
const url = args[0]
const readonly = args[3] === "yes"
const externalData = Stores.FromPromiseWithErr(Utils.downloadJson(url))
return new SvelteUIElement(ComparisonTool, {
url,
state,
tags: tagSource,
layer,
feature,
readonly,
externalData
})
}
},
{
funcName: "linked_data_from_website",
docs: "Attempts to load (via a proxy) the specified website and parsed ld+json from there. Suitable data will be offered to import into OSM",
args: [
{
name: "key",
defaultValue: "website",
doc: "Attempt to load ld+json from the specified URL. This can be in an embedded <script type='ld+json'>"
},
{
name: "useProxy",
defaultValue: "yes",
doc: "If 'yes', uses the provided proxy server. This proxy server will scrape HTML and search for a script with `lang='ld+json'`. If `no`, the data will be downloaded and expects a linked-data-json directly"
},
{
name: "host",
doc: "If not using a proxy, define what host the website is allowed to connect to"
},
{
name: "mode",
doc: "If `display`, only show the data in tabular and readonly form, ignoring already existing tags. This is used to explicitly show all the tags. If unset or anything else, allow to apply/import on OSM"
2024-06-18 03:33:11 +02:00
},
{
name: "collapsed",
defaultValue: "yes",
doc: "If the containing accordion should be closed"
}
],
2024-04-09 13:58:23 +02:00
needsUrls: [Constants.linkedDataProxy, "http://www.schema.org"],
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
): BaseUIElement {
const key = argument[0] ?? "website"
const useProxy = argument[1] !== "no"
const readonly = argument[3] === "readonly"
const isClosed = (argument[4] ?? "yes") === "yes"
const countryStore: Store<string | undefined> = tags.mapD(
2025-01-18 00:30:06 +01:00
(tags) => tags._country
)
const sourceUrl: Store<string | undefined> = tags.mapD((tags) => {
if (!tags[key] || tags[key] === "undefined") {
return null
}
return tags[key]
})
2024-06-16 16:06:26 +02:00
const externalData: Store<{ success: GeoJsonProperties } | { error: any }> =
sourceUrl.bindD(
2024-12-17 04:23:24 +01:00
(url) => {
const country = countryStore.data
if (url.startsWith("https://data.velopark.be/")) {
return Stores.FromPromiseWithErr(
(async () => {
try {
const loadAll =
layer.id.toLowerCase().indexOf("maproulette") >=
0 // Dirty hack
const features =
await LinkedDataLoader.fetchVeloparkEntry(
url,
2025-01-18 00:30:06 +01:00
loadAll
)
const feature =
features.find(
2025-01-18 00:30:06 +01:00
(f) => f.properties["ref:velopark"] === url
) ?? features[0]
const properties = feature.properties
properties["ref:velopark"] = url
console.log(
"Got properties from velopark:",
2025-01-18 00:30:06 +01:00
properties
)
return properties
} catch (e) {
console.error(e)
throw e
}
2025-01-18 00:30:06 +01:00
})()
)
}
if (country === undefined) {
return undefined
}
2024-06-16 16:06:26 +02:00
return Stores.FromPromiseWithErr(
(async () => {
try {
return await LinkedDataLoader.fetchJsonLd(
url,
{ country },
2025-01-18 00:30:06 +01:00
useProxy ? "proxy" : "fetch-lod"
)
2024-06-16 16:06:26 +02:00
} catch (e) {
console.log(
"Could not get with proxy/download LOD, attempting to download directly. Error for ",
url,
"is",
2025-01-18 00:30:06 +01:00
e
)
return await LinkedDataLoader.fetchJsonLd(
url,
{ country },
2025-01-18 00:30:06 +01:00
"fetch-raw"
)
2024-06-16 16:06:26 +02:00
}
2025-01-18 00:30:06 +01:00
})()
2024-06-16 16:06:26 +02:00
)
},
2025-01-18 00:30:06 +01:00
[countryStore]
)
2024-04-13 02:40:21 +02:00
externalData.addCallbackAndRunD((lod) =>
2025-01-18 00:30:06 +01:00
console.log("linked_data_from_website received the following data:", lod)
2024-04-13 02:40:21 +02:00
)
return new Toggle(
new SvelteUIElement(ComparisonTool, {
feature,
state,
tags,
layer,
externalData,
sourceUrl,
2024-06-19 03:22:57 +02:00
readonly,
collapsed: isClosed
2024-04-13 02:40:21 +02:00
}),
undefined,
2025-01-18 00:30:06 +01:00
sourceUrl.map((url) => !!url)
)
}
2024-06-16 16:06:26 +02:00
},
{
funcName: "preset_description",
docs: "Shows the extra description from the presets of the layer, if one matches. It will pick the most specific one (e.g. if preset `A` implies `B`, but `B` does not imply `A`, it'll pick B) or the first one if no ordering can be made. Might be empty",
args: [],
2024-08-09 16:55:08 +02:00
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
2024-08-09 16:55:08 +02:00
): BaseUIElement {
const translation = tagSource.map((tags) => {
const layer = state.theme.getMatchingLayer(tags)
return layer?.getMostMatchingPreset(tags)?.description
})
return new VariableUiElement(translation)
}
2024-08-09 16:55:08 +02:00
},
2024-08-13 18:05:17 +02:00
{
funcName: "group",
docs: "A collapsable group (accordion)",
args: [
{
name: "header",
doc: "The _identifier_ of a single tagRendering. This will be used as header"
2024-08-13 18:05:17 +02:00
},
{
name: "labels",
doc: "A `;`-separated list of either identifiers or label names. All tagRenderings matching this value will be shown in the accordion"
}
2024-08-13 18:05:17 +02:00
],
2024-08-14 13:53:56 +02:00
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
argument: string[],
selectedElement: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
2024-08-14 13:53:56 +02:00
): SvelteUIElement {
2024-08-13 18:05:17 +02:00
const [header, labelsStr] = argument
2024-08-14 13:53:56 +02:00
const labels = labelsStr.split(";").map((x) => x.trim())
2024-08-13 18:05:17 +02:00
return new SvelteUIElement<any, any, any>(GroupedView, {
2024-08-14 13:53:56 +02:00
state,
tags,
selectedElement,
layer,
header,
labels
2024-08-13 18:05:17 +02:00
})
}
},
{
funcName: "preset_type_select",
docs: "An editable tag rendering which allows to change the type",
args: [],
2024-08-14 13:53:56 +02:00
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
argument: string[],
selectedElement: Feature,
2025-01-18 00:30:06 +01:00
layer: LayerConfig
2024-08-14 13:53:56 +02:00
): SvelteUIElement {
const t = Translations.t.preset_type
const question: QuestionableTagRenderingConfigJson = {
id: layer.id + "-type",
question: t.question.translations,
mappings: layer.presets.map((pr) => ({
if: new And(pr.tags).asJson(),
icon: "auto",
then: (pr.description ? t.typeDescription : t.typeTitle).Subs({
title: pr.title,
description: pr.description
}).translations
}))
}
const config = new TagRenderingConfig(question)
return new SvelteUIElement(TagRenderingEditable, {
config,
2024-08-14 13:53:56 +02:00
tags,
selectedElement,
state,
layer
})
}
}
]
2022-01-08 04:22:50 +01:00
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
2024-08-14 13:53:56 +02:00
const regex = /[a-zA-Z_]+/
2022-11-02 13:47:34 +01:00
const invalid = specialVisualizations
.map((sp, i) => ({ sp, i }))
.filter((sp) => sp.sp.funcName === undefined || !sp.sp.funcName.match(regex))
2022-11-02 13:47:34 +01:00
if (invalid.length > 0) {
throw (
2024-08-14 13:53:56 +02:00
"Invalid special visualisation found: funcName is undefined or doesn't match " +
regex +
2022-11-02 13:47:34 +01:00
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"
2022-11-02 13:47:34 +01:00
)
}
2024-08-23 13:13:41 +02:00
const allNames = specialVisualizations.map((f) => f.funcName)
const seen = new Set<string>()
for (let name of allNames) {
name = name.toLowerCase()
2024-08-23 13:13:41 +02:00
if (seen.has(name)) {
throw "Invalid special visualisations: detected a duplicate name: " + name
}
seen.add(name)
}
2022-01-08 04:22:50 +01:00
return specialVisualizations
}
}