Merge branch 'master' into weblate-mapcomplete-layers

This commit is contained in:
Pieter Vander Vennet 2023-09-28 14:49:33 +00:00 committed by GitHub
commit 916b423329
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 2034 additions and 1183 deletions

View file

@ -46,6 +46,7 @@
}, },
{ {
"id": "wikipedia-etymology", "id": "wikipedia-etymology",
"condition": "name~*",
"question": { "question": {
"en": "What is the Wikidata-item that this object is named after?", "en": "What is the Wikidata-item that this object is named after?",
"nl": "Wat is het Wikidata-item van hetgeen dit object is naar vernoemd?", "nl": "Wat is het Wikidata-item van hetgeen dit object is naar vernoemd?",

View file

@ -244,6 +244,33 @@
} }
} }
] ]
},
{
"id": "dogs",
"options": [
{
"question": {
"en": "No preference towards dogs"
}
},
{
"question": {
"en": "Dogs allowed"
},
"osmTags": {
"or": [
"dog=unleashed",
"dog=yes"
]
}
},
{
"question": {
"en": "No dogs allowed"
},
"osmTags": "dog=no"
}
]
} }
] ]
} }

View file

@ -149,7 +149,6 @@
}, },
"tagRenderings": [ "tagRenderings": [
"images", "images",
"level",
{ {
"question": { "question": {
"nl": "Wat is de naam van deze eetgelegenheid?", "nl": "Wat is de naam van deze eetgelegenheid?",
@ -213,6 +212,7 @@
"email", "email",
"phone", "phone",
"payment-options", "payment-options",
"level",
"wheelchair-access", "wheelchair-access",
{ {
"question": { "question": {
@ -1102,7 +1102,8 @@
}, },
"has_organic", "has_organic",
"accepts_cash", "accepts_cash",
"accepts_cards" "accepts_cards",
"dogs"
], ],
"deletion": { "deletion": {
"nonDeleteMappings": [ "nonDeleteMappings": [

View file

@ -154,6 +154,44 @@
} }
], ],
"condition": "id~(node|way|relation)/[0-9]*" "condition": "id~(node|way|relation)/[0-9]*"
},
{
"id": "dogicon",
"labels": [
"defaults"
],
"mappings": [
{
"if": "dog=no",
"#": "ignore-image-in-then",
"then": "<img textmode='\uD83D\uDC15 ⃠' alt='no_dogs' src='./assets/layers/questions/no_dogs.svg'>"
},
{
"if": "dog=leashed",
"#": "ignore-image-in-then",
"then": "<img textmode='\uD83D\uDC15' alt='dogs are allowed but leashed' src='./assets/layers/questions/dogs_leashed.svg'>"
},
{
"if": {
"or": [
"dog=yes",
"dog=unleashed"
]
},
"#": "ignore-image-in-then",
"then": "<img textmode='\uD83D\uDC15' alt='dogs are allowed' src='./assets/layers/questions/dogs_allowed.svg'>"
}
]
},
{
"id": "rating",
"labels": [
"defaults"
],
"icon": {
"class": "w-20 mx-1 flex items-center"
},
"render": "{rating()}"
} }
], ],
"mapRendering": null "mapRendering": null

View file

@ -89,9 +89,25 @@
} }
] ]
}, },
"titleIcons": [
"icons.defaults",
{
"render": "{ref}",
"condition": "ref~*"
},
{
"mappings": [
{
"if": "capacity~*",
"then": "<div class='w-fit font-bold '><img class='w-4 h-4' src='./assets/layers/indoors/room_conference.svg'/> {capacity}<div>"
}
]
}
],
"minzoom": 13, "minzoom": 13,
"tagRenderings": [ "tagRenderings": [
"images", "images",
"level",
{ {
"id": "ref", "id": "ref",
"question": { "question": {
@ -162,7 +178,204 @@
"indoor=corridor" "indoor=corridor"
] ]
} }
} },
{
"id": "room-type",
"question": {
"en": "What type of room is this?"
},
"mappings": [
{
"if": "room=administration",
"then": {
"en": "This is a administrative room"
},
"icon": "./assets/layers/indoors/room_administration.svg"
},
{
"if": "room=auditorium",
"then": {
"en": "This is a auditorium"
},
"icon": "./assets/layers/indoors/room_auditorium.svg"
},
{
"if": "room=bedroom",
"then": {
"en": "This is a bedroom"
},
"icon": "./assets/layers/indoors/room_bedroom.svg"
},
{
"if": "room=chapel",
"then": {
"en": "This is a chapel"
},
"icon": "./assets/layers/indoors/room_chapel.svg"
},
{
"if": "room=class",
"then": {
"en": "This is a classroom"
},
"icon": "./assets/layers/indoors/room_class.svg"
},
{
"if": "room=classroom",
"then": {
"en": "This is a classroom"
},
"icon": "./assets/layers/indoors/room_class.svg",
"hideInAnswer": true
},
{
"if": "room=computer",
"then": {
"en": "This is a computer room"
},
"icon": "./assets/layers/indoors/room_computer.svg"
},
{
"if": "room=conference",
"then": {
"en": "This is a conference room"
},
"icon": "./assets/layers/indoors/room_conference.svg"
},
{
"if": "room=crypt",
"then": {
"en": "This is a crypt"
},
"icon": "./assets/layers/indoors/room_crypt.svg"
},
{
"if": "room=kitchen",
"then": {
"en": "This is a kitchen"
},
"icon": "./assets/layers/indoors/room_kitchen.svg"
},
{
"if": "room=laboratory",
"then": {
"en": "This is a laboratory"
},
"icon": "./assets/layers/indoors/room_laboratory.svg"
},
{
"if": "room=library",
"then": {
"en": "This is a library"
},
"icon": "./assets/layers/indoors/room_library.svg"
},
{
"if": "room=locker",
"then": {
"en": "This is a locker room"
},
"icon": "./assets/layers/indoors/room_locker.svg"
},
{
"if": "room=nursery",
"then": {
"en": "This is a nursery"
},
"icon": "./assets/layers/indoors/room_nursery.svg"
},
{
"if": "room=office",
"then": {
"en": "This is an office"
},
"icon": "./assets/layers/indoors/room_office.svg"
},
{
"if": "room=prison_cell",
"then": {
"en": "This is a prison_cell"
},
"icon": "./assets/layers/indoors/room_prison_cell.svg"
},
{
"if": "room=restaurant",
"then": {
"en": "This is a restaurant"
},
"icon": "./assets/layers/indoors/room_restaurant.svg"
},
{
"if": "room=security_check",
"then": {
"en": "This is a room to perform security checks"
},
"icon": "./assets/layers/indoors/room_security_check.svg"
},
{
"if": "room=sport",
"then": {
"en": "This is a sport room"
},
"icon": "./assets/layers/indoors/room_sport.svg"
},
{
"if": "room=storage",
"then": {
"en": "This is a storage room"
},
"icon": "./assets/layers/indoors/room_storage.svg"
},
{
"if": "room=technical",
"then": {
"en": "This is a technical room"
},
"icon": "./assets/layers/indoors/room_technical.svg"
},
{
"if": "room=toilets",
"then": {
"en": "These are toilets"
},
"icon": "./assets/layers/indoors/room_toilets.svg"
},
{
"if": "room=waiting",
"then": {
"en": "This is a waiting room"
},
"icon": "./assets/layers/indoors/room_waiting.svg"
}
]
},
{
"id": "room-capacity",
"question": {
"en": "How much people can at most fit in this room?"
},
"condition": {
"or": [
"room=waiting",
"room=restaurant",
"room=office",
"room=nursery",
"room=conference",
"room=auditorium",
"room=chapel",
"room=bedroom",
"room=classroom"
]
},
"render": {
"en": "At most {capacity} people fit this room"
},
"freeform": {
"key": "capacity",
"type": "pnat"
}
},
"etymology.wikipedia-etymology"
], ],
"mapRendering": [ "mapRendering": [
{ {
@ -222,7 +435,7 @@
{ {
"if": { "if": {
"or": [ "or": [
"room=adminstration", "room=administration",
"room=auditorium", "room=auditorium",
"room=bedroom", "room=bedroom",
"room=chapel", "room=chapel",

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg3125"
sodipodi:docname="dogs_allowed.svg"
viewBox="0 0 360.94291 306.6074"
sodipodi:version="0.32"
version="1.0"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
width="360.9429"
height="306.60739"
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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs13" />
<sodipodi:namedview
id="base"
bordercolor="#666666"
inkscape:pageshadow="2"
inkscape:window-width="1920"
pagecolor="#ffffff"
inkscape:zoom="1.3715616"
inkscape:window-x="0"
showgrid="false"
borderopacity="1.0"
inkscape:current-layer="svg3125"
inkscape:cx="144.361"
inkscape:cy="169.51481"
inkscape:window-y="0"
inkscape:window-height="995"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:window-maximized="1" />
<path
id="path3137"
style="fill:#000000;stroke:#ffffff;stroke-width:1.15616"
d="M 218.33368,212.89652 H 106.97743 v 65.97861 c 0,36.20557 -54.947977,36.20557 -54.947977,0 V 134.81319 H 27.31312 c -35.3487001,0 -35.6624811,-48.841311 -0.832728,-48.841311 h 44.206996 v 0.0603 L 210.09088,85.983879 294.67915,1.3956086 V 44.480229 l 58.53233,58.532331 c 13.62536,14.14431 5.78082,31.41436 -6.77044,31.41436 h -73.48523 l -0.25344,144.44821 c 0,36.20557 -54.42903,36.20557 -54.42903,0 v -65.97861" />
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work>
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
<dc:publisher>
<cc:Agent
rdf:about="http://openclipart.org/">
<dc:title>Openclipart</dc:title>
</cc:Agent>
</dc:publisher>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/publicdomain/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: OpenClipArt
SPDX-License-Identifier: PD

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="90.859001"
height="84.87442"
viewBox="-0.258 -0.548 90.859003 84.874419"
xml:space="preserve"
sodipodi:docname="dogs_leashed.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
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="defs17">
</defs><sodipodi:namedview
id="namedview15"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="4.1311963"
inkscape:cx="15.733941"
inkscape:cy="70.802735"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<path
id="polygon6"
style="fill:#0300ff;stroke:#0300ff;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1.242,1.834423 53.328,25.562 -1.711,3.859 -51.617,-24.539 z" /><path
style="fill:#000000;fill-opacity:1"
d="M 35.226,58.209423 H 57.82 v 20.164 c 0,3.297 2.656,5.953 5.953,5.953 v 0 c 3.297,0 5.984,-2.656 5.984,-5.953 v -20.164 c 1.781,0 1.953,-1.172 1.953,-2.547 v -15.032 l -18.874,-9.234 H 12.718 l -5.18,-2.93 c -1.656,-0.953 -3.766,-0.367 -4.711,1.297 v 0 c -0.938,1.656 -0.367,3.758 1.297,4.711 l 8.18,4.688 v 39.211 c 0,3.297 2.672,5.953 5.969,5.953 v 0 c 3.297,0 5.969,-2.656 5.969,-5.953 l -0.016,-15.36 10.82,18.266 c 1.68,2.828 5.336,3.758 8.195,2.07 v 0 c 2.805,-1.68 3.734,-5.367 2.047,-8.203 z"
id="path8" /><polygon
style="fill:#000000;fill-opacity:1"
points="109.445,42.488 73.539,25.379 73.539,48.52 90.531,56.605 90.531,53.426 104.367,53.402 "
id="polygon10"
transform="translate(-18.844,-21.224577)" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: NPS Graphics, converted by User:ZyMOS
SPDX-License-Identifier: PD

View file

@ -39,6 +39,26 @@
"https://www.onlinewebfonts.com/icon/464488" "https://www.onlinewebfonts.com/icon/464488"
] ]
}, },
{
"path": "dogs_allowed.svg",
"license": "PUBLIC-DOMAIN",
"authors": [
"OpenClipArt"
],
"sources": [
"https://freesvg.org/no-dogs-round-sign-vector-graphics"
]
},
{
"path": "dogs_leashed.svg",
"license": "PUBLIC-DOMAIN",
"authors": [
" \tNPS Graphics, converted by User:ZyMOS"
],
"sources": [
"https://commons.wikimedia.org/wiki/File:Pictograms-nps-pets_on_leash-2.svg"
]
},
{ {
"path": "nfc_card.svg", "path": "nfc_card.svg",
"license": "CC0-1.0", "license": "CC0-1.0",
@ -49,6 +69,16 @@
"https://wens.be/free-antwerpenize-bicycle-font" "https://wens.be/free-antwerpenize-bicycle-font"
] ]
}, },
{
"path": "no_dogs.svg",
"license": "Public Domain",
"authors": [
"OpenClipArt"
],
"sources": [
"https://freesvg.org/no-dogs-round-sign-vector-graphics"
]
},
{ {
"path": "no_smoking.svg", "path": "no_smoking.svg",
"license": "CC0-1.0", "license": "CC0-1.0",

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg3125"
sodipodi:docname="no_dogs.svg"
viewBox="0 0 430.55 430.55"
sodipodi:version="0.32"
version="1.0"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
width="430.54999"
height="430.54999"
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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs13" />
<sodipodi:namedview
id="base"
bordercolor="#666666"
inkscape:pageshadow="2"
inkscape:window-width="1920"
pagecolor="#ffffff"
inkscape:zoom="1.1508686"
inkscape:window-x="0"
showgrid="false"
borderopacity="1.0"
inkscape:current-layer="svg3125"
inkscape:cx="67.774897"
inkscape:cy="247.20459"
inkscape:window-y="0"
inkscape:window-height="995"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:window-maximized="1" />
<path
id="path3137"
style="fill:#000000;stroke:#ffffff;stroke-width:1.0489"
d="M 248.70672,267.79518 H 147.68191 v 59.85723 c 0,32.84648 -49.85001,32.84648 -49.85001,0 V 196.95628 H 75.408708 c -32.069111,0 -32.35378,-44.3099 -0.755469,-44.3099 h 40.105551 v 0.0547 l 126.46989,-0.0438 76.74032,-76.740324 v 39.087304 l 53.1018,53.10181 c 12.36123,12.83202 5.24449,28.49979 -6.14229,28.49979 h -66.6674 l -0.22993,131.0465 c 0,32.84648 -49.3792,32.84648 -49.3792,0 v -59.85723" />
<path
id="path3141"
style="fill:none;stroke:#ffffff;stroke-width:54;stroke-miterlimit:4;stroke-dasharray:none"
d="M 359,356.71 75.76,73.47" />
<g
id="g3143"
transform="translate(-236.8,-236.8)"
style="stroke-width:40;stroke-miterlimit:4;stroke-dasharray:none">
<path
id="path3145"
style="fill:none;stroke:#ff0000;stroke-width:40;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 452.08,647.35 c -107.85,0 -195.28,-87.43 -195.28,-195.27 0,-107.85 87.43,-195.28 195.28,-195.28 107.84,0 195.27,87.43 195.27,195.28 0,107.84 -87.43,195.27 -195.27,195.27 z" />
</g>
<path
id="path3149"
style="fill:none;stroke:#ff0000;stroke-width:40;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 359.28,356.93 75.41,73.12" />
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work>
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
<dc:publisher>
<cc:Agent
rdf:about="http://openclipart.org/">
<dc:title>Openclipart</dc:title>
</cc:Agent>
</dc:publisher>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/publicdomain/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: OpenClipArt
SPDX-License-Identifier: Public Domain

View file

@ -130,7 +130,7 @@
"id": "reviews", "id": "reviews",
"description": "Shows the reviews module (including the possibility to leave a review)", "description": "Shows the reviews module (including the possibility to leave a review)",
"render": { "render": {
"*": "{reviews()}" "*": "{create_review()}{list_reviews()}"
} }
}, },
{ {
@ -487,6 +487,7 @@
"mappings": [ "mappings": [
{ {
"if": "dog=yes", "if": "dog=yes",
"icon": "./assets/layers/questions/dogs_allowed.svg",
"then": { "then": {
"en": "Dogs are allowed", "en": "Dogs are allowed",
"nl": "honden zijn toegelaten", "nl": "honden zijn toegelaten",
@ -515,6 +516,7 @@
}, },
{ {
"if": "dog=no", "if": "dog=no",
"icon": "./assets/layers/questions/no_dogs.svg",
"then": { "then": {
"en": "Dogs are <b>not</b> allowed", "en": "Dogs are <b>not</b> allowed",
"nl": "honden zijn <b>niet</b> toegelaten", "nl": "honden zijn <b>niet</b> toegelaten",
@ -542,6 +544,7 @@
}, },
{ {
"if": "dog=leashed", "if": "dog=leashed",
"icon": "./assets/layers/questions/dogs_leashed.svg",
"then": { "then": {
"en": "Dogs are allowed, but they have to be leashed", "en": "Dogs are allowed, but they have to be leashed",
"nl": "honden zijn <b>enkel aan de leiband</b> welkom", "nl": "honden zijn <b>enkel aan de leiband</b> welkom",
@ -568,6 +571,8 @@
}, },
{ {
"if": "dog=unleashed", "if": "dog=unleashed",
"icon": "./assets/layers/questions/dogs_allowed.svg",
"then": { "then": {
"en": "Dogs are allowed and can run around freely", "en": "Dogs are allowed and can run around freely",
"nl": "honden zijn welkom en mogen vrij rondlopen", "nl": "honden zijn welkom en mogen vrij rondlopen",
@ -1619,8 +1624,9 @@
}, },
{ {
"id": "multilevels", "id": "multilevels",
"builtin": "level", "builtin": "single_level",
"override": { "override": {
"=labels": [],
"question": { "question": {
"en": "What levels does this elevator go to?", "en": "What levels does this elevator go to?",
"de": "Auf welchen Geschossen hält dieser Aufzug?", "de": "Auf welchen Geschossen hält dieser Aufzug?",
@ -1657,7 +1663,22 @@
} }
}, },
{ {
"id": "level", "id": "repeated",
"labels": [
"level"
],
"condition": "repeat_on~*",
"render": {
"en": "Multiple, identical objects can be found on floors {repeat_on}.",
"nl": "Er zijn verschillende, identieke objecten op verdiepingen {repeat_on}."
}
},
{
"id": "single_level",
"labels": [
"level"
],
"condition": "repeat_on=",
"question": { "question": {
"nl": "Op welke verdieping bevindt dit punt zich?", "nl": "Op welke verdieping bevindt dit punt zich?",
"en": "On what level is this feature located?", "en": "On what level is this feature located?",

View file

@ -348,6 +348,11 @@
"override": { "override": {
"render": "./assets/layers/id_presets/maki-shop.svg", "render": "./assets/layers/id_presets/maki-shop.svg",
"+mappings": [ "+mappings": [
{
"#": "Layer icon rendering",
"if": "id=",
"then": "circle:white;./assets/layers/id_presets/maki-shop.svg"
},
{ {
"if": { "if": {
"or": [ "or": [

View file

@ -723,6 +723,16 @@
"authors": [], "authors": [],
"sources": [] "sources": []
}, },
{
"path": "mangrove_logo.svg",
"license": "LOGO",
"authors": [
"Mangrove.reviews"
],
"sources": [
"https://mangrove.reviews/"
]
},
{ {
"path": "mapcomplete_logo.svg", "path": "mapcomplete_logo.svg",
"license": "LOGO AND CC-BY-SA-4.0", "license": "LOGO AND CC-BY-SA-4.0",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,6 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="374px" height="374px" viewBox="0 0 374 374" version="1.1"> <svg
<g id="surface1"> width="374px"
<path style=" stroke:none;fill-rule:nonzero;fill:#000000;fill-opacity:1;" class="selectable" d="M 197.824219 10.429688 C 193.816406 11.191406 188.988281 15.195312 184.570312 21.386719 C 178.515625 29.949219 173.335938 40.234375 161.109375 68.078125 C 150.957031 91.164062 146.890625 99.34375 141.597656 107.144531 C 135.421875 116.230469 131.152344 118.042969 115.910156 118.074219 C 107.046875 118.074219 99.144531 117.429688 77.4375 114.976562 C 68.371094 113.925781 57.953125 112.84375 50.230469 112.140625 C 44.644531 111.644531 27.0625 111.644531 23.753906 112.140625 C 13.253906 113.777344 8.394531 117.226562 8.015625 123.304688 C 7.8125 127.042969 9.273438 131.046875 12.726562 136.21875 C 18.488281 144.808594 27.761719 154.101562 51.253906 174.875 C 79.195312 199.5625 87.738281 209.175781 88.996094 217.300781 C 90.078125 224.136719 86.214844 236.46875 75.742188 259.902344 C 65.09375 283.742188 63.164062 288.039062 61.40625 292.332031 C 54.035156 310.1875 51.109375 321.492188 52.105469 328.449219 C 53.011719 334.933594 56.347656 338.179688 62.546875 338.558594 C 71.644531 339.171875 85.015625 333.621094 112.253906 317.988281 C 134.164062 305.394531 138.144531 303.144531 143 300.574219 C 156.195312 293.5625 164.003906 290.699219 170.03125 290.609375 C 173.570312 290.582031 174.742188 290.902344 179.101562 293.0625 C 186.855469 296.949219 195.894531 304.167969 217.132812 323.453125 C 246.5625 350.214844 258.238281 358.6875 268.386719 360.734375 C 271.371094 361.347656 273.449219 361.113281 275.847656 359.945312 C 278.863281 358.484375 280.527344 356 281.816406 351.0625 C 282.429688 348.695312 282.488281 347.820312 282.488281 341.859375 C 282.488281 338.207031 282.34375 334.058594 282.136719 332.507812 C 280.644531 321.433594 279.007812 312.816406 274.738281 293.503906 C 268.039062 263.289062 266.574219 254.730469 266.574219 245.378906 C 266.574219 240.644531 267.042969 237.78125 268.183594 235.269531 C 271.136719 228.8125 281.699219 220.953125 305.484375 207.453125 C 324.820312 196.496094 329.796875 193.632812 334.886719 190.566406 C 356.503906 177.53125 366.042969 168.328125 366.042969 160.558594 C 366.042969 156.027344 363.234375 152.582031 357.089844 149.511719 C 348.3125 145.128906 335.207031 142.617188 305.484375 139.519531 C 274.707031 136.363281 266.457031 135.195312 257.59375 132.945312 C 246.269531 130.023438 242.554688 127.074219 238.636719 117.898438 C 235.210938 109.832031 232.460938 99.519531 227.195312 74.59375 C 223.246094 55.777344 220.996094 46.164062 218.742188 38.453125 C 215.378906 26.824219 211.722656 18.933594 207.625 14.375 C 204.875 11.308594 201.160156 9.816406 197.824219 10.429688 Z M 197.824219 10.429688 "/> height="374px"
</g> viewBox="0 0 374 374"
version="1.1"
id="svg5"
sodipodi:docname="star.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
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="defs9" />
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="1.0896688"
inkscape:cx="229.42751"
inkscape:cy="291.37294"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg5" />
<path
sodipodi:type="star"
style="fill:#000000;stroke:#000000;stroke-width:5.7519;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.57519, 0.57519;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:1"
id="path891"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="-20.180035"
sodipodi:cy="177.65573"
sodipodi:r1="44.653515"
sodipodi:r2="111.63379"
sodipodi:arg1="0.74973014"
sodipodi:arg2="1.3780487"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 12.500658,208.08448 -11.2965304,79.13776 -40.2247326,-69.08232 -78.755305,13.71127 53.27107,-59.6036 -37.37692,-70.66373 73.148059,32.24527 55.655095,-57.383864 -8.06308,79.532284 71.773666,35.19855 z"
inkscape:transform-center-x="10.914421"
inkscape:transform-center-y="5.8173361"
transform="matrix(-0.19478496,-1.6320507,1.6320507,-0.19478496,-107.17071,207.1864)" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

@ -1,6 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="374px" height="374px" viewBox="0 0 374 374" version="1.1"> <svg
<g id="surface1"> width="374px"
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 197.824219 10.429688 C 193.816406 11.191406 188.988281 15.195312 184.570312 21.386719 C 178.515625 29.949219 173.335938 40.234375 161.109375 68.078125 C 150.957031 91.164062 146.890625 99.34375 141.597656 107.144531 C 135.421875 116.230469 131.152344 118.042969 115.910156 118.074219 C 107.046875 118.074219 99.144531 117.429688 77.4375 114.976562 C 68.371094 113.925781 57.953125 112.84375 50.230469 112.140625 C 44.644531 111.644531 27.0625 111.644531 23.753906 112.140625 C 13.253906 113.777344 8.394531 117.226562 8.015625 123.304688 C 7.8125 127.042969 9.273438 131.046875 12.726562 136.21875 C 18.488281 144.808594 27.761719 154.101562 51.253906 174.875 C 79.195312 199.5625 87.738281 209.175781 88.996094 217.300781 C 90.078125 224.136719 86.214844 236.46875 75.742188 259.902344 C 65.09375 283.742188 63.164062 288.039062 61.40625 292.332031 C 54.035156 310.1875 51.109375 321.492188 52.105469 328.449219 C 53.011719 334.933594 56.347656 338.179688 62.546875 338.558594 C 71.644531 339.171875 85.015625 333.621094 112.253906 317.988281 C 134.164062 305.394531 138.144531 303.144531 143 300.574219 C 156.195312 293.5625 164.003906 290.699219 170.03125 290.609375 Z M 197.824219 10.429688 "/> height="374px"
</g> viewBox="0 0 374 374"
</svg> version="1.1"
id="svg5"
sodipodi:docname="star_half.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
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="defs9" />
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="0.72869754"
inkscape:cx="148.20964"
inkscape:cy="-18.526205"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg5" />
<path
sodipodi:type="star"
style="fill:none;stroke:#000000;stroke-width:5.7519;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.57519, 0.57519;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:1"
id="path1499"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="-20.180035"
sodipodi:cy="177.65573"
sodipodi:r1="44.653515"
sodipodi:r2="111.63379"
sodipodi:arg1="0.74973014"
sodipodi:arg2="1.3780487"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 12.500658,208.08448 -11.2965304,79.13776 -40.2247326,-69.08232 -78.755305,13.71127 53.27107,-59.6036 -37.37692,-70.66373 73.148059,32.24527 55.655095,-57.383864 -8.06308,79.532284 71.773666,35.19855 z"
inkscape:transform-center-x="10.914421"
inkscape:transform-center-y="5.8173361"
transform="matrix(-0.19478496,-1.6320507,1.6320507,-0.19478496,-107.17071,207.1864)" />
<path
id="rect1847"
style="stroke-width:10;stroke-linecap:round;stroke-dasharray:1, 1"
d="M 187.18463,22.032185 144.39557,144.09468 13.024475,146.42672 117.51862,226.08101 79.141663,351.74312 187.18463,276.97945 Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

@ -1,6 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="374px" height="374px" viewBox="0 0 374 374" version="1.1"> <svg
<g id="surface1"> width="374px"
<path style="fill:none;stroke-width:107.38591;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:4;" d="M 674.316761 62.954545 C 661.244886 65.427807 645.489205 78.502674 631.082102 98.743316 C 611.320739 126.71123 594.430114 160.307487 554.52017 251.283422 C 521.406534 326.684492 508.134375 353.42246 490.856534 378.903743 C 470.721307 408.582888 456.781534 414.505348 407.044318 414.59893 C 378.123295 414.59893 352.353409 412.5 281.532955 404.47861 C 251.930966 401.042781 217.949432 397.513369 192.753693 395.227273 C 174.527841 393.596257 117.153125 393.596257 106.364489 395.227273 C 72.102557 400.574866 56.253409 411.831551 55.011648 431.684492 C 54.344034 443.903743 59.124148 456.97861 70.380114 473.877005 C 89.193466 501.938503 119.449716 532.299465 196.091761 600.160428 C 287.26108 680.828877 315.127273 712.23262 319.22642 738.770053 C 322.764773 761.096257 310.160227 801.377005 275.991761 877.941176 C 241.249148 955.828877 234.946875 969.852941 229.21875 983.890374 C 205.157955 1042.219251 195.624432 1079.157754 198.869034 1101.871658 C 201.819886 1123.061497 212.701989 1133.663102 232.944034 1134.893048 C 262.626136 1136.898396 306.248011 1118.770053 395.120739 1067.700535 C 466.608807 1026.564171 479.600568 1019.21123 495.436364 1010.802139 C 538.484091 987.90107 563.97358 978.542781 583.641477 978.262032 C 595.191193 978.168449 599.009943 979.21123 613.230114 986.283422 C 638.519318 998.970588 668.027841 1022.553476 737.326136 1085.548128 C 833.34233 1172.981283 871.436364 1200.668449 904.55 1207.352941 C 914.297159 1209.358289 921.066761 1208.596257 928.891193 1204.772727 C 938.731818 1200 944.166193 1191.885027 948.372159 1175.748663 C 950.375 1168.02139 950.561932 1165.160428 950.561932 1145.681818 C 950.561932 1133.756684 950.094602 1120.200535 949.413636 1115.13369 C 944.553409 1078.957219 939.2125 1050.802139 925.272727 987.713904 C 903.415057 889.010695 898.634943 861.042781 898.634943 830.494652 C 898.634943 815.026738 900.170455 805.681818 903.882386 797.473262 C 913.522727 776.377005 947.984943 750.695187 1025.588352 706.590909 C 1088.691193 670.802139 1104.914205 661.44385 1121.524432 651.417112 C 1192.064489 608.850267 1223.188636 578.783422 1223.188636 553.395722 C 1223.188636 538.596257 1214.015625 527.339572 1193.973864 517.312834 C 1165.333239 502.994652 1122.579261 494.786096 1025.588352 484.665775 C 925.179261 474.358289 898.26108 470.534759 869.340057 463.181818 C 832.394318 453.636364 820.270455 443.997326 807.478977 414.024064 C 796.316477 387.687166 787.34375 353.983957 770.159375 272.566845 C 757.274432 211.096257 749.91733 179.692513 742.57358 154.491979 C 731.598011 116.497326 719.66108 90.721925 706.295455 75.828877 C 697.322727 65.815508 685.198864 60.949198 674.316761 62.954545 Z M 674.316761 62.954545 " transform="matrix(0.292553,0,0,0.292188,0.0585106,0)"/> height="374px"
</g> viewBox="0 0 374 374"
</svg> version="1.1"
id="svg5"
sodipodi:docname="star_outline.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
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="defs9" />
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="1.0896688"
inkscape:cx="241.35774"
inkscape:cy="291.37294"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg5" />
<path
sodipodi:type="star"
style="fill:none;stroke:#000000;stroke-width:5.7519;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.57519, 0.57519;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:1"
id="path891"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="-20.180035"
sodipodi:cy="177.65573"
sodipodi:r1="44.653515"
sodipodi:r2="111.63379"
sodipodi:arg1="0.74973014"
sodipodi:arg2="1.3780487"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 12.500658,208.08448 -11.2965304,79.13776 -40.2247326,-69.08232 -78.755305,13.71127 53.27107,-59.6036 -37.37692,-70.66373 73.148059,32.24527 55.655095,-57.383864 -8.06308,79.532284 71.773666,35.19855 z"
inkscape:transform-center-x="10.914421"
inkscape:transform-center-y="5.8173361"
transform="matrix(-0.19478496,-1.6320507,1.6320507,-0.19478496,-107.17071,207.1864)" />
</svg>

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

@ -534,10 +534,7 @@
"attribution": "Les ressenyes funcionen gràcies a <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> i estan disponibles sota <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.", "attribution": "Les ressenyes funcionen gràcies a <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> i estan disponibles sota <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Tinc alguna filiació amb aquest objecte</span><br/><span class='subtle'>Marca-ho si n'ets cap, creador, treballador, …</span>", "i_am_affiliated": "<span>Tinc alguna filiació amb aquest objecte</span><br/><span class='subtle'>Marca-ho si n'ets cap, creador, treballador, …</span>",
"name_required": "És requerit un nom per mostrar i crear revisions", "name_required": "És requerit un nom per mostrar i crear revisions",
"no_rating": "Doneu una puntuació abans d'enviar…",
"no_reviews_yet": "No hi ha revisions encara. Sigues el primer a escriure'n una i ajuda al negoci i a les dades lliures!", "no_reviews_yet": "No hi ha revisions encara. Sigues el primer a escriure'n una i ajuda al negoci i a les dades lliures!",
"plz_login": "Entra per deixar una revisió",
"posting_as": "Enviat com",
"save": "Desar", "save": "Desar",
"saved": "<span class=\"thanks\">Revisió compartida. Gràcies per compartir!</span>", "saved": "<span class=\"thanks\">Revisió compartida. Gràcies per compartir!</span>",
"saving_review": "Desant…", "saving_review": "Desant…",

View file

@ -448,10 +448,7 @@
"attribution": "Recenze jsou poskytovány službou <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> a jsou k dispozici pod licencí <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>.", "attribution": "Recenze jsou poskytovány službou <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> a jsou k dispozici pod licencí <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Jsem spojen/a s tímto objektem</span><br/><span class='subtle'>Zaškrtněte, pokud jste vlastníkem, tvůrcem, zaměstnancem, …</span>", "i_am_affiliated": "<span>Jsem spojen/a s tímto objektem</span><br/><span class='subtle'>Zaškrtněte, pokud jste vlastníkem, tvůrcem, zaměstnancem, …</span>",
"name_required": "Pro zobrazení a vytváření recenzí je vyžadováno jméno", "name_required": "Pro zobrazení a vytváření recenzí je vyžadováno jméno",
"no_rating": "Před odesláním udělte hodnocení…",
"no_reviews_yet": "Zatím zde nejsou žádné recenze. Buďte první, kdo ji napíše, a pomozte otevřít data a podnikání!", "no_reviews_yet": "Zatím zde nejsou žádné recenze. Buďte první, kdo ji napíše, a pomozte otevřít data a podnikání!",
"plz_login": "Přihlaste se a zanechte recenzi",
"posting_as": "Přihlášeni jako",
"save": "Uložit", "save": "Uložit",
"saved": "<span class='thanks'>Recenze uložena. Díky za sdílení!</span>", "saved": "<span class='thanks'>Recenze uložena. Díky za sdílení!</span>",
"saving_review": "Ukládání…", "saving_review": "Ukládání…",

View file

@ -383,10 +383,7 @@
"attribution": "Anmeldelserne er baseret på <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> og er tilgængelige under <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.", "attribution": "Anmeldelserne er baseret på <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> og er tilgængelige under <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Jeg er tilknyttet dette objekt</span><br><span class=\"subtle\">Tjek, om du er ejer, skaber, ansat, ...</span>", "i_am_affiliated": "<span>Jeg er tilknyttet dette objekt</span><br><span class=\"subtle\">Tjek, om du er ejer, skaber, ansat, ...</span>",
"name_required": "Der kræves et navn for at vise og oprette anmeldelser", "name_required": "Der kræves et navn for at vise og oprette anmeldelser",
"no_rating": "Ingen vurdering givet",
"no_reviews_yet": "Der er ingen anmeldelser endnu. Vær den første til at skrive en og hjælpe åbne data og forretningen!", "no_reviews_yet": "Der er ingen anmeldelser endnu. Vær den første til at skrive en og hjælpe åbne data og forretningen!",
"plz_login": "Log ind for at give en anmeldelse",
"posting_as": "Anmelder som",
"saved": "<span class=\"thanks\">Anmeldelse gemt. Tak for at bidrage!</span>", "saved": "<span class=\"thanks\">Anmeldelse gemt. Tak for at bidrage!</span>",
"saving_review": "Gemmer…", "saving_review": "Gemmer…",
"title": "{count} Anmeldelser", "title": "{count} Anmeldelser",

View file

@ -536,10 +536,7 @@
"attribution": "Rezensionen werden bereitgestellt von <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> und sind unter <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a> verfügbar.", "attribution": "Rezensionen werden bereitgestellt von <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> und sind unter <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a> verfügbar.",
"i_am_affiliated": "<span>Ich bin an diesem Objekt beteiligt</span><br/><span class='subtle'>Auswählen, wenn Sie Eigentümer, Ersteller, Angestellter … sind</span>", "i_am_affiliated": "<span>Ich bin an diesem Objekt beteiligt</span><br/><span class='subtle'>Auswählen, wenn Sie Eigentümer, Ersteller, Angestellter … sind</span>",
"name_required": "Der Name des Objekts ist erforderlich, um Bewertungen zu erstellen und anzuzeigen", "name_required": "Der Name des Objekts ist erforderlich, um Bewertungen zu erstellen und anzuzeigen",
"no_rating": "Vor dem Absenden eine Bewertung abgeben…",
"no_reviews_yet": "Es gibt noch keine Bewertungen. Hilf mit der ersten Bewertung dem Geschäft und der Open Data Bewegung!", "no_reviews_yet": "Es gibt noch keine Bewertungen. Hilf mit der ersten Bewertung dem Geschäft und der Open Data Bewegung!",
"plz_login": "Anmelden, um eine Bewertung abzugeben",
"posting_as": "Veröffentlichen als",
"save": "Speichern", "save": "Speichern",
"saved": "<span class=\"thanks\">Bewertung gespeichert. Danke fürs Teilen!</span>", "saved": "<span class=\"thanks\">Bewertung gespeichert. Danke fürs Teilen!</span>",
"saving_review": "Speichern…", "saving_review": "Speichern…",

View file

@ -197,6 +197,7 @@
"example": "Example", "example": "Example",
"examples": "Examples", "examples": "Examples",
"fewChangesBefore": "Please, answer a few questions of existing features before adding a new feature.", "fewChangesBefore": "Please, answer a few questions of existing features before adding a new feature.",
"geopermissionDenied": "Using the geolocation was denied",
"getStartedLogin": "Log in with OpenStreetMap to get started", "getStartedLogin": "Log in with OpenStreetMap to get started",
"getStartedNewAccount": " or <a href='https://www.openstreetmap.org/user/new' target='_blank'>create a new account</a>", "getStartedNewAccount": " or <a href='https://www.openstreetmap.org/user/new' target='_blank'>create a new account</a>",
"goToInbox": "Open inbox", "goToInbox": "Open inbox",
@ -556,14 +557,16 @@
"reviews": { "reviews": {
"affiliated_reviewer_warning": "(Affiliated review)", "affiliated_reviewer_warning": "(Affiliated review)",
"attribution": "Reviews are powered by <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> and are available under <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>.", "attribution": "Reviews are powered by <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> and are available under <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>.",
"i_am_affiliated": "<span>I am affiliated with this object</span><br/><span class='subtle'>Check if you are an owner, creator, employee, …</span>", "i_am_affiliated": "I am affiliated with this object",
"i_am_affiliated_explanation": "Check if you are an owner, creator, employee, …",
"name_required": "A name is required in order to display and create reviews", "name_required": "A name is required in order to display and create reviews",
"no_rating": "Give a rating before submitting…",
"no_reviews_yet": "There are no reviews yet. Be the first to write one and help open data and the business!", "no_reviews_yet": "There are no reviews yet. Be the first to write one and help open data and the business!",
"plz_login": "Log in to leave a review", "question": "How would you rate {title()}?",
"posting_as": "Posting as", "question_opinion": "How was your experience?",
"reviewing_as": "Reviewing as {nickname}",
"reviewing_as_anonymous": "Reviewing as anonymous",
"save": "Save", "save": "Save",
"saved": "<span class='thanks'>Review saved. Thanks for sharing!</span>", "saved": "Review saved. Thanks for sharing!",
"saving_review": "Saving…", "saving_review": "Saving…",
"title": "{count} reviews", "title": "{count} reviews",
"title_singular": "One review", "title_singular": "One review",

View file

@ -414,10 +414,7 @@
"reviews": { "reviews": {
"affiliated_reviewer_warning": "(Revisión afiliada)", "affiliated_reviewer_warning": "(Revisión afiliada)",
"name_required": "Se requiere un nombre para mostrar y crear comentarios", "name_required": "Se requiere un nombre para mostrar y crear comentarios",
"no_rating": "Da una calificación antes de enviar…",
"no_reviews_yet": "Aún no hay reseñas. ¡Sé el primero en escribir una y ayuda a los datos abiertos y a los negocios!", "no_reviews_yet": "Aún no hay reseñas. ¡Sé el primero en escribir una y ayuda a los datos abiertos y a los negocios!",
"plz_login": "Inicia sesión para dejar una reseña",
"posting_as": "Publicación como",
"saved": "<span class=\"thanks\">Reseña guardada. ¡Gracias por compartir!</span>", "saved": "<span class=\"thanks\">Reseña guardada. ¡Gracias por compartir!</span>",
"saving_review": "Guardando…", "saving_review": "Guardando…",
"title": "{count} comentarios", "title": "{count} comentarios",

View file

@ -428,10 +428,7 @@
"attribution": "Les avis sont fournis par <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> et sont disponibles sous licence <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.", "attribution": "Les avis sont fournis par <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> et sont disponibles sous licence <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Je suis affilié à cet objet</span><br><span class=\"subtle\">Cochez si vous en êtes le propriétaire, créateur, employé, …</span>", "i_am_affiliated": "<span>Je suis affilié à cet objet</span><br><span class=\"subtle\">Cochez si vous en êtes le propriétaire, créateur, employé, …</span>",
"name_required": "Un nom est requis pour afficher et créer des avis", "name_required": "Un nom est requis pour afficher et créer des avis",
"no_rating": "Aucun score donné",
"no_reviews_yet": "Il n'y a pas encore d'avis. Soyez le premier à en écrire un et aidez le lieu et les données ouvertes !", "no_reviews_yet": "Il n'y a pas encore d'avis. Soyez le premier à en écrire un et aidez le lieu et les données ouvertes !",
"plz_login": "Connectez vous pour laisser un avis",
"posting_as": "Envoi en tant que",
"saved": "<span class=\"thanks\">Avis enregistré. Merci du partage !</span>", "saved": "<span class=\"thanks\">Avis enregistré. Merci du partage !</span>",
"saving_review": "Enregistrement…", "saving_review": "Enregistrement…",
"title": "{count} avis", "title": "{count} avis",

View file

@ -163,10 +163,7 @@
"reviews": { "reviews": {
"affiliated_reviewer_warning": "(Recensión de afiliado)", "affiliated_reviewer_warning": "(Recensión de afiliado)",
"name_required": "Requírese un nome para amosar e crear recensións", "name_required": "Requírese un nome para amosar e crear recensións",
"no_rating": "Sen puntuacións",
"no_reviews_yet": "Non hai recensións aínda. Se o primeiro en escribir unha e axuda ao negocio e aos datos libres!", "no_reviews_yet": "Non hai recensións aínda. Se o primeiro en escribir unha e axuda ao negocio e aos datos libres!",
"plz_login": "Inicia sesión para deixar unha recensión",
"posting_as": "Publicar como",
"saved": "<span class=\"thanks\">Recensión compartida. Grazas por compartir!</span>", "saved": "<span class=\"thanks\">Recensión compartida. Grazas por compartir!</span>",
"saving_review": "Gardando…", "saving_review": "Gardando…",
"title": "{count} recensións", "title": "{count} recensións",

View file

@ -303,10 +303,7 @@
"attribution": "A véleményeket <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> tárolja, és a <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0 licenc</a> szerint érhetők el.", "attribution": "A véleményeket <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> tárolja, és a <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0 licenc</a> szerint érhetők el.",
"i_am_affiliated": "<span>Kapcsolatban állok ezzel a létesítménnyel</span><br><span class=\"subtle\">Ellenőrizd, hogy tulajdonos, alkotó, alkalmazott vagy hasonló vagy-e.</span>", "i_am_affiliated": "<span>Kapcsolatban állok ezzel a létesítménnyel</span><br><span class=\"subtle\">Ellenőrizd, hogy tulajdonos, alkotó, alkalmazott vagy hasonló vagy-e.</span>",
"name_required": "Vélemények megjelenítéséhez és létrehozásához névre van szükség", "name_required": "Vélemények megjelenítéséhez és létrehozásához névre van szükség",
"no_rating": "Még nem kapott értékelést",
"no_reviews_yet": "Még nincs vélemény. Légy Te az első, aki ír, és ezzel támogasd a nyílt adatokat és az üzletet!", "no_reviews_yet": "Még nincs vélemény. Légy Te az első, aki ír, és ezzel támogasd a nyílt adatokat és az üzletet!",
"plz_login": "Értékelés írásához jelentkezz be",
"posting_as": "Közzétéve mint",
"saved": "<span class=\"thanks\">Vélemény elmentve. Köszönjük a megosztást!</span>", "saved": "<span class=\"thanks\">Vélemény elmentve. Köszönjük a megosztást!</span>",
"saving_review": "Mentés…", "saving_review": "Mentés…",
"title": "{count} vélemény", "title": "{count} vélemény",

View file

@ -162,9 +162,6 @@
}, },
"reviews": { "reviews": {
"attribution": "Ulasan didukung oleh <a href=\"https://mangrove.reviews/\" target=\"_blank\"> Mangrove Reviews</a> dan tersedia di bawah <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.", "attribution": "Ulasan didukung oleh <a href=\"https://mangrove.reviews/\" target=\"_blank\"> Mangrove Reviews</a> dan tersedia di bawah <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"no_rating": "Tidak ada peringkat yang diberikan",
"plz_login": "Masuk untuk meninggalkan ulasan",
"posting_as": "Posting sebagai",
"saved": "<span class=\"thanks\"> Ulasan disimpan. Terima kasih sudah berbagi! </span>", "saved": "<span class=\"thanks\"> Ulasan disimpan. Terima kasih sudah berbagi! </span>",
"saving_review": "Menyimpan…", "saving_review": "Menyimpan…",
"title": "{count} ulasan", "title": "{count} ulasan",

View file

@ -307,10 +307,7 @@
"attribution": "Le recensioni sono fornite da <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> e sono disponibili con licenza <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.", "attribution": "Le recensioni sono fornite da <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> e sono disponibili con licenza <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Sono associato con questo oggetto</span><br><span class=\"subtle\">Spunta se sei il proprietario, creatore, dipendente, etc.</span>", "i_am_affiliated": "<span>Sono associato con questo oggetto</span><br><span class=\"subtle\">Spunta se sei il proprietario, creatore, dipendente, etc.</span>",
"name_required": "È richiesto un nome per poter mostrare e creare recensioni", "name_required": "È richiesto un nome per poter mostrare e creare recensioni",
"no_rating": "Nessun voto ricevuto",
"no_reviews_yet": "Non ci sono ancora recensioni. Sii il primo a scriverne una aiutando così i dati liberi e lattività!", "no_reviews_yet": "Non ci sono ancora recensioni. Sii il primo a scriverne una aiutando così i dati liberi e lattività!",
"plz_login": "Accedi per lasciare una recensione",
"posting_as": "Pubblica come",
"saved": "<span class=\"thanks\">Recensione salvata. Grazie per averla condivisa!</span>", "saved": "<span class=\"thanks\">Recensione salvata. Grazie per averla condivisa!</span>",
"saving_review": "Salvataggio…", "saving_review": "Salvataggio…",
"title": "{count} recensioni", "title": "{count} recensioni",

View file

@ -165,10 +165,7 @@
"attribution": "レビューは、<a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> and are available under <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>で公開されます。", "attribution": "レビューは、<a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> and are available under <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>で公開されます。",
"i_am_affiliated": "<span>わたしは、この対象物の関係者です</span><br><span class=\"subtle\">所有者、作成者、従業員などの有無を確認します</span>", "i_am_affiliated": "<span>わたしは、この対象物の関係者です</span><br><span class=\"subtle\">所有者、作成者、従業員などの有無を確認します</span>",
"name_required": "レビューを表示および作成するには名前が必要です", "name_required": "レビューを表示および作成するには名前が必要です",
"no_rating": "評価が与えられていません",
"no_reviews_yet": "まだレビューはありません。最初に書き込みを行い、データとビジネスのオープン化を支援しましょう!", "no_reviews_yet": "まだレビューはありません。最初に書き込みを行い、データとビジネスのオープン化を支援しましょう!",
"plz_login": "ログインしてレビューを終了する",
"posting_as": "としての投稿",
"saved": "<span class=\"thanks\">レビューが保存されました。共有ありがとう!</span>", "saved": "<span class=\"thanks\">レビューが保存されました。共有ありがとう!</span>",
"saving_review": "保存中…", "saving_review": "保存中…",
"title": "{count}個のレビュー", "title": "{count}個のレビュー",

View file

@ -6410,7 +6410,7 @@
} }
} }
}, },
"level": { "single_level": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Situat a planta subterrani" "then": "Situat a planta subterrani"
@ -6543,6 +6543,27 @@
}, },
"question": "Aquest servei té endolls elèctrics, disponibles pels clients quan hi són dins?" "question": "Aquest servei té endolls elèctrics, disponibles pels clients quan hi són dins?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Situat a planta subterrani"
},
"1": {
"then": "Situat a planta zero"
},
"2": {
"then": "Situat a la planta zero"
},
"3": {
"then": "Situat a primera planta"
},
"4": {
"then": "Localitzat a la planta base"
}
},
"question": "A quina planta està situat aquest element?",
"render": "Situat a la planta {level}"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -1767,27 +1767,6 @@
"question": "Jaký je název sítě pro bezdrátový přístup k internetu?", "question": "Jaký je název sítě pro bezdrátový přístup k internetu?",
"render": "Název sítě je <b>{internet_access:ssid}</b>" "render": "Název sítě je <b>{internet_access:ssid}</b>"
}, },
"level": {
"mappings": {
"0": {
"then": "Nachází se v podzemí"
},
"1": {
"then": "Nachází se v přízemí"
},
"2": {
"then": "Nachází se v přízemí"
},
"3": {
"then": "Nachází se v prvním patře"
},
"4": {
"then": "Nachází se v prvním suterénu"
}
},
"question": "V jaké úrovni se tento prvek nachází?",
"render": "Nachází se v {level}. patře"
},
"luminous_or_lit": { "luminous_or_lit": {
"mappings": { "mappings": {
"0": { "0": {
@ -1897,6 +1876,27 @@
}, },
"question": "Má toto zařízení elektrické zásuvky, které jsou zákazníkům k dispozici, když jsou uvnitř?" "question": "Má toto zařízení elektrické zásuvky, které jsou zákazníkům k dispozici, když jsou uvnitř?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Nachází se v podzemí"
},
"1": {
"then": "Nachází se v přízemí"
},
"2": {
"then": "Nachází se v přízemí"
},
"3": {
"then": "Nachází se v prvním patře"
},
"4": {
"then": "Nachází se v prvním suterénu"
}
},
"question": "V jaké úrovni se tento prvek nachází?",
"render": "Nachází se v {level}. patře"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -2163,27 +2163,6 @@
"question": "Hvad er netværksnavnet for den trådløse internetadgang?", "question": "Hvad er netværksnavnet for den trådløse internetadgang?",
"render": "Netværksnavnet er <b>{internet_access:ssid}</b>" "render": "Netværksnavnet er <b>{internet_access:ssid}</b>"
}, },
"level": {
"mappings": {
"0": {
"then": "Placeret under jorden"
},
"1": {
"then": "Beliggende i stueetagen"
},
"2": {
"then": "Beliggende i stueetagen"
},
"3": {
"then": "Beliggende på første sal"
},
"4": {
"then": "Beliggende på første kælderetage"
}
},
"question": "På hvilket niveau er denne funktion placeret?",
"render": "Beliggende på {level}. etage"
},
"multilevels": { "multilevels": {
"override": { "override": {
"question": "Hvilke niveauer går denne elevator til?", "question": "Hvilke niveauer går denne elevator til?",
@ -2237,6 +2216,27 @@
}, },
"question": "Har denne faciliteter stikkontakter tilgængelige for kunder, når de er inde?" "question": "Har denne faciliteter stikkontakter tilgængelige for kunder, når de er inde?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Placeret under jorden"
},
"1": {
"then": "Beliggende i stueetagen"
},
"2": {
"then": "Beliggende i stueetagen"
},
"3": {
"then": "Beliggende på første sal"
},
"4": {
"then": "Beliggende på første kælderetage"
}
},
"question": "På hvilket niveau er denne funktion placeret?",
"render": "Beliggende på {level}. etage"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -7345,27 +7345,6 @@
} }
} }
}, },
"level": {
"mappings": {
"0": {
"then": "Das Objekt befindet sich unter der Erde"
},
"1": {
"then": "Das Objekt befindet sich im Erdgeschoss"
},
"2": {
"then": "Das Objekt befindet sich im Erdgeschoss"
},
"3": {
"then": "Das Objekt befindet sich im 1. Obergeschoss"
},
"4": {
"then": "Das Objekt befindet sich im 1. Untergeschoss"
}
},
"question": "Auf welcher Ebene befindet sich das Objekt?",
"render": "Das Objekt befindet sich im {level}. Geschoss"
},
"luminous_or_lit": { "luminous_or_lit": {
"mappings": { "mappings": {
"0": { "0": {
@ -7481,6 +7460,27 @@
}, },
"question": "Gibt es hier Steckdosen, an denen Kunden ihre Geräte laden können?" "question": "Gibt es hier Steckdosen, an denen Kunden ihre Geräte laden können?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Das Objekt befindet sich unter der Erde"
},
"1": {
"then": "Das Objekt befindet sich im Erdgeschoss"
},
"2": {
"then": "Das Objekt befindet sich im Erdgeschoss"
},
"3": {
"then": "Das Objekt befindet sich im 1. Obergeschoss"
},
"4": {
"then": "Das Objekt befindet sich im 1. Untergeschoss"
}
},
"question": "Auf welcher Ebene befindet sich das Objekt?",
"render": "Das Objekt befindet sich im {level}. Geschoss"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -7405,27 +7405,6 @@
} }
} }
}, },
"level": {
"mappings": {
"0": {
"then": "Located underground"
},
"1": {
"then": "Located on the ground floor"
},
"2": {
"then": "Located on the ground floor"
},
"3": {
"then": "Located on the first floor"
},
"4": {
"then": "Located on the first basement level"
}
},
"question": "On what level is this feature located?",
"render": "Located on the {level}th floor"
},
"luminous_or_lit": { "luminous_or_lit": {
"mappings": { "mappings": {
"0": { "0": {
@ -7524,6 +7503,9 @@
"phone": { "phone": {
"question": "What is the phone number of {title()}?" "question": "What is the phone number of {title()}?"
}, },
"repeated": {
"render": "Multiple, identical objects can be found on floors {repeat_on}."
},
"service:electricity": { "service:electricity": {
"mappings": { "mappings": {
"0": { "0": {
@ -7541,6 +7523,27 @@
}, },
"question": "Does this amenity have electrical outlets, available to customers when they are inside?" "question": "Does this amenity have electrical outlets, available to customers when they are inside?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Located underground"
},
"1": {
"then": "Located on the ground floor"
},
"2": {
"then": "Located on the ground floor"
},
"3": {
"then": "Located on the first floor"
},
"4": {
"then": "Located on the first basement level"
}
},
"question": "On what level is this feature located?",
"render": "Located on the {level}th floor"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -138,7 +138,13 @@
"email": { "email": {
"question": "Kio estas la retpoŝta adreso de {title()}?" "question": "Kio estas la retpoŝta adreso de {title()}?"
}, },
"level": { "opening_hours": {
"render": "<h3>Malfermitaj horoj</h3>{opening_hours_table(opening_hours)}"
},
"phone": {
"question": "Kio estas la telefonnumero de {title()}?"
},
"single_level": {
"mappings": { "mappings": {
"1": { "1": {
"then": "En la teretaĝo" "then": "En la teretaĝo"
@ -152,12 +158,6 @@
}, },
"render": "En la {level}a etaĝo" "render": "En la {level}a etaĝo"
}, },
"opening_hours": {
"render": "<h3>Malfermitaj horoj</h3>{opening_hours_table(opening_hours)}"
},
"phone": {
"question": "Kio estas la telefonnumero de {title()}?"
},
"website": { "website": {
"question": "Kie estas la retejo de {title()}?" "question": "Kie estas la retejo de {title()}?"
} }

View file

@ -3631,27 +3631,6 @@
"question": "¿Cuál es el nombre de red para el acceso inalámbrico a internet?", "question": "¿Cuál es el nombre de red para el acceso inalámbrico a internet?",
"render": "El nombre de red es <b>{internet_access:ssid}</b>" "render": "El nombre de red es <b>{internet_access:ssid}</b>"
}, },
"level": {
"mappings": {
"0": {
"then": "Localizado bajo tierra"
},
"1": {
"then": "Localizado en la planta baja"
},
"2": {
"then": "Localizado en la planta baja"
},
"3": {
"then": "Localizado en la primera planta"
},
"4": {
"then": "Localizada en el primer sótano"
}
},
"question": "¿En qué nivel se encuentra esta característica?",
"render": "Localizada en la {level}° planta"
},
"luminous_or_lit": { "luminous_or_lit": {
"mappings": { "mappings": {
"0": { "0": {
@ -3764,6 +3743,27 @@
}, },
"question": "¿Esta facilidad tiene enchufes eléctricos, disponibles para los clientes cuando están dentro?" "question": "¿Esta facilidad tiene enchufes eléctricos, disponibles para los clientes cuando están dentro?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Localizado bajo tierra"
},
"1": {
"then": "Localizado en la planta baja"
},
"2": {
"then": "Localizado en la planta baja"
},
"3": {
"then": "Localizado en la primera planta"
},
"4": {
"then": "Localizada en el primer sótano"
}
},
"question": "¿En qué nivel se encuentra esta característica?",
"render": "Localizada en la {level}° planta"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -34,27 +34,6 @@
"email": { "email": {
"question": "Ano ang email address ng {title()}?" "question": "Ano ang email address ng {title()}?"
}, },
"level": {
"mappings": {
"0": {
"then": "Nasa ilalim ng lupa"
},
"1": {
"then": "Nasa unang palapag"
},
"2": {
"then": "Nasa unang palapag"
},
"3": {
"then": "Nasa unang palapag"
},
"4": {
"then": "Nasa silong"
}
},
"question": "Anong palapag matatagpuan ang tampók?",
"render": "Natagpuan sa ika-{level} na palapag"
},
"opening_hours": { "opening_hours": {
"question": "Anong oras nagbubukas ang {title()}?", "question": "Anong oras nagbubukas ang {title()}?",
"render": "<h3>Mga oras na bukas</h3>{opening_hours_table(opening_hours)}" "render": "<h3>Mga oras na bukas</h3>{opening_hours_table(opening_hours)}"
@ -102,6 +81,27 @@
}, },
"question": "Merong bang mga intsupe (outlet) sa loob, para sa mga suki?" "question": "Merong bang mga intsupe (outlet) sa loob, para sa mga suki?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Nasa ilalim ng lupa"
},
"1": {
"then": "Nasa unang palapag"
},
"2": {
"then": "Nasa unang palapag"
},
"3": {
"then": "Nasa unang palapag"
},
"4": {
"then": "Nasa silong"
}
},
"question": "Anong palapag matatagpuan ang tampók?",
"render": "Natagpuan sa ika-{level} na palapag"
},
"website": { "website": {
"question": "Ano ang website ng {title()}?" "question": "Ano ang website ng {title()}?"
}, },

View file

@ -4809,27 +4809,6 @@
} }
} }
}, },
"level": {
"mappings": {
"0": {
"then": "En sous-sol"
},
"1": {
"then": "Rez-de-chaussée"
},
"2": {
"then": "Rez-de-chaussée"
},
"3": {
"then": "Premier étage"
},
"4": {
"then": "Sous-sol"
}
},
"question": "À quel étage se situe lélément ?",
"render": "Étage {level}"
},
"luminous_or_lit": { "luminous_or_lit": {
"mappings": { "mappings": {
"0": { "0": {
@ -4939,6 +4918,27 @@
}, },
"question": "Des prises sont elles à disposition des client·e·s en intérieur?" "question": "Des prises sont elles à disposition des client·e·s en intérieur?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "En sous-sol"
},
"1": {
"then": "Rez-de-chaussée"
},
"2": {
"then": "Rez-de-chaussée"
},
"3": {
"then": "Premier étage"
},
"4": {
"then": "Sous-sol"
}
},
"question": "À quel étage se situe lélément ?",
"render": "Étage {level}"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -798,27 +798,6 @@
"email": { "email": {
"question": "Mi a(z) {title()} e-mail címe?" "question": "Mi a(z) {title()} e-mail címe?"
}, },
"level": {
"mappings": {
"0": {
"then": "A föld alatt"
},
"1": {
"then": "A földszinten"
},
"2": {
"then": "A földszinten"
},
"3": {
"then": "Az első emeleten"
},
"4": {
"then": "Az első alagsori szinten"
}
},
"question": "Melyik szinten található ez a létesítmény?",
"render": "A(z) {level}. emeleten"
},
"opening_hours": { "opening_hours": {
"question": "Mikor van nyitva ez: {title()}?", "question": "Mikor van nyitva ez: {title()}?",
"render": "<h3>Nyitva tartás</h3>{opening_hours_table(opening_hours)}" "render": "<h3>Nyitva tartás</h3>{opening_hours_table(opening_hours)}"
@ -875,6 +854,27 @@
}, },
"question": "Van-e ebben a létesítményben olyan konnektor, amely a bent tartózkodó ügyfelek rendelkezésére áll?" "question": "Van-e ebben a létesítményben olyan konnektor, amely a bent tartózkodó ügyfelek rendelkezésére áll?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "A föld alatt"
},
"1": {
"then": "A földszinten"
},
"2": {
"then": "A földszinten"
},
"3": {
"then": "Az első emeleten"
},
"4": {
"then": "Az első alagsori szinten"
}
},
"question": "Melyik szinten található ez a létesítmény?",
"render": "A(z) {level}. emeleten"
},
"website": { "website": {
"question": "Mi a weboldala ennek: {title()}?" "question": "Mi a weboldala ennek: {title()}?"
}, },

View file

@ -503,27 +503,6 @@
"question": "Apa nama jaringan internet nirkabelnya?", "question": "Apa nama jaringan internet nirkabelnya?",
"render": "Nama jaringan ini adalah <b>{internet_access:ssid}</b>" "render": "Nama jaringan ini adalah <b>{internet_access:ssid}</b>"
}, },
"level": {
"mappings": {
"0": {
"then": "Terletak di bawah tanah"
},
"1": {
"then": "Terletak di lantai dasar"
},
"2": {
"then": "Terletak di lantai dasar"
},
"3": {
"then": "Berlokasi di lantai pertama"
},
"4": {
"then": "Terletak di lantai basement pertama"
}
},
"question": "Pada tingkat apa fitur ini diletakkan?",
"render": "Terletak di lantai {level}"
},
"multilevels": { "multilevels": {
"override": { "override": {
"question": "Pada lantai berapa saja lift ini berjalan?", "question": "Pada lantai berapa saja lift ini berjalan?",
@ -569,6 +548,27 @@
"phone": { "phone": {
"question": "Berapa nomor telepon dari {title()}?" "question": "Berapa nomor telepon dari {title()}?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Terletak di bawah tanah"
},
"1": {
"then": "Terletak di lantai dasar"
},
"2": {
"then": "Terletak di lantai dasar"
},
"3": {
"then": "Berlokasi di lantai pertama"
},
"4": {
"then": "Terletak di lantai basement pertama"
}
},
"question": "Pada tingkat apa fitur ini diletakkan?",
"render": "Terletak di lantai {level}"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -1809,24 +1809,6 @@
"email": { "email": {
"question": "Qual è l'indirizzo email di {title()}?" "question": "Qual è l'indirizzo email di {title()}?"
}, },
"level": {
"mappings": {
"0": {
"then": "Si trova sotto il livello stradale"
},
"1": {
"then": "Si trova al pianoterra"
},
"2": {
"then": "Si trova al pianoterra"
},
"3": {
"then": "Si trova al primo piano"
}
},
"question": "A quale piano si trova questo elemento?",
"render": "Si trova al piano numero {level}"
},
"opening_hours": { "opening_hours": {
"question": "Quali sono gli orari di apertura di {title()}?", "question": "Quali sono gli orari di apertura di {title()}?",
"render": "<h3>Orari di apertura</h3>{opening_hours_table(opening_hours)}" "render": "<h3>Orari di apertura</h3>{opening_hours_table(opening_hours)}"
@ -1854,6 +1836,24 @@
"phone": { "phone": {
"question": "Qual è il numero di telefono di {title()}?" "question": "Qual è il numero di telefono di {title()}?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Si trova sotto il livello stradale"
},
"1": {
"then": "Si trova al pianoterra"
},
"2": {
"then": "Si trova al pianoterra"
},
"3": {
"then": "Si trova al primo piano"
}
},
"question": "A quale piano si trova questo elemento?",
"render": "Si trova al piano numero {level}"
},
"website": { "website": {
"question": "Qual è il sito web di {title()}?" "question": "Qual è il sito web di {title()}?"
}, },

View file

@ -530,24 +530,6 @@
"email": { "email": {
"question": "{title()}のEメールアドレスは何ですか" "question": "{title()}のEメールアドレスは何ですか"
}, },
"level": {
"mappings": {
"0": {
"then": "地下にあります"
},
"1": {
"then": "1階にあります"
},
"2": {
"then": "1階にあります"
},
"3": {
"then": "1階にあります"
}
},
"question": "この機能は何階にあるのでしょうか?",
"render": "{level}階にあります"
},
"opening_hours": { "opening_hours": {
"question": "{title()}の営業時間は?", "question": "{title()}の営業時間は?",
"render": "<h3>営業時間</h3>{opening_hours_table(opening_hours)}" "render": "<h3>営業時間</h3>{opening_hours_table(opening_hours)}"
@ -583,6 +565,24 @@
}, },
"question": "このアメニティにはコンセントがあり、お客様が店内にいるときにも利用できますか?" "question": "このアメニティにはコンセントがあり、お客様が店内にいるときにも利用できますか?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "地下にあります"
},
"1": {
"then": "1階にあります"
},
"2": {
"then": "1階にあります"
},
"3": {
"then": "1階にあります"
}
},
"question": "この機能は何階にあるのでしょうか?",
"render": "{level}階にあります"
},
"website": { "website": {
"question": "{title()}のウェブサイトは?" "question": "{title()}のウェブサイトは?"
}, },

View file

@ -642,27 +642,6 @@
"question": "Hva er nettverksnavnet for det trådløse nettverket?", "question": "Hva er nettverksnavnet for det trådløse nettverket?",
"render": "Nettverksnavnet er <b>{internet_access:ssid}</b>" "render": "Nettverksnavnet er <b>{internet_access:ssid}</b>"
}, },
"level": {
"mappings": {
"0": {
"then": "Under bakken"
},
"1": {
"then": "På gateplan"
},
"2": {
"then": "På gateplan"
},
"3": {
"then": "I andre etasje"
},
"4": {
"then": "Er å finne på første kjellernivå"
}
},
"question": "Hvilken etasje befinner funksjonen seg i?",
"render": "Ligger i {level} etasje"
},
"multilevels": { "multilevels": {
"override": { "override": {
"question": "Hvilke etasjer går heisen til?", "question": "Hvilke etasjer går heisen til?",
@ -755,6 +734,27 @@
}, },
"question": "Har denne fasiliteten stikkontakter, tilgjengelig for kunder innendørs?" "question": "Har denne fasiliteten stikkontakter, tilgjengelig for kunder innendørs?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Under bakken"
},
"1": {
"then": "På gateplan"
},
"2": {
"then": "På gateplan"
},
"3": {
"then": "I andre etasje"
},
"4": {
"then": "Er å finne på første kjellernivå"
}
},
"question": "Hvilken etasje befinner funksjonen seg i?",
"render": "Ligger i {level} etasje"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -6881,27 +6881,6 @@
} }
} }
}, },
"level": {
"mappings": {
"0": {
"then": "Bevindt zich ondergronds"
},
"1": {
"then": "Bevindt zich op de begane grond"
},
"2": {
"then": "Bevindt zich gelijkvloers"
},
"3": {
"then": "Bevindt zich op de eerste verdieping"
},
"4": {
"then": "Bevindt zich in de eerste kelderverdieping"
}
},
"question": "Op welke verdieping bevindt dit punt zich?",
"render": "Bevindt zich op de {level}de verdieping"
},
"multilevels": { "multilevels": {
"override": { "override": {
"question": "Naar welke verdiepingen gaat deze lift?", "question": "Naar welke verdiepingen gaat deze lift?",
@ -6980,6 +6959,9 @@
"phone": { "phone": {
"question": "Wat is het telefoonnummer van {title()}?" "question": "Wat is het telefoonnummer van {title()}?"
}, },
"repeated": {
"render": "Er zijn verschillende, identieke objecten op verdiepingen {repeat_on}."
},
"service:electricity": { "service:electricity": {
"mappings": { "mappings": {
"0": { "0": {
@ -6997,6 +6979,27 @@
}, },
"question": "Zijn er stekkers beschikbaar voor klanten die binnen zitten?" "question": "Zijn er stekkers beschikbaar voor klanten die binnen zitten?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Bevindt zich ondergronds"
},
"1": {
"then": "Bevindt zich op de begane grond"
},
"2": {
"then": "Bevindt zich gelijkvloers"
},
"3": {
"then": "Bevindt zich op de eerste verdieping"
},
"4": {
"then": "Bevindt zich in de eerste kelderverdieping"
}
},
"question": "Op welke verdieping bevindt dit punt zich?",
"render": "Bevindt zich op de {level}de verdieping"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -2405,27 +2405,6 @@
"question": "Jaka jest nazwa sieci dla bezprzewodowego dostępu do Internetu?", "question": "Jaka jest nazwa sieci dla bezprzewodowego dostępu do Internetu?",
"render": "Nazwa sieci to <b>{internet_access:ssid}</b>" "render": "Nazwa sieci to <b>{internet_access:ssid}</b>"
}, },
"level": {
"mappings": {
"0": {
"then": "Znajduje się pod ziemią"
},
"1": {
"then": "Znajduje się na parterze"
},
"2": {
"then": "Znajduje się na parterze"
},
"3": {
"then": "Znajduje się na pierwszym piętrze"
},
"4": {
"then": "Położone na pierwszym poziomie piwnicy"
}
},
"question": "Na jakim poziomie znajduje się ta funkcja?",
"render": "Znajduje się na {level} piętrze"
},
"luminous_or_lit": { "luminous_or_lit": {
"mappings": { "mappings": {
"0": { "0": {
@ -2535,6 +2514,27 @@
}, },
"question": "Czy w tym przybytku znajdują się gniazdka elektryczne, gdzie klienci mogą naładować swoje urządzenia?" "question": "Czy w tym przybytku znajdują się gniazdka elektryczne, gdzie klienci mogą naładować swoje urządzenia?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Znajduje się pod ziemią"
},
"1": {
"then": "Znajduje się na parterze"
},
"2": {
"then": "Znajduje się na parterze"
},
"3": {
"then": "Znajduje się na pierwszym piętrze"
},
"4": {
"then": "Położone na pierwszym poziomie piwnicy"
}
},
"question": "Na jakim poziomie znajduje się ta funkcja?",
"render": "Znajduje się na {level} piętrze"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -882,27 +882,6 @@
"question": "Qual é o nome da rede para o acesso sem fios à Internet?", "question": "Qual é o nome da rede para o acesso sem fios à Internet?",
"render": "O nome da rede é <b>{internet_access:ssid}</b>" "render": "O nome da rede é <b>{internet_access:ssid}</b>"
}, },
"level": {
"mappings": {
"0": {
"then": "Está no subsolo"
},
"1": {
"then": "Está ao nível do rés-do-chão"
},
"2": {
"then": "Está ao nível do rés-do-chão"
},
"3": {
"then": "Está no primeiro andar"
},
"4": {
"then": "Localizado no primeiro nível da cave"
}
},
"question": "Em que nível se encontra este elemento?",
"render": "Está no {level}º andar"
},
"multilevels": { "multilevels": {
"override": { "override": {
"question": "Para que pisos vai este elevador?", "question": "Para que pisos vai este elevador?",
@ -956,6 +935,27 @@
}, },
"question": "Esta infraestrutura tem tomadas elétricas, disponíveis para os clientes quando estão no interior?" "question": "Esta infraestrutura tem tomadas elétricas, disponíveis para os clientes quando estão no interior?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Está no subsolo"
},
"1": {
"then": "Está ao nível do rés-do-chão"
},
"2": {
"then": "Está ao nível do rés-do-chão"
},
"3": {
"then": "Está no primeiro andar"
},
"4": {
"then": "Localizado no primeiro nível da cave"
}
},
"question": "Em que nível se encontra este elemento?",
"render": "Está no {level}º andar"
},
"smoking": { "smoking": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -628,24 +628,6 @@
"email": { "email": {
"question": "Qual o endereço de e-mail de {title()}?" "question": "Qual o endereço de e-mail de {title()}?"
}, },
"level": {
"mappings": {
"0": {
"then": "Localizado no subsolo"
},
"1": {
"then": "Localizado no térreo"
},
"2": {
"then": "Localizado no térreo"
},
"3": {
"then": "Localizado no primeiro andar"
}
},
"question": "Em que nível esse recurso está localizado?",
"render": "Localizado no {level}o andar"
},
"opening_hours": { "opening_hours": {
"question": "Qual o horário de funcionamento de {title()}?", "question": "Qual o horário de funcionamento de {title()}?",
"render": "<h3>Horário de funcionamento</h3>{opening_hours_table(opening_hours)}" "render": "<h3>Horário de funcionamento</h3>{opening_hours_table(opening_hours)}"
@ -664,6 +646,24 @@
"phone": { "phone": {
"question": "Qual o número de telefone de {title()}?" "question": "Qual o número de telefone de {title()}?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Localizado no subsolo"
},
"1": {
"then": "Localizado no térreo"
},
"2": {
"then": "Localizado no térreo"
},
"3": {
"then": "Localizado no primeiro andar"
}
},
"question": "Em que nível esse recurso está localizado?",
"render": "Localizado no {level}o andar"
},
"website": { "website": {
"question": "Qual o site de {title()}?" "question": "Qual o site de {title()}?"
}, },

View file

@ -1558,24 +1558,6 @@
"email": { "email": {
"question": "Какой адрес электронной почты у {title()}?" "question": "Какой адрес электронной почты у {title()}?"
}, },
"level": {
"mappings": {
"0": {
"then": "Расположено под землей"
},
"1": {
"then": "Расположено на первом этаже"
},
"2": {
"then": "Расположено на первом этаже"
},
"3": {
"then": "Расположено на первом этаже"
}
},
"question": "На каком этаже находится этот объект?",
"render": "Расположено на {level}ом этаже"
},
"opening_hours": { "opening_hours": {
"question": "Какое время работы у {title()}?", "question": "Какое время работы у {title()}?",
"render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}" "render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}"
@ -1594,6 +1576,24 @@
"phone": { "phone": {
"question": "Какой номер телефона у {title()}?" "question": "Какой номер телефона у {title()}?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Расположено под землей"
},
"1": {
"then": "Расположено на первом этаже"
},
"2": {
"then": "Расположено на первом этаже"
},
"3": {
"then": "Расположено на первом этаже"
}
},
"question": "На каком этаже находится этот объект?",
"render": "Расположено на {level}ом этаже"
},
"website": { "website": {
"question": "Какой сайт у {title()}?" "question": "Какой сайт у {title()}?"
}, },

View file

@ -149,26 +149,6 @@
"email": { "email": {
"question": "Kakšen naslov elektronske pošte ima {title()}?" "question": "Kakšen naslov elektronske pošte ima {title()}?"
}, },
"level": {
"mappings": {
"0": {
"then": "Nahaja se pod zemljo"
},
"1": {
"then": "Nahaja se v pritličju"
},
"2": {
"then": "Nahaja se v pritličju"
},
"3": {
"then": "Nahaja se v prvem nadstropju"
},
"4": {
"then": "Nahaja se v prvi kletni etaži"
}
},
"render": "Nahaja se v {level}. nadstropju"
},
"opening_hours": { "opening_hours": {
"question": "Kakšen odpiralni čas ima {title()}?", "question": "Kakšen odpiralni čas ima {title()}?",
"render": "<h3>Odpiralni čas</h3>{opening_hours_table(opening_hours)}" "render": "<h3>Odpiralni čas</h3>{opening_hours_table(opening_hours)}"
@ -198,6 +178,26 @@
}, },
"phone": { "phone": {
"question": "Kakšno telefonsko številko ima {title()}?" "question": "Kakšno telefonsko številko ima {title()}?"
},
"single_level": {
"mappings": {
"0": {
"then": "Nahaja se pod zemljo"
},
"1": {
"then": "Nahaja se v pritličju"
},
"2": {
"then": "Nahaja se v pritličju"
},
"3": {
"then": "Nahaja se v prvem nadstropju"
},
"4": {
"then": "Nahaja se v prvi kletni etaži"
}
},
"render": "Nahaja se v {level}. nadstropju"
} }
} }
}, },

View file

@ -51,24 +51,6 @@
"email": { "email": {
"question": "Vad är e-postadressen till {title()}?" "question": "Vad är e-postadressen till {title()}?"
}, },
"level": {
"mappings": {
"0": {
"then": "Ligger under jorden"
},
"1": {
"then": "Ligger på bottenvåningen"
},
"2": {
"then": "Ligger på bottenvåningen"
},
"3": {
"then": "Ligger på första våningen"
}
},
"question": "På vilken nivå finns den här funktionen?",
"render": "Ligger på {level}:e våningen"
},
"opening_hours": { "opening_hours": {
"question": "Vilka är öppettiderna för {title()}?", "question": "Vilka är öppettiderna för {title()}?",
"render": "<h3>Öppettider</h3> {opening_hours_table(opening_hours)}" "render": "<h3>Öppettider</h3> {opening_hours_table(opening_hours)}"
@ -104,6 +86,24 @@
}, },
"question": "Har den här bekvämligheten eluttag tillgängliga för kunder när de är inne?" "question": "Har den här bekvämligheten eluttag tillgängliga för kunder när de är inne?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "Ligger under jorden"
},
"1": {
"then": "Ligger på bottenvåningen"
},
"2": {
"then": "Ligger på bottenvåningen"
},
"3": {
"then": "Ligger på första våningen"
}
},
"question": "På vilken nivå finns den här funktionen?",
"render": "Ligger på {level}:e våningen"
},
"website": { "website": {
"question": "Vad är webbplatsen för {title()}?" "question": "Vad är webbplatsen för {title()}?"
}, },

View file

@ -628,27 +628,6 @@
"email": { "email": {
"question": "{title()} 的電子郵件地址是什麼?" "question": "{title()} 的電子郵件地址是什麼?"
}, },
"level": {
"mappings": {
"0": {
"then": "位於地下"
},
"1": {
"then": "位於 1 樓"
},
"2": {
"then": "位於 1 樓"
},
"3": {
"then": "位於 2 樓"
},
"4": {
"then": "位於地下一樓"
}
},
"question": "此圖徽位於哪個樓層/層級?",
"render": "位於 {level} 樓"
},
"opening_hours": { "opening_hours": {
"question": "{title()} 的開放時間是什麼?", "question": "{title()} 的開放時間是什麼?",
"render": "<h3>開放時間</h3>{opening_hours_table(opening_hours)}" "render": "<h3>開放時間</h3>{opening_hours_table(opening_hours)}"
@ -705,6 +684,27 @@
}, },
"question": "這個便利設施有電器設備,能給客戶使用嗎?" "question": "這個便利設施有電器設備,能給客戶使用嗎?"
}, },
"single_level": {
"mappings": {
"0": {
"then": "位於地下"
},
"1": {
"then": "位於 1 樓"
},
"2": {
"then": "位於 1 樓"
},
"3": {
"then": "位於 2 樓"
},
"4": {
"then": "位於地下一樓"
}
},
"question": "此圖徽位於哪個樓層/層級?",
"render": "位於 {level} 樓"
},
"website": { "website": {
"question": "{title()} 網址是什麼?" "question": "{title()} 網址是什麼?"
}, },

View file

@ -401,10 +401,7 @@
"attribution": "Vurderinger er muliggjort av <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> og er tilgjengelige som <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.", "attribution": "Vurderinger er muliggjort av <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> og er tilgjengelige som <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Jeg har en tilknytning til dette objektet</span><br/><span class='subtle'>Sjekk om du er eier, skaper, ansatt, …</span>", "i_am_affiliated": "<span>Jeg har en tilknytning til dette objektet</span><br/><span class='subtle'>Sjekk om du er eier, skaper, ansatt, …</span>",
"name_required": "Et navn kreves for å vise og opprette vurderinger", "name_required": "Et navn kreves for å vise og opprette vurderinger",
"no_rating": "Ingen vurdering gitt",
"no_reviews_yet": "Ingen vurderinger enda. Vær først til å skrive en og hjelp åpen data og bevegelsen.", "no_reviews_yet": "Ingen vurderinger enda. Vær først til å skrive en og hjelp åpen data og bevegelsen.",
"plz_login": "Logg inn for å legge igjen en vurdering",
"posting_as": "Anmelder som",
"saved": "<span class=\"thanks\">Vurdering lagret. Takk for at du deler din mening.</span>", "saved": "<span class=\"thanks\">Vurdering lagret. Takk for at du deler din mening.</span>",
"saving_review": "Lagrer …", "saving_review": "Lagrer …",
"title": "{count} vurderinger", "title": "{count} vurderinger",

View file

@ -530,10 +530,7 @@
"attribution": "De beoordelingen worden voorzien door <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> en zijn beschikbaar onder de<a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0-licentie</a>. ", "attribution": "De beoordelingen worden voorzien door <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> en zijn beschikbaar onder de<a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0-licentie</a>. ",
"i_am_affiliated": "<span>Ik ben persoonlijk betrokken</span><br/><span class='subtle'>Vink aan indien je de oprichter, maker, werknemer, ... of dergelijke bent</span>", "i_am_affiliated": "<span>Ik ben persoonlijk betrokken</span><br/><span class='subtle'>Vink aan indien je de oprichter, maker, werknemer, ... of dergelijke bent</span>",
"name_required": "De naam van dit object moet gekend zijn om een review te kunnen maken", "name_required": "De naam van dit object moet gekend zijn om een review te kunnen maken",
"no_rating": "Geef een beoordeling voordat je verzendt…",
"no_reviews_yet": "Er zijn nog geen beoordelingen. Wees de eerste om een beoordeling te schrijven en help open data en het bedrijf!", "no_reviews_yet": "Er zijn nog geen beoordelingen. Wees de eerste om een beoordeling te schrijven en help open data en het bedrijf!",
"plz_login": "Meld je aan om een beoordeling te geven",
"posting_as": "Ingelogd als",
"save": "Opslaan", "save": "Opslaan",
"saved": "<span class='thanks'>Bedankt om je beoordeling te delen!</span>", "saved": "<span class='thanks'>Bedankt om je beoordeling te delen!</span>",
"saving_review": "Opslaan...", "saving_review": "Opslaan...",

View file

@ -331,10 +331,7 @@
"attribution": "Recenzje są obsługiwane przez <a href=\"https://mangrove.reviews/\" target=\"_blank\">Recenzje Mangrove</a> i są dostępne na licencji <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.", "attribution": "Recenzje są obsługiwane przez <a href=\"https://mangrove.reviews/\" target=\"_blank\">Recenzje Mangrove</a> i są dostępne na licencji <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Jestem powiązany z tym obiektem</span><br><span class=\"subtle\">Sprawdź czy jesteś właścicielem, twórcą, pracownikiem, ...</span>", "i_am_affiliated": "<span>Jestem powiązany z tym obiektem</span><br><span class=\"subtle\">Sprawdź czy jesteś właścicielem, twórcą, pracownikiem, ...</span>",
"name_required": "Nazwa jest wymagana do wyświetlania i tworzenia opinii", "name_required": "Nazwa jest wymagana do wyświetlania i tworzenia opinii",
"no_rating": "Nie podano oceny",
"no_reviews_yet": "Nie ma jeszcze recenzji. Bądź pierwszym, który je napisze i pomóż otworzyć dane i biznes!", "no_reviews_yet": "Nie ma jeszcze recenzji. Bądź pierwszym, który je napisze i pomóż otworzyć dane i biznes!",
"plz_login": "Zaloguj się, aby zostawić opinię",
"posting_as": "Publikowanie jako",
"save": "Zapisz", "save": "Zapisz",
"saved": "<span class=\"thanks\">Opinia została zapisana. Dzięki za udostępnienie!</span>", "saved": "<span class=\"thanks\">Opinia została zapisana. Dzięki za udostępnienie!</span>",
"saving_review": "Zapisywanie…", "saving_review": "Zapisywanie…",

View file

@ -352,10 +352,7 @@
"attribution": "As avaliações são fornecidas por <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> e estão disponíveis sob a licença <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.", "attribution": "As avaliações são fornecidas por <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> e estão disponíveis sob a licença <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Eu sou afiliado a este objeto</span><br><span class=\"subtle\"><br><span class=\"subtle\">Marque isto se for proprietário, criador, funcionário…</span></span>", "i_am_affiliated": "<span>Eu sou afiliado a este objeto</span><br><span class=\"subtle\"><br><span class=\"subtle\">Marque isto se for proprietário, criador, funcionário…</span></span>",
"name_required": "É necessário um nome para mostrar e criar avaliações", "name_required": "É necessário um nome para mostrar e criar avaliações",
"no_rating": "Nenhuma classificação dada",
"no_reviews_yet": "Ainda não existem avaliações. Seja o primeiro a escrever uma e ajude a abrir os dados e os negócios!", "no_reviews_yet": "Ainda não existem avaliações. Seja o primeiro a escrever uma e ajude a abrir os dados e os negócios!",
"plz_login": "Inicie a sessão para deixar uma avaliação",
"posting_as": "Publicar como",
"saved": "<span class=\"thanks\">Avaliação guardada. Obrigado por partilhar!</span>", "saved": "<span class=\"thanks\">Avaliação guardada. Obrigado por partilhar!</span>",
"saving_review": "A guardar…", "saving_review": "A guardar…",
"title": "{count} avaliações", "title": "{count} avaliações",

View file

@ -162,10 +162,7 @@
"attribution": "As resenhas são fornecidas por <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> e estão disponíveis em <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.", "attribution": "As resenhas são fornecidas por <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> e estão disponíveis em <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Eu sou afiliado a este objeto</span><br><span class=\"subtle\"><br><span class=\"subtle\">Verifique se você é proprietário, criador, funcionário, …</span></span>", "i_am_affiliated": "<span>Eu sou afiliado a este objeto</span><br><span class=\"subtle\"><br><span class=\"subtle\">Verifique se você é proprietário, criador, funcionário, …</span></span>",
"name_required": "É necessário um nome para exibir e criar comentários", "name_required": "É necessário um nome para exibir e criar comentários",
"no_rating": "Nenhuma classificação dada",
"no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!", "no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!",
"plz_login": "Entrar para deixar um comentário",
"posting_as": "Postando como",
"saved": "<span class=\"thanks\">Comentário salvo. Obrigado por compartilhar!</span>", "saved": "<span class=\"thanks\">Comentário salvo. Obrigado por compartilhar!</span>",
"saving_review": "Salvando…", "saving_review": "Salvando…",
"title": "{count} comentários", "title": "{count} comentários",

View file

@ -176,10 +176,7 @@
"attribution": "Отзывы созданы на основе <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> и доступны под лицензией <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.", "attribution": "Отзывы созданы на основе <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> и доступны под лицензией <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Я связан с этим объектом</span><br><span class=\"subtle\"> Отметьте если вы создатель, владелец, работник, …</span>", "i_am_affiliated": "<span>Я связан с этим объектом</span><br><span class=\"subtle\"> Отметьте если вы создатель, владелец, работник, …</span>",
"name_required": "Необходимо название, чтобы просматривать и создавать отзывы", "name_required": "Необходимо название, чтобы просматривать и создавать отзывы",
"no_rating": "Нет рейтинга",
"no_reviews_yet": "Пока нет отзывов. Оставьте первый отзыв и помогите открытым данным и бизнесу!", "no_reviews_yet": "Пока нет отзывов. Оставьте первый отзыв и помогите открытым данным и бизнесу!",
"plz_login": "Войдите, чтобы оставить отзыв",
"posting_as": "Публикация от имени",
"saved": "<span class=\"thanks\"> Отзыв сохранен. Спасибо, что поделились! </span>", "saved": "<span class=\"thanks\"> Отзыв сохранен. Спасибо, что поделились! </span>",
"saving_review": "Сохранение…", "saving_review": "Сохранение…",
"title": "{count} отзыв(-ов)", "title": "{count} отзыв(-ов)",

View file

@ -383,10 +383,7 @@
"attribution": "評審系統由<a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a>提供技術支援,採用<a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>授權條款。", "attribution": "評審系統由<a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a>提供技術支援,採用<a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>授權條款。",
"i_am_affiliated": "<span>我是這物件的相關關係者</span><br><span class=\"subtle\">確認你是否是擁有者、創造者、員工等等</span>", "i_am_affiliated": "<span>我是這物件的相關關係者</span><br><span class=\"subtle\">確認你是否是擁有者、創造者、員工等等</span>",
"name_required": "需要有名稱才能顯示和創造審核", "name_required": "需要有名稱才能顯示和創造審核",
"no_rating": "還沒有評分",
"no_reviews_yet": "還沒有審核,當第一個撰寫者來幫助開放資料與商家吧!", "no_reviews_yet": "還沒有審核,當第一個撰寫者來幫助開放資料與商家吧!",
"plz_login": "登入來留下審核",
"posting_as": "以貼文",
"saved": "<span class=\"thanks\">已儲存審核,謝謝你的分享!</span>", "saved": "<span class=\"thanks\">已儲存審核,謝謝你的分享!</span>",
"saving_review": "儲存中…", "saving_review": "儲存中…",
"title": "{count} 審核次數", "title": "{count} 審核次數",

View file

@ -1,6 +1,6 @@
{ {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.33.4", "version": "0.33.5",
"repository": "https://github.com/pietervdvn/MapComplete", "repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily", "description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues", "bugs": "https://github.com/pietervdvn/MapComplete/issues",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -938,10 +938,6 @@ video {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.ml-3 {
margin-left: 0.75rem;
}
.-ml-6 { .-ml-6 {
margin-left: -1.5rem; margin-left: -1.5rem;
} }
@ -1210,11 +1206,6 @@ video {
width: 2.5rem; width: 2.5rem;
} }
.w-max {
width: -webkit-max-content;
width: max-content;
}
.w-48 { .w-48 {
width: 12rem; width: 12rem;
} }
@ -1404,6 +1395,12 @@ video {
margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
} }
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-reverse > :not([hidden]) ~ :not([hidden]) { .space-y-reverse > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 1; --tw-space-y-reverse: 1;
} }
@ -1478,11 +1475,6 @@ video {
text-overflow: clip; text-overflow: clip;
} }
.break-normal {
overflow-wrap: normal;
word-break: normal;
}
.break-all { .break-all {
word-break: break-all; word-break: break-all;
} }
@ -1555,14 +1547,14 @@ video {
border-width: 1px; border-width: 1px;
} }
.border-4 {
border-width: 4px;
}
.border-2 { .border-2 {
border-width: 2px; border-width: 2px;
} }
.border-4 {
border-width: 4px;
}
.border-x { .border-x {
border-left-width: 1px; border-left-width: 1px;
border-right-width: 1px; border-right-width: 1px;
@ -1669,10 +1661,6 @@ video {
padding: 2rem; padding: 2rem;
} }
.p-1 {
padding: 0.25rem;
}
.p-2 { .p-2 {
padding: 0.5rem; padding: 0.5rem;
} }
@ -1681,6 +1669,10 @@ video {
padding: 1rem; padding: 1rem;
} }
.p-1 {
padding: 0.25rem;
}
.p-0\.5 { .p-0\.5 {
padding: 0.125rem; padding: 0.125rem;
} }
@ -1773,10 +1765,6 @@ video {
text-align: justify; text-align: justify;
} }
.align-middle {
vertical-align: middle;
}
.text-xl { .text-xl {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.75rem; line-height: 1.75rem;
@ -1787,16 +1775,6 @@ video {
line-height: 1.75rem; line-height: 1.75rem;
} }
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-3xl { .text-3xl {
font-size: 1.875rem; font-size: 1.875rem;
line-height: 2.25rem; line-height: 2.25rem;
@ -1807,11 +1785,21 @@ video {
line-height: 2rem; line-height: 2rem;
} }
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-base { .text-base {
font-size: 1rem; font-size: 1rem;
line-height: 1.5rem; line-height: 1.5rem;
} }
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.font-bold { .font-bold {
font-weight: 700; font-weight: 700;
} }
@ -1891,10 +1879,6 @@ video {
font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction); font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction);
} }
.leading-none {
line-height: 1;
}
.tracking-tight { .tracking-tight {
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
@ -2662,26 +2646,6 @@ a.link-underline {
opacity: 1; opacity: 1;
} }
@media (prefers-reduced-motion: no-preference) {
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.motion-safe\:animate-spin {
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
}
}
@media (max-width: 480px) { @media (max-width: 480px) {
.max-\[480px\]\:w-full { .max-\[480px\]\:w-full {
width: 100%; width: 100%;
@ -2816,10 +2780,6 @@ a.link-underline {
height: 4rem; height: 4rem;
} }
.md\:h-12 {
height: 3rem;
}
.md\:w-8 { .md\:w-8 {
width: 2rem; width: 2rem;
} }

View file

@ -12,8 +12,8 @@ mkdir dist/assets 2> /dev/null
export NODE_OPTIONS="--max-old-space-size=8192" export NODE_OPTIONS="--max-old-space-size=8192"
# This script ends every line with '&&' to chain everything. A failure will thus stop the build # This script ends every line with '&&' to chain everything. A failure will thus stop the build
# npm run generate:editor-layer-index && npm run generate:editor-layer-index &&
# npm run generate && npm run generate &&
npm run generate:layouts npm run generate:layouts
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then

View file

@ -21,3 +21,4 @@ scp -r dist.zip hetzner:/root/ &&
scp ./scripts/hetzner/config/* hetzner:/root/ scp ./scripts/hetzner/config/* hetzner:/root/
ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start"
rm dist.zip rm dist.zip
npm run clean

View file

@ -339,21 +339,37 @@ export default class SimpleMetaTaggers {
) )
private static levels = new InlineMetaTagger( private static levels = new InlineMetaTagger(
{ {
doc: "Extract the 'level'-tag into a normalized, ';'-separated value", doc: "Extract the 'level'-tag into a normalized, ';'-separated value called '_level' (which also includes 'repeat_on'). The `level` tag (without underscore) will be normalized with only the value of `level`.",
keys: ["_level"], keys: ["_level"],
}, },
(feature) => { (feature) => {
if (feature.properties["level"] === undefined) { let somethingChanged = false
return false if (feature.properties["level"] !== undefined) {
const l = feature.properties["level"]
const newValue = TagUtils.LevelsParser(l).join(";")
if (l !== newValue) {
feature.properties["level"] = newValue
somethingChanged = true
}
} }
const l = feature.properties["level"] if (feature.properties["repeat_on"] !== undefined) {
const newValue = TagUtils.LevelsParser(l).join(";") const l = feature.properties["repeat_on"]
if (l === newValue) { const newValue = TagUtils.LevelsParser(l).join(";")
return false if (l !== newValue) {
feature.properties["repeat_on"] = newValue
somethingChanged = true
}
} }
feature.properties["level"] = newValue
return true const combined = TagUtils.LevelsParser(
(feature.properties.repeat_on ?? "") + ";" + (feature.properties.level ?? "")
).join(";")
if (feature.properties["_level"] !== combined) {
feature.properties["_level"] = combined
somethingChanged = true
}
return somethingChanged
} }
) )
private static canonicalize = new InlineMetaTagger( private static canonicalize = new InlineMetaTagger(

View file

@ -66,11 +66,11 @@ export default class LayerState {
} }
const t = Translations.t.general.levelSelection const t = Translations.t.general.levelSelection
const conditionsOrred = [ const conditionsOrred = [
new Tag("level", "" + level), new Tag("_level", "" + level),
new RegexTag("level", new RegExp("(.*;)?" + level + "(;.*)?")), new RegexTag("_level", new RegExp("(.*;)?" + level + "(;.*)?")),
] ]
if (level === "0") { if (level === "0") {
conditionsOrred.push(new Tag("level", "")) // No level tag is the same as level '0' conditionsOrred.push(new Tag("_level", "")) // No level tag is the same as level '0'
} }
console.log("Setting levels filter to", conditionsOrred) console.log("Setting levels filter to", conditionsOrred)
this.globalFilters.data.push({ this.globalFilters.data.push({

View file

@ -483,13 +483,22 @@ export class TagUtils {
* TagUtils.LevelsParser("-1") // => ["-1"] * TagUtils.LevelsParser("-1") // => ["-1"]
* TagUtils.LevelsParser("0;-1") // => ["0", "-1"] * TagUtils.LevelsParser("0;-1") // => ["0", "-1"]
* TagUtils.LevelsParser(undefined) // => [] * TagUtils.LevelsParser(undefined) // => []
* TagUtils.LevelsParser("") // => []
* TagUtils.LevelsParser(";") // => []
*
*/ */
public static LevelsParser(level: string): string[] { public static LevelsParser(level: string): string[] {
if (level === undefined || level === null) {
return []
}
let spec = Utils.NoNull([level]) let spec = Utils.NoNull([level])
spec = [].concat(...spec.map((s) => s?.split(";"))) spec = [].concat(...spec.map((s) => s?.split(";")))
spec = [].concat( spec = [].concat(
...spec.map((s) => { ...spec.map((s) => {
s = s.trim() s = s.trim()
if (s === "") {
return undefined
}
if (s.indexOf("-") < 0 || s.startsWith("-")) { if (s.indexOf("-") < 0 || s.startsWith("-")) {
return s return s
} }

View file

@ -1,34 +1,35 @@
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../UIEventSource";
import { MangroveReviews, Review } from "mangrove-reviews-typescript" import { MangroveReviews, Review } from "mangrove-reviews-typescript";
import { Utils } from "../../Utils" import { Utils } from "../../Utils";
import { Feature, Position } from "geojson" import { Feature, Position } from "geojson";
import { GeoOperations } from "../GeoOperations" import { GeoOperations } from "../GeoOperations";
export class MangroveIdentity { export class MangroveIdentity {
public readonly keypair: Store<CryptoKeyPair> public readonly keypair: Store<CryptoKeyPair>;
public readonly key_id: Store<string> public readonly key_id: Store<string>;
constructor(mangroveIdentity: UIEventSource<string>) { constructor(mangroveIdentity: UIEventSource<string>) {
const key_id = new UIEventSource<string>(undefined) const key_id = new UIEventSource<string>(undefined);
this.key_id = key_id this.key_id = key_id;
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined) const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined);
this.keypair = keypairEventSource this.keypair = keypairEventSource;
mangroveIdentity.addCallbackAndRunD(async (data) => { mangroveIdentity.addCallbackAndRunD(async (data) => {
if (data === "") { if (data === "") {
return return;
} }
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data)) const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data));
keypairEventSource.setData(keypair) keypairEventSource.setData(keypair);
const pem = await MangroveReviews.publicToPem(keypair.publicKey) const pem = await MangroveReviews.publicToPem(keypair.publicKey);
key_id.setData(pem) key_id.setData(pem);
}) });
try { try {
if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") { if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") {
MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {}) MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {
});
} }
} catch (e) { } catch (e) {
console.error("Could not create identity: ", e) console.error("Could not create identity: ", e);
} }
} }
@ -38,13 +39,13 @@ export class MangroveIdentity {
* @constructor * @constructor
*/ */
private static async CreateIdentity(identity: UIEventSource<string>): Promise<void> { private static async CreateIdentity(identity: UIEventSource<string>): Promise<void> {
const keypair = await MangroveReviews.generateKeypair() const keypair = await MangroveReviews.generateKeypair();
const jwk = await MangroveReviews.keypairToJwk(keypair) const jwk = await MangroveReviews.keypairToJwk(keypair);
if ((identity.data ?? "") !== "") { if ((identity.data ?? "") !== "") {
// Identity has been loaded via osmPreferences by now - we don't overwrite // Identity has been loaded via osmPreferences by now - we don't overwrite
return return;
} }
identity.setData(JSON.stringify(jwk)) identity.setData(JSON.stringify(jwk));
} }
} }
@ -52,17 +53,18 @@ export class MangroveIdentity {
* Tracks all reviews of a given feature, allows to create a new review * Tracks all reviews of a given feature, allows to create a new review
*/ */
export default class FeatureReviews { export default class FeatureReviews {
private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {} private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {};
public readonly subjectUri: Store<string> public readonly subjectUri: Store<string>;
public readonly average: Store<number | null>;
private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store<boolean> })[]> = private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
new UIEventSource([]) new UIEventSource([]);
public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> = public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
this._reviews this._reviews;
private readonly _lat: number private readonly _lat: number;
private readonly _lon: number private readonly _lon: number;
private readonly _uncertainty: number private readonly _uncertainty: number;
private readonly _name: Store<string> private readonly _name: Store<string>;
private readonly _identity: MangroveIdentity private readonly _identity: MangroveIdentity;
private constructor( private constructor(
feature: Feature, feature: Feature,
@ -75,55 +77,72 @@ export default class FeatureReviews {
} }
) { ) {
const centerLonLat = GeoOperations.centerpointCoordinates(feature) const centerLonLat = GeoOperations.centerpointCoordinates(feature)
;[this._lon, this._lat] = centerLonLat ;[this._lon, this._lat] = centerLonLat;
this._identity = this._identity =
mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined)) mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined));
const nameKey = options?.nameKey ?? "name" const nameKey = options?.nameKey ?? "name";
if (feature.geometry.type === "Point") { if (feature.geometry.type === "Point") {
this._uncertainty = options?.uncertaintyRadius ?? 10 this._uncertainty = options?.uncertaintyRadius ?? 10;
} else { } else {
let coordss: Position[][] let coordss: Position[][];
if (feature.geometry.type === "LineString") { if (feature.geometry.type === "LineString") {
coordss = [feature.geometry.coordinates] coordss = [feature.geometry.coordinates];
} else if ( } else if (
feature.geometry.type === "MultiLineString" || feature.geometry.type === "MultiLineString" ||
feature.geometry.type === "Polygon" feature.geometry.type === "Polygon"
) { ) {
coordss = feature.geometry.coordinates coordss = feature.geometry.coordinates;
} }
let maxDistance = 0 let maxDistance = 0;
for (const coords of coordss) { for (const coords of coordss) {
for (const coord of coords) { for (const coord of coords) {
maxDistance = Math.max( maxDistance = Math.max(
maxDistance, maxDistance,
GeoOperations.distanceBetween(centerLonLat, coord) GeoOperations.distanceBetween(centerLonLat, coord)
) );
} }
} }
this._uncertainty = options?.uncertaintyRadius ?? maxDistance this._uncertainty = options?.uncertaintyRadius ?? maxDistance;
} }
this._name = tagsSource.map((tags) => tags[nameKey] ?? options?.fallbackName) this._name = tagsSource.map((tags) => tags[nameKey] ?? options?.fallbackName);
this.subjectUri = this.ConstructSubjectUri() this.subjectUri = this.ConstructSubjectUri();
const self = this const self = this;
this.subjectUri.addCallbackAndRunD(async (sub) => { this.subjectUri.addCallbackAndRunD(async (sub) => {
const reviews = await MangroveReviews.getReviews({ sub }) const reviews = await MangroveReviews.getReviews({ sub });
self.addReviews(reviews.reviews) self.addReviews(reviews.reviews);
}) });
/* We also construct all subject queries _without_ encoding the name to work around a previous bug /* We also construct all subject queries _without_ encoding the name to work around a previous bug
* See https://github.com/giggls/opencampsitemap/issues/30 * See https://github.com/giggls/opencampsitemap/issues/30
*/ */
this.ConstructSubjectUri(true).addCallbackAndRunD(async (sub) => { this.ConstructSubjectUri(true).addCallbackAndRunD(async (sub) => {
try { try {
const reviews = await MangroveReviews.getReviews({ sub }) const reviews = await MangroveReviews.getReviews({ sub });
self.addReviews(reviews.reviews) self.addReviews(reviews.reviews);
} catch (e) { } catch (e) {
console.log("Could not fetch reviews for partially incorrect query ", sub) console.log("Could not fetch reviews for partially incorrect query ", sub);
} }
}) });
this.average = this._reviews.map(reviews => {
if (!reviews) {
return null;
}
if(reviews.length === 0){
return null
}
let sum = 0;
let count = 0;
for (const review of reviews) {
if (review.rating !== undefined) {
count++;
sum += review.rating;
}
}
return Math.round(sum / count)
});
} }
/** /**
@ -139,14 +158,14 @@ export default class FeatureReviews {
uncertaintyRadius?: number uncertaintyRadius?: number
} }
) { ) {
const key = feature.properties.id const key = feature.properties.id;
const cached = FeatureReviews._featureReviewsCache[key] const cached = FeatureReviews._featureReviewsCache[key];
if (cached !== undefined) { if (cached !== undefined) {
return cached return cached;
} }
const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options) const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options);
FeatureReviews._featureReviewsCache[key] = featureReviews FeatureReviews._featureReviewsCache[key] = featureReviews;
return featureReviews return featureReviews;
} }
/** /**
@ -155,15 +174,15 @@ export default class FeatureReviews {
public async createReview(review: Omit<Review, "sub">): Promise<void> { public async createReview(review: Omit<Review, "sub">): Promise<void> {
const r: Review = { const r: Review = {
sub: this.subjectUri.data, sub: this.subjectUri.data,
...review, ...review
} };
const keypair: CryptoKeyPair = this._identity.keypair.data const keypair: CryptoKeyPair = this._identity.keypair.data;
console.log(r) console.log(r);
const jwt = await MangroveReviews.signReview(keypair, r) const jwt = await MangroveReviews.signReview(keypair, r);
console.log("Signed:", jwt) console.log("Signed:", jwt);
await MangroveReviews.submitReview(jwt) await MangroveReviews.submitReview(jwt);
this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) }) this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) });
this._reviews.ping() this._reviews.ping();
} }
/** /**
@ -172,46 +191,48 @@ export default class FeatureReviews {
* @private * @private
*/ */
private addReviews(reviews: { payload: Review; kid: string }[]) { private addReviews(reviews: { payload: Review; kid: string }[]) {
const self = this const self = this;
const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion)) const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion));
let hasNew = false let hasNew = false;
for (const reviewData of reviews) { for (const reviewData of reviews) {
const review = reviewData.payload const review = reviewData.payload;
try { try {
const url = new URL(review.sub) const url = new URL(review.sub);
console.log("URL is", url) console.log("URL is", url);
if (url.protocol === "geo:") { if (url.protocol === "geo:") {
const coordinate = <[number, number]>( const coordinate = <[number, number]>(
url.pathname.split(",").map((n) => Number(n)) url.pathname.split(",").map((n) => Number(n))
) );
const distance = GeoOperations.distanceBetween( const distance = GeoOperations.distanceBetween(
[this._lat, this._lon], [this._lat, this._lon],
coordinate coordinate
) );
if (distance > this._uncertainty) { if (distance > this._uncertainty) {
continue continue;
} }
} }
} catch (e) { } catch (e) {
console.warn(e) console.warn(e);
} }
const key = review.rating + " " + review.opinion const key = review.rating + " " + review.opinion;
if (alreadyKnown.has(key)) { if (alreadyKnown.has(key)) {
continue continue;
} }
self._reviews.data.push({ self._reviews.data.push({
...review, ...review,
madeByLoggedInUser: this._identity.key_id.map((user_key_id) => { madeByLoggedInUser: this._identity.key_id.map((user_key_id) => {
return reviewData.kid === user_key_id return reviewData.kid === user_key_id;
}), })
}) });
hasNew = true hasNew = true;
} }
if (hasNew) { if (hasNew) {
self._reviews.ping() self._reviews.data.sort((a, b) => b.iat - a.iat) // Sort with most recent first
self._reviews.ping();
} }
} }
@ -224,13 +245,13 @@ export default class FeatureReviews {
private ConstructSubjectUri(dontEncodeName: boolean = false): Store<string> { private ConstructSubjectUri(dontEncodeName: boolean = false): Store<string> {
// https://www.rfc-editor.org/rfc/rfc5870#section-3.4.2 // https://www.rfc-editor.org/rfc/rfc5870#section-3.4.2
// `u` stands for `uncertainty`, https://www.rfc-editor.org/rfc/rfc5870#section-3.4.3 // `u` stands for `uncertainty`, https://www.rfc-editor.org/rfc/rfc5870#section-3.4.3
const self = this const self = this;
return this._name.map(function (name) { return this._name.map(function(name) {
let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}` let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}`;
if (name) { if (name) {
uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name)) uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name));
} }
return uri return uri;
}) });
} }
} }

