Merge branch 'develop' into RobinLinde-patch-1

This commit is contained in:
Pieter Vander Vennet 2023-12-02 00:57:15 +01:00 committed by GitHub
commit 90591924b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1056 additions and 488 deletions

View file

@ -343,6 +343,7 @@
},
"id": "Rock type (crag/rock/cliff only)"
},
"reviews",
{
"id": "default_climbing_questions",
"builtin": [

View file

@ -129,6 +129,7 @@
},
"payment-options",
"opening_hours",
"reviews",
{
"id": "shoe_rental",
"question": {

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: Unkown
SPDX-License-Identifier: CC0
SPDX-License-Identifier: CC0-1.0

View file

@ -62,6 +62,11 @@
"cs": "Pitná voda"
}
},
"titleIcons": [
"icons.defaults",
"auto:type",
"auto:seasonal"
],
"pointRendering": [
{
"iconBadges": [
@ -74,6 +79,10 @@
]
},
"then": "close:#c33"
},
{
"if": "tourism=artwork",
"then": "circle:white;./assets/layers/artwork/artwork.svg"
}
],
"iconSize": "40,40",
@ -149,7 +158,10 @@
"mappings": [
{
"if": "operational_status=",
"addExtraTags": ["disused:amenity=","amenity=drinking_water"],
"addExtraTags": [
"disused:amenity=",
"amenity=drinking_water"
],
"then": {
"en": "This drinking water works",
"nl": "Deze drinkwaterfontein werkt",
@ -296,11 +308,134 @@
{
"if": "fee=yes",
"then": {
"en":"One needs to pay to use this drinking water point"
"en": "One needs to pay to use this drinking water point"
}
}
]
},
{
"id": "seasonal",
"question": {
"en": "Is this drinking water point available all year round?"
},
"mappings": [
{
"if": "seasonal=no",
"then": {
"en": "This drinking water point is available all around the year"
}
},
{
"if": "seasonal=summer",
"then": {
"en": "This drinking water point is only available in summer"
}
},
{
"if": "seasonal=spring;summer;autumn",
"icon": "./assets/layers/drinking_water/no_winter.svg",
"then": {
"en": "This drinking water point is closed during the winter"
}
}
]
},
{
"builtin": "opening_hours_24_7",
"override": {
"questionHint": {
"en": "These are the opening hours if the drinking water fountain is operational."
},
"+mappings": [
{
"if": {
"and": [
"seasonal!=no",
{
"or": [
{
"and": [
"seasonal!~.*winter.*",
"_now:date~....-(12|01|02)-.."
]
},
{
"and": [
"seasonal!~.*spring.*",
"_now:date~....-(03|04|05)-.."
]
},
{
"and": [
"seasonal!~.*summer.*",
"_now:date~....-(06|07|08)-.."
]
},
{
"and": [
"seasonal!~.*autumn.*",
"_now:date~....-(09|10|11)-.."
]
}
]
}
]
},
"then": {
"en": "This drinking water fountain is closed this season. As such, the opening hours are not shown."
},
"hideInAnswer": true
}
]
}
},
{
"id": "bench-artwork",
"question": {
"en": "Does this drinking water fountain have an artistic element?",
"nl": "Heeft dit drinkwaterpunt een geintegreerd kunstwerk?"
},
"mappings": [
{
"if": "tourism=artwork",
"addExtraTags": [
"not:tourism:artwork="
],
"then": {
"en": "This drinking water point has an integrated artwork",
"nl": "Dit drinkwaterpunt heeft een geintegreerd kunstwerk"
}
},
{
"if": "not:tourism:artwork=yes",
"then": {
"en": "This drinking water point does not have an integrated artwork",
"nl": "Dit drinkwaterpunt heeft geen geïntegreerd kunstwerk"
},
"addExtraTags": [
"tourism="
]
},
{
"if": "tourism=",
"then": {
"en": "This drinking water point <span class=\"subtle\">probably</span> doesn't have an integrated artwork",
"nl": "Dit drinkwaterpunt heeft <span class=\"subtle\">waarschijnlijk</span> geen geïntegreerd kunstwerk"
},
"hideInAnswer": true
}
],
"questionHint": {
"en": "E.g. it has an integrated statue or other non-trivial, creative work",
"nl": "Bijvoorbeeld een standbeeld of ander, niet-triviaal kunstwerk"
}
},
{
"builtin": "artwork.*artwork-question",
"override": {
"condition": "tourism=artwork"
}
},
{
"id": "render-closest-drinking-water",
"render": {
@ -368,7 +503,7 @@
]
},
"then": {
"en":"This is a historic, manual water pump where no drinking water can be found"
"en": "This is a historic, manual water pump where no drinking water can be found"
}
}
]

View file

