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",
"condition": "name~*",
"question": {
"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?",

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

View file

@ -154,6 +154,44 @@
}
],
"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

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,
"tagRenderings": [
"images",
"level",
{
"id": "ref",
"question": {
@ -162,7 +178,204 @@
"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": [
{
@ -222,7 +435,7 @@
{
"if": {
"or": [
"room=adminstration",
"room=administration",
"room=auditorium",
"room=bedroom",
"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"
]
},
{
"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",
"license": "CC0-1.0",
@ -49,6 +69,16 @@
"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",
"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",
"description": "Shows the reviews module (including the possibility to leave a review)",
"render": {
"*": "{reviews()}"
"*": "{create_review()}{list_reviews()}"
}
},
{
@ -487,6 +487,7 @@
"mappings": [
{
"if": "dog=yes",
"icon": "./assets/layers/questions/dogs_allowed.svg",
"then": {
"en": "Dogs are allowed",
"nl": "honden zijn toegelaten",
@ -515,6 +516,7 @@
},
{
"if": "dog=no",
"icon": "./assets/layers/questions/no_dogs.svg",
"then": {
"en": "Dogs are <b>not</b> allowed",
"nl": "honden zijn <b>niet</b> toegelaten",
@ -542,6 +544,7 @@
},
{
"if": "dog=leashed",
"icon": "./assets/layers/questions/dogs_leashed.svg",
"then": {
"en": "Dogs are allowed, but they have to be leashed",
"nl": "honden zijn <b>enkel aan de leiband</b> welkom",
@ -568,6 +571,8 @@
},
{
"if": "dog=unleashed",
"icon": "./assets/layers/questions/dogs_allowed.svg",
"then": {
"en": "Dogs are allowed and can run around freely",
"nl": "honden zijn welkom en mogen vrij rondlopen",
@ -1619,8 +1624,9 @@
},
{
"id": "multilevels",
"builtin": "level",
"builtin": "single_level",
"override": {
"=labels": [],
"question": {
"en": "What levels does this elevator go to?",
"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": {
"nl": "Op welke verdieping bevindt dit punt zich?",
"en": "On what level is this feature located?",

View file

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

View file

@ -723,6 +723,16 @@
"authors": [],
"sources": []
},
{
"path": "mangrove_logo.svg",
"license": "LOGO",
"authors": [
"Mangrove.reviews"
],
"sources": [
"https://mangrove.reviews/"
]
},
{
"path": "mapcomplete_logo.svg",
"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"?>
<svg xmlns="http://www.w3.org/2000/svg" width="374px" height="374px" viewBox="0 0 374 374" version="1.1">
<g id="surface1">
<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 "/>
</g>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="374px"
height="374px"
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>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,6 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" width="374px" height="374px" viewBox="0 0 374 374" version="1.1">
<g id="surface1">
<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 "/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="374px"
height="374px"
viewBox="0 0 374 374"
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

View file

@ -1,6 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" width="374px" height="374px" viewBox="0 0 374 374" version="1.1">
<g id="surface1">
<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)"/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="374px"
height="374px"
viewBox="0 0 374 374"
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

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>.",
"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",
"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!",
"plz_login": "Entra per deixar una revisió",
"posting_as": "Enviat com",
"save": "Desar",
"saved": "<span class=\"thanks\">Revisió compartida. Gràcies per compartir!</span>",
"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>.",
"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",
"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í!",
"plz_login": "Přihlaste se a zanechte recenzi",
"posting_as": "Přihlášeni jako",
"save": "Uložit",
"saved": "<span class='thanks'>Recenze uložena. Díky za sdílení!</span>",
"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>.",
"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",
"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!",
"plz_login": "Log ind for at give en anmeldelse",
"posting_as": "Anmelder som",
"saved": "<span class=\"thanks\">Anmeldelse gemt. Tak for at bidrage!</span>",
"saving_review": "Gemmer…",
"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.",
"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",
"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!",
"plz_login": "Anmelden, um eine Bewertung abzugeben",
"posting_as": "Veröffentlichen als",
"save": "Speichern",
"saved": "<span class=\"thanks\">Bewertung gespeichert. Danke fürs Teilen!</span>",
"saving_review": "Speichern…",

View file

@ -197,6 +197,7 @@
"example": "Example",
"examples": "Examples",
"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",
"getStartedNewAccount": " or <a href='https://www.openstreetmap.org/user/new' target='_blank'>create a new account</a>",
"goToInbox": "Open inbox",
@ -556,14 +557,16 @@
"reviews": {
"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>.",
"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",
"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!",
"plz_login": "Log in to leave a review",
"posting_as": "Posting as",
"question": "How would you rate {title()}?",
"question_opinion": "How was your experience?",
"reviewing_as": "Reviewing as {nickname}",
"reviewing_as_anonymous": "Reviewing as anonymous",
"save": "Save",
"saved": "<span class='thanks'>Review saved. Thanks for sharing!</span>",
"saved": "Review saved. Thanks for sharing!",
"saving_review": "Saving…",
"title": "{count} reviews",
"title_singular": "One review",

View file

@ -414,10 +414,7 @@
"reviews": {
"affiliated_reviewer_warning": "(Revisión afiliada)",
"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!",
"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>",
"saving_review": "Guardando…",
"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>.",
"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",
"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 !",
"plz_login": "Connectez vous pour laisser un avis",
"posting_as": "Envoi en tant que",
"saved": "<span class=\"thanks\">Avis enregistré. Merci du partage !</span>",
"saving_review": "Enregistrement…",
"title": "{count} avis",

View file

@ -163,10 +163,7 @@
"reviews": {
"affiliated_reviewer_warning": "(Recensión de afiliado)",
"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!",
"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>",
"saving_review": "Gardando…",
"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.",
"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",
"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!",
"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>",
"saving_review": "Mentés…",
"title": "{count} vélemény",

View file

@ -162,9 +162,6 @@
},
"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>.",
"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>",
"saving_review": "Menyimpan…",
"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>.",
"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",
"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à!",
"plz_login": "Accedi per lasciare una recensione",
"posting_as": "Pubblica come",
"saved": "<span class=\"thanks\">Recensione salvata. Grazie per averla condivisa!</span>",
"saving_review": "Salvataggio…",
"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>で公開されます。",
"i_am_affiliated": "<span>わたしは、この対象物の関係者です</span><br><span class=\"subtle\">所有者、作成者、従業員などの有無を確認します</span>",
"name_required": "レビューを表示および作成するには名前が必要です",
"no_rating": "評価が与えられていません",
"no_reviews_yet": "まだレビューはありません。最初に書き込みを行い、データとビジネスのオープン化を支援しましょう!",
"plz_login": "ログインしてレビューを終了する",
"posting_as": "としての投稿",
"saved": "<span class=\"thanks\">レビューが保存されました。共有ありがとう!</span>",
"saving_review": "保存中…",
"title": "{count}個のレビュー",

View file

@ -6410,7 +6410,7 @@
}
}
},
"level": {
"single_level": {
"mappings": {
"0": {
"then": "Situat a planta subterrani"
@ -6543,6 +6543,27 @@
},
"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": {
"mappings": {
"0": {

View file

@ -1767,27 +1767,6 @@
"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>"
},
"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": {
"mappings": {
"0": {
@ -1897,6 +1876,27 @@
},
"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": {
"mappings": {
"0": {

View file

@ -2163,27 +2163,6 @@
"question": "Hvad er netværksnavnet for den trådløse internetadgang?",
"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": {
"override": {
"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?"
},
"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": {
"mappings": {
"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": {
"mappings": {
"0": {
@ -7481,6 +7460,27 @@
},
"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": {
"mappings": {
"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": {
"mappings": {
"0": {
@ -7524,6 +7503,9 @@
"phone": {
"question": "What is the phone number of {title()}?"
},
"repeated": {
"render": "Multiple, identical objects can be found on floors {repeat_on}."
},
"service:electricity": {
"mappings": {
"0": {
@ -7541,6 +7523,27 @@
},
"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": {
"mappings": {
"0": {

View file

@ -138,7 +138,13 @@
"email": {
"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": {
"1": {
"then": "En la teretaĝo"
@ -152,12 +158,6 @@
},
"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": {
"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?",
"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": {
"mappings": {
"0": {
@ -3764,6 +3743,27 @@
},
"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": {
"mappings": {
"0": {

View file

@ -34,27 +34,6 @@
"email": {
"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": {
"question": "Anong oras nagbubukas ang {title()}?",
"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?"
},
"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": {
"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": {
"mappings": {
"0": {
@ -4939,6 +4918,27 @@
},
"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": {
"mappings": {
"0": {

View file

@ -798,27 +798,6 @@
"email": {
"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": {
"question": "Mikor van nyitva ez: {title()}?",
"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?"
},
"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": {
"question": "Mi a weboldala ennek: {title()}?"
},

View file

@ -503,27 +503,6 @@
"question": "Apa nama jaringan internet nirkabelnya?",
"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": {
"override": {
"question": "Pada lantai berapa saja lift ini berjalan?",
@ -569,6 +548,27 @@
"phone": {
"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": {
"mappings": {
"0": {

View file

@ -1809,24 +1809,6 @@
"email": {
"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": {
"question": "Quali sono gli orari di apertura di {title()}?",
"render": "<h3>Orari di apertura</h3>{opening_hours_table(opening_hours)}"
@ -1854,6 +1836,24 @@
"phone": {
"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": {
"question": "Qual è il sito web di {title()}?"
},

View file

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

View file

@ -642,27 +642,6 @@
"question": "Hva er nettverksnavnet for det trådløse nettverket?",
"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": {
"override": {
"question": "Hvilke etasjer går heisen til?",
@ -755,6 +734,27 @@
},
"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": {
"mappings": {
"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": {
"override": {
"question": "Naar welke verdiepingen gaat deze lift?",
@ -6980,6 +6959,9 @@
"phone": {
"question": "Wat is het telefoonnummer van {title()}?"
},
"repeated": {
"render": "Er zijn verschillende, identieke objecten op verdiepingen {repeat_on}."
},
"service:electricity": {
"mappings": {
"0": {
@ -6997,6 +6979,27 @@
},
"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": {
"mappings": {
"0": {

View file

@ -2405,27 +2405,6 @@
"question": "Jaka jest nazwa sieci dla bezprzewodowego dostępu do Internetu?",
"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": {
"mappings": {
"0": {
@ -2535,6 +2514,27 @@
},
"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": {
"mappings": {
"0": {

View file

@ -882,27 +882,6 @@
"question": "Qual é o nome da rede para o acesso sem fios à Internet?",
"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": {
"override": {
"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?"
},
"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": {
"mappings": {
"0": {

View file

@ -628,24 +628,6 @@
"email": {
"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": {
"question": "Qual o horário de funcionamento de {title()}?",
"render": "<h3>Horário de funcionamento</h3>{opening_hours_table(opening_hours)}"
@ -664,6 +646,24 @@
"phone": {
"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": {
"question": "Qual o site de {title()}?"
},

View file

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

View file

@ -149,26 +149,6 @@
"email": {
"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": {
"question": "Kakšen odpiralni čas ima {title()}?",
"render": "<h3>Odpiralni čas</h3>{opening_hours_table(opening_hours)}"
@ -198,6 +178,26 @@
},
"phone": {
"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": {
"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": {
"question": "Vilka är öppettiderna för {title()}?",
"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?"
},
"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": {
"question": "Vad är webbplatsen för {title()}?"
},

View file

@ -628,27 +628,6 @@
"email": {
"question": "{title()} 的電子郵件地址是什麼?"
},
"level": {
"mappings": {
"0": {
"then": "位於地下"
},
"1": {
"then": "位於 1 樓"
},
"2": {
"then": "位於 1 樓"
},
"3": {
"then": "位於 2 樓"
},
"4": {
"then": "位於地下一樓"
}
},
"question": "此圖徽位於哪個樓層/層級?",
"render": "位於 {level} 樓"
},
"opening_hours": {
"question": "{title()} 的開放時間是什麼?",
"render": "<h3>開放時間</h3>{opening_hours_table(opening_hours)}"
@ -705,6 +684,27 @@
},
"question": "這個便利設施有電器設備,能給客戶使用嗎?"
},
"single_level": {
"mappings": {
"0": {
"then": "位於地下"
},
"1": {
"then": "位於 1 樓"
},
"2": {
"then": "位於 1 樓"
},
"3": {
"then": "位於 2 樓"
},
"4": {
"then": "位於地下一樓"
}
},
"question": "此圖徽位於哪個樓層/層級?",
"render": "位於 {level} 樓"
},
"website": {
"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>.",
"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",
"no_rating": "Ingen vurdering gitt",
"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>",
"saving_review": "Lagrer …",
"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>. ",
"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",
"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!",
"plz_login": "Meld je aan om een beoordeling te geven",
"posting_as": "Ingelogd als",
"save": "Opslaan",
"saved": "<span class='thanks'>Bedankt om je beoordeling te delen!</span>",
"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>.",
"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",
"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!",
"plz_login": "Zaloguj się, aby zostawić opinię",
"posting_as": "Publikowanie jako",
"save": "Zapisz",
"saved": "<span class=\"thanks\">Opinia została zapisana. Dzięki za udostępnienie!</span>",
"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>.",
"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",
"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!",
"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>",
"saving_review": "A guardar…",
"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>.",
"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",
"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!",
"plz_login": "Entrar para deixar um comentário",
"posting_as": "Postando como",
"saved": "<span class=\"thanks\">Comentário salvo. Obrigado por compartilhar!</span>",
"saving_review": "Salvando…",
"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>.",
"i_am_affiliated": "<span>Я связан с этим объектом</span><br><span class=\"subtle\"> Отметьте если вы создатель, владелец, работник, …</span>",
"name_required": "Необходимо название, чтобы просматривать и создавать отзывы",
"no_rating": "Нет рейтинга",
"no_reviews_yet": "Пока нет отзывов. Оставьте первый отзыв и помогите открытым данным и бизнесу!",
"plz_login": "Войдите, чтобы оставить отзыв",
"posting_as": "Публикация от имени",
"saved": "<span class=\"thanks\"> Отзыв сохранен. Спасибо, что поделились! </span>",
"saving_review": "Сохранение…",
"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>授權條款。",
"i_am_affiliated": "<span>我是這物件的相關關係者</span><br><span class=\"subtle\">確認你是否是擁有者、創造者、員工等等</span>",
"name_required": "需要有名稱才能顯示和創造審核",
"no_rating": "還沒有評分",
"no_reviews_yet": "還沒有審核,當第一個撰寫者來幫助開放資料與商家吧!",
"plz_login": "登入來留下審核",
"posting_as": "以貼文",
"saved": "<span class=\"thanks\">已儲存審核,謝謝你的分享!</span>",
"saving_review": "儲存中…",
"title": "{count} 審核次數",

View file

@ -1,6 +1,6 @@
{
"name": "mapcomplete",
"version": "0.33.4",
"version": "0.33.5",
"repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily",
"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;
}
.ml-3 {
margin-left: 0.75rem;
}
.-ml-6 {
margin-left: -1.5rem;
}
@ -1210,11 +1206,6 @@ video {
width: 2.5rem;
}
.w-max {
width: -webkit-max-content;
width: max-content;
}
.w-48 {
width: 12rem;
}
@ -1404,6 +1395,12 @@ video {
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]) {
--tw-space-y-reverse: 1;
}
@ -1478,11 +1475,6 @@ video {
text-overflow: clip;
}
.break-normal {
overflow-wrap: normal;
word-break: normal;
}
.break-all {
word-break: break-all;
}
@ -1555,14 +1547,14 @@ video {
border-width: 1px;
}
.border-4 {
border-width: 4px;
}
.border-2 {
border-width: 2px;
}
.border-4 {
border-width: 4px;
}
.border-x {
border-left-width: 1px;
border-right-width: 1px;
@ -1669,10 +1661,6 @@ video {
padding: 2rem;
}
.p-1 {
padding: 0.25rem;
}
.p-2 {
padding: 0.5rem;
}
@ -1681,6 +1669,10 @@ video {
padding: 1rem;
}
.p-1 {
padding: 0.25rem;
}
.p-0\.5 {
padding: 0.125rem;
}
@ -1773,10 +1765,6 @@ video {
text-align: justify;
}
.align-middle {
vertical-align: middle;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
@ -1787,16 +1775,6 @@ video {
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 {
font-size: 1.875rem;
line-height: 2.25rem;
@ -1807,11 +1785,21 @@ video {
line-height: 2rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.font-bold {
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);
}
.leading-none {
line-height: 1;
}
.tracking-tight {
letter-spacing: -0.025em;
}
@ -2662,26 +2646,6 @@ a.link-underline {
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) {
.max-\[480px\]\:w-full {
width: 100%;
@ -2816,10 +2780,6 @@ a.link-underline {
height: 4rem;
}
.md\:h-12 {
height: 3rem;
}
.md\:w-8 {
width: 2rem;
}

View file

@ -12,8 +12,8 @@ mkdir dist/assets 2> /dev/null
export NODE_OPTIONS="--max-old-space-size=8192"
# 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 &&
npm run generate:editor-layer-index &&
npm run generate &&
npm run generate:layouts
if [ $? -ne 0 ]; then

View file

@ -21,3 +21,4 @@ scp -r dist.zip 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"
rm dist.zip
npm run clean

View file

@ -339,21 +339,37 @@ export default class SimpleMetaTaggers {
)
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"],
},
(feature) => {
if (feature.properties["level"] === undefined) {
return false
let somethingChanged = 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"]
const newValue = TagUtils.LevelsParser(l).join(";")
if (l === newValue) {
return false
if (feature.properties["repeat_on"] !== undefined) {
const l = feature.properties["repeat_on"]
const newValue = TagUtils.LevelsParser(l).join(";")
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(

View file

@ -66,11 +66,11 @@ export default class LayerState {
}
const t = Translations.t.general.levelSelection
const conditionsOrred = [
new Tag("level", "" + level),
new RegexTag("level", new RegExp("(.*;)?" + level + "(;.*)?")),
new Tag("_level", "" + level),
new RegexTag("_level", new RegExp("(.*;)?" + level + "(;.*)?")),
]
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)
this.globalFilters.data.push({

View file

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

View file

@ -1,34 +1,35 @@
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
import { MangroveReviews, Review } from "mangrove-reviews-typescript"
import { Utils } from "../../Utils"
import { Feature, Position } from "geojson"
import { GeoOperations } from "../GeoOperations"
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource";
import { MangroveReviews, Review } from "mangrove-reviews-typescript";
import { Utils } from "../../Utils";
import { Feature, Position } from "geojson";
import { GeoOperations } from "../GeoOperations";
export class MangroveIdentity {
public readonly keypair: Store<CryptoKeyPair>
public readonly key_id: Store<string>
public readonly keypair: Store<CryptoKeyPair>;
public readonly key_id: Store<string>;
constructor(mangroveIdentity: UIEventSource<string>) {
const key_id = new UIEventSource<string>(undefined)
this.key_id = key_id
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
this.keypair = keypairEventSource
const key_id = new UIEventSource<string>(undefined);
this.key_id = key_id;
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined);
this.keypair = keypairEventSource;
mangroveIdentity.addCallbackAndRunD(async (data) => {
if (data === "") {
return
return;
}
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))
keypairEventSource.setData(keypair)
const pem = await MangroveReviews.publicToPem(keypair.publicKey)
key_id.setData(pem)
})
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data));
keypairEventSource.setData(keypair);
const pem = await MangroveReviews.publicToPem(keypair.publicKey);
key_id.setData(pem);
});
try {
if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") {
MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {})
MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {
});
}
} catch (e) {
console.error("Could not create identity: ", e)
console.error("Could not create identity: ", e);
}
}
@ -38,13 +39,13 @@ export class MangroveIdentity {
* @constructor
*/
private static async CreateIdentity(identity: UIEventSource<string>): Promise<void> {
const keypair = await MangroveReviews.generateKeypair()
const jwk = await MangroveReviews.keypairToJwk(keypair)
const keypair = await MangroveReviews.generateKeypair();
const jwk = await MangroveReviews.keypairToJwk(keypair);
if ((identity.data ?? "") !== "") {
// 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
*/
export default class FeatureReviews {
private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {}
public readonly subjectUri: Store<string>
private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {};
public readonly subjectUri: Store<string>;
public readonly average: Store<number | null>;
private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
new UIEventSource([])
new UIEventSource([]);
public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
this._reviews
private readonly _lat: number
private readonly _lon: number
private readonly _uncertainty: number
private readonly _name: Store<string>
private readonly _identity: MangroveIdentity
this._reviews;
private readonly _lat: number;
private readonly _lon: number;
private readonly _uncertainty: number;
private readonly _name: Store<string>;
private readonly _identity: MangroveIdentity;
private constructor(
feature: Feature,
@ -75,55 +77,72 @@ export default class FeatureReviews {
}
) {
const centerLonLat = GeoOperations.centerpointCoordinates(feature)
;[this._lon, this._lat] = centerLonLat
;[this._lon, this._lat] = centerLonLat;
this._identity =
mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined))
const nameKey = options?.nameKey ?? "name"
mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined));
const nameKey = options?.nameKey ?? "name";
if (feature.geometry.type === "Point") {
this._uncertainty = options?.uncertaintyRadius ?? 10
this._uncertainty = options?.uncertaintyRadius ?? 10;
} else {
let coordss: Position[][]
let coordss: Position[][];
if (feature.geometry.type === "LineString") {
coordss = [feature.geometry.coordinates]
coordss = [feature.geometry.coordinates];
} else if (
feature.geometry.type === "MultiLineString" ||
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 coord of coords) {
maxDistance = Math.max(
maxDistance,
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) => {
const reviews = await MangroveReviews.getReviews({ sub })
self.addReviews(reviews.reviews)
})
const reviews = await MangroveReviews.getReviews({ sub });
self.addReviews(reviews.reviews);
});
/* We also construct all subject queries _without_ encoding the name to work around a previous bug
* See https://github.com/giggls/opencampsitemap/issues/30
*/
this.ConstructSubjectUri(true).addCallbackAndRunD(async (sub) => {
try {
const reviews = await MangroveReviews.getReviews({ sub })
self.addReviews(reviews.reviews)
const reviews = await MangroveReviews.getReviews({ sub });
self.addReviews(reviews.reviews);
} 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
}
) {
const key = feature.properties.id
const cached = FeatureReviews._featureReviewsCache[key]
const key = feature.properties.id;
const cached = FeatureReviews._featureReviewsCache[key];
if (cached !== undefined) {
return cached
return cached;
}
const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options)
FeatureReviews._featureReviewsCache[key] = featureReviews
return featureReviews
const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options);
FeatureReviews._featureReviewsCache[key] = featureReviews;
return featureReviews;
}
/**
@ -155,15 +174,15 @@ export default class FeatureReviews {
public async createReview(review: Omit<Review, "sub">): Promise<void> {
const r: Review = {
sub: this.subjectUri.data,
...review,
}
const keypair: CryptoKeyPair = this._identity.keypair.data
console.log(r)
const jwt = await MangroveReviews.signReview(keypair, r)
console.log("Signed:", jwt)
await MangroveReviews.submitReview(jwt)
this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) })
this._reviews.ping()
...review
};
const keypair: CryptoKeyPair = this._identity.keypair.data;
console.log(r);
const jwt = await MangroveReviews.signReview(keypair, r);
console.log("Signed:", jwt);
await MangroveReviews.submitReview(jwt);
this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) });
this._reviews.ping();
}
/**
@ -172,46 +191,48 @@ export default class FeatureReviews {
* @private
*/
private addReviews(reviews: { payload: Review; kid: string }[]) {
const self = this
const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion))
const self = this;
const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion));
let hasNew = false
let hasNew = false;
for (const reviewData of reviews) {
const review = reviewData.payload
const review = reviewData.payload;
try {
const url = new URL(review.sub)
console.log("URL is", url)
const url = new URL(review.sub);
console.log("URL is", url);
if (url.protocol === "geo:") {
const coordinate = <[number, number]>(
url.pathname.split(",").map((n) => Number(n))
)
);
const distance = GeoOperations.distanceBetween(
[this._lat, this._lon],
coordinate
)
);
if (distance > this._uncertainty) {
continue
continue;
}
}
} catch (e) {
console.warn(e)
console.warn(e);
}
const key = review.rating + " " + review.opinion
const key = review.rating + " " + review.opinion;
if (alreadyKnown.has(key)) {
continue
continue;
}
self._reviews.data.push({
...review,
madeByLoggedInUser: this._identity.key_id.map((user_key_id) => {
return reviewData.kid === user_key_id
}),
})
hasNew = true
return reviewData.kid === user_key_id;
})
});
hasNew = true;
}
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> {
// 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
const self = this
return this._name.map(function (name) {
let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}`
const self = this;
return this._name.map(function(name) {
let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}`;
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 { SpecialVisualizationState } from "../UI/SpecialVisualization";
import { Changes } from "../Logic/Osm/Changes";
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 LayoutConfig from "./ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
import { Changes } from "../Logic/Osm/Changes"
import { Store, UIEventSource } from "../Logic/UIEventSource"
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";
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 { 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.
*/
export default class ThemeViewState implements SpecialVisualizationState {
readonly layout: LayoutConfig;
readonly map: UIEventSource<MlMap>;
readonly changes: Changes;
readonly featureSwitches: FeatureSwitchState;
readonly featureSwitchIsTesting: Store<boolean>;
readonly featureSwitchUserbadge: Store<boolean>;
readonly layout: LayoutConfig
readonly map: UIEventSource<MlMap>
readonly changes: Changes
readonly featureSwitches: FeatureSwitchState
readonly featureSwitchIsTesting: Store<boolean>
readonly featureSwitchUserbadge: Store<boolean>
readonly featureProperties: FeaturePropertiesStore;
readonly featureProperties: FeaturePropertiesStore
readonly osmConnection: OsmConnection;
readonly selectedElement: UIEventSource<Feature>;
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>;
readonly mapProperties: MapProperties & ExportableMap;
readonly osmObjectDownloader: OsmObjectDownloader;
readonly osmConnection: OsmConnection
readonly selectedElement: UIEventSource<Feature>
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
readonly mapProperties: MapProperties & ExportableMap
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
*/
readonly hasDataInView: Store<FeatureViewState>;
readonly hasDataInView: Store<FeatureViewState>
readonly guistate: MenuState;
readonly fullNodeDatabase?: FullNodeDatabaseSource;
readonly guistate: MenuState
readonly fullNodeDatabase?: FullNodeDatabaseSource
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>;
readonly indexedFeatures: IndexedFeatureSource & LayoutSource;
readonly currentView: FeatureSource<Feature<Polygon>>;
readonly featuresInView: FeatureSource;
readonly newFeatures: WritableFeatureSource;
readonly layerState: LayerState;
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>;
readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>;
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
readonly currentView: FeatureSource<Feature<Polygon>>
readonly featuresInView: FeatureSource
readonly newFeatures: WritableFeatureSource
readonly layerState: LayerState
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>
readonly availableLayers: Store<RasterLayerPolygon[]>;
readonly selectedLayer: UIEventSource<LayerConfig>;
readonly userRelatedState: UserRelatedState;
readonly geolocation: GeoLocationHandler;
readonly availableLayers: Store<RasterLayerPolygon[]>
readonly selectedLayer: UIEventSource<LayerConfig>
readonly userRelatedState: UserRelatedState
readonly geolocation: GeoLocationHandler
readonly imageUploadManager: ImageUploadManager
readonly lastClickObject: WritableFeatureSource;
readonly lastClickObject: WritableFeatureSource
readonly overlayLayerStates: ReadonlyMap<
string,
{ readonly isDisplayed: UIEventSource<boolean> }
>;
>
/**
* All 'level'-tags that are available with the current features
*/
readonly floors: Store<string[]>;
readonly floors: Store<string[]>
constructor(layout: LayoutConfig) {
Utils.initDomPurify();
this.layout = layout;
this.featureSwitches = new FeatureSwitchState(layout);
Utils.initDomPurify()
this.layout = layout
this.featureSwitches = new FeatureSwitchState(layout)
this.guistate = new MenuState(
this.featureSwitches.featureSwitchWelcomeMessage.data,
layout.id
);
this.map = new UIEventSource<MlMap>(undefined);
const initial = new InitialMapPositioning(layout);
this.mapProperties = new MapLibreAdaptor(this.map, initial);
const geolocationState = new GeoLocationState();
)
this.map = new UIEventSource<MlMap>(undefined)
const initial = new InitialMapPositioning(layout)
this.mapProperties = new MapLibreAdaptor(this.map, initial)
const geolocationState = new GeoLocationState()
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting;
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin;
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
this.osmConnection = new OsmConnection({
dryRun: this.featureSwitches.featureSwitchIsTesting,
@ -137,68 +141,66 @@ export default class ThemeViewState implements SpecialVisualizationState {
undefined,
"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.osmConnection,
layout?.language,
layout,
this.featureSwitches,
this.mapProperties
);
)
this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
this.mapProperties.allowRotating.setData(fixated !== "yes");
});
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element");
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer");
this.mapProperties.allowRotating.setData(fixated !== "yes")
})
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer")
this.selectedElementAndLayer = this.selectedElement.mapD(
(feature) => {
const layer = this.selectedLayer.data;
const layer = this.selectedLayer.data
if (!layer) {
return undefined;
return undefined
}
return { layer, feature };
return { layer, feature }
},
[this.selectedLayer]
);
)
this.geolocation = new GeoLocationHandler(
geolocationState,
this.selectedElement,
this.mapProperties,
this.userRelatedState.gpsLocationHistoryRetentionTime
);
)
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location);
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
const self = this;
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id);
const self = this
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) {
const isDisplayed = QueryParameters.GetBooleanQueryParameter(
"overlay-" + rasterInfo.id,
rasterInfo.defaultState ?? true,
"Wether or not overlayer layer " + rasterInfo.id + " is shown"
);
const state = { isDisplayed };
overlayLayerStates.set(rasterInfo.id, state);
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state);
)
const state = { isDisplayed }
overlayLayerStates.set(rasterInfo.id, state)
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state)
}
this.overlayLayerStates = overlayLayerStates;
this.overlayLayerStates = overlayLayerStates
}
{
/* 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
*/
if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) {
this.fullNodeDatabase = new FullNodeDatabaseSource();
this.fullNodeDatabase = new FullNodeDatabaseSource()
}
const layoutSource = new LayoutSource(
@ -208,49 +210,49 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.osmConnection.Backend(),
(id) => self.layerState.filteredLayers.get(id).isDisplayed,
this.fullNodeDatabase
);
)
this.indexedFeatures = layoutSource;
this.indexedFeatures = layoutSource
const empty = [];
let currentViewIndex = 0;
const empty = []
let currentViewIndex = 0
this.currentView = new StaticFeatureSource(
this.mapProperties.bounds.map((bbox) => {
if (!bbox) {
return empty;
return empty
}
currentViewIndex++;
currentViewIndex++
return <Feature[]>[
bbox.asGeoJson({
zoom: this.mapProperties.zoom.data,
...this.mapProperties.location.data,
id: "current_view"
})
];
id: "current_view",
}),
]
})
);
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds);
this.dataIsLoading = layoutSource.isLoading;
)
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
this.dataIsLoading = layoutSource.isLoading
const indexedElements = this.indexedFeatures;
this.featureProperties = new FeaturePropertiesStore(indexedElements);
const indexedElements = this.indexedFeatures
this.featureProperties = new FeaturePropertiesStore(indexedElements)
this.changes = new Changes(
{
dryRun: this.featureSwitches.featureSwitchIsTesting,
allElements: indexedElements,
featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations
historicalUserLocations: this.geolocation.historicalUserLocations,
},
layout?.isLeftRightSensitive() ?? false
);
this.historicalUserLocations = this.geolocation.historicalUserLocations;
)
this.historicalUserLocations = this.geolocation.historicalUserLocations
this.newFeatures = new NewGeometryFromChangesFeatureSource(
this.changes,
indexedElements,
this.featureProperties
);
layoutSource.addSource(this.newFeatures);
)
layoutSource.addSource(this.newFeatures)
const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()).filter(
@ -266,11 +268,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
features.length,
"leftover features, such as",
features[0].properties
);
}
)
},
}
);
this.perLayer = perLayer.perLayer;
)
this.perLayer = perLayer.perLayer
}
this.perLayer.forEach((fs) => {
new SaveFeatureSourceToLocalStorage(
@ -280,74 +282,80 @@ export default class ThemeViewState implements SpecialVisualizationState {
fs,
this.featureProperties,
fs.layer.layerDef.maxAgeOfCache
);
});
)
})
this.floors = this.featuresInView.features.stabilized(500).map((features) => {
if (!features) {
return [];
return []
}
const floors = new Set<string>();
const floors = new Set<string>()
for (const feature of features) {
const level = feature.properties["level"];
let level = feature.properties["_level"]
if (level) {
const levels = level.split(";");
const levels = level.split(";")
for (const l of levels) {
floors.add(l);
floors.add(l)
}
} 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"
sorted.sort();
sorted.sort()
sorted.sort((a, b) => {
// We use the laxer 'parseInt' to deal with floor '1A'
const na = parseInt(a);
const nb = parseInt(b);
const na = parseInt(a)
const nb = parseInt(b)
if (isNaN(na) || isNaN(nb)) {
return 0;
return 0
}
return na - nb;
});
sorted.reverse(/* new list, no side-effects */);
return sorted;
});
return na - nb
})
sorted.reverse(/* new list, no side-effects */)
return sorted
})
const lastClick = (this.lastClickObject = new LastClickFeatureSource(
this.mapProperties.lastClickLocation,
this.layout
));
))
this.osmObjectDownloader = new OsmObjectDownloader(
this.osmConnection.Backend(),
this.changes
);
)
this.perLayerFiltered = this.showNormalDataOn(this.map);
this.perLayerFiltered = this.showNormalDataOn(this.map)
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView;
this.imageUploadManager = new ImageUploadManager(layout, Imgur.singleton, this.featureProperties, this.osmConnection, this.changes)
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
this.imageUploadManager = new ImageUploadManager(
layout,
Imgur.singleton,
this.featureProperties,
this.osmConnection,
this.changes
)
this.initActors();
this.addLastClick(lastClick);
this.drawSpecialLayers();
this.initHotkeys();
this.miscSetup();
this.initActors()
this.addLastClick(lastClick)
this.drawSpecialLayers()
this.initHotkeys()
this.miscSetup()
if (!Utils.runningFromConsole) {
console.log("State setup completed", this);
console.log("State setup completed", this)
}
}
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) => {
const doShowLayer = this.mapProperties.zoom.map(
(z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed]
);
)
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)
@ -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.
* 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(
fs.layer,
fs,
(id) => this.featureProperties.getStore(id),
this.layerState.globalFilters
);
filteringFeatureSource.set(layerName, filtered);
)
filteringFeatureSource.set(layerName, filtered)
new ShowDataLayer(map, {
layer: fs.layer.layerDef,
@ -373,30 +381,30 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
fetchStore: (id) => this.featureProperties.getStore(id)
});
});
return filteringFeatureSource;
fetchStore: (id) => this.featureProperties.getStore(id),
})
})
return filteringFeatureSource
}
/**
* Various small methods that need to be called
*/
private miscSetup() {
this.userRelatedState.markLayoutAsVisited(this.layout);
this.userRelatedState.markLayoutAsVisited(this.layout)
this.selectedElement.addCallbackAndRunD((feature) => {
// 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
// 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") {
return;
return
}
this.lastClickObject.features.setData([]);
});
this.lastClickObject.features.setData([])
})
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 },
Translations.t.hotkeyDocumentation.closeSidebar,
() => {
this.selectedElement.setData(undefined);
this.guistate.closeAll();
this.selectedElement.setData(undefined)
this.guistate.closeAll()
}
);
)
Hotkeys.RegisterHotkey(
{
nomod: "b"
nomod: "b",
},
Translations.t.hotkeyDocumentation.openLayersPanel,
() => {
if (this.featureSwitches.featureSwitchFilter.data) {
this.guistate.openFilterView();
this.guistate.openFilterView()
}
}
);
)
Hotkeys.RegisterHotkey(
{ shift: "O" },
Translations.t.hotkeyDocumentation.selectMapnik,
() => {
this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto);
this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto)
}
);
)
const setLayerCategory = (category: EliCategory) => {
const available = this.availableLayers.data;
const current = this.mapProperties.rasterLayer;
const available = this.availableLayers.data
const current = this.mapProperties.rasterLayer
const best = RasterLayerUtils.SelectBestLayerAccordingTo(
available,
category,
current.data
);
console.log("Best layer for category", category, "is", best.properties.id);
current.setData(best);
};
)
console.log("Best layer for category", category, "is", best.properties.id)
current.setData(best)
}
Hotkeys.RegisterHotkey(
{ nomod: "O" },
Translations.t.hotkeyDocumentation.selectOsmbasedmap,
() => setLayerCategory("osmbasedmap")
);
)
Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () =>
setLayerCategory("map")
);
)
Hotkeys.RegisterHotkey(
{ nomod: "P" },
Translations.t.hotkeyDocumentation.selectAerial,
() => setLayerCategory("photo")
);
)
}
private addLastClick(last_click: LastClickFeatureSource) {
// The last_click gets a _very_ special treatment as it interacts with various parts
const last_click_layer = this.layerState.filteredLayers.get("last_click");
this.featureProperties.trackFeatureSource(last_click);
this.indexedFeatures.addSource(last_click);
const last_click_layer = this.layerState.filteredLayers.get("last_click")
this.featureProperties.trackFeatureSource(last_click)
this.indexedFeatures.addSource(last_click)
last_click.features.addCallbackAndRunD((features) => {
if (this.selectedLayer.data?.id === "last_click") {
// The last-click location moved, but we have selected the last click of the previous location
// So, we update _after_ clearing the selection to make sure no stray data is sticking around
this.selectedElement.setData(undefined);
this.selectedElement.setData(features[0]);
this.selectedElement.setData(undefined)
this.selectedElement.setData(features[0])
}
});
})
new ShowDataLayer(this.map, {
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) {
this.map.data.flyTo({
zoom: Constants.minZoomLevelToAddNewPoint,
center: this.mapProperties.lastClickLocation.data
});
return;
center: this.mapProperties.lastClickLocation.data,
})
return
}
// We first clear the selection to make sure no weird state is around
this.selectedLayer.setData(undefined);
this.selectedElement.setData(undefined);
this.selectedLayer.setData(undefined)
this.selectedElement.setData(undefined)
this.selectedElement.setData(feature);
this.selectedLayer.setData(last_click_layer.layerDef);
}
});
this.selectedElement.setData(feature)
this.selectedLayer.setData(last_click_layer.layerDef)
},
})
}
/**
@ -503,7 +511,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
*/
private drawSpecialLayers() {
type AddedByDefaultTypes = (typeof Constants.added_by_default)[number]
const empty = [];
const empty = []
/**
* 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" })]
)
),
current_view: this.currentView
};
current_view: this.currentView,
}
if (this.layout?.lockLocation) {
const bbox = new BBox(this.layout.lockLocation);
this.mapProperties.maxbounds.setData(bbox);
const bbox = new BBox(this.layout.lockLocation)
this.mapProperties.maxbounds.setData(bbox)
ShowDataLayer.showRange(
this.map,
new StaticFeatureSource([bbox.asGeoJson({})]),
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) {
const params = MetaTagging.createExtraFuncParams(this);
this.featureProperties.trackFeatureSource(specialLayers.current_view);
const params = MetaTagging.createExtraFuncParams(this)
this.featureProperties.trackFeatureSource(specialLayers.current_view)
specialLayers.current_view.features.addCallbackAndRunD((features) => {
MetaTagging.addMetatags(
features,
@ -546,37 +554,37 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.layout,
this.osmObjectDownloader,
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 (
!QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef))
) {
rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true);
rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
}
this.layerState.filteredLayers.forEach((flayer) => {
const id = flayer.layerDef.id;
const features: FeatureSource = specialLayers[id];
const id = flayer.layerDef.id
const features: FeatureSource = specialLayers[id]
if (features === undefined) {
return;
return
}
this.featureProperties.trackFeatureSource(features);
this.featureProperties.trackFeatureSource(features)
// this.indexedFeatures.addSource(features)
new ShowDataLayer(this.map, {
features,
doShowLayer: flayer.isDisplayed,
layer: flayer.layerDef,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer
});
});
selectedLayer: this.selectedLayer,
})
})
}
/**
@ -585,30 +593,35 @@ export default class ThemeViewState implements SpecialVisualizationState {
private initActors() {
// Unselect the selected element if it is panned out of view
this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => {
const selected = this.selectedElement.data;
const selected = this.selectedElement.data
if (selected === undefined) {
return;
return
}
const bbox = BBox.get(selected);
const bbox = BBox.get(selected)
if (!bbox.overlapsWith(bounds)) {
this.selectedElement.setData(undefined);
this.selectedElement.setData(undefined)
}
});
})
this.selectedElement.addCallback((selected) => {
if (selected === undefined) {
// We did _unselect_ an item - we always remove the lastclick-object
this.lastClickObject.features.setData([]);
this.selectedLayer.setData(undefined);
this.lastClickObject.features.setData([])
this.selectedLayer.setData(undefined)
}
});
new ThemeViewStateHashActor(this);
new MetaTagging(this);
new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this);
new ChangeToElementsActor(this.changes, this.featureProperties);
new PendingChangesUploader(this.changes, this.selectedElement);
new SelectedElementTagsUpdater(this);
new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers);
new PreferredRasterLayerSelector(this.mapProperties.rasterLayer, this.availableLayers, this.featureSwitches.backgroundLayerId, this.userRelatedState.preferredBackgroundLayer)
})
new ThemeViewStateHashActor(this)
new MetaTagging(this)
new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this)
new ChangeToElementsActor(this.changes, this.featureProperties)
new PendingChangesUploader(this.changes, this.selectedElement)
new SelectedElementTagsUpdater(this)
new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers)
new PreferredRasterLayerSelector(
this.mapProperties.rasterLayer,
this.availableLayers,
this.featureSwitches.backgroundLayerId,
this.userRelatedState.preferredBackgroundLayer
)
}
}

View file

@ -1,12 +1,14 @@
<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
*/
export let selected: UIEventSource<boolean>
let _c: boolean = selected.data ?? true
$: selected.setData(_c)
export let selected: Writable<boolean>;
let _c: boolean = selected.data ?? true;
$: selected.set(_c);
</script>
<input type="checkbox" bind:checked={_c} />
<label class="no-image-background flex gap-1">
<input bind:checked={_c} type="checkbox" />
<slot />
</label>

View file

@ -55,27 +55,26 @@
{#if filteredLayer.layerDef.name}
<div bind:this={mainElem} class="mb-1.5">
<label class="no-image-background flex gap-1">
<Checkbox selected={isDisplayed} />
<If condition={filteredLayer.isDisplayed}>
<ToSvelte
construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background")}
/>
<ToSvelte
slot="else"
construct={() =>
<Checkbox selected={isDisplayed} >
<If condition={filteredLayer.isDisplayed}>
<ToSvelte
construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background")}
/>
<ToSvelte
slot="else"
construct={() =>
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">
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} />
</span>
{/if}
</label>
{/if}
</Checkbox>
{#if $isDisplayed && filteredLayer.layerDef.filters?.length > 0}
<div id="subfilters" class="ml-4 flex flex-col gap-y-1">
@ -83,10 +82,9 @@
<div>
<!-- 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}
<label>
<Checkbox selected={getBooleanStateFor(filter)} />
{filter.options[0].question}
</label>
<Checkbox selected={getBooleanStateFor(filter)} >
{filter.options[0].question}
</Checkbox>
{/if}
{#if filter.options.length === 1 && filter.options[0].fields.length > 0}

View file

@ -46,7 +46,7 @@
>
{#each layer.titleIcons as titleIconConfig}
{#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
config={titleIconConfig}
{tags}

View file

@ -71,14 +71,20 @@
{:else if $geopermission === "requested"}
<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 -->
<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} />
</button>
{:else if $geopermission !== "denied"}
{:else if $geopermission === "denied"}
<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} />
</button>
{/if}
<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 { Translation } from "./i18n/Translation";
import Translations from "./i18n/Translations";
import ReviewForm from "./Reviews/ReviewForm";
import ReviewElement from "./Reviews/ReviewElement";
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization";
import LiveQueryHandler from "../Logic/Web/LiveQueryHandler";
import { SubtleButton } from "./Base/SubtleButton";
@ -66,6 +64,9 @@ import SendEmail from "./Popup/SendEmail.svelte";
import NearbyImages from "./Popup/NearbyImages.svelte";
import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.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 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",
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",
@ -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",
},
],
constr: (state, tags, args, feature) => {
constr: (state, tags, args, feature, layer) => {
const nameKey = args[0] ?? "name"
let fallbackName = args[1]
const mangrove = FeatureReviews.construct(
const reviews = FeatureReviews.construct(
feature,
tags,
state.userRelatedState.mangroveIdentity,
@ -651,9 +711,7 @@ export default class SpecialVisualizations {
fallbackName,
}
)
const form = new ReviewForm((r) => mangrove.createReview(r), state)
return new ReviewElement(mangrove, form)
return new SvelteUIElement(AllReviews, {reviews, state, tags, feature, layer})
},
},
{

View file

@ -244,7 +244,7 @@ export class Translation extends BaseUIElement {
continue
}
let txt = this.translations[lng]
txt = txt.replace(/(\.|<br\/>|<br>).*/, "")
txt = txt.replace(/(\.|<br\/>|<br>|。).*/, "")
txt = Utils.EllipsesAfter(txt, 255)
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 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())
const state = new ThemeViewState(new LayoutConfig(<any> layout))
const main = new SvelteUIElement(ThemeViewGUI, { state })
main.AttachTo("maindiv")
if (!webgl_support()) {
new FixedUiElement("WebGL is not supported or not enabled. This is essential for MapComplete to function, please enable this.").SetClass("block alert").AttachTo("maindiv")
}else{
MetaTagging.setThemeMetatagging(new ThemeMetaTagging())
const state = new ThemeViewState(new LayoutConfig(<any> layout))
const main = new SvelteUIElement(ThemeViewGUI, { state })
main.AttachTo("maindiv")
}