View file

@ -1,58 +1,62 @@
import LayoutConfig from "./ThemeConfig/LayoutConfig"; import LayoutConfig from "./ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../UI/SpecialVisualization"; import { SpecialVisualizationState } from "../UI/SpecialVisualization"
import { Changes } from "../Logic/Osm/Changes"; import { Changes } from "../Logic/Osm/Changes"
import { Store, UIEventSource } from "../Logic/UIEventSource"; import { Store, UIEventSource } from "../Logic/UIEventSource"
import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource";
import { OsmConnection } from "../Logic/Osm/OsmConnection";
import { ExportableMap, MapProperties } from "./MapProperties";
import LayerState from "../Logic/State/LayerState";
import { Feature, Point, Polygon } from "geojson";
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
import { Map as MlMap } from "maplibre-gl";
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning";
import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor";
import { GeoLocationState } from "../Logic/State/GeoLocationState";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import { QueryParameters } from "../Logic/Web/QueryParameters";
import UserRelatedState from "../Logic/State/UserRelatedState";
import LayerConfig from "./ThemeConfig/LayerConfig";
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler";
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers";
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource";
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore";
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource";
import ShowDataLayer from "../UI/Map/ShowDataLayer";
import TitleHandler from "../Logic/Actors/TitleHandler";
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor";
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader";
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater";
import { BBox } from "../Logic/BBox";
import Constants from "./Constants";
import Hotkeys from "../UI/Base/Hotkeys";
import Translations from "../UI/i18n/Translations";
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore";
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource";
import { MenuState } from "./MenuState";
import MetaTagging from "../Logic/MetaTagging";
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator";
import { import {
NewGeometryFromChangesFeatureSource FeatureSource,
} from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"; IndexedFeatureSource,
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; WritableFeatureSource,
import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer"; } from "../Logic/FeatureSource/FeatureSource"
import { Utils } from "../Utils"; import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { EliCategory } from "./RasterLayerProperties"; import { ExportableMap, MapProperties } from "./MapProperties"
import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"; import LayerState from "../Logic/State/LayerState"
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"; import { Feature, Point, Polygon } from "geojson"
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"; import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"; import { Map as MlMap } from "maplibre-gl"
import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector"; import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"
import FilteredLayer from "./FilteredLayer"; import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"; import { GeoLocationState } from "../Logic/State/GeoLocationState"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"; import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import { Imgur } from "../Logic/ImageProviders/Imgur"; import { QueryParameters } from "../Logic/Web/QueryParameters"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LayerConfig from "./ThemeConfig/LayerConfig"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers"
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
import ShowDataLayer from "../UI/Map/ShowDataLayer"
import TitleHandler from "../Logic/Actors/TitleHandler"
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater"
import { BBox } from "../Logic/BBox"
import Constants from "./Constants"
import Hotkeys from "../UI/Base/Hotkeys"
import Translations from "../UI/i18n/Translations"
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
import { MenuState } from "./MenuState"
import MetaTagging from "../Logic/MetaTagging"
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"
import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer"
import { Utils } from "../Utils"
import { EliCategory } from "./RasterLayerProperties"
import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
import NoElementsInViewDetector, {
FeatureViewState,
} from "../Logic/Actors/NoElementsInViewDetector"
import FilteredLayer from "./FilteredLayer"
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import { Imgur } from "../Logic/ImageProviders/Imgur"
/** /**
* *
@ -63,71 +67,71 @@ import { Imgur } from "../Logic/ImageProviders/Imgur";
* It ties up all the needed elements and starts some actors. * It ties up all the needed elements and starts some actors.
*/ */
export default class ThemeViewState implements SpecialVisualizationState { export default class ThemeViewState implements SpecialVisualizationState {
readonly layout: LayoutConfig; readonly layout: LayoutConfig
readonly map: UIEventSource<MlMap>; readonly map: UIEventSource<MlMap>
readonly changes: Changes; readonly changes: Changes
readonly featureSwitches: FeatureSwitchState; readonly featureSwitches: FeatureSwitchState
readonly featureSwitchIsTesting: Store<boolean>; readonly featureSwitchIsTesting: Store<boolean>
readonly featureSwitchUserbadge: Store<boolean>; readonly featureSwitchUserbadge: Store<boolean>
readonly featureProperties: FeaturePropertiesStore; readonly featureProperties: FeaturePropertiesStore
readonly osmConnection: OsmConnection; readonly osmConnection: OsmConnection
readonly selectedElement: UIEventSource<Feature>; readonly selectedElement: UIEventSource<Feature>
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>; readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
readonly mapProperties: MapProperties & ExportableMap; readonly mapProperties: MapProperties & ExportableMap
readonly osmObjectDownloader: OsmObjectDownloader; readonly osmObjectDownloader: OsmObjectDownloader
readonly dataIsLoading: Store<boolean>; readonly dataIsLoading: Store<boolean>
/** /**
* Indicates if there is _some_ data in view, even if it is not shown due to the filters * Indicates if there is _some_ data in view, even if it is not shown due to the filters
*/ */
readonly hasDataInView: Store<FeatureViewState>; readonly hasDataInView: Store<FeatureViewState>
readonly guistate: MenuState; readonly guistate: MenuState
readonly fullNodeDatabase?: FullNodeDatabaseSource; readonly fullNodeDatabase?: FullNodeDatabaseSource
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>; readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
readonly indexedFeatures: IndexedFeatureSource & LayoutSource; readonly indexedFeatures: IndexedFeatureSource & LayoutSource
readonly currentView: FeatureSource<Feature<Polygon>>; readonly currentView: FeatureSource<Feature<Polygon>>
readonly featuresInView: FeatureSource; readonly featuresInView: FeatureSource
readonly newFeatures: WritableFeatureSource; readonly newFeatures: WritableFeatureSource
readonly layerState: LayerState; readonly layerState: LayerState
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>; readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>; readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>
readonly availableLayers: Store<RasterLayerPolygon[]>; readonly availableLayers: Store<RasterLayerPolygon[]>
readonly selectedLayer: UIEventSource<LayerConfig>; readonly selectedLayer: UIEventSource<LayerConfig>
readonly userRelatedState: UserRelatedState; readonly userRelatedState: UserRelatedState
readonly geolocation: GeoLocationHandler; readonly geolocation: GeoLocationHandler
readonly imageUploadManager: ImageUploadManager readonly imageUploadManager: ImageUploadManager
readonly lastClickObject: WritableFeatureSource; readonly lastClickObject: WritableFeatureSource
readonly overlayLayerStates: ReadonlyMap< readonly overlayLayerStates: ReadonlyMap<
string, string,
{ readonly isDisplayed: UIEventSource<boolean> } { readonly isDisplayed: UIEventSource<boolean> }
>; >
/** /**
* All 'level'-tags that are available with the current features * All 'level'-tags that are available with the current features
*/ */
readonly floors: Store<string[]>; readonly floors: Store<string[]>
constructor(layout: LayoutConfig) { constructor(layout: LayoutConfig) {
Utils.initDomPurify(); Utils.initDomPurify()
this.layout = layout; this.layout = layout
this.featureSwitches = new FeatureSwitchState(layout); this.featureSwitches = new FeatureSwitchState(layout)
this.guistate = new MenuState( this.guistate = new MenuState(
this.featureSwitches.featureSwitchWelcomeMessage.data, this.featureSwitches.featureSwitchWelcomeMessage.data,
layout.id layout.id
); )
this.map = new UIEventSource<MlMap>(undefined); this.map = new UIEventSource<MlMap>(undefined)
const initial = new InitialMapPositioning(layout); const initial = new InitialMapPositioning(layout)
this.mapProperties = new MapLibreAdaptor(this.map, initial); this.mapProperties = new MapLibreAdaptor(this.map, initial)
const geolocationState = new GeoLocationState(); const geolocationState = new GeoLocationState()
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting; this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin; this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
this.osmConnection = new OsmConnection({ this.osmConnection = new OsmConnection({
dryRun: this.featureSwitches.featureSwitchIsTesting, dryRun: this.featureSwitches.featureSwitchIsTesting,
@ -137,68 +141,66 @@ export default class ThemeViewState implements SpecialVisualizationState {
undefined, undefined,
"Used to complete the login" "Used to complete the login"
), ),
osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data,
}); })
this.userRelatedState = new UserRelatedState( this.userRelatedState = new UserRelatedState(
this.osmConnection, this.osmConnection,
layout?.language, layout?.language,
layout, layout,
this.featureSwitches, this.featureSwitches,
this.mapProperties this.mapProperties
); )
this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => { this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
this.mapProperties.allowRotating.setData(fixated !== "yes"); this.mapProperties.allowRotating.setData(fixated !== "yes")
}); })
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element"); this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer"); this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer")
this.selectedElementAndLayer = this.selectedElement.mapD( this.selectedElementAndLayer = this.selectedElement.mapD(
(feature) => { (feature) => {
const layer = this.selectedLayer.data; const layer = this.selectedLayer.data
if (!layer) { if (!layer) {
return undefined; return undefined
} }
return { layer, feature }; return { layer, feature }
}, },
[this.selectedLayer] [this.selectedLayer]
); )
this.geolocation = new GeoLocationHandler( this.geolocation = new GeoLocationHandler(
geolocationState, geolocationState,
this.selectedElement, this.selectedElement,
this.mapProperties, this.mapProperties,
this.userRelatedState.gpsLocationHistoryRetentionTime this.userRelatedState.gpsLocationHistoryRetentionTime
); )
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location); this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
const self = this
const self = this; this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id);
{ {
const overlayLayerStates = new Map<string, { isDisplayed: UIEventSource<boolean> }>(); const overlayLayerStates = new Map<string, { isDisplayed: UIEventSource<boolean> }>()
for (const rasterInfo of this.layout.tileLayerSources) { for (const rasterInfo of this.layout.tileLayerSources) {
const isDisplayed = QueryParameters.GetBooleanQueryParameter( const isDisplayed = QueryParameters.GetBooleanQueryParameter(
"overlay-" + rasterInfo.id, "overlay-" + rasterInfo.id,
rasterInfo.defaultState ?? true, rasterInfo.defaultState ?? true,
"Wether or not overlayer layer " + rasterInfo.id + " is shown" "Wether or not overlayer layer " + rasterInfo.id + " is shown"
); )
const state = { isDisplayed }; const state = { isDisplayed }
overlayLayerStates.set(rasterInfo.id, state); overlayLayerStates.set(rasterInfo.id, state)
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state); new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state)
} }
this.overlayLayerStates = overlayLayerStates; this.overlayLayerStates = overlayLayerStates
} }
{ {
/* Setup the layout source /* Setup the layout source
* A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too * A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too
*/ */
if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) { if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) {
this.fullNodeDatabase = new FullNodeDatabaseSource(); this.fullNodeDatabase = new FullNodeDatabaseSource()
} }
const layoutSource = new LayoutSource( const layoutSource = new LayoutSource(
@ -208,49 +210,49 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.osmConnection.Backend(), this.osmConnection.Backend(),
(id) => self.layerState.filteredLayers.get(id).isDisplayed, (id) => self.layerState.filteredLayers.get(id).isDisplayed,
this.fullNodeDatabase this.fullNodeDatabase
); )
this.indexedFeatures = layoutSource; this.indexedFeatures = layoutSource
const empty = []; const empty = []
let currentViewIndex = 0; let currentViewIndex = 0
this.currentView = new StaticFeatureSource( this.currentView = new StaticFeatureSource(
this.mapProperties.bounds.map((bbox) => { this.mapProperties.bounds.map((bbox) => {
if (!bbox) { if (!bbox) {
return empty; return empty
} }
currentViewIndex++; currentViewIndex++
return <Feature[]>[ return <Feature[]>[
bbox.asGeoJson({ bbox.asGeoJson({
zoom: this.mapProperties.zoom.data, zoom: this.mapProperties.zoom.data,
...this.mapProperties.location.data, ...this.mapProperties.location.data,
id: "current_view" id: "current_view",
}) }),
]; ]
}) })
); )
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds); this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
this.dataIsLoading = layoutSource.isLoading; this.dataIsLoading = layoutSource.isLoading
const indexedElements = this.indexedFeatures; const indexedElements = this.indexedFeatures
this.featureProperties = new FeaturePropertiesStore(indexedElements); this.featureProperties = new FeaturePropertiesStore(indexedElements)
this.changes = new Changes( this.changes = new Changes(
{ {
dryRun: this.featureSwitches.featureSwitchIsTesting, dryRun: this.featureSwitches.featureSwitchIsTesting,
allElements: indexedElements, allElements: indexedElements,
featurePropertiesStore: this.featureProperties, featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection, osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations historicalUserLocations: this.geolocation.historicalUserLocations,
}, },
layout?.isLeftRightSensitive() ?? false layout?.isLeftRightSensitive() ?? false
); )
this.historicalUserLocations = this.geolocation.historicalUserLocations; this.historicalUserLocations = this.geolocation.historicalUserLocations
this.newFeatures = new NewGeometryFromChangesFeatureSource( this.newFeatures = new NewGeometryFromChangesFeatureSource(
this.changes, this.changes,
indexedElements, indexedElements,
this.featureProperties this.featureProperties
); )
layoutSource.addSource(this.newFeatures); layoutSource.addSource(this.newFeatures)
const perLayer = new PerLayerFeatureSourceSplitter( const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()).filter( Array.from(this.layerState.filteredLayers.values()).filter(
@ -266,11 +268,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
features.length, features.length,
"leftover features, such as", "leftover features, such as",
features[0].properties features[0].properties
); )
} },
} }
); )
this.perLayer = perLayer.perLayer; this.perLayer = perLayer.perLayer
} }
this.perLayer.forEach((fs) => { this.perLayer.forEach((fs) => {
new SaveFeatureSourceToLocalStorage( new SaveFeatureSourceToLocalStorage(
@ -280,74 +282,80 @@ export default class ThemeViewState implements SpecialVisualizationState {
fs, fs,
this.featureProperties, this.featureProperties,
fs.layer.layerDef.maxAgeOfCache fs.layer.layerDef.maxAgeOfCache
); )
}); })
this.floors = this.featuresInView.features.stabilized(500).map((features) => { this.floors = this.featuresInView.features.stabilized(500).map((features) => {
if (!features) { if (!features) {
return []; return []
} }
const floors = new Set<string>(); const floors = new Set<string>()
for (const feature of features) { for (const feature of features) {
const level = feature.properties["level"]; let level = feature.properties["_level"]
if (level) { if (level) {
const levels = level.split(";"); const levels = level.split(";")
for (const l of levels) { for (const l of levels) {
floors.add(l); floors.add(l)
} }
} else { } else {
floors.add("0"); // '0' is the default and is thus _always_ present floors.add("0") // '0' is the default and is thus _always_ present
} }
} }
const sorted = Array.from(floors); const sorted = Array.from(floors)
// Sort alphabetically first, to deal with floor "A", "B" and "C" // Sort alphabetically first, to deal with floor "A", "B" and "C"
sorted.sort(); sorted.sort()
sorted.sort((a, b) => { sorted.sort((a, b) => {
// We use the laxer 'parseInt' to deal with floor '1A' // We use the laxer 'parseInt' to deal with floor '1A'
const na = parseInt(a); const na = parseInt(a)
const nb = parseInt(b); const nb = parseInt(b)
if (isNaN(na) || isNaN(nb)) { if (isNaN(na) || isNaN(nb)) {
return 0; return 0
} }
return na - nb; return na - nb
}); })
sorted.reverse(/* new list, no side-effects */); sorted.reverse(/* new list, no side-effects */)
return sorted; return sorted
}); })
const lastClick = (this.lastClickObject = new LastClickFeatureSource( const lastClick = (this.lastClickObject = new LastClickFeatureSource(
this.mapProperties.lastClickLocation, this.mapProperties.lastClickLocation,
this.layout this.layout
)); ))
this.osmObjectDownloader = new OsmObjectDownloader( this.osmObjectDownloader = new OsmObjectDownloader(
this.osmConnection.Backend(), this.osmConnection.Backend(),
this.changes this.changes
); )
this.perLayerFiltered = this.showNormalDataOn(this.map); this.perLayerFiltered = this.showNormalDataOn(this.map)
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView; this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
this.imageUploadManager = new ImageUploadManager(layout, Imgur.singleton, this.featureProperties, this.osmConnection, this.changes) this.imageUploadManager = new ImageUploadManager(
layout,
Imgur.singleton,
this.featureProperties,
this.osmConnection,
this.changes
)
this.initActors(); this.initActors()
this.addLastClick(lastClick); this.addLastClick(lastClick)
this.drawSpecialLayers(); this.drawSpecialLayers()
this.initHotkeys(); this.initHotkeys()
this.miscSetup(); this.miscSetup()
if (!Utils.runningFromConsole) { if (!Utils.runningFromConsole) {
console.log("State setup completed", this); console.log("State setup completed", this)
} }
} }
public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> { public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> {
const filteringFeatureSource = new Map<string, FilteringFeatureSource>(); const filteringFeatureSource = new Map<string, FilteringFeatureSource>()
this.perLayer.forEach((fs, layerName) => { this.perLayer.forEach((fs, layerName) => {
const doShowLayer = this.mapProperties.zoom.map( const doShowLayer = this.mapProperties.zoom.map(
(z) => (z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed] [fs.layer.isDisplayed]
); )
if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) { if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) {
/* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined) /* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined)
@ -357,15 +365,15 @@ export default class ThemeViewState implements SpecialVisualizationState {
* Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden. * Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden.
* However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer! * However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer!
* */ * */
return; return
} }
const filtered = new FilteringFeatureSource( const filtered = new FilteringFeatureSource(
fs.layer, fs.layer,
fs, fs,
(id) => this.featureProperties.getStore(id), (id) => this.featureProperties.getStore(id),
this.layerState.globalFilters this.layerState.globalFilters
); )
filteringFeatureSource.set(layerName, filtered); filteringFeatureSource.set(layerName, filtered)
new ShowDataLayer(map, { new ShowDataLayer(map, {
layer: fs.layer.layerDef, layer: fs.layer.layerDef,
@ -373,30 +381,30 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer, doShowLayer,
selectedElement: this.selectedElement, selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer, selectedLayer: this.selectedLayer,
fetchStore: (id) => this.featureProperties.getStore(id) fetchStore: (id) => this.featureProperties.getStore(id),
}); })
}); })
return filteringFeatureSource; return filteringFeatureSource
} }
/** /**
* Various small methods that need to be called * Various small methods that need to be called
*/ */
private miscSetup() { private miscSetup() {
this.userRelatedState.markLayoutAsVisited(this.layout); this.userRelatedState.markLayoutAsVisited(this.layout)
this.selectedElement.addCallbackAndRunD((feature) => { this.selectedElement.addCallbackAndRunD((feature) => {
// As soon as we have a selected element, we clear the selected element // As soon as we have a selected element, we clear the selected element
// This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature // This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature
// The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear // The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear
if (feature.properties.id === "last_click") { if (feature.properties.id === "last_click") {
return; return
} }
this.lastClickObject.features.setData([]); this.lastClickObject.features.setData([])
}); })
if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) { if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) {
Utils.LoadCustomCss(this.layout.customCss); Utils.LoadCustomCss(this.layout.customCss)
} }
} }
@ -405,74 +413,74 @@ export default class ThemeViewState implements SpecialVisualizationState {
{ nomod: "Escape", onUp: true }, { nomod: "Escape", onUp: true },
Translations.t.hotkeyDocumentation.closeSidebar, Translations.t.hotkeyDocumentation.closeSidebar,
() => { () => {
this.selectedElement.setData(undefined); this.selectedElement.setData(undefined)
this.guistate.closeAll(); this.guistate.closeAll()
} }
); )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
nomod: "b" nomod: "b",
}, },
Translations.t.hotkeyDocumentation.openLayersPanel, Translations.t.hotkeyDocumentation.openLayersPanel,
() => { () => {
if (this.featureSwitches.featureSwitchFilter.data) { if (this.featureSwitches.featureSwitchFilter.data) {
this.guistate.openFilterView(); this.guistate.openFilterView()
} }
} }
); )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ shift: "O" }, { shift: "O" },
Translations.t.hotkeyDocumentation.selectMapnik, Translations.t.hotkeyDocumentation.selectMapnik,
() => { () => {
this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto); this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto)
} }
); )
const setLayerCategory = (category: EliCategory) => { const setLayerCategory = (category: EliCategory) => {
const available = this.availableLayers.data; const available = this.availableLayers.data
const current = this.mapProperties.rasterLayer; const current = this.mapProperties.rasterLayer
const best = RasterLayerUtils.SelectBestLayerAccordingTo( const best = RasterLayerUtils.SelectBestLayerAccordingTo(
available, available,
category, category,
current.data current.data
); )
console.log("Best layer for category", category, "is", best.properties.id); console.log("Best layer for category", category, "is", best.properties.id)
current.setData(best); current.setData(best)
}; }
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ nomod: "O" }, { nomod: "O" },
Translations.t.hotkeyDocumentation.selectOsmbasedmap, Translations.t.hotkeyDocumentation.selectOsmbasedmap,
() => setLayerCategory("osmbasedmap") () => setLayerCategory("osmbasedmap")
); )
Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () => Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () =>
setLayerCategory("map") setLayerCategory("map")
); )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ nomod: "P" }, { nomod: "P" },
Translations.t.hotkeyDocumentation.selectAerial, Translations.t.hotkeyDocumentation.selectAerial,
() => setLayerCategory("photo") () => setLayerCategory("photo")
); )
} }
private addLastClick(last_click: LastClickFeatureSource) { private addLastClick(last_click: LastClickFeatureSource) {
// The last_click gets a _very_ special treatment as it interacts with various parts // The last_click gets a _very_ special treatment as it interacts with various parts
const last_click_layer = this.layerState.filteredLayers.get("last_click"); const last_click_layer = this.layerState.filteredLayers.get("last_click")
this.featureProperties.trackFeatureSource(last_click); this.featureProperties.trackFeatureSource(last_click)
this.indexedFeatures.addSource(last_click); this.indexedFeatures.addSource(last_click)
last_click.features.addCallbackAndRunD((features) => { last_click.features.addCallbackAndRunD((features) => {
if (this.selectedLayer.data?.id === "last_click") { if (this.selectedLayer.data?.id === "last_click") {
// The last-click location moved, but we have selected the last click of the previous location // 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 // So, we update _after_ clearing the selection to make sure no stray data is sticking around
this.selectedElement.setData(undefined); this.selectedElement.setData(undefined)
this.selectedElement.setData(features[0]); this.selectedElement.setData(features[0])
} }
}); })
new ShowDataLayer(this.map, { new ShowDataLayer(this.map, {
features: new FilteringFeatureSource(last_click_layer, last_click), features: new FilteringFeatureSource(last_click_layer, last_click),
@ -484,18 +492,18 @@ export default class ThemeViewState implements SpecialVisualizationState {
if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) { if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) {
this.map.data.flyTo({ this.map.data.flyTo({
zoom: Constants.minZoomLevelToAddNewPoint, zoom: Constants.minZoomLevelToAddNewPoint,
center: this.mapProperties.lastClickLocation.data center: this.mapProperties.lastClickLocation.data,
}); })
return; return
} }
// We first clear the selection to make sure no weird state is around // We first clear the selection to make sure no weird state is around
this.selectedLayer.setData(undefined); this.selectedLayer.setData(undefined)
this.selectedElement.setData(undefined); this.selectedElement.setData(undefined)
this.selectedElement.setData(feature); this.selectedElement.setData(feature)
this.selectedLayer.setData(last_click_layer.layerDef); this.selectedLayer.setData(last_click_layer.layerDef)
} },
}); })
} }
/** /**
@ -503,7 +511,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
*/ */
private drawSpecialLayers() { private drawSpecialLayers() {
type AddedByDefaultTypes = (typeof Constants.added_by_default)[number] type AddedByDefaultTypes = (typeof Constants.added_by_default)[number]
const empty = []; const empty = []
/** /**
* A listing which maps the layerId onto the featureSource * A listing which maps the layerId onto the featureSource
*/ */
@ -523,21 +531,21 @@ export default class ThemeViewState implements SpecialVisualizationState {
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })] bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })]
) )
), ),
current_view: this.currentView current_view: this.currentView,
}; }
if (this.layout?.lockLocation) { if (this.layout?.lockLocation) {
const bbox = new BBox(this.layout.lockLocation); const bbox = new BBox(this.layout.lockLocation)
this.mapProperties.maxbounds.setData(bbox); this.mapProperties.maxbounds.setData(bbox)
ShowDataLayer.showRange( ShowDataLayer.showRange(
this.map, this.map,
new StaticFeatureSource([bbox.asGeoJson({})]), new StaticFeatureSource([bbox.asGeoJson({})]),
this.featureSwitches.featureSwitchIsTesting this.featureSwitches.featureSwitchIsTesting
); )
} }
const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view"); const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view")
if (currentViewLayer?.tagRenderings?.length > 0) { if (currentViewLayer?.tagRenderings?.length > 0) {
const params = MetaTagging.createExtraFuncParams(this); const params = MetaTagging.createExtraFuncParams(this)
this.featureProperties.trackFeatureSource(specialLayers.current_view); this.featureProperties.trackFeatureSource(specialLayers.current_view)
specialLayers.current_view.features.addCallbackAndRunD((features) => { specialLayers.current_view.features.addCallbackAndRunD((features) => {
MetaTagging.addMetatags( MetaTagging.addMetatags(
features, features,
@ -546,37 +554,37 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.layout, this.layout,
this.osmObjectDownloader, this.osmObjectDownloader,
this.featureProperties this.featureProperties
); )
}); })
} }
const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range"); const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range")
const rangeIsDisplayed = rangeFLayer?.isDisplayed; const rangeIsDisplayed = rangeFLayer?.isDisplayed
if ( if (
!QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef)) !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef))
) { ) {
rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true); rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
} }
this.layerState.filteredLayers.forEach((flayer) => { this.layerState.filteredLayers.forEach((flayer) => {
const id = flayer.layerDef.id; const id = flayer.layerDef.id
const features: FeatureSource = specialLayers[id]; const features: FeatureSource = specialLayers[id]
if (features === undefined) { if (features === undefined) {
return; return
} }
this.featureProperties.trackFeatureSource(features); this.featureProperties.trackFeatureSource(features)
// this.indexedFeatures.addSource(features) // this.indexedFeatures.addSource(features)
new ShowDataLayer(this.map, { new ShowDataLayer(this.map, {
features, features,
doShowLayer: flayer.isDisplayed, doShowLayer: flayer.isDisplayed,
layer: flayer.layerDef, layer: flayer.layerDef,
selectedElement: this.selectedElement, selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer selectedLayer: this.selectedLayer,
}); })
}); })
} }
/** /**
@ -585,30 +593,35 @@ export default class ThemeViewState implements SpecialVisualizationState {
private initActors() { private initActors() {
// Unselect the selected element if it is panned out of view // Unselect the selected element if it is panned out of view
this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => { this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => {
const selected = this.selectedElement.data; const selected = this.selectedElement.data
if (selected === undefined) { if (selected === undefined) {
return; return
} }
const bbox = BBox.get(selected); const bbox = BBox.get(selected)
if (!bbox.overlapsWith(bounds)) { if (!bbox.overlapsWith(bounds)) {
this.selectedElement.setData(undefined); this.selectedElement.setData(undefined)
} }
}); })
this.selectedElement.addCallback((selected) => { this.selectedElement.addCallback((selected) => {
if (selected === undefined) { if (selected === undefined) {
// We did _unselect_ an item - we always remove the lastclick-object // We did _unselect_ an item - we always remove the lastclick-object
this.lastClickObject.features.setData([]); this.lastClickObject.features.setData([])
this.selectedLayer.setData(undefined); this.selectedLayer.setData(undefined)
} }
}); })
new ThemeViewStateHashActor(this); new ThemeViewStateHashActor(this)
new MetaTagging(this); new MetaTagging(this)
new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this); new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this)
new ChangeToElementsActor(this.changes, this.featureProperties); new ChangeToElementsActor(this.changes, this.featureProperties)
new PendingChangesUploader(this.changes, this.selectedElement); new PendingChangesUploader(this.changes, this.selectedElement)
new SelectedElementTagsUpdater(this); new SelectedElementTagsUpdater(this)
new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers); new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers)
new PreferredRasterLayerSelector(this.mapProperties.rasterLayer, this.availableLayers, this.featureSwitches.backgroundLayerId, this.userRelatedState.preferredBackgroundLayer) new PreferredRasterLayerSelector(
this.mapProperties.rasterLayer,
this.availableLayers,
this.featureSwitches.backgroundLayerId,
this.userRelatedState.preferredBackgroundLayer
)
} }
} }

View file

@ -1,12 +1,14 @@
<script lang="ts"> <script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource.js" import type { Writable } from "svelte/store";
/** /**
* For some stupid reason, it is very hard to bind inputs * For some stupid reason, it is very hard to bind inputs
*/ */
export let selected: UIEventSource<boolean> export let selected: Writable<boolean>;
let _c: boolean = selected.data ?? true let _c: boolean = selected.data ?? true;
$: selected.setData(_c) $: selected.set(_c);
</script> </script>
<label class="no-image-background flex gap-1">
<input type="checkbox" bind:checked={_c} /> <input bind:checked={_c} type="checkbox" />
<slot />
</label>

