Refactoring: put all special visualisations into their own class, add their location into the documentation

This commit is contained in:
Pieter Vander Vennet 2025-06-26 05:20:12 +02:00
parent c0e7c9e8fa
commit ae5205f92d
29 changed files with 2270 additions and 2075 deletions

View file

@ -1,37 +1,9 @@
import { FixedUiElement } from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement"
import { default as FeatureTitle } from "./Popup/Title.svelte"
import {
RenderingSpecification,
SpecialVisualization,
SpecialVisualizationState,
} from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz"
import { RenderingSpecification, SpecialVisualization } from "./SpecialVisualization"
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
import { MultiApplyViz } from "./Popup/MultiApplyViz"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import AllTagsPanel from "./Popup/AllTagsPanel/AllTagsPanel.svelte"
import { VariableUiElement } from "./Base/VariableUIElement"
import { Translation } from "./i18n/Translation"
import Translations from "./i18n/Translations"
import AutoApplyButtonVis from "./Popup/AutoApplyButtonVis"
import { LanguageElement } from "./Popup/LanguageElement/LanguageElement"
import SvelteUIElement from "./Base/SvelteUIElement"
import { Feature, LineString } from "geojson"
import { GeoOperations } from "../Logic/GeoOperations"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import ExportFeatureButton from "./Popup/ExportFeatureButton.svelte"
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"
import Constants from "../Models/Constants"
import { TagUtils } from "../Logic/Tags/TagUtils"
import NextChangeViz from "./OpeningHours/NextChangeViz.svelte"
import { Unit } from "../Models/Unit"
import DirectionIndicator from "./Base/DirectionIndicator.svelte"
import SpecialVisualisationUtils from "./SpecialVisualisationUtils"
import MarkdownUtils from "../Utils/MarkdownUtils"
import { And } from "../Logic/Tags/And"
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { ImageVisualisations } from "./SpecialVisualisations/ImageVisualisations"
import { NoteVisualisations } from "./SpecialVisualisations/NoteVisualisations"
import { FavouriteVisualisations } from "./SpecialVisualisations/FavouriteVisualisations"
@ -39,13 +11,13 @@ import { UISpecialVisualisations } from "./SpecialVisualisations/UISpecialVisual
import { SettingsVisualisations } from "./SpecialVisualisations/SettingsVisualisations"
import { ReviewSpecialVisualisations } from "./SpecialVisualisations/ReviewSpecialVisualisations"
import { DataImportSpecialVisualisations } from "./SpecialVisualisations/DataImportSpecialVisualisations"
import TagrenderingManipulationSpecialVisualisations from "./SpecialVisualisations/TagrenderingManipulationSpecialVisualisations"
import { WebAndCommunicationSpecialVisualisations } from "./SpecialVisualisations/WebAndCommunicationSpecialVisualisations"
import ClearGPSHistory from "./BigComponents/ClearGPSHistory.svelte"
import AllFeaturesStatistics from "./Statistics/AllFeaturesStatistics.svelte"
import OpeningHoursWithError from "./OpeningHours/Visualisation/OpeningHoursWithError.svelte"
import { OH } from "./OpeningHours/OpeningHours"
import opening_hours from "opening_hours"
import TagrenderingManipulationSpecialVisualisations
from "./SpecialVisualisations/TagrenderingManipulationSpecialVisualisations"
import {
WebAndCommunicationSpecialVisualisations,
} from "./SpecialVisualisations/WebAndCommunicationSpecialVisualisations"
import { DataVisualisations } from "./Popup/DataVisualisations"
import { DataExportVisualisations } from "./Popup/DataExportVisualisations"
export default class SpecialVisualizations {
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
@ -73,6 +45,12 @@ export default class SpecialVisualizations {
const example =
viz.example ??
"`{" + viz.funcName + "(" + viz.args.map((arg) => arg.defaultValue).join(",") + ")}`"
let definitionPlace = ""
if(viz.definedIn){
const path = viz.definedIn
definitionPlace = `Defined in [${path.markdownLocation}](${path.markdownLocation})`
}
return [
"### " + viz.funcName,
viz.docs,
@ -88,6 +66,7 @@ export default class SpecialVisualizations {
})
)
: undefined,
definitionPlace,
"#### Example usage of " + viz.funcName,
example,
].join("\n\n")
@ -119,6 +98,7 @@ export default class SpecialVisualizations {
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.",
data: "Visualises data of a POI, sometimes with data updating capabilities",
favourites:
"Elements relating to marking an object as favourite (giving it a heart). Default element",
settings: "Elements part of the usersettings-ui",
@ -132,18 +112,19 @@ export default class SpecialVisualizations {
"Special visualisations which reuse other tagRenderings to show data, but with a twist.",
web_and_communication:
"Tools to show data from external websites, which link to external websites or which link to external profiles",
ui: "Elements to support the user interface, e.g. 'title', 'translated'"
}
const helpTexts: string[] = []
let lastGroup: string = null
for (const viz of vis) {
if (viz.group !== lastGroup) {
lastGroup = viz.group
if (viz.group?.toLowerCase() !== lastGroup) {
lastGroup = viz.group?.toLowerCase()
if (viz.group === undefined) {
helpTexts.push("## Unclassified elements\n\nVarious elements")
} else {
helpTexts.push("## " + viz.group)
if (!groupExplanations[viz.group]) {
if (!groupExplanations[viz.group.toLowerCase()]) {
throw (
"\n\n >>>> ERROR <<<< Unknown visualisation group type: " +
viz.group +
@ -207,416 +188,10 @@ export default class SpecialVisualizations {
...DataImportSpecialVisualisations.initList(),
...TagrenderingManipulationSpecialVisualisations.initList(),
...WebAndCommunicationSpecialVisualisations.initList(),
new HistogramViz(),
{
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,
layer: LayerConfig
) {
if (feature.geometry.type !== "LineString") {
return undefined
}
const t = Translations.t.general.download
return new SvelteUIElement(ExportFeatureButton, {
tags,
feature,
layer,
mimetype: "{gpx=application/gpx+xml}",
extension: "gpx",
construct: (feature: Feature<LineString>, title: string) =>
GeoOperations.toGpx(feature, title),
helpertext: t.downloadGpxHelper,
maintext: t.downloadFeatureAsGpx,
})
},
},
...DataVisualisations.initList(),
...DataExportVisualisations.initList(),
new UploadToOsmViz(),
new MultiApplyViz(),
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>>,
_,
__,
layer: LayerConfig
) => new SvelteUIElement(AllTagsPanel, { tags, layer }),
},
{
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: [
{
name: "key",
defaultValue: "opening_hours",
type: "key",
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
const openingHoursStore: Store<opening_hours | "error" | undefined> =
OH.CreateOhObjectStore(tagSource, key, prefix, postfix)
return new SvelteUIElement(OpeningHoursWithError, {
tags: tagSource,
key,
opening_hours_obj: openingHoursStore,
})
},
},
{
funcName: "opening_hours_state",
docs: "A small element, showing if the POI is currently open and when the next change is",
args: [
{
name: "key",
type: "key",
defaultValue: "opening_hours",
doc: "The tagkey from which the opening hours are read.",
},
{
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__",
},
],
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[]
): SvelteUIElement {
const keyToUse = args[0]
const prefix = args[1]
const postfix = args[2]
return new SvelteUIElement(NextChangeViz, {
state,
keyToUse,
tags,
prefix,
postfix,
})
},
},
{
funcName: "canonical",
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",
type: "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(
...(state?.theme?.layers?.map((lyr) => lyr.units) ?? [])
)
const unit = allUnits.filter((unit) =>
unit.isApplicableToKey(key)
)[0]
if (unit === undefined) {
return value
}
const getCountry = () => tagSource.data._country
return unit.asHumanLongValue(value, getCountry)
})
)
},
},
{
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, {
tags,
feature,
layer,
mimetype: "application/vnd.geo+json",
extension: "geojson",
construct: (feature: Feature<LineString>) =>
JSON.stringify(feature, null, " "),
maintext: t.downloadFeatureAsGeojson,
helpertext: t.downloadGeoJsonHelper,
})
},
},
{
funcName: "clear_location_history",
docs: "A button to remove the travelled track information from the device",
args: [],
constr: (state) => {
return new SvelteUIElement(ClearGPSHistory, { state })
},
},
{
funcName: "title",
args: [],
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,
layer: LayerConfig
) => {
return new SvelteUIElement(FeatureTitle, { state, tags, feature, layer })
},
},
{
funcName: "statistics",
docs: "Show general statistics about all the elements currently in view. Intended to use on the `current_view`-layer. They will be split per layer",
args: [],
constr: (state) => new SvelteUIElement(AllFeaturesStatistics, { state }),
},
{
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",
args: [
{
name: "key",
type: "key",
doc: "The attribute to interpret as json",
defaultValue: "value",
},
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[]
): BaseUIElement {
return new VariableUiElement(
tagSource.map((tags) => {
const v = tags[argument[0] ?? "value"]
try {
const tr = typeof v === "string" ? JSON.parse(v) : v
return new Translation(tr).SetClass("font-bold")
} catch (e) {
console.error("Cannot create a translation for", v, "due to", e)
return JSON.stringify(v)
}
})
)
},
},
{
funcName: "braced",
docs: "Show a literal text within braces",
args: [
{
name: "text",
required: true,
doc: "The value to show",
},
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
return new FixedUiElement("{" + args[0] + "}")
},
},
{
funcName: "tags",
docs: "Shows a (json of) tags in a human-readable way + links to the wiki",
args: [
{
name: "key",
type: "key",
defaultValue: "value",
doc: "The key to look for the tags",
},
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[]
): 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
).SetClass("alert")
}
})
)
},
},
{
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
): 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",
type: "key",
doc: "The attribute containing the degrees",
defaultValue: "_direction:centerpoint",
},
{
name: "offset",
doc: "Offset value that is added to the actual value, e.g. `180` to indicate the opposite (backward) direction",
defaultValue: "0",
},
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[]
): BaseUIElement {
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
const offset = args[1] === "" ? 0 : Number(args[1])
return new VariableUiElement(
tagSource
.map((tags) => {
console.log("Direction value", tags[key], key)
return tags[key]
})
.mapD((value) => {
const dir = GeoOperations.bearingToHuman(
GeoOperations.parseBearing(value) + offset
)
console.log("Human dir", dir)
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
})
)
},
},
{
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: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>
): BaseUIElement {
const translation = tagSource.map((tags) => {
const layer = state.theme.getMatchingLayer(tags)
return layer?.getMostMatchingPreset(tags)?.description
})
return new VariableUiElement(translation)
},
},
{
funcName: "preset_type_select",
docs: "An editable tag rendering which allows to change the type",
args: [],
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
argument: string[],
selectedElement: Feature,
layer: LayerConfig
): 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,
tags,
selectedElement,
state,
layer,
})
},
},
]
specialVisualizations.push(new AutoApplyButtonVis(specialVisualizations))