forked from MapComplete/MapComplete
Merge pull request #1731 from pietervdvn/feature/favourites
Feature/favourites
This commit is contained in:
commit
4197ec0055
80 changed files with 2715 additions and 1059 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
|
||||
|
|
29
Docs/UserTests/2023-12-4 User Test Favourites.md
Normal file
29
Docs/UserTests/2023-12-4 User Test Favourites.md
Normal file
|
@ -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.
|
|
@ -29,7 +29,8 @@
|
|||
"natural=stone"
|
||||
]
|
||||
},
|
||||
"climbing="
|
||||
"climbing=",
|
||||
"sport!=climbing"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
47
assets/layers/favourite/favourite.proto.json
Normal file
47
assets/layers/favourite/favourite.proto.json
Normal file
|
@ -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": [
|
||||
|
||||
]
|
||||
}
|
|
@ -14,7 +14,8 @@
|
|||
{
|
||||
"id": "wikipedialink",
|
||||
"labels": [
|
||||
"defaults"
|
||||
"defaults",
|
||||
"in_favourite"
|
||||
],
|
||||
"render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank' rel='noopener'><img src='./assets/svg/wikipedia.svg' textmode='📖' alt='Wikipedia'/></a>",
|
||||
"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": "<a href='tel:{phone}'><img textmode='📞' alt='phone' src='./assets/layers/questions/phone.svg'/></a>",
|
||||
"mappings": [
|
||||
|
@ -89,7 +103,8 @@
|
|||
{
|
||||
"id": "emaillink",
|
||||
"labels": [
|
||||
"defaults"
|
||||
"defaults",
|
||||
"in_favourite"
|
||||
],
|
||||
"render": "<a href='mailto:{email}'><img textmode='✉️' alt='email' src='./assets/layers/questions/send_email.svg'/></a>",
|
||||
"mappings": [
|
||||
|
@ -109,7 +124,8 @@
|
|||
{
|
||||
"id": "websitelink",
|
||||
"labels": [
|
||||
"defaults"
|
||||
"defaults",
|
||||
"in_favourite"
|
||||
],
|
||||
"render": "<a href='{website}' target='_blank' rel='noopener'><img textmode='🌐' alt='website' src='./assets/layers/icons/website.svg'/></a>",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
62
assets/svg/center.svg
Normal file
62
assets/svg/center.svg
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="544.02838"
|
||||
height="544.02838"
|
||||
viewBox="0 0 544.02838 544.02838"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="center.svg"
|
||||
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showguides="true"
|
||||
inkscape:zoom="0.90326851"
|
||||
inkscape:cx="393.57068"
|
||||
inkscape:cy="250.756"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="995"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1">
|
||||
<sodipodi:guide
|
||||
position="171.95879,103.32864"
|
||||
orientation="0,-1"
|
||||
id="guide4"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="271.68286,132.35281"
|
||||
orientation="1,0"
|
||||
id="guide5"
|
||||
inkscape:locked="false" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
d="m 365.63918,111.75001 h -62.375 V 15.9375 c 0,-8.75 -7,-15.9375 -15.625,-15.9375 h -31.1875 c -8.5625,0 -15.625,7.1875 -15.625,15.9375 v 95.81251 h -62.375 l 93.5625,127.75 z"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="ccsssscccc" />
|
||||
<path
|
||||
d="m 432.27837,365.63919 v -62.375 h 95.8125 c 8.75,0 15.9375,-7 15.9375,-15.625 v -31.1875 c 0,-8.5625 -7.1875,-15.625 -15.9375,-15.625 h -95.8125 v -62.375 l -127.75,93.5625 z"
|
||||
id="path1-5"
|
||||
sodipodi:nodetypes="ccsssscccc" />
|
||||
<path
|
||||
d="m 178.38918,432.27838 h 62.375 v 95.8125 c 0,8.75 7,15.9375 15.625,15.9375 h 31.1875 c 8.5625,0 15.625,-7.1875 15.625,-15.9375 v -95.8125 h 62.375 l -93.5625,-127.75 z"
|
||||
id="path2"
|
||||
sodipodi:nodetypes="ccsssscccc" />
|
||||
<path
|
||||
d="m 111.75,178.38919 v 62.375 H 15.9375 c -8.75,0 -15.9375,7 -15.9375,15.625 v 31.1875 c 0,8.5625 7.1875,15.625 15.9375,15.625 H 111.75 v 62.375 l 127.74999,-93.5625 z"
|
||||
id="path3"
|
||||
sodipodi:nodetypes="ccsssscccc" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
2
assets/svg/center.svg.license
Normal file
2
assets/svg/center.svg.license
Normal file
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: Pieter Vander Vennet
|
||||
SPDX-License-Identifier: CC0-1.0
|
|
@ -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",
|
||||
|
|
|
@ -69,10 +69,12 @@
|
|||
},
|
||||
"+titleIcons": [
|
||||
{
|
||||
"id": "climbing_length",
|
||||
"render": "<div class='flex' style='word-wrap: normal; padding-right: 0.25rem;'><img src='./assets/themes/climbing/height.svg' style='height: 1.75rem;'/>{climbing:length}m</div>",
|
||||
"condition": "climbing:length~*"
|
||||
},
|
||||
{
|
||||
"id": "climbing_bolts",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "__bolts_max~*",
|
||||
|
@ -95,6 +97,7 @@
|
|||
"render": "<div class='w-8 flex justify-center rounded-right-full climbing-{__difficulty_max:char}'> {__difficulty_max}</div>"
|
||||
},
|
||||
{
|
||||
"id": "difficulty",
|
||||
"render": "<div class='flex justify-center rounded-full pl-1 pr-1 climbing-{__difficulty:char}'> {climbing:grade:french}</div>",
|
||||
"condition": "__difficulty:char~*"
|
||||
}
|
||||
|
|
|
@ -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ć"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -349,4 +349,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,22 @@
|
|||
"panelIntro": "<h3>Your personal theme</h3>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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
304
scripts/generateFavouritesLayer.ts
Normal file
304
scripts/generateFavouritesLayer.ts
Normal file
|
@ -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 = <QuestionableTagRenderingConfigJson>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 = <QuestionableTagRenderingConfigJson>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<string>()
|
||||
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 = <TagRenderingConfigJson>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: <Translatable>tr.render,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
mappings.push({ if: tags, then: <Record<string, string>>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<void> {
|
||||
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()
|
|
@ -27,7 +27,8 @@ function genImages(dryrun = false) {
|
|||
"star_outline",
|
||||
"star",
|
||||
"osm_logo_us",
|
||||
|
||||
"triangle",
|
||||
"teardrop_with_hole_green",
|
||||
"SocialImageForeground",
|
||||
"wikipedia",
|
||||
"Upload",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)),
|
||||
},
|
||||
|
|
|
@ -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<string, { data: LayoutConfig } | { func: () => LayoutConfig }> =
|
||||
new Map()
|
||||
constructor() {
|
||||
private readonly raw: Map<string, LayoutConfigJson> = new Map()
|
||||
private readonly dict: Map<string, LayoutConfig> = new Map()
|
||||
|
||||
constructor(includeFavouriteLayer = true) {
|
||||
for (const layoutConfigJson of known_themes["themes"]) {
|
||||
this.dict.set(layoutConfigJson.id, {
|
||||
func: () => {
|
||||
const layout = new LayoutConfig(<LayoutConfigJson>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() {
|
||||
|
|
|
@ -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<Feature>
|
||||
featureProperties: { getStore: (id: string) => UIEventSource<Record<string, string>> }
|
||||
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<Feature>
|
||||
featureProperties: FeaturePropertiesStore
|
||||
changes: Changes
|
||||
osmConnection: OsmConnection
|
||||
layout: LayoutConfig
|
||||
osmObjectDownloader: OsmObjectDownloader
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
}
|
||||
|
||||
constructor(state: {
|
||||
selectedElement: UIEventSource<Feature>
|
||||
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)
|
||||
}
|
||||
|
|
220
src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts
Normal file
220
src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts
Normal file
|
@ -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<string[]>
|
||||
|
||||
/**
|
||||
* All favourites, including the ones which are filtered away because they are already displayed
|
||||
*/
|
||||
public readonly allFavourites: Store<Feature[]>
|
||||
|
||||
constructor(state: SpecialVisualizationState) {
|
||||
const features: Store<Feature[]> = Stores.ListStabilized(
|
||||
state.osmConnection.preferencesHandler.preferences.map((prefs) => {
|
||||
const feats: Feature[] = []
|
||||
const allIds = new Set<string>()
|
||||
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<string, string>): 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<string, string>,
|
||||
id: string
|
||||
): Record<string, string> {
|
||||
const properties: Record<string, string> = {}
|
||||
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<string, string>) {
|
||||
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<Record<string, string>>) {
|
||||
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<Record<string, string> & { 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<Record<string, string>> },
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,10 +6,14 @@ import FilteringFeatureSource from "./FilteringFeatureSource"
|
|||
import LayerState from "../../State/LayerState"
|
||||
|
||||
export default class NearbyFeatureSource implements FeatureSource {
|
||||
private readonly _result = new UIEventSource<Feature[]>(undefined)
|
||||
|
||||
public readonly features: Store<Feature[]>
|
||||
private readonly _targetPoint: Store<{ lon: number; lat: number }>
|
||||
private readonly _numberOfNeededFeatures: number
|
||||
private readonly _layerState?: LayerState
|
||||
private readonly _currentZoom: Store<number>
|
||||
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<number>
|
||||
) {
|
||||
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<Feature[]>(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
|
||||
*/
|
||||
|
|
|
@ -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<Point, { date?: string; altitude?: number | string }>[],
|
||||
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<string, edge>()
|
||||
|
||||
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 = ` <wpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">`
|
||||
for (const key in l.properties) {
|
||||
const keyCleaned = key.replaceAll(":", "__")
|
||||
trkpt += ` <${keyCleaned}>${l.properties[key]}</${keyCleaned}>\n`
|
||||
if (key === "website") {
|
||||
trkpt += ` <link>${l.properties[key]}</link>\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 += " </wpt>\n"
|
||||
trackPoints.push(trkpt)
|
||||
}
|
||||
|
||||
// Lets merge them back together!
|
||||
|
||||
let didMergeSomething = false
|
||||
let allMergedEdges = Array.from(allEdgesByKey.values())
|
||||
const allEdgesByStartPoint = new Map<string, edge[]>()
|
||||
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<edge>()
|
||||
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 =
|
||||
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
|
||||
return (
|
||||
header +
|
||||
"\n<name>" +
|
||||
title +
|
||||
"</name>\n<trk><trkseg>\n" +
|
||||
trackPoints.join("\n") +
|
||||
"\n</trkseg></trk></gpx>"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<LinkImageAction> {
|
||||
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)
|
||||
|
|
|
@ -12,6 +12,10 @@ export class OsmPreferences {
|
|||
"all-osm-preferences",
|
||||
{}
|
||||
)
|
||||
/**
|
||||
* A map containing the individual preference sources
|
||||
* @private
|
||||
*/
|
||||
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
|
||||
private auth: any
|
||||
private userDetails: UIEventSource<UserDetails>
|
||||
|
@ -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<string>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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<string, string>) {
|
||||
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
|
||||
|
|
|
@ -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<string, string>): { k: string; v: string }[] {
|
||||
|
@ -20,15 +25,64 @@ export default class ComparingTag implements TagsFilter {
|
|||
}
|
||||
|
||||
asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }[] {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -15,13 +15,14 @@ type Tags = Record<string, string>
|
|||
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
|
||||
|
|
|
@ -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, string>
|
||||
): string
|
||||
|
||||
abstract asJson(): TagConfigJson
|
||||
|
||||
abstract usedKeys(): string[]
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,7 +14,7 @@ export class MangroveIdentity {
|
|||
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
|
||||
this.keypair = keypairEventSource
|
||||
mangroveIdentity.addCallbackAndRunD(async (data) => {
|
||||
if (data === "") {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))
|
||||
|
|
|
@ -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[] = <any>Constants._defaultPinIcons
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ export class MenuState {
|
|||
public static readonly _menuviewTabs = [
|
||||
"about",
|
||||
"settings",
|
||||
"favourites",
|
||||
"community",
|
||||
"privacy",
|
||||
"advanced",
|
||||
|
|
|
@ -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<string, QuestionableTagRenderingConfigJson>
|
||||
|
@ -81,18 +82,36 @@ export class Pure<TIn, TOut> extends Conversion<TIn, TOut> {
|
|||
}
|
||||
}
|
||||
|
||||
export class Bypass<T> extends DesugaringStep<T> {
|
||||
private readonly _applyIf: (t: T) => boolean
|
||||
private readonly _step: DesugaringStep<T>
|
||||
constructor(applyIf: (t: T) => boolean, step: DesugaringStep<T>) {
|
||||
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<X, Y> extends Conversion<X[], Y[]> {
|
||||
private readonly _step: Conversion<X, Y>
|
||||
private readonly _msg: string
|
||||
private readonly _filter: (x: X) => boolean
|
||||
|
||||
constructor(step: Conversion<X, Y>, msg?: string) {
|
||||
constructor(step: Conversion<X, Y>, 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[] {
|
||||
|
|
|
@ -85,7 +85,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
|
|||
description: trs(t.description, { title: layer.title.render }),
|
||||
source: {
|
||||
osmTags: {
|
||||
and: ["id~*"],
|
||||
and: ["id~[0-9]+", "comment_url~.*notes/[0-9]*g.json"],
|
||||
},
|
||||
geoJson:
|
||||
"https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=" +
|
||||
|
|
|
@ -10,7 +10,10 @@ import {
|
|||
SetDefault,
|
||||
} from "./Conversion"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||
import {
|
||||
MinimalTagRenderingConfigJson,
|
||||
TagRenderingConfigJson,
|
||||
} from "../Json/TagRenderingConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import RewritableConfigJson from "../Json/RewritableConfigJson"
|
||||
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
|
||||
|
@ -563,6 +566,16 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
|
||||
export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
||||
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<LayerConfigJson> {
|
|||
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<IconConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
class AddFavouriteBadges extends DesugaringStep<LayerConfigJson> {
|
||||
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<LayerConfigJson> {
|
||||
constructor() {
|
||||
super(
|
||||
|
@ -1203,6 +1248,10 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
|
|||
if (!json.tagRenderings) {
|
||||
return json
|
||||
}
|
||||
if (json.titleIcons.some((ti) => ti === "icons.rating" || ti["id"] === "rating")) {
|
||||
// already added
|
||||
return json
|
||||
}
|
||||
|
||||
const specialVis: Exclude<RenderingSpecification, string>[] = <
|
||||
Exclude<RenderingSpecification, string>[]
|
||||
|
@ -1238,23 +1287,28 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
|
|||
continue
|
||||
}
|
||||
const trId = titleIcon.substring("auto:".length)
|
||||
const tr = <QuestionableTagRenderingConfigJson>json.tagRenderings.find((tr) => tr["id"] === trId)
|
||||
const tr = <QuestionableTagRenderingConfigJson>(
|
||||
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 = `<img class="m-1 h-6 w-6 low-interaction rounded" src='${path}'/>`
|
||||
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] = <TagRenderingConfigJson>{
|
||||
|
@ -1292,6 +1346,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
),
|
||||
new SetDefault("titleIcons", ["icons.defaults"]),
|
||||
new AddRatingBadge(),
|
||||
new AddFavouriteBadges(),
|
||||
new AutoTitleIcon(),
|
||||
new On(
|
||||
"titleIcons",
|
||||
|
|
|
@ -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<any> {
|
||||
class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> {
|
||||
private readonly _languages: string[]
|
||||
|
||||
constructor(...languages: string[]) {
|
||||
|
@ -35,7 +34,9 @@ class ValidateLanguageCompleteness extends DesugaringStep<any> {
|
|||
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<any> {
|
|||
)
|
||||
})
|
||||
}
|
||||
|
||||
obj.layers = origLayers
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
@ -276,9 +277,9 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
|||
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(<any>layer.id) < 0,
|
||||
new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -974,7 +975,7 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
|
|||
"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<LayerConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
|
||||
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 }
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Feature<Polygon>>
|
||||
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<string, GeoIndexedStoreForLayer>
|
||||
|
@ -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, {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<button class={clss} on:click={() => osmConnection.AttemptLogin()}>
|
||||
<ToSvelte construct={Svg.login_svg().SetClass("w-12 m-1")} />
|
||||
<slot name="message">
|
||||
<slot>
|
||||
<Tr t={Translations.t.general.loginWithOpenStreetMap} />
|
||||
</slot>
|
||||
</button>
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import Tr from "./Tr.svelte"
|
||||
|
||||
export let osmConnection: OsmConnection
|
||||
export let osmConnection: OsmConnection;
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click={() => {
|
||||
state.osmConnection.LogOut()
|
||||
osmConnection.LogOut()
|
||||
}}
|
||||
>
|
||||
<Logout class="h-6 w-6" />
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
const uiElem = typeof construct === "function" ? construct() : construct
|
||||
html = uiElem?.ConstructElement()
|
||||
if (html !== undefined) {
|
||||
elem.replaceWith(html)
|
||||
elem?.replaceWith(html)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -121,9 +121,9 @@ export default class UploadTraceToOsmUI extends LoginToggle {
|
|||
]).SetClass("flex p-2 rounded-xl border-2 subtle-border items-center"),
|
||||
new Toggle(
|
||||
confirmPanel,
|
||||
new SubtleButton(new SvelteUIElement(Upload), t.title).onClick(() =>
|
||||
clicked.setData(true)
|
||||
),
|
||||
new SubtleButton(new SvelteUIElement(Upload), t.title)
|
||||
.onClick(() => clicked.setData(true))
|
||||
.SetClass("w-full"),
|
||||
clicked
|
||||
),
|
||||
uploadFinished
|
||||
|
|
83
src/UI/Favourites/FavouriteSummary.svelte
Normal file
83
src/UI/Favourites/FavouriteSummary.svelte
Normal file
|
@ -0,0 +1,83 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte";
|
||||
import type { Feature } from "geojson";
|
||||
import { ImmutableStore } from "../../Logic/UIEventSource";
|
||||
import { GeoOperations } from "../../Logic/GeoOperations";
|
||||
import Center from "../../assets/svg/Center.svelte";
|
||||
|
||||
export let feature: Feature;
|
||||
let properties: Record<string, string> = feature.properties;
|
||||
export let state: SpecialVisualizationState;
|
||||
let tags = state.featureProperties.getStore(properties.id) ?? new ImmutableStore(properties);
|
||||
|
||||
const favLayer = state.layerState.filteredLayers.get("favourite");
|
||||
const favConfig = favLayer.layerDef;
|
||||
const titleConfig = favConfig.title;
|
||||
|
||||
function center() {
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature);
|
||||
state.mapProperties.location.setData(
|
||||
{ lon, lat }
|
||||
);
|
||||
const z = state.mapProperties.zoom.data
|
||||
state.mapProperties.zoom.setData( Math.min(17, Math.max(12, z )) )
|
||||
state.guistate.menuIsOpened.setData(false);
|
||||
}
|
||||
|
||||
function select() {
|
||||
state.selectedLayer.setData(favConfig);
|
||||
state.selectedElement.setData(feature);
|
||||
center();
|
||||
}
|
||||
|
||||
const coord = GeoOperations.centerpointCoordinates(feature);
|
||||
const distance = state.mapProperties.location.stabilized(500).mapD(({ lon, lat }) => {
|
||||
let meters = Math.round(GeoOperations.distanceBetween(coord, [lon, lat]));
|
||||
|
||||
if (meters < 1000) {
|
||||
return meters + "m";
|
||||
}
|
||||
|
||||
meters = Math.round(meters / 100);
|
||||
const kmStr = "" + meters;
|
||||
|
||||
|
||||
return kmStr.substring(0, kmStr.length - 1) + "." + kmStr.substring(kmStr.length - 1) + "km";
|
||||
});
|
||||
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"];
|
||||
|
||||
</script>
|
||||
|
||||
<div class="px-1 my-1 border-2 border-dashed border-gray-300 rounded grid grid-cols-2 items-center no-weblate">
|
||||
<button class="cursor-pointer ml-1 m-0 link justify-self-start" on:click={() => select()}>
|
||||
<TagRenderingAnswer config={titleConfig} extraClasses="underline" layer={favConfig} selectedElement={feature}
|
||||
{tags} />
|
||||
</button>
|
||||
|
||||
|
||||
<div class="flex items-center justify-self-end title-icons links-as-button gap-x-0.5 p-1 pt-0.5 sm:pt-1">
|
||||
{#each favConfig.titleIcons as titleIconConfig}
|
||||
{#if (titleIconBlacklist.indexOf(titleIconConfig.id) < 0) && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...properties, ...state.userRelatedState.preferencesAsTags.data }) ?? true) && titleIconConfig.IsKnown(properties)}
|
||||
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
|
||||
<TagRenderingAnswer
|
||||
config={titleIconConfig}
|
||||
{tags}
|
||||
selectedElement={feature}
|
||||
{state}
|
||||
layer={favLayer}
|
||||
extraClasses="h-full justify-center"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<button class="p-1" on:click={() => center()}>
|
||||
<Center class="w-6 h-6" />
|
||||
</button>
|
||||
<div class="w-14">
|
||||
{$distance}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
68
src/UI/Favourites/Favourites.svelte
Normal file
68
src/UI/Favourites/Favourites.svelte
Normal file
|
@ -0,0 +1,68 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import FavouriteSummary from "./FavouriteSummary.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { Utils } from "../../Utils";
|
||||
import { GeoOperations } from "../../Logic/GeoOperations";
|
||||
import type { Feature, LineString, Point } from "geojson";
|
||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||
import LoginButton from "../Base/LoginButton.svelte";
|
||||
|
||||
/**
|
||||
* A panel showing all your favourites
|
||||
*/
|
||||
export let state: SpecialVisualizationState;
|
||||
let favourites = state.favourites.allFavourites;
|
||||
|
||||
function downloadGeojson() {
|
||||
const contents = { features: favourites.data, type: "FeatureCollection" };
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
JSON.stringify(contents),
|
||||
"mapcomplete-favourites-" + (new Date().toISOString()) + ".geojson",
|
||||
{
|
||||
mimetype: "application/vnd.geo+json"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function downloadGPX() {
|
||||
const gpx = GeoOperations.toGpxPoints(<Feature<Point>>favourites.data, "MapComplete favourites");
|
||||
Utils.offerContentsAsDownloadableFile(gpx,
|
||||
"mapcomplete-favourites-" + (new Date().toISOString()) + ".gpx",
|
||||
{
|
||||
mimetype: "{gpx=application/gpx+xml}"
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<LoginToggle {state}>
|
||||
<div slot="not-logged-in">
|
||||
<LoginButton osmConnection={state.osmConnection}>
|
||||
<Tr t={Translations.t.favouritePoi.loginToSeeList}/>
|
||||
</LoginButton>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
|
||||
<Tr t={Translations.t.favouritePoi.intro.Subs({length: $favourites?.length ?? 0})} />
|
||||
<Tr t={Translations.t.favouritePoi.privacy} />
|
||||
|
||||
{#each $favourites as feature (feature.properties.id)}
|
||||
<FavouriteSummary {feature} {state} />
|
||||
{/each}
|
||||
|
||||
<div class="mt-8">
|
||||
<button class="flex p-2" on:click={() => downloadGeojson()}>
|
||||
<DownloadIcon class="h-6 w-6" />
|
||||
<Tr t={Translations.t.favouritePoi.downloadGeojson} />
|
||||
</button>
|
||||
<button class="flex p-2" on:click={() => downloadGPX()}>
|
||||
<DownloadIcon class="h-6 w-6" />
|
||||
<Tr t={Translations.t.favouritePoi.downloadGpx} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</LoginToggle>
|
|
@ -1,27 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Pin from "../../assets/svg/Pin.svelte"
|
||||
import Square from "../../assets/svg/Square.svelte"
|
||||
import Circle from "../../assets/svg/Circle.svelte"
|
||||
import Checkmark from "../../assets/svg/Checkmark.svelte"
|
||||
import Clock from "../../assets/svg/Clock.svelte"
|
||||
import Close from "../../assets/svg/Close.svelte"
|
||||
import Crosshair from "../../assets/svg/Crosshair.svelte"
|
||||
import Help from "../../assets/svg/Help.svelte"
|
||||
import Home from "../../assets/svg/Home.svelte"
|
||||
import Invalid from "../../assets/svg/Invalid.svelte"
|
||||
import Location from "../../assets/svg/Location.svelte"
|
||||
import Location_empty from "../../assets/svg/Location_empty.svelte"
|
||||
import Location_locked from "../../assets/svg/Location_locked.svelte"
|
||||
import Note from "../../assets/svg/Note.svelte"
|
||||
import Resolved from "../../assets/svg/Resolved.svelte"
|
||||
import Ring from "../../assets/svg/Ring.svelte"
|
||||
import Scissors from "../../assets/svg/Scissors.svelte"
|
||||
import Teardrop from "../../assets/svg/Teardrop.svelte"
|
||||
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"
|
||||
import Triangle from "../../assets/svg/Triangle.svelte"
|
||||
import Icon from "./Icon.svelte"
|
||||
import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
/**
|
||||
* Renders a single icon.
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
/**
|
||||
* Renders a 'marker', which consists of multiple 'icons'
|
||||
*/
|
||||
export let marker: IconConfig[] = config?.marker
|
||||
export let marker: IconConfig[] = config?.marker;
|
||||
export let tags: Store<Record<string, string>>
|
||||
export let rotation: TagRenderingConfig
|
||||
export let rotation: TagRenderingConfig = undefined;
|
||||
let _rotation = rotation
|
||||
? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt)
|
||||
: new ImmutableStore(0)
|
||||
|
@ -18,7 +18,9 @@
|
|||
{#if marker && marker}
|
||||
<div class="relative h-full w-full" style={`transform: rotate(${$_rotation})`}>
|
||||
{#each marker as icon}
|
||||
<DynamicIcon {icon} {tags} />
|
||||
<div class="absolute top-0 left-0 h-full w-full">
|
||||
<DynamicIcon {icon} {tags} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
<script lang="ts">
|
||||
import Pin from "../../assets/svg/Pin.svelte"
|
||||
import Square from "../../assets/svg/Square.svelte"
|
||||
import Circle from "../../assets/svg/Circle.svelte"
|
||||
import Checkmark from "../../assets/svg/Checkmark.svelte"
|
||||
import Clock from "../../assets/svg/Clock.svelte"
|
||||
import Close from "../../assets/svg/Close.svelte"
|
||||
import Crosshair from "../../assets/svg/Crosshair.svelte"
|
||||
import Help from "../../assets/svg/Help.svelte"
|
||||
import Home from "../../assets/svg/Home.svelte"
|
||||
import Invalid from "../../assets/svg/Invalid.svelte"
|
||||
import Location from "../../assets/svg/Location.svelte"
|
||||
import Location_empty from "../../assets/svg/Location_empty.svelte"
|
||||
import Location_locked from "../../assets/svg/Location_locked.svelte"
|
||||
import Note from "../../assets/svg/Note.svelte"
|
||||
import Resolved from "../../assets/svg/Resolved.svelte"
|
||||
import Ring from "../../assets/svg/Ring.svelte"
|
||||
import Scissors from "../../assets/svg/Scissors.svelte"
|
||||
import Teardrop from "../../assets/svg/Teardrop.svelte"
|
||||
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"
|
||||
import Triangle from "../../assets/svg/Triangle.svelte"
|
||||
import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte"
|
||||
import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte"
|
||||
import Gps_arrow from "../../assets/svg/Gps_arrow.svelte"
|
||||
import Pin from "../../assets/svg/Pin.svelte";
|
||||
import Square from "../../assets/svg/Square.svelte";
|
||||
import Circle from "../../assets/svg/Circle.svelte";
|
||||
import Checkmark from "../../assets/svg/Checkmark.svelte";
|
||||
import Clock from "../../assets/svg/Clock.svelte";
|
||||
import Close from "../../assets/svg/Close.svelte";
|
||||
import Crosshair from "../../assets/svg/Crosshair.svelte";
|
||||
import Help from "../../assets/svg/Help.svelte";
|
||||
import Home from "../../assets/svg/Home.svelte";
|
||||
import Invalid from "../../assets/svg/Invalid.svelte";
|
||||
import Location from "../../assets/svg/Location.svelte";
|
||||
import Location_empty from "../../assets/svg/Location_empty.svelte";
|
||||
import Location_locked from "../../assets/svg/Location_locked.svelte";
|
||||
import Note from "../../assets/svg/Note.svelte";
|
||||
import Resolved from "../../assets/svg/Resolved.svelte";
|
||||
import Ring from "../../assets/svg/Ring.svelte";
|
||||
import Scissors from "../../assets/svg/Scissors.svelte";
|
||||
import Teardrop from "../../assets/svg/Teardrop.svelte";
|
||||
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte";
|
||||
import Triangle from "../../assets/svg/Triangle.svelte";
|
||||
import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte";
|
||||
import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte";
|
||||
import Gps_arrow from "../../assets/svg/Gps_arrow.svelte";
|
||||
import { HeartIcon } from "@babeard/svelte-heroicons/solid";
|
||||
import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline";
|
||||
|
||||
/**
|
||||
* Renders a single icon.
|
||||
|
@ -29,68 +31,72 @@
|
|||
* Icons -placed on top of each other- form a 'Marker' together
|
||||
*/
|
||||
|
||||
export let icon: string | undefined
|
||||
export let color: string | undefined
|
||||
export let icon: string | undefined;
|
||||
export let color: string | undefined = undefined
|
||||
export let clss: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
{#if icon}
|
||||
<div class="absolute top-0 left-0 h-full w-full">
|
||||
{#if icon === "pin"}
|
||||
<Pin {color} />
|
||||
<Pin {color} class={clss}/>
|
||||
{:else if icon === "square"}
|
||||
<Square {color} />
|
||||
<Square {color} class={clss}/>
|
||||
{:else if icon === "circle"}
|
||||
<Circle {color} />
|
||||
<Circle {color} class={clss}/>
|
||||
{:else if icon === "checkmark"}
|
||||
<Checkmark {color} />
|
||||
<Checkmark {color} class={clss}/>
|
||||
{:else if icon === "clock"}
|
||||
<Clock {color} />
|
||||
<Clock {color} class={clss}/>
|
||||
{:else if icon === "close"}
|
||||
<Close {color} />
|
||||
<Close {color} class={clss}/>
|
||||
{:else if icon === "crosshair"}
|
||||
<Crosshair {color} />
|
||||
<Crosshair {color} class={clss}/>
|
||||
{:else if icon === "help"}
|
||||
<Help {color} />
|
||||
<Help {color} class={clss}/>
|
||||
{:else if icon === "home"}
|
||||
<Home {color} />
|
||||
<Home {color} class={clss}/>
|
||||
{:else if icon === "invalid"}
|
||||
<Invalid {color} />
|
||||
<Invalid {color} class={clss}/>
|
||||
{:else if icon === "location"}
|
||||
<Location {color} />
|
||||
<Location {color} class={clss}/>
|
||||
{:else if icon === "location_empty"}
|
||||
<Location_empty {color} />
|
||||
<Location_empty {color} class={clss}/>
|
||||
{:else if icon === "location_locked"}
|
||||
<Location_locked {color} />
|
||||
<Location_locked {color} class={clss}/>
|
||||
{:else if icon === "note"}
|
||||
<Note {color} />
|
||||
<Note {color} class={clss}/>
|
||||
{:else if icon === "resolved"}
|
||||
<Resolved {color} />
|
||||
<Resolved {color} class={clss}/>
|
||||
{:else if icon === "ring"}
|
||||
<Ring {color} />
|
||||
<Ring {color} class={clss}/>
|
||||
{:else if icon === "scissors"}
|
||||
<Scissors {color} />
|
||||
<Scissors {color} class={clss}/>
|
||||
{:else if icon === "teardrop"}
|
||||
<Teardrop {color} />
|
||||
<Teardrop {color} class={clss}/>
|
||||
{:else if icon === "teardrop_with_hole_green"}
|
||||
<Teardrop_with_hole_green {color} />
|
||||
<Teardrop_with_hole_green {color} class={clss}/>
|
||||
{:else if icon === "triangle"}
|
||||
<Triangle {color} />
|
||||
<Triangle {color} class={clss}/>
|
||||
{:else if icon === "brick_wall_square"}
|
||||
<Brick_wall_square {color} />
|
||||
<Brick_wall_square {color} class={clss}/>
|
||||
{:else if icon === "brick_wall_round"}
|
||||
<Brick_wall_round {color} />
|
||||
<Brick_wall_round {color} class={clss}/>
|
||||
{:else if icon === "gps_arrow"}
|
||||
<Gps_arrow {color} />
|
||||
<Gps_arrow {color} class={clss}/>
|
||||
{:else if icon === "checkmark"}
|
||||
<Checkmark {color} />
|
||||
<Checkmark {color} class={clss}/>
|
||||
{:else if icon === "help"}
|
||||
<Help {color} />
|
||||
<Help {color} class={clss}/>
|
||||
{:else if icon === "close"}
|
||||
<Close {color} />
|
||||
<Close {color} class={clss}/>
|
||||
{:else if icon === "invalid"}
|
||||
<Invalid {color} />
|
||||
<Invalid {color} class={clss}/>
|
||||
{:else if icon === "heart"}
|
||||
<HeartIcon class={clss}/>
|
||||
{:else if icon === "heart_outline"}
|
||||
<HeartOutlineIcon class={clss}/>
|
||||
{:else}
|
||||
<img class="h-full w-full" src={icon} />
|
||||
<img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true"
|
||||
alt="" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
<script lang="ts">
|
||||
import Icon from "./Icon.svelte"
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
/**
|
||||
* Renders a 'marker', which consists of multiple 'icons'
|
||||
*/
|
||||
export let icons: { icon: string; color: string }[]
|
||||
export let icons: { icon: string; color: string }[];
|
||||
</script>
|
||||
|
||||
{#if icons !== undefined && icons.length > 0}
|
||||
<div class="relative h-full w-full">
|
||||
{#each icons as icon}
|
||||
<Icon icon={icon.icon} color={icon.color} />
|
||||
<div class="absolute top-0 left-0 h-full w-full">
|
||||
<Icon icon={icon.icon} color={icon.color} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -12,11 +12,9 @@ import { Feature, Point } from "geojson"
|
|||
import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"
|
||||
import { Utils } from "../../Utils"
|
||||
import * as range_layer from "../../../assets/layers/range/range.json"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
|
||||
import { CLIENT_RENEG_LIMIT } from "tls"
|
||||
|
||||
class PointRenderingLayer {
|
||||
private readonly _config: PointRenderingConfig
|
||||
|
|
50
src/UI/OpeningHours/NextChangeViz.svelte
Normal file
50
src/UI/OpeningHours/NextChangeViz.svelte
Normal file
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts">/**
|
||||
* Simple visualisation which shows when the POI opens/closes next.
|
||||
*/
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { Store, Stores } from "../../Logic/UIEventSource";
|
||||
import { OH } from "./OpeningHours";
|
||||
import opening_hours from "opening_hours";
|
||||
import Clock from "../../assets/svg/Clock.svelte";
|
||||
import { Utils } from "../../Utils";
|
||||
import Circle from "../../assets/svg/Circle.svelte";
|
||||
import Ring from "../../assets/svg/Ring.svelte";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export let state: SpecialVisualizationState;
|
||||
export let tags: Store<Record<string, string>>;
|
||||
export let keyToUse: string = "opening_hours";
|
||||
export let prefix: string = undefined;
|
||||
export let postfix: string = undefined;
|
||||
let oh: Store<opening_hours | "error" | undefined> = OH.CreateOhObjectStore(tags, keyToUse, prefix, postfix);
|
||||
|
||||
let currentState = oh.mapD(oh => typeof oh === "string" ? undefined : oh.getState());
|
||||
let tomorrow = new Date();
|
||||
tomorrow.setTime(tomorrow.getTime() + 24 * 60 * 60 * 1000);
|
||||
let nextChange = oh
|
||||
.mapD(oh => typeof oh === "string" ? undefined : oh.getNextChange(new Date(), tomorrow), [Stores.Chronic(5 * 60 * 1000)])
|
||||
.mapD(date => Utils.TwoDigits(date.getHours()) + ":" + Utils.TwoDigits(date.getMinutes()));
|
||||
|
||||
let size = nextChange.map(change => change === undefined ? "absolute h-7 w-7" : "absolute h-5 w-5 top-0 left-1/4");
|
||||
|
||||
</script>
|
||||
|
||||
{#if $currentState !== undefined}
|
||||
<div class="relative h-8 w-8">
|
||||
{#if $currentState === true}
|
||||
<Ring class={$size} color="#0f0" style="z-index: 0" />
|
||||
<Clock class={$size} color="#0f0" style="z-index: 0" />
|
||||
{:else if $currentState === false}
|
||||
<Circle class={$size} color="#f00" style="z-index: 0" />
|
||||
<Clock class={$size} color="#fff" style="z-index: 0" />
|
||||
{/if}
|
||||
|
||||
{#if $nextChange !== undefined}
|
||||
<span class="absolute bottom-0 font-bold text-sm" style="z-index: 1; background-color: #ffffff88; margin-top: 3px">
|
||||
{$nextChange}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
{/if}
|
|
@ -1,5 +1,6 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import opening_hours from "opening_hours"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
|
||||
export interface OpeningHour {
|
||||
weekday: number // 0 is monday, 1 is tuesday, ...
|
||||
|
@ -494,10 +495,48 @@ This list will be sorted
|
|||
|
||||
return [changeHours, changeHourText]
|
||||
}
|
||||
public static CreateOhObjectStore(
|
||||
tags: Store<Record<string, string>>,
|
||||
key: string = "opening_hours",
|
||||
prefixToIgnore?: string,
|
||||
postfixToIgnore?: string
|
||||
): Store<opening_hours | undefined | "error"> {
|
||||
prefixToIgnore ??= ""
|
||||
postfixToIgnore ??= ""
|
||||
const country = tags.map((tags) => tags._country)
|
||||
return tags
|
||||
.mapD((tags) => {
|
||||
const value: string = tags[key]
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (
|
||||
(prefixToIgnore || postfixToIgnore) &&
|
||||
value.startsWith(prefixToIgnore) &&
|
||||
value.endsWith(postfixToIgnore)
|
||||
) {
|
||||
return value
|
||||
.substring(prefixToIgnore.length, value.length - postfixToIgnore.length)
|
||||
.trim()
|
||||
}
|
||||
return value
|
||||
})
|
||||
.mapD(
|
||||
(ohtext) => {
|
||||
try {
|
||||
return OH.CreateOhObject(<any>tags.data, ohtext, country.data)
|
||||
} catch (e) {
|
||||
return "error"
|
||||
}
|
||||
},
|
||||
[country]
|
||||
)
|
||||
}
|
||||
public static CreateOhObject(
|
||||
tags: Record<string, string> & { _lat: number; _lon: number; _country?: string },
|
||||
textToParse: string
|
||||
textToParse: string,
|
||||
country?: string
|
||||
) {
|
||||
// noinspection JSPotentiallyInvalidConstructorUsage
|
||||
return new opening_hours(
|
||||
|
@ -506,7 +545,7 @@ This list will be sorted
|
|||
lat: tags._lat,
|
||||
lon: tags._lon,
|
||||
address: {
|
||||
country_code: tags._country?.toLowerCase(),
|
||||
country_code: country.toLowerCase(),
|
||||
state: undefined,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,7 +3,6 @@ import Combine from "../Base/Combine"
|
|||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { OH } from "./OpeningHours"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Constants from "../../Models/Constants"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
|
@ -30,48 +29,20 @@ export default class OpeningHoursVisualization extends Toggle {
|
|||
prefix = "",
|
||||
postfix = ""
|
||||
) {
|
||||
const country = tags.map((tags) => tags._country)
|
||||
const openingHoursStore = OH.CreateOhObjectStore(tags, key, prefix, postfix)
|
||||
const ohTable = new VariableUiElement(
|
||||
tags
|
||||
.map((tags) => {
|
||||
const value: string = tags[key]
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (value.startsWith(prefix) && value.endsWith(postfix)) {
|
||||
return value.substring(prefix.length, value.length - postfix.length).trim()
|
||||
}
|
||||
return value
|
||||
}) // This mapping will absorb all other changes to tags in order to prevent regeneration
|
||||
.map(
|
||||
(ohtext) => {
|
||||
if (ohtext === undefined) {
|
||||
return new FixedUiElement(
|
||||
"No opening hours defined with key " + key
|
||||
).SetClass("alert")
|
||||
}
|
||||
try {
|
||||
return OpeningHoursVisualization.CreateFullVisualisation(
|
||||
OH.CreateOhObject(<any>tags.data, ohtext)
|
||||
)
|
||||
} catch (e) {
|
||||
console.warn(e, e.stack)
|
||||
return new Combine([
|
||||
Translations.t.general.opening_hours.error_loading,
|
||||
new Toggle(
|
||||
new FixedUiElement(e).SetClass("subtle"),
|
||||
undefined,
|
||||
state?.osmConnection?.userDetails.map(
|
||||
(userdetails) =>
|
||||
userdetails.csCount >=
|
||||
Constants.userJourney.tagsVisibleAndWikiLinked
|
||||
)
|
||||
),
|
||||
])
|
||||
}
|
||||
},
|
||||
[country]
|
||||
)
|
||||
openingHoursStore.map((opening_hours_obj) => {
|
||||
if (opening_hours_obj === undefined) {
|
||||
return new FixedUiElement("No opening hours defined with key " + key).SetClass(
|
||||
"alert"
|
||||
)
|
||||
}
|
||||
|
||||
if (opening_hours_obj === "error") {
|
||||
return Translations.t.general.opening_hours.error_loading
|
||||
}
|
||||
return OpeningHoursVisualization.CreateFullVisualisation(opening_hours_obj)
|
||||
})
|
||||
)
|
||||
|
||||
super(
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
2. What do we want to add?
|
||||
3. Are all elements of this category visible? (i.e. there are no filters possibly hiding this, is the data still loading, ...) -->
|
||||
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
|
||||
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} />
|
||||
<Tr t={Translations.t.general.add.pleaseLogin} />
|
||||
</LoginButton>
|
||||
<div class="h-full w-full">
|
||||
{#if $zoom < Constants.minZoomLevelToAddNewPoint}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? [])
|
||||
)
|
||||
|
||||
const allTags = tags.map((tags) => {
|
||||
const allTags = tags.mapD((tags) => {
|
||||
const parts: (string | BaseUIElement)[][] = []
|
||||
for (const key in tags) {
|
||||
let v = tags[key]
|
||||
|
|
|
@ -31,14 +31,16 @@ export class ExportAsGpxViz implements SpecialVisualization {
|
|||
t.downloadFeatureAsGpx.SetClass("font-bold text-lg"),
|
||||
t.downloadGpxHelper.SetClass("subtle"),
|
||||
]).SetClass("flex flex-col")
|
||||
).onClick(() => {
|
||||
console.log("Exporting as GPX!")
|
||||
const tags = tagSource.data
|
||||
const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track"
|
||||
const gpx = GeoOperations.toGpx(<Feature<LineString>>feature, title)
|
||||
Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", {
|
||||
mimetype: "{gpx=application/gpx+xml}",
|
||||
)
|
||||
.SetClass("w-full")
|
||||
.onClick(() => {
|
||||
console.log("Exporting as GPX!")
|
||||
const tags = tagSource.data
|
||||
const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track"
|
||||
const gpx = GeoOperations.toGpx(<Feature<LineString>>feature, title)
|
||||
Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", {
|
||||
mimetype: "{gpx=application/gpx+xml}",
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
|
||||
const t = Translations.t.image.nearby
|
||||
const c = [lon, lat]
|
||||
console.log(">>>", image)
|
||||
let attributedImage = new AttributedImage({
|
||||
url: image.thumbUrl ?? image.pictureUrl,
|
||||
provider: AllImageProviders.byName(image.provider),
|
||||
|
@ -45,7 +44,7 @@
|
|||
const url = image.osmTags[key]
|
||||
if (isLinked) {
|
||||
const action = new LinkImageAction(currentTags.id, key, url, tags, {
|
||||
theme: state.layout.id,
|
||||
theme: tags.data._orig_theme ?? state.layout.id,
|
||||
changeType: "link-image",
|
||||
})
|
||||
state.changes.applyAction(action)
|
||||
|
@ -54,7 +53,7 @@
|
|||
const v = currentTags[k]
|
||||
if (v === url) {
|
||||
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, {
|
||||
theme: state.layout.id,
|
||||
theme: tags.data._orig_theme ?? state.layout.id,
|
||||
changeType: "remove-image",
|
||||
})
|
||||
state.changes.applyAction(action)
|
||||
|
|
48
src/UI/Popup/MarkAsFavourite.svelte
Normal file
48
src/UI/Popup/MarkAsFavourite.svelte
Normal file
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { HeartIcon as HeartSolidIcon } from "@babeard/svelte-heroicons/solid";
|
||||
import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||
import type { Feature } from "geojson";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
/**
|
||||
* A full-blown 'mark as favourite'-button
|
||||
*/
|
||||
export let state: SpecialVisualizationState;
|
||||
export let feature: Feature
|
||||
export let tags: Record<string, string>;
|
||||
export let layer: LayerConfig
|
||||
let isFavourite = tags?.map(tags => tags._favourite === "yes");
|
||||
const t = Translations.t.favouritePoi;
|
||||
|
||||
function markFavourite(isFavourite: boolean) {
|
||||
state.favourites.markAsFavourite(feature, layer.id, state.layout.id, tags, isFavourite)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
{#if $isFavourite}
|
||||
<div class="flex h-fit items-start">
|
||||
<HeartSolidIcon class="w-16 shrink-0 mr-2" on:click={() => markFavourite(false)} />
|
||||
<div class="flex flex-col w-full">
|
||||
<button class="flex flex-col items-start" on:click={() => markFavourite(false)}>
|
||||
<Tr t={t.button.unmark} />
|
||||
<Tr cls="normal-font subtle" t={t.button.unmarkNotDeleted}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Tr cls="font-bold thanks m-2 p-2 block" t={t.button.isFavourite} />
|
||||
{:else}
|
||||
<div class="flex items-start">
|
||||
<HeartOutlineIcon class="w-16 shrink-0 mr-2" on:click={() => markFavourite(true)} />
|
||||
<button class="flex w-full flex-col items-start" on:click={() => markFavourite(true)}>
|
||||
<Tr t={t.button.markAsFavouriteTitle} />
|
||||
<Tr cls="normal-font subtle" t={t.button.markDescription}/>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</LoginToggle>
|
36
src/UI/Popup/MarkAsFavouriteMini.svelte
Normal file
36
src/UI/Popup/MarkAsFavouriteMini.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { HeartIcon as HeartSolidIcon } from "@babeard/svelte-heroicons/solid";
|
||||
import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline";
|
||||
import Translations from "../i18n/Translations";
|
||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||
import type { Feature } from "geojson";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
/**
|
||||
* A small 'mark as favourite'-button to serve as title-icon
|
||||
*/
|
||||
export let state: SpecialVisualizationState;
|
||||
export let feature: Feature;
|
||||
export let tags: Record<string, string>;
|
||||
export let layer: LayerConfig;
|
||||
let isFavourite = tags?.map(tags => tags._favourite === "yes");
|
||||
const t = Translations.t.favouritePoi;
|
||||
|
||||
function markFavourite(isFavourite: boolean) {
|
||||
state.favourites.markAsFavourite(feature, layer.id, state.layout.id, tags, isFavourite);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
{#if $isFavourite}
|
||||
<button class="p-0 m-0 h-8 w-8" on:click={() => markFavourite(false)}>
|
||||
<HeartSolidIcon/>
|
||||
</button>
|
||||
{:else}
|
||||
<button class="p-0 m-0 h-8 w-8 no-image-background" on:click={() => markFavourite(true)} >
|
||||
<HeartOutlineIcon/>
|
||||
</button>
|
||||
{/if}
|
||||
</LoginToggle>
|
|
@ -3,16 +3,15 @@
|
|||
* Shows all questions for which the answers are unknown.
|
||||
* The questions can either be shown all at once or one at a time (in which case they can be skipped)
|
||||
*/
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import If from "../../Base/If.svelte"
|
||||
import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import Translations from "../../i18n/Translations.js"
|
||||
import { Utils } from "../../../Utils"
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { Feature } from "geojson";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
import { Utils } from "../../../Utils";
|
||||
|
||||
export let layer: LayerConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
onDestroy(
|
||||
tags.addCallbackAndRun((tags) => {
|
||||
_tags = tags
|
||||
console.log("Getting render value for", _tags,config)
|
||||
trs = Utils.NoNull(config?.GetRenderValues(_tags))
|
||||
})
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
import Translations from "../../i18n/Translations.js"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { Utils } from "../../../Utils"
|
||||
|
||||
import { twMerge } from "tailwind-merge"
|
||||
export let config: TagRenderingConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let selectedElement: Feature | undefined
|
||||
|
@ -71,7 +71,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={htmlElem} class={clss}>
|
||||
<div bind:this={htmlElem} class={twMerge(clss, "tr-"+config.id)}>
|
||||
{#if config.question && (!editingEnabled || $editingEnabled)}
|
||||
{#if editMode}
|
||||
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer}>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
import Icon from "../../Map/Icon.svelte";
|
||||
|
||||
export let selectedElement: Feature
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
@ -27,13 +28,8 @@
|
|||
</script>
|
||||
|
||||
{#if mapping.icon !== undefined}
|
||||
<div class="inline-flex items-center">
|
||||
<img
|
||||
class={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-1")}
|
||||
src={mapping.icon}
|
||||
aria-hidden="true"
|
||||
alt=""
|
||||
/>
|
||||
<div class="inline-flex">
|
||||
<Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-1")}/>
|
||||
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement} />
|
||||
</div>
|
||||
{:else if mapping.then !== undefined}
|
||||
|
|
|
@ -1,188 +1,210 @@
|
|||
<script lang="ts">
|
||||
import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import type { Feature } from "geojson"
|
||||
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||
import FreeformInput from "./FreeformInput.svelte"
|
||||
import Translations from "../../i18n/Translations.js"
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import SpecialTranslation from "./SpecialTranslation.svelte"
|
||||
import TagHint from "../TagHint.svelte"
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte"
|
||||
import SubtleButton from "../../Base/SubtleButton.svelte"
|
||||
import Loading from "../../Base/Loading.svelte"
|
||||
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Constants from "../../../Models/Constants"
|
||||
import { Unit } from "../../../Models/Unit"
|
||||
import UserRelatedState from "../../../Logic/State/UserRelatedState"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import Search from "../../../assets/svg/Search.svelte"
|
||||
import Login from "../../../assets/svg/Login.svelte"
|
||||
import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import type { Feature } from "geojson";
|
||||
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter";
|
||||
import FreeformInput from "./FreeformInput.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import SpecialTranslation from "./SpecialTranslation.svelte";
|
||||
import TagHint from "../TagHint.svelte";
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte";
|
||||
import SubtleButton from "../../Base/SubtleButton.svelte";
|
||||
import Loading from "../../Base/Loading.svelte";
|
||||
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte";
|
||||
import { Translation } from "../../i18n/Translation";
|
||||
import Constants from "../../../Models/Constants";
|
||||
import { Unit } from "../../../Models/Unit";
|
||||
import UserRelatedState from "../../../Logic/State/UserRelatedState";
|
||||
import { twJoin } from "tailwind-merge";
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils";
|
||||
import Search from "../../../assets/svg/Search.svelte";
|
||||
import Login from "../../../assets/svg/Login.svelte";
|
||||
|
||||
export let config: TagRenderingConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let selectedElement: Feature
|
||||
export let state: SpecialVisualizationState
|
||||
export let layer: LayerConfig | undefined
|
||||
export let config: TagRenderingConfig;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let selectedElement: Feature;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let layer: LayerConfig | undefined;
|
||||
export let selectedTags: TagsFilter = undefined;
|
||||
|
||||
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
|
||||
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined);
|
||||
|
||||
let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
|
||||
let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key));
|
||||
|
||||
// Will be bound if a freeform is available
|
||||
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
|
||||
let selectedMapping: number = undefined
|
||||
let checkedMappings: boolean[]
|
||||
// Will be bound if a freeform is available
|
||||
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]);
|
||||
let selectedMapping: number = undefined;
|
||||
let checkedMappings: boolean[];
|
||||
|
||||
/**
|
||||
* Prepares and fills the checkedMappings
|
||||
*/
|
||||
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) {
|
||||
mappings = confg.mappings?.filter((m) => {
|
||||
if (typeof m.hideInAnswer === "boolean") {
|
||||
return !m.hideInAnswer
|
||||
}
|
||||
return !m.hideInAnswer.matchesProperties(tgs)
|
||||
let mappings: Mapping[] = config?.mappings;
|
||||
let searchTerm: UIEventSource<string> = new UIEventSource("");
|
||||
|
||||
let dispatch = createEventDispatcher<{
|
||||
saved: {
|
||||
config: TagRenderingConfig
|
||||
applied: TagsFilter
|
||||
}
|
||||
}>();
|
||||
|
||||
/**
|
||||
* Prepares and fills the checkedMappings
|
||||
*/
|
||||
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) {
|
||||
mappings = confg.mappings?.filter((m) => {
|
||||
if (typeof m.hideInAnswer === "boolean") {
|
||||
return !m.hideInAnswer;
|
||||
}
|
||||
return !m.hideInAnswer.matchesProperties(tgs);
|
||||
});
|
||||
// We received a new config -> reinit
|
||||
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key));
|
||||
|
||||
if (
|
||||
confg.mappings?.length > 0 &&
|
||||
confg.multiAnswer &&
|
||||
(checkedMappings === undefined ||
|
||||
checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0))
|
||||
) {
|
||||
const seenFreeforms = [];
|
||||
TagUtils.FlattenMultiAnswer();
|
||||
checkedMappings = [
|
||||
...confg.mappings.map((mapping) => {
|
||||
const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs);
|
||||
if (matches && confg.freeform) {
|
||||
const newProps = TagUtils.changeAsProperties(mapping.if.asChange());
|
||||
seenFreeforms.push(newProps[confg.freeform.key]);
|
||||
}
|
||||
return matches;
|
||||
})
|
||||
// We received a new config -> reinit
|
||||
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
|
||||
];
|
||||
|
||||
if (
|
||||
confg.mappings?.length > 0 &&
|
||||
confg.multiAnswer &&
|
||||
(checkedMappings === undefined ||
|
||||
checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0))
|
||||
) {
|
||||
const seenFreeforms = []
|
||||
TagUtils.FlattenMultiAnswer()
|
||||
checkedMappings = [
|
||||
...confg.mappings.map((mapping) => {
|
||||
const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs)
|
||||
if (matches && confg.freeform) {
|
||||
const newProps = TagUtils.changeAsProperties(mapping.if.asChange())
|
||||
seenFreeforms.push(newProps[confg.freeform.key])
|
||||
}
|
||||
return matches
|
||||
}),
|
||||
]
|
||||
|
||||
if (tgs !== undefined && confg.freeform) {
|
||||
const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? []
|
||||
for (const seenFreeform of seenFreeforms) {
|
||||
if (!seenFreeform) {
|
||||
continue
|
||||
}
|
||||
const index = unseenFreeformValues.indexOf(seenFreeform)
|
||||
if (index < 0) {
|
||||
continue
|
||||
}
|
||||
unseenFreeformValues.splice(index, 1)
|
||||
}
|
||||
// TODO this has _to much_ values
|
||||
freeformInput.setData(unseenFreeformValues.join(";"))
|
||||
checkedMappings.push(unseenFreeformValues.length > 0)
|
||||
}
|
||||
if (tgs !== undefined && confg.freeform) {
|
||||
const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? [];
|
||||
for (const seenFreeform of seenFreeforms) {
|
||||
if (!seenFreeform) {
|
||||
continue;
|
||||
}
|
||||
const index = unseenFreeformValues.indexOf(seenFreeform);
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
unseenFreeformValues.splice(index, 1);
|
||||
}
|
||||
if (confg.freeform?.key) {
|
||||
if (!confg.multiAnswer) {
|
||||
// Somehow, setting multi-answer freeform values is broken if this is not set
|
||||
freeformInput.setData(tgs[confg.freeform.key])
|
||||
}
|
||||
// TODO this has _to much_ values
|
||||
freeformInput.setData(unseenFreeformValues.join(";"));
|
||||
checkedMappings.push(unseenFreeformValues.length > 0);
|
||||
}
|
||||
}
|
||||
if (confg.freeform?.key) {
|
||||
if (!confg.multiAnswer) {
|
||||
// Somehow, setting multi-answer freeform values is broken if this is not set
|
||||
freeformInput.setData(tgs[confg.freeform.key]);
|
||||
}
|
||||
|
||||
} else {
|
||||
freeformInput.setData(undefined);
|
||||
}
|
||||
feedback.setData(undefined);
|
||||
}
|
||||
|
||||
$: {
|
||||
// Even though 'config' is not declared as a store, Svelte uses it as one to update the component
|
||||
// We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes
|
||||
initialize($tags, config);
|
||||
}
|
||||
|
||||
$: {
|
||||
try {
|
||||
selectedTags = config?.constructChangeSpecification(
|
||||
$freeformInput,
|
||||
selectedMapping,
|
||||
checkedMappings,
|
||||
tags.data
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Could not calculate changeSpecification:", e);
|
||||
selectedTags = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onSave() {
|
||||
if (selectedTags === undefined) {
|
||||
console.log("SelectedTags is undefined, ignoring 'onSave'-event");
|
||||
return;
|
||||
}
|
||||
if (layer === undefined || (layer?.source === null && layer.id !== "favourite")) {
|
||||
/**
|
||||
* This is a special, priviliged layer.
|
||||
* We simply apply the tags onto the records
|
||||
*/
|
||||
const kv = selectedTags.asChange(tags.data);
|
||||
for (const { k, v } of kv) {
|
||||
if (v === undefined || v === "") {
|
||||
delete tags.data[k];
|
||||
} else {
|
||||
freeformInput.setData(undefined)
|
||||
freeformInput.setData(undefined);
|
||||
}
|
||||
feedback.setData(undefined)
|
||||
feedback.setData(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
// Even though 'config' is not declared as a store, Svelte uses it as one to update the component
|
||||
// We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes
|
||||
initialize($tags, config)
|
||||
dispatch("saved", { config, applied: selectedTags });
|
||||
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
|
||||
theme: tags.data["_orig_theme"] ?? state.layout.id,
|
||||
changeType: "answer"
|
||||
});
|
||||
freeformInput.setData(undefined);
|
||||
selectedMapping = undefined;
|
||||
selectedTags = undefined;
|
||||
|
||||
change
|
||||
.CreateChangeDescriptions()
|
||||
.then((changes) => state.changes.applyChanges(changes))
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
function onInputKeypress(e: Event) {
|
||||
if (e.key === "Enter") {
|
||||
onSave();
|
||||
}
|
||||
export let selectedTags: TagsFilter = undefined
|
||||
}
|
||||
|
||||
let mappings: Mapping[] = config?.mappings
|
||||
let searchTerm: UIEventSource<string> = new UIEventSource("")
|
||||
|
||||
$: {
|
||||
try {
|
||||
selectedTags = config?.constructChangeSpecification(
|
||||
$freeformInput,
|
||||
selectedMapping,
|
||||
checkedMappings,
|
||||
tags.data,
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("Could not calculate changeSpecification:", e)
|
||||
selectedTags = undefined
|
||||
}
|
||||
$: {
|
||||
try {
|
||||
selectedTags = config?.constructChangeSpecification(
|
||||
$freeformInput,
|
||||
selectedMapping,
|
||||
checkedMappings,
|
||||
tags.data
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Could not calculate changeSpecification:", e);
|
||||
selectedTags = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let dispatch = createEventDispatcher<{
|
||||
saved: {
|
||||
config: TagRenderingConfig
|
||||
applied: TagsFilter
|
||||
}
|
||||
}>()
|
||||
|
||||
function onSave() {
|
||||
if (selectedTags === undefined) {
|
||||
console.log("SelectedTags is undefined, ignoring 'onSave'-event")
|
||||
return
|
||||
}
|
||||
if (layer === undefined || layer?.source === null) {
|
||||
/**
|
||||
* This is a special, priviliged layer.
|
||||
* We simply apply the tags onto the records
|
||||
*/
|
||||
const kv = selectedTags.asChange(tags.data)
|
||||
for (const { k, v } of kv) {
|
||||
if (v === undefined || v === "") {
|
||||
delete tags.data[k]
|
||||
} else {
|
||||
tags.data[k] = v
|
||||
}
|
||||
}
|
||||
tags.ping()
|
||||
return
|
||||
}
|
||||
|
||||
dispatch("saved", { config, applied: selectedTags })
|
||||
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
|
||||
theme: state.layout.id,
|
||||
changeType: "answer",
|
||||
})
|
||||
freeformInput.setData(undefined)
|
||||
selectedMapping = undefined
|
||||
selectedTags = undefined
|
||||
|
||||
change
|
||||
.CreateChangeDescriptions()
|
||||
.then((changes) => state.changes.applyChanges(changes))
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false)
|
||||
let featureSwitchIsDebugging =
|
||||
state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false)
|
||||
let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined)
|
||||
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0
|
||||
let question = config.question
|
||||
$: question = config.question
|
||||
if (state?.osmConnection) {
|
||||
onDestroy(
|
||||
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
|
||||
numberOfCs = ud.csCount
|
||||
}),
|
||||
)
|
||||
}
|
||||
let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false);
|
||||
let featureSwitchIsDebugging =
|
||||
state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false);
|
||||
let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined);
|
||||
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0;
|
||||
let question = config.question;
|
||||
$: question = config.question;
|
||||
if (state?.osmConnection) {
|
||||
onDestroy(
|
||||
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
|
||||
numberOfCs = ud.csCount;
|
||||
})
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if question !== undefined}
|
||||
|
@ -246,9 +268,8 @@
|
|||
bind:group={selectedMapping}
|
||||
name={"mappings-radio-" + config.id}
|
||||
value={i}
|
||||
on:keypress={(e) => {
|
||||
if (e.key === "Enter") onSave()
|
||||
}}
|
||||
on:keypress={e => onInputKeypress(e)}
|
||||
|
||||
/>
|
||||
</TagRenderingMappingInput>
|
||||
{/each}
|
||||
|
@ -259,6 +280,7 @@
|
|||
bind:group={selectedMapping}
|
||||
name={"mappings-radio-" + config.id}
|
||||
value={config.mappings?.length}
|
||||
on:keypress={e => onInputKeypress(e)}
|
||||
/>
|
||||
<FreeformInput
|
||||
{config}
|
||||
|
@ -290,6 +312,7 @@
|
|||
type="checkbox"
|
||||
name={"mappings-checkbox-" + config.id + "-" + i}
|
||||
bind:checked={checkedMappings[i]}
|
||||
on:keypress={e => onInputKeypress(e)}
|
||||
/>
|
||||
</TagRenderingMappingInput>
|
||||
{/each}
|
||||
|
@ -299,6 +322,7 @@
|
|||
type="checkbox"
|
||||
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
|
||||
bind:checked={checkedMappings[config.mappings.length]}
|
||||
on:keypress={e => onInputKeypress(e)}
|
||||
/>
|
||||
<FreeformInput
|
||||
{config}
|
||||
|
@ -307,7 +331,6 @@
|
|||
{unit}
|
||||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
on:selected={() => (checkedMappings[config.mappings.length] = true)}
|
||||
on:submit={onSave}
|
||||
/>
|
||||
</label>
|
||||
|
|
|
@ -17,6 +17,7 @@ import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
|
|||
import { RasterLayerPolygon } from "../Models/RasterLayers"
|
||||
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
||||
import { OsmTags } from "../Models/OsmFeature"
|
||||
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||
|
||||
/**
|
||||
* The state needed to render a special Visualisation.
|
||||
|
@ -33,7 +34,6 @@ export interface SpecialVisualizationState {
|
|||
}
|
||||
|
||||
readonly indexedFeatures: IndexedFeatureSource
|
||||
|
||||
/**
|
||||
* Some features will create a new element that should be displayed.
|
||||
* These can be injected by appending them to this featuresource (and pinging it)
|
||||
|
@ -59,6 +59,8 @@ export interface SpecialVisualizationState {
|
|||
readonly selectedLayer: UIEventSource<LayerConfig>
|
||||
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
|
||||
|
||||
readonly favourites: FavouritesFeatureSource
|
||||
|
||||
/**
|
||||
* If data is currently being fetched from external sources
|
||||
*/
|
||||
|
|
|
@ -79,6 +79,9 @@ import ThemeViewState from "../Models/ThemeViewState"
|
|||
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
|
||||
import LogoutButton from "./Base/LogoutButton.svelte"
|
||||
import OpenJosm from "./Base/OpenJosm.svelte"
|
||||
import MarkAsFavourite from "./Popup/MarkAsFavourite.svelte"
|
||||
import MarkAsFavouriteMini from "./Popup/MarkAsFavouriteMini.svelte"
|
||||
import NextChangeViz from "./OpeningHours/NextChangeViz.svelte"
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
@ -532,6 +535,9 @@ export default class SpecialVisualizations {
|
|||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
if (!layer.deletion) {
|
||||
return undefined
|
||||
}
|
||||
return new SvelteUIElement(DeleteWizard, {
|
||||
tags: tagSource,
|
||||
deleteConfig: layer.deletion,
|
||||
|
@ -822,6 +828,46 @@ export default class SpecialVisualizations {
|
|||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "opening_hours_state",
|
||||
docs: "A small element, showing if the POI is currently open and when the next change is",
|
||||
args: [
|
||||
{
|
||||
name: "key",
|
||||
defaultValue: "opening_hours",
|
||||
doc: "The tagkey from which the opening hours are read.",
|
||||
},
|
||||
{
|
||||
name: "prefix",
|
||||
defaultValue: "",
|
||||
doc: "Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__",
|
||||
},
|
||||
{
|
||||
name: "postfix",
|
||||
defaultValue: "",
|
||||
doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__",
|
||||
},
|
||||
],
|
||||
needsUrls: [],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
const keyToUse = args[0]
|
||||
const prefix = args[1]
|
||||
const postfix = args[2]
|
||||
return new SvelteUIElement(NextChangeViz, {
|
||||
state,
|
||||
keyToUse,
|
||||
tags,
|
||||
prefix,
|
||||
postfix,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "canonical",
|
||||
needsUrls: [],
|
||||
|
@ -872,20 +918,22 @@ export default class SpecialVisualizations {
|
|||
t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"),
|
||||
t.downloadGeoJsonHelper.SetClass("subtle"),
|
||||
]).SetClass("flex flex-col")
|
||||
).onClick(() => {
|
||||
console.log("Exporting as Geojson")
|
||||
const tags = tagSource.data
|
||||
const title =
|
||||
layer?.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson"
|
||||
const data = JSON.stringify(feature, null, " ")
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
data,
|
||||
title + "_mapcomplete_export.geojson",
|
||||
{
|
||||
mimetype: "application/vnd.geo+json",
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
.onClick(() => {
|
||||
console.log("Exporting as Geojson")
|
||||
const tags = tagSource.data
|
||||
const title =
|
||||
layer?.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson"
|
||||
const data = JSON.stringify(feature, null, " ")
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
data,
|
||||
title + "_mapcomplete_export.geojson",
|
||||
{
|
||||
mimetype: "application/vnd.geo+json",
|
||||
}
|
||||
)
|
||||
})
|
||||
.SetClass("w-full")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1482,7 +1530,7 @@ export default class SpecialVisualizations {
|
|||
const tags = (<ThemeViewState>(
|
||||
state
|
||||
)).geolocation.currentUserLocation.features.map(
|
||||
(features) => features[0].properties
|
||||
(features) => features[0]?.properties
|
||||
)
|
||||
return new SvelteUIElement(AllTagsPanel, {
|
||||
state,
|
||||
|
@ -1490,6 +1538,46 @@ export default class SpecialVisualizations {
|
|||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "favourite_status",
|
||||
needsUrls: [],
|
||||
docs: "A button that allows a (logged in) contributor to mark a location as a favourite location",
|
||||
args: [],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
return new SvelteUIElement(MarkAsFavourite, {
|
||||
tags: tagSource,
|
||||
state,
|
||||
layer,
|
||||
feature,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "favourite_icon",
|
||||
needsUrls: [],
|
||||
docs: "A small button that allows a (logged in) contributor to mark a location as a favourite location, sized to fit a title-icon",
|
||||
args: [],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
return new SvelteUIElement(MarkAsFavouriteMini, {
|
||||
tags: tagSource,
|
||||
state,
|
||||
layer,
|
||||
feature,
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Marker from "../Map/Marker.svelte"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"
|
||||
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
|
||||
import Marker from "../Map/Marker.svelte";
|
||||
import NextButton from "../Base/NextButton.svelte";
|
||||
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts";
|
||||
import { AllSharedLayers } from "../../Customizations/AllSharedLayers";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let info: { id: string; owner: number }
|
||||
export let category: "layers" | "themes"
|
||||
export let osmConnection: OsmConnection
|
||||
export let info: { id: string; owner: number };
|
||||
export let category: "layers" | "themes";
|
||||
export let osmConnection: OsmConnection;
|
||||
const dispatch = createEventDispatcher<{ layerSelected: string }>();
|
||||
|
||||
let displayName = UIEventSource.FromPromise(
|
||||
osmConnection.getInformationAboutUser(info.owner),
|
||||
).mapD((response) => response.display_name)
|
||||
let displayName = UIEventSource.FromPromise(
|
||||
osmConnection.getInformationAboutUser(info.owner)
|
||||
).mapD((response) => response.display_name);
|
||||
let selfId = osmConnection.userDetails.mapD((ud) => ud.uid);
|
||||
|
||||
let selfId = osmConnection.userDetails.mapD((ud) => ud.uid)
|
||||
|
||||
function fetchIconDescription(layerId): any {
|
||||
if (category === "themes") {
|
||||
return AllKnownLayouts.allKnownLayouts.get(layerId).icon
|
||||
}
|
||||
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon
|
||||
function fetchIconDescription(layerId): any {
|
||||
if (category === "themes") {
|
||||
return AllKnownLayouts.allKnownLayouts.get(layerId).icon;
|
||||
}
|
||||
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ layerSelected: string }>()
|
||||
</script>
|
||||
|
||||
<NextButton clss="small" on:click={() => dispatch("layerSelected", info)}>
|
||||
|
|
|
@ -1,90 +1,91 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import MapControlButton from "./Base/MapControlButton.svelte"
|
||||
import ToSvelte from "./Base/ToSvelte.svelte"
|
||||
import If from "./Base/If.svelte"
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl"
|
||||
import type { Feature } from "geojson"
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import Filterview from "./BigComponents/Filterview.svelte"
|
||||
import ThemeViewState from "../Models/ThemeViewState"
|
||||
import type { MapProperties } from "../Models/MapProperties"
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte"
|
||||
import Translations from "./i18n/Translations"
|
||||
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Tr from "./Base/Tr.svelte"
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
|
||||
import FloatOver from "./Base/FloatOver.svelte"
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
|
||||
import Constants from "../Models/Constants"
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte"
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||
import LoginToggle from "./Base/LoginToggle.svelte"
|
||||
import LoginButton from "./Base/LoginButton.svelte"
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
||||
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
|
||||
import ModalRight from "./Base/ModalRight.svelte"
|
||||
import { Utils } from "../Utils"
|
||||
import Hotkeys from "./Base/Hotkeys"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
|
||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
|
||||
import type { RasterLayerPolygon } from "../Models/RasterLayers"
|
||||
import { AvailableRasterLayers } from "../Models/RasterLayers"
|
||||
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
|
||||
import IfHidden from "./Base/IfHidden.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
|
||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
|
||||
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import StateIndicator from "./BigComponents/StateIndicator.svelte"
|
||||
import ShareScreen from "./BigComponents/ShareScreen.svelte"
|
||||
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
|
||||
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
|
||||
import Cross from "../assets/svg/Cross.svelte"
|
||||
import Summary from "./BigComponents/Summary.svelte"
|
||||
import Mastodon from "../assets/svg/Mastodon.svelte"
|
||||
import Bug from "../assets/svg/Bug.svelte"
|
||||
import Liberapay from "../assets/svg/Liberapay.svelte"
|
||||
import Min from "../assets/svg/Min.svelte"
|
||||
import Plus from "../assets/svg/Plus.svelte"
|
||||
import Filter from "../assets/svg/Filter.svelte"
|
||||
import Add from "../assets/svg/Add.svelte"
|
||||
import Statistics from "../assets/svg/Statistics.svelte"
|
||||
import Community from "../assets/svg/Community.svelte"
|
||||
import Download from "../assets/svg/Download.svelte"
|
||||
import Share from "../assets/svg/Share.svelte"
|
||||
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
|
||||
import OpenJosm from "./Base/OpenJosm.svelte"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte";
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||
import MapControlButton from "./Base/MapControlButton.svelte";
|
||||
import ToSvelte from "./Base/ToSvelte.svelte";
|
||||
import If from "./Base/If.svelte";
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl";
|
||||
import type { Feature } from "geojson";
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import Filterview from "./BigComponents/Filterview.svelte";
|
||||
import ThemeViewState from "../Models/ThemeViewState";
|
||||
import type { MapProperties } from "../Models/MapProperties";
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte";
|
||||
import Translations from "./i18n/Translations";
|
||||
import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import Tr from "./Base/Tr.svelte";
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
|
||||
import FloatOver from "./Base/FloatOver.svelte";
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
|
||||
import Constants from "../Models/Constants";
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte";
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState";
|
||||
import LoginToggle from "./Base/LoginToggle.svelte";
|
||||
import LoginButton from "./Base/LoginButton.svelte";
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel";
|
||||
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte";
|
||||
import ModalRight from "./Base/ModalRight.svelte";
|
||||
import { Utils } from "../Utils";
|
||||
import Hotkeys from "./Base/Hotkeys";
|
||||
import { VariableUiElement } from "./Base/VariableUIElement";
|
||||
import SvelteUIElement from "./Base/SvelteUIElement";
|
||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte";
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
|
||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
|
||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
|
||||
import type { RasterLayerPolygon } from "../Models/RasterLayers";
|
||||
import { AvailableRasterLayers } from "../Models/RasterLayers";
|
||||
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
|
||||
import IfHidden from "./Base/IfHidden.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
import MapillaryLink from "./BigComponents/MapillaryLink.svelte";
|
||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte";
|
||||
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
|
||||
import StateIndicator from "./BigComponents/StateIndicator.svelte";
|
||||
import ShareScreen from "./BigComponents/ShareScreen.svelte";
|
||||
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte";
|
||||
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte";
|
||||
import Cross from "../assets/svg/Cross.svelte";
|
||||
import Summary from "./BigComponents/Summary.svelte";
|
||||
import LanguagePicker from "./InputElement/LanguagePicker.svelte";
|
||||
import Mastodon from "../assets/svg/Mastodon.svelte";
|
||||
import Bug from "../assets/svg/Bug.svelte";
|
||||
import Liberapay from "../assets/svg/Liberapay.svelte";
|
||||
import OpenJosm from "./Base/OpenJosm.svelte";
|
||||
import Min from "../assets/svg/Min.svelte";
|
||||
import Plus from "../assets/svg/Plus.svelte";
|
||||
import Filter from "../assets/svg/Filter.svelte";
|
||||
import Add from "../assets/svg/Add.svelte";
|
||||
import Statistics from "../assets/svg/Statistics.svelte";
|
||||
import Community from "../assets/svg/Community.svelte";
|
||||
import Download from "../assets/svg/Download.svelte";
|
||||
import Share from "../assets/svg/Share.svelte";
|
||||
import Favourites from "./Favourites/Favourites.svelte";
|
||||
|
||||
export let state: ThemeViewState
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
|
||||
let maplibremap: UIEventSource<MlMap> = state.map
|
||||
let selectedElement: UIEventSource<Feature> = state.selectedElement
|
||||
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
|
||||
|
||||
let currentZoom = state.mapProperties.zoom
|
||||
let showCrosshair = state.userRelatedState.showCrosshair
|
||||
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation
|
||||
let centerFeatures = state.closestFeatures.features
|
||||
const selectedElementView = selectedElement.map(
|
||||
(selectedElement) => {
|
||||
// Svelte doesn't properly reload some of the legacy UI-elements
|
||||
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
||||
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
||||
const layer = selectedLayer.data
|
||||
if (selectedElement === undefined || layer === undefined) {
|
||||
return undefined
|
||||
}
|
||||
let currentZoom = state.mapProperties.zoom;
|
||||
let showCrosshair = state.userRelatedState.showCrosshair;
|
||||
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation;
|
||||
let centerFeatures = state.closestFeatures.features;
|
||||
const selectedElementView = selectedElement.map(
|
||||
(selectedElement) => {
|
||||
// Svelte doesn't properly reload some of the legacy UI-elements
|
||||
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
||||
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
||||
const layer = selectedLayer.data;
|
||||
if (selectedElement === undefined || layer === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
|
||||
return undefined
|
||||
|
@ -230,17 +231,15 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{#if $arrowKeysWereUsed !== undefined}
|
||||
{#if $centerFeatures.length > 0}
|
||||
<div class="interactive pointer-events-auto p-1">
|
||||
{#each $centerFeatures as feat, i (feat.properties.id)}
|
||||
<div class="flex">
|
||||
<b>{i + 1}.</b>
|
||||
<Summary {state} feature={feat} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $arrowKeysWereUsed !== undefined && $centerFeatures?.length > 0}
|
||||
<div class="pointer-events-auto interactive p-1">
|
||||
{#each $centerFeatures as feat, i (feat.properties.id)}
|
||||
<div class="flex">
|
||||
<b>{i+1}.</b><Summary {state} feature={feat}/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col items-end">
|
||||
<!-- bottom right elements -->
|
||||
|
@ -495,22 +494,31 @@
|
|||
</div>
|
||||
|
||||
<div class="flex" slot="title2">
|
||||
<HeartIcon class="h-6 w-6" />
|
||||
<Tr t={Translations.t.favouritePoi.tab}/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col m-2" slot="content2">
|
||||
<h3> <Tr t={Translations.t.favouritePoi.title}/></h3>
|
||||
<Favourites {state}/>
|
||||
</div>
|
||||
<div class="flex" slot="title3">
|
||||
<Community class="h-6 w-6" />
|
||||
<Tr t={Translations.t.communityIndex.title} />
|
||||
</div>
|
||||
<div class="m-2" slot="content2">
|
||||
<div class="m-2" slot="content3">
|
||||
<CommunityIndexView location={state.mapProperties.location} />
|
||||
</div>
|
||||
<div class="flex" slot="title3">
|
||||
<div class="flex" slot="title4">
|
||||
<EyeIcon class="w-6" />
|
||||
<Tr t={Translations.t.privacy.title} />
|
||||
</div>
|
||||
<div class="m-2" slot="content3">
|
||||
<div class="m-2" slot="content4">
|
||||
<ToSvelte construct={() => new PrivacyPolicy()} />
|
||||
</div>
|
||||
|
||||
<Tr slot="title4" t={Translations.t.advanced.title} />
|
||||
<div class="m-2 flex flex-col" slot="content4">
|
||||
<Tr slot="title5" t={Translations.t.advanced.title} />
|
||||
<div class="m-2 flex flex-col" slot="content5">
|
||||
<If condition={featureSwitches.featureSwitchEnableLogin}>
|
||||
<OpenIdEditor mapProperties={state.mapProperties} />
|
||||
<OpenJosm {state} />
|
||||
|
|
|
@ -301,10 +301,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
if (str === undefined || str === null) {
|
||||
return undefined
|
||||
}
|
||||
if (typeof str !== "string") {
|
||||
console.error("Not a string:", str)
|
||||
return undefined
|
||||
}
|
||||
if (str.length <= l) {
|
||||
return str
|
||||
}
|
||||
return str.substr(0, l - 3) + "..."
|
||||
return str.substr(0, l - 1) + "…"
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,229 +1,426 @@
|
|||
{
|
||||
"#": "Generated with generateStats.ts",
|
||||
"date": "2023-12-04T14:42:01.299Z",
|
||||
"keys": {
|
||||
"addr:street": 117211930,
|
||||
"addr:housenumber": 125040768,
|
||||
"emergency": 1939478,
|
||||
"barrier": 18424246,
|
||||
"tourism": 2683525,
|
||||
"amenity": 20541353,
|
||||
"bench": 894256,
|
||||
"rental": 8838,
|
||||
"bicycle_rental": 7447,
|
||||
"vending": 206755,
|
||||
"service:bicycle:rental": 3570,
|
||||
"pub": 316,
|
||||
"theme": 426,
|
||||
"service:bicycle:.*": 0,
|
||||
"service:bicycle:cleaning": 807,
|
||||
"shop": 5062252,
|
||||
"service:bicycle:retail": 9162,
|
||||
"network": 2181336,
|
||||
"sport": 2194801,
|
||||
"service:bicycle:repair": 11381,
|
||||
"association": 369,
|
||||
"ngo": 42,
|
||||
"leisure": 7368076,
|
||||
"club": 38429,
|
||||
"disused:amenity": 40880,
|
||||
"planned:amenity": 205,
|
||||
"tileId": 0,
|
||||
"construction:amenity": 1206,
|
||||
"cycleway": 906487,
|
||||
"highway": 218189453,
|
||||
"bicycle": 6218071,
|
||||
"cyclestreet": 8185,
|
||||
"camera:direction": 40676,
|
||||
"direction": 1896015,
|
||||
"access": 16030036,
|
||||
"entrance": 2954076,
|
||||
"name:etymology": 24485,
|
||||
"memorial": 132172,
|
||||
"indoor": 353116,
|
||||
"name:etymology:wikidata": 285224,
|
||||
"landuse": 35524214,
|
||||
"name": 88330405,
|
||||
"protect_class": 73801,
|
||||
"information": 831513,
|
||||
"man_made": 5116088,
|
||||
"boundary": 2142378,
|
||||
"tower:type": 451658,
|
||||
"playground": 109175,
|
||||
"route": 939184,
|
||||
"surveillance:type": 116760,
|
||||
"natural": 52353504,
|
||||
"building": 500469053
|
||||
"FIXME": 119237,
|
||||
"access": 20023328,
|
||||
"addr:housenumber": 146524978,
|
||||
"addr:street": 137485111,
|
||||
"advertising": 158347,
|
||||
"amenity": 25340913,
|
||||
"area": 1803451,
|
||||
"association": 757,
|
||||
"barrier": 23634152,
|
||||
"bench": 1300789,
|
||||
"bicycle": 7507086,
|
||||
"bicycle_rental": 26948,
|
||||
"boundary": 2366033,
|
||||
"brand": 2317628,
|
||||
"building": 585543589,
|
||||
"camera:direction": 61201,
|
||||
"climbing": 9051,
|
||||
"club": 53046,
|
||||
"construction:amenity": 1943,
|
||||
"conveying": 27311,
|
||||
"craft": 296376,
|
||||
"crossing": 8736722,
|
||||
"cyclestreet": 12505,
|
||||
"cycleway": 1016837,
|
||||
"direction": 2978834,
|
||||
"disused:amenity": 63413,
|
||||
"dog": 95086,
|
||||
"door": 280843,
|
||||
"drinking_water": 136067,
|
||||
"emergency": 2542692,
|
||||
"entrance": 3769592,
|
||||
"fixme": 1746318,
|
||||
"footway": 7540651,
|
||||
"generator:source": 2387982,
|
||||
"healthcare": 790125,
|
||||
"highway": 249307936,
|
||||
"indoor": 562051,
|
||||
"information": 1073014,
|
||||
"isced:2011:level": 27,
|
||||
"isced:level:2011": 74,
|
||||
"landuse": 41730047,
|
||||
"leisure": 8955744,
|
||||
"man_made": 6799900,
|
||||
"memorial": 209327,
|
||||
"motorcar": 621864,
|
||||
"name": 98684655,
|
||||
"name:etymology": 56375,
|
||||
"name:etymology:wikidata": 1174439,
|
||||
"name:nl": 80468,
|
||||
"natural": 64176097,
|
||||
"ngo": 57,
|
||||
"office": 1092855,
|
||||
"parking_space": 600707,
|
||||
"planned:amenity": 237,
|
||||
"playground": 182188,
|
||||
"post_office": 16379,
|
||||
"protect_class": 83815,
|
||||
"pub": 324,
|
||||
"public_transport": 5111577,
|
||||
"railway": 7068070,
|
||||
"recycling_type": 385569,
|
||||
"ref": 18607577,
|
||||
"rental": 13611,
|
||||
"route": 1075802,
|
||||
"service:bicycle:cleaning": 1179,
|
||||
"service:bicycle:pump": 14053,
|
||||
"service:bicycle:pump:operational_status": 344,
|
||||
"service:bicycle:rental": 4599,
|
||||
"service:bicycle:repair": 15470,
|
||||
"service:bicycle:retail": 11467,
|
||||
"service:bicycle:tools": 6227,
|
||||
"shelter": 1647743,
|
||||
"shop": 5860878,
|
||||
"species": 1656206,
|
||||
"species:wikidata": 107778,
|
||||
"sport": 2580042,
|
||||
"subject": 40076,
|
||||
"surface:colour": 17851,
|
||||
"surveillance:type": 171923,
|
||||
"theme": 906,
|
||||
"toilets": 90842,
|
||||
"tourism": 3211694,
|
||||
"tower:type": 596349,
|
||||
"type": 11757856,
|
||||
"vending": 252016
|
||||
},
|
||||
"tags": {
|
||||
"emergency": {
|
||||
"defibrillator": 51273,
|
||||
"ambulance_station": 11047,
|
||||
"fire_extinguisher": 7355,
|
||||
"fire_hydrant": 1598739
|
||||
},
|
||||
"barrier": {
|
||||
"cycle_barrier": 104166,
|
||||
"bollard": 502220,
|
||||
"wall": 3535056
|
||||
},
|
||||
"tourism": {
|
||||
"artwork": 187470,
|
||||
"map": 51,
|
||||
"viewpoint": 191765
|
||||
"advertising": {
|
||||
"billboard": 76420,
|
||||
"board": 15040,
|
||||
"column": 21212,
|
||||
"flag": 4264,
|
||||
"poster_box": 22932,
|
||||
"screen": 1352,
|
||||
"sculpture": 145,
|
||||
"sign": 6172,
|
||||
"tarp": 407,
|
||||
"totem": 7097,
|
||||
"wall_painting": 132
|
||||
},
|
||||
"amenity": {
|
||||
"bench": 1736979,
|
||||
"bicycle_library": 36,
|
||||
"bicycle_rental": 49082,
|
||||
"vending_machine": 201871,
|
||||
"bar": 199662,
|
||||
"pub": 174979,
|
||||
"cafe": 467521,
|
||||
"restaurant": 1211671,
|
||||
"bicycle_wash": 44,
|
||||
"bike_wash": 0,
|
||||
"bicycle_repair_station": 9247,
|
||||
"bicycle_parking": 435959,
|
||||
"binoculars": 479,
|
||||
"biergarten": 10309,
|
||||
"charging_station": 65402,
|
||||
"drinking_water": 250463,
|
||||
"fast_food": 460079,
|
||||
"fire_station": 122200,
|
||||
"parking": 4255206,
|
||||
"public_bookcase": 13120,
|
||||
"toilets": 350648,
|
||||
"recycling": 333925,
|
||||
"waste_basket": 550357,
|
||||
"waste_disposal": 156765
|
||||
},
|
||||
"bench": {
|
||||
"stand_up_bench": 87,
|
||||
"yes": 524993
|
||||
},
|
||||
"service:bicycle:rental": {
|
||||
"yes": 3054
|
||||
},
|
||||
"pub": {
|
||||
"cycling": 9,
|
||||
"bicycle": 0
|
||||
},
|
||||
"theme": {
|
||||
"cycling": 8,
|
||||
"bicycle": 16
|
||||
},
|
||||
"service:bicycle:cleaning": {
|
||||
"yes": 607,
|
||||
"diy": 0
|
||||
},
|
||||
"shop": {
|
||||
"bicycle": 46488,
|
||||
"sports": 37024
|
||||
},
|
||||
"sport": {
|
||||
"cycling": 6045,
|
||||
"bicycle": 96
|
||||
"animal_shelter": 6056,
|
||||
"atm": 207899,
|
||||
"bank": 389470,
|
||||
"bar": 219208,
|
||||
"bench": 2313183,
|
||||
"bicycle_library": 46,
|
||||
"bicycle_parking": 616881,
|
||||
"bicycle_rental": 63710,
|
||||
"bicycle_repair_station": 14026,
|
||||
"bicycle_wash": 79,
|
||||
"biergarten": 10323,
|
||||
"bike_wash": 1,
|
||||
"binoculars": 1109,
|
||||
"cafe": 530066,
|
||||
"car_rental": 26726,
|
||||
"charging_station": 111996,
|
||||
"childcare": 50390,
|
||||
"clinic": 179739,
|
||||
"clock": 25274,
|
||||
"college": 64379,
|
||||
"dentist": 122076,
|
||||
"doctors": 166850,
|
||||
"drinking_water": 294750,
|
||||
"fast_food": 533335,
|
||||
"fire_station": 131842,
|
||||
"hospital": 204756,
|
||||
"ice_cream": 48853,
|
||||
"kindergarten": 294441,
|
||||
"nightclub": 22779,
|
||||
"parcel_locker": 44270,
|
||||
"parking": 5158899,
|
||||
"parking_space": 2292063,
|
||||
"pharmacy": 383181,
|
||||
"post_box": 370286,
|
||||
"post_office": 198908,
|
||||
"pub": 185475,
|
||||
"public_bookcase": 21608,
|
||||
"reception_desk": 2426,
|
||||
"recycling": 417512,
|
||||
"restaurant": 1346895,
|
||||
"school": 1286594,
|
||||
"shelter": 494594,
|
||||
"shower": 27029,
|
||||
"ticket_validator": 7730,
|
||||
"toilets": 417991,
|
||||
"university": 54299,
|
||||
"vending_machine": 247257,
|
||||
"veterinary": 52813,
|
||||
"waste_basket": 759718,
|
||||
"waste_disposal": 219245
|
||||
},
|
||||
"association": {
|
||||
"cycling": 5,
|
||||
"bicycle": 20
|
||||
"bicycle": 47,
|
||||
"cycling": 5
|
||||
},
|
||||
"ngo": {
|
||||
"cycling": 0,
|
||||
"bicycle": 0
|
||||
"barrier": {
|
||||
"bollard": 668017,
|
||||
"cycle_barrier": 122201,
|
||||
"kerb": 1178769,
|
||||
"retaining_wall": 472454,
|
||||
"wall": 4448788
|
||||
},
|
||||
"leisure": {
|
||||
"bird_hide": 5669,
|
||||
"nature_reserve": 117016,
|
||||
"picnic_table": 206322,
|
||||
"pitch": 1990293,
|
||||
"playground": 705102
|
||||
},
|
||||
"club": {
|
||||
"cycling": 3,
|
||||
"bicycle": 49
|
||||
},
|
||||
"disused:amenity": {
|
||||
"charging_station": 164
|
||||
},
|
||||
"planned:amenity": {
|
||||
"charging_station": 115
|
||||
},
|
||||
"construction:amenity": {
|
||||
"charging_station": 221
|
||||
},
|
||||
"cycleway": {
|
||||
"lane": 314576,
|
||||
"track": 86541,
|
||||
"shared_lane": 60824
|
||||
},
|
||||
"highway": {
|
||||
"residential": 61321708,
|
||||
"crossing": 6119521,
|
||||
"cycleway": 1423789,
|
||||
"traffic_signals": 1512639,
|
||||
"tertiary": 7051727,
|
||||
"unclassified": 15756878,
|
||||
"secondary": 4486617,
|
||||
"primary": 3110552,
|
||||
"footway": 16496620,
|
||||
"path": 11438303,
|
||||
"steps": 1327396,
|
||||
"corridor": 27051,
|
||||
"pedestrian": 685989,
|
||||
"bridleway": 102280,
|
||||
"track": 22670967,
|
||||
"living_street": 1519108,
|
||||
"street_lamp": 2811705
|
||||
"bench": {
|
||||
"stand_up_bench": 212,
|
||||
"yes": 778144
|
||||
},
|
||||
"bicycle": {
|
||||
"designated": 1110839
|
||||
},
|
||||
"cyclestreet": {
|
||||
"yes": 8164
|
||||
},
|
||||
"access": {
|
||||
"public": 6222,
|
||||
"yes": 1363526
|
||||
},
|
||||
"memorial": {
|
||||
"ghost_bike": 503
|
||||
},
|
||||
"indoor": {
|
||||
"door": 9722
|
||||
},
|
||||
"landuse": {
|
||||
"grass": 4898559,
|
||||
"village_green": 104681
|
||||
},
|
||||
"name": {
|
||||
"Park Oude God": 1
|
||||
},
|
||||
"information": {
|
||||
"board": 242007,
|
||||
"map": 85912,
|
||||
"office": 24139,
|
||||
"visitor_centre": 285
|
||||
},
|
||||
"man_made": {
|
||||
"surveillance": 148172,
|
||||
"watermill": 9699
|
||||
"designated": 1499247,
|
||||
"no": 1614544,
|
||||
"yes": 3753651
|
||||
},
|
||||
"boundary": {
|
||||
"protected_area": 97075
|
||||
"protected_area": 111282
|
||||
},
|
||||
"tower:type": {
|
||||
"observation": 19654
|
||||
"climbing": {
|
||||
"area": 191,
|
||||
"crag": 2873,
|
||||
"route": 1040,
|
||||
"site": 14
|
||||
},
|
||||
"playground": {
|
||||
"forest": 56
|
||||
"club": {
|
||||
"bicycle": 60,
|
||||
"climbing": 1,
|
||||
"cycling": 7
|
||||
},
|
||||
"surveillance:type": {
|
||||
"camera": 112963,
|
||||
"ALPR": 2522,
|
||||
"ANPR": 3
|
||||
"construction:amenity": {
|
||||
"charging_station": 259
|
||||
},
|
||||
"conveying": {
|
||||
"yes": 12153
|
||||
},
|
||||
"craft": {
|
||||
"key_cutter": 3711,
|
||||
"shoe_repair": 64
|
||||
},
|
||||
"crossing": {
|
||||
"traffic_signals": 1408141
|
||||
},
|
||||
"cyclestreet": {
|
||||
"yes": 12480
|
||||
},
|
||||
"cycleway": {
|
||||
"lane": 300810,
|
||||
"shared_lane": 71051,
|
||||
"track": 77166
|
||||
},
|
||||
"disused:amenity": {
|
||||
"charging_station": 289,
|
||||
"drinking_water": 2758
|
||||
},
|
||||
"dog": {
|
||||
"unleashed": 727
|
||||
},
|
||||
"drinking_water": {
|
||||
"yes": 74561
|
||||
},
|
||||
"emergency": {
|
||||
"ambulance_station": 13020,
|
||||
"defibrillator": 80699,
|
||||
"fire_extinguisher": 11605,
|
||||
"fire_hydrant": 1928477
|
||||
},
|
||||
"footway": {
|
||||
"crossing": 3111184
|
||||
},
|
||||
"generator:source": {
|
||||
"wind": 390537
|
||||
},
|
||||
"healthcare": {
|
||||
"physiotherapist": 17548
|
||||
},
|
||||
"highway": {
|
||||
"bridleway": 107507,
|
||||
"bus_stop": 3459595,
|
||||
"corridor": 46847,
|
||||
"crossing": 8505991,
|
||||
"cycleway": 1693405,
|
||||
"elevator": 39221,
|
||||
"footway": 21573091,
|
||||
"living_street": 1753722,
|
||||
"motorway": 1182914,
|
||||
"motorway_link": 829035,
|
||||
"path": 13690001,
|
||||
"pedestrian": 767066,
|
||||
"primary": 3462637,
|
||||
"primary_link": 433106,
|
||||
"residential": 65553821,
|
||||
"secondary": 5008689,
|
||||
"secondary_link": 340521,
|
||||
"service": 54202864,
|
||||
"speed_camera": 61915,
|
||||
"speed_display": 2621,
|
||||
"steps": 1618344,
|
||||
"street_lamp": 3879570,
|
||||
"tertiary": 7809143,
|
||||
"tertiary_link": 245867,
|
||||
"track": 25718176,
|
||||
"traffic_signals": 1709993,
|
||||
"trunk": 1679773,
|
||||
"trunk_link": 519826,
|
||||
"unclassified": 16914480
|
||||
},
|
||||
"indoor": {
|
||||
"area": 25332,
|
||||
"corridor": 17609,
|
||||
"door": 19157,
|
||||
"level": 4253,
|
||||
"room": 157006,
|
||||
"wall": 32366
|
||||
},
|
||||
"information": {
|
||||
"board": 321201,
|
||||
"guidepost": 520873,
|
||||
"map": 108166,
|
||||
"office": 27749,
|
||||
"route_marker": 59596,
|
||||
"visitor_centre": 523
|
||||
},
|
||||
"isced:level:2011": {
|
||||
"early_childhood": 0
|
||||
},
|
||||
"landuse": {
|
||||
"village_green": 102589
|
||||
},
|
||||
"leisure": {
|
||||
"bird_hide": 6607,
|
||||
"dog_park": 21993,
|
||||
"fitness_centre": 72920,
|
||||
"fitness_station": 62923,
|
||||
"hackerspace": 1537,
|
||||
"nature_reserve": 129575,
|
||||
"park": 1168747,
|
||||
"picnic_table": 302582,
|
||||
"pitch": 2307262,
|
||||
"playground": 821692,
|
||||
"sports_centre": 231823,
|
||||
"track": 124600
|
||||
},
|
||||
"man_made": {
|
||||
"surveillance": 205953
|
||||
},
|
||||
"memorial": {
|
||||
"ghost_bike": 748,
|
||||
"plaque": 45536
|
||||
},
|
||||
"motorcar": {
|
||||
"no": 270350,
|
||||
"yes": 190966
|
||||
},
|
||||
"natural": {
|
||||
"tree": 18245059
|
||||
"cliff": 761375,
|
||||
"rock": 229114,
|
||||
"stone": 52141,
|
||||
"tree": 23309774
|
||||
},
|
||||
"ngo": {
|
||||
"bicycle": 0,
|
||||
"cycling": 0
|
||||
},
|
||||
"office": {
|
||||
"government": 250353
|
||||
},
|
||||
"parking_space": {
|
||||
"disabled": 161162
|
||||
},
|
||||
"planned:amenity": {
|
||||
"charging_station": 72
|
||||
},
|
||||
"playground": {
|
||||
"forest": 77
|
||||
},
|
||||
"post_office": {
|
||||
"post_partner": 7560
|
||||
},
|
||||
"pub": {
|
||||
"bicycle": 0,
|
||||
"cycling": 12
|
||||
},
|
||||
"public_transport": {
|
||||
"platform": 3254387
|
||||
},
|
||||
"railway": {
|
||||
"platform": 167408
|
||||
},
|
||||
"recycling_type": {
|
||||
"centre": 29508,
|
||||
"container": 355016
|
||||
},
|
||||
"route": {
|
||||
"bus": 272174
|
||||
},
|
||||
"service:bicycle:cleaning": {
|
||||
"diy": 4,
|
||||
"yes": 909
|
||||
},
|
||||
"service:bicycle:pump": {
|
||||
"no": 1548,
|
||||
"yes": 12452
|
||||
},
|
||||
"service:bicycle:pump:operational_status": {
|
||||
"broken": 122
|
||||
},
|
||||
"service:bicycle:rental": {
|
||||
"yes": 3902
|
||||
},
|
||||
"service:bicycle:repair": {
|
||||
"yes": 15134
|
||||
},
|
||||
"service:bicycle:tools": {
|
||||
"no": 354,
|
||||
"yes": 5872
|
||||
},
|
||||
"shelter": {
|
||||
"yes": 884942
|
||||
},
|
||||
"shop": {
|
||||
"bicycle": 51336,
|
||||
"bicycle_rental": 1,
|
||||
"rental": 5206,
|
||||
"sports": 40802
|
||||
},
|
||||
"sport": {
|
||||
"bicycle": 114,
|
||||
"climbing": 29028,
|
||||
"cycling": 8225
|
||||
},
|
||||
"surface:colour": {
|
||||
"rainbow": 217
|
||||
},
|
||||
"surveillance:type": {
|
||||
"ALPR": 4424,
|
||||
"ANPR": 3,
|
||||
"camera": 165247
|
||||
},
|
||||
"theme": {
|
||||
"bicycle": 16,
|
||||
"cycling": 7
|
||||
},
|
||||
"toilets": {
|
||||
"yes": 70811
|
||||
},
|
||||
"tourism": {
|
||||
"artwork": 245861,
|
||||
"hotel": 407208,
|
||||
"map": 51,
|
||||
"viewpoint": 219932
|
||||
},
|
||||
"tower:type": {
|
||||
"observation": 23057
|
||||
},
|
||||
"type": {
|
||||
"route": 1005677
|
||||
},
|
||||
"vending": {
|
||||
"elongated_coin": 816,
|
||||
"parcel_pickup;parcel_mail_in": 522,
|
||||
"parking_tickets": 70753,
|
||||
"public_transport_tickets": 26895
|
||||
}
|
||||
}
|
||||
}
|
4
src/assets/svg/Center.svelte
Normal file
4
src/assets/svg/Center.svelte
Normal file
|
@ -0,0 +1,4 @@
|
|||
<script>
|
||||
export let color = "#000000"
|
||||
</script>
|
||||
<svg {...$$restProps} on:click on:mouseover on:mouseenter on:mouseleave on:keydown width="544.02838" height="544.02838" viewBox="0 0 544.02838 544.02838" version="1.1" id="svg1" sodipodi:docname="center.svg" inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <defs id="defs1" /> <sodipodi:namedview id="namedview1" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1" inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" showguides="true" inkscape:zoom="0.90326851" inkscape:cx="393.57068" inkscape:cy="250.756" inkscape:window-width="1920" inkscape:window-height="995" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg1"> <sodipodi:guide position="171.95879,103.32864" orientation="0,-1" id="guide4" inkscape:locked="false" /> <sodipodi:guide position="271.68286,132.35281" orientation="1,0" id="guide5" inkscape:locked="false" /> </sodipodi:namedview> <path d="m 365.63918,111.75001 h -62.375 V 15.9375 c 0,-8.75 -7,-15.9375 -15.625,-15.9375 h -31.1875 c -8.5625,0 -15.625,7.1875 -15.625,15.9375 v 95.81251 h -62.375 l 93.5625,127.75 z" id="path1" sodipodi:nodetypes="ccsssscccc" /> <path d="m 432.27837,365.63919 v -62.375 h 95.8125 c 8.75,0 15.9375,-7 15.9375,-15.625 v -31.1875 c 0,-8.5625 -7.1875,-15.625 -15.9375,-15.625 h -95.8125 v -62.375 l -127.75,93.5625 z" id="path1-5" sodipodi:nodetypes="ccsssscccc" /> <path d="m 178.38918,432.27838 h 62.375 v 95.8125 c 0,8.75 7,15.9375 15.625,15.9375 h 31.1875 c 8.5625,0 15.625,-7.1875 15.625,-15.9375 v -95.8125 h 62.375 l -93.5625,-127.75 z" id="path2" sodipodi:nodetypes="ccsssscccc" /> <path d="m 111.75,178.38919 v 62.375 H 15.9375 c -8.75,0 -15.9375,7 -15.9375,15.625 v 31.1875 c 0,8.5625 7.1875,15.625 15.9375,15.625 H 111.75 v 62.375 l 127.74999,-93.5625 z" id="path3" sodipodi:nodetypes="ccsssscccc" /> </svg>
|
|
@ -280,6 +280,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;;
|
||||
}
|
||||
|
|
|
@ -125,7 +125,21 @@ describe("PrepareTheme", () => {
|
|||
en: "Test layer - please ignore",
|
||||
},
|
||||
titleIcons: [],
|
||||
pointRendering: [{ location: ["point"], label: "xyz" }],
|
||||
pointRendering: [
|
||||
{
|
||||
location: ["point"],
|
||||
label: "xyz",
|
||||
iconBadges: [
|
||||
{
|
||||
if: "_favourite=yes",
|
||||
then: <any>{
|
||||
id: "circlewhiteheartred",
|
||||
render: "circle:white;heart:red",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
lineRendering: [{ width: 1 }],
|
||||
}
|
||||
const sharedLayers = constructSharedLayers()
|
||||
|
@ -165,7 +179,21 @@ describe("PrepareTheme", () => {
|
|||
id: "layer-example",
|
||||
name: null,
|
||||
minzoom: 18,
|
||||
pointRendering: [{ location: ["point"], label: "xyz" }],
|
||||
pointRendering: [
|
||||
{
|
||||
location: ["point"],
|
||||
label: "xyz",
|
||||
iconBadges: [
|
||||
{
|
||||
if: "_favourite=yes",
|
||||
then: {
|
||||
id: "circlewhiteheartred",
|
||||
render: "circle:white;heart:red",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
lineRendering: [{ width: 1 }],
|
||||
titleIcons: [],
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue