diff --git a/.gitignore b/.gitignore
index c5a6e0be26..7124fdc776 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ scratch
assets/editor-layer-index.json
assets/generated/*
src/assets/generated/
+assets/layers/favourite/favourite.json
public/*.webmanifest
/*.html
!/index.html
diff --git a/Docs/UserTests/2023-12-4 User Test Favourites.md b/Docs/UserTests/2023-12-4 User Test Favourites.md
new file mode 100644
index 0000000000..27231f9f29
--- /dev/null
+++ b/Docs/UserTests/2023-12-4 User Test Favourites.md
@@ -0,0 +1,29 @@
+
+## Task
+
+Add a (specified) feature as favourite
+Find and use the list of favourites
+Determine information from this list
+Open the popup from this list
+
+## Background info
+
+User has used mapcomplete before
+
+## Results
+
+The user is asked to mark a specified bicycle shop as favourite. They find the big button to mark as favourite at the bottom.
+
+When asked to select another feature, they choose a bicycle pump. When hinted that 'they can add this in a different way', they immediately select the heart title icon.
+
+When asked to open the list of favourites, they open the 'hamburger'-menu. After a bit of looking, they spot the 'Your favourites'-button.
+
+They are a bit confused. The specified bicycle shop is advertised as `building or wall`.
+
+The bicycle pump is shown correctly, the icons are clear. When asked to open the popup for one of them, they click directly on the link.
+
+## Surfaced issues
+
+Due to the way the title is generated, wrong titles appeared: all titles from all layers are mixed and used as title, if the tags match. As such, the title `building or wall` appeared, as it happened to be on top and the bicycle shop had a `building~*` tag.
+
+This was resolved by sorting those titles by popularity. The least occuring tags/titles are placed first, so that the most specific title is shown. This might, in some cases, still result in differing titles (e.g. if something is e.g. both a shop and a café), but this should be exceptional.
diff --git a/assets/layers/climbing_opportunity/climbing_opportunity.json b/assets/layers/climbing_opportunity/climbing_opportunity.json
index 89fd5d6795..11d7727910 100644
--- a/assets/layers/climbing_opportunity/climbing_opportunity.json
+++ b/assets/layers/climbing_opportunity/climbing_opportunity.json
@@ -29,7 +29,8 @@
"natural=stone"
]
},
- "climbing="
+ "climbing=",
+ "sport!=climbing"
]
}
},
diff --git a/assets/layers/favourite/favourite.proto.json b/assets/layers/favourite/favourite.proto.json
new file mode 100644
index 0000000000..7ad0e3d256
--- /dev/null
+++ b/assets/layers/favourite/favourite.proto.json
@@ -0,0 +1,47 @@
+{
+ "#":"no-translations",
+ "#dont-translate": "*",
+ "pointRendering": [
+ {
+ "location": [
+ "point",
+ "centroid"
+ ],
+ "marker": [
+ {
+ "icon": {
+ "render": "heart",
+ "mappings": [
+ {
+ "if": "_favourite=no",
+ "then": "heart_outline"
+ }
+ ]
+ },
+ "color": "red"
+ }
+ ]
+ }
+ ],
+ "description": {
+ "en": "A generic map layer which shows locations that a contributor marked as favourite",
+ "nl": "Een laag met persoonlijke favourieten"
+ },
+ "name": {
+ "en": "Favourites",
+ "nl": "Favorieten"
+ },
+ "id": "favourite",
+ "source": "special",
+ "isShown": "_favourite=yes",
+ "minzoom": 0,
+ "title": {
+ "render": {
+ "en": "Favourite location",
+ "nl": "Favoriete locatie"
+ }
+ },
+ "tagRenderings": [
+
+ ]
+}
diff --git a/assets/layers/icons/icons.json b/assets/layers/icons/icons.json
index 8c34354282..4e8c386074 100644
--- a/assets/layers/icons/icons.json
+++ b/assets/layers/icons/icons.json
@@ -14,7 +14,8 @@
{
"id": "wikipedialink",
"labels": [
- "defaults"
+ "defaults",
+ "in_favourite"
],
"render": "",
"condition": {
@@ -66,10 +67,23 @@
],
"metacondition": "__showTimeSensitiveIcons!=no"
},
+ {
+ "id": "open_until",
+ "labels": [
+ "defaults",
+ "in_favourite"
+ ],
+ "#": "Titleicon showing 'open until 17:00'",
+ "icon": {
+ "class": "w-20 mx-1 flex items-center"
+ },
+ "render": "{opening_hours_state()}"
+ },
{
"id": "phonelink",
"labels": [
- "defaults"
+ "defaults",
+ "in_favourite"
],
"render": "",
"mappings": [
@@ -89,7 +103,8 @@
{
"id": "emaillink",
"labels": [
- "defaults"
+ "defaults",
+ "in_favourite"
],
"render": "",
"mappings": [
@@ -109,7 +124,8 @@
{
"id": "websitelink",
"labels": [
- "defaults"
+ "defaults",
+ "in_favourite"
],
"render": "",
"condition": "website~*"
@@ -117,7 +133,8 @@
{
"id": "smokingicon",
"labels": [
- "defaults"
+ "defaults",
+ "in_favourite"
],
"mappings": [
{
@@ -140,6 +157,15 @@
"render": "{share_link()}",
"metacondition": "_supports_sharing=yes"
},
+ {
+ "id": "favourite_title_icon",
+ "labels": [
+ "defaults"
+ ],
+ "render": {
+ "*": "{favourite_icon()}"
+ }
+ },
{
"id": "osmlink",
"labels": [
@@ -162,7 +188,8 @@
{
"id": "dogicon",
"labels": [
- "defaults"
+ "defaults",
+ "in_favourite"
],
"mappings": [
{
@@ -193,6 +220,13 @@
"class": "w-20 mx-1 flex items-center"
},
"render": "{rating()}"
+ },
+ {
+ "id": "favourite_icon",
+ "description": "Only for rendering",
+ "condition": "_favourite=yes",
+ "icon": "circle:white;heart:red",
+ "metacondition": "__showTimeSensitiveIcons!=no"
}
]
}
diff --git a/assets/svg/center.svg b/assets/svg/center.svg
new file mode 100644
index 0000000000..d2c0ce8d41
--- /dev/null
+++ b/assets/svg/center.svg
@@ -0,0 +1,62 @@
+
+
diff --git a/assets/svg/center.svg.license b/assets/svg/center.svg.license
new file mode 100644
index 0000000000..ed02883002
--- /dev/null
+++ b/assets/svg/center.svg.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: Pieter Vander Vennet
+SPDX-License-Identifier: CC0-1.0
\ No newline at end of file
diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json
index d6ea4605ae..0ba4ac2b90 100644
--- a/assets/svg/license_info.json
+++ b/assets/svg/license_info.json
@@ -153,6 +153,14 @@
"https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg"
]
},
+ {
+ "path": "center.svg",
+ "license": "CC0-1.0",
+ "authors": [
+ "Pieter Vander Vennet"
+ ],
+ "sources": []
+ },
{
"path": "checkmark.svg",
"license": "CC0-1.0",
diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json
index da398b01a1..82a23b6be1 100644
--- a/assets/themes/climbing/climbing.json
+++ b/assets/themes/climbing/climbing.json
@@ -69,10 +69,12 @@
},
"+titleIcons": [
{
+ "id": "climbing_length",
"render": "
",
"condition": "__difficulty:char~*"
}
diff --git a/assets/themes/sidewalks/sidewalks.json b/assets/themes/sidewalks/sidewalks.json
index 03b439468d..459225a4a7 100644
--- a/assets/themes/sidewalks/sidewalks.json
+++ b/assets/themes/sidewalks/sidewalks.json
@@ -166,31 +166,31 @@
{
"if": "sidewalk:left|right=yes",
"then": {
- "en": "Yes, there is a sidewalk on this side of the road",
- "de": "Ja, es gibt einen Bürgersteig auf dieser Straßenseite",
- "da": "Ja, der er et fortov på denne side af vejen",
- "nl": "Ja, er is een stoep aan deze kant van de weg",
- "fr": "Oui, il y a un trottoir de ce côté de la route",
- "ca": "Sí, hi ha una vorera a aquest costat del carrer",
- "es": "Sí, hay una acera en este lado de la calle",
- "cs": "Ano, na této straně silnice je chodník",
- "it": "Sì, c'è un marciapiede su questo lato della strada",
- "pl": "Tak, jest chodnik z boku drogi"
+ "en": "There is a sidewalk on this side of the road",
+ "de": "Es gibt einen Bürgersteig auf dieser Straßenseite",
+ "da": "Der er et fortov på denne side af vejen",
+ "nl": "Er is een stoep aan deze kant van de weg",
+ "fr": "Il y a un trottoir de ce côté de la route",
+ "ca": "Hi ha una vorera a aquest costat del carrer",
+ "es": "Hay una acera en este lado de la calle",
+ "cs": "Na této straně silnice je chodník",
+ "it": "C'è un marciapiede su questo lato della strada",
+ "pl": "Jest chodnik z boku drogi"
}
},
{
"if": "sidewalk:left|right=no",
"then": {
- "en": "No, there is no sidewalk to walk on",
- "de": "Nein, es gibt keinen Bürgersteig für Fußgänger",
- "da": "Nej, der er ikke noget fortov at gå på",
- "nl": "Nee, er is geen stoep om op te lopen",
- "fr": "Non, il n'y a pas de trottoir où marcher",
- "ca": "No, no hi ha vorera per la que caminar",
- "es": "No, no hay acera por la que caminar",
- "cs": "Ne, není tu žádný chodník",
- "it": "No, non c'è un marciapiede su cui camminare",
- "pl": "Nie, nie ma chodnika, którym można chodzić"
+ "en": "There is no sidewalk to walk on",
+ "de": "Es gibt keinen Bürgersteig für Fußgänger",
+ "da": "Der er ikke noget fortov at gå på",
+ "nl": "Er is geen stoep om op te lopen",
+ "fr": "Il n'y a pas de trottoir où marcher",
+ "ca": "No hi ha vorera per la que caminar",
+ "es": "No hay acera por la que caminar",
+ "cs": "Není tu žádný chodník",
+ "it": "Non c'è un marciapiede su cui camminare",
+ "pl": "Nie ma chodnika, którym można chodzić"
}
},
{
diff --git a/assets/themes/walkingnodes/walkingnodes.json b/assets/themes/walkingnodes/walkingnodes.json
index d27754160f..65076cb820 100644
--- a/assets/themes/walkingnodes/walkingnodes.json
+++ b/assets/themes/walkingnodes/walkingnodes.json
@@ -349,4 +349,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/langs/en.json b/langs/en.json
index 2a9968421a..b722dd1d4a 100644
--- a/langs/en.json
+++ b/langs/en.json
@@ -50,6 +50,22 @@
"panelIntro": "
Your personal theme
Activate your favourite layers from all the official themes",
"reload": "Reload the data"
},
+ "favouritePoi": {
+ "button": {
+ "isFavourite": "This location is currently marked as favourite and will show up on all thematic maps of MapComplete you visit.",
+ "markAsFavouriteTitle": "Mark this location as favourite location",
+ "markDescription": "Add this location to a personal list of your favourites",
+ "unmark": "Remove from your personal list of favourites",
+ "unmarkNotDeleted": "This point will not be deleted and still be visible on the appropriate map for you and others"
+ },
+ "downloadGeojson": "Download your favourites as geojson",
+ "downloadGpx": "Download your favourites as GPX",
+ "intro": "You marked {length} locations as a favourite location.",
+ "introPrivacy": "This list is only visible to you",
+ "loginToSeeList": "Login to see the list of locations you marked as favourite",
+ "tab": "Your favourites",
+ "title": "Your favourite locations"
+ },
"flyer": {
"aerial": "This map uses a different background, namely aerial imagery by Agentschap Informatie Vlaanderen",
"callToAction": "Test it on mapcomplete.org",
@@ -404,6 +420,7 @@
"key": "Key combination",
"openLayersPanel": "Opens the layers and filters panel",
"selectAerial": "Set the background to aerial or satellite imagery. Toggles between the two best, available layers",
+ "selectFavourites": "Open the favourites page",
"selectItem": "Select the POI which is closest to the map center (crosshair). Only when in keyboard navigation is used",
"selectMap": "Set the background to a map from external sources. Toggles between the two best, available layers",
"selectMapnik": "Set the background layer to OpenStreetMap-carto",
diff --git a/package.json b/package.json
index 05473cf6e9..d51d0025cc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "mapcomplete",
- "version": "0.35.2",
+ "version": "0.36.0",
"repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
@@ -65,7 +65,7 @@
"generate:service-worker": "tsc src/service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i.bak \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js && rm public/service-worker.js.bak",
"optimize-images": "cd assets/generated/ && find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'",
"generate:stats": "vite-node scripts/GenerateSeries.ts",
- "reset:layeroverview": "mkdir -p ./src/assets/generated/layers; echo {\\\"themes\\\":[]} > ./src/assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./src/assets/generated/known_layers.json && rm -f ./src/assets/generated/layers/*.json && rm -f ./src/assets/generated/themes/*.json && cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json && npm run generate:layeroverview && vite-node scripts/generateLayerOverview.ts -- --force",
+ "reset:layeroverview": "mkdir -p ./src/assets/generated/layers; echo {\\\"themes\\\":[]} > ./src/assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./src/assets/generated/known_layers.json && rm -f ./src/assets/generated/layers/*.json && rm -f ./src/assets/generated/themes/*.json && cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json && echo '{}' > ./src/assets/generated/layers/favourite.json && npm run generate:layeroverview && vite-node scripts/generateLayerOverview.ts -- --force",
"generate": "mkdir -p ./assets/generated; npm run generate:licenses; npm run generate:images; npm run generate:charging-stations; npm run generate:translations; npm run reset:layeroverview; npm run generate:service-worker",
"generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -",
"prepare-deploy": "npm run generate:service-worker && ./scripts/build.sh",
diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css
index 26b1638ae8..ae394847be 100644
--- a/public/css/index-tailwind-output.css
+++ b/public/css/index-tailwind-output.css
@@ -745,6 +745,10 @@ video {
top: 2.5rem;
}
+.left-1\/4 {
+ left: 25%;
+}
+
.isolate {
isolation: isolate;
}
@@ -765,10 +769,6 @@ video {
float: left;
}
-.m-8 {
- margin: 2rem;
-}
-
.m-4 {
margin: 1rem;
}
@@ -781,6 +781,10 @@ video {
margin: 0px;
}
+.m-8 {
+ margin: 2rem;
+}
+
.m-2 {
margin: 0.5rem;
}
@@ -841,10 +845,6 @@ video {
margin-right: 3rem;
}
-.mb-4 {
- margin-bottom: 1rem;
-}
-
.mt-4 {
margin-top: 1rem;
}
@@ -881,6 +881,10 @@ video {
margin-right: 0.25rem;
}
+.mb-4 {
+ margin-bottom: 1rem;
+}
+
.ml-1 {
margin-left: 0.25rem;
}
@@ -1088,6 +1092,10 @@ video {
height: 2.75rem;
}
+.h-5 {
+ height: 1.25rem;
+}
+
.h-48 {
height: 12rem;
}
@@ -1198,6 +1206,14 @@ video {
width: 50%;
}
+.w-14 {
+ width: 3.5rem;
+}
+
+.w-5 {
+ width: 1.25rem;
+}
+
.w-10 {
width: 2.5rem;
}
@@ -1289,6 +1305,10 @@ video {
appearance: none;
}
+.grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@@ -1441,6 +1461,14 @@ video {
align-self: center;
}
+.justify-self-start {
+ justify-self: start;
+}
+
+.justify-self-end {
+ justify-self: end;
+}
+
.overflow-auto {
overflow: auto;
}
@@ -2335,6 +2363,16 @@ button.disabled:hover, .button.disabled:hover {
color: unset;
}
+button.link {
+ border: none;
+ text-decoration: underline;
+ background-color: unset;
+}
+
+button.link:hover {
+ color:unset;
+}
+
.interactive button.disabled svg path, .interactive .button.disabled svg path {
fill: var(--interactive-foreground) !important;
}
diff --git a/scripts/build.sh b/scripts/build.sh
index d352e3166c..697ddff4a4 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -10,7 +10,7 @@ mkdir dist 2> /dev/null
mkdir dist/assets 2> /dev/null
-export NODE_OPTIONS="--max-old-space-size=8192"
+export NODE_OPTIONS="--max-old-space-size=16384"
# This script ends every line with '&&' to chain everything. A failure will thus stop the build
npm run generate:editor-layer-index &&
@@ -48,7 +48,7 @@ else
exit 1
fi
-export NODE_OPTIONS=--max-old-space-size=7000
+export NODE_OPTIONS=--max-old-space-size=16000
which vite
vite build --sourcemap
# Copy the layer files, as these might contain assets (e.g. svgs)
diff --git a/scripts/generateFavouritesLayer.ts b/scripts/generateFavouritesLayer.ts
new file mode 100644
index 0000000000..29b25d17d2
--- /dev/null
+++ b/scripts/generateFavouritesLayer.ts
@@ -0,0 +1,304 @@
+import Script from "./Script"
+import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
+import { existsSync, readFileSync, writeFileSync } from "fs"
+import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
+import { AllKnownLayoutsLazy } from "../src/Customizations/AllKnownLayouts"
+import { Utils } from "../src/Utils"
+import { AddEditingElements } from "../src/Models/ThemeConfig/Conversion/PrepareLayer"
+import {
+ MappingConfigJson,
+ QuestionableTagRenderingConfigJson,
+} from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
+import { TagConfigJson } from "../src/Models/ThemeConfig/Json/TagConfigJson"
+import { TagUtils } from "../src/Logic/Tags/TagUtils"
+import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
+import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable"
+
+export class GenerateFavouritesLayer extends Script {
+ private readonly layers: LayerConfigJson[] = []
+
+ constructor() {
+ super("Prepares the 'favourites'-layer")
+ const allThemes = new AllKnownLayoutsLazy(false).values()
+ for (const theme of allThemes) {
+ if (theme.hideFromOverview) {
+ continue
+ }
+ for (const layer of theme.layers) {
+ if (!layer.source) {
+ continue
+ }
+ if (layer.source.geojsonSource) {
+ continue
+ }
+ const layerConfig = AllSharedLayers.getSharedLayersConfigs().get(layer.id)
+ if (!layerConfig) {
+ continue
+ }
+ this.layers.push(layerConfig)
+ }
+ }
+ }
+
+ private sortMappings(mappings: MappingConfigJson[]): MappingConfigJson[] {
+ const sortedMappings: MappingConfigJson[] = [...mappings]
+ sortedMappings.sort((a, b) => {
+ const aTag = TagUtils.Tag(a.if)
+ const bTag = TagUtils.Tag(b.if)
+ const aPop = TagUtils.GetPopularity(aTag)
+ const bPop = TagUtils.GetPopularity(bTag)
+ return aPop - bPop
+ })
+
+ return sortedMappings
+ }
+ private addTagRenderings(proto: LayerConfigJson) {
+ const blacklistedIds = new Set([
+ "images",
+ "questions",
+ "mapillary",
+ "leftover-questions",
+ "last_edit",
+ "minimap",
+ "move-button",
+ "delete-button",
+ "all-tags",
+ "all_tags",
+ ...AddEditingElements.addedElements,
+ ])
+
+ const generatedTagRenderings: (string | QuestionableTagRenderingConfigJson)[] = []
+ const trPerId = new Map<
+ string,
+ { conditions: TagConfigJson[]; tr: QuestionableTagRenderingConfigJson }
+ >()
+ for (const layerConfig of this.layers) {
+ if (!layerConfig.tagRenderings) {
+ continue
+ }
+ for (const tagRendering of layerConfig.tagRenderings) {
+ if (typeof tagRendering === "string") {
+ if (blacklistedIds.has(tagRendering)) {
+ continue
+ }
+ generatedTagRenderings.push(tagRendering)
+ blacklistedIds.add(tagRendering)
+ continue
+ }
+ if (tagRendering["builtin"]) {
+ continue
+ }
+ const id = tagRendering.id
+ if (blacklistedIds.has(id)) {
+ continue
+ }
+ if (trPerId.has(id)) {
+ const old = trPerId.get(id).tr
+
+ // We need to figure out if this was a 'recycled' tag rendering or just happens to have the same id
+ function isSame(fieldName: string) {
+ return old[fieldName]?.["en"] === tagRendering[fieldName]?.["en"]
+ }
+
+ const sameQuestion = isSame("question") && isSame("render")
+ if (!sameQuestion) {
+ const newTr = Utils.Clone(tagRendering)
+ newTr.id = layerConfig.id + "_" + newTr.id
+ if (blacklistedIds.has(newTr.id)) {
+ continue
+ }
+ newTr.condition = {
+ and: Utils.NoNull([newTr.condition, layerConfig.source["osmTags"]]),
+ }
+ generatedTagRenderings.push(newTr)
+ blacklistedIds.add(newTr.id)
+ continue
+ }
+ }
+ if (!trPerId.has(id)) {
+ const newTr = Utils.Clone(tagRendering)
+ generatedTagRenderings.push(newTr)
+ trPerId.set(newTr.id, { tr: newTr, conditions: [] })
+ }
+ const conditions = trPerId.get(id).conditions
+ if (tagRendering["condition"]) {
+ conditions.push({
+ and: [tagRendering["condition"], layerConfig.source["osmTags"]],
+ })
+ } else {
+ conditions.push(layerConfig.source["osmTags"])
+ }
+ }
+ }
+
+ for (const { tr, conditions } of Array.from(trPerId.values())) {
+ const optimized = TagUtils.optimzeJson({ or: conditions })
+ if (optimized === true) {
+ continue
+ }
+ if (optimized === false) {
+ throw "Optimized into 'false', this is weird..."
+ }
+ tr.condition = optimized
+ }
+
+ const allTags: QuestionableTagRenderingConfigJson = {
+ id: "all-tags",
+ render: { "*": "{all_tags()}" },
+
+ metacondition: {
+ or: [
+ "__featureSwitchIsDebugging=true",
+ "mapcomplete-show_tags=full",
+ "mapcomplete-show_debug=yes",
+ ],
+ },
+ }
+ proto.tagRenderings = [
+ "images",
+ ...generatedTagRenderings,
+ ...proto.tagRenderings,
+ "questions",
+ allTags,
+ ]
+ }
+
+ private addTitleIcons(proto: LayerConfigJson) {
+ proto.titleIcons = []
+ const seenTitleIcons = new Set()
+ for (const layer of this.layers) {
+ for (const titleIcon of layer.titleIcons) {
+ if (typeof titleIcon === "string") {
+ continue
+ }
+ if (titleIcon["labels"]?.indexOf("defaults") >= 0) {
+ continue
+ }
+ if (titleIcon.id === "rating") {
+ if (!seenTitleIcons.has("rating")) {
+ proto.titleIcons.unshift("icons.rating")
+ seenTitleIcons.add("rating")
+ }
+ continue
+ }
+ if (seenTitleIcons.has(titleIcon.id)) {
+ continue
+ }
+ seenTitleIcons.add(titleIcon.id)
+ console.log("Adding ", titleIcon.id)
+ proto.titleIcons.push(titleIcon)
+ }
+ }
+ proto.titleIcons.push("icons.defaults")
+ }
+
+ private addTitle(proto: LayerConfigJson) {
+ let mappings: MappingConfigJson[] = []
+ for (const layer of this.layers) {
+ const t = layer.title
+ const tags: TagConfigJson = layer.source["osmTags"]
+ if (!t) {
+ continue
+ }
+ if (typeof t === "string") {
+ mappings.push({ if: tags, then: t })
+ } else if (t["render"] !== undefined || t["mappings"] !== undefined) {
+ const tr = t
+ for (let i = 0; i < (tr.mappings ?? []).length; i++) {
+ const mapping = tr.mappings[i]
+ const optimized = TagUtils.optimzeJson({
+ and: [mapping.if, tags],
+ })
+ if (optimized === false) {
+ console.warn(
+ "The following tags yielded 'false':",
+ JSON.stringify(mapping.if),
+ JSON.stringify(tags)
+ )
+ continue
+ }
+ if (optimized === true) {
+ console.error(
+ "The following tags yielded 'false':",
+ JSON.stringify(mapping.if),
+ JSON.stringify(tags)
+ )
+ throw "Tags for title optimized to true"
+ }
+
+ if (!mapping.then) {
+ throw (
+ "The title has a missing 'then' for mapping " +
+ i +
+ " in layer " +
+ layer.id
+ )
+ }
+ mappings.push({
+ if: optimized,
+ then: mapping.then,
+ })
+ }
+ if (tr.render) {
+ mappings.push({
+ if: tags,
+ then: tr.render,
+ })
+ }
+ } else {
+ mappings.push({ if: tags, then: >t })
+ }
+ }
+
+ mappings = this.sortMappings(mappings)
+
+ if (proto.title["mappings"]) {
+ mappings.unshift(...proto.title["mappings"])
+ }
+ if (proto.title["render"]) {
+ mappings.push({
+ if: "id~*",
+ then: proto.title["render"],
+ })
+ }
+
+ for (const mapping of mappings) {
+ const opt = TagUtils.optimzeJson(mapping.if)
+ if (typeof opt === "boolean") {
+ continue
+ }
+ mapping.if = opt
+ }
+
+ proto.title = {
+ mappings,
+ }
+ }
+
+ async main(args: string[]): Promise {
+ console.log("Generating the favourite layer: stealing _all_ tagRenderings")
+ const proto = this.readLayer("favourite/favourite.proto.json")
+ this.addTagRenderings(proto)
+ this.addTitle(proto)
+ this.addTitleIcons(proto)
+ const targetContent = JSON.stringify(proto, null, " ")
+ const path = "./assets/layers/favourite/favourite.json"
+ if (existsSync(path)) {
+ if (readFileSync(path, "utf8") === targetContent) {
+ return // No need to actually write the file, it is identical
+ }
+ }
+ writeFileSync(path, targetContent)
+ }
+
+ private readLayer(path: string): LayerConfigJson {
+ try {
+ return JSON.parse(readFileSync("./assets/layers/" + path, "utf8"))
+ } catch (e) {
+ console.error("Could not read ./assets/layers/" + path)
+ throw e
+ }
+ }
+}
+
+new GenerateFavouritesLayer().run()
diff --git a/scripts/generateIncludedImages.ts b/scripts/generateIncludedImages.ts
index dd7160a40e..6e0d643481 100644
--- a/scripts/generateIncludedImages.ts
+++ b/scripts/generateIncludedImages.ts
@@ -27,7 +27,8 @@ function genImages(dryrun = false) {
"star_outline",
"star",
"osm_logo_us",
-
+ "triangle",
+ "teardrop_with_hole_green",
"SocialImageForeground",
"wikipedia",
"Upload",
diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts
index 32f06ee64a..dcfbb71ad7 100644
--- a/scripts/generateLayerOverview.ts
+++ b/scripts/generateLayerOverview.ts
@@ -28,6 +28,7 @@ import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Js
import LayerConfig from "../src/Models/ThemeConfig/LayerConfig"
import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig"
import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext"
+import { GenerateFavouritesLayer } from "./generateFavouritesLayer"
// This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
// It spits out an overview of those to be used to load them
@@ -381,16 +382,11 @@ class LayerOverviewUtils extends Script {
forceReload
)
- writeFileSync(
- "./src/assets/generated/known_themes.json",
- JSON.stringify({
- themes: Array.from(sharedThemes.values()),
- })
- )
-
writeFileSync(
"./src/assets/generated/known_layers.json",
- JSON.stringify({ layers: Array.from(sharedLayers.values()) })
+ JSON.stringify({
+ layers: Array.from(sharedLayers.values()).filter((l) => l.id !== "favourite"),
+ })
)
const mcChangesPath = "./assets/themes/mapcomplete-changes/mapcomplete-changes.json"
@@ -428,6 +424,19 @@ class LayerOverviewUtils extends Script {
ConversionContext.construct([], [])
)
+ for (const [_, theme] of sharedThemes) {
+ theme.layers = theme.layers.filter(
+ (l) => Constants.added_by_default.indexOf(l["id"]) < 0
+ )
+ }
+
+ writeFileSync(
+ "./src/assets/generated/known_themes.json",
+ JSON.stringify({
+ themes: Array.from(sharedThemes.values()),
+ })
+ )
+
const end = new Date()
const millisNeeded = end.getTime() - start.getTime()
if (AllSharedLayers.getSharedLayersConfigs().size == 0) {
@@ -791,4 +800,5 @@ class LayerOverviewUtils extends Script {
}
}
+new GenerateFavouritesLayer().run()
new LayerOverviewUtils().run()
diff --git a/scripts/generateStats.ts b/scripts/generateStats.ts
index 1dd5ced370..2931cbaf79 100644
--- a/scripts/generateStats.ts
+++ b/scripts/generateStats.ts
@@ -4,6 +4,8 @@ import { TagUtils } from "../src/Logic/Tags/TagUtils"
import { Utils } from "../src/Utils"
import { writeFileSync } from "fs"
import ScriptUtils from "./ScriptUtils"
+import TagRenderingConfig from "../src/Models/ThemeConfig/TagRenderingConfig"
+import { And } from "../src/Logic/Tags/And"
/* Downloads stats on osmSource-tags and keys from tagInfo */
@@ -21,7 +23,12 @@ async function main(includeTags = true) {
continue
}
- const sources = TagUtils.Tag(layer.source["osmTags"])
+ const sourcesList = [TagUtils.Tag(layer.source["osmTags"])]
+ if (layer?.title) {
+ sourcesList.push(...new TagRenderingConfig(layer.title).usedTags())
+ }
+
+ const sources = new And(sourcesList)
const allKeys = sources.usedKeys()
for (const key of allKeys) {
if (!keysAndTags.has(key)) {
@@ -68,6 +75,8 @@ async function main(includeTags = true) {
"./src/assets/key_totals.json",
JSON.stringify(
{
+ "#": "Generated with generateStats.ts",
+ date: new Date().toISOString(),
keys: Utils.MapToObj(keyTotal, (t) => t),
tags: Utils.MapToObj(tagTotal, (v) => Utils.MapToObj(v, (t) => t)),
},
diff --git a/src/Customizations/AllKnownLayouts.ts b/src/Customizations/AllKnownLayouts.ts
index 12c27223c5..6d3ada24bb 100644
--- a/src/Customizations/AllKnownLayouts.ts
+++ b/src/Customizations/AllKnownLayouts.ts
@@ -1,45 +1,54 @@
import known_themes from "../assets/generated/known_themes.json"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
+import favourite from "../assets/generated/layers/favourite.json"
import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson"
+import { AllSharedLayers } from "./AllSharedLayers"
+import Constants from "../Models/Constants"
/**
* Somewhat of a dictionary, which lazily parses needed themes
*/
export class AllKnownLayoutsLazy {
- private readonly dict: Map LayoutConfig }> =
- new Map()
- constructor() {
+ private readonly raw: Map = new Map()
+ private readonly dict: Map = new Map()
+
+ constructor(includeFavouriteLayer = true) {
for (const layoutConfigJson of known_themes["themes"]) {
- this.dict.set(layoutConfigJson.id, {
- func: () => {
- const layout = new LayoutConfig(layoutConfigJson, true)
- for (let i = 0; i < layout.layers.length; i++) {
- let layer = layout.layers[i]
- if (typeof layer === "string") {
- throw "Layer " + layer + " was not expanded in " + layout.id
- }
+ for (const layerId of Constants.added_by_default) {
+ if (layerId === "favourite" && favourite.id) {
+ if (includeFavouriteLayer) {
+ layoutConfigJson.layers.push(favourite)
}
- return layout
- },
- })
+ continue
+ }
+ const defaultLayer = AllSharedLayers.getSharedLayersConfigs().get(layerId)
+ if (defaultLayer === undefined) {
+ console.error("Could not find builtin layer", layerId)
+ continue
+ }
+ layoutConfigJson.layers.push(defaultLayer)
+ }
+ this.raw.set(layoutConfigJson.id, layoutConfigJson)
}
}
+ public getConfig(key: string): LayoutConfigJson {
+ return this.raw.get(key)
+ }
+
public get(key: string): LayoutConfig {
- const thunk = this.dict.get(key)
- if (thunk === undefined) {
- return undefined
+ const cached = this.dict.get(key)
+ if (cached !== undefined) {
+ return cached
}
- if (thunk["data"]) {
- return thunk["data"]
- }
- const layout = thunk["func"]()
- this.dict.set(key, { data: layout })
+
+ const layout = new LayoutConfig(this.getConfig(key))
+ this.dict.set(key, layout)
return layout
}
public keys() {
- return this.dict.keys()
+ return this.raw.keys()
}
public values() {
diff --git a/src/Logic/Actors/SelectedElementTagsUpdater.ts b/src/Logic/Actors/SelectedElementTagsUpdater.ts
index 91078a53b5..8bc510c56b 100644
--- a/src/Logic/Actors/SelectedElementTagsUpdater.ts
+++ b/src/Logic/Actors/SelectedElementTagsUpdater.ts
@@ -6,13 +6,21 @@ import { Changes } from "../Osm/Changes"
import { OsmConnection } from "../Osm/OsmConnection"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import SimpleMetaTagger from "../SimpleMetaTagger"
-import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import { Feature } from "geojson"
import { OsmTags } from "../../Models/OsmFeature"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { Utils } from "../../Utils"
+interface TagsUpdaterState {
+ selectedElement: UIEventSource
+ featureProperties: { getStore: (id: string) => UIEventSource> }
+ changes: Changes
+ osmConnection: OsmConnection
+ layout: LayoutConfig
+ osmObjectDownloader: OsmObjectDownloader
+ indexedFeatures: IndexedFeatureSource
+}
export default class SelectedElementTagsUpdater {
private static readonly metatags = new Set([
"timestamp",
@@ -23,38 +31,18 @@ export default class SelectedElementTagsUpdater {
"id",
])
- private readonly state: {
- selectedElement: UIEventSource
- featureProperties: FeaturePropertiesStore
- changes: Changes
- osmConnection: OsmConnection
- layout: LayoutConfig
- osmObjectDownloader: OsmObjectDownloader
- indexedFeatures: IndexedFeatureSource
- }
-
- constructor(state: {
- selectedElement: UIEventSource
- featureProperties: FeaturePropertiesStore
- indexedFeatures: IndexedFeatureSource
- changes: Changes
- osmConnection: OsmConnection
- layout: LayoutConfig
- osmObjectDownloader: OsmObjectDownloader
- }) {
- this.state = state
+ constructor(state: TagsUpdaterState) {
state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => {
if (!isLoggedIn && !Utils.runningFromConsole) {
return
}
- this.installCallback()
+ this.installCallback(state)
// We only have to do this once...
return true
})
}
- private installCallback() {
- const state = this.state
+ private installCallback(state: TagsUpdaterState) {
state.selectedElement.addCallbackAndRunD(async (s) => {
let id = s.properties?.id
if (!id) {
@@ -94,7 +82,7 @@ export default class SelectedElementTagsUpdater {
oldFeature.geometry = newGeometry
state.featureProperties.getStore(id)?.ping()
}
- this.applyUpdate(latestTags, id)
+ SelectedElementTagsUpdater.applyUpdate(latestTags, id, state)
console.log("Updated", id)
} catch (e) {
@@ -102,8 +90,7 @@ export default class SelectedElementTagsUpdater {
}
})
}
- private applyUpdate(latestTags: OsmTags, id: string) {
- const state = this.state
+ public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) {
try {
const leftRightSensitive = state.layout.isLeftRightSensitive()
@@ -162,11 +149,16 @@ export default class SelectedElementTagsUpdater {
}
if (somethingChanged) {
- console.log("Detected upstream changes to the object when opening it, updating...")
+ console.log(
+ "Detected upstream changes to the object " +
+ id +
+ " when opening it, updating..."
+ )
currentTagsSource.ping()
} else {
console.debug("Fetched latest tags for ", id, "but detected no changes")
}
+ return currentTags
} catch (e) {
console.error("Updating the tags of selected element ", id, "failed due to", e)
}
diff --git a/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts b/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts
new file mode 100644
index 0000000000..1f2764062b
--- /dev/null
+++ b/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts
@@ -0,0 +1,220 @@
+import StaticFeatureSource from "./StaticFeatureSource"
+import { Feature } from "geojson"
+import { Store, Stores, UIEventSource } from "../../UIEventSource"
+import { OsmConnection } from "../../Osm/OsmConnection"
+import { OsmId } from "../../../Models/OsmFeature"
+import { GeoOperations } from "../../GeoOperations"
+import { IndexedFeatureSource } from "../FeatureSource"
+import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
+import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
+import SelectedElementTagsUpdater from "../../Actors/SelectedElementTagsUpdater"
+
+/**
+ * Generates the favourites from the preferences and marks them as favourite
+ */
+export default class FavouritesFeatureSource extends StaticFeatureSource {
+ public static readonly prefix = "mapcomplete-favourite-"
+ private readonly _osmConnection: OsmConnection
+ private readonly _detectedIds: Store
+
+ /**
+ * All favourites, including the ones which are filtered away because they are already displayed
+ */
+ public readonly allFavourites: Store
+
+ constructor(state: SpecialVisualizationState) {
+ const features: Store = Stores.ListStabilized(
+ state.osmConnection.preferencesHandler.preferences.map((prefs) => {
+ const feats: Feature[] = []
+ const allIds = new Set()
+ for (const key in prefs) {
+ if (!key.startsWith(FavouritesFeatureSource.prefix)) {
+ continue
+ }
+
+ try {
+ const feat = FavouritesFeatureSource.ExtractFavourite(key, prefs)
+ if (!feat) {
+ continue
+ }
+ feats.push(feat)
+ allIds.add(feat.properties.id)
+ } catch (e) {
+ console.error("Could not create favourite from", key, "due to", e)
+ }
+ }
+ return feats
+ })
+ )
+
+ const featuresWithoutAlreadyPresent = features.map((features) =>
+ features.filter(
+ (feat) => !state.layout.layers.some((l) => l.id === feat.properties._orig_layer)
+ )
+ )
+
+ super(featuresWithoutAlreadyPresent)
+ this.allFavourites = features
+
+ this._osmConnection = state.osmConnection
+ this._detectedIds = Stores.ListStabilized(
+ features.map((feats) => feats.map((f) => f.properties.id))
+ )
+ let allFeatures = state.indexedFeatures
+ this._detectedIds.addCallbackAndRunD((detected) =>
+ this.markFeatures(detected, state.featureProperties, allFeatures)
+ )
+ // We use the indexedFeatureSource as signal to update
+ allFeatures.features.map((_) =>
+ this.markFeatures(this._detectedIds.data, state.featureProperties, allFeatures)
+ )
+
+ this.allFavourites.addCallbackD((features) => {
+ for (const feature of features) {
+ this.updateFeature(feature, state.osmObjectDownloader, state)
+ }
+
+ return true
+ })
+ }
+
+ private async updateFeature(
+ feature: Feature,
+ osmObjectDownloader: OsmObjectDownloader,
+ state: SpecialVisualizationState
+ ) {
+ const id = feature.properties.id
+ const upstream = await osmObjectDownloader.DownloadObjectAsync(id)
+ if (upstream === "deleted") {
+ this.removeFavourite(feature)
+ return
+ }
+ console.log("Updating metadata due to favourite of", id)
+ const latestTags = SelectedElementTagsUpdater.applyUpdate(upstream.tags, id, state)
+ this.updatePropertiesOfFavourite(latestTags)
+ }
+
+ private static ExtractFavourite(key: string, prefs: Record): Feature {
+ const id = key.substring(FavouritesFeatureSource.prefix.length)
+ const osmId = id.replace("-", "/")
+ if (id.indexOf("-property-") > 0 || id.endsWith("-layer") || id.endsWith("-theme")) {
+ return undefined
+ }
+ const geometry = <[number, number]>JSON.parse(prefs[key])
+ const properties = FavouritesFeatureSource.getPropertiesFor(prefs, id)
+ properties._orig_layer = prefs[FavouritesFeatureSource.prefix + id + "-layer"]
+ properties._orig_theme = prefs[FavouritesFeatureSource.prefix + id + "-theme"]
+
+ properties.id = osmId
+ properties._favourite = "yes"
+ return {
+ type: "Feature",
+ properties,
+ geometry: {
+ type: "Point",
+ coordinates: geometry,
+ },
+ }
+ }
+
+ private static getPropertiesFor(
+ prefs: Record,
+ id: string
+ ): Record {
+ const properties: Record = {}
+ const minLength = FavouritesFeatureSource.prefix.length + id.length + "-property-".length
+ for (const key in prefs) {
+ if (key.length < minLength) {
+ continue
+ }
+ if (!key.startsWith(FavouritesFeatureSource.prefix + id)) {
+ continue
+ }
+ const propertyName = key.substring(minLength).replaceAll("__", ":")
+ properties[propertyName] = prefs[key]
+ }
+ return properties
+ }
+
+ /**
+ * Sets all the (normal) properties as the feature is updated
+ */
+ private updatePropertiesOfFavourite(properties: Record) {
+ const id = properties?.id?.replace("/", "-")
+ if (!id) {
+ return
+ }
+ console.log("Updating store for", id)
+ for (const key in properties) {
+ const pref = this._osmConnection.GetPreference(
+ "favourite-" + id + "-property-" + key.replaceAll(":", "__")
+ )
+ const v = properties[key]
+ if (v === "" || !v) {
+ continue
+ }
+ pref.setData("" + v)
+ }
+ }
+
+ public removeFavourite(feature: Feature, tags?: UIEventSource>) {
+ const id = feature.properties.id.replace("/", "-")
+ const pref = this._osmConnection.GetPreference("favourite-" + id)
+ this._osmConnection.preferencesHandler.removeAllWithPrefix("mapcomplete-favourite-" + id)
+ if (tags) {
+ delete tags.data._favourite
+ tags.ping()
+ }
+ }
+
+ public markAsFavourite(
+ feature: Feature,
+ layer: string,
+ theme: string,
+ tags: UIEventSource & { id: OsmId }>,
+ isFavourite: boolean = true
+ ) {
+ {
+ if (!isFavourite) {
+ this.removeFavourite(feature, tags)
+ return
+ }
+ const id = tags.data.id.replace("/", "-")
+ const pref = this._osmConnection.GetPreference("favourite-" + id)
+ const center = GeoOperations.centerpointCoordinates(feature)
+ pref.setData(JSON.stringify(center))
+ this._osmConnection.GetPreference("favourite-" + id + "-layer").setData(layer)
+ this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme)
+ this.updatePropertiesOfFavourite(tags.data)
+ }
+ tags.data._favourite = "yes"
+ tags.ping()
+ }
+
+ private markFeatures(
+ detected: string[],
+ featureProperties: { getStore(id: string): UIEventSource> },
+ allFeatures: IndexedFeatureSource
+ ) {
+ const feature = allFeatures.features.data
+ for (const f of feature) {
+ const id = f.properties.id
+ if (!id) {
+ continue
+ }
+ const store = featureProperties.getStore(id)
+ const origValue = store.data._favourite
+ if (detected.indexOf(id) >= 0) {
+ if (origValue !== "yes") {
+ store.data._favourite = "yes"
+ store.ping()
+ }
+ } else {
+ if (origValue) {
+ store.data._favourite = ""
+ store.ping()
+ }
+ }
+ }
+ }
+}
diff --git a/src/Logic/FeatureSource/Sources/NearbyFeatureSource.ts b/src/Logic/FeatureSource/Sources/NearbyFeatureSource.ts
index 669c86a114..703103bc01 100644
--- a/src/Logic/FeatureSource/Sources/NearbyFeatureSource.ts
+++ b/src/Logic/FeatureSource/Sources/NearbyFeatureSource.ts
@@ -6,10 +6,14 @@ import FilteringFeatureSource from "./FilteringFeatureSource"
import LayerState from "../../State/LayerState"
export default class NearbyFeatureSource implements FeatureSource {
+ private readonly _result = new UIEventSource(undefined)
+
public readonly features: Store
private readonly _targetPoint: Store<{ lon: number; lat: number }>
private readonly _numberOfNeededFeatures: number
+ private readonly _layerState?: LayerState
private readonly _currentZoom: Store
+ private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = []
constructor(
targetPoint: Store<{ lon: number; lat: number }>,
@@ -18,43 +22,46 @@ export default class NearbyFeatureSource implements FeatureSource {
layerState?: LayerState,
currentZoom?: Store
) {
+ this._layerState = layerState
this._targetPoint = targetPoint.stabilized(100)
this._numberOfNeededFeatures = numberOfNeededFeatures
this._currentZoom = currentZoom.stabilized(500)
- const allSources: Store<{ feat: Feature; d: number }[]>[] = []
- let minzoom = 999
-
- const result = new UIEventSource(undefined)
- this.features = Stores.ListStabilized(result)
-
- function update() {
- let features: { feat: Feature; d: number }[] = []
- for (const src of allSources) {
- features.push(...src.data)
- }
- features.sort((a, b) => a.d - b.d)
- if (numberOfNeededFeatures !== undefined) {
- features = features.slice(0, numberOfNeededFeatures)
- }
- result.setData(features.map((f) => f.feat))
- }
+ this.features = Stores.ListStabilized(this._result)
sources.forEach((source, layer) => {
- const flayer = layerState?.filteredLayers.get(layer)
- minzoom = Math.min(minzoom, flayer.layerDef.minzoom)
- const calcSource = this.createSource(
- source.features,
- flayer.layerDef.minzoom,
- flayer.isDisplayed
- )
- calcSource.addCallbackAndRunD((features) => {
- update()
- })
- allSources.push(calcSource)
+ this.registerSource(source, layer)
})
}
+ public registerSource(source: FeatureSource, layerId: string) {
+ const flayer = this._layerState?.filteredLayers.get(layerId)
+ if (!flayer) {
+ return
+ }
+ const calcSource = this.createSource(
+ source.features,
+ flayer.layerDef.minzoom,
+ flayer.isDisplayed
+ )
+ calcSource.addCallbackAndRunD((features) => {
+ this.update()
+ })
+ this._allSources.push(calcSource)
+ }
+
+ private update() {
+ let features: { feat: Feature; d: number }[] = []
+ for (const src of this._allSources) {
+ features.push(...src.data)
+ }
+ features.sort((a, b) => a.d - b.d)
+ if (this._numberOfNeededFeatures !== undefined) {
+ features = features.slice(0, this._numberOfNeededFeatures)
+ }
+ this._result.setData(features.map((f) => f.feat))
+ }
+
/**
* Sorts the given source by distance, slices down to the required number
*/
diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts
index e03cc13b1b..61128d1b06 100644
--- a/src/Logic/GeoOperations.ts
+++ b/src/Logic/GeoOperations.ts
@@ -501,147 +501,43 @@ export class GeoOperations {
)
}
- public static IdentifieCommonSegments(coordinatess: [number, number][][]): {
- originalIndex: number
- segmentShardWith: number[]
- coordinates: []
- }[] {
- // An edge. Note that the edge might be reversed to fix the sorting condition: start[0] < end[0] && (start[0] != end[0] || start[0] < end[1])
- type edge = {
- start: [number, number]
- end: [number, number]
- intermediate: [number, number][]
- members: { index: number; isReversed: boolean }[]
+ /**
+ * Given a list of points, convert into a GPX-list, e.g. for favourites
+ * @param locations
+ * @param title
+ */
+ public static toGpxPoints(
+ locations: Feature[],
+ title?: string
+ ) {
+ title = title?.trim()
+ if (title === undefined || title === "") {
+ title = "Created with MapComplete"
}
-
- // The strategy:
- // 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them
- // 2. Join these edges back together - as long as their membership groups are the same
- // 3. Convert to results
-
- const allEdgesByKey = new Map()
-
- for (let index = 0; index < coordinatess.length; index++) {
- const coordinates = coordinatess[index]
- for (let i = 0; i < coordinates.length - 1; i++) {
- const c0 = coordinates[i]
- const c1 = coordinates[i + 1]
- const isReversed = c0[0] > c1[0] || (c0[0] == c1[0] && c0[1] > c1[1])
-
- let key: string
- if (isReversed) {
- key = "" + c1 + ";" + c0
- } else {
- key = "" + c0 + ";" + c1
+ title = Utils.EncodeXmlValue(title)
+ const trackPoints: string[] = []
+ for (const l of locations) {
+ let trkpt = ` `
+ for (const key in l.properties) {
+ const keyCleaned = key.replaceAll(":", "__")
+ trkpt += ` <${keyCleaned}>${l.properties[key]}${keyCleaned}>\n`
+ if (key === "website") {
+ trkpt += ` ${l.properties[key]}\n`
}
- const member = { index, isReversed }
- if (allEdgesByKey.has(key)) {
- allEdgesByKey.get(key).members.push(member)
- continue
- }
-
- let edge: edge
- if (!isReversed) {
- edge = {
- start: c0,
- end: c1,
- members: [member],
- intermediate: [],
- }
- } else {
- edge = {
- start: c1,
- end: c0,
- members: [member],
- intermediate: [],
- }
- }
- allEdgesByKey.set(key, edge)
}
+ trkpt += " \n"
+ trackPoints.push(trkpt)
}
-
- // Lets merge them back together!
-
- let didMergeSomething = false
- let allMergedEdges = Array.from(allEdgesByKey.values())
- const allEdgesByStartPoint = new Map()
- for (const edge of allMergedEdges) {
- edge.members.sort((m0, m1) => m0.index - m1.index)
-
- const kstart = edge.start + ""
- if (!allEdgesByStartPoint.has(kstart)) {
- allEdgesByStartPoint.set(kstart, [])
- }
- allEdgesByStartPoint.get(kstart).push(edge)
- }
-
- function membersAreCompatible(first: edge, second: edge): boolean {
- // There must be an exact match between the members
- if (first.members === second.members) {
- return true
- }
-
- if (first.members.length !== second.members.length) {
- return false
- }
-
- // Members are sorted and have the same length, so we can check quickly
- for (let i = 0; i < first.members.length; i++) {
- const m0 = first.members[i]
- const m1 = second.members[i]
- if (m0.index !== m1.index || m0.isReversed !== m1.isReversed) {
- return false
- }
- }
-
- // Allrigth, they are the same, lets mark this permanently
- second.members = first.members
- return true
- }
-
- do {
- didMergeSomething = false
- // We use 'allMergedEdges' as our running list
- const consumed = new Set()
- for (const edge of allMergedEdges) {
- // Can we make this edge longer at the end?
- if (consumed.has(edge)) {
- continue
- }
-
- console.log("Considering edge", edge)
- const matchingEndEdges = allEdgesByStartPoint.get(edge.end + "")
- console.log("Matchign endpoints:", matchingEndEdges)
- if (matchingEndEdges === undefined) {
- continue
- }
-
- for (let i = 0; i < matchingEndEdges.length; i++) {
- const endEdge = matchingEndEdges[i]
-
- if (consumed.has(endEdge)) {
- continue
- }
-
- if (!membersAreCompatible(edge, endEdge)) {
- continue
- }
-
- // We can make the segment longer!
- didMergeSomething = true
- console.log("Merging ", edge, "with ", endEdge)
- edge.intermediate.push(edge.end)
- edge.end = endEdge.end
- consumed.add(endEdge)
- matchingEndEdges.splice(i, 1)
- break
- }
- }
-
- allMergedEdges = allMergedEdges.filter((edge) => !consumed.has(edge))
- } while (didMergeSomething)
-
- return []
+ const header =
+ ''
+ return (
+ header +
+ "\n" +
+ title +
+ "\n\n" +
+ trackPoints.join("\n") +
+ "\n"
+ )
}
/**
diff --git a/src/Logic/ImageProviders/ImageUploadManager.ts b/src/Logic/ImageProviders/ImageUploadManager.ts
index 07a9ff3aea..17b3b8f56a 100644
--- a/src/Logic/ImageProviders/ImageUploadManager.ts
+++ b/src/Logic/ImageProviders/ImageUploadManager.ts
@@ -107,7 +107,8 @@ export class ImageUploadManager {
title,
description,
file,
- targetKey
+ targetKey,
+ tags.data["_orig_theme"]
)
if (!isNaN(Number(featureId))) {
// This is a map note
@@ -126,7 +127,8 @@ export class ImageUploadManager {
title: string,
description: string,
blob: File,
- targetKey: string | undefined
+ targetKey: string | undefined,
+ theme?: string
): Promise {
this.increaseCountFor(this._uploadStarted, featureId)
const properties = this._featureProperties.getStore(featureId)
@@ -148,7 +150,7 @@ export class ImageUploadManager {
console.log("Uploading done, creating action for", featureId)
key = targetKey ?? key
const action = new LinkImageAction(featureId, key, value, properties, {
- theme: this._layout.id,
+ theme: theme ?? this._layout.id,
changeType: "add-image",
})
this.increaseCountFor(this._uploadFinished, featureId)
diff --git a/src/Logic/Osm/OsmPreferences.ts b/src/Logic/Osm/OsmPreferences.ts
index 778b741365..1035dec823 100644
--- a/src/Logic/Osm/OsmPreferences.ts
+++ b/src/Logic/Osm/OsmPreferences.ts
@@ -12,6 +12,10 @@ export class OsmPreferences {
"all-osm-preferences",
{}
)
+ /**
+ * A map containing the individual preference sources
+ * @private
+ */
private readonly preferenceSources = new Map>()
private auth: any
private userDetails: UIEventSource
@@ -21,7 +25,10 @@ export class OsmPreferences {
this.auth = auth
this.userDetails = osmConnection.userDetails
const self = this
- osmConnection.OnLoggedIn(() => self.UpdatePreferences())
+ osmConnection.OnLoggedIn(() => {
+ self.UpdatePreferences(true)
+ return true
+ })
}
/**
@@ -72,11 +79,19 @@ export class OsmPreferences {
let i = 0
while (str !== "") {
if (str === undefined || str === "undefined") {
+ source.setData(undefined)
throw (
"Got 'undefined' or a literal string containing 'undefined' for a long preference with name " +
key
)
}
+ if (str === "undefined") {
+ source.setData(undefined)
+ throw (
+ "Got a literal string containing 'undefined' for a long preference with name " +
+ key
+ )
+ }
if (i > 100) {
throw "This long preference is getting very long... "
}
@@ -197,7 +212,7 @@ export class OsmPreferences {
})
}
- private UpdatePreferences() {
+ private UpdatePreferences(forceUpdate?: boolean) {
const self = this
this.auth.xhr(
{
@@ -210,11 +225,22 @@ export class OsmPreferences {
return
}
const prefs = value.getElementsByTagName("preference")
+ const seenKeys = new Set()
for (let i = 0; i < prefs.length; i++) {
const pref = prefs[i]
const k = pref.getAttribute("k")
const v = pref.getAttribute("v")
self.preferences.data[k] = v
+ seenKeys.add(k)
+ }
+ if (forceUpdate) {
+ for (let key in self.preferences.data) {
+ if (seenKeys.has(key)) {
+ continue
+ }
+ console.log("Deleting key", key, "as we didn't find it upstream")
+ delete self.preferences.data[key]
+ }
}
// We merge all the preferences: new keys are uploaded
@@ -285,4 +311,14 @@ 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/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts
index f38f32badd..2fbbdc3697 100644
--- a/src/Logic/State/UserRelatedState.ts
+++ b/src/Logic/State/UserRelatedState.ts
@@ -294,6 +294,9 @@ export default class UserRelatedState {
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
for (const k in newPrefs) {
const v = newPrefs[k]
+ if (v === "undefined" || !v) {
+ continue
+ }
if (k.endsWith("-combined-length")) {
const l = Number(v)
const key = k.substring(0, k.length - "length".length)
@@ -308,7 +311,6 @@ export default class UserRelatedState {
}
amendedPrefs.ping()
- console.log("Amended prefs are:", amendedPrefs.data)
})
const translationMode = osmConnection.GetPreference("translation-mode")
diff --git a/src/Logic/Tags/And.ts b/src/Logic/Tags/And.ts
index 378518dd14..dff0919f87 100644
--- a/src/Logic/Tags/And.ts
+++ b/src/Logic/Tags/And.ts
@@ -3,6 +3,7 @@ import { Or } from "./Or"
import { TagUtils } from "./TagUtils"
import { Tag } from "./Tag"
import { RegexTag } from "./RegexTag"
+import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export class And extends TagsFilter {
public and: TagsFilter[]
@@ -72,6 +73,10 @@ export class And extends TagsFilter {
return allChoices
}
+ asJson(): TagConfigJson {
+ return { and: this.and.map((a) => a.asJson()) }
+ }
+
asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record) {
return this.and
.map((t) => {
@@ -228,6 +233,15 @@ export class And extends TagsFilter {
return And.construct(newAnds)
}
+ /**
+ * const raw = {"and": [{"or":["leisure=playground","playground!=forest"]},{"or":["leisure=playground","playground!=forest"]}]}
+ * const parsed = TagUtils.Tag(raw)
+ * parsed.optimize().asJson() // => {"or":["leisure=playground","playground!=forest"]}
+ *
+ * const raw = {"and": [{"and":["advertising=screen"]}, {"and":["advertising~*"]}]}]
+ * const parsed = TagUtils.Tag(raw)
+ * parsed.optimize().asJson() // => "advertising=screen"
+ */
optimize(): TagsFilter | boolean {
if (this.and.length === 0) {
return true
@@ -289,9 +303,17 @@ export class And extends TagsFilter {
optimized.splice(i, 1)
i--
}
- } else if (v !== opt.value) {
- // detected an internal conflict
- return false
+ } else {
+ if (!v.match(opt.value)) {
+ // We _know_ that for the key of the RegexTag `opt`, the value will be `v`.
+ // As such, if `opt.value` cannot match `v`, we detected an internal conflict and can fail
+
+ return false
+ } else {
+ // Another tag already provided a _stricter_ value then this regex, so we can remove this one!
+ optimized.splice(i, 1)
+ i--
+ }
}
}
}
@@ -369,10 +391,13 @@ export class And extends TagsFilter {
const elements = containedOr.or.filter(
(candidate) => !commonValues.some((cv) => cv.shadows(candidate))
)
- newOrs.push(Or.construct(elements))
+ if (elements.length > 0) {
+ newOrs.push(Or.construct(elements))
+ }
+ }
+ if (newOrs.length > 0) {
+ commonValues.push(And.construct(newOrs))
}
-
- commonValues.push(And.construct(newOrs))
const result = new Or(commonValues).optimize()
if (result === false) {
return false
diff --git a/src/Logic/Tags/ComparingTag.ts b/src/Logic/Tags/ComparingTag.ts
index 685cd2550a..8009662f4b 100644
--- a/src/Logic/Tags/ComparingTag.ts
+++ b/src/Logic/Tags/ComparingTag.ts
@@ -1,18 +1,23 @@
import { TagsFilter } from "./TagsFilter"
+import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
+import { Tag } from "./Tag"
export default class ComparingTag implements TagsFilter {
private readonly _key: string
private readonly _predicate: (value: string) => boolean
- private readonly _representation: string
+ private readonly _representation: "<" | ">" | "<=" | ">="
+ private readonly _boundary: string
constructor(
key: string,
predicate: (value: string | undefined) => boolean,
- representation: string = ""
+ representation: "<" | ">" | "<=" | ">=",
+ boundary: string
) {
this._key = key
this._predicate = predicate
this._representation = representation
+ this._boundary = boundary
}
asChange(properties: Record): { k: string; v: string }[] {
@@ -20,15 +25,64 @@ export default class ComparingTag implements TagsFilter {
}
asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record) {
- return this._key + this._representation
+ return this._key + this._representation + this._boundary
}
asOverpass(): string[] {
throw "A comparable tag can not be used as overpass filter"
}
+ /**
+ * const tg = new ComparingTag("key", value => (Number(value) < 42), "<", "42")
+ * const tg0 = new ComparingTag("key", value => (Number(value) < 42), "<", "42")
+ * const tg1 = new ComparingTag("key", value => (Number(value) <= 42), "<=", "42")
+ * const against = new ComparingTag("key", value => (Number(value) > 0), ">", "0")
+ * tg.shadows(new Tag("key", "41")) // => true
+ * tg.shadows(new Tag("key", "0")) // => true
+ * tg.shadows(new Tag("key", "43")) // => false
+ * tg.shadows(new Tag("key", "0")) // => true
+ * tg.shadows(tg) // => true
+ * tg.shadows(tg0) // => true
+ * tg.shadows(against) // => false
+ * tg1.shadows(tg0) // => true
+ * tg0.shadows(tg1) // => false
+ *
+ */
shadows(other: TagsFilter): boolean {
- return other === this
+ if (other === this) {
+ return true
+ }
+ if (other instanceof ComparingTag) {
+ if (other._key !== this._key) {
+ return false
+ }
+ const selfDesc = this._representation === "<" || this._representation === "<="
+ const otherDesc = other._representation === "<" || other._representation === "<="
+ if (selfDesc !== otherDesc) {
+ return false
+ }
+ if (
+ this._boundary === other._boundary &&
+ this._representation === other._representation
+ ) {
+ return true
+ }
+ if (this._predicate(other._boundary)) {
+ return true
+ }
+ return false
+ }
+
+ if (other instanceof Tag) {
+ if (other.key !== this._key) {
+ return false
+ }
+ if (this.matchesProperties({ [other.key]: other.value })) {
+ return true
+ }
+ }
+
+ return false
}
isUsableAsAnswer(): boolean {
@@ -38,7 +92,7 @@ export default class ComparingTag implements TagsFilter {
/**
* Checks if the properties match
*
- * const t = new ComparingTag("key", (x => Number(x) < 42))
+ * const t = new ComparingTag("key", (x => Number(x) < 42), "<", "42")
* t.matchesProperties({key: 42}) // => false
* t.matchesProperties({key: 41}) // => true
* t.matchesProperties({key: 0}) // => true
@@ -56,6 +110,10 @@ export default class ComparingTag implements TagsFilter {
return []
}
+ asJson(): TagConfigJson {
+ return this._key + this._representation
+ }
+
optimize(): TagsFilter | boolean {
return this
}
diff --git a/src/Logic/Tags/Or.ts b/src/Logic/Tags/Or.ts
index f7018c40b0..a0c0f6622c 100644
--- a/src/Logic/Tags/Or.ts
+++ b/src/Logic/Tags/Or.ts
@@ -1,6 +1,7 @@
import { TagsFilter } from "./TagsFilter"
import { TagUtils } from "./TagUtils"
import { And } from "./And"
+import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export class Or extends TagsFilter {
public or: TagsFilter[]
@@ -27,6 +28,10 @@ export class Or extends TagsFilter {
return false
}
+ asJson(): TagConfigJson {
+ return { or: this.or.map((o) => o.asJson()) }
+ }
+
/**
*
* import {Tag} from "./Tag";
@@ -157,6 +162,12 @@ export class Or extends TagsFilter {
return Or.construct(newOrs)
}
+ /**
+ * const raw = {"or": [{"and":["leisure=playground","playground!=forest"]},{"and":["leisure=playground","playground!=forest"]}]}
+ * const parsed = TagUtils.Tag(raw)
+ * parsed.optimize().asJson() // => {"and":["leisure=playground","playground!=forest"]}
+ *
+ */
optimize(): TagsFilter | boolean {
if (this.or.length === 0) {
return false
@@ -174,9 +185,9 @@ export class Or extends TagsFilter {
const newOrs: TagsFilter[] = []
let containedAnds: And[] = []
for (const tf of optimized) {
- if (tf instanceof Or) {
+ if (tf["or"]) {
// expand all the nested ors...
- newOrs.push(...tf.or)
+ newOrs.push(...tf["or"])
} else if (tf instanceof And) {
// partition of all the ands
containedAnds.push(tf)
@@ -191,7 +202,7 @@ export class Or extends TagsFilter {
const cleanedContainedANds: And[] = []
outer: for (let containedAnd of containedAnds) {
for (const known of newOrs) {
- // input for optimazation: (K=V | (X=Y & K=V))
+ // input for optimization: (K=V | (X=Y & K=V))
// containedAnd: (X=Y & K=V)
// newOrs (and thus known): (K=V) --> false
const cleaned = containedAnd.removePhraseConsideredKnown(known, false)
@@ -236,16 +247,21 @@ export class Or extends TagsFilter {
const elements = containedAnd.and.filter(
(candidate) => !commonValues.some((cv) => cv.shadows(candidate))
)
+ if (elements.length == 0) {
+ continue
+ }
newAnds.push(And.construct(elements))
}
+ if (newAnds.length > 0) {
+ commonValues.push(Or.construct(newAnds))
+ }
- commonValues.push(Or.construct(newAnds))
const result = new And(commonValues).optimize()
if (result === true) {
return true
} else if (result === false) {
// neutral element: skip
- } else {
+ } else if (commonValues.length > 0) {
newOrs.push(And.construct(commonValues))
}
}
diff --git a/src/Logic/Tags/RegexTag.ts b/src/Logic/Tags/RegexTag.ts
index e6486113a7..f024f82517 100644
--- a/src/Logic/Tags/RegexTag.ts
+++ b/src/Logic/Tags/RegexTag.ts
@@ -1,5 +1,6 @@
import { Tag } from "./Tag"
import { TagsFilter } from "./TagsFilter"
+import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export class RegexTag extends TagsFilter {
public readonly key: RegExp | string
@@ -11,6 +12,9 @@ export class RegexTag extends TagsFilter {
super()
this.key = key
this.value = value
+ if (this.value instanceof RegExp && ("" + this.value).startsWith("^(^(")) {
+ throw "Detected a duplicate start marker ^(^( in a regextag:" + this.value
+ }
this.invert = invert
this.matchesEmpty = RegexTag.doesMatch("", this.value)
}
@@ -41,11 +45,21 @@ export class RegexTag extends TagsFilter {
return possibleRegex.test(fromTag)
}
- private static source(r: string | RegExp) {
+ private static source(r: string | RegExp, includeStartMarker: boolean = true) {
if (typeof r === "string") {
return r
}
- return r.source
+ if (r === undefined) {
+ return undefined
+ }
+ const src = r.source
+ if (includeStartMarker) {
+ return src
+ }
+ if (src.startsWith("^(") && src.endsWith(")$")) {
+ return src.substring(2, src.length - 2)
+ }
+ return src
}
/**
@@ -82,6 +96,24 @@ export class RegexTag extends TagsFilter {
}
}
+ /**
+ * import { TagUtils } from "./TagUtils";
+ *
+ * const t = TagUtils.Tag("a~b")
+ * t.asJson() // => "a~b"
+ *
+ * const t = TagUtils.Tag("a=")
+ * t.asJson() // => "a="
+ */
+ asJson(): TagConfigJson {
+ const v = RegexTag.source(this.value, false)
+ if (typeof this.key === "string") {
+ const oper = typeof this.value === "string" ? "=" : "~"
+ return `${this.key}${this.invert ? "!" : ""}${oper}${v}`
+ }
+ return `${this.key.source}${this.invert ? "!" : ""}~~${v}`
+ }
+
isUsableAsAnswer(): boolean {
return false
}
@@ -293,7 +325,7 @@ export class RegexTag extends TagsFilter {
if (typeof this.key === "string") {
return [this.key]
}
- throw "Key cannot be determined as it is a regex"
+ return []
}
usedTags(): { key: string; value: string }[] {
diff --git a/src/Logic/Tags/SubstitutingTag.ts b/src/Logic/Tags/SubstitutingTag.ts
index 7d5435a068..6e9d293116 100644
--- a/src/Logic/Tags/SubstitutingTag.ts
+++ b/src/Logic/Tags/SubstitutingTag.ts
@@ -1,6 +1,7 @@
import { TagsFilter } from "./TagsFilter"
import { Tag } from "./Tag"
import { Utils } from "../../Utils"
+import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
/**
* The substituting-tag uses the tags of a feature a variables and replaces them.
@@ -45,6 +46,10 @@ export default class SubstitutingTag implements TagsFilter {
)
}
+ asJson(): TagConfigJson {
+ return this._key + (this._invert ? "!" : "") + ":=" + this._value
+ }
+
asOverpass(): string[] {
throw "A variable with substitution can not be used to query overpass"
}
diff --git a/src/Logic/Tags/Tag.ts b/src/Logic/Tags/Tag.ts
index 7cb01a9d5e..b532b7053c 100644
--- a/src/Logic/Tags/Tag.ts
+++ b/src/Logic/Tags/Tag.ts
@@ -1,5 +1,6 @@
import { Utils } from "../../Utils"
import { TagsFilter } from "./TagsFilter"
+import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export class Tag extends TagsFilter {
public key: string
@@ -67,6 +68,10 @@ export class Tag extends TagsFilter {
return [`["${this.key}"="${this.value}"]`]
}
+ asJson(): TagConfigJson {
+ return this.key + "=" + this.value
+ }
+
/**
const t = new Tag("key", "value")
diff --git a/src/Logic/Tags/TagUtils.ts b/src/Logic/Tags/TagUtils.ts
index adc8629229..1893399cec 100644
--- a/src/Logic/Tags/TagUtils.ts
+++ b/src/Logic/Tags/TagUtils.ts
@@ -15,13 +15,14 @@ type Tags = Record
export type UploadableTag = Tag | SubstitutingTag | And
export class TagUtils {
- public static readonly comparators: ReadonlyArray<[string, (a: number, b: number) => boolean]> =
- [
- ["<=", (a, b) => a <= b],
- [">=", (a, b) => a >= b],
- ["<", (a, b) => a < b],
- [">", (a, b) => a > b],
- ]
+ public static readonly comparators: ReadonlyArray<
+ ["<" | ">" | "<=" | ">=", (a: number, b: number) => boolean]
+ > = [
+ ["<=", (a, b) => a <= b],
+ [">=", (a, b) => a >= b],
+ ["<", (a, b) => a < b],
+ [">", (a, b) => a > b],
+ ]
public static modeDocumentation: Record<
string,
{ name: string; docs: string; uploadable?: boolean; overpassSupport: boolean }
@@ -324,6 +325,14 @@ export class TagUtils {
return tags
}
+ static optimzeJson(json: TagConfigJson): TagConfigJson | boolean {
+ const optimized = TagUtils.Tag(json).optimize()
+ if (optimized === true || optimized === false) {
+ return optimized
+ }
+ return optimized.asJson()
+ }
+
/**
* Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set.
*
@@ -735,11 +744,10 @@ export class TagUtils {
const tag = json as string
for (const [operator, comparator] of TagUtils.comparators) {
if (tag.indexOf(operator) >= 0) {
- const split = Utils.SplitFirst(tag, operator)
-
- let val = Number(split[1].trim())
+ const split = Utils.SplitFirst(tag, operator).map((v) => v.trim())
+ let val = Number(split[1])
if (isNaN(val)) {
- val = new Date(split[1].trim()).getTime()
+ val = new Date(split[1]).getTime()
}
const f = (value: string | number | undefined) => {
@@ -762,7 +770,7 @@ export class TagUtils {
}
return comparator(b, val)
}
- return new ComparingTag(split[0], f, operator + val)
+ return new ComparingTag(split[0], f, operator, "" + val)
}
}
@@ -861,6 +869,27 @@ export class TagUtils {
return TagUtils.keyCounts.keys[key]
}
+ public static GetPopularity(tag: TagsFilter): number | undefined {
+ if (tag instanceof And) {
+ return Math.min(...Utils.NoNull(tag.and.map((t) => TagUtils.GetPopularity(t)))) - 1
+ }
+ if (tag instanceof Or) {
+ return Math.max(...Utils.NoNull(tag.or.map((t) => TagUtils.GetPopularity(t)))) + 1
+ }
+ if (tag instanceof Tag) {
+ return TagUtils.GetCount(tag.key, tag.value)
+ }
+ if (tag instanceof RegexTag) {
+ const key = tag.key
+ if (key instanceof RegExp || tag.invert || tag.isNegative()) {
+ return undefined
+ }
+ return TagUtils.GetCount(key)
+ }
+
+ return undefined
+ }
+
private static order(a: TagsFilter, b: TagsFilter, usePopularity: boolean): number {
const rta = a instanceof RegexTag
const rtb = b instanceof RegexTag
diff --git a/src/Logic/Tags/TagsFilter.ts b/src/Logic/Tags/TagsFilter.ts
index b06158b4f9..e925a76ef6 100644
--- a/src/Logic/Tags/TagsFilter.ts
+++ b/src/Logic/Tags/TagsFilter.ts
@@ -1,3 +1,5 @@
+import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
+
export abstract class TagsFilter {
abstract asOverpass(): string[]
@@ -17,6 +19,8 @@ export abstract class TagsFilter {
properties: Record
): string
+ abstract asJson(): TagConfigJson
+
abstract usedKeys(): string[]
/**
diff --git a/src/Logic/Web/MangroveReviews.ts b/src/Logic/Web/MangroveReviews.ts
index 19e2228d49..8bcfe38076 100644
--- a/src/Logic/Web/MangroveReviews.ts
+++ b/src/Logic/Web/MangroveReviews.ts
@@ -14,7 +14,7 @@ export class MangroveIdentity {
const keypairEventSource = new UIEventSource(undefined)
this.keypair = keypairEventSource
mangroveIdentity.addCallbackAndRunD(async (data) => {
- if (data === "") {
+ if (!data) {
return
}
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))
diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts
index f15232e3d7..bf2e0e5ccf 100644
--- a/src/Models/Constants.ts
+++ b/src/Models/Constants.ts
@@ -23,6 +23,7 @@ export default class Constants {
"gps_track",
"range",
"last_click",
+ "favourite",
] as const
/**
* Special layers which are not included in a theme by default
@@ -131,6 +132,8 @@ export default class Constants {
"clock",
"invalid",
"close",
+ "heart",
+ "heart_outline",
] as const
public static readonly defaultPinIcons: string[] = Constants._defaultPinIcons
diff --git a/src/Models/MenuState.ts b/src/Models/MenuState.ts
index 63dda397cf..1238580237 100644
--- a/src/Models/MenuState.ts
+++ b/src/Models/MenuState.ts
@@ -24,6 +24,7 @@ export class MenuState {
public static readonly _menuviewTabs = [
"about",
"settings",
+ "favourites",
"community",
"privacy",
"advanced",
diff --git a/src/Models/ThemeConfig/Conversion/Conversion.ts b/src/Models/ThemeConfig/Conversion/Conversion.ts
index b6422bd55b..d740287316 100644
--- a/src/Models/ThemeConfig/Conversion/Conversion.ts
+++ b/src/Models/ThemeConfig/Conversion/Conversion.ts
@@ -2,6 +2,7 @@ import { LayerConfigJson } from "../Json/LayerConfigJson"
import { Utils } from "../../../Utils"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import { ConversionContext } from "./ConversionContext"
+import { T } from "vitest/dist/types-aac763a5"
export interface DesugaringContext {
tagRenderings: Map
@@ -81,18 +82,36 @@ export class Pure extends Conversion {
}
}
+export class Bypass extends DesugaringStep {
+ private readonly _applyIf: (t: T) => boolean
+ private readonly _step: DesugaringStep
+ constructor(applyIf: (t: T) => boolean, step: DesugaringStep) {
+ super("Applies the step on the object, if the object satisfies the predicate", [], "Bypass")
+ this._applyIf = applyIf
+ this._step = step
+ }
+
+ convert(json: T, context: ConversionContext): T {
+ if (!this._applyIf(json)) {
+ return json
+ }
+ return this._step.convert(json, context)
+ }
+}
+
export class Each extends Conversion {
private readonly _step: Conversion
private readonly _msg: string
+ private readonly _filter: (x: X) => boolean
- constructor(step: Conversion, msg?: string) {
+ constructor(step: Conversion, options?: { msg?: string }) {
super(
"Applies the given step on every element of the list",
[],
"OnEach(" + step.name + ")"
)
this._step = step
- this._msg = msg
+ this._msg = options?.msg
}
convert(values: X[], context: ConversionContext): Y[] {
diff --git a/src/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts b/src/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts
index c88eb65417..aa587e65e2 100644
--- a/src/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts
+++ b/src/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts
@@ -85,7 +85,7 @@ export default class CreateNoteImportLayer extends Conversion {
}
export class AddEditingElements extends DesugaringStep {
+ static addedElements: string[] = [
+ "minimap",
+ "just_created",
+ "split_button",
+ "move_button",
+ "delete_button",
+ "last_edit",
+ "favourite_state",
+ "all_tags",
+ ]
private readonly _desugaring: DesugaringContext
constructor(desugaring: DesugaringContext) {
@@ -636,6 +649,13 @@ export class AddEditingElements extends DesugaringStep {
json.tagRenderings.push(this._desugaring.tagRenderings.get("last_edit"))
}
+ if (!usedSpecialFunctions.has("favourite_status")) {
+ json.tagRenderings.push({
+ id: "favourite_status",
+ render: { "*": "{favourite_status()}" },
+ })
+ }
+
if (!usedSpecialFunctions.has("all_tags")) {
const trc: QuestionableTagRenderingConfigJson = {
id: "all-tags",
@@ -1190,6 +1210,31 @@ class ExpandMarkerRenderings extends DesugaringStep {
}
}
+class AddFavouriteBadges extends DesugaringStep {
+ constructor() {
+ super(
+ "Adds the favourite heart to the title and the rendering badges",
+ [],
+ "AddFavouriteBadges"
+ )
+ }
+
+ convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
+ if (json.source === "special" || json.source === "special:library") {
+ return json
+ }
+ const pr = json.pointRendering?.[0]
+ if (pr) {
+ pr.iconBadges ??= []
+ if (!pr.iconBadges.some((ti) => ti.if === "_favourite=yes")) {
+ pr.iconBadges.push({ if: "_favourite=yes", then: "circle:white;heart:red" })
+ }
+ }
+
+ return json
+ }
+}
+
export class AddRatingBadge extends DesugaringStep {
constructor() {
super(
@@ -1203,6 +1248,10 @@ export class AddRatingBadge extends DesugaringStep {
if (!json.tagRenderings) {
return json
}
+ if (json.titleIcons.some((ti) => ti === "icons.rating" || ti["id"] === "rating")) {
+ // already added
+ return json
+ }
const specialVis: Exclude[] = <
Exclude[]
@@ -1238,23 +1287,28 @@ export class AutoTitleIcon extends DesugaringStep {
continue
}
const trId = titleIcon.substring("auto:".length)
- const tr = json.tagRenderings.find((tr) => tr["id"] === trId)
+ const tr = (
+ json.tagRenderings.find((tr) => tr["id"] === trId)
+ )
if (tr === undefined) {
- context
- .enters("titleIcons", i)
- .err("TagRendering with id " + trId + " not found")
+ context.enters("titleIcons", i).err("TagRendering with id " + trId + " not found")
continue
}
- const mappings: { if: TagConfigJson, then: string }[] = tr.mappings?.filter(m => m.icon !== undefined)
- .map(m => {
+ const mappings: { if: TagConfigJson; then: string }[] = tr.mappings
+ ?.filter((m) => m.icon !== undefined)
+ .map((m) => {
const path: string = typeof m.icon === "string" ? m.icon : m.icon.path
const img = ``
- return ({ if: m.if, then: img })
+ return { if: m.if, then: img }
})
if (mappings.length === 0) {
context
.enters("titleIcons", i)
- .warn("TagRendering with id " + trId + " does not have any icons, not generating an icon for this")
+ .warn(
+ "TagRendering with id " +
+ trId +
+ " does not have any icons, not generating an icon for this"
+ )
continue
}
json.titleIcons[i] = {
@@ -1292,6 +1346,7 @@ export class PrepareLayer extends Fuse {
),
new SetDefault("titleIcons", ["icons.defaults"]),
new AddRatingBadge(),
+ new AddFavouriteBadges(),
new AutoTitleIcon(),
new On(
"titleIcons",
diff --git a/src/Models/ThemeConfig/Conversion/Validation.ts b/src/Models/ThemeConfig/Conversion/Validation.ts
index 59130c1e9e..07c04ed307 100644
--- a/src/Models/ThemeConfig/Conversion/Validation.ts
+++ b/src/Models/ThemeConfig/Conversion/Validation.ts
@@ -1,4 +1,4 @@
-import { Conversion, DesugaringStep, Each, Fuse, On, Pipe, Pure } from "./Conversion"
+import { Bypass, Conversion, DesugaringStep, Each, Fuse, On } from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import LayerConfig from "../LayerConfig"
import { Utils } from "../../../Utils"
@@ -11,7 +11,6 @@ import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { ExtractImages } from "./FixImages"
import { And } from "../../../Logic/Tags/And"
import Translations from "../../../UI/i18n/Translations"
-import Svg from "../../../Svg"
import FilterConfigJson from "../Json/FilterConfigJson"
import DeleteConfig from "../DeleteConfig"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
@@ -23,7 +22,7 @@ import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { Translatable } from "../Json/Translatable"
import { ConversionContext } from "./ConversionContext"
-class ValidateLanguageCompleteness extends DesugaringStep {
+class ValidateLanguageCompleteness extends DesugaringStep {
private readonly _languages: string[]
constructor(...languages: string[]) {
@@ -35,7 +34,9 @@ class ValidateLanguageCompleteness extends DesugaringStep {
this._languages = languages ?? ["en"]
}
- convert(obj: any, context: ConversionContext): LayerConfig {
+ convert(obj: LayoutConfig, context: ConversionContext): LayoutConfig {
+ const origLayers = obj.layers
+ obj.layers = [...obj.layers].filter((l) => l["id"] !== "favourite")
const translations = Translation.ExtractAllTranslationsFrom(obj)
for (const neededLanguage of this._languages) {
translations
@@ -57,7 +58,7 @@ class ValidateLanguageCompleteness extends DesugaringStep {
)
})
}
-
+ obj.layers = origLayers
return obj
}
}
@@ -276,9 +277,9 @@ export class ValidateThemeAndLayers extends Fuse {
new On(
"layers",
new Each(
- new Pipe(
- new ValidateLayer(undefined, isBuiltin, doesImageExist, false, true),
- new Pure((x) => x?.raw)
+ new Bypass(
+ (layer) => Constants.added_by_default.indexOf(layer.id) < 0,
+ new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true)
)
)
)
@@ -974,7 +975,7 @@ export class ValidateTagRenderings extends Fuse {
"Various validation on tagRenderingConfigs",
new DetectShadowedMappings(layerConfig),
new DetectConflictingAddExtraTags(),
- new DetectNonErasedKeysInMappings(),
+ // TODO enable new DetectNonErasedKeysInMappings(),
new DetectMappingsWithImages(doesImageExist),
new On("render", new ValidatePossibleLinks()),
new On("question", new ValidatePossibleLinks()),
@@ -1356,6 +1357,34 @@ export class PrevalidateLayer extends DesugaringStep {
}
}
+export class ValidateLayerConfig extends DesugaringStep {
+ private readonly validator: ValidateLayer
+ constructor(
+ path: string,
+ isBuiltin: boolean,
+ doesImageExist: DoesImageExist,
+ studioValidations: boolean = false,
+ skipDefaultLayers: boolean = false
+ ) {
+ super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig")
+ this.validator = new ValidateLayer(
+ path,
+ isBuiltin,
+ doesImageExist,
+ studioValidations,
+ skipDefaultLayers
+ )
+ }
+
+ convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
+ const prepared = this.validator.convert(json, context)
+ if (!prepared) {
+ context.err("Preparing layer failed")
+ return undefined
+ }
+ return prepared?.raw
+ }
+}
export class ValidateLayer extends Conversion<
LayerConfigJson,
{ parsed: LayerConfig; raw: LayerConfigJson }
diff --git a/src/Models/ThemeConfig/Json/LayerConfigJson.ts b/src/Models/ThemeConfig/Json/LayerConfigJson.ts
index 50e5870bc4..7c81ddf4ca 100644
--- a/src/Models/ThemeConfig/Json/LayerConfigJson.ts
+++ b/src/Models/ThemeConfig/Json/LayerConfigJson.ts
@@ -245,7 +245,7 @@ export interface LayerConfigJson {
* Type: icon[]
* group: infobox
*/
- titleIcons?: (string | TagRenderingConfigJson)[] | ["defaults"]
+ titleIcons?: (string | (TagRenderingConfigJson & { id?: string }))[] | ["defaults"]
/**
* Creates points to render on the map.
diff --git a/src/Models/ThemeConfig/LayoutConfig.ts b/src/Models/ThemeConfig/LayoutConfig.ts
index ce538d6b29..77ee2ef489 100644
--- a/src/Models/ThemeConfig/LayoutConfig.ts
+++ b/src/Models/ThemeConfig/LayoutConfig.ts
@@ -305,6 +305,9 @@ export default class LayoutConfig implements LayoutInformation {
}
for (const layer of this.layers) {
if (!layer.source) {
+ if (layer.isShown?.matchesProperties(tags)) {
+ return layer
+ }
continue
}
if (layer.source.osmTags.matchesProperties(tags)) {
diff --git a/src/Models/ThemeConfig/TagRenderingConfig.ts b/src/Models/ThemeConfig/TagRenderingConfig.ts
index 424141bd48..17c8404893 100644
--- a/src/Models/ThemeConfig/TagRenderingConfig.ts
+++ b/src/Models/ThemeConfig/TagRenderingConfig.ts
@@ -16,10 +16,10 @@ import {
} from "./Json/QuestionableTagRenderingConfigJson"
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import { Paragraph } from "../../UI/Base/Paragraph"
-import Svg from "../../Svg"
import Validators, { ValidatorType } from "../../UI/InputElement/Validators"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import Constants from "../Constants"
+import { RegexTag } from "../../Logic/Tags/RegexTag"
export interface Icon {}
@@ -800,4 +800,25 @@ export default class TagRenderingConfig {
labels,
]).SetClass("flex flex-col")
}
+
+ public usedTags(): TagsFilter[] {
+ const tags: TagsFilter[] = []
+ tags.push(
+ this.metacondition,
+ this.condition,
+ this.freeform?.key ? new RegexTag(this.freeform?.key, /.*/) : undefined,
+ this.invalidValues
+ )
+ for (const m of this.mappings ?? []) {
+ tags.push(m.if)
+ tags.push(m.priorityIf)
+ tags.push(...(m.addExtraTags ?? []))
+ if (typeof m.hideInAnswer !== "boolean") {
+ tags.push(m.hideInAnswer)
+ }
+ tags.push(m.ifnot)
+ }
+
+ return Utils.NoNull(tags)
+ }
}
diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts
index 0cc0119e6c..1db7f7ac5e 100644
--- a/src/Models/ThemeViewState.ts
+++ b/src/Models/ThemeViewState.ts
@@ -58,6 +58,7 @@ import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLay
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import { Imgur } from "../Logic/ImageProviders/Imgur"
import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSource"
+import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
/**
*
@@ -96,10 +97,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
readonly currentView: FeatureSource>
readonly featuresInView: FeatureSource
+ readonly favourites: FavouritesFeatureSource
/**
* Contains a few (<10) >features that are near the center of the map.
*/
- readonly closestFeatures: FeatureSource
+ readonly closestFeatures: NearbyFeatureSource
readonly newFeatures: WritableFeatureSource
readonly layerState: LayerState
readonly perLayer: ReadonlyMap
@@ -220,8 +222,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.fullNodeDatabase
)
- this.indexedFeatures = layoutSource
-
let currentViewIndex = 0
const empty = []
this.currentView = new StaticFeatureSource(
@@ -242,13 +242,13 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
this.dataIsLoading = layoutSource.isLoading
+ this.indexedFeatures = layoutSource
+ this.featureProperties = new FeaturePropertiesStore(layoutSource)
- const indexedElements = this.indexedFeatures
- this.featureProperties = new FeaturePropertiesStore(indexedElements)
this.changes = new Changes(
{
dryRun: this.featureSwitches.featureSwitchIsTesting,
- allElements: indexedElements,
+ allElements: layoutSource,
featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations,
@@ -258,7 +258,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.historicalUserLocations = this.geolocation.historicalUserLocations
this.newFeatures = new NewGeometryFromChangesFeatureSource(
this.changes,
- indexedElements,
+ layoutSource,
this.featureProperties
)
layoutSource.addSource(this.newFeatures)
@@ -327,10 +327,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
return sorted
})
- const lastClick = (this.lastClickObject = new LastClickFeatureSource(
+ this.lastClickObject = new LastClickFeatureSource(
this.mapProperties.lastClickLocation,
this.layout
- ))
+ )
this.osmObjectDownloader = new OsmObjectDownloader(
this.osmConnection.Backend(),
@@ -353,6 +353,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.osmConnection,
this.changes
)
+ this.favourites = new FavouritesFeatureSource(this)
this.initActors()
this.drawSpecialLayers()
@@ -456,6 +457,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
* @private
*/
private selectClosestAtCenter(i: number = 0) {
+ this.mapProperties.lastKeyNavigation.setData(Date.now() / 1000)
const toSelect = this.closestFeatures.features.data[i]
if (!toSelect) {
return
@@ -465,6 +467,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.selectedLayer.setData(layer)
this.selectedElement.setData(toSelect)
}
+
private initHotkeys() {
Hotkeys.RegisterHotkey(
{ nomod: "Escape", onUp: true },
@@ -476,6 +479,15 @@ export default class ThemeViewState implements SpecialVisualizationState {
}
)
+ Hotkeys.RegisterHotkey(
+ { nomod: "f" },
+ Translations.t.hotkeyDocumentation.selectFavourites,
+ () => {
+ this.guistate.menuViewTab.setData("favourites")
+ this.guistate.menuIsOpened.setData(true)
+ }
+ )
+
this.mapProperties.lastKeyNavigation.addCallbackAndRunD((_) => {
Hotkeys.RegisterHotkey(
{
@@ -561,46 +573,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
})
}
- private addLastClick(last_click: LastClickFeatureSource) {
- // The last_click gets a _very_ special treatment as it interacts with various parts
-
- this.featureProperties.trackFeatureSource(last_click)
- this.indexedFeatures.addSource(last_click)
-
- last_click.features.addCallbackAndRunD((features) => {
- if (this.selectedLayer.data?.id === "last_click") {
- // The last-click location moved, but we have selected the last click of the previous location
- // So, we update _after_ clearing the selection to make sure no stray data is sticking around
- this.selectedElement.setData(undefined)
- this.selectedElement.setData(features[0])
- }
- })
-
- new ShowDataLayer(this.map, {
- features: new FilteringFeatureSource(this.newPointDialog, last_click),
- doShowLayer: this.featureSwitches.featureSwitchEnableLogin,
- layer: this.newPointDialog.layerDef,
- selectedElement: this.selectedElement,
- selectedLayer: this.selectedLayer,
- metaTags: this.userRelatedState.preferencesAsTags,
- onClick: (feature: Feature) => {
- if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) {
- this.map.data.flyTo({
- zoom: Constants.minZoomLevelToAddNewPoint,
- center: this.mapProperties.lastClickLocation.data,
- })
- return
- }
- // We first clear the selection to make sure no weird state is around
- this.selectedLayer.setData(undefined)
- this.selectedElement.setData(undefined)
-
- this.selectedElement.setData(feature)
- this.selectedLayer.setData(this.newPointDialog.layerDef)
- },
- })
- }
-
/**
* Add the special layers to the map
*/
@@ -627,7 +599,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
)
),
current_view: this.currentView,
+ favourite: this.favourites,
}
+
+ this.closestFeatures.registerSource(specialLayers.favourite, "favourite")
if (this.layout?.lockLocation) {
const bbox = new BBox(this.layout.lockLocation)
this.mapProperties.maxbounds.setData(bbox)
@@ -654,21 +629,23 @@ export default class ThemeViewState implements SpecialVisualizationState {
}
const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range")
-
const rangeIsDisplayed = rangeFLayer?.isDisplayed
-
if (
!QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef))
) {
rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
}
+ // enumarate all 'normal' layers and match them with the appropriate 'special' layer - if applicable
this.layerState.filteredLayers.forEach((flayer) => {
const id = flayer.layerDef.id
const features: FeatureSource = specialLayers[id]
if (features === undefined) {
return
}
+ if (id === "favourite") {
+ console.log("Matching special layer", id, flayer)
+ }
this.featureProperties.trackFeatureSource(features)
new ShowDataLayer(this.map, {
diff --git a/src/UI/Base/LoginButton.svelte b/src/UI/Base/LoginButton.svelte
index 504d487132..7381f7b781 100644
--- a/src/UI/Base/LoginButton.svelte
+++ b/src/UI/Base/LoginButton.svelte
@@ -11,7 +11,7 @@
diff --git a/src/UI/Base/LogoutButton.svelte b/src/UI/Base/LogoutButton.svelte
index f42bcf7511..996fea08a8 100644
--- a/src/UI/Base/LogoutButton.svelte
+++ b/src/UI/Base/LogoutButton.svelte
@@ -4,12 +4,12 @@
import Translations from "../i18n/Translations"
import Tr from "./Tr.svelte"
- export let osmConnection: OsmConnection
+ export let osmConnection: OsmConnection;