From eb47c4d5b9a0379cc87b773c73638965fb247ee8 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 27 Feb 2024 03:32:56 +0100 Subject: [PATCH 001/213] Fix: long domain names cause horizontal scroll with taghints --- public/css/index-tailwind-output.css | 12 ++++-------- src/UI/Popup/TagHint.svelte | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 81bf9ff94..3e3a35150 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -896,10 +896,6 @@ video { margin-right: 4rem; } -.mb-4 { - margin-bottom: 1rem; -} - .mt-4 { margin-top: 1rem; } @@ -932,6 +928,10 @@ video { margin-right: 0.25rem; } +.mb-4 { + margin-bottom: 1rem; +} + .ml-1 { margin-left: 0.25rem; } @@ -1163,10 +1163,6 @@ video { height: 20rem; } -.h-5\/6 { - height: 83.333333%; -} - .h-56 { height: 14rem; } diff --git a/src/UI/Popup/TagHint.svelte b/src/UI/Popup/TagHint.svelte index 84e23fed3..099422e15 100644 --- a/src/UI/Popup/TagHint.svelte +++ b/src/UI/Popup/TagHint.svelte @@ -24,7 +24,7 @@ {#if !userDetails || $userDetails.loggedIn} -
+
{#if tags === undefined} {:else if embedIn === undefined} From 33d450047dc043c5329a14494d0e5d90a2d8e53d Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 28 Feb 2024 02:04:51 +0100 Subject: [PATCH 002/213] Add memorial theme, some fixes to benches and artwork to support memorial theme --- assets/layers/artwork/artwork.json | 20 ++- assets/layers/bench/bench.json | 23 ++- assets/layers/ghost_bike/ghost_bike.json | 2 +- assets/layers/memorial/license_info.json | 10 ++ assets/layers/memorial/memorial.json | 173 +++++++++++++++++++- assets/layers/memorial/memorial.svg | 59 +++++++ assets/layers/memorial/memorial.svg.license | 2 + assets/themes/memorials/memorials.json | 29 ++++ src/Models/ThemeConfig/LayerConfig.ts | 89 +++++----- 9 files changed, 343 insertions(+), 64 deletions(-) create mode 100644 assets/layers/memorial/memorial.svg create mode 100644 assets/layers/memorial/memorial.svg.license create mode 100644 assets/themes/memorials/memorials.json diff --git a/assets/layers/artwork/artwork.json b/assets/layers/artwork/artwork.json index 7ed51de84..e89dd7751 100644 --- a/assets/layers/artwork/artwork.json +++ b/assets/layers/artwork/artwork.json @@ -661,7 +661,11 @@ "freeform": { "key": "artist_name" }, - "condition": "artist:wikidata=", + "condition": { + "and": [ + "artist:wikidata=" + ] + }, "id": "artwork-artist_name", "labels": [ "artwork-question" @@ -744,7 +748,11 @@ "wikipedia", { "id": "artwork_subject", - "condition": "subject:wikidata~*", + "condition": { + "and": [ + "subject:wikidata~*" + ] + }, "question": { "en": "What does this artwork depict?", "de": "Was zeigt dieses Kunstwerk?", @@ -856,9 +864,13 @@ ] }, { - "builtin": "bench.*bench-questions", + "builtin": "bench.bench-questions", "override": { - "condition": "amenity=bench" + "condition": { + "and": [ + "amenity=bench" + ] + } } } ], diff --git a/assets/layers/bench/bench.json b/assets/layers/bench/bench.json index 110ddfb2d..9a0d7fd32 100644 --- a/assets/layers/bench/bench.json +++ b/assets/layers/bench/bench.json @@ -331,8 +331,7 @@ "he": "חומר: {material}" }, "freeform": { - "key": "material", - "addExtraTags": [] + "key": "material" }, "mappings": [ { @@ -1045,11 +1044,15 @@ "bench-questions" ], "condition": { - "or": [ - "historic=memorial", - "inscription~*", - "memorial=bench", - "tourism=artwork" + "and": [ + { + "or": [ + "historic=memorial", + "inscription~*", + "memorial=bench", + "tourism=artwork" + ] + } ] }, "question": { @@ -1112,7 +1115,11 @@ { "builtin": "artwork.*artwork-question", "override": { - "condition": "tourism=artwork" + "condition": { + "and": [ + "tourism=artwork" + ] + } } } ], diff --git a/assets/layers/ghost_bike/ghost_bike.json b/assets/layers/ghost_bike/ghost_bike.json index 6088e9ded..829591532 100644 --- a/assets/layers/ghost_bike/ghost_bike.json +++ b/assets/layers/ghost_bike/ghost_bike.json @@ -327,6 +327,6 @@ }, "allowMove": { "enableRelocation": false, - "enableImproveAccuraccy": true + "enableImproveAccuracy": true } } diff --git a/assets/layers/memorial/license_info.json b/assets/layers/memorial/license_info.json index a02ef7b01..9227d6a8f 100644 --- a/assets/layers/memorial/license_info.json +++ b/assets/layers/memorial/license_info.json @@ -1,4 +1,14 @@ [ + { + "path": "memorial.svg", + "license": "CC0-1.0", + "authors": [ + "OSM-Carto" + ], + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Memorial-16.svg" + ] + }, { "path": "plaque.svg", "license": "CC0-1.0", diff --git a/assets/layers/memorial/memorial.json b/assets/layers/memorial/memorial.json index c49171f44..a3516e29e 100644 --- a/assets/layers/memorial/memorial.json +++ b/assets/layers/memorial/memorial.json @@ -2,15 +2,34 @@ "id": "memorial", "description": "Layer showing memorial plaques, based upon a unofficial theme. Can be expanded to have multiple types of memorials later on", "source": { - "osmTags": "memorial=plaque" + "osmTags": { + "or": [ + "memorial~*", + "historic=memorial" + ] + } + }, + "name": { + "en": "Memorials" }, "title": { "render": { "en": "Memorial plaque", - "de": "Gedenktafel", "ca": "Placa commemorativa", - "cs": "Pamětní deska" - } + "cs": "Pamětní deska", + "de": "Gedenktafel" + }, + "mappings": [ + { + "if": "memorial=plaque", + "then": { + "en": "Memorial plaque", + "de": "Gedenktafel", + "ca": "Placa commemorativa", + "cs": "Pamětní deska" + } + } + ] }, "pointRendering": [ { @@ -24,33 +43,169 @@ "color": "white" }, { - "icon": "./assets/layers/memorial/plaque.svg" + "icon": { + "render": "./assets/layers/memorial/memorial.svg", + "mappings": [ + { + "if": "memorial=plaque", + "then": "./assets/layers/memorial/plaque.svg" + }, + { + "if": { + "or": [ + "memorial=bench", + "amenity=bench" + ] + }, + "then": "./assets/layers/bench/bench.svg" + } + ] + } } ] } ], "lineRendering": [], "tagRenderings": [ + "images", + { + "id": "memorial-type", + "question": { + "en": "What type of memorial is this?" + }, + "mappings": [ + { + "if": "memorial=statue", + "then": { + "en": "This is a statue" + }, + "addExtraTags": [ + "tourism=artwork", + "artwork=statue" + ] + }, + { + "if": "memorial=plaque", + "then": { + "en": "This is a plaque" + } + }, + { + "if": "memorial=bench", + "then": { + "en": "This is a commemorative bench" + }, + "addExtraTags": [ + "amenity=bench" + ] + }, + { + "if": "memorial=ghost_bike", + "then": { + "en": "This is a ghost bike - a bicycle painted white to remember a cyclist whom deceased because of a car crash" + } + } + ] + }, { "id": "inscription", "question": { "en": "What is the inscription of this plaque?", - "de": "Wie lautet die Inschrift auf dieser Gedenktafel?", "ca": "Quina és la inscripció d'aquesta placa?", - "cs": "Jaký je nápis na této desce?" + "cs": "Jaký je nápis na této desce?", + "de": "Wie lautet die Inschrift auf dieser Gedenktafel?" }, + "#:condition": "Benches have a separate inscription question", + "condition": "memorial!=bench", "render": { "en": "The inscription on this plaque reads:

{inscription}

", - "de": "Die Inschrift auf dieser Gedenktafel lautet:

{inscription}

", "ca": "La inscripció d'aquesta placa diu:

{inscription}

", - "cs": "Nápis na této desce zní:

{inscription}

" + "cs": "Nápis na této desce zní:

{inscription}

", + "de": "Die Inschrift auf dieser Gedenktafel lautet:

{inscription}

" }, "freeform": { "key": "inscription", "type": "text" + }, + "mappings": [ + { + "if": "not:inscription=yes", + "then": { + "en": "This memorial does not have an inscription" + }, + "addExtraTags": [ + "inscription=" + ] + } + ] + }, + { + "id": "wikidata", + "freeform": { + "key": "subject:wikidata", + "type": "wikidata", + "helperArgs": [ + "subject;memorial:conflict" + ] + }, + "question": { + "en": "What is the Wikipedia page about the person or event that is remembered here?" + }, + "questionHint": { + "en": "If the person or event does not have a Wikipedia page or Wikidata entity, skip this question." + }, + "render": { + "special": { + "type": "wikipedia", + "keyToShowWikipediaFor": "subject:wikidata" + }, + "before": { + "en": "

Wikipedia page about the remembered event or person

" + } + } + }, + { + "question": { + "en": "When was this memorial installed?" + }, + "render": { + "nl": "Geplaatst op {start_date}", + "en": "Placed on {start_date}", + "it": "Piazzata in data {start_date}", + "fr": "Placé le {start_date}", + "ru": "Установлен {start_date}", + "de": "Aufgestellt am {start_date}", + "ca": "Col·locat el {start_date}", + "cs": "Umístěno {start_date}" + }, + "freeform": { + "key": "start_date", + "type": "date" + }, + "id": "start_date" + }, + { + "builtin": "bench.bench-questions", + "override": { + "condition": { + "+and": [ + "amenity=bench" + ] + } } } ], + "presets": [ + { + "title": { + "en": "a memorial" + }, + "tags": [ + "historic=memorial" + ] + } + ], + "minzoom": 9, "deletion": true, "allowMove": { "enableImproveAccuracy": true, diff --git a/assets/layers/memorial/memorial.svg b/assets/layers/memorial/memorial.svg new file mode 100644 index 000000000..81760a8f6 --- /dev/null +++ b/assets/layers/memorial/memorial.svg @@ -0,0 +1,59 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/assets/layers/memorial/memorial.svg.license b/assets/layers/memorial/memorial.svg.license new file mode 100644 index 000000000..eb7905b0c --- /dev/null +++ b/assets/layers/memorial/memorial.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: OSM-Carto +SPDX-License-Identifier: CC0 \ No newline at end of file diff --git a/assets/themes/memorials/memorials.json b/assets/themes/memorials/memorials.json new file mode 100644 index 000000000..fd7b166d9 --- /dev/null +++ b/assets/themes/memorials/memorials.json @@ -0,0 +1,29 @@ +{ + "id": "memorials", + "icon": "./assets/layers/memorial/memorial.svg", + "description": { + "en": "Memorials are physical objects permantently placed in the public space to remember a person or event. They can be a wide range of objects, such as statues, plaques, paintings, military objects (such as tanks), ..." + }, + "title": { + "en": "Memorials" + }, + "layers": [ + { + "builtin": [ + "ghost_bike", + "memorial" + ], + "override": { + "minzoom": 9 + } + }, + { + "builtin": [ + "bench" + ], + "override": { + "minzoom": 18 + } + } + ] +} diff --git a/src/Models/ThemeConfig/LayerConfig.ts b/src/Models/ThemeConfig/LayerConfig.ts index e3a0ac247..2921b547f 100644 --- a/src/Models/ThemeConfig/LayerConfig.ts +++ b/src/Models/ThemeConfig/LayerConfig.ts @@ -91,7 +91,7 @@ export default class LayerConfig extends WithContextLoader { mercatorCrs: json.source["mercatorCrs"], idKey: json.source["idKey"], }, - json.id + json.id, ) } @@ -106,8 +106,8 @@ export default class LayerConfig extends WithContextLoader { } this.units = [].concat( ...(json.units ?? []).map((unitJson, i) => - Unit.fromJson(unitJson, `${context}.unit[${i}]`) - ) + Unit.fromJson(unitJson, `${context}.unit[${i}]`), + ), ) if (json.description !== undefined) { @@ -122,7 +122,7 @@ export default class LayerConfig extends WithContextLoader { if (json.calculatedTags !== undefined) { if (!official) { console.warn( - `Unofficial theme ${this.id} with custom javascript! This is a security risk` + `Unofficial theme ${this.id} with custom javascript! This is a security risk`, ) } this.calculatedTags = [] @@ -191,7 +191,7 @@ export default class LayerConfig extends WithContextLoader { tags: pr.tags.map((t) => TagUtils.SimpleTag(t)), description: Translations.T( pr.description, - `${translationContext}.presets.${i}.description` + `${translationContext}.presets.${i}.description`, ), preciseInput: preciseInput, exampleImages: pr.exampleImages, @@ -205,7 +205,7 @@ export default class LayerConfig extends WithContextLoader { if (json.lineRendering) { this.lineRendering = Utils.NoNull(json.lineRendering).map( - (r, i) => new LineRenderingConfig(r, `${context}[${i}]`) + (r, i) => new LineRenderingConfig(r, `${context}[${i}]`), ) } else { this.lineRendering = [] @@ -213,7 +213,7 @@ export default class LayerConfig extends WithContextLoader { if (json.pointRendering) { this.mapRendering = Utils.NoNull(json.pointRendering).map( - (r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`) + (r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`), ) } else { this.mapRendering = [] @@ -225,7 +225,7 @@ export default class LayerConfig extends WithContextLoader { r.location.has("centroid") || r.location.has("projected_centerpoint") || r.location.has("start") || - r.location.has("end") + r.location.has("end"), ) if ( @@ -247,7 +247,7 @@ export default class LayerConfig extends WithContextLoader { Constants.priviliged_layers.indexOf(this.id) < 0 && this.source !== null /*library layer*/ && !this.source?.geojsonSource?.startsWith( - "https://api.openstreetmap.org/api/0.6/notes.json" + "https://api.openstreetmap.org/api/0.6/notes.json", ) ) { throw ( @@ -266,7 +266,7 @@ export default class LayerConfig extends WithContextLoader { typeof tr !== "string" && tr["builtin"] === undefined && tr["id"] === undefined && - tr["rewrite"] === undefined + tr["rewrite"] === undefined, ) ?? [] if (missingIds?.length > 0 && official) { console.error("Some tagRenderings of", this.id, "are missing an id:", missingIds) @@ -277,8 +277,8 @@ export default class LayerConfig extends WithContextLoader { (tr, i) => new TagRenderingConfig( tr, - this.id + ".tagRenderings[" + i + "]" - ) + this.id + ".tagRenderings[" + i + "]", + ), ) if ( @@ -352,7 +352,7 @@ export default class LayerConfig extends WithContextLoader { public GetBaseTags(): Record { return TagUtils.changeAsProperties( - this.source?.osmTags?.asChange({ id: "node/-1" }) ?? [{ k: "id", v: "node/-1" }] + this.source?.osmTags?.asChange({ id: "node/-1" }) ?? [{ k: "id", v: "node/-1" }], ) } @@ -365,7 +365,7 @@ export default class LayerConfig extends WithContextLoader { neededLayer: string }[] = [], addedByDefault = false, - canBeIncluded = true + canBeIncluded = true, ): BaseUIElement { const extraProps: (string | BaseUIElement)[] = [] @@ -374,32 +374,32 @@ export default class LayerConfig extends WithContextLoader { if (canBeIncluded) { if (addedByDefault) { extraProps.push( - "**This layer is included automatically in every theme. This layer might contain no points**" + "**This layer is included automatically in every theme. This layer might contain no points**", ) } if (this.shownByDefault === false) { extraProps.push( - "This layer is not visible by default and must be enabled in the filter by the user. " + "This layer is not visible by default and must be enabled in the filter by the user. ", ) } if (this.title === undefined) { extraProps.push( - "Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable." + "Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable.", ) } if (this.name === undefined && this.shownByDefault === false) { extraProps.push( - "This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-=true" + "This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-=true", ) } if (this.name === undefined) { extraProps.push( - "Not visible in the layer selection by default. If you want to make this layer toggable, override `name`" + "Not visible in the layer selection by default. If you want to make this layer toggable, override `name`", ) } if (this.mapRendering.length === 0) { extraProps.push( - "Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`" + "Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`", ) } @@ -411,23 +411,28 @@ export default class LayerConfig extends WithContextLoader { : undefined, "This layer is loaded from an external source, namely ", new FixedUiElement(this.source.geojsonSource).SetClass("code"), - ]) + ]), ) } } else { extraProps.push( - "This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data." + "This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.", ) } let usingLayer: BaseUIElement[] = [] - if (usedInThemes?.length > 0 && !addedByDefault) { - usingLayer = [ - new Title("Themes using this layer", 4), - new List( - (usedInThemes ?? []).map((id) => new Link(id, "https://mapcomplete.org/" + id)) - ), - ] + if (!addedByDefault) { + + if (usedInThemes?.length > 0) { + usingLayer = [ + new Title("Themes using this layer", 4), + new List( + (usedInThemes ?? []).map((id) => new Link(id, "https://mapcomplete.org/" + id)), + ), + ] + } else if(this.source !== null) { + usingLayer = [new FixedUiElement("No themes use this layer")] + } } for (const dep of dependencies) { @@ -438,7 +443,7 @@ export default class LayerConfig extends WithContextLoader { " into the layout as it depends on it: ", dep.reason, "(" + dep.context + ")", - ]) + ]), ) } @@ -447,7 +452,7 @@ export default class LayerConfig extends WithContextLoader { new Combine([ "This layer is needed as dependency for layer", new Link(revDep, "#" + revDep), - ]) + ]), ) } @@ -459,14 +464,14 @@ export default class LayerConfig extends WithContextLoader { return undefined } const embedded: (Link | string)[] = values.values?.map((v) => - Link.OsmWiki(values.key, v, true).SetClass("mr-2") + Link.OsmWiki(values.key, v, true).SetClass("mr-2"), ) ?? ["_no preset options defined, or no values in them_"] return [ new Combine([ new Link( "", "https://taginfo.openstreetmap.org/keys/" + values.key + "#values", - true + true, ), Link.OsmWiki(values.key), ]).SetClass("flex"), @@ -475,7 +480,7 @@ export default class LayerConfig extends WithContextLoader { : new Link(values.type, "../SpecialInputElements.md#" + values.type), new Combine(embedded).SetClass("flex"), ] - }) + }), ) let quickOverview: BaseUIElement = undefined @@ -485,7 +490,7 @@ export default class LayerConfig extends WithContextLoader { "this quick overview is incomplete", new Table( ["attribute", "type", "values which are supported by this layer"], - tableRows + tableRows, ).SetClass("zebra-table"), ]).SetClass("flex-col flex") } @@ -499,7 +504,7 @@ export default class LayerConfig extends WithContextLoader { (mr) => mr.RenderIcon(new ImmutableStore({ id: "node/-1" }), { includeBadges: false, - }).html + }).html, ) .find((i) => i !== undefined) } @@ -511,7 +516,7 @@ export default class LayerConfig extends WithContextLoader { "Execute on overpass", Overpass.AsOverpassTurboLink(this.source.osmTags.optimize()) .replaceAll("(", "%28") - .replaceAll(")", "%29") + .replaceAll(")", "%29"), ) } catch (e) { console.error("Could not generate overpasslink for " + this.id) @@ -533,19 +538,19 @@ export default class LayerConfig extends WithContextLoader { const parts = neededTags["and"] tagsDescription.push( "Elements must match **all** of the following expressions:", - parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n") + parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n"), ) } else if (neededTags["or"]) { const parts = neededTags["or"] tagsDescription.push( "Elements must match **any** of the following expressions:", - parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n") + parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n"), ) } else { tagsDescription.push( "Elements must match the expression **" + - neededTags.asHumanString(true, false, {}) + - "**" + neededTags.asHumanString(true, false, {}) + + "**", ) } @@ -556,7 +561,7 @@ export default class LayerConfig extends WithContextLoader { return new Combine([ new Combine([new Title(this.id, 1), iconImg, this.description, "\n"]).SetClass( - "flex flex-col" + "flex flex-col", ), new List(extraProps), ...usingLayer, From ca1e4eba29b13501ca605a79f8305e7fc400b8f2 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 28 Feb 2024 02:07:31 +0100 Subject: [PATCH 003/213] Scripts: documentation script now also shows hidden themes in layer used themes --- scripts/generateDocs.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index a31ffe55a..331ef6f52 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -314,9 +314,6 @@ export class GenerateDocs extends Script { const themesPerLayer = new Map() for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) { - if (layout.hideFromOverview) { - continue - } for (const layer of layout.layers) { if (!builtinLayerIds.has(layer.id)) { // This is an inline layer From 27d99a295389e5749223e05a511cb783451dd0ca Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 28 Feb 2024 02:08:15 +0100 Subject: [PATCH 004/213] Improve error output --- src/Logic/Tags/TagUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logic/Tags/TagUtils.ts b/src/Logic/Tags/TagUtils.ts index 87929d480..7e746c028 100644 --- a/src/Logic/Tags/TagUtils.ts +++ b/src/Logic/Tags/TagUtils.ts @@ -729,7 +729,7 @@ export class TagUtils { } if (typeof json != "string") { if (json["and"] !== undefined && json["or"] !== undefined) { - throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined. Did you override a value? Perhaps use \`"=parent": { ... }\` instead of \"parent": {...}\` to trigger a replacement and not a fuse of values` + throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined. Did you override a value? Perhaps use \`"=parent": { ... }\` instead of \"parent": {...}\` to trigger a replacement and not a fuse of values. The value is ${JSON.stringify(json)}` } if (json["and"] !== undefined) { return new And(json["and"].map((t) => TagUtils.Tag(t, context))) From 9df228ac5df626bad8a643dd8ebcbf0330ea2a4d Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 28 Feb 2024 02:09:27 +0100 Subject: [PATCH 005/213] Refactoring: calculate selected layer in selected element view --- src/UI/BigComponents/SelectedElementView.svelte | 3 ++- src/UI/ThemeViewGUI.svelte | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/UI/BigComponents/SelectedElementView.svelte b/src/UI/BigComponents/SelectedElementView.svelte index c9b1afd7b..5965e7745 100644 --- a/src/UI/BigComponents/SelectedElementView.svelte +++ b/src/UI/BigComponents/SelectedElementView.svelte @@ -10,13 +10,14 @@ import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" export let state: SpecialVisualizationState - export let layer: LayerConfig export let selectedElement: Feature export let highlightedRendering: UIEventSource = undefined export let tags: UIEventSource> = state.featureProperties.getStore( selectedElement.properties.id ) + + let layer: LayerConfig =state.layout.getMatchingLayer(tags.data) let stillMatches = tags.map(tags => !layer?.source?.osmTags || layer.source.osmTags?.matchesProperties(tags)) diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 2dac52e78..4cd7ea378 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -402,7 +402,7 @@
- +
{/if} From 1cf2a94d8e16afdfc247085205b92eca6c6f9be6 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 28 Feb 2024 02:13:19 +0100 Subject: [PATCH 006/213] Fix typing error --- src/UI/InputElement/Validator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UI/InputElement/Validator.ts b/src/UI/InputElement/Validator.ts index 7aeee9654..1bec3faa8 100644 --- a/src/UI/InputElement/Validator.ts +++ b/src/UI/InputElement/Validator.ts @@ -57,8 +57,8 @@ export abstract class Validator { * * Returns 'undefined' if the element is valid */ - public getFeedback(s: string, _?: () => string): Translation | undefined { - if (this.isValid(s)) { + public getFeedback(s: string, getCountry?: () => string): Translation | undefined { + if (this.isValid(s, getCountry)) { return undefined } const tr = Translations.t.validation[this.name] @@ -71,7 +71,7 @@ export abstract class Validator { return Translations.t.validation[this.name].description } - public isValid(_: string): boolean { + public isValid(_: string, getCountry?: () => string): boolean { return true } From 6897b888170399db9a5d4e649d89dee2f9d0b30f Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 28 Feb 2024 02:13:36 +0100 Subject: [PATCH 007/213] Themes: improve error messages --- src/Models/ThemeConfig/Conversion/Validation.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Models/ThemeConfig/Conversion/Validation.ts b/src/Models/ThemeConfig/Conversion/Validation.ts index 14f9c519c..6947409c9 100644 --- a/src/Models/ThemeConfig/Conversion/Validation.ts +++ b/src/Models/ThemeConfig/Conversion/Validation.ts @@ -1012,6 +1012,13 @@ class MiscTagRenderingChecks extends DesugaringStep { ) { continue } + if(json.freeform.key.indexOf("wikidata")>=0){ + context + .enter("render") + .err( + `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?` + ) + } context .enter("render") .err( @@ -1264,7 +1271,7 @@ export class PrevalidateLayer extends DesugaringStep { // It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list context .enter("tagRenderings") - .err("Some tagrenderings have a duplicate id: " + duplicates.join(", ")) + .err("Some tagrenderings have a duplicate id: " + duplicates.join(", ")+"\n"+JSON.stringify(json.tagRenderings.filter(tr=> duplicates.indexOf(tr["id"])>=0))) } } From b795273974a77e190a3f0eb3193905e8bc862f30 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 28 Feb 2024 02:13:56 +0100 Subject: [PATCH 008/213] Improvements to wikidata element --- src/UI/InputElement/InputHelpers.ts | 7 +++---- src/UI/Popup/TagRendering/FreeformInput.svelte | 1 - src/UI/Wikipedia/WikidataSearchBox.ts | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/UI/InputElement/InputHelpers.ts b/src/UI/InputElement/InputHelpers.ts index 8c0fe1785..0e7099235 100644 --- a/src/UI/InputElement/InputHelpers.ts +++ b/src/UI/InputElement/InputHelpers.ts @@ -69,11 +69,10 @@ export default class InputHelpers { ) { const inputHelperOptions = props const args = inputHelperOptions.args ?? [] - const searchKey = args[0] ?? "name" + const searchKey: string = args[0] ?? "name" - const searchFor = ( - (inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "") - ) + const searchFor: string = searchKey.split(";").map(k => inputHelperOptions.feature?.properties[k]?.toLowerCase()) + .find(foundValue => !!foundValue) ?? "" let searchForValue: UIEventSource = new UIEventSource(searchFor) const options: any = args[1] diff --git a/src/UI/Popup/TagRendering/FreeformInput.svelte b/src/UI/Popup/TagRendering/FreeformInput.svelte index 42aa3ecff..e0c85b22d 100644 --- a/src/UI/Popup/TagRendering/FreeformInput.svelte +++ b/src/UI/Popup/TagRendering/FreeformInput.svelte @@ -76,6 +76,5 @@ {value} {state} on:submit - {unvalidatedText} />
diff --git a/src/UI/Wikipedia/WikidataSearchBox.ts b/src/UI/Wikipedia/WikidataSearchBox.ts index 48197e856..a2728bab9 100644 --- a/src/UI/Wikipedia/WikidataSearchBox.ts +++ b/src/UI/Wikipedia/WikidataSearchBox.ts @@ -20,7 +20,7 @@ export default class WikidataSearchBox extends InputElement { new Table( ["name", "doc"], [ - ["key", "the value of this tag will initialize search (default: name)"], + ["key", "the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search"], [ "options", new Combine([ From 59f56bd251c2523cb81ca07c74f9107c70560869 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 28 Feb 2024 12:37:17 +0100 Subject: [PATCH 009/213] Themes: improvements to memorial theme --- assets/layers/memorial/memorial.json | 33 ++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/assets/layers/memorial/memorial.json b/assets/layers/memorial/memorial.json index a3516e29e..c1179a803 100644 --- a/assets/layers/memorial/memorial.json +++ b/assets/layers/memorial/memorial.json @@ -14,10 +14,7 @@ }, "title": { "render": { - "en": "Memorial plaque", - "ca": "Placa commemorativa", - "cs": "Pamětní deska", - "de": "Gedenktafel" + "en": "Memorial" }, "mappings": [ { @@ -140,7 +137,29 @@ ] }, { - "id": "wikidata", + "id": "memorial-wikidata", + "freeform": { + "key": "wikidata", + "type": "wikidata" + }, + "question": { + "en": "What is the Wikipedia page about this memorial?" + }, + "questionHint": { + "en": "This is a about the memorial itself, not about the person or event that the memorial remembers. If this memorial does not have a Wikipedia page or Wikidata entity, skip this question." + }, + "render": { + "special": { + "type": "wikipedia", + "keyToShowWikipediaFor": "wikidata" + }, + "before": { + "en": "

Wikipedia page about the memorial

" + } + } + }, + { + "id": "subject-wikidata", "freeform": { "key": "subject:wikidata", "type": "wikidata", @@ -185,8 +204,8 @@ "id": "start_date" }, { - "builtin": "bench.bench-questions", - "override": { + "builtin": "bench.bench-questions", + "override": { "condition": { "+and": [ "amenity=bench" From 346f45cff8f1e9c1ea9d68b1503662a1b29031e8 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 29 Feb 2024 10:57:44 +0100 Subject: [PATCH 010/213] Fix current view --- src/Models/ThemeConfig/LayoutConfig.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Models/ThemeConfig/LayoutConfig.ts b/src/Models/ThemeConfig/LayoutConfig.ts index 2b50cebfb..353635849 100644 --- a/src/Models/ThemeConfig/LayoutConfig.ts +++ b/src/Models/ThemeConfig/LayoutConfig.ts @@ -313,6 +313,9 @@ export default class LayoutConfig implements LayoutInformation { if (tags === undefined) { return undefined } + if(tags.id.startsWith("current_view")){ + return this.getLayer("current_view") + } for (const layer of this.layers) { if (!layer.source) { if (layer.isShown?.matchesProperties(tags)) { From 66976ea44da8096fdb42376fd034a96cdd38d5cd Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 1 Mar 2024 00:30:21 +0100 Subject: [PATCH 011/213] UX: sharescreen: fixes, slight reorg, add QR --- src/UI/BigComponents/ShareScreen.svelte | 91 ++++++++++++++----------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/src/UI/BigComponents/ShareScreen.svelte b/src/UI/BigComponents/ShareScreen.svelte index 16633d63e..fb90ed12a 100644 --- a/src/UI/BigComponents/ShareScreen.svelte +++ b/src/UI/BigComponents/ShareScreen.svelte @@ -11,10 +11,11 @@ import Tr from "../Base/Tr.svelte" import Translations from "../i18n/Translations" import { Utils } from "../../Utils" - import Svg from "../../Svg" - import ToSvelte from "../Base/ToSvelte.svelte" import { DocumentDuplicateIcon } from "@rgossiaux/svelte-heroicons/outline" import Share from "../../assets/svg/Share.svelte" + import ToSvelte from "../Base/ToSvelte.svelte" + import Img from "../Base/Img" + import Qr from "../../Utils/Qr" export let state: ThemeViewState const tr = Translations.t.general.sharescreen @@ -69,22 +70,32 @@ } -
- -
- {#if typeof navigator?.share === "function"} - - {/if} - {#if navigator.clipboard !== undefined} - - {/if} -
Utils.selectTextIn(e.target)}> - {linkToShare} +
+
+ +
+ + +
+ {#if typeof navigator?.share === "function"} + + {/if} + {#if navigator.clipboard !== undefined} + + {/if} +
Utils.selectTextIn(e.target)}> + {linkToShare} +
+
+ + new Img(new Qr(linkToShare).toImageElement(125)).SetStyle( + "width: 125px" + )} />
@@ -95,29 +106,31 @@ - From de3aa8b6231a67372773c337f2f23486a62462ce Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 3 Mar 2024 13:51:36 +0100 Subject: [PATCH 012/213] Add bicycle parking theme, #mastodon request --- assets/themes/mapcomplete-changes/mapcomplete-changes.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index 22f2c1763..dc1134e94 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -149,6 +149,10 @@ "if": "theme=benches", "then": "./assets/themes/benches/bench_poi.svg" }, + { + "if": "theme=bicycle_parkings", + "then": "./assets/themes/bicycle_parkings/logo.svg" + }, { "if": "theme=bicycle_rental", "then": "./assets/themes/bicycle_rental/logo.svg" From a0953e3cb8e25da145defacea02a173b4f6bdf42 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 3 Mar 2024 13:52:00 +0100 Subject: [PATCH 013/213] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8eec40b88..9038bcc87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.40.0", + "version": "0.40.2", "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", "bugs": "https://github.com/pietervdvn/MapComplete/issues", From 425d61044b02caa9b8c6012c4df86af55d06c227 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 3 Mar 2024 14:14:45 +0100 Subject: [PATCH 014/213] Actually add bicycle parking theme --- .../bicycle_parkings/bicycle_parkings.json | 19 ++++++++ .../themes/bicycle_parkings/license_info.json | 10 +++++ assets/themes/bicycle_parkings/logo.svg | 45 +++++++++++++++++++ .../themes/bicycle_parkings/logo.svg.license | 2 + 4 files changed, 76 insertions(+) create mode 100644 assets/themes/bicycle_parkings/bicycle_parkings.json create mode 100644 assets/themes/bicycle_parkings/license_info.json create mode 100644 assets/themes/bicycle_parkings/logo.svg create mode 100644 assets/themes/bicycle_parkings/logo.svg.license diff --git a/assets/themes/bicycle_parkings/bicycle_parkings.json b/assets/themes/bicycle_parkings/bicycle_parkings.json new file mode 100644 index 000000000..d4d3f9f13 --- /dev/null +++ b/assets/themes/bicycle_parkings/bicycle_parkings.json @@ -0,0 +1,19 @@ +{ + "id": "bicycle_parkings", + "description": { + "en": "A map showing all types of bicycle parkings" + }, + "title": { + "en": "Bicycle parkings" + }, + "icon": "./assets/themes/bicycle_parkings/logo.svg", + "layers": [ + { + "builtin": + "bike_parking", + "override": { + "minzoom": 12 + } + } + ] +} diff --git a/assets/themes/bicycle_parkings/license_info.json b/assets/themes/bicycle_parkings/license_info.json new file mode 100644 index 000000000..8f6e1f424 --- /dev/null +++ b/assets/themes/bicycle_parkings/license_info.json @@ -0,0 +1,10 @@ +[ + { + "path": "logo.svg", + "license": "CC0-1.0", + "authors": [ + "Pieter Vander Vennet" + ], + "sources": [] + } +] \ No newline at end of file diff --git a/assets/themes/bicycle_parkings/logo.svg b/assets/themes/bicycle_parkings/logo.svg new file mode 100644 index 000000000..9b14f8735 --- /dev/null +++ b/assets/themes/bicycle_parkings/logo.svg @@ -0,0 +1,45 @@ + + diff --git a/assets/themes/bicycle_parkings/logo.svg.license b/assets/themes/bicycle_parkings/logo.svg.license new file mode 100644 index 000000000..ed0288300 --- /dev/null +++ b/assets/themes/bicycle_parkings/logo.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Pieter Vander Vennet +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file From f7403296db9ec5f94b40da17a527d4335b877022 Mon Sep 17 00:00:00 2001 From: mcliquid Date: Tue, 27 Feb 2024 11:57:54 +0000 Subject: [PATCH 015/213] Translated using Weblate (German) Currently translated at 100.0% (3387 of 3387 strings) Translation: MapComplete/Layer translations Translate-URL: https://hosted.weblate.org/projects/mapcomplete/layers/de/ --- langs/layers/de.json | 1001 ++++++++++++++++++++++++++---------------- 1 file changed, 627 insertions(+), 374 deletions(-) diff --git a/langs/layers/de.json b/langs/layers/de.json index 5c2f8a7f4..bf77eb7cf 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -35,6 +35,16 @@ "1": { "title": "eine freistehende Posterbox" }, + "10": { + "description": "Verwendet für Werbeschilder, Leuchtreklamen, Logos und institutionelle Eingangsschilder", + "title": "ein Schild" + }, + "11": { + "title": "eine Skulptur" + }, + "12": { + "title": "eine Wandmalerei" + }, "2": { "title": "eine wandmontierte Posterbox" }, @@ -61,16 +71,6 @@ }, "9": { "title": "ein Totem" - }, - "10": { - "description": "Verwendet für Werbeschilder, Leuchtreklamen, Logos und institutionelle Eingangsschilder", - "title": "ein Schild" - }, - "11": { - "title": "eine Skulptur" - }, - "12": { - "title": "eine Wandmalerei" } }, "tagRenderings": { @@ -165,6 +165,9 @@ "1": { "then": "Dies ist ein Brett" }, + "10": { + "then": "Dies ist eine Wandmalerei" + }, "2": { "then": "Dies ist eine Litfaßsäule" }, @@ -188,9 +191,6 @@ }, "9": { "then": "Dies ist ein Totem" - }, - "10": { - "then": "Dies ist eine Wandmalerei" } }, "question": "Welche Art von Werbung ist das?", @@ -205,6 +205,9 @@ "1": { "then": "Brett" }, + "10": { + "then": "Wandmalerei" + }, "2": { "then": "Posterbox" }, @@ -228,9 +231,6 @@ }, "9": { "then": "Totem" - }, - "10": { - "then": "Wandmalerei" } } } @@ -277,6 +277,9 @@ "1": { "then": "Es handelt sich um eine Seilbahn, bei der die Kabinen in ständigen Kreisen fahren" }, + "10": { + "then": "Eine Seilrutsche. (Eine Touristenattraktion, bei der abenteuerlustige Menschen mit hoher Geschwindigkeit hinunterfahren) " + }, "2": { "then": "Ein offener Sessellift mit Sitzgelegenheiten und Zugang zur Außenluft." }, @@ -300,9 +303,6 @@ }, "9": { "then": "Ein magic carpet (ein Förderband auf dem Boden)" - }, - "10": { - "then": "Eine Seilrutsche. (Eine Touristenattraktion, bei der abenteuerlustige Menschen mit hoher Geschwindigkeit hinunterfahren) " } }, "question": "Um welchen Seilbahntyp handelt es sich?" @@ -430,6 +430,15 @@ "1": { "then": "Wandbild" }, + "10": { + "then": "Azulejo (spanische dekorative Fliesenarbeit)" + }, + "11": { + "then": "Fliesenarbeit" + }, + "12": { + "then": "Holzschnitzerei" + }, "2": { "then": "Malerei" }, @@ -453,15 +462,6 @@ }, "9": { "then": "Relief" - }, - "10": { - "then": "Azulejo (spanische dekorative Fliesenarbeit)" - }, - "11": { - "then": "Fliesenarbeit" - }, - "12": { - "then": "Holzschnitzerei" } }, "question": "Um welche Art Kunstwerk handelt es sich?", @@ -2091,6 +2091,27 @@ "1": { "question": "Verfügt über einen
Schuko-Stecker ohne Erdungsstift (CEE7/4 Typ F)
" }, + "10": { + "question": "Hat einen
Typ 2 (Mennekes)
Anschluss mit Kabel" + }, + "11": { + "question": "Hat einen
Tesla Supercharger CCS (Typ 2 CSS vonTesla)
Anschluss" + }, + "12": { + "question": "Hat einen
Tesla Supercharger (Destination)
Anschluss" + }, + "13": { + "question": "Hat einen
Tesla Supercharger (Destination) (Typ 2 von Tesla)
Anschluss mit Kabel" + }, + "14": { + "question": "Hat einen
USB-Anschluss zum Aufladen von Telefonen und kleinen Elektrogeräten
" + }, + "15": { + "question": "Hat einen
Bosch Active Connect Anschluss mit 3 Pins
und Kabel" + }, + "16": { + "question": "Hat einen
Bosch Active Connect Anschluss mit 5 Pins
und Kabel" + }, "2": { "question": "Verfügt über einen
europäischen Netzstecker mit Erdungsstift (CEE7/4 Typ E)
Anschluss" }, @@ -2114,27 +2135,6 @@ }, "9": { "question": "Hat einen
Typ 2 CCS (Mennekes)
Anschluss" - }, - "10": { - "question": "Hat einen
Typ 2 (Mennekes)
Anschluss mit Kabel" - }, - "11": { - "question": "Hat einen
Tesla Supercharger CCS (Typ 2 CSS vonTesla)
Anschluss" - }, - "12": { - "question": "Hat einen
Tesla Supercharger (Destination)
Anschluss" - }, - "13": { - "question": "Hat einen
Tesla Supercharger (Destination) (Typ 2 von Tesla)
Anschluss mit Kabel" - }, - "14": { - "question": "Hat einen
USB-Anschluss zum Aufladen von Telefonen und kleinen Elektrogeräten
" - }, - "15": { - "question": "Hat einen
Bosch Active Connect Anschluss mit 3 Pins
und Kabel" - }, - "16": { - "question": "Hat einen
Bosch Active Connect Anschluss mit 5 Pins
und Kabel" } } } @@ -2190,30 +2190,6 @@ "1": { "then": "Schuko-Stecker ohne Erdungsstift (CEE7/4 Typ F)" }, - "2": { - "then": "Europäischer Netzstecker mit Erdungsstift (CEE7/4 Typ E)" - }, - "3": { - "then": "Europäischer Netzstecker mit Erdungsstift (CEE7/4 Typ E)" - }, - "4": { - "then": "Chademo-Anschluss" - }, - "5": { - "then": "Chademo-Anschluss" - }, - "6": { - "then": "Typ 1 mit Kabel (J1772)" - }, - "7": { - "then": "Typ 1 mit Kabel (J1772)" - }, - "8": { - "then": "Typ 1 ohne Kabel (J1772)" - }, - "9": { - "then": " Typ 1 ohne Kabel (J1772)" - }, "10": { "then": "Typ 1 CCS (Typ 1 Combo)" }, @@ -2244,6 +2220,9 @@ "19": { "then": "Typ 2 mit Kabel (mennekes)" }, + "2": { + "then": "Europäischer Netzstecker mit Erdungsstift (CEE7/4 Typ E)" + }, "20": { "then": "Tesla Supercharger CCS (Typ 2 CSS von Tesla)" }, @@ -2274,11 +2253,32 @@ "29": { "then": " Bosch Active Connect mit 3 Pins und Kabel" }, + "3": { + "then": "Europäischer Netzstecker mit Erdungsstift (CEE7/4 Typ E)" + }, "30": { "then": "Bosch Active Connect mit 5 Pins und Kabel" }, "31": { "then": " Bosch Active Connect mit 5 Pins und Kabel" + }, + "4": { + "then": "Chademo-Anschluss" + }, + "5": { + "then": "Chademo-Anschluss" + }, + "6": { + "then": "Typ 1 mit Kabel (J1772)" + }, + "7": { + "then": "Typ 1 mit Kabel (J1772)" + }, + "8": { + "then": "Typ 1 ohne Kabel (J1772)" + }, + "9": { + "then": " Typ 1 ohne Kabel (J1772)" } }, "question": "Welche Ladeanschlüsse gibt es hier?" @@ -2472,6 +2472,24 @@ "1": { "2": "Europäischer Netzstecker mit Erdungsstift (CEE7/4 Typ E)" }, + "10": { + "2": "Tesla Supercharger CCS (Typ 2 CSS von Tesla)" + }, + "11": { + "2": "Tesla Supercharger (Destination)" + }, + "12": { + "2": "Tesla Supercharger (Destination) (Typ 2 mit Kabel von Tesla)" + }, + "13": { + "2": "USB zum Aufladen von Handys und kleinen Elektrogeräten" + }, + "14": { + "2": " Bosch Active Connect mit 3 Pins und Kabel" + }, + "15": { + "2": " Bosch Active Connect mit 5 Pins und Kabel" + }, "2": { "2": "Chademo-Stecker" }, @@ -2495,24 +2513,6 @@ }, "9": { "2": "Typ 2 mit Kabel (Mennekes)" - }, - "10": { - "2": "Tesla Supercharger CCS (Typ 2 CSS von Tesla)" - }, - "11": { - "2": "Tesla Supercharger (Destination)" - }, - "12": { - "2": "Tesla Supercharger (Destination) (Typ 2 mit Kabel von Tesla)" - }, - "13": { - "2": "USB zum Aufladen von Handys und kleinen Elektrogeräten" - }, - "14": { - "2": " Bosch Active Connect mit 3 Pins und Kabel" - }, - "15": { - "2": " Bosch Active Connect mit 5 Pins und Kabel" } } } @@ -3310,6 +3310,15 @@ "1": { "then": "Dieser Radweg hat einen festen Belag" }, + "10": { + "then": "Dieser Radweg besteht aus feinem Schotter" + }, + "11": { + "then": "Der Radweg ist aus Kies" + }, + "12": { + "then": "Dieser Radweg besteht aus Rohboden" + }, "2": { "then": "Der Radweg ist aus Asphalt" }, @@ -3333,15 +3342,6 @@ }, "9": { "then": "Der Radweg ist aus Schotter" - }, - "10": { - "then": "Dieser Radweg besteht aus feinem Schotter" - }, - "11": { - "then": "Der Radweg ist aus Kies" - }, - "12": { - "then": "Dieser Radweg besteht aus Rohboden" } }, "question": "Was ist der Belag dieses Radwegs?", @@ -3390,6 +3390,15 @@ "1": { "then": "Dieser Radweg hat einen festen Belag" }, + "10": { + "then": "Dieser Radweg besteht aus feinem Schotter" + }, + "11": { + "then": "Der Radweg ist aus Kies" + }, + "12": { + "then": "Dieser Radweg besteht aus Rohboden" + }, "2": { "then": "Der Radweg ist aus Asphalt" }, @@ -3413,15 +3422,6 @@ }, "9": { "then": "Der Radweg ist aus Schotter" - }, - "10": { - "then": "Dieser Radweg besteht aus feinem Schotter" - }, - "11": { - "then": "Der Radweg ist aus Kies" - }, - "12": { - "then": "Dieser Radweg besteht aus Rohboden" } }, "question": "Was ist der Belag dieser Straße?", @@ -4394,6 +4394,54 @@ } } }, + "10": { + "options": { + "0": { + "question": "Keine Bevorzugung von Hunden" + }, + "1": { + "question": "Hunde erlaubt" + }, + "2": { + "question": "Keine Hunde erlaubt" + } + } + }, + "11": { + "options": { + "0": { + "question": "Internetzugang vorhanden" + } + } + }, + "12": { + "options": { + "0": { + "question": "Stromanschluss vorhanden" + } + } + }, + "13": { + "options": { + "0": { + "question": "Hat zuckerfreie Angebote" + } + } + }, + "14": { + "options": { + "0": { + "question": "Hat glutenfreie Angebote" + } + } + }, + "15": { + "options": { + "0": { + "question": "Hat laktosefreie Angebote" + } + } + }, "2": { "options": { "0": { @@ -4464,54 +4512,6 @@ "question": "Nutzung kostenlos" } } - }, - "10": { - "options": { - "0": { - "question": "Keine Bevorzugung von Hunden" - }, - "1": { - "question": "Hunde erlaubt" - }, - "2": { - "question": "Keine Hunde erlaubt" - } - } - }, - "11": { - "options": { - "0": { - "question": "Internetzugang vorhanden" - } - } - }, - "12": { - "options": { - "0": { - "question": "Stromanschluss vorhanden" - } - } - }, - "13": { - "options": { - "0": { - "question": "Hat zuckerfreie Angebote" - } - } - }, - "14": { - "options": { - "0": { - "question": "Hat glutenfreie Angebote" - } - } - }, - "15": { - "options": { - "0": { - "question": "Hat laktosefreie Angebote" - } - } } } }, @@ -4631,30 +4631,6 @@ "1": { "then": "Die Fitness-Station hat ein Schild mit Anweisungen für eine bestimmte Übung." }, - "2": { - "then": "Die Fitness-Station hat eine Einrichtung für Sit-ups." - }, - "3": { - "then": "Die Fitness-Station hat eine Vorrichtung für Liegestütze. In der Regel eine oder mehrere niedrige Reckstangen." - }, - "4": { - "then": "Die Fitness-Station hat Stangen zum Dehnen." - }, - "5": { - "then": "Die Fitness-Station hat eine Vorrichtung für Rückenstrecker (Hyperextensions)." - }, - "6": { - "then": "Die Fitness-Station hat Ringe für Gymnastikübungen." - }, - "7": { - "then": "Die Fitness-Station hat eine horizontale Leiter (Monkey Bars)." - }, - "8": { - "then": "Die Fitness-Station hat eine Sprossenwand zum Klettern." - }, - "9": { - "then": "Die Fitness-Station hat Pfosten für Slalomübungen." - }, "10": { "then": "Die Fitness-Station hat Trittsteine." }, @@ -4685,6 +4661,9 @@ "19": { "then": "Die Fitness-Station hat Kampfseile (battle ropes)." }, + "2": { + "then": "Die Fitness-Station hat eine Einrichtung für Sit-ups." + }, "20": { "then": "Die Fitness-Station hat ein Fahrradergometer." }, @@ -4699,6 +4678,27 @@ }, "24": { "then": "Die Fitness-Station hat eine Slackline." + }, + "3": { + "then": "Die Fitness-Station hat eine Vorrichtung für Liegestütze. In der Regel eine oder mehrere niedrige Reckstangen." + }, + "4": { + "then": "Die Fitness-Station hat Stangen zum Dehnen." + }, + "5": { + "then": "Die Fitness-Station hat eine Vorrichtung für Rückenstrecker (Hyperextensions)." + }, + "6": { + "then": "Die Fitness-Station hat Ringe für Gymnastikübungen." + }, + "7": { + "then": "Die Fitness-Station hat eine horizontale Leiter (Monkey Bars)." + }, + "8": { + "then": "Die Fitness-Station hat eine Sprossenwand zum Klettern." + }, + "9": { + "then": "Die Fitness-Station hat Pfosten für Slalomübungen." } }, "question": "Welche Übungsgeräte gibt es an dieser Fitness-Station?" @@ -4818,6 +4818,21 @@ "1": { "then": "Dies ist eine Pommesbude" }, + "10": { + "then": "Hier werden chinesische Gerichte serviert" + }, + "11": { + "then": "Hier werden griechische Gerichte serviert" + }, + "12": { + "then": "Hier werden indische Gerichte serviert" + }, + "13": { + "then": "Hier werden türkische Gerichte serviert" + }, + "14": { + "then": "Hier werden thailändische Gerichte serviert" + }, "2": { "then": "Bietet vorwiegend Pastagerichte an" }, @@ -4841,21 +4856,6 @@ }, "9": { "then": "Hier werden französische Gerichte serviert" - }, - "10": { - "then": "Hier werden chinesische Gerichte serviert" - }, - "11": { - "then": "Hier werden griechische Gerichte serviert" - }, - "12": { - "then": "Hier werden indische Gerichte serviert" - }, - "13": { - "then": "Hier werden türkische Gerichte serviert" - }, - "14": { - "then": "Hier werden thailändische Gerichte serviert" } }, "question": "Was für Essen gibt es hier?", @@ -5555,30 +5555,6 @@ "1": { "then": "Dies ist ein Auditorium" }, - "2": { - "then": "Dies ist ein Schlafzimmer" - }, - "3": { - "then": "Dies ist eine Kapelle" - }, - "4": { - "then": "Dies ist ein Klassenzimmer" - }, - "5": { - "then": "Dies ist ein Klassenzimmer" - }, - "6": { - "then": "Dies ist ein Computerraum" - }, - "7": { - "then": "Dies ist ein Konferenzraum" - }, - "8": { - "then": "Dies ist eine Krypta" - }, - "9": { - "then": "Dies ist eine Küche" - }, "10": { "then": "Dies ist ein Labor" }, @@ -5609,6 +5585,9 @@ "19": { "then": "Dies ist ein Lagerraum" }, + "2": { + "then": "Dies ist ein Schlafzimmer" + }, "20": { "then": "Dies ist ein Technikraum" }, @@ -5617,6 +5596,27 @@ }, "22": { "then": "Dies ist ein Wartezimmer" + }, + "3": { + "then": "Dies ist eine Kapelle" + }, + "4": { + "then": "Dies ist ein Klassenzimmer" + }, + "5": { + "then": "Dies ist ein Klassenzimmer" + }, + "6": { + "then": "Dies ist ein Computerraum" + }, + "7": { + "then": "Dies ist ein Konferenzraum" + }, + "8": { + "then": "Dies ist eine Krypta" + }, + "9": { + "then": "Dies ist eine Küche" } }, "question": "Wie wird dieser Raum genutzt?" @@ -5658,6 +5658,12 @@ "render": "Informationstafel" } }, + "item_with_image": { + "name": "Element mit mindestens einem Bild", + "title": { + "render": "POI mit Bild" + } + }, "kerbs": { "description": "Eine Ebene, die Bordsteine zeigt.", "filter": { @@ -6274,6 +6280,19 @@ } } }, + "10": { + "options": { + "0": { + "question": "Alle Notizen" + }, + "1": { + "question": "Importnotizen ausblenden" + }, + "2": { + "question": "Nur Importnotizen anzeigen" + } + } + }, "2": { "options": { "0": { @@ -6329,19 +6348,6 @@ "question": "Nur offene Notizen anzeigen" } } - }, - "10": { - "options": { - "0": { - "question": "Alle Notizen" - }, - "1": { - "question": "Importnotizen ausblenden" - }, - "2": { - "question": "Nur Importnotizen anzeigen" - } - } } }, "name": "OpenStreetMap-Hinweise", @@ -6666,6 +6672,18 @@ "1": { "then": "Dies ist ein normaler Stellplatz." }, + "10": { + "then": "Dies ist ein Stellplatz, der für das Personal reserviert ist." + }, + "11": { + "then": "Dies ist ein Stellplatz, der für Taxis reserviert ist." + }, + "12": { + "then": "Dies ist ein Stellplatz, der für Fahrzeuge mit Anhänger reserviert ist." + }, + "13": { + "then": "Dies ist ein Stellplatz, der für Carsharing reserviert ist." + }, "2": { "then": "Dies ist ein Behindertenstellplatz." }, @@ -6689,18 +6707,6 @@ }, "9": { "then": "Dies ist ein Stellplatz, der für Eltern mit Kindern reserviert ist." - }, - "10": { - "then": "Dies ist ein Stellplatz, der für das Personal reserviert ist." - }, - "11": { - "then": "Dies ist ein Stellplatz, der für Taxis reserviert ist." - }, - "12": { - "then": "Dies ist ein Stellplatz, der für Fahrzeuge mit Anhänger reserviert ist." - }, - "13": { - "then": "Dies ist ein Stellplatz, der für Carsharing reserviert ist." } }, "question": "Welche Art von Stellplatz ist dies?" @@ -6993,6 +6999,101 @@ "render": "Spielplatz" } }, + "playground_equipment": { + "description": "Ebene mit Spielplatzausrüstung", + "name": "Spielplatzausstattung", + "presets": { + "0": { + "description": "Ein genauer Typ wird später gefragt", + "title": "ein Spielgerät" + } + }, + "tagRenderings": { + "type": { + "freeform": { + "placeholder": "Art des Geräts" + }, + "mappings": { + "0": { + "then": "Das ist eine Schaukel" + }, + "1": { + "then": "Dies ist eine Struktur aus mehreren angeschlossenen Spielgeräten" + }, + "10": { + "then": "Dies ist ein Seilzug" + }, + "11": { + "then": "Dies ist ein horizontaler Balken" + }, + "12": { + "then": "Dies ist ein Hüpfspiel" + }, + "13": { + "then": "Dies ist ein Planschbecken" + }, + "14": { + "then": "Das ist eine Kletterwand" + }, + "15": { + "then": "Das ist eine Karte" + }, + "16": { + "then": "Dies ist eine Brücke (entweder als eigenständiges Gerät oder als Teil einer größeren Struktur)" + }, + "17": { + "then": "Das ist ein Hüpfkissen" + }, + "18": { + "then": "Dies ist ein Aktivitätspanel" + }, + "19": { + "then": "Dies ist eine Jugendherberge" + }, + "2": { + "then": "Das ist eine Rutsche" + }, + "20": { + "then": "Dies ist ein Trichter, mit dem man Trichterball spielen kann" + }, + "21": { + "then": "Dies ist ein sich drehender Kreis" + }, + "3": { + "then": "Dies ist ein Sandkasten" + }, + "4": { + "then": "Dies ist ein Springreiter" + }, + "5": { + "then": "Dies ist ein Kletterrahmen" + }, + "6": { + "then": "Dies ist eine Wippe" + }, + "7": { + "then": "Das ist ein Spielhaus" + }, + "8": { + "then": "Dies ist ein Karussell" + }, + "9": { + "then": "Dies ist eine Korbschaukel" + } + }, + "question": "Was ist das für ein Gerät?", + "render": "Das ist ein {playground}" + }, + "wheelchair-access": { + "override": { + "question": "Ist dieses Gerät mit Rollstuhl erreichbar?" + } + } + }, + "title": { + "render": "Spielplatzgerät" + } + }, "postboxes": { "description": "Die Ebene zeigt Briefkästen.", "name": "Briefkästen", @@ -7007,6 +7108,43 @@ }, "postoffices": { "description": "Eine Ebene mit Postämtern.", + "filter": { + "1": { + "options": { + "0": { + "question": "Bietet Briefpost an" + } + } + }, + "2": { + "options": { + "0": { + "question": "Bietet Paketaufgabe an" + } + } + }, + "3": { + "options": { + "0": { + "question": "Bietet die Abholung von verpassten Paketen an" + } + } + }, + "4": { + "options": { + "0": { + "question": "Akzeptiert die Abholung von Paketen, die hierher geschickt werden" + } + } + }, + "5": { + "options": { + "0": { + "question": "Verkauft Briefmarken" + } + } + } + }, "name": "Poststellen", "presets": { "0": { @@ -7301,6 +7439,21 @@ "1": { "then": "2-Cent-Münzen werden akzeptiert" }, + "10": { + "then": "20-Centime-Münzen werden akzeptiert" + }, + "11": { + "then": "½-Schweizer Franken-Münzen werden akzeptiert" + }, + "12": { + "then": "1-Schweizer Franken-Münzen werden akzeptiert" + }, + "13": { + "then": "2-Schweizer Franken-Münzen werden akzeptiert" + }, + "14": { + "then": "5-Schweizer Franken-Münzen werden akzeptiert" + }, "2": { "then": "5-Cent-Münzen werden akzeptiert" }, @@ -7324,21 +7477,6 @@ }, "9": { "then": "10-Centime-Münzen werden akzeptiert" - }, - "10": { - "then": "20-Centime-Münzen werden akzeptiert" - }, - "11": { - "then": "½-Schweizer Franken-Münzen werden akzeptiert" - }, - "12": { - "then": "1-Schweizer Franken-Münzen werden akzeptiert" - }, - "13": { - "then": "2-Schweizer Franken-Münzen werden akzeptiert" - }, - "14": { - "then": "5-Schweizer Franken-Münzen werden akzeptiert" } }, "question": "Mit welchen Münzen kann man hier bezahlen?" @@ -7351,6 +7489,15 @@ "1": { "then": "10-Euro-Scheine werden angenommen" }, + "10": { + "then": "100-Schweizer Franken-Scheine werden akzeptiert" + }, + "11": { + "then": "200-Schweizer Franken-Scheine werden akzeptiert" + }, + "12": { + "then": "1000-Schweizer Franken-Scheine werden akzeptiert" + }, "2": { "then": "20-Euro-Scheine werden angenommen" }, @@ -7374,15 +7521,6 @@ }, "9": { "then": "50-Schweizer Franken-Scheine werden akzeptiert" - }, - "10": { - "then": "100-Schweizer Franken-Scheine werden akzeptiert" - }, - "11": { - "then": "200-Schweizer Franken-Scheine werden akzeptiert" - }, - "12": { - "then": "1000-Schweizer Franken-Scheine werden akzeptiert" } }, "question": "Mit welchen Banknoten kann man hier bezahlen?" @@ -7836,30 +7974,6 @@ "1": { "question": "Recycling von Batterien" }, - "2": { - "question": "Recycling von Getränkekartons" - }, - "3": { - "question": "Recycling von Dosen" - }, - "4": { - "question": "Recycling von Kleidung" - }, - "5": { - "question": "Recycling von Speiseöl" - }, - "6": { - "question": "Recycling von Motoröl" - }, - "7": { - "question": "Recycling von Leuchtstoffröhren" - }, - "8": { - "question": "Recycling von Grünabfällen" - }, - "9": { - "question": "Recycling von Glasflaschen" - }, "10": { "question": "Recycling von Glas" }, @@ -7890,11 +8004,35 @@ "19": { "question": "Recycling von Restabfällen" }, + "2": { + "question": "Recycling von Getränkekartons" + }, "20": { "question": "Recycling von Druckerpatronen" }, "21": { "question": "Recycling von Fahrrädern" + }, + "3": { + "question": "Recycling von Dosen" + }, + "4": { + "question": "Recycling von Kleidung" + }, + "5": { + "question": "Recycling von Speiseöl" + }, + "6": { + "question": "Recycling von Motoröl" + }, + "7": { + "question": "Recycling von Leuchtstoffröhren" + }, + "8": { + "question": "Recycling von Grünabfällen" + }, + "9": { + "question": "Recycling von Glasflaschen" } } }, @@ -7962,30 +8100,6 @@ "1": { "then": "Getränkekartons können hier recycelt werden" }, - "2": { - "then": "Dosen können hier recycelt werden" - }, - "3": { - "then": "Kleidung kann hier recycelt werden" - }, - "4": { - "then": "Speiseöl kann hier recycelt werden" - }, - "5": { - "then": "Motoröl kann hier recycelt werden" - }, - "6": { - "then": "Hier können Leuchtstoffröhren recycelt werden" - }, - "7": { - "then": "Grünabfälle können hier recycelt werden" - }, - "8": { - "then": "Bio-Abfall kann hier recycelt werden" - }, - "9": { - "then": "Glasflaschen können hier recycelt werden" - }, "10": { "then": "Glas kann hier recycelt werden" }, @@ -8016,6 +8130,9 @@ "19": { "then": "Schuhe können hier recycelt werden" }, + "2": { + "then": "Dosen können hier recycelt werden" + }, "20": { "then": "Elektrokleingeräte können hier recycelt werden" }, @@ -8030,6 +8147,27 @@ }, "24": { "then": "Fahrräder können hier recycelt werden" + }, + "3": { + "then": "Kleidung kann hier recycelt werden" + }, + "4": { + "then": "Speiseöl kann hier recycelt werden" + }, + "5": { + "then": "Motoröl kann hier recycelt werden" + }, + "6": { + "then": "Hier können Leuchtstoffröhren recycelt werden" + }, + "7": { + "then": "Grünabfälle können hier recycelt werden" + }, + "8": { + "then": "Bio-Abfall kann hier recycelt werden" + }, + "9": { + "then": "Glasflaschen können hier recycelt werden" } }, "question": "Was kann hier recycelt werden?" @@ -8563,13 +8701,123 @@ "render": "Geschwindigkeitsreduzierte Straße" } }, - "souvenir_note": { + "souvenir_coin": { + "description": "Ebene mit Automaten, die Souvenir-Münzen verkaufen", + "name": "Souvenir-Münzautomaten", + "presets": { + "0": { + "description": "Hinzufügen eines Automaten für Souvenirmünzen", + "title": "ein Souvenir-Münzautomaten" + } + }, "tagRenderings": { + "charge": { + "freeform": { + "placeholder": "Kosten (z.B. 2 EUR)" + }, + "mappings": { + "0": { + "then": "Eine Souvenirmünze kostet 2 Euro" + } + }, + "question": "Wie viel kostet eine Souvenirmünze?", + "render": "Eine Souvenirmünze kostet {charge}" + }, + "designs": { + "override": { + "mappings": { + "0": { + "then": "Diese Maschine hat ein Design zur Verfügung" + }, + "1": { + "then": "Diese Maschine hat zwei Designs verfügbar" + }, + "2": { + "then": "Diese Maschine hat drei Designs verfügbar" + }, + "3": { + "then": "Diese Maschine hat vier Designs verfügbar" + } + }, + "render": "Diese Maschine hat {coin:design_count} Designs verfügbar" + } + }, + "indoor": { + "mappings": { + "0": { + "then": "Diese Maschine befindet sich im Innenbereich." + }, + "1": { + "then": "Diese Maschine befindet sich im Freien." + } + }, + "question": "Ist diese Maschine im Innenbereich?" + } + }, + "title": { + "render": "Souvenir-Münzautomat" + } + }, + "souvenir_note": { + "description": "Ebene zeigt Maschinen, die Souvenir-Banknoten verkaufen", + "name": "Souvenir Banknotenmaschinen", + "presets": { + "0": { + "description": "Hinzufügen eines Automaten, der Souvenir-Banknoten verkauft", + "title": "eine Souvenir-Banknotenmaschine" + } + }, + "tagRenderings": { + "charge": { + "freeform": { + "placeholder": "Kosten (z.B. 2 EUR)" + }, + "mappings": { + "0": { + "then": "Eine Souvenirnote kostet 2 Euro" + }, + "1": { + "then": "Eine Souvenirnote kostet 3 Euro" + } + }, + "question": "Wie viel kostet eine Souvenirnote?", + "render": "Eine Souvenirnote kostet {charge}" + }, "designs": { "freeform": { "placeholder": "Motivanzahl (z.B. 5)" - } + }, + "mappings": { + "0": { + "then": "Diese Maschine hat ein Design zur Verfügung." + }, + "1": { + "then": "Diese Maschine hat zwei Designs verfügbar." + }, + "2": { + "then": "Diese Maschine hat drei Designs verfügbar." + }, + "3": { + "then": "Diese Maschine hat vier Designs verfügbar." + } + }, + "question": "Wie viele Designs sind verfügbar?", + "render": "Diese Maschine verfügt über {note:design_count} Designs." + }, + "indoor": { + "mappings": { + "0": { + "then": "Diese Maschine befindet sich im Innenbereich." + }, + "1": { + "then": "Diese Maschine befindet sich im Freien." + } + }, + "question": "Ist diese Maschine im Innenbereich?" } + }, + "title": { + "render": "Souvenir-Banknotenautomat" } }, "speed_camera": { @@ -8981,6 +9229,12 @@ "1": { "then": "Diese Straßenlaterne verwendet LEDs" }, + "10": { + "then": "Diese Straßenlaterne verwendet Hochdruck-Natriumdampflampen (orange mit weiß)" + }, + "11": { + "then": "Diese Straßenlaterne wird mit Gas beleuchtet" + }, "2": { "then": "Diese Straßenlaterne verwendet Glühlampenlicht" }, @@ -9004,12 +9258,6 @@ }, "9": { "then": "Diese Straßenlaterne verwendet Niederdruck-Natriumdampflampen (einfarbig orange)" - }, - "10": { - "then": "Diese Straßenlaterne verwendet Hochdruck-Natriumdampflampen (orange mit weiß)" - }, - "11": { - "then": "Diese Straßenlaterne wird mit Gas beleuchtet" } }, "question": "Mit welcher Art von Beleuchtung arbeitet diese Straßenlaterne?" @@ -9072,6 +9320,11 @@ "render": "Stripclub" } }, + "summary": { + "title": { + "render": "Übersicht" + } + }, "surveillance_camera": { "description": "Diese Ebene zeigt die Überwachungskameras an und ermöglicht es, Informationen zu aktualisieren und neue Kameras hinzuzufügen", "name": "Überwachungskameras", @@ -10399,30 +10652,6 @@ "1": { "question": "Verkauf von Getränken" }, - "2": { - "question": "Verkauf von Süßigkeiten" - }, - "3": { - "question": "Verkauf von Lebensmitteln" - }, - "4": { - "question": "Verkauf von Zigaretten" - }, - "5": { - "question": "Verkauf von Kondomen" - }, - "6": { - "question": "Verkauf von Kaffee" - }, - "7": { - "question": "Verkauf von Trinkwasser" - }, - "8": { - "question": "Verkauf von Zeitungen" - }, - "9": { - "question": "Verkauf von Fahrradschläuchen" - }, "10": { "question": "Verkauf von Milch" }, @@ -10453,6 +10682,9 @@ "19": { "question": "Verkauf von Blumen" }, + "2": { + "question": "Verkauf von Süßigkeiten" + }, "20": { "question": "Verkauf von Parkscheinen" }, @@ -10476,6 +10708,27 @@ }, "27": { "question": "Verkauf von Fahrradschlössern" + }, + "3": { + "question": "Verkauf von Lebensmitteln" + }, + "4": { + "question": "Verkauf von Zigaretten" + }, + "5": { + "question": "Verkauf von Kondomen" + }, + "6": { + "question": "Verkauf von Kaffee" + }, + "7": { + "question": "Verkauf von Trinkwasser" + }, + "8": { + "question": "Verkauf von Zeitungen" + }, + "9": { + "question": "Verkauf von Fahrradschläuchen" } } } @@ -10522,30 +10775,6 @@ "1": { "then": "Süßigkeiten werden verkauft" }, - "2": { - "then": "Lebensmittel werden verkauft" - }, - "3": { - "then": "Zigaretten werden verkauft" - }, - "4": { - "then": "Kondome werden verkauft" - }, - "5": { - "then": "Kaffee wird verkauft" - }, - "6": { - "then": "Trinkwasser wird verkauft" - }, - "7": { - "then": "Zeitungen werden verkauft" - }, - "8": { - "then": "Fahrradschläuche werden verkauft" - }, - "9": { - "then": "Milch wird verkauft" - }, "10": { "then": "Brot wird verkauft" }, @@ -10576,6 +10805,9 @@ "19": { "then": "Parkscheine werden verkauft" }, + "2": { + "then": "Lebensmittel werden verkauft" + }, "20": { "then": "Souvenirmünzen werden verkauft" }, @@ -10596,6 +10828,27 @@ }, "26": { "then": "Fahrradschlösser werden verkauft" + }, + "3": { + "then": "Zigaretten werden verkauft" + }, + "4": { + "then": "Kondome werden verkauft" + }, + "5": { + "then": "Kaffee wird verkauft" + }, + "6": { + "then": "Trinkwasser wird verkauft" + }, + "7": { + "then": "Zeitungen werden verkauft" + }, + "8": { + "then": "Fahrradschläuche werden verkauft" + }, + "9": { + "then": "Milch wird verkauft" } }, "question": "Was wird in diesem Automaten verkauft?", @@ -10887,4 +11140,4 @@ "render": "Windrad" } } -} \ No newline at end of file +} From 88fabd4d36cedbb1a40cd824a56be55cb5a7d83c Mon Sep 17 00:00:00 2001 From: Berete Baba Date: Fri, 1 Mar 2024 21:24:05 +0000 Subject: [PATCH 016/213] Translated using Weblate (French) Currently translated at 59.0% (2000 of 3387 strings) Translation: MapComplete/Layer translations Translate-URL: https://hosted.weblate.org/projects/mapcomplete/layers/fr/ --- langs/layers/fr.json | 294 +++++++++++++++++++++---------------------- 1 file changed, 147 insertions(+), 147 deletions(-) diff --git a/langs/layers/fr.json b/langs/layers/fr.json index d9c150f98..710d51ef2 100644 --- a/langs/layers/fr.json +++ b/langs/layers/fr.json @@ -33,6 +33,16 @@ "1": { "title": "un panneau à affiches scellé au sol" }, + "10": { + "description": "Désigne une enseigne publicitaire, une enseigne néon, les logos ou des indications d'entrées", + "title": "une enseigne" + }, + "11": { + "title": "une sculpture" + }, + "12": { + "title": "une peinture murale" + }, "2": { "title": "un panneau à affiches monté sur un mur" }, @@ -59,16 +69,6 @@ }, "9": { "title": "un totem" - }, - "10": { - "description": "Désigne une enseigne publicitaire, une enseigne néon, les logos ou des indications d'entrées", - "title": "une enseigne" - }, - "11": { - "title": "une sculpture" - }, - "12": { - "title": "une peinture murale" } }, "tagRenderings": { @@ -160,6 +160,9 @@ "1": { "then": "C'est un petit panneau" }, + "10": { + "then": "C'est une peinture murale" + }, "2": { "then": "C'est une colonne" }, @@ -183,9 +186,6 @@ }, "9": { "then": "C'est un totem" - }, - "10": { - "then": "C'est une peinture murale" } }, "question": "De quel type de dispositif publicitaire s'agit-il ?" @@ -196,6 +196,9 @@ "1": { "then": "Petit panneau" }, + "10": { + "then": "Peinture murale" + }, "3": { "then": "Colonne" }, @@ -216,9 +219,6 @@ }, "9": { "then": "Totem" - }, - "10": { - "then": "Peinture murale" } } } @@ -300,6 +300,15 @@ "1": { "then": "Peinture murale" }, + "10": { + "then": "Azulejo (faïence latine)" + }, + "11": { + "then": "Carrelage" + }, + "12": { + "then": "Sculpture sur bois" + }, "2": { "then": "Peinture" }, @@ -323,15 +332,6 @@ }, "9": { "then": "Relief" - }, - "10": { - "then": "Azulejo (faïence latine)" - }, - "11": { - "then": "Carrelage" - }, - "12": { - "then": "Sculpture sur bois" } }, "question": "Quel est le type de cette œuvre d'art ?", @@ -1644,7 +1644,7 @@ }, "2": { "description": "Un café pour prendre un thé, un café ou une boisson alcoolisée dans un environnement calme", - "title": "un café" + "title": "un café adama" }, "3": { "description": "Une boîte de nuit ou discothèque pour danser sur de la musique de DJ accompagnée de jeux de lumière et un bar pour prendre une boisson (alcoolisée)", @@ -2408,6 +2408,15 @@ "1": { "then": "Cette piste cyclable est goudronée" }, + "10": { + "then": "Cette piste cyclable est faite en graviers fins" + }, + "11": { + "then": "Cette piste cyclable est en cailloux" + }, + "12": { + "then": "Cette piste cyclable est faite en sol brut" + }, "2": { "then": "Cette piste cyclable est asphaltée" }, @@ -2431,15 +2440,6 @@ }, "9": { "then": "Cette piste cyclable est faite en graviers" - }, - "10": { - "then": "Cette piste cyclable est faite en graviers fins" - }, - "11": { - "then": "Cette piste cyclable est en cailloux" - }, - "12": { - "then": "Cette piste cyclable est faite en sol brut" } }, "question": "De quoi est faite la surface de la piste cyclable ?", @@ -2488,6 +2488,15 @@ "1": { "then": "Cette piste cyclable est pavée" }, + "10": { + "then": "Cette piste cyclable est faite en graviers fins" + }, + "11": { + "then": "Cette piste cyclable est en cailloux" + }, + "12": { + "then": "Cette piste cyclable est faite en sol brut" + }, "2": { "then": "Cette piste cyclable est asphaltée" }, @@ -2511,15 +2520,6 @@ }, "9": { "then": "Cette piste cyclable est faite en graviers" - }, - "10": { - "then": "Cette piste cyclable est faite en graviers fins" - }, - "11": { - "then": "Cette piste cyclable est en cailloux" - }, - "12": { - "then": "Cette piste cyclable est faite en sol brut" } }, "question": "De quel materiel est faite cette rue ?", @@ -3351,6 +3351,21 @@ "1": { "then": "C'est une friterie" }, + "10": { + "then": "Des plats chinois sont servis ici" + }, + "11": { + "then": "Des plats grecs sont servis ici" + }, + "12": { + "then": "Des plats indiens sont servis ici" + }, + "13": { + "then": "Des plats turcs sont servis ici" + }, + "14": { + "then": "Des plats thaïlandais sont servis ici" + }, "2": { "then": "Restaurant Italien" }, @@ -3374,21 +3389,6 @@ }, "9": { "then": "Des plats français sont servis ici" - }, - "10": { - "then": "Des plats chinois sont servis ici" - }, - "11": { - "then": "Des plats grecs sont servis ici" - }, - "12": { - "then": "Des plats indiens sont servis ici" - }, - "13": { - "then": "Des plats turcs sont servis ici" - }, - "14": { - "then": "Des plats thaïlandais sont servis ici" } }, "question": "Quelle type de nourriture est servie ici ?", @@ -4933,30 +4933,6 @@ "1": { "question": "Recyclage de piles et batteries domestiques" }, - "2": { - "question": "Recyclage d'emballage de boissons" - }, - "3": { - "question": "Recyclage de boites de conserve et de canettes" - }, - "4": { - "question": "Recyclage de vêtements" - }, - "5": { - "question": "Recyclage des huiles de friture" - }, - "6": { - "question": "Recyclage des huiles de moteur" - }, - "7": { - "question": "Recyclage des lampes fluorescentes" - }, - "8": { - "question": "Recyclage des déchets verts" - }, - "9": { - "question": "Recyclage des bouteilles en verre et des bocaux" - }, "10": { "question": "Recyclage de tout type de verre" }, @@ -4987,11 +4963,35 @@ "19": { "question": "Recyclage des autres déchets" }, + "2": { + "question": "Recyclage d'emballage de boissons" + }, "20": { "question": "Recyclage des cartouches d'imprimante" }, "21": { "question": "Recyclage des vélos" + }, + "3": { + "question": "Recyclage de boites de conserve et de canettes" + }, + "4": { + "question": "Recyclage de vêtements" + }, + "5": { + "question": "Recyclage des huiles de friture" + }, + "6": { + "question": "Recyclage des huiles de moteur" + }, + "7": { + "question": "Recyclage des lampes fluorescentes" + }, + "8": { + "question": "Recyclage des déchets verts" + }, + "9": { + "question": "Recyclage des bouteilles en verre et des bocaux" } } }, @@ -5054,30 +5054,6 @@ "1": { "then": "Les briques alimentaires en carton peuvent être recyclées ici" }, - "2": { - "then": "Les boites de conserve et canettes peuvent être recyclées ici" - }, - "3": { - "then": "Les vêtements peuvent être recyclés ici" - }, - "4": { - "then": "Les huiles de friture peuvent être recyclées ici" - }, - "5": { - "then": "Les huiles de moteur peuvent être recyclées ici" - }, - "6": { - "then": "Les lampes fluorescentes peuvent être recyclées ici" - }, - "7": { - "then": "Les déchets verts peuvent être recyclés ici" - }, - "8": { - "then": "Les déchets organiques peuvent être recyclés ici" - }, - "9": { - "then": "Les bouteilles en verre et bocaux peuvent être recyclés ici" - }, "10": { "then": "Tout type de verre peut être recyclé ici" }, @@ -5108,6 +5084,9 @@ "19": { "then": "Les chaussures peuvent être recyclées ici" }, + "2": { + "then": "Les boites de conserve et canettes peuvent être recyclées ici" + }, "20": { "then": "Les petits appareils électriques peuvent être recyclés ici" }, @@ -5122,6 +5101,27 @@ }, "24": { "then": "Les vélos peuvent être recyclés ici" + }, + "3": { + "then": "Les vêtements peuvent être recyclés ici" + }, + "4": { + "then": "Les huiles de friture peuvent être recyclées ici" + }, + "5": { + "then": "Les huiles de moteur peuvent être recyclées ici" + }, + "6": { + "then": "Les lampes fluorescentes peuvent être recyclées ici" + }, + "7": { + "then": "Les déchets verts peuvent être recyclés ici" + }, + "8": { + "then": "Les déchets organiques peuvent être recyclés ici" + }, + "9": { + "then": "Les bouteilles en verre et bocaux peuvent être recyclés ici" } }, "question": "Que peut-on recycler ici ?" @@ -6428,6 +6428,27 @@ "1": { "question": "Vente de boissons" }, + "10": { + "question": "Vente de lait" + }, + "11": { + "question": "Vente de pain" + }, + "12": { + "question": "Vente d'œufs" + }, + "13": { + "question": "Vente de fromage" + }, + "14": { + "question": "Vente de miel" + }, + "15": { + "question": "Vente de pommes de terre" + }, + "19": { + "question": "Vente de fleurs" + }, "2": { "question": "Ventre de confiseries" }, @@ -6451,27 +6472,6 @@ }, "9": { "question": "Vente de chambres à air pour vélo" - }, - "10": { - "question": "Vente de lait" - }, - "11": { - "question": "Vente de pain" - }, - "12": { - "question": "Vente d'œufs" - }, - "13": { - "question": "Vente de fromage" - }, - "14": { - "question": "Vente de miel" - }, - "15": { - "question": "Vente de pommes de terre" - }, - "19": { - "question": "Vente de fleurs" } } } @@ -6512,6 +6512,24 @@ "1": { "then": "Vent des confiseries" }, + "10": { + "then": "Vent du pain" + }, + "11": { + "then": "Vent des œufs" + }, + "12": { + "then": "Vent du fromage" + }, + "13": { + "then": "Vent du miel" + }, + "14": { + "then": "Vent des pommes de terre" + }, + "18": { + "then": "Vent des fleurs" + }, "2": { "then": "Vent de la nourriture" }, @@ -6535,24 +6553,6 @@ }, "9": { "then": "Vent du lait" - }, - "10": { - "then": "Vent du pain" - }, - "11": { - "then": "Vent des œufs" - }, - "12": { - "then": "Vent du fromage" - }, - "13": { - "then": "Vent du miel" - }, - "14": { - "then": "Vent des pommes de terre" - }, - "18": { - "then": "Vent des fleurs" } }, "question": "Que vent ce distributeur ?", @@ -6755,4 +6755,4 @@ "render": "éolienne" } } -} \ No newline at end of file +} From 3a708eba5b10d6c67a03f58210661ad43673f49f Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 4 Mar 2024 13:38:30 +0100 Subject: [PATCH 017/213] Fix #1803 --- assets/themes/street_lighting/street_lighting.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/themes/street_lighting/street_lighting.json b/assets/themes/street_lighting/street_lighting.json index 98ff87cc2..380c3a7ae 100644 --- a/assets/themes/street_lighting/street_lighting.json +++ b/assets/themes/street_lighting/street_lighting.json @@ -358,6 +358,10 @@ { "if": "lit=no", "then": "#303030" + }, + { + "if": "lit=yes", + "then": "#ff0" } ] } From 46a298cc20a24947b373850ba7300d22e5092a62 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 4 Mar 2024 13:41:32 +0100 Subject: [PATCH 018/213] Refactoring: some cleanup --- src/UI/Map/ShowDataLayer.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/UI/Map/ShowDataLayer.ts b/src/UI/Map/ShowDataLayer.ts index 295d1fac1..255df6676 100644 --- a/src/UI/Map/ShowDataLayer.ts +++ b/src/UI/Map/ShowDataLayer.ts @@ -276,7 +276,6 @@ class LineRenderingLayer { }, } const filter = img.if?.asMapboxExpression() - console.log(">>>", this._layername, imgId, img.if, "-->", filter) if (filter) { spec.filter = filter } @@ -552,14 +551,13 @@ export default class ShowDataLayer { } private initDrawFeatures(map: MlMap) { - let { features, doShowLayer, fetchStore, selectedElement, selectedLayer } = this._options + let { features, doShowLayer, fetchStore, selectedElement } = this._options const onClick = this._options.onClick ?? (this._options.layer.title === undefined ? undefined : (feature: Feature) => { selectedElement?.setData(feature) - selectedLayer?.setData(this._options.layer) }) if (this._options.drawLines !== false) { for (let i = 0; i < this._options.layer.lineRendering.length; i++) { From 52be661629b15bd4cb753546daf439028e4f36bc Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 4 Mar 2024 13:47:00 +0100 Subject: [PATCH 019/213] Add link to documentation in testing mode to the title element --- .../BigComponents/SelectedElementTitle.svelte | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/UI/BigComponents/SelectedElementTitle.svelte b/src/UI/BigComponents/SelectedElementTitle.svelte index be5aeaf32..084f5e725 100644 --- a/src/UI/BigComponents/SelectedElementTitle.svelte +++ b/src/UI/BigComponents/SelectedElementTitle.svelte @@ -13,12 +13,15 @@ export let layer: LayerConfig export let selectedElement: Feature let tags: UIEventSource> = state.featureProperties.getStore( - selectedElement.properties.id + selectedElement.properties.id, ) $: { tags = state.featureProperties.getStore(selectedElement.properties.id) } + let isTesting = state.featureSwitchIsTesting + let isDebugging = state.featureSwitches.featureSwitchIsDebugging + let metatags: Store> = state.userRelatedState.preferencesAsTags @@ -39,7 +42,7 @@ class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 pt-0.5 sm:pt-1" > {#each layer.titleIcons as titleIconConfig} - {#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ...$metatags, ...$tags } ) ?? true) && titleIconConfig.IsKnown($tags)} + {#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...$metatags, ...$tags }) ?? true) && titleIconConfig.IsKnown($tags)}
{/if} {/each} + + {#if $isTesting || $isDebugging} + {layer.id} + {/if}
@@ -68,7 +75,7 @@ {/if} From 48159b25f76f4a7df3e9d7fe31f459d5414f44e2 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 4 Mar 2024 15:31:09 +0100 Subject: [PATCH 020/213] UX: fix #1805, disable zoom-in and zoom-out buttons when maxrange reached --- src/Models/ThemeViewState.ts | 14 ++ src/UI/Base/MapControlButton.svelte | 4 +- src/UI/Base/SubtleButton.svelte | 20 +-- .../NewPointLocationInput.svelte | 6 +- src/UI/BigComponents/WaySplitMap.svelte | 15 +- .../InputElement/Helpers/LocationInput.svelte | 2 + src/UI/Map/SmallZoomButtons.svelte | 29 ++++ src/UI/Popup/SplitRoadWizard.svelte | 112 +++++++++++++ src/UI/Popup/SplitRoadWizard.ts | 147 ------------------ src/UI/SpecialVisualization.ts | 4 + src/UI/SpecialVisualizations.ts | 23 ++- src/UI/ThemeViewGUI.svelte | 5 +- src/Utils.ts | 5 +- 13 files changed, 202 insertions(+), 184 deletions(-) create mode 100644 src/UI/Map/SmallZoomButtons.svelte create mode 100644 src/UI/Popup/SplitRoadWizard.svelte delete mode 100644 src/UI/Popup/SplitRoadWizard.ts diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 50fe06d71..d798ebb8c 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -439,6 +439,19 @@ export default class ThemeViewState implements SpecialVisualizationState { this.selectedElement.setData(feature) } + public showCurrentLocationOn(map: Store): ShowDataLayer { + const id = "gps_location" + const flayerGps = this.layerState.filteredLayers.get(id) + const features = this.geolocation.currentUserLocation + return new ShowDataLayer(map, { + features, + doShowLayer: flayerGps.isDisplayed, + layer: flayerGps.layerDef, + metaTags: this.userRelatedState.preferencesAsTags, + selectedElement: this.selectedElement, + }) + } + /** * Various small methods that need to be called */ @@ -671,6 +684,7 @@ export default class ThemeViewState implements SpecialVisualizationState { ) return new SummaryTileSourceRewriter(summaryTileSource, this.layerState.filteredLayers) } + /** * Add the special layers to the map */ diff --git a/src/UI/Base/MapControlButton.svelte b/src/UI/Base/MapControlButton.svelte index 38122911f..55bc60626 100644 --- a/src/UI/Base/MapControlButton.svelte +++ b/src/UI/Base/MapControlButton.svelte @@ -3,12 +3,14 @@ import { twJoin } from "tailwind-merge" import { Translation } from "../i18n/Translation" import { ariaLabel } from "../../Utils/ariaLabel" + import { ImmutableStore, Store } from "../../Logic/UIEventSource" /** * A round button with an icon and possible a small text, which hovers above the map */ const dispatch = createEventDispatcher() export let cls = "m-0.5 p-0.5 sm:p-1 md:m-1" + export let enabled : Store = new ImmutableStore(true) export let arialabel: Translation = undefined @@ -16,7 +18,7 @@ on:click={(e) => dispatch("click", e)} on:keydown use:ariaLabel={arialabel} - class={twJoin("pointer-events-auto relative h-fit w-fit rounded-full", cls)} + class={twJoin("pointer-events-auto relative h-fit w-fit rounded-full", cls, $enabled ? "" : "disabled")} > diff --git a/src/UI/Base/SubtleButton.svelte b/src/UI/Base/SubtleButton.svelte index 06431a4da..76a2fe63d 100644 --- a/src/UI/Base/SubtleButton.svelte +++ b/src/UI/Base/SubtleButton.svelte @@ -1,31 +1,17 @@ diff --git a/src/UI/BigComponents/NewPointLocationInput.svelte b/src/UI/BigComponents/NewPointLocationInput.svelte index 95d1b72cb..0996f095e 100644 --- a/src/UI/BigComponents/NewPointLocationInput.svelte +++ b/src/UI/BigComponents/NewPointLocationInput.svelte @@ -53,9 +53,6 @@ lat: number }>(undefined) - const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>() - - const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16) const map: UIEventSource = new UIEventSource(undefined) let initialMapProperties: Partial & { location } = { zoom: new UIEventSource(19), @@ -73,6 +70,7 @@ minzoom: new UIEventSource(18), rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer), } + state?.showCurrentLocationOn(map) if (targetLayer) { const featuresForLayer = state.perLayer.get(targetLayer.id) @@ -120,7 +118,7 @@ dispatch("click", data)} + on:click mapProperties={initialMapProperties} value={preciseLocation} initialCoordinate={coordinate} diff --git a/src/UI/BigComponents/WaySplitMap.svelte b/src/UI/BigComponents/WaySplitMap.svelte index d45fd2c2a..b6b4afd53 100644 --- a/src/UI/BigComponents/WaySplitMap.svelte +++ b/src/UI/BigComponents/WaySplitMap.svelte @@ -23,18 +23,20 @@ import { GeoOperations } from "../../Logic/GeoOperations" import { BBox } from "../../Logic/BBox" import type { Feature, LineString, Point } from "geojson" + import type { SpecialVisualizationState } from "../SpecialVisualization" + import SmallZoomButtons from "../Map/SmallZoomButtons.svelte" const splitpoint_style = new LayerConfig( split_point, "(BUILTIN) SplitRoadWizard.ts", - true - ) as const + true, + ) const splitroad_style = new LayerConfig( split_road, "(BUILTIN) SplitRoadWizard.ts", - true - ) as const + true, + ) /** * The way to focus on @@ -45,6 +47,7 @@ * A default is given */ export let layer: LayerConfig = splitroad_style + export let state: SpecialVisualizationState | undefined = undefined /** * Optional: use these properties to set e.g. background layer */ @@ -58,6 +61,7 @@ adaptor.bounds.setData(BBox.get(wayGeojson).pad(2)) adaptor.maxbounds.setData(BBox.get(wayGeojson).pad(2)) + state?.showCurrentLocationOn(map) new ShowDataLayer(map, { features: new StaticFeatureSource([wayGeojson]), drawMarkers: false, @@ -101,6 +105,7 @@ }) -
+
+
diff --git a/src/UI/InputElement/Helpers/LocationInput.svelte b/src/UI/InputElement/Helpers/LocationInput.svelte index 3aad6bf8f..3f4570a9c 100644 --- a/src/UI/InputElement/Helpers/LocationInput.svelte +++ b/src/UI/InputElement/Helpers/LocationInput.svelte @@ -13,6 +13,7 @@ import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import { createEventDispatcher, onDestroy } from "svelte" import Move_arrows from "../../../assets/svg/Move_arrows.svelte" + import SmallZoomButtons from "../../Map/SmallZoomButtons.svelte" /** * A visualisation to pick a location on a map background @@ -95,4 +96,5 @@
+
diff --git a/src/UI/Map/SmallZoomButtons.svelte b/src/UI/Map/SmallZoomButtons.svelte new file mode 100644 index 000000000..4fc58fa88 --- /dev/null +++ b/src/UI/Map/SmallZoomButtons.svelte @@ -0,0 +1,29 @@ + +
+ adaptor.zoom.update((z) => z + 1)} + > + + + adaptor.zoom.update((z) => z - 1)} + > + + +
diff --git a/src/UI/Popup/SplitRoadWizard.svelte b/src/UI/Popup/SplitRoadWizard.svelte new file mode 100644 index 000000000..c1b464d99 --- /dev/null +++ b/src/UI/Popup/SplitRoadWizard.svelte @@ -0,0 +1,112 @@ + + + + + + + {#if step === "deleted"} + + {:else if step === "initial"} + + {:else if step === "loading_way"} + + + {:else if step === "splitting"} +
+
+ +
+
+ { + splitPoints.set([]) + step = "initial" + }}> + + + doSplit()}> + + +
+ +
+ {:else if step === "has_been_split"} + + + {/if} + +
+ + diff --git a/src/UI/Popup/SplitRoadWizard.ts b/src/UI/Popup/SplitRoadWizard.ts deleted file mode 100644 index ac4b76f7d..000000000 --- a/src/UI/Popup/SplitRoadWizard.ts +++ /dev/null @@ -1,147 +0,0 @@ -import Toggle from "../Input/Toggle" -import { UIEventSource } from "../../Logic/UIEventSource" -import { SubtleButton } from "../Base/SubtleButton" -import Combine from "../Base/Combine" -import { Button } from "../Base/Button" -import Translations from "../i18n/Translations" -import SplitAction from "../../Logic/Osm/Actions/SplitAction" -import Title from "../Base/Title" -import BaseUIElement from "../BaseUIElement" -import { VariableUiElement } from "../Base/VariableUIElement" -import { LoginToggle } from "./LoginButton" -import SvelteUIElement from "../Base/SvelteUIElement" -import WaySplitMap from "../BigComponents/WaySplitMap.svelte" -import { Feature, Point } from "geojson" -import { WayId } from "../../Models/OsmFeature" -import { OsmConnection } from "../../Logic/Osm/OsmConnection" -import { Changes } from "../../Logic/Osm/Changes" -import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader" -import Scissors from "../../assets/svg/Scissors.svelte" - -export default class SplitRoadWizard extends Combine { - public dialogIsOpened: UIEventSource - - /** - * A UI Element used for splitting roads - * - * @param id The id of the road to remove - * @param state the state of the application - */ - constructor( - id: WayId, - state: { - layout?: LayoutConfig - osmConnection?: OsmConnection - osmObjectDownloader?: OsmObjectDownloader - changes?: Changes - indexedFeatures?: IndexedFeatureSource - selectedElement?: UIEventSource - } - ) { - const t = Translations.t.split - - // Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring - const splitPoints = new UIEventSource[]>([]) - - const hasBeenSplit = new UIEventSource(false) - - // Toggle variable between show split button and map - const splitClicked = new UIEventSource(false) - - const leafletMap = new UIEventSource(undefined) - - function initMap() { - ;(async function ( - id: WayId, - splitPoints: UIEventSource - ): Promise { - return new SvelteUIElement(WaySplitMap, { - osmWay: await state.osmObjectDownloader.DownloadObjectAsync(id), - splitPoints, - }) - })(id, splitPoints).then((mapComponent) => - leafletMap.setData(mapComponent.SetClass("w-full h-80")) - ) - } - - // Toggle between splitmap - const splitButton = new SubtleButton( - new SvelteUIElement(Scissors).SetClass("h-6 w-6"), - new Toggle( - t.splitAgain.Clone().SetClass("text-lg font-bold"), - t.inviteToSplit.Clone().SetClass("text-lg font-bold"), - hasBeenSplit - ) - ) - - const splitToggle = new LoginToggle(splitButton, t.loginToSplit.Clone(), state) - - // Save button - const saveButton = new Button(t.split.Clone(), async () => { - hasBeenSplit.setData(true) - splitClicked.setData(false) - const splitAction = new SplitAction( - id, - splitPoints.data.map((ff) => <[number, number]>(ff.geometry).coordinates), - { - theme: state?.layout?.id, - }, - 5 - ) - await state.changes?.applyAction(splitAction) - // We throw away the old map and splitpoints, and create a new map from scratch - splitPoints.setData([]) - - // Close the popup. The contributor has to select a segment again to make sure they continue editing the correct segment; see #1219 - state.selectedElement?.setData(undefined) - }) - - saveButton.SetClass("btn btn-primary mr-3") - const disabledSaveButton = new Button(t.split.Clone(), undefined) - disabledSaveButton.SetClass("btn btn-disabled mr-3") - // Only show the save button if there are split points defined - const saveToggle = new Toggle( - disabledSaveButton, - saveButton, - splitPoints.map((data) => data.length === 0) - ) - - const cancelButton = Translations.t.general.cancel - .Clone() // Not using Button() element to prevent full width button - .SetClass("btn btn-secondary mr-3") - .onClick(() => { - splitPoints.setData([]) - splitClicked.setData(false) - }) - - cancelButton.SetClass("btn btn-secondary block") - - const splitTitle = new Title(t.splitTitle) - - const mapView = new Combine([ - splitTitle, - new VariableUiElement(leafletMap), - new Combine([cancelButton, saveToggle]).SetClass("flex flex-row"), - ]) - mapView.SetClass("question") - super([ - Toggle.If(hasBeenSplit, () => - t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full") - ), - new Toggle(mapView, splitToggle, splitClicked), - ]) - splitClicked.addCallback((view) => { - if (view) { - initMap() - } - }) - this.dialogIsOpened = splitClicked - const self = this - splitButton.onClick(() => { - splitClicked.setData(true) - self.ScrollIntoView() - }) - } -} diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index d377fc833..ff0547eb5 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -22,6 +22,8 @@ import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider" import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" import { SummaryTileSourceRewriter } from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource" import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" +import { Map as MlMap } from "maplibre-gl" +import ShowDataLayer from "./Map/ShowDataLayer" /** * The state needed to render a special Visualisation. @@ -86,6 +88,8 @@ export interface SpecialVisualizationState { readonly previewedImage: UIEventSource readonly geolocation: GeoLocationHandler + + showCurrentLocationOn(map: Store): ShowDataLayer } export interface SpecialVisualization { diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index e353fcdd9..38a08f116 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -46,8 +46,6 @@ import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte" import UserProfile from "./BigComponents/UserProfile.svelte" import LayerConfig from "../Models/ThemeConfig/LayerConfig" import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" -import { WayId } from "../Models/OsmFeature" -import SplitRoadWizard from "./Popup/SplitRoadWizard" import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz" import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte" import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte" @@ -93,6 +91,7 @@ import SpecialVisualisationUtils from "./SpecialVisualisationUtils" import LoginButton from "./Base/LoginButton.svelte" import Toggle from "./Input/Toggle" import ImportReviewIdentity from "./Reviews/ImportReviewIdentity.svelte" +import SplitRoadWizard from "./Popup/SplitRoadWizard.svelte" class NearbyImageVis implements SpecialVisualization { // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests @@ -432,7 +431,7 @@ export default class SpecialVisualizations { .map((tags) => tags.id) .map((id) => { if (id.startsWith("way/")) { - return new SplitRoadWizard(id, state) + return new SvelteUIElement(SplitRoadWizard, { id, state }) } return undefined }) @@ -741,12 +740,20 @@ export default class SpecialVisualizations { { funcName: "import_mangrove_key", docs: "Only makes sense in the usersettings. Allows to import a mangrove public key and to use this to make reviews", - args: [{ - name: "text", - doc: "The text that is shown on the button", - }], + args: [ + { + name: "text", + doc: "The text that is shown on the button", + }, + ], needsUrls: [], - constr(state: SpecialVisualizationState, tagSource: UIEventSource>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement { + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + argument: string[], + feature: Feature, + layer: LayerConfig + ): BaseUIElement { const [text] = argument return new SvelteUIElement(ImportReviewIdentity, { state, text }) }, diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 4cd7ea378..0a2b6806e 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -118,7 +118,8 @@ let viewport: UIEventSource = new UIEventSource(undefined) let mapproperties: MapProperties = state.mapProperties state.mapProperties.installCustomKeyboardHandler(viewport) - + let canZoomIn = mapproperties.maxzoom.map(mz => mapproperties.zoom.data < mz, [mapproperties.zoom] ) + let canZoomOut = mapproperties.minzoom.map(mz => mapproperties.zoom.data > mz, [mapproperties.zoom] ) function updateViewport() { const rect = viewport.data?.getBoundingClientRect() if (!rect) { @@ -329,12 +330,14 @@ mapproperties.zoom.update((z) => z + 1)} on:keydown={forwardEventToMap} > mapproperties.zoom.update((z) => z - 1)} on:keydown={forwardEventToMap} diff --git a/src/Utils.ts b/src/Utils.ts index 4f099ed2e..bba793510 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1390,7 +1390,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be d.setUTCMinutes(0) } - public static scrollIntoView(element: HTMLBaseElement | HTMLDivElement) { + public static scrollIntoView(element: HTMLBaseElement | HTMLDivElement): void { + if (!element) { + return + } // Is the element completely in the view? const parentRect = Utils.findParentWithScrolling(element)?.getBoundingClientRect() if (!parentRect) { From 4aefd43be7e665913d9ce5445f6b9c41f33e8775 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 5 Mar 2024 00:16:17 +0100 Subject: [PATCH 021/213] Typing: add 'readonly' to tag method, add 'applyOn' convenience method --- src/Logic/Tags/And.ts | 2 +- src/Logic/Tags/ComparingTag.ts | 5 +++-- src/Logic/Tags/Or.ts | 2 +- src/Logic/Tags/SubstitutingTag.ts | 5 +++-- src/Logic/Tags/TagsFilter.ts | 19 ++++++++++++++----- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Logic/Tags/And.ts b/src/Logic/Tags/And.ts index 88be5afcc..57c6ac3ba 100644 --- a/src/Logic/Tags/And.ts +++ b/src/Logic/Tags/And.ts @@ -159,7 +159,7 @@ export class And extends TagsFilter { return [].concat(...this.and.map((subkeys) => subkeys.usedTags())) } - asChange(properties: Record): { k: string; v: string }[] { + asChange(properties: Readonly>): { k: string; v: string }[] { const result = [] for (const tagsFilter of this.and) { result.push(...tagsFilter.asChange(properties)) diff --git a/src/Logic/Tags/ComparingTag.ts b/src/Logic/Tags/ComparingTag.ts index 861507c95..42a26c497 100644 --- a/src/Logic/Tags/ComparingTag.ts +++ b/src/Logic/Tags/ComparingTag.ts @@ -3,7 +3,7 @@ import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import { Tag } from "./Tag" import { ExpressionSpecification } from "maplibre-gl" -export default class ComparingTag implements TagsFilter { +export default class ComparingTag extends TagsFilter { private readonly _key: string private readonly _predicate: (value: string) => boolean private readonly _representation: "<" | ">" | "<=" | ">=" @@ -15,13 +15,14 @@ export default class ComparingTag implements TagsFilter { representation: "<" | ">" | "<=" | ">=", boundary: string ) { + super() this._key = key this._predicate = predicate this._representation = representation this._boundary = boundary } - asChange(_: Record): { k: string; v: string }[] { + asChange(_: Readonly>): { k: string; v: string }[] { throw "A comparable tag can not be used to be uploaded to OSM" } diff --git a/src/Logic/Tags/Or.ts b/src/Logic/Tags/Or.ts index bd19c0794..871361f67 100644 --- a/src/Logic/Tags/Or.ts +++ b/src/Logic/Tags/Or.ts @@ -96,7 +96,7 @@ export class Or extends TagsFilter { return [].concat(...this.or.map((subkeys) => subkeys.usedTags())) } - asChange(properties: Record): { k: string; v: string }[] { + asChange(properties: Readonly>): { k: string; v: string }[] { const result = [] for (const tagsFilter of this.or) { result.push(...tagsFilter.asChange(properties)) diff --git a/src/Logic/Tags/SubstitutingTag.ts b/src/Logic/Tags/SubstitutingTag.ts index bf13dc7c3..acdda6835 100644 --- a/src/Logic/Tags/SubstitutingTag.ts +++ b/src/Logic/Tags/SubstitutingTag.ts @@ -13,12 +13,13 @@ import { ExpressionSpecification } from "maplibre-gl" * The 'key' is always fixed and should not contain substitutions. * This cannot be used to query features */ -export default class SubstitutingTag implements TagsFilter { +export default class SubstitutingTag extends TagsFilter { private readonly _key: string private readonly _value: string private readonly _invert: boolean constructor(key: string, value: string, invert = false) { + super() this._key = key this._value = value this._invert = invert @@ -99,7 +100,7 @@ export default class SubstitutingTag implements TagsFilter { return [] } - asChange(properties: Record): { k: string; v: string }[] { + asChange(properties: Readonly>): { k: string; v: string }[] { if (this._invert) { throw "An inverted substituting tag can not be used to create a change" } diff --git a/src/Logic/Tags/TagsFilter.ts b/src/Logic/Tags/TagsFilter.ts index c75b7f6f4..5c6070755 100644 --- a/src/Logic/Tags/TagsFilter.ts +++ b/src/Logic/Tags/TagsFilter.ts @@ -15,9 +15,9 @@ export abstract class TagsFilter { abstract matchesProperties(properties: Record): boolean abstract asHumanString( - linkToWiki: boolean, - shorten: boolean, - properties: Record + linkToWiki?: boolean, + shorten?: boolean, + properties?: Record ): string abstract asJson(): TagConfigJson @@ -34,9 +34,18 @@ export abstract class TagsFilter { * Converts the tagsFilter into a list of key-values that should be uploaded to OSM. * Throws an error if not applicable. * - * Note: properties are the already existing tags-object. It is only used in the substituting tag + * @param properties are the already existing tags-object. It is only used in the substituting tag and will not be changed */ - abstract asChange(properties: Record): { k: string; v: string }[] + abstract asChange(properties: Readonly>): { k: string; v: string }[] + + public applyOn(properties: Readonly>): Record { + const copy = { ...properties } + const changes = this.asChange(properties) + for (const { k, v } of changes) { + copy[k] = v + } + return copy + } /** * Returns an optimized version (or self) of this tagsFilter From 68ea4e3d9f255e989cf158159b434ed6bfc6f55a Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 5 Mar 2024 00:17:03 +0100 Subject: [PATCH 022/213] Fix: a freeform key will mimick a mapping if appropriate (and avoid the 'addExtraTags') --- src/Models/ThemeConfig/TagRenderingConfig.ts | 21 +++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Models/ThemeConfig/TagRenderingConfig.ts b/src/Models/ThemeConfig/TagRenderingConfig.ts index cb1396748..d3c1a8bd0 100644 --- a/src/Models/ThemeConfig/TagRenderingConfig.ts +++ b/src/Models/ThemeConfig/TagRenderingConfig.ts @@ -628,6 +628,14 @@ export default class TagRenderingConfig { * config.constructChangeSpecification("", undefined, undefined, {}) // => undefined * config.constructChangeSpecification("5", undefined, undefined, {}).optimize() // => new Tag("capacity", "5") * + * // Should pick a mapping, even if freeform is used + * const config = new TagRenderingConfig({"id": "shop-types", render: "Shop type is {shop}", freeform: {key: "shop", addExtraTags:["fixme=freeform shop type used"]}, mappings:[{if: "shop=second_hand", then: "Second hand shop"}]}) + * config.constructChangeSpecification("freeform", 1, undefined, {}).asHumanString(false, false, {}) // => "shop=freeform & fixme=freeform shop type used" + * config.constructChangeSpecification("freeform", undefined, undefined, {}).asHumanString(false, false, {}) // => "shop=freeform & fixme=freeform shop type used" + * config.constructChangeSpecification("second_hand", 1, undefined, {}).asHumanString(false, false, {}) // => "shop=second_hand" + * + * + * * @param freeformValue The freeform value which will be applied as 'freeform.key'. Ignored if 'freeform.key' is not set * * @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform @@ -667,11 +675,22 @@ export default class TagRenderingConfig { this.mappings.length == 0 || (singleSelectedMapping === this.mappings.length && !this.multiAnswer)) ) { + const freeformOnly = { [this.freeform.key]: freeformValue } + const matchingMapping = this.mappings?.find((m) => m.if.matchesProperties(freeformOnly)) + if (matchingMapping) { + return new And([matchingMapping.if, ...(matchingMapping.addExtraTags ?? [])]) + } // Either no mappings, or this is a radio-button selected freeform value - return new And([ + const tag = new And([ new Tag(this.freeform.key, freeformValue), ...(this.freeform.addExtraTags ?? []), ]) + const newProperties = tag.applyOn(currentProperties) + if (this.invalidValues?.matchesProperties(newProperties)) { + return undefined + } + + return tag } if (this.multiAnswer) { From 99af2f31b6d28449983875ed02fd89278f70baf5 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 5 Mar 2024 00:18:26 +0100 Subject: [PATCH 023/213] Fix #1808 --- .../Popup/TagRendering/TagRenderingQuestion.svelte | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte b/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte index cb41c8aa0..f156ea9b9 100644 --- a/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte +++ b/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte @@ -241,7 +241,7 @@
onSave()} + on:submit|preventDefault={() =>{ /*onSave(); This submit is not needed and triggers to early, causing bugs: see #1808*/}} >
@@ -285,7 +285,7 @@ feature={selectedElement} value={freeformInput} unvalidatedText={freeformInputUnvalidated} - on:submit={onSave} + on:submit={() => onSave()} /> {:else if mappings !== undefined && !config.multiAnswer} @@ -329,7 +329,7 @@ value={freeformInput} unvalidatedText={freeformInputUnvalidated} on:selected={() => (selectedMapping = config.mappings?.length)} - on:submit={onSave} + on:submit={() => onSave()} /> {/if} @@ -372,7 +372,7 @@ feature={selectedElement} value={freeformInput} unvalidatedText={freeformInputUnvalidated} - on:submit={onSave} + on:submit={() => onSave()} /> {/if} @@ -397,13 +397,13 @@ {#if allowDeleteOfFreeform && (mappings?.length ?? 0) === 0 && $freeformInput === undefined && $freeformInputUnvalidated === ""} - {:else}
- + From 39afc645725cee3f97858e1adbac1a13cd507ae4 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 7 Mar 2024 17:36:50 +0100 Subject: [PATCH 045/213] Attempt to fix build --- scripts/generateLayouts.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 54cf8e66e..58b716f42 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -12,7 +12,6 @@ import SpecialVisualizations from "../src/UI/SpecialVisualizations" import Constants from "../src/Models/Constants" import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers" import { ImmutableStore } from "../src/Logic/UIEventSource" -import * as crypto from "crypto" import * as eli from "../src/assets/editor-layer-index.json" import * as eli_global from "../src/assets/global-raster-layers.json" import ValidationUtils from "../src/Models/ThemeConfig/Conversion/ValidationUtils" @@ -374,15 +373,6 @@ async function generateCsp( ].join("\n") } -const removeOtherLanguages = readFileSync("./src/UI/RemoveOtherLanguages.js", "utf8") - .split("\n") - .map((s) => s.trim()) - .join("\n") -const removeOtherLanguagesHash = crypto - .createHash("sha256") - .update(removeOtherLanguages) - .digest("base64") - async function createLandingPage( layout: LayoutConfig, layoutJson: LayoutConfigJson, @@ -461,9 +451,6 @@ async function createLandingPage( const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title }) const templateLines = template.split("\n") - const removeOtherLanguagesReference = templateLines.find( - (line) => line.indexOf("./src/UI/RemoveOtherLanguages.js") >= 0 - ) let output = template .replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1")) .replace( @@ -474,10 +461,9 @@ async function createLandingPage( .replace( //, await generateCsp(layout, layoutJson, { - scriptSrcs: [`'sha256-${removeOtherLanguagesHash}'`], + scriptSrcs: [], }) ) - .replace(removeOtherLanguagesReference, "") .replace( /.*/s, asLangSpan(layout.shortDescription) From a118c295d641852584bbd16f07c419a3fa896591 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 8 Mar 2024 17:51:50 +0100 Subject: [PATCH 046/213] Fix: enable settings again, fix #1815 --- src/UI/BigComponents/SelectedElementView.svelte | 3 ++- src/UI/ThemeViewGUI.svelte | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UI/BigComponents/SelectedElementView.svelte b/src/UI/BigComponents/SelectedElementView.svelte index 5965e7745..91ca81b5d 100644 --- a/src/UI/BigComponents/SelectedElementView.svelte +++ b/src/UI/BigComponents/SelectedElementView.svelte @@ -8,6 +8,7 @@ import Translations from "../i18n/Translations" import Tr from "../Base/Tr.svelte" import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" + import UserRelatedState from "../../Logic/State/UserRelatedState" export let state: SpecialVisualizationState export let selectedElement: Feature @@ -17,7 +18,7 @@ selectedElement.properties.id ) - let layer: LayerConfig =state.layout.getMatchingLayer(tags.data) + let layer: LayerConfig = selectedElement.properties.id === "settings" ? UserRelatedState.usersettingsConfig : state.layout.getMatchingLayer(tags.data) let stillMatches = tags.map(tags => !layer?.source?.osmTags || layer.source.osmTags?.matchesProperties(tags)) diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 0a2b6806e..69dc3a8b3 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -583,10 +583,9 @@
Date: Sun, 10 Mar 2024 23:29:18 +0100 Subject: [PATCH 047/213] Themes: allow to move a shelter --- assets/layers/shelter/shelter.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/layers/shelter/shelter.json b/assets/layers/shelter/shelter.json index 5b2f7a77a..3569cd53d 100644 --- a/assets/layers/shelter/shelter.json +++ b/assets/layers/shelter/shelter.json @@ -120,5 +120,9 @@ "type": "string" } } - ] + ], + "allowMove": { + "enableRelocation": false, + "enableImproveAccuracy": true + } } From 39c079db45aaadf98bf651d66d6bd88dbbe04bb2 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 10 Mar 2024 23:29:43 +0100 Subject: [PATCH 048/213] Themes: allow to add pictures to a 'shelter' --- assets/layers/shelter/shelter.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/layers/shelter/shelter.json b/assets/layers/shelter/shelter.json index 3569cd53d..79cc2fae9 100644 --- a/assets/layers/shelter/shelter.json +++ b/assets/layers/shelter/shelter.json @@ -45,6 +45,7 @@ ], "lineRendering": [], "tagRenderings": [ + "images", { "id": "shelter-type", "mappings": [ From e36e594b89796fdd5173b4cb676a28bc4e406a82 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 11 Mar 2024 00:01:44 +0100 Subject: [PATCH 049/213] Repair 'fake user' functionality --- src/Logic/Osm/OsmConnection.ts | 12 ++++----- src/Logic/Osm/OsmPreferences.ts | 43 ++++++++++++++++++++------------- src/UI/ThemeViewGUI.svelte | 3 +++ 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/Logic/Osm/OsmConnection.ts b/src/Logic/Osm/OsmConnection.ts index 4c6d028de..b97bc4473 100644 --- a/src/Logic/Osm/OsmConnection.ts +++ b/src/Logic/Osm/OsmConnection.ts @@ -91,9 +91,11 @@ export class OsmConnection { ud.name = "Fake user" ud.totalMessages = 42 ud.languages = ["en"] + this.loadingStatus.setData("logged-in") } const self = this this.UpdateCapabilities() + this.isLoggedIn = this.userDetails.map( (user) => user.loggedIn && @@ -112,10 +114,7 @@ export class OsmConnection { this.updateAuthObject() - this.preferencesHandler = new OsmPreferences( - this.auth, - this - ) + this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser) if (options.oauth_token?.data !== undefined) { console.log(options.oauth_token.data) @@ -554,13 +553,12 @@ export class OsmConnection { } private UpdateCapabilities(): void { - const self = this if (this.fakeUser) { return } this.FetchCapabilities().then(({ api, gpx }) => { - self.apiIsOnline.setData(api) - self.gpxServiceIsOnline.setData(gpx) + this.apiIsOnline.setData(api) + this.gpxServiceIsOnline.setData(gpx) }) } diff --git a/src/Logic/Osm/OsmPreferences.ts b/src/Logic/Osm/OsmPreferences.ts index 1035dec82..597dd61ea 100644 --- a/src/Logic/Osm/OsmPreferences.ts +++ b/src/Logic/Osm/OsmPreferences.ts @@ -2,6 +2,9 @@ import { UIEventSource } from "../UIEventSource" import UserDetails, { OsmConnection } from "./OsmConnection" import { Utils } from "../../Utils" import { LocalStorageSource } from "../Web/LocalStorageSource" +// @ts-ignore +import { osmAuth } from "osm-auth" +import OSMAuthInstance = OSMAuth.OSMAuthInstance export class OsmPreferences { /** @@ -17,16 +20,17 @@ export class OsmPreferences { * @private */ private readonly preferenceSources = new Map>() - private auth: any + private readonly auth: OSMAuthInstance private userDetails: UIEventSource private longPreferences = {} + private readonly _fakeUser: boolean - constructor(auth, osmConnection: OsmConnection) { + constructor(auth: OSMAuthInstance, osmConnection: OsmConnection, fakeUser: boolean = false) { this.auth = auth + this._fakeUser = fakeUser this.userDetails = osmConnection.userDetails - const self = this osmConnection.OnLoggedIn(() => { - self.UpdatePreferences(true) + this.UpdatePreferences(true) return true }) } @@ -212,8 +216,21 @@ export class OsmPreferences { }) } + removeAllWithPrefix(prefix: string) { + for (const key in this.preferences.data) { + if (key.startsWith(prefix)) { + this.GetPreference(key, "", { prefix: "" }).setData(undefined) + console.log("Clearing preference", key) + } + } + this.preferences.ping() + } + private UpdatePreferences(forceUpdate?: boolean) { const self = this + if (this._fakeUser) { + return + } this.auth.xhr( { method: "GET", @@ -272,13 +289,15 @@ export class OsmPreferences { } const self = this console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15)) - + if (this._fakeUser) { + return + } if (v === undefined || v === "") { this.auth.xhr( { method: "DELETE", path: "/api/0.6/user/preferences/" + encodeURIComponent(k), - options: { header: { "Content-Type": "text/plain" } }, + headers: { "Content-Type": "text/plain" }, }, function (error) { if (error) { @@ -297,7 +316,7 @@ export class OsmPreferences { { method: "PUT", path: "/api/0.6/user/preferences/" + encodeURIComponent(k), - options: { header: { "Content-Type": "text/plain" } }, + headers: { "Content-Type": "text/plain" }, content: v, }, function (error) { @@ -311,14 +330,4 @@ export class OsmPreferences { } ) } - - removeAllWithPrefix(prefix: string) { - for (const key in this.preferences.data) { - if (key.startsWith(prefix)) { - this.GetPreference(key, "", { prefix: "" }).setData(undefined) - console.log("Clearing preference", key) - } - } - this.preferences.ping() - } } diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 69dc3a8b3..2305e0d31 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -257,6 +257,9 @@
Testmode
+ +
Faking a user (Testmode)
+
From 6394ee8e6800d2cd23d30383ffea9629964c4dc9 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 11 Mar 2024 01:17:33 +0100 Subject: [PATCH 050/213] Fix: fix #1817, some more improvements to the loading screen --- scripts/generateLayouts.ts | 1062 ++++++++++++++++++------------------ theme.html | 6 +- 2 files changed, 545 insertions(+), 523 deletions(-) diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 58b716f42..adf069f5b 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -17,403 +17,420 @@ import * as eli_global from "../src/assets/global-raster-layers.json" import ValidationUtils from "../src/Models/ThemeConfig/Conversion/ValidationUtils" import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson" import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" +import Script from "./Script" +import crypto from "crypto" const sharp = require("sharp") -const template = readFileSync("theme.html", "utf8") -let codeTemplate = readFileSync("src/index_theme.ts.template", "utf8") -function enc(str: string): string { - return encodeURIComponent(str.toLowerCase()) -} +class GenerateLayouts extends Script { + private readonly template = readFileSync("theme.html", "utf8") + private readonly codeTemplate = readFileSync("src/index_theme.ts.template", "utf8") + private readonly removeOtherLanguages = readFileSync("src/UI/RemoveOtherLanguages.ts", "utf8") + .split("\n") + .slice(1) + .map((s) => s.trim()) + .filter((s) => s !== "") + .join("\n") + private readonly removeOtherLanguagesHash = + "sha256-" + crypto.createHash("sha256").update(this.removeOtherLanguages).digest("base64") + private previousSrc: Set = new Set() + private eliUrlsCached: string[] + private date = new Date().toISOString() -async function createIcon(iconPath: string, size: number, alreadyWritten: string[]) { - let name = iconPath.split(".").slice(0, -1).join(".") // drop svg suffix - if (name.startsWith("./")) { - name = name.substr(2) + constructor() { + super("Generates an '.html' and 'index_.ts' for every theme") } - const newname = `assets/generated/images/${name.replace(/\//g, "_")}${size}.png` - const targetpath = `public/${newname}` - if (alreadyWritten.indexOf(newname) >= 0) { - return newname + enc(str: string): string { + return encodeURIComponent(str.toLowerCase()) } - alreadyWritten.push(newname) - if (existsSync(targetpath)) { + + async createIcon(iconPath: string, size: number, alreadyWritten: string[]) { + let name = iconPath.split(".").slice(0, -1).join(".") // drop svg suffix + if (name.startsWith("./")) { + name = name.substring(2) + } + + const newname = `assets/generated/images/${name.replace(/\//g, "_")}${size}.png` + const targetpath = `public/${newname}` + if (alreadyWritten.indexOf(newname) >= 0) { + return newname + } + alreadyWritten.push(newname) + if (existsSync(targetpath)) { + return newname + } + + if (!existsSync(iconPath)) { + throw "No file at " + iconPath + } + + try { + // We already read to file, in order to crash here if the file is not found + let img = await sharp(iconPath) + let resized = await img.resize(size) + await resized.toFile(targetpath) + console.log("Created png version at ", newname) + } catch (e) { + console.error("Could not read icon", iconPath, " to create a PNG due to", e) + } + return newname } - if (!existsSync(iconPath)) { - throw "No file at " + iconPath - } - - try { - // We already read to file, in order to crash here if the file is not found - let img = await sharp(iconPath) - let resized = await img.resize(size) - await resized.toFile(targetpath) - console.log("Created png version at ", newname) - } catch (e) { - console.error("Could not read icon", iconPath, " to create a PNG due to", e) - } - - return newname -} - -async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): Promise { - if (!layout.icon.endsWith(".svg")) { - console.warn( - "Not creating a social image for " + - layout.id + - " as it is _not_ a .svg: " + - layout.icon + async createSocialImage(layout: LayoutConfig, template: "" | "Wide"): Promise { + if (!layout.icon.endsWith(".svg")) { + console.warn( + "Not creating a social image for " + + layout.id + + " as it is _not_ a .svg: " + + layout.icon + ) + return undefined + } + const path = `./public/assets/generated/images/social_image_${layout.id}_${template}.svg` + if (existsSync(path)) { + return path + } + const svg = await ScriptUtils.ReadSvg(layout.icon) + let width: string = svg.$.width + if (width === undefined) { + throw "The logo at " + layout.icon + " does not have a defined width" + } + if (width?.endsWith("px")) { + width = width.substring(0, width.length - 2) + } + if (width?.endsWith("%")) { + throw "The logo at " + layout.icon + " has a relative width; this is not supported" + } + delete svg["defs"] + delete svg["$"] + let templateSvg = await ScriptUtils.ReadSvg( + "./public/assets/SocialImageTemplate" + template + ".svg" ) - return undefined - } - const path = `./public/assets/generated/images/social_image_${layout.id}_${template}.svg` - if (existsSync(path)) { + templateSvg = Utils.WalkJson( + templateSvg, + (leaf) => { + const { cx, cy, r } = leaf["circle"][0].$ + return { + $: { + id: "icon", + transform: `translate(${cx - r},${cy - r}) scale(${ + (r * 2) / Number(width) + }) `, + }, + g: [svg], + } + }, + (mightBeTokenToReplace) => { + if (mightBeTokenToReplace?.circle === undefined) { + return false + } + return mightBeTokenToReplace.circle[0]?.$?.style?.indexOf("fill:#ff00ff") >= 0 + } + ) + + const builder = new xml2js.Builder() + const xml = builder.buildObject({ svg: templateSvg }) + writeFileSync(path, xml) + console.log("Created social image at ", path) return path } - const svg = await ScriptUtils.ReadSvg(layout.icon) - let width: string = svg.$.width - if (width === undefined) { - throw "The logo at " + layout.icon + " does not have a defined width" - } - if (width?.endsWith("px")) { - width = width.substring(0, width.length - 2) - } - if (width?.endsWith("%")) { - throw "The logo at " + layout.icon + " has a relative width; this is not supported" - } - delete svg["defs"] - delete svg["$"] - let templateSvg = await ScriptUtils.ReadSvg( - "./public/assets/SocialImageTemplate" + template + ".svg" - ) - templateSvg = Utils.WalkJson( - templateSvg, - (leaf) => { - const { cx, cy, r } = leaf["circle"][0].$ - return { - $: { - id: "icon", - transform: `translate(${cx - r},${cy - r}) scale(${(r * 2) / Number(width)}) `, - }, - g: [svg], + + async createManifest( + layout: LayoutConfig, + alreadyWritten: string[] + ): Promise<{ + manifest: any + whiteIcons: string[] + }> { + Translation.forcedLanguage = "en" + const icons = [] + + const whiteIcons: string[] = [] + let icon = layout.icon + if (icon.endsWith(".svg") || icon.startsWith(" { - if (mightBeTokenToReplace?.circle === undefined) { - return false + + let path = layout.icon + if (layout.icon.startsWith("<")) { + // THis is already the svg + path = "./public/assets/generated/images/" + layout.id + "_logo.svg" + writeFileSync(path, layout.icon) } - return mightBeTokenToReplace.circle[0]?.$?.style?.indexOf("fill:#ff00ff") >= 0 - } - ) - const builder = new xml2js.Builder() - const xml = builder.buildObject({ svg: templateSvg }) - writeFileSync(path, xml) - console.log("Created social image at ", path) - return path -} - -async function createManifest( - layout: LayoutConfig, - alreadyWritten: string[] -): Promise<{ - manifest: any - whiteIcons: string[] -}> { - Translation.forcedLanguage = "en" - const icons = [] - - const whiteIcons: string[] = [] - let icon = layout.icon - if (icon.endsWith(".svg") || icon.startsWith("${t.translations[lang]}`) - } - return values.join("\n") -} - -let previousSrc: Set = new Set() - -let eliUrlsCached: string[] - -async function eliUrls(): Promise { - if (eliUrlsCached) { - return eliUrlsCached - } - const urls: string[] = [] - const regex = /{switch:([^}]+)}/ - const rasterLayers = [ - AvailableRasterLayers.maptilerDefaultLayer, - ...eli.features, - ...eli_global.layers.map((properties) => ({ properties })), - ] - for (const feature of rasterLayers) { - const f = feature - const url = f.properties.url - const match = url.match(regex) - if (match) { - const domains = match[1].split(",") - const subpart = match[0] - urls.push(...domains.map((d) => url.replace(subpart, d))) } else { - urls.push(url) + console.log(icon) + throw "Icon is not an svg for " + layout.id } + const ogTitle = Translations.T(layout.title).txt + const ogDescr = Translations.T(layout.description ?? "").txt - if (f.properties.type === "vector") { - // We also need to whitelist eventual sources - const styleSpec = await Utils.downloadJsonCached(f.properties.url, 1000 * 120) - for (const key of Object.keys(styleSpec.sources)) { - const url = styleSpec.sources[key].url - if (!url) { - continue - } - let urlClipped = url - if (url.indexOf("?") > 0) { - urlClipped = url?.substring(0, url.indexOf("?")) - } - console.log("Source url ", key, url) - urls.push(url) - if (urlClipped.endsWith(".json")) { - const tileInfo = await Utils.downloadJsonCached(url, 1000 * 120) - urls.push(tileInfo["tiles"] ?? []) - } + const manifest = { + name: ogTitle, + short_name: ogTitle, + start_url: `${layout.id.toLowerCase()}.html`, + lang: "en", + display: "standalone", + background_color: "#fff", + description: ogDescr, + orientation: "portrait-primary, landscape-primary", + icons: icons, + categories: ["map", "navigation"], + } + return { + manifest, + whiteIcons, + } + } + + asLangSpan(t: Translation, tag = "span"): string { + const values: string[] = [] + for (const lang in t.translations) { + if (lang === "_context") { + continue } - urls.push(...(styleSpec["tiles"] ?? [])) - urls.push(styleSpec["sprite"]) - urls.push(styleSpec["glyphs"]) + values.push(`<${tag} lang="${lang}">${t.translations[lang]}`) } + return values.join("\n") } - eliUrlsCached = urls - return Utils.NoNull(urls).sort() -} -async function generateCsp( - layout: LayoutConfig, - layoutJson: LayoutConfigJson, - options: { - scriptSrcs: string[] - } -): Promise { - const apiUrls: string[] = [ - ...Constants.defaultOverpassUrls, - Constants.countryCoderEndpoint, - Constants.nominatimEndpoint, - "https://www.openstreetmap.org", - "https://api.openstreetmap.org", - "https://pietervdvn.goatcounter.com", - "https://cache.mapcomplete.org", - ].concat(...(await eliUrls())) - - SpecialVisualizations.specialVisualizations.forEach((sv) => { - if (typeof sv.needsUrls === "function") { - // Handled below - return + async eliUrls(): Promise { + if (this.eliUrlsCached) { + return this.eliUrlsCached } - apiUrls.push(...(sv.needsUrls ?? [])) - }) + const urls: string[] = [] + const regex = /{switch:([^}]+)}/ + const rasterLayers = [ + AvailableRasterLayers.maptilerDefaultLayer, + ...eli.features, + ...eli_global.layers.map((properties) => ({ properties })), + ] + for (const feature of rasterLayers) { + const f = feature + const url = f.properties.url + const match = url.match(regex) + if (match) { + const domains = match[1].split(",") + const subpart = match[0] + urls.push(...domains.map((d) => url.replace(subpart, d))) + } else { + urls.push(url) + } - const usedSpecialVisualisations = [].concat( - ...layoutJson.layers.map((l) => - ValidationUtils.getAllSpecialVisualisations( - (l).tagRenderings ?? [] + if (f.properties.type === "vector") { + // We also need to whitelist eventual sources + const styleSpec = await Utils.downloadJsonCached(f.properties.url, 1000 * 120) + for (const key of Object.keys(styleSpec.sources)) { + const url = styleSpec.sources[key].url + if (!url) { + continue + } + let urlClipped = url + if (url.indexOf("?") > 0) { + urlClipped = url?.substring(0, url.indexOf("?")) + } + console.log("Source url ", key, url) + urls.push(url) + if (urlClipped.endsWith(".json")) { + const tileInfo = await Utils.downloadJsonCached(url, 1000 * 120) + urls.push(tileInfo["tiles"] ?? []) + } + } + urls.push(...(styleSpec["tiles"] ?? [])) + urls.push(styleSpec["sprite"]) + urls.push(styleSpec["glyphs"]) + } + } + this.eliUrlsCached = urls + return Utils.NoNull(urls).sort() + } + + async generateCsp( + layout: LayoutConfig, + layoutJson: LayoutConfigJson, + options: { + scriptSrcs: string[] + } + ): Promise { + const apiUrls: string[] = [ + ...Constants.defaultOverpassUrls, + Constants.countryCoderEndpoint, + Constants.nominatimEndpoint, + "https://www.openstreetmap.org", + "https://api.openstreetmap.org", + "https://pietervdvn.goatcounter.com", + "https://cache.mapcomplete.org", + ].concat(...(await this.eliUrls())) + + SpecialVisualizations.specialVisualizations.forEach((sv) => { + if (typeof sv.needsUrls === "function") { + // Handled below + return + } + apiUrls.push(...(sv.needsUrls ?? [])) + }) + + const usedSpecialVisualisations = [].concat( + ...layoutJson.layers.map((l) => + ValidationUtils.getAllSpecialVisualisations( + (l).tagRenderings ?? [] + ) ) ) - ) - for (const usedSpecialVisualisation of usedSpecialVisualisations) { - if (typeof usedSpecialVisualisation === "string") { - continue - } - const neededUrls = usedSpecialVisualisation.func.needsUrls ?? [] - if (typeof neededUrls === "function") { - let needed: string | string[] = neededUrls(usedSpecialVisualisation.args) - if (typeof needed === "string") { - needed = [needed] + for (const usedSpecialVisualisation of usedSpecialVisualisations) { + if (typeof usedSpecialVisualisation === "string") { + continue } - apiUrls.push(...needed) - } - } - - const geojsonSources: string[] = layout.layers.map((l) => l.source?.geojsonSource) - const hosts = new Set() - const eliLayers: RasterLayerPolygon[] = AvailableRasterLayers.layersAvailableAt( - new ImmutableStore({ lon: 0, lat: 0 }) - ).data - const vectorLayers = eliLayers.filter((l) => l.properties.type === "vector") - const vectorSources = vectorLayers.map((l) => l.properties.url) - apiUrls.push(...vectorSources) - for (let connectSource of apiUrls.concat(geojsonSources)) { - if (!connectSource) { - continue - } - try { - if (!connectSource.startsWith("http")) { - connectSource = "https://" + connectSource + const neededUrls = usedSpecialVisualisation.func.needsUrls ?? [] + if (typeof neededUrls === "function") { + let needed: string | string[] = neededUrls(usedSpecialVisualisation.args) + if (typeof needed === "string") { + needed = [needed] + } + apiUrls.push(...needed) } - const url = new URL(connectSource) - hosts.add("https://" + url.host) - } catch (e) { - hosts.add(connectSource) } - } - if (hosts.has("*")) { - throw "* is not allowed as connect-src" - } - - const connectSrc = Array.from(hosts).sort() - - const newSrcs = connectSrc.filter((newItem) => !previousSrc.has(newItem)) - - console.log( - "Got", - hosts.size, - "connect-src items for theme", - layout.id, - "(extra sources: ", - newSrcs.join(" ") + ")" - ) - previousSrc = hosts - - const csp: Record = { - "default-src": "'self'", - "child-src": "'self' blob: ", - "img-src": "* data:", // maplibre depends on 'data:' to load - "connect-src": "'self' " + connectSrc.join(" "), - "report-to": "https://report.mapcomplete.org/csp", - "worker-src": "'self' blob:", // Vite somehow loads the worker via a 'blob' - "style-src": "'self' 'unsafe-inline'", // unsafe-inline is needed to change the default background pin colours - "script-src": ["'self'", "https://gc.zgo.at/count.js", ...(options?.scriptSrcs ?? [])].join( - " " - ), - } - const content = Object.keys(csp) - .map((k) => k + " " + csp[k]) - .join(" ; ") - - return [ - ``, - ``, - ].join("\n") -} - -async function createLandingPage( - layout: LayoutConfig, - layoutJson: LayoutConfigJson, - manifest, - whiteIcons, - alreadyWritten -) { - Locale.language.setData(layout.language[0]) - const targetLanguage = layout.language[0] - const ogTitle = Translations.T(layout.title).textFor(targetLanguage).replace(/"/g, '\\"') - const ogDescr = Translations.T( - layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap" - ) - .textFor(targetLanguage) - .replace(/"/g, '\\"') - let ogImage = layout.socialImage - let twitterImage = ogImage - if (ogImage === LayoutConfig.defaultSocialImage && layout.official) { - ogImage = (await createSocialImage(layout, "")) ?? layout.socialImage - twitterImage = (await createSocialImage(layout, "Wide")) ?? layout.socialImage - } - if (twitterImage.endsWith(".svg")) { - // svgs are badly supported as social image, we use a generated svg instead - twitterImage = await createIcon(twitterImage, 512, alreadyWritten) - } - - if (ogImage.endsWith(".svg")) { - ogImage = await createIcon(ogImage, 512, alreadyWritten) - } - - let customCss = "" - if (layout.customCss !== undefined && layout.customCss !== "") { - try { - const cssContent = readFileSync(layout.customCss) - customCss = "" - } catch (e) { - customCss = `` + const geojsonSources: string[] = layout.layers.map((l) => l.source?.geojsonSource) + const hosts = new Set() + const eliLayers: RasterLayerPolygon[] = AvailableRasterLayers.layersAvailableAt( + new ImmutableStore({ lon: 0, lat: 0 }) + ).data + const vectorLayers = eliLayers.filter((l) => l.properties.type === "vector") + const vectorSources = vectorLayers.map((l) => l.properties.url) + apiUrls.push(...vectorSources) + for (let connectSource of apiUrls.concat(geojsonSources)) { + if (!connectSource) { + continue + } + try { + if (!connectSource.startsWith("http")) { + connectSource = "https://" + connectSource + } + const url = new URL(connectSource) + hosts.add("https://" + url.host) + } catch (e) { + hosts.add(connectSource) + } } + + if (hosts.has("*")) { + throw "* is not allowed as connect-src" + } + + const connectSrc = Array.from(hosts).sort() + + const newSrcs = connectSrc.filter((newItem) => !this.previousSrc.has(newItem)) + + console.log( + "Got", + hosts.size, + "connect-src items for theme", + layout.id, + newSrcs.length > 0 ? "(extra sources: " + newSrcs.join(" ") + ")" : "" + ) + this.previousSrc = hosts + + const csp: Record = { + "default-src": "'self'", + "child-src": "'self' blob: ", + "img-src": "* data:", // maplibre depends on 'data:' to load + "connect-src": "'self' " + connectSrc.join(" "), + "report-to": "https://report.mapcomplete.org/csp", + "worker-src": "'self' blob:", // Vite somehow loads the worker via a 'blob' + "style-src": "'self' 'unsafe-inline'", // unsafe-inline is needed to change the default background pin colours + "script-src": [ + "'self'", + "https://gc.zgo.at/count.js", + ...(options?.scriptSrcs?.map((s) => "'" + s + "'") ?? []), + ].join(" "), + } + const content = Object.keys(csp) + .map((k) => k + " " + csp[k]) + .join(" ; ") + + return [ + ``, + ``, + ].join("\n") } - const og = ` + async createLandingPage( + layout: LayoutConfig, + layoutJson: LayoutConfigJson, + whiteIcons, + alreadyWritten + ) { + Locale.language.setData(layout.language[0]) + const targetLanguage = layout.language[0] + const ogTitle = Translations.T(layout.title).textFor(targetLanguage).replace(/"/g, '\\"') + const ogDescr = Translations.T( + layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap" + ) + .textFor(targetLanguage) + .replace(/"/g, '\\"') + let ogImage = layout.socialImage + let twitterImage = ogImage + if (ogImage === LayoutConfig.defaultSocialImage && layout.official) { + ogImage = (await this.createSocialImage(layout, "")) ?? layout.socialImage + twitterImage = (await this.createSocialImage(layout, "Wide")) ?? layout.socialImage + } + if (twitterImage.endsWith(".svg")) { + // svgs are badly supported as social image, we use a generated svg instead + twitterImage = await this.createIcon(twitterImage, 512, alreadyWritten) + } + + if (ogImage.endsWith(".svg")) { + ogImage = await this.createIcon(ogImage, 512, alreadyWritten) + } + + let customCss = "" + if (layout.customCss !== undefined && layout.customCss !== "") { + try { + const cssContent = readFileSync(layout.customCss) + customCss = "" + } catch (e) { + customCss = `` + } + } + + const og = ` @@ -424,174 +441,179 @@ async function createLandingPage( ` - let icon = layout.icon - if (icon.startsWith("`) - } - let themeSpecific = [ - `${ogTitle}`, - ``, - og, - customCss, - ``, - ...apple_icons, - ].join("\n") - - const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title }) - const templateLines = template.split("\n") - let output = template - .replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1")) - .replace( - "Made with OpenStreetMap", - Translations.t.general.poweredByOsm.textFor(targetLanguage) - ) - .replace(/.*/s, themeSpecific) - .replace( - //, - await generateCsp(layout, layoutJson, { - scriptSrcs: [], - }) - ) - .replace( - /.*/s, - asLangSpan(layout.shortDescription) - ) - .replace( - /.*/s, - "" - ) - - .replace( - /.*\/src\/index\.ts.*/, - `` - ) - .replace("Version", Constants.vNumber) - - return output -} - -async function createIndexFor(theme: LayoutConfig) { - const filename = "index_" + theme.id + ".ts" - - const imports = [ - `import layout from "./src/assets/generated/themes/${theme.id}.json"`, - `import { ThemeMetaTagging } from "./src/assets/generated/metatagging/${theme.id}"`, - ] - for (const layerName of Constants.added_by_default) { - imports.push(`import ${layerName} from "./src/assets/generated/layers/${layerName}.json"`) - } - writeFileSync(filename, imports.join("\n") + "\n") - - const addLayers = [] - - for (const layerName of Constants.added_by_default) { - addLayers.push(` layout.layers.push( ${layerName})`) - } - - codeTemplate = codeTemplate.replace(" // LAYOUT.ADD_LAYERS", addLayers.join("\n")) - - appendFileSync(filename, codeTemplate) -} - -function createDir(path) { - if (!existsSync(path)) { - mkdirSync(path) - } -} - -async function main(): Promise { - const alreadyWritten = [] - createDir("./public/assets/") - createDir("./public/assets/generated") - createDir("./public/assets/generated/images") - - const blacklist = [ - "", - "test", - ".", - "..", - "manifest", - "index", - "land", - "preferences", - "account", - "openstreetmap", - "custom", - "theme", - ] - // @ts-ignore - const all: LayoutConfigJson[] = all_known_layouts.themes - const args = process.argv - const theme = args[2] - if (theme !== undefined) { - console.warn("Only generating layout " + theme) - } - for (const i in all) { - const layoutConfigJson: LayoutConfigJson = all[i] - if (theme !== undefined && layoutConfigJson.id !== theme) { - continue - } - const layout = new LayoutConfig(layoutConfigJson, true) - const layoutName = layout.id - if (blacklist.indexOf(layoutName.toLowerCase()) >= 0) { - console.log(`Skipping a layout with name${layoutName}, it is on the blacklist`) - continue - } - const err = (err) => { - if (err !== null) { - console.log("Could not write manifest for ", layoutName, " because ", err) + const apple_icons = [] + for (const icon of whiteIcons) { + if (!existsSync(icon)) { + continue } + const size = icon.replace(/[^0-9]/g, "") + apple_icons.push(``) } - const { manifest, whiteIcons } = await createManifest(layout, alreadyWritten) - const manif = JSON.stringify(manifest, undefined, 2) - const manifestLocation = encodeURIComponent(layout.id.toLowerCase()) + ".webmanifest" - writeFile("public/" + manifestLocation, manif, err) - // Create a landing page for the given theme - const landing = await createLandingPage( - layout, - layoutConfigJson, - manifest, - whiteIcons, + let themeSpecific = [ + `${ogTitle}`, + ``, + og, + customCss, + ``, + ...apple_icons, + ].join("\n") + + const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title }) + // const templateLines: string[] = this.template.split("\n").slice(1) // Slice to remove the 'export {}'-line + + return this.template + .replace("Loading MapComplete, hang on...", this.asLangSpan(loadingText, "h1")) + .replace( + "Made with OpenStreetMap", + Translations.t.general.poweredByOsm.textFor(targetLanguage) + ) + .replace(/.*/s, themeSpecific) + .replace( + //, + await this.generateCsp(layout, layoutJson, { + scriptSrcs: [this.removeOtherLanguagesHash], + }) + ) + .replace( + /.*/s, + this.asLangSpan(layout.shortDescription) + ) + .replace( + /.*/s, + "" + ) + .replace( + /.*\/src\/index\.ts.*/, + `` + ) + + .replace( + /\n.*RemoveOtherLanguages.*\n/i, + "\n\n" + ) + .replace("Version", `${Constants.vNumber}
${this.date}
`) + } + + async createIndexFor(theme: LayoutConfig) { + const filename = "index_" + theme.id + ".ts" + + const imports = [ + `import layout from "./src/assets/generated/themes/${theme.id}.json"`, + `import { ThemeMetaTagging } from "./src/assets/generated/metatagging/${theme.id}"`, + ] + for (const layerName of Constants.added_by_default) { + imports.push( + `import ${layerName} from "./src/assets/generated/layers/${layerName}.json"` + ) + } + writeFileSync(filename, imports.join("\n") + "\n") + + const addLayers = [] + + for (const layerName of Constants.added_by_default) { + addLayers.push(` layout.layers.push( ${layerName})`) + } + + let codeTemplate = this.codeTemplate.replace( + " // LAYOUT.ADD_LAYERS", + addLayers.join("\n") + ) + + appendFileSync(filename, codeTemplate) + } + + createDir(path) { + if (!existsSync(path)) { + mkdirSync(path) + } + } + + async main(): Promise { + const alreadyWritten = [] + this.createDir("./public/assets/") + this.createDir("./public/assets/generated") + this.createDir("./public/assets/generated/images") + + const blacklist = [ + "", + "test", + ".", + "..", + "manifest", + "index", + "land", + "preferences", + "account", + "openstreetmap", + "custom", + "theme", + ] + // @ts-ignore + const all: LayoutConfigJson[] = all_known_layouts.themes + const args = process.argv + const theme = args[2] + if (theme !== undefined) { + console.warn("Only generating layout " + theme) + } + for (const i in all) { + const layoutConfigJson: LayoutConfigJson = all[i] + if (theme !== undefined && layoutConfigJson.id !== theme) { + continue + } + const layout = new LayoutConfig(layoutConfigJson, true) + const layoutName = layout.id + if (blacklist.indexOf(layoutName.toLowerCase()) >= 0) { + console.log(`Skipping a layout with name${layoutName}, it is on the blacklist`) + continue + } + const err = (err) => { + if (err !== null) { + console.log("Could not write manifest for ", layoutName, " because ", err) + } + } + const { manifest, whiteIcons } = await this.createManifest(layout, alreadyWritten) + const manif = JSON.stringify(manifest, undefined, 2) + const manifestLocation = encodeURIComponent(layout.id.toLowerCase()) + ".webmanifest" + writeFile("public/" + manifestLocation, manif, err) + + // Create a landing page for the given theme + const landing = await this.createLandingPage( + layout, + layoutConfigJson, + whiteIcons, + alreadyWritten + ) + + writeFile(this.enc(layout.id) + ".html", landing, err) + await this.createIndexFor(layout) + } + + const { manifest } = await this.createManifest( + new LayoutConfig({ + icon: "./assets/svg/mapcomplete_logo.svg", + id: "index", + layers: [], + socialImage: "assets/SocialImage.png", + startLat: 0, + startLon: 0, + startZoom: 0, + title: { en: "MapComplete" }, + description: { en: "A thematic map viewer and editor based on OpenStreetMap" }, + }), alreadyWritten ) - writeFile(enc(layout.id) + ".html", landing, err) - await createIndexFor(layout) + const manif = JSON.stringify(manifest, undefined, 2) + writeFileSync("public/index.webmanifest", manif) } - - const { manifest } = await createManifest( - new LayoutConfig({ - icon: "./assets/svg/mapcomplete_logo.svg", - id: "index", - layers: [], - socialImage: "assets/SocialImage.png", - startLat: 0, - startLon: 0, - startZoom: 0, - title: { en: "MapComplete" }, - description: { en: "A thematic map viewer and editor based on OpenStreetMap" }, - }), - alreadyWritten - ) - - const manif = JSON.stringify(manifest, undefined, 2) - writeFileSync("public/index.webmanifest", manif) } -ScriptUtils.fixUtils() -main().then(() => { - console.log("All done!") -}) +new GenerateLayouts().run() diff --git a/theme.html b/theme.html index 239c52c56..4dbf83f21 100644 --- a/theme.html +++ b/theme.html @@ -59,12 +59,12 @@

-
+
- + -
+
Version
From 8630a729e88d0a61726eaea6230f50e1ebee7797 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 11 Mar 2024 01:42:58 +0100 Subject: [PATCH 051/213] Performance: add preload --- theme.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/theme.html b/theme.html index 4dbf83f21..6d0d7051b 100644 --- a/theme.html +++ b/theme.html @@ -14,6 +14,15 @@ + + + + + + + + + MapComplete From 2100edd5910ab5fbf9c107ed6ef732a32a9cc395 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 11 Mar 2024 01:46:15 +0100 Subject: [PATCH 052/213] Refactoring: cleanup some legacy code, fix #1813#issuecomment-1987190633 --- index.html | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/index.html b/index.html index 750021b71..2a5d50f2a 100644 --- a/index.html +++ b/index.html @@ -12,8 +12,6 @@ - - MapComplete @@ -34,16 +32,6 @@ - - - - - @@ -55,21 +43,8 @@ - + + From 761280bdac028bf5d14ae81e650ee141dc167673 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 11 Mar 2024 14:29:15 +0100 Subject: [PATCH 053/213] Add branch name in version --- scripts/generateLayouts.ts | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index adf069f5b..c3fafbc96 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -36,6 +36,7 @@ class GenerateLayouts extends Script { private previousSrc: Set = new Set() private eliUrlsCached: string[] private date = new Date().toISOString() + private branchName: string = undefined constructor() { super("Generates an '.html' and 'index_.ts' for every theme") @@ -45,6 +46,27 @@ class GenerateLayouts extends Script { return encodeURIComponent(str.toLowerCase()) } + getBranchName(): Promise { + if (this.branchName) { + return Promise.resolve(this.branchName) + } + const { exec } = require("child_process") + return new Promise((resolve, reject) => { + exec("git rev-parse --abbrev-ref HEAD", (err, stdout, stderr) => { + if (err) { + reject(err) + return + } + + if (typeof stdout === "string") { + this.branchName = stdout.trim() + resolve(stdout.trim()) + } + reject("Did not get output") + }) + }) + } + async createIcon(iconPath: string, size: number, alreadyWritten: string[]) { let name = iconPath.split(".").slice(0, -1).join(".") // drop svg suffix if (name.startsWith("./")) { @@ -466,6 +488,13 @@ class GenerateLayouts extends Script { ...apple_icons, ].join("\n") + let branchname = await this.getBranchName() + if (branchname === "master" || branchname === "main") { + branchname = "" + } else { + branchname = "
" + branchname + "
" + } + const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title }) // const templateLines: string[] = this.template.split("\n").slice(1) // Slice to remove the 'export {}'-line @@ -488,7 +517,7 @@ class GenerateLayouts extends Script { ) .replace( /.*/s, - "" + "" ) .replace( /.*\/src\/index\.ts.*/, @@ -499,7 +528,10 @@ class GenerateLayouts extends Script { /\n.*RemoveOtherLanguages.*\n/i, "\n\n" ) - .replace("Version", `${Constants.vNumber}
${this.date}
`) + .replace( + "Version", + `${Constants.vNumber}
${this.date}
${branchname}` + ) } async createIndexFor(theme: LayoutConfig) { From 6563476c6564a62c749cc860f07f070113266f33 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 11 Mar 2024 16:00:22 +0100 Subject: [PATCH 054/213] UX: improve infobox for deleted items, fix #1632 --- langs/en.json | 1 + .../BigComponents/SelectedElementTitle.svelte | 39 ++++++++++--------- .../BigComponents/SelectedElementView.svelte | 34 +++++++++------- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/langs/en.json b/langs/en.json index 1cdc592ee..76b2aebd9 100644 --- a/langs/en.json +++ b/langs/en.json @@ -20,6 +20,7 @@ "cancel": "Cancel", "cannotBeDeleted": "This feature can not be deleted", "delete": "Delete", + "deletedTitle": "Deleted feature", "explanations": { "hardDelete": "This feature will be deleted in OpenStreetMap. It can be recovered by an experienced contributor", "retagNoOtherThemes": "This feature will be reclassified and hidden from this application", diff --git a/src/UI/BigComponents/SelectedElementTitle.svelte b/src/UI/BigComponents/SelectedElementTitle.svelte index 084f5e725..a00e58a72 100644 --- a/src/UI/BigComponents/SelectedElementTitle.svelte +++ b/src/UI/BigComponents/SelectedElementTitle.svelte @@ -21,15 +21,17 @@ let isTesting = state.featureSwitchIsTesting let isDebugging = state.featureSwitches.featureSwitchIsDebugging - + let metatags: Store> = state.userRelatedState.preferencesAsTags -{#if $tags._deleted === "yes"} - -{:else} -
-
+
+
+ {#if $tags._deleted === "yes"} +

+ +

+ {:else}

@@ -37,7 +39,6 @@

-
-
- - + {/if}
-{/if} + +