@ -33,6 +33,14 @@
"https://osoc.be/editions/2020/cyclofix"
]
},
{
"path": "no_winter.svg",
"license": "CC0-1.0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
},
{
"path": "tap.svg",
"license": "CC0-1.0",
@ -42,5 +50,13 @@
"sources": [
"https://commons.wikimedia.org/wiki/File:Water_DIN-style.svg"
]
},
{
"path": "winter.svg",
"license": "CC0-1.0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
}
]

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg"
version="1.1"
width="307.91855"
height="343.46448"
viewBox="0 0 307.91855 343.46448"
sodipodi:docname="no_winter.svg"
inkscape:version="1.3.1 (1:1.3.1+202311172155+91b66b0783)"
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"
inkscape:zoom="1.2257936"
inkscape:cx="190.48884"
inkscape:cy="271.25284"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg" />
<g
id="g1"
transform="rotate(-0.2875813,162.85132,95.170788)">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 5.008225,86.10653 302.13815,258.03113"
id="path3"
sodipodi:nodetypes="cc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 153.09023,0.048073 1.65028,343.280557"
id="path3-7"
sodipodi:nodetypes="cc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 302.54004,85.82932 5.2907,257.54738"
id="path4"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 5.786561,200.08323 53.987051,25.55191 -3.26951,61.20155"
id="path6" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 56.719332,59.041006 3.20013,59.642774 -55.391241,26.23225"
id="path7" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 201.87442,31.964241 152.44006,65.486554 101.3842,31.580519"
id="path8" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 203.26283,313.48917 -49.04644,-34.08735 -51.44157,33.31792"
id="path9" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 300.19729,199.07429 -53.31225,26.93149 4.84196,61.09726"
id="path10" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 251.37871,59.718988 -4.44648,59.562822 54.83066,27.38467"
id="path11" />
</g>
<path
style="fill:#000000;stroke:#e40000;stroke-width:25;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 23.115301,297.04602 290.36883,29.792506"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Pieter Vander Vennet
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg"
version="1.1"
width="307.54224"
height="343.37671"
viewBox="0 0 307.54224 343.37671"
sodipodi:docname="no_winter.svg"
inkscape:version="1.3.1 (1:1.3.1+202311172155+91b66b0783)"
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"
inkscape:zoom="1.2257936"
inkscape:cx="190.48884"
inkscape:cy="271.25285"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 5.008225,86.10653 302.13815,258.03113"
id="path3"
sodipodi:nodetypes="cc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 153.09023,0.048073 1.65028,343.280557"
id="path3-7"
sodipodi:nodetypes="cc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 302.54004,85.82932 5.2907,257.54738"
id="path4"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 5.786561,200.08323 53.987051,25.55191 -3.26951,61.20155"
id="path6" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 56.719332,59.041006 3.20013,59.642774 -55.391241,26.23225"
id="path7" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 201.87442,31.964241 152.44006,65.486554 101.3842,31.580519"
id="path8" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 203.26283,313.48917 -49.04644,-34.08735 -51.44157,33.31792"
id="path9" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 300.19729,199.07429 -53.31225,26.93149 4.84196,61.09726"
id="path10" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 251.37871,59.718988 -4.44648,59.562822 54.83066,27.38467"
id="path11" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Pieter Vander Vennet
SPDX-License-Identifier: CC0-1.0

View file

@ -27,7 +27,7 @@
{
"#": "ignore-image-in-then",
"if": "wikipedia=",
"then": "<a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank' rel='noopener'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>"
"then": "<a class='h-8' href='https://www.wikidata.org/wiki/{wikidata}' target='_blank' rel='noopener'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>"
}
]
},

View file

@ -459,8 +459,8 @@
{
"if": "surface=fine_gravel",
"then": {
"en":"The surface is <b>fine gravel</b>",
"nl":"De ondergrond bestaat uit <b>grind</b>"
"en": "The surface is <b>fine gravel</b>",
"nl": "De ondergrond bestaat uit <b>grind</b>"
}
}
],

View file

@ -345,8 +345,7 @@
"key": "access:description"
}
},
"questions",
"reviews"
"questions"
]
}
},

View file