View file

@ -55,27 +55,26 @@
{#if filteredLayer.layerDef.name} {#if filteredLayer.layerDef.name}
<div bind:this={mainElem} class="mb-1.5"> <div bind:this={mainElem} class="mb-1.5">
<label class="no-image-background flex gap-1"> <Checkbox selected={isDisplayed} >
<Checkbox selected={isDisplayed} /> <If condition={filteredLayer.isDisplayed}>
<If condition={filteredLayer.isDisplayed}> <ToSvelte
<ToSvelte construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background")}
construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background")} />
/> <ToSvelte
<ToSvelte slot="else"
slot="else" construct={() =>
construct={() =>
layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background opacity-50")} layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background opacity-50")}
/> />
</If> </If>
{filteredLayer.layerDef.name} {filteredLayer.layerDef.name}
{#if $zoomlevel < layer.minzoom} {#if $zoomlevel < layer.minzoom}
<span class="alert"> <span class="alert">
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} /> <Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} />
</span> </span>
{/if} {/if}
</label> </Checkbox>
{#if $isDisplayed && filteredLayer.layerDef.filters?.length > 0} {#if $isDisplayed && filteredLayer.layerDef.filters?.length > 0}
<div id="subfilters" class="ml-4 flex flex-col gap-y-1"> <div id="subfilters" class="ml-4 flex flex-col gap-y-1">
@ -83,10 +82,9 @@
<div> <div>
<!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with searchable fields --> <!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with searchable fields -->
{#if filter.options.length === 1 && filter.options[0].fields.length === 0} {#if filter.options.length === 1 && filter.options[0].fields.length === 0}
<label> <Checkbox selected={getBooleanStateFor(filter)} >
<Checkbox selected={getBooleanStateFor(filter)} /> {filter.options[0].question}
{filter.options[0].question} </Checkbox>
</label>
{/if} {/if}
{#if filter.options.length === 1 && filter.options[0].fields.length > 0} {#if filter.options.length === 1 && filter.options[0].fields.length > 0}

View file

@ -46,7 +46,7 @@
> >
{#each layer.titleIcons as titleIconConfig} {#each layer.titleIcons as titleIconConfig}
{#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ..._metatags, ..._tags } ) ?? true) && titleIconConfig.IsKnown(_tags)} {#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ..._metatags, ..._tags } ) ?? true) && titleIconConfig.IsKnown(_tags)}
<div class="flex h-8 w-8 items-center"> <div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
<TagRenderingAnswer <TagRenderingAnswer
config={titleIconConfig} config={titleIconConfig}
{tags} {tags}

View file

@ -71,14 +71,20 @@
{:else if $geopermission === "requested"} {:else if $geopermission === "requested"}
<button class="flex w-full items-center gap-x-2 disabled" on:click={jumpToCurrentLocation}> <button class="flex w-full items-center gap-x-2 disabled" on:click={jumpToCurrentLocation}>
<!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup --> <!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup -->
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetClass("animate-spin")} /> <ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetStyle("animation: 3s linear 0s infinite normal none running spin;")} />
<Tr t={Translations.t.general.waitingForGeopermission} /> <Tr t={Translations.t.general.waitingForGeopermission} />
</button> </button>
{:else if $geopermission !== "denied"} {:else if $geopermission === "denied"}
<button class="flex w-full items-center gap-x-2 disabled"> <button class="flex w-full items-center gap-x-2 disabled">
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetClass("motion-safe:animate-spin")} /> <ToSvelte construct={Svg.location_refused_svg().SetClass("w-8 h-8")} />
<Tr t={Translations.t.general.geopermissionDenied} />
</button>
{:else }
<button class="flex w-full items-center gap-x-2 disabled">
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetStyle("animation: 3s linear 0s infinite normal none running spin;")} />
<Tr t={Translations.t.general.waitingForLocation} /> <Tr t={Translations.t.general.waitingForLocation} />
</button> </button>
{/if} {/if}
<div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2"> <div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2">

View file

@ -0,0 +1,47 @@
<script lang="ts">
import FeatureReviews from "../../Logic/Web/MangroveReviews";
import SingleReview from "./SingleReview.svelte";
import { Utils } from "../../Utils";
import StarsBar from "./StarsBar.svelte";
import ReviewForm from "./ReviewForm.svelte";
import Translations from "../i18n/Translations";
import Tr from "../Base/Tr.svelte";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import { UIEventSource } from "../../Logic/UIEventSource";
import type { Feature } from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ToSvelte from "../Base/ToSvelte.svelte";
import Svg from "../../Svg";
/**
* An element showing all reviews
*/
export let reviews: FeatureReviews;
export let state: SpecialVisualizationState;
export let tags: UIEventSource<Record<string, string>>;
export let feature: Feature;
export let layer: LayerConfig;
let average = reviews.average;
let _reviews = [];
reviews.reviews.addCallbackAndRunD(r => {
_reviews = Utils.NoNull(r);
});
</script>
<div class="border-gray-300 border-dashed border-2">
{#if _reviews.length > 1}
<StarsBar score={$average}></StarsBar>
{/if}
{#if _reviews.length > 0}
{#each _reviews as review}
<SingleReview {review}></SingleReview>
{/each}
{:else}
<Tr t={Translations.t.reviews.no_reviews_yet} />
{/if}
<div class="flex justify-end">
<ToSvelte construct={Svg.mangrove_logo_svg().SetClass("w-12 h-12")} />
<Tr t={Translations.t.reviews.attribution} />
</div>
</div>

View file

@ -1,56 +0,0 @@
import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import SingleReview from "./SingleReview"
import BaseUIElement from "../BaseUIElement"
import Img from "../Base/Img"
import { VariableUiElement } from "../Base/VariableUIElement"
import Link from "../Base/Link"
import FeatureReviews from "../../Logic/Web/MangroveReviews"
/**
* Shows the reviews and scoring base on mangrove.reviews
* The middle element is some other component shown in the middle, e.g. the review input element
*/
export default class ReviewElement extends VariableUiElement {
constructor(reviews: FeatureReviews, middleElement: BaseUIElement) {
super(
reviews.reviews.map(
(revs) => {
const elements = []
revs.sort((a, b) => b.iat - a.iat) // Sort with most recent first
const avg =
revs.map((review) => review.rating).reduce((a, b) => a + b, 0) / revs.length
elements.push(
new Combine([
SingleReview.GenStars(avg),
new Link(
revs.length === 1
? Translations.t.reviews.title_singular.Clone()
: Translations.t.reviews.title.Subs({
count: "" + revs.length,
}),
`https://mangrove.reviews/search?sub=${encodeURIComponent(
reviews.subjectUri.data
)}`,
true
),
]).SetClass("font-2xl flex justify-between items-center pl-2 pr-2")
)
elements.push(middleElement)
elements.push(...revs.map((review) => new SingleReview(review)))
elements.push(
new Combine([
Translations.t.reviews.attribution.Clone(),
new Img("./assets/mangrove_logo.png"),
]).SetClass("review-attribution")
)
return new Combine(elements).SetClass("block")
},
[reviews.subjectUri]
)
)
}
}

View file

@ -0,0 +1,97 @@
<script lang="ts">
import FeatureReviews from "../../Logic/Web/MangroveReviews";
import StarsBar from "./StarsBar.svelte";
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import { UIEventSource } from "../../Logic/UIEventSource";
import type { Feature } from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import Translations from "../i18n/Translations";
import Checkbox from "../Base/Checkbox.svelte";
import Tr from "../Base/Tr.svelte";
import If from "../Base/If.svelte";
import Loading from "../Base/Loading.svelte";
import { Review } from "mangrove-reviews-typescript";
import { Utils } from "../../Utils";
export let state: SpecialVisualizationState;
export let tags: UIEventSource<Record<string, string>>;
export let feature: Feature;
export let layer: LayerConfig;
/**
* The form to create a new review.
* This is multi-stepped.
*/
export let reviews: FeatureReviews;
let score = 0;
let confirmedScore = undefined;
let isAffiliated = new UIEventSource(false);
let opinion = new UIEventSource<string>(undefined);
const t = Translations.t.reviews;
let _state: "ask" | "saving" | "done" = "ask";
const connection = state.osmConnection;
async function save() {
_state = "saving";
let nickname = undefined;
if (connection.isLoggedIn.data) {
nickname = connection.userDetails.data.name;
}
const review: Omit<Review, "sub"> = {
rating: confirmedScore,
opinion: opinion.data,
metadata: { nickname, is_affiliated: isAffiliated.data }
};
if (state.featureSwitchIsTesting.data) {
console.log("Testing - not actually saving review", review);
await Utils.waitFor(1000);
} else {
await reviews.createReview(review);
}
_state = "done";
}
</script>
{#if _state === "done"}
<Tr cls="thanks w-full" t={t.saved} />
{:else if _state === "saving"}
<Loading>
<Tr t={t.saving_review} />
</Loading>
{:else}
<div class="interactive border-interactive p-1">
<div class="font-bold">
<SpecialTranslation {feature} {layer} {state} t={Translations.t.reviews.question} {tags}></SpecialTranslation>
</div>
<StarsBar on:click={e => {confirmedScore = e.detail.score}} on:hover={e => {score = e.detail.score}}
on:mouseout={e => {score = null}} score={score ?? confirmedScore ?? 0}
starSize="w-8 h-8"></StarsBar>
{#if confirmedScore !== undefined}
<Tr cls="font-bold mt-2" t={t.question_opinion} />
<textarea bind:value={$opinion} inputmode="text" rows="3" class="w-full mb-1" />
<Checkbox selected={isAffiliated}>
<div class="flex flex-col">
<Tr t={t.i_am_affiliated} />
<Tr cls="subtle" t={t.i_am_affiliated_explanation} />
</div>
</Checkbox>
<div class="flex w-full justify-between flex-wrap items-center">
<If condition={state.osmConnection.isLoggedIn}>
<Tr t={t.reviewing_as.Subs({nickname: state.osmConnection.userDetails.data.name})} />
<Tr slot="else" t={t.reviewing_as_anonymous} />
</If>
<button class="primary" on:click={save}>
<Tr t={t.save} />
</button>
</div>
<Tr cls="subtle mt-4" t={t.tos} />
{/if}
</div>
{/if}

View file

@ -1,101 +0,0 @@
import { Review } from "mangrove-reviews-typescript"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { TextField } from "../Input/TextField"
import Translations from "../i18n/Translations"
import Combine from "../Base/Combine"
import Svg from "../../Svg"
import { VariableUiElement } from "../Base/VariableUIElement"
import { CheckBox } from "../Input/Checkboxes"
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Toggle from "../Input/Toggle"
import { LoginToggle } from "../Popup/LoginButton"
import { SubtleButton } from "../Base/SubtleButton"
export default class ReviewForm extends LoginToggle {
constructor(
onSave: (r: Omit<Review, "sub">) => Promise<void>,
state: {
readonly osmConnection: OsmConnection
readonly featureSwitchUserbadge: Store<boolean>
}
) {
/* made_by_user: new UIEventSource<boolean>(true),
rating: undefined,
comment: undefined,
author: osmConnection.userDetails.data.name,
affiliated: false,
date: new Date(),*/
const commentForm = new TextField({
placeholder: Translations.t.reviews.write_a_comment.Clone(),
htmlType: "area",
textAreaRows: 5,
})
const rating = new UIEventSource<number>(undefined)
const isAffiliated = new CheckBox(Translations.t.reviews.i_am_affiliated)
const reviewMade = new UIEventSource(false)
const postingAs = new Combine([
Translations.t.reviews.posting_as.Clone(),
new VariableUiElement(
state.osmConnection.userDetails.map((ud: UserDetails) => ud.name)
).SetClass("review-author"),
]).SetStyle("display:flex;flex-direction: column;align-items: flex-end;margin-left: auto;")
const saveButton = new Toggle(
Translations.t.reviews.no_rating.SetClass("block alert"),
new SubtleButton(Svg.confirm_svg(), Translations.t.reviews.save)
.OnClickWithLoading(
Translations.t.reviews.saving_review.SetClass("alert"),
async () => {
const review: Omit<Review, "sub"> = {
rating: rating.data,
opinion: commentForm.GetValue().data,
metadata: { nickname: state.osmConnection.userDetails.data.name },
}
await onSave(review)
}
)
.SetClass("break-normal"),
rating.map((r) => r === undefined, [commentForm.GetValue()])
)
const stars = []
for (let i = 1; i <= 5; i++) {
stars.push(
new VariableUiElement(
rating.map((score) => {
if (score === undefined) {
return Svg.star_outline.replace(/#000000/g, "#ccc")
}
return score < i * 20 ? Svg.star_outline : Svg.star
})
).onClick(() => {
rating.setData(i * 20)
})
)
}
const form = new Combine([
new Combine([new Combine(stars).SetClass("review-form-rating"), postingAs]).SetClass(
"flex"
),
commentForm,
new Combine([isAffiliated, saveButton]),
Translations.t.reviews.tos.Clone().SetClass("subtle"),
])
.SetClass("flex flex-col p-4")
.SetStyle(
"border-radius: 1em;" +
" background-color: var(--subtle-detail-color);" +
" color: var(--subtle-detail-color-contrast);" +
" border: 2px solid var(--subtle-detail-color-contrast)"
)
super(
new Toggle(Translations.t.reviews.saved.Clone().SetClass("thanks"), form, reviewMade),
Translations.t.reviews.plz_login,
state
)
}
}

View file

@ -0,0 +1,38 @@
<script lang="ts">
import { Review } from "mangrove-reviews-typescript";
import { Store } from "../../Logic/UIEventSource";
import StarsBar from "./StarsBar.svelte";
import Translations from "../i18n/Translations";
import Tr from "../Base/Tr.svelte";
export let review: Review & { madeByLoggedInUser: Store<boolean> };
let name = review.metadata.nickname;
name ??= (review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "").trim();
if (name.length === 0) {
name = "Anonymous";
}
let d = new Date();
d.setTime(review.iat * 1000);
let date = d.toDateString();
let byLoggedInUser = review.madeByLoggedInUser;
</script>
<div class={"low-interaction p-1 px-2 rounded-lg "+ ($byLoggedInUser ? "border-interactive" : "")}>
<div class="flex justify-between items-center">
<StarsBar score={review.rating}></StarsBar>
<div class="flex flex-wrap space-x-2">
<div class="font-bold">
{name}
</div>
<span class="subtle">
{date}
</span>
</div>
</div>
{#if review.opinion}
{review.opinion}
{/if}
{#if review.metadata.is_affiliated}
<Tr t={Translations.t.reviews.affiliated_reviewer_warning} />
{/if}
</div>

View file

@ -1,64 +0,0 @@
import Combine from "../Base/Combine"
import { FixedUiElement } from "../Base/FixedUiElement"
import Translations from "../i18n/Translations"
import { Utils } from "../../Utils"
import BaseUIElement from "../BaseUIElement"
import Img from "../Base/Img"
import { Review } from "mangrove-reviews-typescript"
import { Store } from "../../Logic/UIEventSource"
export default class SingleReview extends Combine {
constructor(review: Review & { madeByLoggedInUser: Store<boolean> }) {
const date = new Date(review.iat * 1000)
const reviewAuthor =
review.metadata.nickname ??
(review.metadata.given_name ?? "") + (review.metadata.family_name ?? "")
const authorElement = new FixedUiElement(reviewAuthor).SetClass("font-bold")
super([
new Combine([SingleReview.GenStars(review.rating)]),
new FixedUiElement(review.opinion),
new Combine([
new Combine([
authorElement,
review.metadata.is_affiliated
? Translations.t.reviews.affiliated_reviewer_warning
: "",
]).SetStyle("margin-right: 0.5em"),
new FixedUiElement(
`${date.getFullYear()}-${Utils.TwoDigits(
date.getMonth() + 1
)}-${Utils.TwoDigits(date.getDate())} ${Utils.TwoDigits(
date.getHours()
)}:${Utils.TwoDigits(date.getMinutes())}`
).SetClass("subtle"),
]).SetClass("flex mb-4 justify-end"),
])
this.SetClass("block p-2 m-4 rounded-xl subtle-background review-element")
review.madeByLoggedInUser.addCallbackAndRun((madeByUser) => {
if (madeByUser) {
authorElement.SetClass("thanks")
} else {
authorElement.RemoveClass("thanks")
}
})
}
public static GenStars(rating: number): BaseUIElement {
if (rating === undefined) {
return Translations.t.reviews.no_rating
}
if (rating < 10) {
rating = 10
}
const scoreTen = Math.round(rating / 10)
return new Combine([
...Utils.TimesT(scoreTen / 2, (_) =>
new Img("./assets/svg/star.svg").SetClass("'h-8 w-8 md:h-12")
),
scoreTen % 2 == 1
? new Img("./assets/svg/star_half.svg").SetClass("h-8 w-8 md:h-12")
: undefined,
]).SetClass("flex w-max")
}
}

View file

@ -0,0 +1,32 @@
<script lang="ts">
import ToSvelte from "../Base/ToSvelte.svelte";
import Svg from "../../Svg";
import { createEventDispatcher } from "svelte";
export let score: number;
export let cutoff: number;
export let starSize = "w-h h-4";
let dispatch = createEventDispatcher<{ hover: { score: number } }>();
let container: HTMLElement;
function getScore(e: MouseEvent): number {
const x = e.clientX - e.target.getBoundingClientRect().x;
const w = container.getClientRects()[0]?.width;
return (x / w) < 0.5 ? cutoff - 10 : cutoff;
}
</script>
<div bind:this={container} on:click={(e) => dispatch("click", {score: getScore(e)})}
on:mousemove={(e) => dispatch("hover", { score: getScore(e) })}>
{#if score >= cutoff}
<ToSvelte construct={Svg.star_svg().SetClass(starSize)} />
{:else if score + 10 >= cutoff}
<ToSvelte construct={Svg.star_half_svg().SetClass(starSize)} />
{:else}
<ToSvelte construct={Svg.star_outline_svg().SetClass(starSize)} />
{/if}
</div>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import StarElement from "./StarElement.svelte";
/**
* Number between 0 and 100. Every 10 points, another half star is added
*/
export let score: number;
let dispatch = createEventDispatcher<{ hover: number, click: number }>();
let cutoffs = [20,40,60,80,100]
export let starSize = "w-h h-4"
</script>
{#if score !== undefined}
<div class="flex" on:mouseout>
{#each cutoffs as cutoff}
<StarElement {score} {cutoff} {starSize} on:hover on:click/>
{/each}
</div>
{/if}

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { Store } from "../../Logic/UIEventSource";
import StarsBar from "./StarsBar.svelte";
export let score: Store<number>;
</script>
{#if $score !== undefined && $score !== null}
<StarsBar score={$score} />
{/if}

View file

@ -23,8 +23,6 @@ import { Utils } from "../Utils";
import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"; import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata";
import { Translation } from "./i18n/Translation"; import { Translation } from "./i18n/Translation";
import Translations from "./i18n/Translations"; import Translations from "./i18n/Translations";
import ReviewForm from "./Reviews/ReviewForm";
import ReviewElement from "./Reviews/ReviewElement";
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"; import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization";
import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; import LiveQueryHandler from "../Logic/Web/LiveQueryHandler";
import { SubtleButton } from "./Base/SubtleButton"; import { SubtleButton } from "./Base/SubtleButton";
@ -66,6 +64,9 @@ import SendEmail from "./Popup/SendEmail.svelte";
import NearbyImages from "./Popup/NearbyImages.svelte"; import NearbyImages from "./Popup/NearbyImages.svelte";
import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte"; import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte";
import UploadImage from "./Image/UploadImage.svelte"; import UploadImage from "./Image/UploadImage.svelte";
import AllReviews from "./Reviews/AllReviews.svelte";
import StarsBarIcon from "./Reviews/StarsBarIcon.svelte";
import ReviewForm from "./Reviews/ReviewForm.svelte";
class NearbyImageVis implements SpecialVisualization { class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -624,7 +625,66 @@ export default class SpecialVisualizations {
}, },
}, },
{ {
funcName: "reviews", funcName: "rating",
docs: "Shows stars which represent the avarage rating on mangrove.reviews",
args: [
{
name: "subjectKey",
defaultValue: "name",
doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>",
},
{
name: "fallback",
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value",
},
],
constr: (state, tags, args, feature, layer) => {
const nameKey = args[0] ?? "name"
let fallbackName = args[1]
const reviews = FeatureReviews.construct(
feature,
tags,
state.userRelatedState.mangroveIdentity,
{
nameKey: nameKey,
fallbackName,
}
)
return new SvelteUIElement(StarsBarIcon, {score:reviews.average, reviews, state, tags, feature, layer})
},
},
{
funcName: "create_review",
docs: "Invites the contributor to leave a review. Somewhat small UI-element until interacted",
args: [
{
name: "subjectKey",
defaultValue: "name",
doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>",
},
{
name: "fallback",
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value",
},
],
constr: (state, tags, args, feature, layer) => {
const nameKey = args[0] ?? "name"
let fallbackName = args[1]
const reviews = FeatureReviews.construct(
feature,
tags,
state.userRelatedState.mangroveIdentity,
{
nameKey: nameKey,
fallbackName,
}
)
return new SvelteUIElement(ReviewForm, {reviews, state, tags, feature, layer})
},
},
{
funcName: "list_reviews",
docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten", docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten",
example: example:
"`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used", "`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used",
@ -639,10 +699,10 @@ export default class SpecialVisualizations {
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value", doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value",
}, },
], ],
constr: (state, tags, args, feature) => { constr: (state, tags, args, feature, layer) => {
const nameKey = args[0] ?? "name" const nameKey = args[0] ?? "name"
let fallbackName = args[1] let fallbackName = args[1]
const mangrove = FeatureReviews.construct( const reviews = FeatureReviews.construct(
feature, feature,
tags, tags,
state.userRelatedState.mangroveIdentity, state.userRelatedState.mangroveIdentity,
@ -651,9 +711,7 @@ export default class SpecialVisualizations {
fallbackName, fallbackName,
} }
) )
return new SvelteUIElement(AllReviews, {reviews, state, tags, feature, layer})
const form = new ReviewForm((r) => mangrove.createReview(r), state)
return new ReviewElement(mangrove, form)
}, },
}, },
{ {

View file

@ -244,7 +244,7 @@ export class Translation extends BaseUIElement {
continue continue
} }
let txt = this.translations[lng] let txt = this.translations[lng]
txt = txt.replace(/(\.|<br\/>|<br>).*/, "") txt = txt.replace(/(\.|<br\/>|<br>|。).*/, "")
txt = Utils.EllipsesAfter(txt, 255) txt = Utils.EllipsesAfter(txt, 255)
tr[lng] = txt.trim() tr[lng] = txt.trim()
} }

View file

@ -4,9 +4,23 @@ import ThemeViewGUI from "./src/UI/ThemeViewGUI.svelte"
import LayoutConfig from "./src/Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "./src/Models/ThemeConfig/LayoutConfig";
import MetaTagging from "./src/Logic/MetaTagging"; import MetaTagging from "./src/Logic/MetaTagging";
function webgl_support() {
try {
var canvas = document.createElement("canvas")
return (
!!window.WebGLRenderingContext &&
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
)
} catch (e) {
return false
}
}
MetaTagging.setThemeMetatagging(new ThemeMetaTagging()) if (!webgl_support()) {
const state = new ThemeViewState(new LayoutConfig(<any> layout)) new FixedUiElement("WebGL is not supported or not enabled. This is essential for MapComplete to function, please enable this.").SetClass("block alert").AttachTo("maindiv")
const main = new SvelteUIElement(ThemeViewGUI, { state }) }else{
main.AttachTo("maindiv") MetaTagging.setThemeMetatagging(new ThemeMetaTagging())
const state = new ThemeViewState(new LayoutConfig(<any> layout))
const main = new SvelteUIElement(ThemeViewGUI, { state })
main.AttachTo("maindiv")
}