@ -102,9 +102,10 @@
"=pointRendering": [
{
"marker": [
{"icon": "pin",
"color":"#ba2792"}
,
{
"icon": "pin",
"color": "#ba2792"
},
{
"icon": "./assets/themes/cyclofix/key.svg"
}

View file

@ -542,7 +542,9 @@
"leisure=garden",
"garden:type=facade_garden"
],
"snapToLayer":["walls_and_buildings"],
"snapToLayer": [
"walls_and_buildings"
],
"title": {
"nl": "een geveltuintje",
"en": "a facade garden",

View file

@ -166,31 +166,31 @@
{
"if": "sidewalk:left|right=yes",
"then": {
"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"
"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"
}
},
{
"if": "sidewalk:left|right=no",
"then": {
"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ć"
"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ć"
}
},
{

View file

@ -4259,6 +4259,21 @@
"question": "Is this drinking water spot still operational?",
"render": "The operational status is <i>{operational_status}</i>"
},
"bench-artwork": {
"mappings": {
"0": {
"then": "This drinking water point has an integrated artwork"
},
"1": {
"then": "This drinking water point does not have an integrated artwork"
},
"2": {
"then": "This drinking water point <span class=\"subtle\">probably</span> doesn't have an integrated artwork"
}
},
"question": "Does this drinking water fountain have an artistic element?",
"questionHint": "E.g. it has an integrated statue or other non-trivial, creative work"
},
"fee": {
"mappings": {
"0": {
@ -4270,9 +4285,33 @@
},
"question": "Is this drinking water point free to use?"
},
"opening_hours_24_7": {
"override": {
"+mappings": {
"0": {
"then": "This drinking water fountain is closed this season. As such, the opening hours are not shown."
}
},
"questionHint": "These are the opening hours if the drinking water fountain is operational."
}
},
"render-closest-drinking-water": {
"render": "<a href='#{_closest_other_drinking_water_id}'>There is another drinking water fountain at {_closest_other_drinking_water_distance} meters</a>"
},
"seasonal": {
"mappings": {
"0": {
"then": "This drinking water point is available all around the year"
},
"1": {
"then": "This drinking water point is only available in summer"
},
"2": {
"then": "This drinking water point is closed during the winter"
}
},
"question": "Is this drinking water point available all year round?"
},
"type": {
"mappings": {
"0": {

View file

@ -4092,6 +4092,21 @@
"question": "Is deze drinkwaterkraan nog steeds werkende?",
"render": "Deze waterkraan-status is <i>{operational_status}</i>"
},
"bench-artwork": {
"mappings": {
"0": {
"then": "Dit drinkwaterpunt heeft een geintegreerd kunstwerk"
},
"1": {
"then": "Dit drinkwaterpunt heeft geen geïntegreerd kunstwerk"
},
"2": {
"then": "Dit drinkwaterpunt heeft <span class=\"subtle\">waarschijnlijk</span> geen geïntegreerd kunstwerk"
}
},
"question": "Heeft dit drinkwaterpunt een geintegreerd kunstwerk?",
"questionHint": "Bijvoorbeeld een standbeeld of ander, niet-triviaal kunstwerk"
},
"render-closest-drinking-water": {
"render": "<a href='#{_closest_other_drinking_water_id}'>Er bevindt zich een ander drinkwaterpunt op {_closest_other_drinking_water_distance} meter</a>"
},

168
package-lock.json generated
View file

@ -92,7 +92,7 @@
"prettier-plugin-tailwindcss": "^0.3.0",
"read-file": "^0.2.0",
"sass": "^1.58.0",
"sharp": "^0.30.5",
"sharp": "^0.32.6",
"svelte": "^3.55.1",
"svelte-check": "^3.0.2",
"svelte-preprocess": "^5.0.1",
@ -4704,6 +4704,12 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b4a": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
"integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==",
"dev": true
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.10.tgz",
@ -5898,9 +5904,9 @@
}
},
"node_modules/detect-libc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
"dev": true,
"engines": {
"node": ">=8"
@ -6868,6 +6874,12 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"dev": true
},
"node_modules/fast-glob": {
"version": "3.2.12",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
@ -9144,6 +9156,12 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
"dev": true
},
"node_modules/node-html-parser": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.5.tgz",
@ -10035,6 +10053,12 @@
}
]
},
"node_modules/queue-tick": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
"dev": true
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
@ -10679,23 +10703,23 @@
}
},
"node_modules/sharp": {
"version": "0.30.7",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz",
"integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==",
"version": "0.32.6",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.1",
"node-addon-api": "^5.0.0",
"detect-libc": "^2.0.2",
"node-addon-api": "^6.1.0",
"prebuild-install": "^7.1.1",
"semver": "^7.3.7",
"semver": "^7.5.4",
"simple-get": "^4.0.1",
"tar-fs": "^2.1.1",
"tar-fs": "^3.0.4",
"tunnel-agent": "^0.6.0"
},
"engines": {
"node": ">=12.13.0"
"node": ">=14.15.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
@ -10744,12 +10768,6 @@
"node": ">=10"
}
},
"node_modules/sharp/node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"dev": true
},
"node_modules/sharp/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@ -10765,6 +10783,28 @@
"node": ">=10"
}
},
"node_modules/sharp/node_modules/tar-fs": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz",
"integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==",
"dev": true,
"dependencies": {
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
}
},
"node_modules/sharp/node_modules/tar-stream": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz",
"integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==",
"dev": true,
"dependencies": {
"b4a": "^1.6.4",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/sharp/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@ -11113,6 +11153,16 @@
"node": "*"
}
},
"node_modules/streamx": {
"version": "2.15.5",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.5.tgz",
"integrity": "sha512-9thPGMkKC2GctCzyCUjME3yR03x2xNo0GPKGkRw2UMYN+gqWa9uqpyNWhmsNCutU5zHmkUum0LsCRQTXUgUCAg==",
"dev": true,
"dependencies": {
"fast-fifo": "^1.1.0",
"queue-tick": "^1.0.1"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -16879,6 +16929,12 @@
"proxy-from-env": "^1.1.0"
}
},
"b4a": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
"integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==",
"dev": true
},
"babel-plugin-polyfill-corejs2": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.10.tgz",
@ -17748,9 +17804,9 @@
"dev": true
},
"detect-libc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
"dev": true
},
"detective": {
@ -18485,6 +18541,12 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"dev": true
},
"fast-glob": {
"version": "3.2.12",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
@ -20209,6 +20271,12 @@
}
}
},
"node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
"dev": true
},
"node-html-parser": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.5.tgz",
@ -20786,6 +20854,12 @@
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
},
"queue-tick": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
"dev": true
},
"quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
@ -21290,18 +21364,18 @@
}
},
"sharp": {
"version": "0.30.7",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz",
"integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==",
"version": "0.32.6",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
"dev": true,
"requires": {
"color": "^4.2.3",
"detect-libc": "^2.0.1",
"node-addon-api": "^5.0.0",
"detect-libc": "^2.0.2",
"node-addon-api": "^6.1.0",
"prebuild-install": "^7.1.1",
"semver": "^7.3.7",
"semver": "^7.5.4",
"simple-get": "^4.0.1",
"tar-fs": "^2.1.1",
"tar-fs": "^3.0.4",
"tunnel-agent": "^0.6.0"
},
"dependencies": {
@ -21339,12 +21413,6 @@
"yallist": "^4.0.0"
}
},
"node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"dev": true
},
"semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@ -21354,6 +21422,28 @@
"lru-cache": "^6.0.0"
}
},
"tar-fs": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz",
"integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==",
"dev": true,
"requires": {
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
}
},
"tar-stream": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz",
"integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==",
"dev": true,
"requires": {
"b4a": "^1.6.4",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@ -21606,6 +21696,16 @@
"resolved": "https://registry.npmjs.org/store/-/store-2.0.12.tgz",
"integrity": "sha512-eO9xlzDpXLiMr9W1nQ3Nfp9EzZieIQc10zPPMP5jsVV7bLOziSFFBP0XoDXACEIFtdI+rIz0NwWVA/QVJ8zJtw=="
},
"streamx": {
"version": "2.15.5",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.5.tgz",
"integrity": "sha512-9thPGMkKC2GctCzyCUjME3yR03x2xNo0GPKGkRw2UMYN+gqWa9uqpyNWhmsNCutU5zHmkUum0LsCRQTXUgUCAg==",
"dev": true,
"requires": {
"fast-fifo": "^1.1.0",
"queue-tick": "^1.0.1"
}
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",

View file

@ -62,7 +62,7 @@
"generate:schemas": "ts2json-schema -p src/Models/ThemeConfig/Json/ -o Docs/Schemas/ -t tsconfig.json -R . -m \".*ConfigJson\" && echo 'tsjson is done' && vite-node scripts/fixSchemas.ts ",
"fix:schemas": "vite-node scripts/fixSchemas.ts ",
"watch:schemas": "cd Models/ThemeConfig/Json & ls | entr -s 'npm run generate:schemas' ",
"generate:service-worker": "tsc src/service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js",
"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",
@ -178,7 +178,7 @@
"prettier-plugin-tailwindcss": "^0.3.0",
"read-file": "^0.2.0",
"sass": "^1.58.0",
"sharp": "^0.30.5",
"sharp": "^0.32.6",
"svelte": "^3.55.1",
"svelte-check": "^3.0.2",
"svelte-preprocess": "^5.0.1",

View file

@ -841,6 +841,10 @@ video {
margin-right: 3rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mt-4 {
margin-top: 1rem;
}
@ -877,10 +881,6 @@ video {
margin-right: 0.25rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.ml-1 {
margin-left: 0.25rem;
}
@ -929,6 +929,10 @@ video {
margin-right: 0.75rem;
}
.mt-12 {
margin-top: 3rem;
}
.mr-12 {
margin-right: 3rem;
}
@ -1784,6 +1788,10 @@ video {
padding-bottom: 0.5rem;
}
.pt-1 {
padding-top: 0.25rem;
}
.text-center {
text-align: center;
}

View file

@ -44,6 +44,36 @@ async function prepareFile(url: string): Promise<string> {
}
return null
}
async function handleDelete(req: http.IncomingMessage, res: ServerResponse) {
let body = ""
req.on("data", (chunk) => {
body = body + chunk
})
const paths = req.url.split("/")
console.log("Got a valid delete to:", paths.join("/"))
for (let i = 1; i < paths.length; i++) {
const p = paths.slice(0, i)
const dir = STATIC_PATH + p.join("/")
if (!fs.existsSync(dir)) {
res.writeHead(304, { "Content-Type": MIME_TYPES.html })
res.write("<html><body>No parent directory, nothing deleted</body></html>", "utf8")
res.end()
return
}
}
const path = STATIC_PATH + paths.join("/")
if(!fs.existsSync(path)){
res.writeHead(304, { "Content-Type": MIME_TYPES.html })
res.write("<html><body>File not found</body></html>", "utf8")
res.end()
return
}
fs.renameSync(path, path+".bak")
res.writeHead(200, { "Content-Type": MIME_TYPES.html })
res.write("<html><body>File moved to backup</body></html>", "utf8")
res.end()
}
async function handlePost(req: http.IncomingMessage, res: ServerResponse) {
let body = ""
@ -53,6 +83,7 @@ async function handlePost(req: http.IncomingMessage, res: ServerResponse) {
await new Promise((resolve) => req.on("end", resolve))
console.log(new Date().toISOString())
let parsed: any
try {
parsed = JSON.parse(body)
@ -84,7 +115,7 @@ async function handlePost(req: http.IncomingMessage, res: ServerResponse) {
http.createServer(async (req: http.IncomingMessage, res) => {
try {
console.log(req.method + " " + req.url, "from:", req.headers.origin)
console.log(req.method + " " + req.url, "from:", req.headers.origin, new Date().toISOString())
res.setHeader(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
@ -101,6 +132,12 @@ http.createServer(async (req: http.IncomingMessage, res) => {
return
}
if(req.method === "DELETE"){
console.log("Got a DELETE", new Date())
await handleDelete(req, res)
return
}
const url = new URL(`http://127.0.0.1/` + req.url)
console.log("URL pathname is")
if (url.pathname.endsWith("overview")) {

View file

@ -1,42 +1,14 @@
import { Utils } from "../../Utils"
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
export class ThemeMetaTagging {
public static readonly themeName = "usersettings"
public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
feat.properties._description
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
?.at(1)
)
Utils.AddLazyProperty(
feat.properties,
"_d",
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
}
}
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
feat.properties['__current_backgroun'] = 'initial_value'
}
}

View file

@ -10,10 +10,7 @@ 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"
@ -26,7 +23,7 @@ import predifined_filters from "../../../../assets/layers/filters/filters.json"
import { TagConfigJson } from "../Json/TagConfigJson"
import PointRenderingConfigJson, { IconConfigJson } from "../Json/PointRenderingConfigJson"
import ValidationUtils from "./ValidationUtils"
import { RenderingSpecification, SpecialVisualization } from "../../../UI/SpecialVisualization"
import { RenderingSpecification } from "../../../UI/SpecialVisualization"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import { ConfigMeta } from "../../../UI/Studio/configMeta"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
@ -1220,6 +1217,54 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
return json
}
}
export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
constructor() {
super(
"The auto-icon creates a (non-clickable) title icon based on a tagRendering which has icons",
["titleIcons"],
"AutoTitleIcon"
)
}
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
json = { ...json }
json.titleIcons = [...json.titleIcons]
for (let i = 0; i < json.titleIcons.length; i++) {
const titleIcon = json.titleIcons[i]
if (typeof titleIcon !== "string") {
continue
}
if (!titleIcon.startsWith("auto:")) {
continue
}
const trId = titleIcon.substring("auto:".length)
const tr = <QuestionableTagRenderingConfigJson>json.tagRenderings.find((tr) => tr["id"] === trId)
if (tr === undefined) {
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 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 })
})
if (mappings.length === 0) {
context
.enters("titleIcons", i)
.warn("TagRendering with id " + trId + " does not have any icons, not generating an icon for this")
continue
}
json.titleIcons[i] = <TagRenderingConfigJson>{
id: "title_icon_auto_" + trId,
mappings,
}
}
return json
}
}
export class PrepareLayer extends Fuse<LayerConfigJson> {
constructor(state: DesugaringContext) {
@ -1247,6 +1292,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
),
new SetDefault("titleIcons", ["icons.defaults"]),
new AddRatingBadge(),
new AutoTitleIcon(),
new On(
"titleIcons",
(layer) =>

View file

@ -240,6 +240,8 @@ export interface LayerConfigJson {
* Use an empty array to hide them.
* Note that "defaults" will insert all the default titleIcons (which are added automatically)
*
* Use `auto:<tagrenderingId>` to automatically create an icon based on a tagRendering which has icons
*
* Type: icon[]
* group: infobox
*/

View file

@ -118,7 +118,7 @@ export default class MoreScreen extends Combine {
if (search === undefined) {
return true
}
search = search.toLocaleLowerCase()
search = Utils.RemoveDiacritics(search.toLocaleLowerCase())
if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) {
return true
}
@ -131,7 +131,7 @@ export default class MoreScreen extends Combine {
continue
}
const term = entity["*"] ?? entity[Locale.language.data]
if (term?.toLowerCase()?.indexOf(search) >= 0) {
if (Utils.RemoveDiacritics(term?.toLowerCase())?.indexOf(search) >= 0) {
return true
}
}

View file

@ -27,7 +27,7 @@
</script>
{#if mapping.icon !== undefined}
<div class="inline-flex">
<div class="inline-flex items-center">
<img
class={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-1")}
src={mapping.icon}

View file

@ -1,202 +1,200 @@
<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
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)
})
// We received a new config -> reinit
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
/**
* 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
}),
]
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)
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)
}
}
// 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)
}
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
}
}
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]
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 {
tags.data[k] = v
freeformInput.setData(undefined)
}
}
tags.ping()
return
feedback.setData(undefined)
}
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
$: {
// 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)
}
export let selectedTags: TagsFilter = undefined
change
.CreateChangeDescriptions()
.then((changes) => state.changes.applyChanges(changes))
.catch(console.error)
}
let mappings: Mapping[] = config?.mappings
let searchTerm: UIEventSource<string> = new UIEventSource("")
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
})
)
}
$: {
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
}),
)
}
</script>
{#if question !== undefined}
<div
class="interactive border-interactive relative flex flex-col overflow-y-auto p-1 px-2"
style="max-height: 85vh"
class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
style="max-height: 75vh"
>
<div class="sticky top-0" style="z-index: 11">
<div class="interactive sticky top-0 flex justify-between">
<div class="sticky top-0 interactive pt-1 flex justify-between" style="z-index: 11">
<span class="font-bold">
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
</span>
<slot name="upper-right" />
</div>
<slot name="upper-right" />
</div>
{#if config.questionhint}
@ -213,7 +211,7 @@
{#if config.mappings?.length >= 8}
<div class="sticky flex w-full">
<Search class="h-6 w-6"/>
<Search class="h-6 w-6" />
<input type="text" bind:value={$searchTerm} class="w-full" />
</div>
{/if}

View file

@ -9,7 +9,7 @@
import { Utils } from "../../Utils"
import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"
import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte"
import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"
import { ChevronRightIcon, TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import SchemaBasedInput from "./SchemaBasedInput.svelte"
import FloatOver from "../Base/FloatOver.svelte"
import TagRenderingInput from "./TagRenderingInput.svelte"
@ -21,6 +21,7 @@
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw
export let state: EditLayerState
export let backToStudio: () => void
let messages = state.messages
let hasErrors = messages.mapD(
(m: ConversionMessage[]) => m.filter((m) => m.level === "error").length
@ -72,6 +73,10 @@
})
let highlightedItem: UIEventSource<HighlightedTagRendering> = state.highlightedItem
function deleteLayer() {
state.delete()
backToStudio()
}
</script>
<div class="flex h-screen flex-col">
@ -113,6 +118,12 @@
</div>
<div class="flex flex-col" slot="content0">
<Region {state} configs={perRegion["Basic"]} />
<div class="mt-12">
<button on:click={() => deleteLayer()} class="small" >
<TrashIcon class="h-6 w-6"/> Delete this layer
</button>
</div>
</div>
<div slot="title1" class="flex">

View file

@ -107,6 +107,9 @@ export abstract class EditJsonState<T> {
return entry
}
public async delete(){
await this.server.delete(this.getId().data, this.category)
}
public getStoreFor<T>(path: ReadonlyArray<string | number>): UIEventSource<T | undefined> {
const key = path.join(".")

View file

@ -58,7 +58,14 @@ export default class StudioServer {
return undefined
}
}
async delete(id: string, category: "layers" | "themes") {
if (id === undefined || id === "") {
return
}
await fetch(this.urlFor(id, category), {
method: "DELETE"
})
}
async update(id: string, config: string, category: "layers" | "themes") {
if (id === undefined || id === "") {
return

View file

@ -260,7 +260,7 @@
<Loading />
</div>
{:else if state === "editing_layer"}
<EditLayer state={editLayerState}>
<EditLayer state={editLayerState} backToStudio={() => {state = undefined}}>
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
@ -284,22 +284,23 @@
</BackButton>
</EditTheme>
{/if}
</LoginToggle>
</If>
{#if { intro, tagrenderings: intro_tagrenderings }[$showIntro]?.sections}
<FloatOver
on:close={() => {
{#if { intro, tagrenderings: intro_tagrenderings }[$showIntro]?.sections}
<FloatOver
on:close={() => {
showIntro.setData("no")
}}
>
<div class="flex h-full p-4 pr-12">
<Walkthrough
pages={{ intro, tagrenderings: intro_tagrenderings }[$showIntro]?.sections}
on:done={() => {
>
<div class="flex h-full p-4 pr-12">
<Walkthrough
pages={{ intro, tagrenderings: intro_tagrenderings }[$showIntro]?.sections}
on:done={() => {
showIntro.setData("no")
}}
/>
</div>
</FloatOver>
{/if}
/>
</div>
</FloatOver>
{/if}
</LoginToggle>
</If>

View file

@ -66,71 +66,70 @@
import OpenJosm from "./Base/OpenJosm.svelte"
export let state: ThemeViewState
let layout = state.layout
let layout = state.layout
let maplibremap: UIEventSource<MlMap> = state.map
let selectedElement: UIEventSource<Feature> = state.selectedElement
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
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
$: console.log("Centerfeatures are", $centerFeatures)
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
}
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
return undefined
}
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementView, {
state,
layer,
selectedElement,
tags,
}).SetClass("h-full w-full")
},
[selectedLayer]
)
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementView, {
state,
layer,
selectedElement,
tags,
}).SetClass("h-full w-full")
},
[selectedLayer],
)
const selectedElementTitle = 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
}
const selectedElementTitle = 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
}
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags })
},
[selectedLayer]
)
const tags = state.featureProperties.getStore(selectedElement.properties.id)
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags })
},
[selectedLayer],
)
let mapproperties: MapProperties = state.mapProperties
let featureSwitches: FeatureSwitchState = state.featureSwitches
let availableLayers = state.availableLayers
let userdetails = state.osmConnection.userDetails
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
let rasterLayerName =
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name
})
)
let mapproperties: MapProperties = state.mapProperties
let featureSwitches: FeatureSwitchState = state.featureSwitches
let availableLayers = state.availableLayers
let userdetails = state.osmConnection.userDetails
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
let rasterLayerName =
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name
}),
)
</script>
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
@ -214,7 +213,7 @@
<!-- bottom left elements -->
<If condition={state.featureSwitches.featureSwitchFilter}>
<MapControlButton on:click={() => state.guistate.openFilterView()}>
<Filter class="h-6 w-6"/>
<Filter class="h-6 w-6" />
</MapControlButton>
</If>
<If condition={state.featureSwitches.featureSwitchBackgroundSelection}>
@ -231,16 +230,17 @@
</a>
</div>
</div>
{#if $arrowKeysWereUsed !== undefined}
<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 $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}
<div class="flex flex-col items-end">
<!-- bottom right elements -->
@ -257,7 +257,7 @@
<Plus class="w-8 h-8" />
</MapControlButton>
<MapControlButton on:click={() => mapproperties.zoom.update((z) => z - 1)}>
<Min class="w-8 h-8"/>
<Min class="w-8 h-8" />
</MapControlButton>
<If condition={featureSwitches.featureSwitchGeolocation}>
<MapControlButton>
@ -350,7 +350,7 @@
</div>
<div class="flex" slot="title1">
<Filter class="w-4 h-4"/>
<Filter class="w-4 h-4" />
<Tr t={Translations.t.general.menu.filter} />
</div>
@ -374,7 +374,7 @@
<div class="flex" slot="title2">
<If condition={state.featureSwitches.featureSwitchEnableExport}>
<Download class="w-4 h-4"/>
<Download class="w-4 h-4" />
<Tr t={Translations.t.general.download.title} />
</If>
</div>
@ -389,7 +389,7 @@
<ToSvelte construct={() => new CopyrightPanel(state)} slot="content3" />
<div class="flex" slot="title4">
<Share class="w-4 h-4"/>
<Share class="w-4 h-4" />
<Tr t={Translations.t.general.sharescreen.title} />
</div>
<div class="m-2" slot="content4">
@ -441,12 +441,12 @@
<Tr t={Translations.t.general.aboutMapComplete.intro} />
<a class="flex" href={Utils.HomepageLink()}>
<Add class="h-6 w-6"/>
<Add class="h-6 w-6" />
<Tr t={Translations.t.general.backToIndex} />
</a>
<a class="flex" href="https://github.com/pietervdvn/MapComplete/issues" target="_blank">
<Bug class="h-6 w-6"/>
<Bug class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.openIssueTracker} />
</a>
@ -495,7 +495,7 @@
</div>
<div class="flex" slot="title2">
<Community class="w-6 h-6"/>
<Community class="w-6 h-6" />
<Tr t={Translations.t.communityIndex.title} />
</div>
<div class="m-2" slot="content2">
@ -513,7 +513,7 @@
<div class="m-2 flex flex-col" slot="content4">
<If condition={featureSwitches.featureSwitchEnableLogin}>
<OpenIdEditor mapProperties={state.mapProperties} />
<OpenJosm {state}/>
<OpenJosm {state} />
<MapillaryLink mapProperties={state.mapProperties} />
</If>

View file

@ -11,7 +11,7 @@ export class Utils {
public static readonly assets_path = "./assets/svg/"
public static externalDownloadFunction: (
url: string,
headers?: any
headers?: any,
) => Promise<{ content: string } | { redirect: string }>
public static Special_visualizations_tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`.
This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature.
@ -150,7 +150,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
if (Utils.runningFromConsole) {
return
}
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
DOMPurify.addHook("afterSanitizeAttributes", function(node) {
// set all elements owning target to target=_blank + add noopener noreferrer
const target = node.getAttribute("target")
if (target) {
@ -172,7 +172,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
*/
public static ParseVisArgs(
specs: { name: string; defaultValue?: string }[],
args: string[]
args: string[],
): Record<string, string> {
const parsed: Record<string, string> = {}
if (args.length > specs.length) {
@ -320,7 +320,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
object: any,
name: string,
init: () => any,
whenDone?: () => void
whenDone?: () => void,
) {
Object.defineProperty(object, name, {
enumerable: false,
@ -343,7 +343,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
object: any,
name: string,
init: () => Promise<any>,
whenDone?: () => void
whenDone?: () => void,
) {
Object.defineProperty(object, name, {
enumerable: false,
@ -483,7 +483,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static SubstituteKeys(
txt: string | undefined,
tags: Record<string, any> | undefined,
useLang?: string
useLang?: string,
): string | undefined {
if (txt === undefined) {
return undefined
@ -519,7 +519,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
"SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is",
key,
"\nThe value is",
v
v,
)
v = v.InnerConstructElement()?.textContent
}
@ -561,38 +561,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
target.push(...source)
}
/**
* Recursively rewrites all keys from `+key`, `key+` and `=key` into `key
*
* Utils.CleanMergeObject({"condition":{"and+":["xyz"]}} // => {"condition":{"and":["xyz"]}}
* @param obj
* @constructor
* @private
*/
private static CleanMergeObject(obj: any) {
if (Array.isArray(obj)) {
const result = []
for (const el of obj) {
result.push(Utils.CleanMergeObject(el))
}
return result
}
if (typeof obj !== "object") {
return obj
}
const newObj = {}
for (let objKey in obj) {
let cleanKey = objKey
if (objKey.startsWith("+") || objKey.startsWith("=")) {
cleanKey = objKey.substring(1)
} else if (objKey.endsWith("+") || objKey.endsWith("=")) {
cleanKey = objKey.substring(0, objKey.length - 1)
}
newObj[cleanKey] = Utils.CleanMergeObject(obj[objKey])
}
return newObj
}
/**
* Copies all key-value pairs of the source into the target. This will change the target
* If the key starts with a '+', the values of the list will be appended to the target instead of overwritten
@ -664,7 +632,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
if (!Array.isArray(targetV)) {
throw new Error(
"Cannot concatenate: value to add is not an array: " +
JSON.stringify(targetV)
JSON.stringify(targetV),
)
}
if (Array.isArray(sourceV)) {
@ -672,9 +640,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} else {
throw new Error(
"Could not merge concatenate " +
JSON.stringify(sourceV) +
" and " +
JSON.stringify(targetV)
JSON.stringify(sourceV) +
" and " +
JSON.stringify(targetV),
)
}
} else {
@ -719,7 +687,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
path: string[],
object: any,
replaceLeaf: (leaf: any, travelledPath: string[]) => any,
travelledPath: string[] = []
travelledPath: string[] = [],
): void {
if (object == null) {
return
@ -750,7 +718,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
if (Array.isArray(sub)) {
sub.forEach((el, i) =>
Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, head, "" + i])
Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, head, "" + i]),
)
return
}
@ -767,7 +735,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
path: string[],
object: any,
collectedList: { leaf: any; path: string[] }[] = [],
travelledPath: string[] = []
travelledPath: string[] = [],
): { leaf: any; path: string[] }[] {
if (object === undefined || object === null) {
return collectedList
@ -797,7 +765,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
if (Array.isArray(sub)) {
sub.forEach((el, i) =>
Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i])
Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i]),
)
return collectedList
}
@ -841,7 +809,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
json: any,
f: (v: object | number | string | boolean | undefined, path: string[]) => any,
isLeaf: (object) => boolean = undefined,
path: string[] = []
path: string[] = [],
) {
if (json === undefined || json === null) {
return f(json, path)
@ -880,7 +848,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
json: any,
collect: (v: number | string | boolean | undefined, path: string[]) => any,
isLeaf: (object) => boolean = undefined,
path = []
path = [],
): void {
if (json === undefined) {
return
@ -955,7 +923,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
continue
}
const i = part.charCodeAt(0)
result += '"' + keys[i] + '":' + part.substring(1)
result += "\"" + keys[i] + "\":" + part.substring(1)
}
return result
@ -982,7 +950,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
url: string,
headers?: any,
method: "POST" | "GET" | "PUT" | "UPDATE" | "DELETE" | "OPTIONS" = "GET",
content?: string
content?: string,
): Promise<
| { content: string }
| { redirect: string }
@ -1047,7 +1015,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static async downloadJsonCached(
url: string,
maxCacheTimeMs: number,
headers?: any
headers?: any,
): Promise<any> {
const result = await Utils.downloadJsonAdvanced(url, headers)
if (result["content"]) {
@ -1059,7 +1027,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static async downloadJsonCachedAdvanced(
url: string,
maxCacheTimeMs: number,
headers?: any
headers?: any,
): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> {
const cached = Utils._download_cache.get(url)
if (cached !== undefined) {
@ -1069,9 +1037,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
const promise =
/*NO AWAIT as we work with the promise directly */ Utils.downloadJsonAdvanced(
url,
headers
)
url,
headers,
)
Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() })
return await promise
}
@ -1086,7 +1054,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static async downloadJsonAdvanced(
url: string,
headers?: any
headers?: any,
): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> {
const injected = Utils.injectedDownloads[url]
if (injected !== undefined) {
@ -1095,7 +1063,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
const result = await Utils.downloadAdvanced(
url,
Utils.Merge({ accept: "application/json" }, headers ?? {})
Utils.Merge({ accept: "application/json" }, headers ?? {}),
)
if (result["error"] !== undefined) {
return <{ error: string; url: string; statuscode?: number }>result
@ -1115,7 +1083,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
"due to",
e,
"\n",
e.stack
e.stack,
)
return { error: "malformed", url }
}
@ -1136,7 +1104,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
| "{gpx=application/gpx+xml}"
| "application/json"
| "image/png"
}
},
) {
const element = document.createElement("a")
let file
@ -1240,7 +1208,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
e.preventDefault()
return false
},
false
false,
)
}
@ -1324,7 +1292,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static sortedByLevenshteinDistance<T>(
reference: string,
ts: T[],
getName: (t: T) => string
getName: (t: T) => string,
): T[] {
const withDistance: [T, number][] = ts.map((t) => [
t,
@ -1350,7 +1318,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
track[j][i] = Math.min(
track[j][i - 1] + 1, // deletion
track[j - 1][i] + 1, // insertion
track[j - 1][i - 1] + indicator // substitution
track[j - 1][i - 1] + indicator, // substitution
)
}
}
@ -1359,7 +1327,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static MapToObj<V, T>(
d: Map<string, V>,
onValue: (t: V, key: string) => T
onValue: (t: V, key: string) => T,
): Record<string, T> {
const o = {}
const keys = Array.from(d.keys())
@ -1376,7 +1344,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
* Utils.TransposeMap({"a" : ["b", "c"], "x" : ["b", "y"]}) // => {"b" : ["a", "x"], "c" : ["a"], "y" : ["x"]}
*/
public static TransposeMap<K extends string, V extends string>(
d: Record<K, V[]>
d: Record<K, V[]>,
): Record<V, K[]> {
const newD: Record<V, K[]> = <any>{}
@ -1450,7 +1418,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
public static asDict(
tags: { key: string; value: string | number }[]
tags: { key: string; value: string | number }[],
): Map<string, string | number> {
const d = new Map<string, string | number>()
@ -1491,21 +1459,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
element.scrollIntoView({ behavior: "smooth", block: "nearest" })
}
private static findParentWithScrolling(element: HTMLBaseElement): HTMLBaseElement {
// Check if the element itself has scrolling
if (element.scrollHeight > element.clientHeight) {
return element
}
// If the element does not have scrolling, check if it has a parent element
if (!element.parentElement) {
return null
}
// If the element has a parent, repeat the process for the parent element
return Utils.findParentWithScrolling(<HTMLBaseElement>element.parentElement)
}
/**
* Returns true if the contents of `a` are the same (and in the same order) as `b`.
* Might have false negatives in some cases
@ -1572,7 +1525,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
*
*/
public static splitIntoSubstitutionParts(
template: string
template: string,
): ({ message: string } | { subs: string })[] {
const preparts = template.split("{")
const spec: ({ message: string } | { subs: string })[] = []
@ -1633,6 +1586,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
}
public static RemoveDiacritics(str?: string): string {
if(!str){
return str
}
return str.normalize("NFD").replace(/\p{Diacritic}/gu, "")
}
public static randomString(length: number): string {
let result = ""
for (let i = 0; i < length; i++) {
@ -1641,9 +1601,57 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
return result
}
/**
* Recursively rewrites all keys from `+key`, `key+` and `=key` into `key
*
* Utils.CleanMergeObject({"condition":{"and+":["xyz"]}} // => {"condition":{"and":["xyz"]}}
* @param obj
* @constructor
* @private
*/
private static CleanMergeObject(obj: any) {
if (Array.isArray(obj)) {
const result = []
for (const el of obj) {
result.push(Utils.CleanMergeObject(el))
}
return result
}
if (typeof obj !== "object") {
return obj
}
const newObj = {}
for (let objKey in obj) {
let cleanKey = objKey
if (objKey.startsWith("+") || objKey.startsWith("=")) {
cleanKey = objKey.substring(1)
} else if (objKey.endsWith("+") || objKey.endsWith("=")) {
cleanKey = objKey.substring(0, objKey.length - 1)
}
newObj[cleanKey] = Utils.CleanMergeObject(obj[objKey])
}
return newObj
}
private static findParentWithScrolling(element: HTMLBaseElement): HTMLBaseElement {
// Check if the element itself has scrolling
if (element.scrollHeight > element.clientHeight) {
return element
}
// If the element does not have scrolling, check if it has a parent element
if (!element.parentElement) {
return null
}
// If the element has a parent, repeat the process for the parent element
return Utils.findParentWithScrolling(<HTMLBaseElement>element.parentElement)
}
private static colorDiff(
c0: { r: number; g: number; b: number },
c1: { r: number; g: number; b: number }
c1: { r: number; g: number; b: number },
) {
return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b)
}

File diff suppressed because one or more lines are too long