Merge branch 'develop'

This commit is contained in:
Pieter Vander Vennet 2023-09-25 13:00:49 +02:00
commit 7ace25d377
161 changed files with 3351 additions and 2461 deletions

View file

@ -19,7 +19,7 @@ runs:
shell: bash shell: bash
- name: REUSE compliance check - name: REUSE compliance check
uses: fsfe/reuse-action@v2 uses: fsfe/reuse-action@952281636420dd0b691786c93e9d3af06032f138
- name: create generated dir - name: create generated dir
run: mkdir ./assets/generated run: mkdir ./assets/generated

View file

@ -89,7 +89,7 @@ jobs:
env: env:
TARGET_BRANCH: ${{ env.TARGET_BRANCH }} TARGET_BRANCH: ${{ env.TARGET_BRANCH }}
- uses: mshick/add-pr-comment@v1 - uses: mshick/add-pr-comment@a96c578acba98b60f16c6866d5f20478dc4ef68b
name: Comment the PR with the review URL name: Comment the PR with the review URL
if: ${{ success() && github.ref != 'refs/heads/develop' && github.ref != 'refs/heads/master' }} if: ${{ success() && github.ref != 'refs/heads/develop' && github.ref != 'refs/heads/master' }}
with: with:

View file

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport"> <meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://gc.zgo.at/; img-src *; connect-src 'self' https://www.openstreetmap.org/ https://api.openstreetmap.org/;">
<link href="./css/mobile.css" rel="stylesheet"/> <link href="./css/mobile.css" rel="stylesheet"/>
<link href="./css/tagrendering.css" rel="stylesheet"/> <link href="./css/tagrendering.css" rel="stylesheet"/>
<link href="./css/index-tailwind-output.css" rel="stylesheet"/> <link href="./css/index-tailwind-output.css" rel="stylesheet"/>

View file

@ -890,6 +890,9 @@
"mappings": [ "mappings": [
{ {
"if": "tourism=artwork", "if": "tourism=artwork",
"addExtraTags": [
"not:tourism:artwork="
],
"then": { "then": {
"en": "This bench has an integrated artwork", "en": "This bench has an integrated artwork",
"nl": "Deze bank heeft een geïntegreerd kunstwerk", "nl": "Deze bank heeft een geïntegreerd kunstwerk",
@ -902,7 +905,7 @@
} }
}, },
{ {
"if": "tourism=", "if": "not:tourism:artwork=yes",
"then": { "then": {
"en": "This bench does not have an integrated artwork", "en": "This bench does not have an integrated artwork",
"nl": "Deze bank heeft geen geïntegreerd kunstwerk", "nl": "Deze bank heeft geen geïntegreerd kunstwerk",
@ -913,7 +916,18 @@
"cs": "Tato lavička nemá integrované umělecké dílo", "cs": "Tato lavička nemá integrované umělecké dílo",
"he": "לספסל זה אין יצירת אמנות משולבת", "he": "לספסל זה אין יצירת אמנות משולבת",
"pl": "Ta ławka nie ma wbudowanego dzieła sztuki" "pl": "Ta ławka nie ma wbudowanego dzieła sztuki"
} },
"addExtraTags": [
"tourism="
]
},
{
"if": "tourism=",
"then": {
"en": "This bench <span class=\"subtle\">probably</span> doesn't have an integrated artwork",
"nl": "Deze bank heeft <span class=\"subtle\">waarschijnlijk</span> geen geïntegreerd kunstwerk"
},
"hideInAnswer": true
} }
], ],
"questionHint": { "questionHint": {

View file

@ -363,5 +363,6 @@
"fr": "Un vélo café est un café à destination des cyclistes avec, par exemple, des services tels quune pompe, et de nombreuses décorations liées aux vélos, etc.", "fr": "Un vélo café est un café à destination des cyclistes avec, par exemple, des services tels quune pompe, et de nombreuses décorations liées aux vélos, etc.",
"cs": "Cyklokavárna je kavárna zaměřená na cyklisty, například se službami, jako je pumpa, se spoustou výzdoby související s jízdními koly, …", "cs": "Cyklokavárna je kavárna zaměřená na cyklisty, například se službami, jako je pumpa, se spoustou výzdoby související s jízdními koly, …",
"ca": "Un cafè ciclista és un cafè enfocat a ciclistes, per exemple, amb serveis com una manxa, amb molta decoració relacionada amb el ciclisme, …" "ca": "Un cafè ciclista és un cafè enfocat a ciclistes, per exemple, amb serveis com una manxa, amb molta decoració relacionada amb el ciclisme, …"
} },
"deletion": true
} }

View file

@ -1,14 +0,0 @@
{
"id": "matchpoint",
"description": "The default rendering for a locationInput which snaps onto another object",
"source": "special",
"mapRendering": [
{
"location": [
"point",
"centroid"
],
"icon": "./assets/svg/crosshair-empty.svg"
}
]
}

View file

@ -23,7 +23,7 @@
"osmTags": "amenity=recycling" "osmTags": "amenity=recycling"
}, },
"calculatedTags": [ "calculatedTags": [
"_waste_amount=Object.values(Object.keys(feat.properties).filter((key) => key.startsWith('recycling:')).reduce((cur, key) => { return Object.assign(cur, { [key]: feat.properties[key] })}, {})).reduce((n, x) => n + (x == \"yes\"), 0);" "_waste_amount=Object.keys(feat.properties).filter(key => key.startsWith('recycling:')).filter(k => feat.properties[k] === 'yes').length"
], ],
"minzoom": 10, "minzoom": 10,
"title": { "title": {

View file

@ -20,7 +20,7 @@
} }
}, },
"calculatedTags": [ "calculatedTags": [
"_enclosing=feat.enclosingFeatures('school').map(f => f.feat.properties.id)", "_enclosing=enclosingFeatures(feat)('school').map(f => f.feat.properties.id)",
"_is_enclosed=feat.properties._enclosing != '[]'" "_is_enclosed=feat.properties._enclosing != '[]'"
], ],
"isShown": { "isShown": {

View file

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="500"
height="500"
viewBox="0 0 132.29166 132.29167"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="ALPR.svg"
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">
<sodipodi:namedview
id="namedview7"
pagecolor="#b1a245"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:document-units="mm"
showgrid="false"
units="px"
width="500px"
inkscape:object-paths="true"
inkscape:snap-smooth-nodes="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="false"
inkscape:zoom="1.4442101"
inkscape:cx="179.33679"
inkscape:cy="233.69176"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
id="circle5562"
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.15875,0.15875;stroke-dashoffset:0;stroke-opacity:1"
d="M 29.428339,95.048375 A 28.373579,28.373579 0 0 1 1.0546228,66.674664 28.373579,28.373579 0 0 1 29.428339,38.302588 a 28.373579,28.373579 0 0 1 0.573442,0.02143 v -0.01156 h 72.311059 a 28.373579,28.373579 0 0 1 0.26529,-0.0099 28.373579,28.373579 0 0 1 0.2653,0.0099 h 2.27563 v 0.116999 a 28.373579,28.373579 0 0 1 25.83279,28.245193 28.373579,28.373579 0 0 1 -25.83279,28.258373 v 0.08733 h -1.29518 a 28.373579,28.373579 0 0 1 -1.24575,0.02802 28.373579,28.373579 0 0 1 -0.75141,-0.02802 H 30.675739 a 28.373579,28.373579 0 0 1 -1.2474,0.02802 z" />
<circle
style="fill:none;stroke:#ffffff;stroke-width:5.51088;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.551088, 0.551088;stroke-dashoffset:0;stroke-opacity:1"
id="circle4462"
cx="103.66268"
cy="-66.675468"
r="17.653652"
transform="scale(1,-1)" />
<circle
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:5.51112;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.551112, 0.551112;stroke-dashoffset:0;stroke-opacity:1"
id="circle4544"
cx="28.500475"
cy="-66.675468"
r="17.653652"
transform="scale(1,-1)" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:6.89558;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.689558, 0.689558;stroke-dashoffset:0;stroke-opacity:1"
id="path4884"
cx="52.892296"
cy="-48.590332"
r="5.0460854"
transform="scale(1,-1)" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:6.89558;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.689558, 0.689558;stroke-dashoffset:0;stroke-opacity:1"
id="circle4966"
cx="65.282982"
cy="-48.590332"
r="5.0460854"
transform="scale(1,-1)" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:6.89558;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.689558, 0.689558;stroke-dashoffset:0;stroke-opacity:1"
id="circle4968"
cx="77.673676"
cy="-48.590332"
r="5.0460854"
transform="scale(1,-1)" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:6.89558;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.689558, 0.689558;stroke-dashoffset:0;stroke-opacity:1"
id="circle5142"
cx="52.892296"
cy="-48.590332"
r="5.0460854"
transform="scale(1,-1)" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:6.89558;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.689558, 0.689558;stroke-dashoffset:0;stroke-opacity:1"
id="circle5144"
cx="65.282982"
cy="-48.590332"
r="5.0460854"
transform="scale(1,-1)" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:6.89558;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.689558, 0.689558;stroke-dashoffset:0;stroke-opacity:1"
id="circle5146"
cx="77.673676"
cy="-48.590332"
r="5.0460854"
transform="scale(1,-1)" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:6.89558;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.689558, 0.689558;stroke-dashoffset:0;stroke-opacity:1"
id="circle5148"
cx="59.221966"
cy="-59.741222"
r="5.0460854"
transform="scale(1,-1)" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:6.89558;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.689558, 0.689558;stroke-dashoffset:0;stroke-opacity:1"
id="circle5150"
cx="71.612656"
cy="-59.741222"
r="5.0460854"
transform="scale(1,-1)" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:6.89558;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.689558, 0.689558;stroke-dashoffset:0;stroke-opacity:1"
id="circle5576"
cx="65.282982"
cy="70.004204"
r="5.0460854" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: synx508
SPDX-License-Identifier: CC-BY-NC 2.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: https://commons.wikimedia.org/wiki/User:Mbrickn
SPDX-License-Identifier: CC-BY 4.0

View file

@ -0,0 +1,30 @@
[
{
"path": "ALPR.svg",
"license": "CC0-1.0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
},
{
"path": "ALPR_Example.jpg",
"license": "CC-BY-NC 2.0",
"authors": [
"synx508"
],
"sources": [
"https://www.flickr.com/photos/synx508/5742253934/"
]
},
{
"path": "ALPR_Example2.jpg",
"license": "CC-BY 4.0",
"authors": [
"https://commons.wikimedia.org/wiki/User:Mbrickn"
],
"sources": [
"https://commons.wikimedia.org/wiki/File:ANPR_Camera_Front.jpg"
]
}
]

View file

@ -40,6 +40,32 @@
}, },
"tagRenderings": [ "tagRenderings": [
"images", "images",
{
"id": "has_alpr",
"question": {
"en": "Can this camera automatically detect license plates?"
},
"questionHint": {
"en": "An <b>ALPR</b> (Automatic License Plate Reader) typically has two lenses and an array of infrared LEDS in between."
},
"mappings": [
{
"if": "surveillance:type=camera",
"then": {
"en": "This is a camera without number plate recognition."
}
},
{
"if": "surveillance:type=ALPR",
"then": {
"en": "This is an ALPR (Automatic License Plate Reader)"
},
"icon": {
"path": "./assets/layers/surveillance_camera/ALPR.svg"
}
}
]
},
{ {
"question": { "question": {
"en": "What kind of camera is this?", "en": "What kind of camera is this?",
@ -53,11 +79,7 @@
}, },
"mappings": [ "mappings": [
{ {
"if": { "if": "camera:type=fixed",
"and": [
"camera:type=fixed"
]
},
"then": { "then": {
"en": "A fixed (non-moving) camera", "en": "A fixed (non-moving) camera",
"nl": "Een vaste camera", "nl": "Een vaste camera",
@ -66,14 +88,11 @@
"de": "Eine fest montierte (nicht bewegliche) Kamera", "de": "Eine fest montierte (nicht bewegliche) Kamera",
"ca": "Una càmera fixa (no movible)", "ca": "Una càmera fixa (no movible)",
"es": "Cámara fija (no móvil)" "es": "Cámara fija (no móvil)"
} },
"icon": "./assets/themes/surveillance/cam_right.svg"
}, },
{ {
"if": { "if": "camera:type=dome",
"and": [
"camera:type=dome"
]
},
"then": { "then": {
"en": "A dome camera (which can turn)", "en": "A dome camera (which can turn)",
"nl": "Een dome (bolvormige camera die kan draaien)", "nl": "Een dome (bolvormige camera die kan draaien)",
@ -83,7 +102,8 @@
"de": "Eine Kuppelkamera (drehbar)", "de": "Eine Kuppelkamera (drehbar)",
"ca": "Càmera de cúpula (que pot girar)", "ca": "Càmera de cúpula (que pot girar)",
"es": "Cámara con domo (que se puede girar)" "es": "Cámara con domo (que se puede girar)"
} },
"icon": "./assets/themes/surveillance/dome.svg"
}, },
{ {
"if": { "if": {
@ -595,6 +615,40 @@
"fr": "une caméra de surveillance fixée au mur" "fr": "une caméra de surveillance fixée au mur"
}, },
"snapToLayer": "walls_and_buildings" "snapToLayer": "walls_and_buildings"
},
{
"tags": [
"man_made=surveillance",
"surveillance:type=ALPR"
],
"title": {
"en": "an ALPR camera (Automatic Number Plate Reader)"
},
"description": {
"en": "An ALPR typically has two lenses and an array of infrared lights."
},
"exampleImages": [
"./assets/layers/surveillance_camera/ALPR_Example.jpg",
"./assets/layers/surveillance_camera/ALPR_Example2.jpg"
]
},
{
"tags": [
"man_made=surveillance",
"surveillance:type=ALPR",
"camera:mount=wall"
],
"title": {
"en": "an ALPR camera (Automatic Number Plate Reader) mounted on a wall"
},
"description": {
"en": "An ALPR typically has two lenses and an array of infrared lights."
},
"exampleImages": [
"./assets/layers/surveillance_camera/ALPR_Example.jpg",
"./assets/layers/surveillance_camera/ALPR_Example2.jpg"
],
"snapToLayer": "walls_and_buildings"
} }
], ],
"mapRendering": [ "mapRendering": [
@ -602,6 +656,10 @@
"icon": { "icon": {
"render": "./assets/themes/surveillance/logo.svg", "render": "./assets/themes/surveillance/logo.svg",
"mappings": [ "mappings": [
{
"if": "surveillance:type=ALPR",
"then": "./assets/layers/surveillance_camera/ALPR.svg"
},
{ {
"if": "camera:type=dome", "if": "camera:type=dome",
"then": "./assets/themes/surveillance/dome.svg" "then": "./assets/themes/surveillance/dome.svg"
@ -619,15 +677,17 @@
"iconSize": { "iconSize": {
"mappings": [ "mappings": [
{ {
"if": "camera:type=dome", "if": {
"then": "50,50,center" "and": [
}, "camera:type=fixed",
{ "surveillance:type=camera",
"if": "_direction:leftright~*", "_direction:leftright~*"
]
},
"then": "100,35,center" "then": "100,35,center"
} }
], ],
"render": "50,50,center" "render": "35,35,center"
}, },
"location": [ "location": [
"point", "point",
@ -638,7 +698,12 @@
"render": "calc({_direction:numerical}deg + 90deg)", "render": "calc({_direction:numerical}deg + 90deg)",
"mappings": [ "mappings": [
{ {
"if": "camera:type=dome", "if": {
"or": [
"camera:type=dome",
"surveillance:type=ALPR"
]
},
"then": "0" "then": "0"
}, },
{ {

View file

@ -74,8 +74,7 @@
"images", "images",
{ {
"id": "plantnet", "id": "plantnet",
"render": "{plantnet_detection()}", "render": "{plantnet_detection()}"
"condition": "species:wikidata="
}, },
{ {
"id": "tree-species-wikidata", "id": "tree-species-wikidata",

View file

@ -23,7 +23,8 @@
"_d=feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? ''", "_d=feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? ''",
"_mastodon_candidate_a=(feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName(\"a\")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) ", "_mastodon_candidate_a=(feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName(\"a\")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) ",
"_mastodon_link=(feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName(\"a\")).filter(a => a.getAttribute(\"rel\")?.indexOf('me') >= 0)[0]?.href})(feat) ", "_mastodon_link=(feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName(\"a\")).filter(a => a.getAttribute(\"rel\")?.indexOf('me') >= 0)[0]?.href})(feat) ",
"_mastodon_candidate=feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a" "_mastodon_candidate=feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a",
"__current_background:='initial_value'"
], ],
"tagRenderings": [ "tagRenderings": [
{ {
@ -103,6 +104,72 @@
"*": "{logout()}" "*": "{logout()}"
} }
}, },
{
"id": "background-layer-readonly",
"condition": {
"and": [
"_theme:backgroundLayer~*",
"mapcomplete-preferred-background-layer~*",
"_theme:backgroundLayer!:={mapcomplete-preferred-background-layer}"
]
},
"render": {
"en": "This thematic map has a predefined background layer set. Your default theme setting does not apply"
}
},
{
"id": "background-layer",
"question": {
"en": "What background layer should be shown by default?"
},
"condition": "_theme:backgroundLayer=",
"mappings": [
{
"if": "mapcomplete-preferred-background-layer=",
"then": {
"en": "Use the default background layer"
}
},
{
"if": "mapcomplete-preferred-background-layer=osm",
"then": {
"en": "Use OpenStreetMap-carto as default layer"
}
},
{
"if": "mapcomplete-preferred-background-layer=photo",
"then": {
"en": "Use aerial imagery as default background"
}
},
{
"if": "mapcomplete-preferred-background-layer=map",
"then": {
"en": "Use a non-openstreetmap based map as default background"
}
},
{
"if": "mapcomplete-preferred-background-layer:={__current_background}",
"then": {
"en": "Use the current background layer (<span class='code'>{__current_background}</span>) as default background"
},
"hideInAnswer": {
"or": [
"__current_background=",
"__current_background=osm",
"__current_background=initial_value"
]
}
},
{
"if": "mapcomplete-preferred-background-layer~*",
"then": {
"en": "Use background layer <span class='code'>{mapcomplete-preferred-background-layer}</span> as default background"
},
"hideInAnswer": true
}
]
},
{ {
"id": "picture-license", "id": "picture-license",
"description": "This question is not meant to be placed on an OpenStreetMap-element; however it is used in the user information panel to ask which license the user wants", "description": "This question is not meant to be placed on an OpenStreetMap-element; however it is used in the user information panel to ask which license the user wants",

View file

@ -31,7 +31,7 @@
"startLat": 52.99238, "startLat": 52.99238,
"startLon": 6.570614, "startLon": 6.570614,
"startZoom": 20, "startZoom": 20,
"defaultBackgroundId": "CartoDB.Positron", "defaultBackgroundId": "maptiler.backdrop",
"layers": [ "layers": [
{ {
"builtin": "cycleways_and_roads", "builtin": "cycleways_and_roads",

View file

@ -57,7 +57,6 @@
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 1.5, "widenFactor": 1.5,
"defaultBackgroundId": "CartoDB.Voyager",
"layers": [ "layers": [
"charging_station" "charging_station"
] ]

View file

@ -265,6 +265,6 @@
] ]
} }
], ],
"defaultBackgroundId": "CartoDB.Positron", "defaultBackgroundId": "maptiler.backdrop",
"credits": "L'imaginaire" "credits": "L'imaginaire"
} }

View file

@ -45,7 +45,6 @@
"cs": "Mapa, kde můžete prohlížet a upravovat věci související s cyklistickou infrastrukturou. Vytvořeno během #osoc21." "cs": "Mapa, kde můžete prohlížet a upravovat věci související s cyklistickou infrastrukturou. Vytvořeno během #osoc21."
}, },
"hideFromOverview": false, "hideFromOverview": false,
"defaultBackgroundId": "CartoDB.Voyager",
"icon": "./assets/themes/cycle_infra/cycle-infra.svg", "icon": "./assets/themes/cycle_infra/cycle-infra.svg",
"startLat": 51, "startLat": 51,
"startLon": 3.75, "startLon": 3.75,

View file

@ -36,7 +36,6 @@
"credits": "Originally created during Open Summer of Code by Pieter Fiers, Thibault Declercq, Pierre Barban, Joost Schouppe and Pieter Vander Vennet", "credits": "Originally created during Open Summer of Code by Pieter Fiers, Thibault Declercq, Pierre Barban, Joost Schouppe and Pieter Vander Vennet",
"icon": "./assets/themes/cyclofix/logo.svg", "icon": "./assets/themes/cyclofix/logo.svg",
"startLat": 0, "startLat": 0,
"defaultBackgroundId": "CartoDB.Voyager",
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 2, "widenFactor": 2,

View file

@ -37,7 +37,6 @@
}, },
"icon": "./assets/themes/drinking_water/logo.svg", "icon": "./assets/themes/drinking_water/logo.svg",
"startLat": 50.8465573, "startLat": 50.8465573,
"defaultBackgroundId": "CartoDB.Voyager",
"startLon": 4.351697, "startLon": 4.351697,
"startZoom": 16, "startZoom": 16,
"widenFactor": 2, "widenFactor": 2,

View file

@ -24,7 +24,6 @@
"eu": "Hezkuntza", "eu": "Hezkuntza",
"pl": "Edukacja" "pl": "Edukacja"
}, },
"defaultBackgroundId": "CartoDB.Voyager",
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 0, "startZoom": 0,

View file

@ -44,7 +44,7 @@
"layers": [ "layers": [
"ghost_bike" "ghost_bike"
], ],
"defaultBackgroundId": "CartoDB.Positron", "defaultBackgroundId": "maptiler.backdrop",
"clustering": { "clustering": {
"maxZoom": 0 "maxZoom": 0
} }

View file

@ -27,7 +27,6 @@
}, },
"icon": "./assets/layers/doctors/doctors.svg", "icon": "./assets/layers/doctors/doctors.svg",
"startLat": 50.8465573, "startLat": 50.8465573,
"defaultBackgroundId": "CartoDB.Voyager",
"startLon": 4.351697, "startLon": 4.351697,
"startZoom": 16, "startZoom": 16,
"widenFactor": 2, "widenFactor": 2,

View file

@ -27,7 +27,6 @@
}, },
"icon": "./assets/layers/entrance/entrance.svg", "icon": "./assets/layers/entrance/entrance.svg",
"startLat": 51.17181, "startLat": 51.17181,
"defaultBackgroundId": "CartoDB.Voyager",
"startLon": 4.144383, "startLon": 4.144383,
"startZoom": 14, "startZoom": 14,
"widenFactor": 2, "widenFactor": 2,

View file

@ -47,7 +47,7 @@
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 5, "widenFactor": 5,
"defaultBackgroundId": "CartoDB.Positron", "defaultBackgroundId": "maptiler.backdrop",
"layers": [ "layers": [
"map" "map"
] ]

View file

@ -24,7 +24,6 @@
}, },
"icon": "./assets/themes/onwheels/crest.svg", "icon": "./assets/themes/onwheels/crest.svg",
"startLat": 50.86622, "startLat": 50.86622,
"defaultBackgroundId": "CartoDB.Voyager",
"startLon": 4.350103, "startLon": 4.350103,
"startZoom": 17, "startZoom": 17,
"widenFactor": 2, "widenFactor": 2,

View file

@ -41,6 +41,5 @@
"layers": [ "layers": [
"windturbine" "windturbine"
], ],
"defaultBackgroundId": "CartoDB.Voyager",
"credits": "Seppe Santens" "credits": "Seppe Santens"
} }

View file

@ -29,7 +29,6 @@
}, },
"icon": "./assets/themes/osm_community_index/osm.svg", "icon": "./assets/themes/osm_community_index/osm.svg",
"startLat": 50.8465573, "startLat": 50.8465573,
"defaultBackgroundId": "CartoDB.Voyager",
"startLon": 4.351697, "startLon": 4.351697,
"startZoom": 16, "startZoom": 16,
"clustering": false, "clustering": false,

View file

@ -144,14 +144,7 @@
"width": 5 "width": 5
} }
], ],
"presets": [ "=presets": [],
{
"tags": [
"shop=yes",
"dog=yes"
]
}
],
"source": { "source": {
"=osmTags": { "=osmTags": {
"and": [ "and": [

View file

@ -46,7 +46,6 @@
"startLon": 9.9937, "startLon": 9.9937,
"startZoom": 13, "startZoom": 13,
"widenFactor": 1.5, "widenFactor": 1.5,
"defaultBackgroundId": "CartoDB.Voyager",
"clustering": { "clustering": {
"maxZoom": 14, "maxZoom": 14,
"minNeededElements": 100 "minNeededElements": 100

View file

@ -24,7 +24,6 @@
}, },
"icon": "./assets/themes/rainbow_crossings/logo.svg", "icon": "./assets/themes/rainbow_crossings/logo.svg",
"startLat": 50.8465573, "startLat": 50.8465573,
"defaultBackgroundId": "CartoDB.Voyager",
"startLon": 4.351697, "startLon": 4.351697,
"startZoom": 16, "startZoom": 16,
"widenFactor": 2, "widenFactor": 2,

View file

@ -27,7 +27,7 @@
"startZoom": 12, "startZoom": 12,
"widenFactor": 1.2, "widenFactor": 1.2,
"socialImage": "./assets/themes/speelplekken/social_image.jpg", "socialImage": "./assets/themes/speelplekken/social_image.jpg",
"defaultBackgroundId": "CartoDB.Positron", "defaultBackgroundId": "maptiler.backdrop",
"layers": [ "layers": [
{ {
"id": "shadow", "id": "shadow",

View file

@ -27,7 +27,7 @@
"startLon": 0, "startLon": 0,
"startZoom": 0, "startZoom": 0,
"hideFromOverview": true, "hideFromOverview": true,
"defaultBackgroundId": "CartoDB.Positron", "defaultBackgroundId": "maptiler.backdrop",
"layers": [ "layers": [
{ {
"builtin": "indoors", "builtin": "indoors",

View file

@ -54,7 +54,7 @@
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 2, "widenFactor": 2,
"defaultBackgroundId": "osm", "defaultBackgroundId": "maptiler.carto",
"layers": [ "layers": [
"surveillance_camera" "surveillance_camera"
] ]

View file

@ -29,6 +29,7 @@
"sameAs": "vending_machine" "sameAs": "vending_machine"
}, },
"minzoom": 18, "minzoom": 18,
"=presets": [],
"source": { "source": {
"osmTags": { "osmTags": {
"and": [ "and": [

View file

@ -26,7 +26,6 @@
}, },
"icon": "./assets/layers/walls_and_buildings/walls_and_buildings.png", "icon": "./assets/layers/walls_and_buildings/walls_and_buildings.png",
"startLat": 50.8465573, "startLat": 50.8465573,
"defaultBackgroundId": "CartoDB.Voyager",
"startLon": 4.351697, "startLon": 4.351697,
"startZoom": 16, "startZoom": 16,
"widenFactor": 2, "widenFactor": 2,

3
config.json Normal file
View file

@ -0,0 +1,3 @@
{
"#": "Settings in this file override the `config`-section of `package.json`"
}

View file

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport"> <meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://gc.zgo.at/; img-src *; connect-src 'self' https://www.openstreetmap.org/ https://api.openstreetmap.org/;">
<link href="./css/mobile.css" rel="stylesheet"/> <link href="./css/mobile.css" rel="stylesheet"/>
<link href="./css/openinghourstable.css" rel="stylesheet"/> <link href="./css/openinghourstable.css" rel="stylesheet"/>
<link href="./css/tagrendering.css" rel="stylesheet"/> <link href="./css/tagrendering.css" rel="stylesheet"/>
@ -16,8 +17,6 @@
<title>MapComplete</title> <title>MapComplete</title>
<link href="./index.webmanifest" rel="manifest"> <link href="./index.webmanifest" rel="manifest">
<!-- Mastodon link verification: https://docs.joinmastodon.org/user/profile/#Link%20verification -->
<a rel="me" href="https://en.osm.town/@MapComplete" style="display: none">Mastodon</a>
<link href="./assets/svg/add.svg" rel="icon" sizes="any" type="image/svg+xml"> <link href="./assets/svg/add.svg" rel="icon" sizes="any" type="image/svg+xml">
<meta content="./assets/SocialImage.png" property="og:image"> <meta content="./assets/SocialImage.png" property="og:image">
<meta content="MapComplete - editable, thematic maps with OpenStreetMap" property="og:title"> <meta content="MapComplete - editable, thematic maps with OpenStreetMap" property="og:title">
@ -48,10 +47,12 @@
</head> </head>
<body> <body>
<!-- Mastodon link verification: https://docs.joinmastodon.org/user/profile/#Link%20verification -->
<a rel="me" href="https://en.osm.town/@MapComplete" class="hidden">Mastodon</a>
<div id="main"></div> <div id="main"></div>
<script type="module" src="./src/all_themes_index.ts"></script> <script type="module" src="./src/all_themes_index.ts"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js" crossorigin="anonymous" <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous"
integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script> integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
<script> <script>

View file

@ -344,6 +344,8 @@
}, },
"useSearch": "Use the search above to see presets", "useSearch": "Use the search above to see presets",
"useSearchForMore": "Use the search function to search within {total} more values…", "useSearchForMore": "Use the search function to search within {total} more values…",
"waitingForGeopermission": "Waiting for your permission to use the geolocation…",
"waitingForLocation": "Searching your current location…",
"weekdays": { "weekdays": {
"abbreviations": { "abbreviations": {
"friday": "Fri", "friday": "Fri",
@ -380,6 +382,7 @@
"born": "Born: {value}", "born": "Born: {value}",
"died": "Died: {value}" "died": "Died: {value}"
}, },
"readMore": "Read the rest of the article",
"searchToShort": "Your search query is too short, enter a longer text", "searchToShort": "Your search query is too short, enter a longer text",
"searchWikidata": "Search on Wikidata", "searchWikidata": "Search on Wikidata",
"wikipediaboxTitle": "Wikipedia" "wikipediaboxTitle": "Wikipedia"
@ -413,6 +416,22 @@
"pleaseLogin": "Please log in to add a picture", "pleaseLogin": "Please log in to add a picture",
"respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.", "respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.",
"toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}", "toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}",
"upload": {
"failReasons": "You might have lost connection to the internet",
"failReasonsAdvanced": "Alternatively, make sure your browser and extensions do not block third-party API's.",
"multiple": {
"done": "{count} images are successfully uploaded. Thank you!",
"partiallyDone": "{count} images are getting uploaded, {done} images are done…",
"someFailed": "Sorry, we could not upload {count} images",
"uploading": "{count} images are getting uploaded…"
},
"one": {
"done": "Your image was successfully uploaded. Thank you!",
"failed": "Sorry, we could not upload your image",
"retrying": "Your image is getting uploaded again…",
"uploading": "Your image is getting uploaded…"
}
},
"uploadDone": "Your picture has been added. Thanks for helping out!", "uploadDone": "Your picture has been added. Thanks for helping out!",
"uploadFailed": "Could not upload your picture. Are you connected to the Internet, and allow third party API's? The Brave browser or the uMatrix plugin might block them.", "uploadFailed": "Could not upload your picture. Are you connected to the Internet, and allow third party API's? The Brave browser or the uMatrix plugin might block them.",
"uploadMultipleDone": "{count} pictures have been added. Thanks for helping out!", "uploadMultipleDone": "{count} pictures have been added. Thanks for helping out!",
@ -498,7 +517,9 @@
}, },
"plantDetection": { "plantDetection": {
"back": "Back to species overview", "back": "Back to species overview",
"button": "Automatically detect the plant species using the AI of Plantnet.org",
"confirm": "Select species", "confirm": "Select species",
"done": "The species has been applied",
"error": "Something went wrong while detecting the tree species: {error}", "error": "Something went wrong while detecting the tree species: {error}",
"howTo": { "howTo": {
"intro": "For optimal results,", "intro": "For optimal results,",
@ -515,7 +536,8 @@
"poweredByPlantnet": "Powered by <a href='https://plantnet.org' target='_blank'>plantnet.org</a>", "poweredByPlantnet": "Powered by <a href='https://plantnet.org' target='_blank'>plantnet.org</a>",
"querying": "Querying plantnet.org with {length} images", "querying": "Querying plantnet.org with {length} images",
"seeInfo": "See more information about the species", "seeInfo": "See more information about the species",
"takeImages": "Take images of the tree to automatically detect the tree type" "takeImages": "Take images of the tree to automatically detect the tree type",
"tryAgain": "Select a different species"
}, },
"privacy": { "privacy": {
"editing": "When you make a change to the map, this change is recorded on OpenStreetMap and is publicly available to anyone. A changeset made with MapComplete includes the following data: <ul><li>The changes you made</li><li>Your username</li><li>When this change is made</li><li>The theme you used while making the change</li><li>The language of the user interface</li><li>An indication of how close you were to changed objects. Other mappers can use this information to determine if a change was made based on survey or on remote research</li></ul> Please refer to <a href='https://wiki.osmfoundation.org/wiki/Privacy_Policy' target='_blank'>the privacy policy on OpenStreetMap.org</a> for detailed information. We'd like to remind you that you can use a fictional name when signing up.", "editing": "When you make a change to the map, this change is recorded on OpenStreetMap and is publicly available to anyone. A changeset made with MapComplete includes the following data: <ul><li>The changes you made</li><li>Your username</li><li>When this change is made</li><li>The theme you used while making the change</li><li>The language of the user interface</li><li>An indication of how close you were to changed objects. Other mappers can use this information to determine if a change was made based on survey or on remote research</li></ul> Please refer to <a href='https://wiki.osmfoundation.org/wiki/Privacy_Policy' target='_blank'>the privacy policy on OpenStreetMap.org</a> for detailed information. We'd like to remind you that you can use a fictional name when signing up.",

View file

@ -672,6 +672,9 @@
}, },
"1": { "1": {
"then": "This bench does not have an integrated artwork" "then": "This bench does not have an integrated artwork"
},
"2": {
"then": "This bench <span class=\"subtle\">probably</span> doesn't have an integrated artwork"
} }
}, },
"question": "Does this bench have an artistic element?", "question": "Does this bench have an artistic element?",
@ -8765,6 +8768,14 @@
}, },
"1": { "1": {
"title": "a surveillance camera mounted on a wall" "title": "a surveillance camera mounted on a wall"
},
"2": {
"description": "An ALPR typically has two lenses and an array of infrared lights.",
"title": "an ALPR camera (Automatic Number Plate Reader)"
},
"3": {
"description": "An ALPR typically has two lenses and an array of infrared lights.",
"title": "an ALPR camera (Automatic Number Plate Reader) mounted on a wall"
} }
}, },
"tagRenderings": { "tagRenderings": {
@ -8858,6 +8869,18 @@
"question": "In which geographical direction does this camera film?", "question": "In which geographical direction does this camera film?",
"render": "Films to a compass heading of {camera:direction}" "render": "Films to a compass heading of {camera:direction}"
}, },
"has_alpr": {
"mappings": {
"0": {
"then": "This is a camera without number plate recognition."
},
"1": {
"then": "This is an ALPR (Automatic License Plate Reader)"
}
},
"question": "Can this camera automatically detect license plates?",
"questionHint": "An <b>ALPR</b> (Automatic License Plate Reader) typically has two lenses and an array of infrared LEDS in between."
},
"is_indoor": { "is_indoor": {
"mappings": { "mappings": {
"0": { "0": {
@ -9629,6 +9652,32 @@
}, },
"question": "Should questions for unknown data fields appear one-by-one or together?" "question": "Should questions for unknown data fields appear one-by-one or together?"
}, },
"background-layer": {
"mappings": {
"0": {
"then": "Use the default background layer"
},
"1": {
"then": "Use OpenStreetMap-carto as default layer"
},
"2": {
"then": "Use aerial imagery as default background"
},
"3": {
"then": "Use a non-openstreetmap based map as default background"
},
"4": {
"then": "Use the current background layer (<span class='code'>{__current_background}</span>) as default background"
},
"5": {
"then": "Use background layer <span class='code'>{mapcomplete-preferred-background-layer}</span> as default background"
}
},
"question": "What background layer should be shown by default?"
},
"background-layer-readonly": {
"render": "This thematic map has a predefined background layer set. Your default theme setting does not apply"
},
"contributor-thanks": { "contributor-thanks": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -568,6 +568,9 @@
}, },
"1": { "1": {
"then": "Deze bank heeft geen geïntegreerd kunstwerk" "then": "Deze bank heeft geen geïntegreerd kunstwerk"
},
"2": {
"then": "Deze bank heeft <span class=\"subtle\">waarschijnlijk</span> geen geïntegreerd kunstwerk"
} }
}, },
"question": "Heeft deze bank een geïntegreerd kunstwerk?", "question": "Heeft deze bank een geïntegreerd kunstwerk?",

58
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.32.0", "version": "0.33.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.32.0", "version": "0.33.1",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
"@rgossiaux/svelte-headlessui": "^1.0.2", "@rgossiaux/svelte-headlessui": "^1.0.2",
@ -18,12 +18,14 @@
"@turf/distance": "^6.5.0", "@turf/distance": "^6.5.0",
"@turf/length": "^6.5.0", "@turf/length": "^6.5.0",
"@turf/turf": "^6.5.0", "@turf/turf": "^6.5.0",
"@types/dompurify": "^3.0.2",
"@types/showdown": "^2.0.0", "@types/showdown": "^2.0.0",
"chart.js": "^3.8.0", "chart.js": "^3.8.0",
"country-language": "^0.1.7", "country-language": "^0.1.7",
"country-to-currency": "^1.0.10", "country-to-currency": "^1.0.10",
"csv-parse": "^5.1.0", "csv-parse": "^5.1.0",
"doctest-ts-improved": "^0.8.8", "doctest-ts-improved": "^0.8.8",
"dompurify": "^3.0.5",
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"fake-dom": "^1.0.4", "fake-dom": "^1.0.4",
@ -3799,6 +3801,14 @@
"@types/chai": "*" "@types/chai": "*"
} }
}, },
"node_modules/@types/dompurify": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.2.tgz",
"integrity": "sha512-YBL4ziFebbbfQfH5mlC+QTJsvh0oJUrWbmxKMyEdL7emlHJqGR2Qb34TEFKj+VCayBvjKy3xczMFNhugThUsfQ==",
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
@ -3926,6 +3936,11 @@
"resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.0.tgz",
"integrity": "sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA==" "integrity": "sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA=="
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz",
"integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ=="
},
"node_modules/@types/wikidata-sdk": { "node_modules/@types/wikidata-sdk": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/wikidata-sdk/-/wikidata-sdk-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/wikidata-sdk/-/wikidata-sdk-6.1.0.tgz",
@ -6009,10 +6024,9 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "2.4.3", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", "integrity": "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A=="
"optional": true
}, },
"node_modules/domutils": { "node_modules/domutils": {
"version": "1.3.0", "version": "1.3.0",
@ -8394,6 +8408,12 @@
"url": "https://opencollective.com/core-js" "url": "https://opencollective.com/core-js"
} }
}, },
"node_modules/jspdf/node_modules/dompurify": {
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz",
"integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==",
"optional": true
},
"node_modules/jsprim": { "node_modules/jsprim": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@ -16125,6 +16145,14 @@
"@types/chai": "*" "@types/chai": "*"
} }
}, },
"@types/dompurify": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.2.tgz",
"integrity": "sha512-YBL4ziFebbbfQfH5mlC+QTJsvh0oJUrWbmxKMyEdL7emlHJqGR2Qb34TEFKj+VCayBvjKy3xczMFNhugThUsfQ==",
"requires": {
"@types/trusted-types": "*"
}
},
"@types/estree": { "@types/estree": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
@ -16252,6 +16280,11 @@
"resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.0.tgz",
"integrity": "sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA==" "integrity": "sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA=="
}, },
"@types/trusted-types": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz",
"integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ=="
},
"@types/wikidata-sdk": { "@types/wikidata-sdk": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/wikidata-sdk/-/wikidata-sdk-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/wikidata-sdk/-/wikidata-sdk-6.1.0.tgz",
@ -17770,10 +17803,9 @@
} }
}, },
"dompurify": { "dompurify": {
"version": "2.4.3", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", "integrity": "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A=="
"optional": true
}, },
"domutils": { "domutils": {
"version": "1.3.0", "version": "1.3.0",
@ -19559,6 +19591,12 @@
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz",
"integrity": "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==", "integrity": "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==",
"optional": true "optional": true
},
"dompurify": {
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz",
"integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==",
"optional": true
} }
} }
}, },

View file

@ -1,6 +1,6 @@
{ {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.32.0", "version": "0.33.4",
"repository": "https://github.com/pietervdvn/MapComplete", "repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily", "description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues", "bugs": "https://github.com/pietervdvn/MapComplete/issues",
@ -8,7 +8,8 @@
"main": "index.ts", "main": "index.ts",
"type": "module", "type": "module",
"config": { "config": {
"#": "Various endpoints that are instance-specific", "#": "Various endpoints that are instance-specific. This is the default configuration, which is re-exported in 'Constants.ts'.",
"#": "Use MAPCOMPLETE_CONFIGURATION to use an additional configuration, e.g. `MAPCOMPLETE_CONFIGURATION=config_hetzner`",
"#oauth_credentials:comment": [ "#oauth_credentials:comment": [
"`oauth_credentials` are the OAuth-2 credentials for the production-OSM server and the test-server.", "`oauth_credentials` are the OAuth-2 credentials for the production-OSM server and the test-server.",
"Are you deploying your own instance? Register your application too.", "Are you deploying your own instance? Register your application too.",
@ -115,12 +116,14 @@
"@turf/distance": "^6.5.0", "@turf/distance": "^6.5.0",
"@turf/length": "^6.5.0", "@turf/length": "^6.5.0",
"@turf/turf": "^6.5.0", "@turf/turf": "^6.5.0",
"@types/dompurify": "^3.0.2",
"@types/showdown": "^2.0.0", "@types/showdown": "^2.0.0",
"chart.js": "^3.8.0", "chart.js": "^3.8.0",
"country-language": "^0.1.7", "country-language": "^0.1.7",
"country-to-currency": "^1.0.10", "country-to-currency": "^1.0.10",
"csv-parse": "^5.1.0", "csv-parse": "^5.1.0",
"doctest-ts-improved": "^0.8.8", "doctest-ts-improved": "^0.8.8",
"dompurify": "^3.0.5",
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"fake-dom": "^1.0.4", "fake-dom": "^1.0.4",

View file

@ -761,6 +761,10 @@ video {
isolation: auto; isolation: auto;
} }
.-z-10 {
z-index: -10;
}
.float-right { .float-right {
float: right; float: right;
} }
@ -854,6 +858,10 @@ video {
margin-right: 3rem; margin-right: 3rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.mr-2 { .mr-2 {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
@ -882,10 +890,6 @@ video {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.ml-1 { .ml-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
@ -1632,16 +1636,16 @@ video {
background-color: rgb(248 113 113 / var(--tw-bg-opacity)); background-color: rgb(248 113 113 / var(--tw-bg-opacity));
} }
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-black { .bg-black {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity)); background-color: rgb(0 0 0 / var(--tw-bg-opacity));
} }
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-gray-200 { .bg-gray-200 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity)); background-color: rgb(229 231 235 / var(--tw-bg-opacity));
@ -1709,11 +1713,6 @@ video {
padding-right: 0.5rem; padding-right: 0.5rem;
} }
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.pl-1 { .pl-1 {
padding-left: 0.25rem; padding-left: 0.25rem;
} }
@ -2209,6 +2208,11 @@ input[type=text] {
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.border-region {
border: 2px dashed var(--interactive-background);
border-radius: 0.5rem;
}
/******************* Styling of input elements **********************/ /******************* Styling of input elements **********************/
/** /**
@ -2658,6 +2662,26 @@ a.link-underline {
opacity: 1; opacity: 1;
} }
@media (prefers-reduced-motion: no-preference) {
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.motion-safe\:animate-spin {
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
}
}
@media (max-width: 480px) { @media (max-width: 480px) {
.max-\[480px\]\:w-full { .max-\[480px\]\:w-full {
width: 100%; width: 100%;

View file

@ -273,7 +273,6 @@ class GenerateSeries extends Script {
allFeatures = allFeatures.filter((f) => f.properties.metadata?.theme !== "EMPTY CS") allFeatures = allFeatures.filter((f) => f.properties.metadata?.theme !== "EMPTY CS")
const centerpoints = allFeatures.map((f) => GeoOperations.centerpoint(f)) const centerpoints = allFeatures.map((f) => GeoOperations.centerpoint(f))
console.log("Found", centerpoints.length, " changesets in total") console.log("Found", centerpoints.length, " changesets in total")
const path = `${targetDir}/all_centerpoints.geojson`
const perBbox = GeoOperations.spreadIntoBboxes(centerpoints, options.zoomlevel) const perBbox = GeoOperations.spreadIntoBboxes(centerpoints, options.zoomlevel)

View file

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

View file

@ -50,8 +50,11 @@ async function fetchRegularLanguages() {
const result = await Utils.downloadJson(url, { "User-Agent": "MapComplete script" }) const result = await Utils.downloadJson(url, { "User-Agent": "MapComplete script" })
const bindings = <LanguageSpecResult[]>result.results.bindings const bindings = <LanguageSpecResult[]>result.results.bindings
// Traditional chinese = 繁體中文 or 正體中文
const zh_hant = await fetchSpecial(18130932, "zh_Hant") const zh_hant = await fetchSpecial(18130932, "zh_Hant")
const zh_hans = await fetchSpecial(13414913, "zh_Hant")
// Simplified chinese = 簡體中文 or 简体中文(
const zh_hans = await fetchSpecial(13414913, "zh_Hans")
const pt_br = await fetchSpecial(750553, "pt_BR") const pt_br = await fetchSpecial(750553, "pt_BR")
const punjabi = await fetchSpecial(58635, "pa_PK") const punjabi = await fetchSpecial(58635, "pa_PK")
const Shahmukhi = await Wikidata.LoadWikidataEntryAsync(133800) const Shahmukhi = await Wikidata.LoadWikidataEntryAsync(133800)

View file

@ -26,7 +26,7 @@ function asList(hist: Map<string, number>): ContributorList {
} }
function main() { function main() {
exec("git log --pretty='%aN %%!%% %s' ", (error, stdout, stderr) => { exec("git log --pretty='%aN %%!%% %s' ", (_, stdout) => {
const entries = stdout.split("\n").filter((str) => str !== "") const entries = stdout.split("\n").filter((str) => str !== "")
const codeContributors = new Map<string, number>() const codeContributors = new Map<string, number>()
const translationContributors = new Map<string, number>() const translationContributors = new Map<string, number>()

View file

@ -21,6 +21,7 @@ import { Utils } from "../src/Utils"
import Script from "./Script" import Script from "./Script"
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers" import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
import { parse as parse_html } from "node-html-parser" import { parse as parse_html } from "node-html-parser"
import { ExtraFunctions } from "../src/Logic/ExtraFunctions"
// This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files. // This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
// It spits out an overview of those to be used to load them // It spits out an overview of those to be used to load them
@ -395,10 +396,133 @@ class LayerOverviewUtils extends Script {
skippedLayers.length + skippedLayers.length +
" layers" " layers"
) )
// We always need the calculated tags of 'usersettings', so we export them separately
this.extractJavascriptCodeForLayer(
state.sharedLayers.get("usersettings"),
"./src/Logic/State/UserSettingsMetaTagging.ts"
)
return sharedLayers return sharedLayers
} }
/**
* Given: a fully expanded themeConfigJson
*
* Will extract a dictionary of the special code and write it into a javascript file which can be imported.
* This removes the need for _eval_, allowing for a correct CSP
* @param themeFile
* @private
*/
private extractJavascriptCode(themeFile: LayoutConfigJson) {
const allCode = [
"import {Feature} from 'geojson'",
'import { ExtraFuncType } from "../../../Logic/ExtraFunctions";',
'import { Utils } from "../../../Utils"',
"export class ThemeMetaTagging {",
" public static readonly themeName = " + JSON.stringify(themeFile.id),
"",
]
for (const layer of themeFile.layers) {
const l = <LayerConfigJson>layer
const id = l.id.replace(/[^a-zA-Z0-9_]/g, "_")
const code = l.calculatedTags ?? []
allCode.push(
" public metaTaggging_for_" +
id +
"(feat: Feature, helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>) {"
)
allCode.push(" const {" + ExtraFunctions.types.join(", ") + "} = helperFunctions")
for (const line of code) {
const firstEq = line.indexOf("=")
let attributeName = line.substring(0, firstEq).trim()
const expression = line.substring(firstEq + 1)
const isStrict = attributeName.endsWith(":")
if (!isStrict) {
allCode.push(
" Utils.AddLazyProperty(feat.properties, '" +
attributeName +
"', () => " +
expression +
" ) "
)
} else {
attributeName = attributeName.substring(0, attributeName.length - 1).trim()
allCode.push(" feat.properties['" + attributeName + "'] = " + expression)
}
}
allCode.push(" }")
}
const targetDir = "./src/assets/generated/metatagging/"
if (!existsSync(targetDir)) {
mkdirSync(targetDir)
}
allCode.push("}")
writeFileSync(targetDir + themeFile.id + ".ts", allCode.join("\n"))
}
private extractJavascriptCodeForLayer(l: LayerConfigJson, targetPath?: string) {
if (!l) {
return // Probably a bootstrapping run
}
let importPath = "../../../"
if (targetPath) {
const l = targetPath.split("/")
if (l.length == 1) {
importPath = "./"
} else {
importPath = ""
for (let i = 0; i < l.length - 3; i++) {
const _ = l[i]
importPath += "../"
}
}
}
const allCode = [
`import { Utils } from "${importPath}Utils"`,
`/** This code is autogenerated - do not edit. Edit ./assets/layers/${l?.id}/${l?.id}.json instead */`,
"export class ThemeMetaTagging {",
" public static readonly themeName = " + JSON.stringify(l.id),
"",
]
const code = l.calculatedTags ?? []
allCode.push(
" public metaTaggging_for_" + l.id + "(feat: {properties: Record<string, string>}) {"
)
for (const line of code) {
const firstEq = line.indexOf("=")
let attributeName = line.substring(0, firstEq).trim()
const expression = line.substring(firstEq + 1)
const isStrict = attributeName.endsWith(":")
if (!isStrict) {
allCode.push(
" Utils.AddLazyProperty(feat.properties, '" +
attributeName +
"', () => " +
expression +
" ) "
)
} else {
attributeName = attributeName.substring(0, attributeName.length - 2).trim()
allCode.push(" feat.properties['" + attributeName + "'] = " + expression)
}
}
allCode.push(" }")
allCode.push("}")
const targetDir = "./src/assets/generated/metatagging/"
if (!targetPath) {
if (!existsSync(targetDir)) {
mkdirSync(targetDir)
}
}
writeFileSync(targetPath ?? targetDir + "layer_" + l.id + ".ts", allCode.join("\n"))
}
private buildThemeIndex( private buildThemeIndex(
licensePaths: Set<string>, licensePaths: Set<string>,
sharedLayers: Map<string, LayerConfigJson>, sharedLayers: Map<string, LayerConfigJson>,
@ -436,6 +560,7 @@ class LayerOverviewUtils extends Script {
}) })
const skippedThemes: string[] = [] const skippedThemes: string[] = []
for (let i = 0; i < themeFiles.length; i++) { for (let i = 0; i < themeFiles.length; i++) {
const themeInfo = themeFiles[i] const themeInfo = themeFiles[i]
const themePath = themeInfo.path const themePath = themeInfo.path
@ -443,6 +568,7 @@ class LayerOverviewUtils extends Script {
const targetPath = const targetPath =
LayerOverviewUtils.themePath + "/" + themePath.substring(themePath.lastIndexOf("/")) LayerOverviewUtils.themePath + "/" + themePath.substring(themePath.lastIndexOf("/"))
const usedLayers = Array.from( const usedLayers = Array.from(
LayerOverviewUtils.extractLayerIdsFrom(themeFile, false) LayerOverviewUtils.extractLayerIdsFrom(themeFile, false)
).map((id) => LayerOverviewUtils.layerPath + id + ".json") ).map((id) => LayerOverviewUtils.layerPath + id + ".json")
@ -504,6 +630,8 @@ class LayerOverviewUtils extends Script {
this.writeTheme(themeFile) this.writeTheme(themeFile)
fixed.set(themeFile.id, themeFile) fixed.set(themeFile.id, themeFile)
this.extractJavascriptCode(themeFile)
} catch (e) { } catch (e) {
console.error("ERROR: could not prepare theme " + themePath + " due to " + e) console.error("ERROR: could not prepare theme " + themePath + " due to " + e)
throw e throw e

View file

@ -200,6 +200,26 @@ function asLangSpan(t: Translation, tag = "span"): string {
return values.join("\n") return values.join("\n")
} }
let cspCached: string = undefined
function generateCsp(): string {
if (cspCached !== undefined) {
return cspCached
}
const csp = {
"default-src": "'self'",
"script-src": "'self'",
"img-src": "*",
"connect-src": "*",
}
const content = Object.keys(csp)
.map((k) => k + ": " + csp[k])
.join("; ")
cspCached = `<meta http-equiv="Content-Security-Policy" content="${content}">`
return cspCached
}
async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) { async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) {
Locale.language.setData(layout.language[0]) Locale.language.setData(layout.language[0])
const targetLanguage = layout.language[0] const targetLanguage = layout.language[0]
@ -279,6 +299,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
Translations.t.general.poweredByOsm.textFor(targetLanguage) Translations.t.general.poweredByOsm.textFor(targetLanguage)
) )
.replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific) .replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific)
.replace(/<!-- CSP -->/, generateCsp())
.replace( .replace(
/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s, /<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s,
asLangSpan(layout.shortDescription) asLangSpan(layout.shortDescription)
@ -298,7 +319,12 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
async function createIndexFor(theme: LayoutConfig) { async function createIndexFor(theme: LayoutConfig) {
const filename = "index_" + theme.id + ".ts" const filename = "index_" + theme.id + ".ts"
writeFileSync(filename, `import layout from "./src/assets/generated/themes/${theme.id}.json"\n`)
const imports = [
`import layout from "./src/assets/generated/themes/${theme.id}.json"`,
`import { ThemeMetaTagging } from "./src/assets/generated/metatagging/${theme.id}"`,
]
writeFileSync(filename, imports.join("\n") + "\n")
appendFileSync(filename, codeTemplate) appendFileSync(filename, codeTemplate)
} }

View file

@ -3,7 +3,7 @@ import SmallLicense from "../src/Models/smallLicense"
import ScriptUtils from "./ScriptUtils" import ScriptUtils from "./ScriptUtils"
import Script from "./Script" import Script from "./Script"
import { Utils } from "../src/Utils" import { Utils } from "../src/Utils"
const prompt = require("prompt-sync")()
export class GenerateLicenseInfo extends Script { export class GenerateLicenseInfo extends Script {
private static readonly needsLicenseRef = new Set( private static readonly needsLicenseRef = new Set(
ScriptUtils.readDirRecSync("./LICENSES") ScriptUtils.readDirRecSync("./LICENSES")

View file

@ -34,8 +34,6 @@ function generateTagOverview(
return overview return overview
} }
function tagrenderingToTaginfoDescription(tr: TagRenderingConfig) {}
function generateLayerUsage(layer: LayerConfig, layout: LayoutConfig): any[] { function generateLayerUsage(layer: LayerConfig, layout: LayoutConfig): any[] {
if (layer.name === undefined) { if (layer.name === undefined) {
return [] // Probably a duplicate or irrelevant layer return [] // Probably a duplicate or irrelevant layer

View file

@ -0,0 +1,4 @@
{
"#":"Some configuration tweaks specifically for hetzner",
"country_coder_host": "https://countrycoder.mapcomplete.org/"
}

View file

@ -0,0 +1,21 @@
hosted.mapcomplete.org {
root * public/
file_server
header {
+Permissions-Policy "interest-cohort=()"
+Report-To `\{"group":"csp-endpoint", "max_age": 86400,"endpoints": [\{"url": "https://report.mapcomplete.org/csp"}], "include_subdomains": true}`
+Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https://gc.zgo.at ; img-src * ; report-uri https://report.mapcomplete.org/csp ; report-to csp-endpoint ;"
}
}
countrycoder.mapcomplete.org {
root * tiles/
file_server
}
report.mapcomplete.org {
reverse_proxy {
to http://127.0.0.1:2600
}
}

View file

@ -0,0 +1,7 @@
{
"store": "console",
"allowedOrigin": null,
"port": 2600,
"domainWhitelist": ["localhost:10179", "localhost:2600","hosted.mapcomplete.org", "dev.mapcomplete.org", "mapcomplete.org","*"],
"sourceBlacklist": ["chrome-extension://gighmmpiobklfepjocnamgkkbiglidom"]
}

View file

@ -0,0 +1,23 @@
#! /bin/bash
### To be run from the root of the repository
# Some pointers to get started:
# apt install npm
# apt install unzip
# npm i -g csp-logger
# wget https://github.com/pietervdvn/latlon2country/raw/main/tiles.zip
# unzip tiles.zip
MAPCOMPLETE_CONFIGURATION="config_hetzner"
npm run reset:layeroverview
npm run test
cp config.json config.json.bu &&
cp ./scripts/hetzner/config.json . &&
npm run prepare-deploy &&
mv config.json.bu config.json &&
zip dist.zip -r dist/* &&
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

View file

@ -3,10 +3,10 @@ import { writeFileSync } from "fs"
import { import {
FixLegacyTheme, FixLegacyTheme,
UpdateLegacyLayer, UpdateLegacyLayer,
} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert" } from "../src/Models/ThemeConfig/Conversion/LegacyJsonConvert"
import Translations from "../UI/i18n/Translations" import Translations from "../src/UI/i18n/Translations"
import { Translation } from "../UI/i18n/Translation" import { Translation } from "../src/UI/i18n/Translation"
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
/* /*
* This script reads all theme and layer files and reformats them inplace * This script reads all theme and layer files and reformats them inplace

View file

@ -0,0 +1,67 @@
import { Store, UIEventSource } from "../UIEventSource";
import { RasterLayerPolygon } from "../../Models/RasterLayers";
/**
* Selects the appropriate raster layer as background for the given query parameter, theme setting, user preference or default value.
*
* It the requested layer is not available, a layer of the same type will be selected.
*/
export class PreferredRasterLayerSelector {
private readonly _rasterLayerSetting: UIEventSource<RasterLayerPolygon>;
private readonly _availableLayers: Store<RasterLayerPolygon[]>;
private readonly _preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>;
private readonly _queryParameter: UIEventSource<string>;
constructor(rasterLayerSetting: UIEventSource<RasterLayerPolygon>, availableLayers: Store<RasterLayerPolygon[]>, queryParameter: UIEventSource<string>, preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>) {
this._rasterLayerSetting = rasterLayerSetting;
this._availableLayers = availableLayers;
this._queryParameter = queryParameter;
this._preferredBackgroundLayer = preferredBackgroundLayer;
const self = this;
this._rasterLayerSetting.addCallbackD(layer => {
if (layer.properties.id !== this._queryParameter.data) {
this._queryParameter.setData(undefined);
return true;
}
});
this._queryParameter.addCallbackAndRunD(_ => {
const isApplied = self.updateLayer();
if (!isApplied) {
// A different layer was set as background
// We remove this queryParameter instead
self._queryParameter.setData(undefined);
return true; // Unregister
}
});
this._preferredBackgroundLayer.addCallbackD(_ => self.updateLayer());
this._availableLayers.addCallbackD(_ => self.updateLayer());
}
/**
* Returns 'true' if the target layer is set or is the current layer
* @private
*/
private updateLayer() {
// What is the ID of the layer we have to (try to) load?
const targetLayerId = this._queryParameter.data ?? this._preferredBackgroundLayer.data;
const available = this._availableLayers.data;
const isCategory = targetLayerId === "photo" || targetLayerId === "osmbasedmap" || targetLayerId === "map"
const foundLayer = isCategory ? available.find(l => l.properties.category === targetLayerId) : available.find(l => l.properties.id === targetLayerId);
if (foundLayer) {
this._rasterLayerSetting.setData(foundLayer);
return true;
}
// The current layer is not in view
}
}

View file

@ -454,12 +454,16 @@ export class ExtraFunctions {
"To enable this feature, add a field `calculatedTags` in the layer object, e.g.:", "To enable this feature, add a field `calculatedTags` in the layer object, e.g.:",
"````", "````",
'"calculatedTags": [', '"calculatedTags": [',
' "_someKey=javascript-expression",', ' "_someKey=javascript-expression (lazy execution)",',
' "_some_other_key:=javascript expression (strict execution)',
' "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",', ' "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",',
" \"_distanceCloserThen3Km=distanceTo(feat)( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ", " \"_distanceCloserThen3Km=distanceTo(feat)( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ",
" ]", " ]",
"````", "````",
"", "",
"By using `:=` as separator, the attribute will be calculated as soone as the data is loaded (strict evaluation)",
"The default behaviour, using `=` as separator, is lazy loading",
"",
"The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object:", "The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object:",
new List([ new List([

View file

@ -1,52 +1,19 @@
import { FeatureSource } from "../FeatureSource" import { FeatureSource } from "../FeatureSource"
import { UIEventSource } from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
import { OsmTags } from "../../../Models/OsmFeature"
/** /**
* Constructs a UIEventStore for the properties of every Feature, indexed by id * Constructs a UIEventStore for the properties of every Feature, indexed by id
*/ */
export default class FeaturePropertiesStore { export default class FeaturePropertiesStore {
private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>() private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>()
public readonly aliases = new Map<string, string>()
constructor(...sources: FeatureSource[]) { constructor(...sources: FeatureSource[]) {
for (const source of sources) { for (const source of sources) {
this.trackFeatureSource(source) this.trackFeatureSource(source)
} }
} }
public getStore(id: string): UIEventSource<Record<string, string>> {
return this._elements.get(id)
}
public trackFeatureSource(source: FeatureSource) {
const self = this
source.features.addCallbackAndRunD((features) => {
for (const feature of features) {
const id = feature.properties.id
if (id === undefined) {
console.trace("Error: feature without ID:", feature)
throw "Error: feature without ID"
}
const source = self._elements.get(id)
if (source === undefined) {
self._elements.set(id, new UIEventSource<any>(feature.properties))
continue
}
if (source.data === feature.properties) {
continue
}
// Update the tags in the old store and link them
const changeMade = FeaturePropertiesStore.mergeTags(source.data, feature.properties)
feature.properties = source.data
if (changeMade) {
source.ping()
}
}
})
}
/** /**
* Overwrites the tags of the old properties object, returns true if a change was made. * Overwrites the tags of the old properties object, returns true if a change was made.
* Metatags are overriden if they are in the new properties, but not removed * Metatags are overriden if they are in the new properties, but not removed
@ -67,7 +34,7 @@ export default class FeaturePropertiesStore {
} }
if (newProperties[oldPropertiesKey] === undefined) { if (newProperties[oldPropertiesKey] === undefined) {
changeMade = true changeMade = true
delete oldProperties[oldPropertiesKey] // delete oldProperties[oldPropertiesKey]
} }
} }
@ -83,7 +50,48 @@ export default class FeaturePropertiesStore {
return changeMade return changeMade
} }
// noinspection JSUnusedGlobalSymbols public getStore(id: string): UIEventSource<Record<string, string>> {
const store = this._elements.get(id)
if (store === undefined) {
console.error("PANIC: no store for", id)
}
return store
}
public trackFeature(feature: { properties: OsmTags }) {
const id = feature.properties.id
if (id === undefined) {
console.trace("Error: feature without ID:", feature)
throw "Error: feature without ID"
}
const source = this._elements.get(id)
if (source === undefined) {
this._elements.set(id, new UIEventSource<any>(feature.properties))
return
}
if (source.data === feature.properties) {
return
}
// Update the tags in the old store and link them
const changeMade = FeaturePropertiesStore.mergeTags(source.data, feature.properties)
feature.properties = <any>source.data
if (changeMade) {
source.ping()
}
}
public trackFeatureSource(source: FeatureSource) {
const self = this
source.features.addCallbackAndRunD((features) => {
for (const feature of features) {
self.trackFeature(<any>feature)
}
})
}
public addAlias(oldId: string, newId: string): void { public addAlias(oldId: string, newId: string): void {
if (newId === undefined) { if (newId === undefined) {
// We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap! // We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
@ -103,6 +111,7 @@ export default class FeaturePropertiesStore {
} }
element.data.id = newId element.data.id = newId
this._elements.set(newId, element) this._elements.set(newId, element)
this.aliases.set(newId, oldId)
element.ping() element.ping()
} }

View file

@ -82,7 +82,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
} }
const newList = [] const newList = []
all.forEach((value, key) => { all.forEach((value) => {
newList.push(value) newList.push(value)
}) })
this.features.setData(newList) this.features.setData(newList)

View file

@ -4,8 +4,9 @@ import { IndexedFeatureSource, WritableFeatureSource } from "../FeatureSource"
import { UIEventSource } from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
import { ChangeDescription } from "../../Osm/Actions/ChangeDescription" import { ChangeDescription } from "../../Osm/Actions/ChangeDescription"
import { OsmId, OsmTags } from "../../../Models/OsmFeature" import { OsmId, OsmTags } from "../../../Models/OsmFeature"
import { Feature } from "geojson" import { Feature, Point } from "geojson"
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" import { TagUtils } from "../../Tags/TagUtils"
import FeaturePropertiesStore from "../Actors/FeaturePropertiesStore"
export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource { export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource {
// This class name truly puts the 'Java' into 'Javascript' // This class name truly puts the 'Java' into 'Javascript'
@ -15,115 +16,145 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc
* *
* These elements are probably created by the 'SimpleAddUi' which generates a new point, but the import functionality might create a line or polygon too. * These elements are probably created by the 'SimpleAddUi' which generates a new point, but the import functionality might create a line or polygon too.
* Other sources of new points are e.g. imports from nodes * Other sources of new points are e.g. imports from nodes
*
* Alternatively, an already existing point might suddenly match the layer, especially if a point in a wall is reused
*
* Note that the FeaturePropertiesStore will track a featuresource, such as this one
*/ */
public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([]) public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
private readonly _seenChanges: Set<ChangeDescription>
private readonly _features: Feature[]
private readonly _backend: string
private readonly _allElementStorage: IndexedFeatureSource
private _featureProperties: FeaturePropertiesStore
constructor(changes: Changes, allElementStorage: IndexedFeatureSource, backendUrl: string) { constructor(
const seenChanges = new Set<ChangeDescription>() changes: Changes,
const features = this.features.data allElementStorage: IndexedFeatureSource,
featureProperties: FeaturePropertiesStore
) {
this._allElementStorage = allElementStorage
this._featureProperties = featureProperties
this._seenChanges = new Set<ChangeDescription>()
this._features = this.features.data
this._backend = changes.backend
const self = this const self = this
const backend = changes.backend changes.pendingChanges.addCallbackAndRunD((changes) => self.handleChanges(changes))
changes.pendingChanges.addCallbackAndRunD((changes) => { }
if (changes.length === 0) {
return private addNewFeature(feature: Feature) {
const features = this._features
feature.id = feature.properties.id
features.push(feature)
}
/**
* Handles a single pending change
* @returns true if something changed
* @param change
* @private
*/
private handleChange(change: ChangeDescription): boolean {
const backend = this._backend
const allElementStorage = this._allElementStorage
console.log("Handling pending change")
if (change.id > 0) {
// This is an already existing object
// In _most_ of the cases, this means that this _isn't_ a new object
// However, when a point is snapped to an already existing point, we have to create a representation for this point!
// For this, we introspect the change
if (allElementStorage.featuresById.data.has(change.type + "/" + change.id)) {
// The current point already exists, we don't have to do anything here
return false
}
console.debug("Detected a reused point, for", change)
// The 'allElementsStore' does _not_ have this point yet, so we have to create it
// However, we already create a store for it
const { lon, lat } = <{ lon: number; lat: number }>change.changes
const feature = <Feature<Point, OsmTags>>{
type: "Feature",
properties: {
id: <OsmId>change.type + "/" + change.id,
...TagUtils.changeAsProperties(change.tags),
},
geometry: {
type: "Point",
coordinates: [lon, lat],
},
}
this._featureProperties.trackFeature(feature)
this.addNewFeature(feature)
return true
} else if (change.changes === undefined) {
// The geometry is not described - not a new point or geometry change, but probably a tagchange to a newly created point
// Not something that should be handled here
return false
}
try {
const tags: OsmTags & { id: OsmId & string } = {
id: <OsmId & string>(change.type + "/" + change.id),
}
for (const kv of change.tags) {
tags[kv.k] = kv.v
} }
let somethingChanged = false tags["_backend"] = this._backend
function add(feature) { switch (change.type) {
feature.id = feature.properties.id case "node":
features.push(feature) const n = new OsmNode(change.id)
somethingChanged = true n.tags = tags
n.lat = change.changes["lat"]
n.lon = change.changes["lon"]
const geojson = n.asGeoJson()
this.addNewFeature(geojson)
break
case "way":
const w = new OsmWay(change.id)
w.tags = tags
w.nodes = change.changes["nodes"]
w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [lat, lon])
this.addNewFeature(w.asGeoJson())
break
case "relation":
const r = new OsmRelation(change.id)
r.tags = tags
r.members = change.changes["members"]
this.addNewFeature(r.asGeoJson())
break
}
return true
} catch (e) {
console.error("Could not generate a new geometry to render on screen for:", e)
}
}
private handleChanges(changes: ChangeDescription[]) {
const seenChanges = this._seenChanges
if (changes.length === 0) {
return
}
let somethingChanged = false
for (const change of changes) {
if (seenChanges.has(change)) {
// Already handled
continue
}
seenChanges.add(change)
if (change.tags === undefined) {
// If tags is undefined, this is probably a new point that is part of a split road
continue
} }
for (const change of changes) { somethingChanged ||= this.handleChange(change)
if (seenChanges.has(change)) { }
// Already handled if (somethingChanged) {
continue this.features.ping()
} }
seenChanges.add(change)
if (change.tags === undefined) {
// If tags is undefined, this is probably a new point that is part of a split road
continue
}
console.log("Handling pending change")
if (change.id > 0) {
// This is an already existing object
// In _most_ of the cases, this means that this _isn't_ a new object
// However, when a point is snapped to an already existing point, we have to create a representation for this point!
// For this, we introspect the change
if (allElementStorage.featuresById.data.has(change.type + "/" + change.id)) {
// The current point already exists, we don't have to do anything here
continue
}
console.debug("Detected a reused point")
// The 'allElementsStore' does _not_ have this point yet, so we have to create it
new OsmObjectDownloader(backend)
.DownloadObjectAsync(change.type + "/" + change.id)
.then((feat) => {
console.log("Got the reused point:", feat)
if (feat === "deleted") {
throw "Panic: snapping to a point, but this point has been deleted in the meantime"
}
for (const kv of change.tags) {
feat.tags[kv.k] = kv.v
}
const geojson = feat.asGeoJson()
self.features.data.push(geojson)
self.features.ping()
})
continue
} else if (change.changes === undefined) {
// The geometry is not described - not a new point or geometry change, but probably a tagchange to a newly created point
// Not something that should be handled here
continue
}
try {
const tags: OsmTags & { id: OsmId & string } = {
id: <OsmId & string>(change.type + "/" + change.id),
}
for (const kv of change.tags) {
tags[kv.k] = kv.v
}
tags["_backend"] = backendUrl
switch (change.type) {
case "node":
const n = new OsmNode(change.id)
n.tags = tags
n.lat = change.changes["lat"]
n.lon = change.changes["lon"]
const geojson = n.asGeoJson()
add(geojson)
break
case "way":
const w = new OsmWay(change.id)
w.tags = tags
w.nodes = change.changes["nodes"]
w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [
lat,
lon,
])
add(w.asGeoJson())
break
case "relation":
const r = new OsmRelation(change.id)
r.tags = tags
r.members = change.changes["members"]
add(r.asGeoJson())
break
}
} catch (e) {
console.error("Could not generate a new geometry to render on screen for:", e)
}
}
if (somethingChanged) {
self.features.ping()
}
})
} }
} }

View file

@ -20,7 +20,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
private options: { private options: {
bounds: Store<BBox> bounds: Store<BBox>
readonly allowedFeatures: TagsFilter readonly allowedFeatures: TagsFilter
backend?: "https://openstreetmap.org/" | string backend?: "https://api.openstreetmap.org/" | string
/** /**
* If given: this featureSwitch will not update if the store contains 'false' * If given: this featureSwitch will not update if the store contains 'false'
*/ */
@ -41,7 +41,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
constructor(options: { constructor(options: {
bounds: Store<BBox> bounds: Store<BBox>
readonly allowedFeatures: TagsFilter readonly allowedFeatures: TagsFilter
backend?: "https://openstreetmap.org/" | string backend?: "https://api.openstreetmap.org/" | string
/** /**
* If given: this featureSwitch will not update if the store contains 'false' * If given: this featureSwitch will not update if the store contains 'false'
*/ */
@ -54,7 +54,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
this._bounds = options.bounds this._bounds = options.bounds
this.allowedTags = options.allowedFeatures this.allowedTags = options.allowedFeatures
this.isActive = options.isActive ?? new ImmutableStore(true) this.isActive = options.isActive ?? new ImmutableStore(true)
this._backend = options.backend ?? "https://www.openstreetmap.org" this._backend = options.backend ?? "https://api.openstreetmap.org"
this._bounds.addCallbackAndRunD((bbox) => this.loadData(bbox)) this._bounds.addCallbackAndRunD((bbox) => this.loadData(bbox))
this._patchRelations = options?.patchRelations ?? true this._patchRelations = options?.patchRelations ?? true
} }

View file

@ -1,7 +1,6 @@
import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject" import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject"
import { UIEventSource } from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
import { BBox } from "../../BBox" import { BBox } from "../../BBox"
import StaticFeatureSource from "../Sources/StaticFeatureSource"
import { Tiles } from "../../../Models/TileRange" import { Tiles } from "../../../Models/TileRange"
export default class FullNodeDatabaseSource { export default class FullNodeDatabaseSource {
@ -48,11 +47,7 @@ export default class FullNodeDatabaseSource {
src.ping() src.ping()
} }
} }
const asGeojsonFeatures = Array.from(nodesById.values()).map((osmNode) =>
osmNode.asGeoJson()
)
const featureSource = new StaticFeatureSource(asGeojsonFeatures)
const tileId = Tiles.tile_index(z, x, y) const tileId = Tiles.tile_index(z, x, y)
this.loadedTiles.set(tileId, nodesById) this.loadedTiles.set(tileId, nodesById)
} }

View file

@ -771,7 +771,6 @@ export class GeoOperations {
const splitup = turf.lineSplit(<Feature<LineString>>toSplit, boundary) const splitup = turf.lineSplit(<Feature<LineString>>toSplit, boundary)
const kept = [] const kept = []
for (const f of splitup.features) { for (const f of splitup.features) {
const ls = <Feature<LineString>>f
if (!GeoOperations.inside(GeoOperations.centerpointCoordinates(f), boundary)) { if (!GeoOperations.inside(GeoOperations.centerpointCoordinates(f), boundary)) {
continue continue
} }

View file

@ -0,0 +1,159 @@
import { ImageUploader } from "./ImageUploader";
import LinkImageAction from "../Osm/Actions/LinkImageAction";
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore";
import { OsmId, OsmTags } from "../../Models/OsmFeature";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import { Store, UIEventSource } from "../UIEventSource";
import { OsmConnection } from "../Osm/OsmConnection";
import { Changes } from "../Osm/Changes";
import Translations from "../../UI/i18n/Translations";
import NoteCommentElement from "../../UI/Popup/NoteCommentElement";
/**
* The ImageUploadManager has a
*/
export class ImageUploadManager {
private readonly _uploader: ImageUploader;
private readonly _featureProperties: FeaturePropertiesStore;
private readonly _layout: LayoutConfig;
private readonly _uploadStarted: Map<string, UIEventSource<number>> = new Map();
private readonly _uploadFinished: Map<string, UIEventSource<number>> = new Map();
private readonly _uploadFailed: Map<string, UIEventSource<number>> = new Map();
private readonly _uploadRetried: Map<string, UIEventSource<number>> = new Map();
private readonly _uploadRetriedSuccess: Map<string, UIEventSource<number>> = new Map();
private readonly _osmConnection: OsmConnection;
private readonly _changes: Changes;
constructor(layout: LayoutConfig, uploader: ImageUploader, featureProperties: FeaturePropertiesStore, osmConnection: OsmConnection, changes: Changes) {
this._uploader = uploader;
this._featureProperties = featureProperties;
this._layout = layout;
this._osmConnection = osmConnection;
this._changes = changes;
}
/**
* Gets various counters.
* Note that counters can only increase
* If a retry was a success, both 'retrySuccess' _and_ 'uploadFinished' will be increased
* @param featureId: the id of the feature you want information for. '*' has a global counter
*/
public getCountsFor(featureId: string | "*"): {
retried: Store<number>;
uploadStarted: Store<number>;
retrySuccess: Store<number>;
failed: Store<number>;
uploadFinished: Store<number>
} {
return {
uploadStarted: this.getCounterFor(this._uploadStarted, featureId),
uploadFinished: this.getCounterFor(this._uploadFinished, featureId),
retried: this.getCounterFor(this._uploadRetried, featureId),
failed: this.getCounterFor(this._uploadFailed, featureId),
retrySuccess: this.getCounterFor(this._uploadRetriedSuccess, featureId)
};
}
/**
* Uploads the given image, applies the correct title and license for the known user.
* Will then add this image to the OSM-feature or the OSM-note
*/
public async uploadImageAndApply(file: File, tagsStore: UIEventSource<OsmTags>) : Promise<void>{
const sizeInBytes = file.size;
const tags= tagsStore.data
const featureId = <OsmId>tags.id;
const self = this;
if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) {
this.increaseCountFor(this._uploadStarted, featureId);
this.increaseCountFor(this._uploadFailed, featureId);
throw (
Translations.t.image.toBig.Subs({
actual_size: Math.floor(sizeInBytes / 1000000) + "MB",
max_size: self._uploader.maxFileSizeInMegabytes + "MB"
}).txt
);
}
const licenseStore = this._osmConnection?.GetPreference("pictures-license", "CC0");
const license = licenseStore?.data ?? "CC0";
const matchingLayer = this._layout?.getMatchingLayer(tags);
const title =
matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.textFor("en") ??
tags.name ??
"https//osm.org/" + tags.id;
const description = [
"author:" + this._osmConnection.userDetails.data.name,
"license:" + license,
"osmid:" + tags.id
].join("\n");
console.log("Upload done, creating ");
const action = await this.uploadImageWithLicense(featureId, title, description, file);
if(!isNaN(Number( featureId))){
// THis is a map note
const url = action._url
await this._osmConnection.addCommentToNote(featureId, url)
NoteCommentElement.addCommentTo(url, <UIEventSource<any>> tagsStore, {osmConnection: this._osmConnection})
return
}
await this._changes.applyAction(action);
}
private async uploadImageWithLicense(
featureId: OsmId,
title: string, description: string, blob: File
): Promise<LinkImageAction> {
this.increaseCountFor(this._uploadStarted, featureId);
const properties = this._featureProperties.getStore(featureId);
let key: string;
let value: string;
try {
({ key, value } = await this._uploader.uploadImage(title, description, blob));
} catch (e) {
this.increaseCountFor(this._uploadRetried, featureId);
console.error("Could not upload image, trying again:", e);
try {
({ key, value } = await this._uploader.uploadImage(title, description, blob));
this.increaseCountFor(this._uploadRetriedSuccess, featureId);
} catch (e) {
console.error("Could again not upload image due to", e);
this.increaseCountFor(this._uploadFailed, featureId);
}
}
console.log("Uploading done, creating action for", featureId);
const action = new LinkImageAction(featureId, key, value, properties, {
theme: this._layout.id,
changeType: "add-image"
});
this.increaseCountFor(this._uploadFinished, featureId);
return action;
}
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
if (this._featureProperties.aliases.has(key)) {
key = this._featureProperties.aliases.get(key);
}
if (!collection.has(key)) {
collection.set(key, new UIEventSource<number>(0));
}
return collection.get(key);
}
private increaseCountFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
const counter = this.getCounterFor(collection, key);
counter.setData(counter.data + 1);
const global = this.getCounterFor(collection, "*");
global.setData(counter.data + 1);
}
}

View file

@ -0,0 +1,15 @@
export interface ImageUploader {
maxFileSizeInMegabytes?: number;
/**
* Uploads the 'blob' as image, with some metadata.
* Returns the URL to be linked + the appropriate key to add this to OSM
* @param title
* @param description
* @param blob
*/
uploadImage(
title: string,
description: string,
blob: File
): Promise<{ key: string, value: string }>;
}

View file

@ -1,60 +1,30 @@
import ImageProvider, { ProvidedImage } from "./ImageProvider" import ImageProvider, { ProvidedImage } from "./ImageProvider";
import BaseUIElement from "../../UI/BaseUIElement" import BaseUIElement from "../../UI/BaseUIElement";
import { Utils } from "../../Utils" import { Utils } from "../../Utils";
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants";
import { LicenseInfo } from "./LicenseInfo" import { LicenseInfo } from "./LicenseInfo";
import { ImageUploader } from "./ImageUploader";
export class Imgur extends ImageProvider { export class Imgur extends ImageProvider implements ImageUploader{
public static readonly defaultValuePrefix = ["https://i.imgur.com"] public static readonly defaultValuePrefix = ["https://i.imgur.com"]
public static readonly singleton = new Imgur() public static readonly singleton = new Imgur()
public readonly defaultKeyPrefixes: string[] = ["image"] public readonly defaultKeyPrefixes: string[] = ["image"]
public readonly maxFileSizeInMegabytes = 10
private constructor() { private constructor() {
super() super()
} }
static uploadMultiple( /**
* Uploads an image, returns the URL where to find the image
* @param title
* @param description
* @param blob
*/
public async uploadImage(
title: string, title: string,
description: string, description: string,
blobs: FileList, blob: File
handleSuccessfullUpload: (imageURL: string) => Promise<void>, ): Promise<{ key: string, value: string }> {
allDone: () => void,
onFail: (reason: string) => void,
offset: number = 0
) {
if (blobs.length == offset) {
allDone()
return
}
const blob = blobs.item(offset)
const self = this
this.uploadImage(
title,
description,
blob,
async (imageUrl) => {
await handleSuccessfullUpload(imageUrl)
self.uploadMultiple(
title,
description,
blobs,
handleSuccessfullUpload,
allDone,
onFail,
offset + 1
)
},
onFail
)
}
static uploadImage(
title: string,
description: string,
blob: File,
handleSuccessfullUpload: (imageURL: string) => Promise<void>,
onFail: (reason: string) => void
) {
const apiUrl = "https://api.imgur.com/3/image" const apiUrl = "https://api.imgur.com/3/image"
const apiKey = Constants.ImgurApiKey const apiKey = Constants.ImgurApiKey
@ -63,6 +33,7 @@ export class Imgur extends ImageProvider {
formData.append("title", title) formData.append("title", title)
formData.append("description", description) formData.append("description", description)
const settings: RequestInit = { const settings: RequestInit = {
method: "POST", method: "POST",
body: formData, body: formData,
@ -74,17 +45,9 @@ export class Imgur extends ImageProvider {
} }
// Response contains stringified JSON // Response contains stringified JSON
// Image URL available at response.data.link const response = await fetch(apiUrl, settings)
fetch(apiUrl, settings) const content = await response.json()
.then(async function (response) { return { key: "image", value: content.data.link }
const content = await response.json()
await handleSuccessfullUpload(content.data.link)
})
.catch((reason) => {
console.log("Uploading to IMGUR failed", reason)
// @ts-ignore
onFail(reason)
})
} }
SourceIcon(): BaseUIElement { SourceIcon(): BaseUIElement {

View file

@ -1,43 +0,0 @@
import { UIEventSource } from "../UIEventSource"
import { Imgur } from "./Imgur"
export default class ImgurUploader {
public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([])
public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([])
public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([])
public maxFileSizeInMegabytes = 10
private readonly _handleSuccessUrl: (string) => Promise<void>
constructor(handleSuccessUrl: (string) => Promise<void>) {
this._handleSuccessUrl = handleSuccessUrl
}
public uploadMany(title: string, description: string, files: FileList): void {
for (let i = 0; i < files.length; i++) {
this.queue.data.push(files.item(i).name)
}
this.queue.ping()
const self = this
this.queue.setData([...self.queue.data])
Imgur.uploadMultiple(
title,
description,
files,
async function (url) {
console.log("File saved at", url)
self.success.data.push(url)
self.success.ping()
await self._handleSuccessUrl(url)
},
function () {
console.log("All uploads completed")
},
function (failReason) {
console.log("Upload failed due to ", failReason)
self.failed.setData([...self.failed.data, failReason])
}
)
}
}

View file

@ -9,7 +9,6 @@ import { IndexedFeatureSource } from "./FeatureSource/FeatureSource"
import OsmObjectDownloader from "./Osm/OsmObjectDownloader" import OsmObjectDownloader from "./Osm/OsmObjectDownloader"
import { Utils } from "../Utils" import { Utils } from "../Utils"
import { Store, UIEventSource } from "./UIEventSource" import { Store, UIEventSource } from "./UIEventSource"
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
/** /**
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
@ -19,6 +18,7 @@ import { SpecialVisualizationState } from "../UI/SpecialVisualization"
export default class MetaTagging { export default class MetaTagging {
private static errorPrintCount = 0 private static errorPrintCount = 0
private static readonly stopErrorOutputAt = 10 private static readonly stopErrorOutputAt = 10
private static metataggingObject: any = undefined
private static retaggingFuncCache = new Map< private static retaggingFuncCache = new Map<
string, string,
((feature: Feature, propertiesStore: UIEventSource<any>) => void)[] ((feature: Feature, propertiesStore: UIEventSource<any>) => void)[]
@ -77,6 +77,23 @@ export default class MetaTagging {
}) })
} }
// noinspection JSUnusedGlobalSymbols
/**
* The 'metaTagging'-object is an object which contains some functions.
* Those functions are named `metaTaggging_for_<layer_name>` and are constructed based on the 'calculatedField' for this layer.
*
* If they are set, those functions will be used instead of parsing them at runtime.
*
* This means that we can avoid using eval, resulting in faster and safer code (at the cost of more complexity) - at least for official themes.
*
* Note: this function might appear unused while developing, it is used in the generated `index_<themename>.ts` files.
*
* @param metatagging
*/
public static setThemeMetatagging(metatagging: any) {
MetaTagging.metataggingObject = metatagging
}
/** /**
* This method (re)calculates all metatags and calculated tags on every given feature. * This method (re)calculates all metatags and calculated tags on every given feature.
* The given features should be part of the given layer * The given features should be part of the given layer
@ -298,6 +315,40 @@ export default class MetaTagging {
layer: LayerConfig, layer: LayerConfig,
helpers: Record<ExtraFuncType, (feature: Feature) => Function> helpers: Record<ExtraFuncType, (feature: Feature) => Function>
): (feature: Feature, tags: UIEventSource<Record<string, any>>) => boolean { ): (feature: Feature, tags: UIEventSource<Record<string, any>>) => boolean {
if (MetaTagging.metataggingObject) {
const id = layer.id.replace(/[^a-zA-Z0-9_]/g, "_")
const funcName = "metaTaggging_for_" + id
if (typeof MetaTagging.metataggingObject[funcName] !== "function") {
console.log(MetaTagging.metataggingObject)
throw (
"Error: metatagging-object for this theme does not have an entry at " +
funcName +
" (or it is not a function)"
)
}
// public metaTaggging_for_walls_and_buildings(feat: Feature, helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>) {
//
const func: (feat: Feature, helperFunctions: Record<string, any>) => void =
MetaTagging.metataggingObject[funcName]
return (feature: Feature) => {
const tags = feature.properties
if (tags === undefined) {
return
}
try {
func(feature, helpers)
} catch (e) {
console.error("Could not calculate calculated tags in exported class: ", e)
}
return true // Something changed
}
}
console.warn(
"Static MetataggingObject for theme is not set; using `new Function` (aka `eval`) to get calculated tags. This might trip up the CSP"
)
const calculatedTags: [string, string, boolean][] = layer.calculatedTags const calculatedTags: [string, string, boolean][] = layer.calculatedTags
if (calculatedTags === undefined || calculatedTags.length === 0) { if (calculatedTags === undefined || calculatedTags.length === 0) {
return undefined return undefined

View file

@ -97,7 +97,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
}, },
meta: this.meta, meta: this.meta,
} }
if (this._snapOnto === undefined) { if (this._snapOnto?.coordinates === undefined) {
return [newPointChange] return [newPointChange]
} }
@ -113,6 +113,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
console.log("Attempting to snap:", { geojson, projected, projectedCoor, index }) console.log("Attempting to snap:", { geojson, projected, projectedCoor, index })
// We check that it isn't close to an already existing point // We check that it isn't close to an already existing point
let reusedPointId = undefined let reusedPointId = undefined
let reusedPointCoordinates: [number, number] = undefined
let outerring: [number, number][] let outerring: [number, number][]
if (geojson.geometry.type === "LineString") { if (geojson.geometry.type === "LineString") {
@ -125,11 +126,13 @@ export default class CreateNewNodeAction extends OsmCreateAction {
if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) { if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) {
// We reuse this point instead! // We reuse this point instead!
reusedPointId = this._snapOnto.nodes[index] reusedPointId = this._snapOnto.nodes[index]
reusedPointCoordinates = this._snapOnto.coordinates[index]
} }
const next = outerring[index + 1] const next = outerring[index + 1]
if (GeoOperations.distanceBetween(next, projectedCoor) < this._reusePointDistance) { if (GeoOperations.distanceBetween(next, projectedCoor) < this._reusePointDistance) {
// We reuse this point instead! // We reuse this point instead!
reusedPointId = this._snapOnto.nodes[index + 1] reusedPointId = this._snapOnto.nodes[index + 1]
reusedPointCoordinates = this._snapOnto.coordinates[index + 1]
} }
if (reusedPointId !== undefined) { if (reusedPointId !== undefined) {
this.setElementId(reusedPointId) this.setElementId(reusedPointId)
@ -139,12 +142,13 @@ export default class CreateNewNodeAction extends OsmCreateAction {
type: "node", type: "node",
id: reusedPointId, id: reusedPointId,
meta: this.meta, meta: this.meta,
changes: { lat: reusedPointCoordinates[0], lon: reusedPointCoordinates[1] },
}, },
] ]
} }
const locations = [ const locations = [
...this._snapOnto.coordinates.map(([lat, lon]) => <[number, number]>[lon, lat]), ...this._snapOnto.coordinates?.map(([lat, lon]) => <[number, number]>[lon, lat]),
] ]
const ids = [...this._snapOnto.nodes] const ids = [...this._snapOnto.nodes]

View file

@ -0,0 +1,54 @@
import ChangeTagAction from "./ChangeTagAction";
import { Tag } from "../../Tags/Tag";
import OsmChangeAction from "./OsmChangeAction";
import { Changes } from "../Changes";
import { ChangeDescription } from "./ChangeDescription";
import { Store } from "../../UIEventSource";
export default class LinkImageAction extends OsmChangeAction {
private readonly _proposedKey: "image" | "mapillary" | "wiki_commons" | string;
public readonly _url: string;
private readonly _currentTags: Store<Record<string, string>>;
private readonly _meta: { theme: string; changeType: "add-image" | "link-image" };
/**
* Adds an image-link to a feature
* @param elementId
* @param proposedKey a key which might be used, typically `image`. If the key is already used with a different URL, `key+":0"` will be used instead (or a higher number if needed)
* @param url
* @param currentTags
* @param meta
*
*/
constructor(
elementId: string,
proposedKey: "image" | "mapillary" | "wiki_commons" | string,
url: string,
currentTags: Store<Record<string, string>>,
meta: {
theme: string
changeType: "add-image" | "link-image"
}
) {
super(elementId, true)
this._proposedKey = proposedKey;
this._url = url;
this._currentTags = currentTags;
this._meta = meta;
}
protected CreateChangeDescriptions(): Promise<ChangeDescription[]> {
let key = this._proposedKey
let i = 0
const currentTags = this._currentTags.data
const url = this._url
while (currentTags[key] !== undefined && currentTags[key] !== url) {
key = this._proposedKey + ":" + i
i++
}
const tagChangeAction = new ChangeTagAction ( this.mainObjectId, new Tag(key, url), currentTags, this._meta)
return tagChangeAction.CreateChangeDescriptions()
}
}

View file

@ -1,32 +0,0 @@
import ChangeTagAction from "./ChangeTagAction"
import { Tag } from "../../Tags/Tag"
export default class LinkPicture extends ChangeTagAction {
/**
* Adds a link to an image
* @param elementId
* @param proposedKey: a key which might be used, typically `image`. If the key is already used with a different URL, `key+":0"` will be used instead (or a higher number if needed)
* @param url
* @param currentTags
* @param meta
*
*/
constructor(
elementId: string,
proposedKey: "image" | "mapillary" | "wiki_commons" | string,
url: string,
currentTags: Record<string, string>,
meta: {
theme: string
changeType: "add-image" | "link-image"
}
) {
let key = proposedKey
let i = 0
while (currentTags[key] !== undefined && currentTags[key] !== url) {
key = proposedKey + ":" + i
i++
}
super(elementId, new Tag(key, url), currentTags, meta)
}
}

View file

@ -19,6 +19,9 @@ export default abstract class OsmChangeAction {
constructor(mainObjectId: string, trackStatistics: boolean = true) { constructor(mainObjectId: string, trackStatistics: boolean = true) {
this.trackStatistics = trackStatistics this.trackStatistics = trackStatistics
this.mainObjectId = mainObjectId this.mainObjectId = mainObjectId
if(mainObjectId === undefined || mainObjectId === null){
throw "OsmObject received '"+mainObjectId+"' as mainObjectId"
}
} }
public async Perform(changes: Changes) { public async Perform(changes: Changes) {

View file

@ -215,7 +215,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
throw "Invalid ID to conflate: " + this.wayToReplaceId throw "Invalid ID to conflate: " + this.wayToReplaceId
} }
const url = `${ const url = `${
this.state.osmConnection?._oauth_config?.url ?? "https://openstreetmap.org" this.state.osmConnection?._oauth_config?.url ?? "https://api.openstreetmap.org"
}/api/0.6/${this.wayToReplaceId}/full` }/api/0.6/${this.wayToReplaceId}/full`
const rawData = await Utils.downloadJsonCached(url, 1000) const rawData = await Utils.downloadJsonCached(url, 1000)
parsed = OsmObject.ParseObjects(rawData.elements) parsed = OsmObject.ParseObjects(rawData.elements)

View file

@ -5,6 +5,7 @@ import Locale from "../../UI/i18n/Locale"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import { Changes } from "./Changes" import { Changes } from "./Changes"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore";
export interface ChangesetTag { export interface ChangesetTag {
key: string key: string
@ -13,7 +14,7 @@ export interface ChangesetTag {
} }
export class ChangesetHandler { export class ChangesetHandler {
private readonly allElements: { addAlias: (id0: String, id1: string) => void } private readonly allElements: FeaturePropertiesStore
private osmConnection: OsmConnection private osmConnection: OsmConnection
private readonly changes: Changes private readonly changes: Changes
private readonly _dryRun: Store<boolean> private readonly _dryRun: Store<boolean>
@ -29,11 +30,11 @@ export class ChangesetHandler {
constructor( constructor(
dryRun: Store<boolean>, dryRun: Store<boolean>,
osmConnection: OsmConnection, osmConnection: OsmConnection,
allElements: { addAlias: (id0: string, id1: string) => void } | undefined, allElements: FeaturePropertiesStore | { addAlias: (id0: string, id1: string) => void } | undefined,
changes: Changes changes: Changes
) { ) {
this.osmConnection = osmConnection this.osmConnection = osmConnection
this.allElements = allElements this.allElements = <FeaturePropertiesStore> allElements
this.changes = changes this.changes = changes
this._dryRun = dryRun this._dryRun = dryRun
this.userDetails = osmConnection.userDetails this.userDetails = osmConnection.userDetails

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@ import OsmToGeoJson from "osmtogeojson"
import { Feature, LineString, Polygon } from "geojson" import { Feature, LineString, Polygon } from "geojson"
export abstract class OsmObject { export abstract class OsmObject {
private static defaultBackend = "https://www.openstreetmap.org/" private static defaultBackend = "https://api.openstreetmap.org/"
protected static backendURL = OsmObject.defaultBackend protected static backendURL = OsmObject.defaultBackend
private static polygonFeatures = OsmObject.constructPolygonFeatures() private static polygonFeatures = OsmObject.constructPolygonFeatures()
type: "node" | "way" | "relation" type: "node" | "way" | "relation"

View file

@ -17,7 +17,7 @@ export default class OsmObjectDownloader {
private historyCache = new Map<string, UIEventSource<OsmObject[]>>() private historyCache = new Map<string, UIEventSource<OsmObject[]>>()
constructor( constructor(
backend: string = "https://www.openstreetmap.org", backend: string = "https://api.openstreetmap.org",
changes?: { changes?: {
readonly pendingChanges: UIEventSource<ChangeDescription[]> readonly pendingChanges: UIEventSource<ChangeDescription[]>
readonly isUploading: Store<boolean> readonly isUploading: Store<boolean>

View file

@ -219,7 +219,7 @@ class RewriteMetaInfoTags extends SimpleMetaTagger {
move("changeset", "_last_edit:changeset") move("changeset", "_last_edit:changeset")
move("timestamp", "_last_edit:timestamp") move("timestamp", "_last_edit:timestamp")
move("version", "_version_number") move("version", "_version_number")
feature.properties._backend = feature.properties._backend ?? "https://openstreetmap.org" feature.properties._backend = feature.properties._backend ?? "https://api.openstreetmap.org"
return movedSomething return movedSomething
} }
} }

View file

@ -198,7 +198,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
this.backgroundLayerId = QueryParameters.GetQueryParameter( this.backgroundLayerId = QueryParameters.GetQueryParameter(
"background", "background",
layoutToUse?.defaultBackgroundId ?? "osm", layoutToUse?.defaultBackgroundId,
"The id of the background layer to start with" "The id of the background layer to start with"
) )
} }

View file

@ -1,13 +1,13 @@
import { UIEventSource } from "../UIEventSource" import { UIEventSource } from "../UIEventSource";
import { LocalStorageSource } from "../Web/LocalStorageSource" import { LocalStorageSource } from "../Web/LocalStorageSource";
import { QueryParameters } from "../Web/QueryParameters" import { QueryParameters } from "../Web/QueryParameters";
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied" export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
export interface GeoLocationPointProperties extends GeolocationCoordinates { export interface GeoLocationPointProperties extends GeolocationCoordinates {
id: "gps" id: "gps";
"user:location": "yes" "user:location": "yes";
date: string date: string;
} }
/** /**
@ -23,22 +23,22 @@ export class GeoLocationState {
*/ */
public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource( public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource(
"prompt" "prompt"
) );
/** /**
* Important to determine e.g. if we move automatically on fix or not * Important to determine e.g. if we move automatically on fix or not
*/ */
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined) public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined);
/** /**
* If true: the map will center (and re-center) to this location * If true: the map will center (and re-center) to this location
*/ */
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true) public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true);
/** /**
* The latest GeoLocationCoordinates, as given by the WebAPI * The latest GeoLocationCoordinates, as given by the WebAPI
*/ */
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> = public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
new UIEventSource<GeolocationCoordinates | undefined>(undefined) new UIEventSource<GeolocationCoordinates | undefined>(undefined);
/** /**
* A small flag on localstorage. If the user previously granted the geolocation, it will be set. * A small flag on localstorage. If the user previously granted the geolocation, it will be set.
@ -50,69 +50,49 @@ export class GeoLocationState {
*/ */
private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>( private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>(
LocalStorageSource.Get("geolocation-permissions") LocalStorageSource.Get("geolocation-permissions")
) );
/** /**
* Used to detect a permission retraction * Used to detect a permission retraction
*/ */
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false) private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false);
constructor() { constructor() {
const self = this const self = this;
this.permission.addCallbackAndRunD(async (state) => { this.permission.addCallbackAndRunD(async (state) => {
if (state === "granted") { if (state === "granted") {
self._previousLocationGrant.setData("true") self._previousLocationGrant.setData("true");
self._grantedThisSession.setData(true) self._grantedThisSession.setData(true);
} }
if (state === "prompt" && self._grantedThisSession.data) { if (state === "prompt" && self._grantedThisSession.data) {
// This is _really_ weird: we had a grant earlier, but it's 'prompt' now? // This is _really_ weird: we had a grant earlier, but it's 'prompt' now?
// This means that the rights have been revoked again! // This means that the rights have been revoked again!
// self.permission.setData("denied") self._previousLocationGrant.setData("false");
self._previousLocationGrant.setData("false") self.permission.setData("denied");
self.permission.setData("denied") self.currentGPSLocation.setData(undefined);
self.currentGPSLocation.setData(undefined) console.warn("Detected a downgrade in permissions!");
console.warn("Detected a downgrade in permissions!")
} }
if (state === "denied") { if (state === "denied") {
self._previousLocationGrant.setData("false") self._previousLocationGrant.setData("false");
} }
}) });
console.log("Previous location grant:", this._previousLocationGrant.data) console.log("Previous location grant:", this._previousLocationGrant.data);
if (this._previousLocationGrant.data === "true") { if (this._previousLocationGrant.data === "true") {
// A previous visit successfully granted permission. Chance is high that we are allowed to use it again! // A previous visit successfully granted permission. Chance is high that we are allowed to use it again!
// We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them // We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them
this._previousLocationGrant.setData("false") this._previousLocationGrant.setData("false");
console.log("Requesting access to GPS as this was previously granted") console.log("Requesting access to GPS as this was previously granted");
const latLonGivenViaUrl = const latLonGivenViaUrl =
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon") QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon");
if (!latLonGivenViaUrl) { if (!latLonGivenViaUrl) {
this.requestMoment.setData(new Date()) this.requestMoment.setData(new Date());
} }
this.requestPermission() this.requestPermission();
} }
} }
/**
* Installs the listener for updates
* @private
*/
private async startWatching() {
const self = this
navigator.geolocation.watchPosition(
function (position) {
self.currentGPSLocation.setData(position.coords)
self._previousLocationGrant.setData("true")
},
function () {
console.warn("Could not get location with navigator.geolocation")
},
{
enableHighAccuracy: true,
}
)
}
/** /**
* Requests the user to allow access to their position. * Requests the user to allow access to their position.
* When granted, will be written to the 'geolocationState'. * When granted, will be written to the 'geolocationState'.
@ -121,33 +101,57 @@ export class GeoLocationState {
public requestPermission() { public requestPermission() {
if (typeof navigator === "undefined") { if (typeof navigator === "undefined") {
// Not compatible with this browser // Not compatible with this browser
this.permission.setData("denied") this.permission.setData("denied");
return return;
} }
if (this.permission.data !== "prompt" && this.permission.data !== "requested") { if (this.permission.data !== "prompt" && this.permission.data !== "requested") {
// If the user denies the first prompt, revokes the deny and then tries again, we have to run the flow as well // If the user denies the first prompt, revokes the deny and then tries again, we have to run the flow as well
// Hence that we continue the flow if it is "requested" // Hence that we continue the flow if it is "requested"
return return;
} }
this.permission.setData("requested") this.permission.setData("requested");
try { try {
navigator?.permissions navigator?.permissions
?.query({ name: "geolocation" }) ?.query({ name: "geolocation" })
.then((status) => { .then((status) => {
console.log("Status update: received geolocation permission is ", status.state) const self = this;
this.permission.setData(status.state) if(status.state === "granted" || status.state === "denied"){
const self = this
status.onchange = function () {
self.permission.setData(status.state) self.permission.setData(status.state)
return
} }
status.addEventListener("change", (e) => {
self.permission.setData(status.state);
});
// The code above might have reset it to 'prompt', but we _did_ request permission!
this.permission.setData("requested") this.permission.setData("requested")
// We _must_ call 'startWatching', as that is the actual trigger for the popup... // We _must_ call 'startWatching', as that is the actual trigger for the popup...
self.startWatching() self.startWatching();
}) })
.catch((e) => console.error("Could not get geopermission", e)) .catch((e) => console.error("Could not get geopermission", e));
} catch (e) { } catch (e) {
console.error("Could not get permission:", e) console.error("Could not get permission:", e);
} }
} }
/**
* Installs the listener for updates
* @private
*/
private async startWatching() {
const self = this;
navigator.geolocation.watchPosition(
function(position) {
self.currentGPSLocation.setData(position.coords);
self._previousLocationGrant.setData("true");
},
function() {
console.warn("Could not get location with navigator.geolocation");
},
{
enableHighAccuracy: true
}
);
}
} }

View file

@ -1,21 +1,23 @@
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import { OsmConnection } from "../Osm/OsmConnection" import { OsmConnection } from "../Osm/OsmConnection";
import { MangroveIdentity } from "../Web/MangroveReviews" import { MangroveIdentity } from "../Web/MangroveReviews";
import { Store, Stores, UIEventSource } from "../UIEventSource" import { Store, Stores, UIEventSource } from "../UIEventSource";
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource";
import { FeatureSource } from "../FeatureSource/FeatureSource" import { FeatureSource } from "../FeatureSource/FeatureSource";
import { Feature } from "geojson" import { Feature } from "geojson";
import { Utils } from "../../Utils" import { Utils } from "../../Utils";
import translators from "../../assets/translators.json" import translators from "../../assets/translators.json";
import codeContributors from "../../assets/contributors.json" import codeContributors from "../../assets/contributors.json";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
import usersettings from "../../../src/assets/generated/layers/usersettings.json" import usersettings from "../../../src/assets/generated/layers/usersettings.json";
import Locale from "../../UI/i18n/Locale" import Locale from "../../UI/i18n/Locale";
import LinkToWeblate from "../../UI/Base/LinkToWeblate" import LinkToWeblate from "../../UI/Base/LinkToWeblate";
import FeatureSwitchState from "./FeatureSwitchState" import FeatureSwitchState from "./FeatureSwitchState";
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants";
import { QueryParameters } from "../Web/QueryParameters" import { QueryParameters } from "../Web/QueryParameters";
import { ThemeMetaTagging } from "./UserSettingsMetaTagging";
import { MapProperties } from "../../Models/MapProperties";
/** /**
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
@ -40,6 +42,8 @@ export default class UserRelatedState {
public readonly fixateNorth: UIEventSource<undefined | "yes"> public readonly fixateNorth: UIEventSource<undefined | "yes">
public readonly homeLocation: FeatureSource public readonly homeLocation: FeatureSource
public readonly language: UIEventSource<string> public readonly language: UIEventSource<string>
public readonly preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>
public readonly imageLicense : UIEventSource<string>
/** /**
* The number of seconds that the GPS-locations are stored in memory. * The number of seconds that the GPS-locations are stored in memory.
* Time in seconds * Time in seconds
@ -51,16 +55,23 @@ export default class UserRelatedState {
/** /**
* Preferences as tags exposes many preferences and state properties as record. * Preferences as tags exposes many preferences and state properties as record.
* This is used to bridge the internal state with the usersettings.json layerconfig file * This is used to bridge the internal state with the usersettings.json layerconfig file
*
* Some metainformation that should not be edited starts with a single underscore
* Constants and query parameters start with two underscores
* Note: these are linked via OsmConnection.preferences which exports all preferences as UIEventSource
*/ */
public readonly preferencesAsTags: UIEventSource<Record<string, string>> public readonly preferencesAsTags: UIEventSource<Record<string, string>>
private readonly _mapProperties: MapProperties;
constructor( constructor(
osmConnection: OsmConnection, osmConnection: OsmConnection,
availableLanguages?: string[], availableLanguages?: string[],
layout?: LayoutConfig, layout?: LayoutConfig,
featureSwitches?: FeatureSwitchState featureSwitches?: FeatureSwitchState,
mapProperties?: MapProperties
) { ) {
this.osmConnection = osmConnection this.osmConnection = osmConnection
this._mapProperties = mapProperties;
{ {
const translationMode: UIEventSource<undefined | "true" | "false" | "mobile" | string> = const translationMode: UIEventSource<undefined | "true" | "false" | "mobile" | string> =
this.osmConnection.GetPreference("translation-mode", "false") this.osmConnection.GetPreference("translation-mode", "false")
@ -89,11 +100,17 @@ export default class UserRelatedState {
) )
this.language = this.osmConnection.GetPreference("language") this.language = this.osmConnection.GetPreference("language")
this.showTags = <UIEventSource<any>>this.osmConnection.GetPreference("show_tags") this.showTags = <UIEventSource<any>>this.osmConnection.GetPreference("show_tags")
this.fixateNorth = <any>this.osmConnection.GetPreference("fixate-north") this.fixateNorth = <UIEventSource<"yes">>this.osmConnection.GetPreference("fixate-north")
this.mangroveIdentity = new MangroveIdentity( this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove") this.osmConnection.GetLongPreference("identity", "mangrove")
) )
this.preferredBackgroundLayer= this.osmConnection.GetPreference("preferred-background-layer", undefined, {
documentation: "The ID of a layer or layer category that MapComplete uses by default"
})
this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", {
documentation: "The license under which new images are uploaded"
})
this.installedUserThemes = this.InitInstalledUserThemes() this.installedUserThemes = this.InitInstalledUserThemes()
this.homeLocation = this.initHomeLocation() this.homeLocation = this.initHomeLocation()
@ -245,6 +262,7 @@ export default class UserRelatedState {
): UIEventSource<Record<string, string>> { ): UIEventSource<Record<string, string>> {
const amendedPrefs = new UIEventSource<Record<string, string>>({ const amendedPrefs = new UIEventSource<Record<string, string>>({
_theme: layout?.id, _theme: layout?.id,
"_theme:backgroundLayer": layout?.defaultBackgroundId,
_backend: this.osmConnection.Backend(), _backend: this.osmConnection.Backend(),
_applicationOpened: new Date().toISOString(), _applicationOpened: new Date().toISOString(),
_supports_sharing: _supports_sharing:
@ -259,6 +277,7 @@ export default class UserRelatedState {
amendedPrefs.data["__url_parameter_initialized:" + key] = "yes" amendedPrefs.data["__url_parameter_initialized:" + key] = "yes"
} }
const osmConnection = this.osmConnection const osmConnection = this.osmConnection
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => { osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
for (const k in newPrefs) { for (const k in newPrefs) {
@ -279,7 +298,6 @@ export default class UserRelatedState {
amendedPrefs.ping() amendedPrefs.ping()
console.log("Amended prefs are:", amendedPrefs.data) console.log("Amended prefs are:", amendedPrefs.data)
}) })
const usersettingsConfig = UserRelatedState.usersettingsConfig
const translationMode = osmConnection.GetPreference("translation-mode") const translationMode = osmConnection.GetPreference("translation-mode")
Locale.language.mapD( Locale.language.mapD(
@ -326,30 +344,14 @@ export default class UserRelatedState {
}, },
[translationMode] [translationMode]
) )
const usersettingMetaTagging = new ThemeMetaTagging()
osmConnection.userDetails.addCallback((userDetails) => { osmConnection.userDetails.addCallback((userDetails) => {
for (const k in userDetails) { for (const k in userDetails) {
amendedPrefs.data["_" + k] = "" + userDetails[k] amendedPrefs.data["_" + k] = "" + userDetails[k]
} }
for (const [name, code, _] of usersettingsConfig.calculatedTags) { usersettingMetaTagging.metaTaggging_for_usersettings({ properties: amendedPrefs.data })
try {
let result = new Function("feat", "return " + code + ";")({
properties: amendedPrefs.data,
})
if (result !== undefined && result !== "" && result !== null) {
if (typeof result !== "string") {
result = JSON.stringify(result)
}
amendedPrefs.data[name] = result
}
} catch (e) {
console.error(
"Calculating a tag for userprofile-settings failed for variable",
name,
e
)
}
}
const simplifiedName = userDetails.name.toLowerCase().replace(/\s+/g, "") const simplifiedName = userDetails.name.toLowerCase().replace(/\s+/g, "")
const isTranslator = translators.contributors.find( const isTranslator = translators.contributors.find(
@ -403,6 +405,13 @@ export default class UserRelatedState {
} }
} }
this._mapProperties?.rasterLayer?.addCallbackAndRun(l => {
amendedPrefs.data["__current_background"] = l?.properties?.id
amendedPrefs.ping()
})
return amendedPrefs return amendedPrefs
} }
} }

View file

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

View file

@ -515,7 +515,7 @@ class MappedStore<TIn, T> extends Store<T> {
} }
private unregisterFromUpstream() { private unregisterFromUpstream() {
console.log("Unregistering callbacks for", this.tag) console.debug("Unregistering callbacks for", this.tag)
this._callbacksAreRegistered = false this._callbacksAreRegistered = false
this._unregisterFromUpstream() this._unregisterFromUpstream()
this._unregisterFromExtraStores?.forEach((unr) => unr()) this._unregisterFromExtraStores?.forEach((unr) => unr())

View file

@ -985,6 +985,27 @@ export default class PlantNet {
} }
} }
export interface PlantNetSpeciesMatch {
score: number
gbif: { id: string /*Actually a number*/ }
species: {
scientificNameWithoutAuthor: string
scientificNameAuthorship: string
genus: {
scientificNameWithoutAuthor: string
scientificNameAuthorship: string
scientificName: string
}
family: {
scientificNameWithoutAuthor: string
scientificNameAuthorship: string
scientificName: string
}
commonNames: string[]
scientificName: string
}
}
export interface PlantNetResult { export interface PlantNetResult {
query: { query: {
project: string project: string
@ -995,26 +1016,7 @@ export interface PlantNetResult {
language: string language: string
preferedReferential: string preferedReferential: string
bestMatch: string bestMatch: string
results: { results: PlantNetSpeciesMatch[]
score: number
gbif: { id: string /*Actually a number*/ }
species: {
scientificNameWithoutAuthor: string
scientificNameAuthorship: string
genus: {
scientificNameWithoutAuthor: string
scientificNameAuthorship: string
scientificName: string
}
family: {
scientificNameWithoutAuthor: string
scientificNameAuthorship: string
scientificName: string
}
commonNames: string[]
scientificName: string
}
}[]
version: string version: string
remainingIdentificationRequests: number remainingIdentificationRequests: number
} }

View file

@ -159,7 +159,7 @@ export default class Wikidata {
*/ */
public static async searchAdvanced( public static async searchAdvanced(
text: string, text: string,
options: WikidataAdvancedSearchoptions options?: WikidataAdvancedSearchoptions
): Promise< ): Promise<
{ {
id: string id: string
@ -185,7 +185,7 @@ export default class Wikidata {
?num wikibase:apiOrdinal true . ?num wikibase:apiOrdinal true .
bd:serviceParam wikibase:limit ${ bd:serviceParam wikibase:limit ${
Math.round( Math.round(
(options.maxCount ?? 20) * 1.5 (options?.maxCount ?? 20) * 1.5
) /*Some padding for disambiguation pages */ ) /*Some padding for disambiguation pages */
} . } .
?label wikibase:apiOutput mwapi:label . ?label wikibase:apiOutput mwapi:label .
@ -193,7 +193,7 @@ export default class Wikidata {
} }
${instanceOf} ${instanceOf}
${minusPhrases.join("\n ")} ${minusPhrases.join("\n ")}
} ORDER BY ASC(?num) LIMIT ${options.maxCount ?? 20}` } ORDER BY ASC(?num) LIMIT ${options?.maxCount ?? 20}`
const url = wds.sparqlQuery(sparql) const url = wds.sparqlQuery(sparql)
const result = await Utils.downloadJson(url) const result = await Utils.downloadJson(url)

View file

@ -73,7 +73,6 @@ export default class Wikipedia {
if (cached) { if (cached) {
return cached return cached
} }
console.log("Constructing store for", cachekey)
const store = new UIEventSource<FullWikipediaDetails>({}, cachekey) const store = new UIEventSource<FullWikipediaDetails>({}, cachekey)
Wikipedia._fullDetailsCache.set(cachekey, store) Wikipedia._fullDetailsCache.set(cachekey, store)
@ -123,12 +122,15 @@ export default class Wikipedia {
} }
const wikipedia = new Wikipedia({ language: data.language }) const wikipedia = new Wikipedia({ language: data.language })
wikipedia.GetArticleHtml(data.pagename).then((article) => { wikipedia.GetArticleHtml(data.pagename).then((article) => {
article = Utils.purify(article)
data.fullArticle = article data.fullArticle = article
const content = document.createElement("div") const content = document.createElement("div")
content.innerHTML = article content.innerHTML = article
const firstParagraph = content.getElementsByTagName("p").item(0) const firstParagraph = content.getElementsByTagName("p").item(0)
data.firstParagraph = firstParagraph.innerHTML if (firstParagraph) {
content.removeChild(firstParagraph) data.firstParagraph = firstParagraph.innerHTML
content.removeChild(firstParagraph)
}
data.restOfArticle = content.innerHTML data.restOfArticle = content.innerHTML
store.ping() store.ping()
}) })
@ -194,53 +196,6 @@ export default class Wikipedia {
encodeURIComponent(searchTerm) encodeURIComponent(searchTerm)
return (await Utils.downloadJson(url))["query"]["search"] return (await Utils.downloadJson(url))["query"]["search"]
} }
/**
* Searches via 'index.php' and scrapes the result.
* This gives better results then via the API
* @param searchTerm
*/
public async searchViaIndex(
searchTerm: string
): Promise<{ title: string; snippet: string; url: string }[]> {
const url = `${this.backend}/w/index.php?search=${encodeURIComponent(searchTerm)}&ns0=1`
const result = await Utils.downloadAdvanced(url)
if (result["redirect"]) {
const targetUrl = result["redirect"]
// This is an exact match
return [
{
title: this.extractPageName(targetUrl)?.trim(),
url: targetUrl,
snippet: "",
},
]
}
if (result["error"]) {
throw "Could not download: " + JSON.stringify(result)
}
const el = document.createElement("html")
el.innerHTML = result["content"].replace(/href="\//g, 'href="' + this.backend + "/")
const searchResults = el.getElementsByClassName("mw-search-results")
const individualResults = Array.from(
searchResults[0]?.getElementsByClassName("mw-search-result") ?? []
)
return individualResults.map((result) => {
const toRemove = Array.from(result.getElementsByClassName("searchalttitle"))
for (const toRm of toRemove) {
toRm.parentElement.removeChild(toRm)
}
return {
title: result
.getElementsByClassName("mw-search-result-heading")[0]
.textContent.trim(),
url: result.getElementsByTagName("a")[0].href,
snippet: result.getElementsByClassName("searchresult")[0].textContent,
}
})
}
/** /**
* Returns the innerHTML for the given article as string. * Returns the innerHTML for the given article as string.
* Some cleanup is applied to this. * Some cleanup is applied to this.
@ -262,7 +217,7 @@ export default class Wikipedia {
if (response?.parse?.text === undefined) { if (response?.parse?.text === undefined) {
return undefined return undefined
} }
const html = response["parse"]["text"]["*"] const html = Utils.purify(response["parse"]["text"]["*"])
if (html === undefined) { if (html === undefined) {
return undefined return undefined
} }

View file

@ -1,14 +1,11 @@
import * as meta from "../../package.json" import * as packagefile from "../../package.json"
import * as extraconfig from "../../config.json"
import { Utils } from "../Utils" import { Utils } from "../Utils"
export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number] export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number]
export default class Constants { export default class Constants {
public static vNumber = meta.version public static vNumber = packagefile.version
public static ImgurApiKey = meta.config.api_keys.imgur
public static readonly mapillary_client_token_v4 = meta.config.api_keys.mapillary_v4
/** /**
* API key for Maproulette * API key for Maproulette
* *
@ -17,9 +14,6 @@ export default class Constants {
* Using an empty string however does work for most actions, but will attribute all actions to the Superuser. * Using an empty string however does work for most actions, but will attribute all actions to the Superuser.
*/ */
public static readonly MaprouletteApiKey = "" public static readonly MaprouletteApiKey = ""
public static defaultOverpassUrls = meta.config.default_overpass_urls
public static readonly added_by_default = [ public static readonly added_by_default = [
"selected_element", "selected_element",
"gps_location", "gps_location",
@ -37,7 +31,6 @@ export default class Constants {
"split_point", "split_point",
"split_road", "split_road",
"current_view", "current_view",
"matchpoint",
"import_candidate", "import_candidate",
"usersettings", "usersettings",
] as const ] as const
@ -48,7 +41,6 @@ export default class Constants {
...Constants.added_by_default, ...Constants.added_by_default,
...Constants.no_include, ...Constants.no_include,
] as const ] as const
// The user journey states thresholds when a new feature gets unlocked // The user journey states thresholds when a new feature gets unlocked
public static userJourney = { public static userJourney = {
moreScreenUnlock: 1, moreScreenUnlock: 1,
@ -105,7 +97,14 @@ export default class Constants {
* In seconds * In seconds
*/ */
static zoomToLocationTimeout = 15 static zoomToLocationTimeout = 15
static countryCoderEndpoint: string = meta.config.country_coder_host private static readonly config = (() => {
const defaultConfig = packagefile.config
return { ...defaultConfig, ...extraconfig }
})()
public static ImgurApiKey = Constants.config.api_keys.imgur
public static readonly mapillary_client_token_v4 = Constants.config.api_keys.mapillary_v4
public static defaultOverpassUrls = Constants.config.default_overpass_urls
static countryCoderEndpoint: string = Constants.config.country_coder_host
/** /**
* These are the values that are allowed to use as 'backdrop' icon for a map pin * These are the values that are allowed to use as 'backdrop' icon for a map pin

View file

@ -1,43 +1,43 @@
import { Feature, Polygon } from "geojson" import { Feature, Polygon } from "geojson";
import * as editorlayerindex from "../assets/editor-layer-index.json" import * as editorlayerindex from "../assets/editor-layer-index.json";
import * as globallayers from "../assets/global-raster-layers.json" import * as globallayers from "../assets/global-raster-layers.json";
import { BBox } from "../Logic/BBox" import { BBox } from "../Logic/BBox";
import { Store, Stores } from "../Logic/UIEventSource" import { Store, Stores } from "../Logic/UIEventSource";
import { GeoOperations } from "../Logic/GeoOperations" import { GeoOperations } from "../Logic/GeoOperations";
import { RasterLayerProperties } from "./RasterLayerProperties" import { RasterLayerProperties } from "./RasterLayerProperties";
export class AvailableRasterLayers { export class AvailableRasterLayers {
public static EditorLayerIndex: (Feature<Polygon, EditorLayerIndexProperties> & public static EditorLayerIndex: (Feature<Polygon, EditorLayerIndexProperties> &
RasterLayerPolygon)[] = <any>editorlayerindex.features RasterLayerPolygon)[] = <any>editorlayerindex.features;
public static globalLayers: RasterLayerPolygon[] = globallayers.layers.map( public static globalLayers: RasterLayerPolygon[] = globallayers.layers.map(
(properties) => (properties) =>
<RasterLayerPolygon>{ <RasterLayerPolygon>{
type: "Feature", type: "Feature",
properties, properties,
geometry: BBox.global.asGeometry(), geometry: BBox.global.asGeometry()
} }
) );
public static readonly osmCartoProperties: RasterLayerProperties = { public static readonly osmCartoProperties: RasterLayerProperties = {
id: "osm", id: "osm",
name: "OpenStreetMap", name: "OpenStreetMap",
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: { attribution: {
text: "OpenStreetMap", text: "OpenStreetMap",
url: "https://openStreetMap.org/copyright", url: "https://openStreetMap.org/copyright"
}, },
best: true, best: true,
max_zoom: 19, max_zoom: 19,
min_zoom: 0, min_zoom: 0,
category: "osmbasedmap", category: "osmbasedmap"
} };
public static readonly osmCarto: RasterLayerPolygon = { public static readonly osmCarto: RasterLayerPolygon = {
type: "Feature", type: "Feature",
properties: AvailableRasterLayers.osmCartoProperties, properties: AvailableRasterLayers.osmCartoProperties,
geometry: BBox.global.asGeometry(), geometry: BBox.global.asGeometry()
} };
public static readonly maplibre: RasterLayerPolygon = { public static readonly maptilerDefaultLayer: RasterLayerPolygon = {
type: "Feature", type: "Feature",
properties: { properties: {
name: "MapTiler", name: "MapTiler",
@ -47,12 +47,43 @@ export class AvailableRasterLayers {
type: "vector", type: "vector",
attribution: { attribution: {
text: "Maptiler", text: "Maptiler",
url: "https://www.maptiler.com/copyright/", url: "https://www.maptiler.com/copyright/"
}, }
}, },
geometry: BBox.global.asGeometry(), geometry: BBox.global.asGeometry()
} };
public static readonly maptilerCarto: RasterLayerPolygon = {
type: "Feature",
properties: {
name: "MapTiler Carto",
url: "https://api.maptiler.com/maps/openstreetmap/style.json?key=GvoVAJgu46I5rZapJuAy",
category: "osmbasedmap",
id: "maptiler.carto",
type: "vector",
attribution: {
text: "Maptiler",
url: "https://www.maptiler.com/copyright/"
}
},
geometry: BBox.global.asGeometry()
};
public static readonly maptilerBackdrop: RasterLayerPolygon = {
type: "Feature",
properties: {
name: "MapTiler Backdrop",
url: "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy",
category: "osmbasedmap",
id: "maptiler.backdrop",
type: "vector",
attribution: {
text: "Maptiler",
url: "https://www.maptiler.com/copyright/"
}
},
geometry: BBox.global.asGeometry()
};
public static readonly americana: RasterLayerPolygon = { public static readonly americana: RasterLayerPolygon = {
type: "Feature", type: "Feature",
properties: { properties: {
@ -63,41 +94,43 @@ export class AvailableRasterLayers {
type: "vector", type: "vector",
attribution: { attribution: {
text: "Americana", text: "Americana",
url: "https://github.com/ZeLonewolf/openstreetmap-americana/", url: "https://github.com/ZeLonewolf/openstreetmap-americana/"
}, }
}, },
geometry: BBox.global.asGeometry(), geometry: BBox.global.asGeometry()
} };
public static layersAvailableAt( public static layersAvailableAt(
location: Store<{ lon: number; lat: number }> location: Store<{ lon: number; lat: number }>
): Store<RasterLayerPolygon[]> { ): Store<RasterLayerPolygon[]> {
const availableLayersBboxes = Stores.ListStabilized( const availableLayersBboxes = Stores.ListStabilized(
location.mapD((loc) => { location.mapD((loc) => {
const lonlat: [number, number] = [loc.lon, loc.lat] const lonlat: [number, number] = [loc.lon, loc.lat];
return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) => return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) =>
BBox.get(eliPolygon).contains(lonlat) BBox.get(eliPolygon).contains(lonlat)
) );
}) })
) );
const available = Stores.ListStabilized( const available = Stores.ListStabilized(
availableLayersBboxes.map((eliPolygons) => { availableLayersBboxes.map((eliPolygons) => {
const loc = location.data const loc = location.data;
const lonlat: [number, number] = [loc.lon, loc.lat] const lonlat: [number, number] = [loc.lon, loc.lat];
const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => { const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
if (eliPolygon.geometry === null) { if (eliPolygon.geometry === null) {
return true // global ELI-layer return true; // global ELI-layer
} }
return GeoOperations.inside(lonlat, eliPolygon) return GeoOperations.inside(lonlat, eliPolygon);
}) });
matching.unshift(AvailableRasterLayers.osmCarto) matching.push(...AvailableRasterLayers.globalLayers);
matching.unshift(AvailableRasterLayers.americana) matching.unshift(AvailableRasterLayers.maptilerDefaultLayer,
matching.unshift(AvailableRasterLayers.maplibre) AvailableRasterLayers.osmCarto,
matching.push(...AvailableRasterLayers.globalLayers) AvailableRasterLayers.maptilerCarto,
return matching AvailableRasterLayers.maptilerBackdrop,
AvailableRasterLayers.americana);
return matching;
}) })
) );
return available return available;
} }
} }
@ -115,22 +148,22 @@ export class RasterLayerUtils {
preferredCategory: string, preferredCategory: string,
ignoreLayer?: RasterLayerPolygon ignoreLayer?: RasterLayerPolygon
): RasterLayerPolygon { ): RasterLayerPolygon {
let secondBest: RasterLayerPolygon = undefined let secondBest: RasterLayerPolygon = undefined;
for (const rasterLayer of available) { for (const rasterLayer of available) {
if (rasterLayer === ignoreLayer) { if (rasterLayer === ignoreLayer) {
continue continue;
} }
const p = rasterLayer.properties const p = rasterLayer.properties;
if (p.category === preferredCategory) { if (p.category === preferredCategory) {
if (p.best) { if (p.best) {
return rasterLayer return rasterLayer;
} }
if (!secondBest) { if (!secondBest) {
secondBest = rasterLayer secondBest = rasterLayer;
} }
} }
} }
return secondBest return secondBest;
} }
} }
@ -146,11 +179,11 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
/** /**
* The name of the imagery source * The name of the imagery source
*/ */
readonly name: string readonly name: string;
/** /**
* Whether the imagery name should be translated * Whether the imagery name should be translated
*/ */
readonly i18n?: boolean readonly i18n?: boolean;
readonly type: readonly type:
| "tms" | "tms"
| "wms" | "wms"
@ -158,7 +191,7 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
| "scanex" | "scanex"
| "wms_endpoint" | "wms_endpoint"
| "wmts" | "wmts"
| "vector" /* Vector is not actually part of the ELI-spec, we add it for vector layers */ | "vector"; /* Vector is not actually part of the ELI-spec, we add it for vector layers */
/** /**
* A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories. * A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories.
*/ */
@ -170,53 +203,53 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
| "historicphoto" | "historicphoto"
| "qa" | "qa"
| "elevation" | "elevation"
| "other" | "other";
/** /**
* A URL template for imagery tiles * A URL template for imagery tiles
*/ */
readonly url: string readonly url: string;
readonly min_zoom?: number readonly min_zoom?: number;
readonly max_zoom?: number readonly max_zoom?: number;
/** /**
* explicit/implicit permission by the owner for use in OSM * explicit/implicit permission by the owner for use in OSM
*/ */
readonly permission_osm?: "explicit" | "implicit" | "no" readonly permission_osm?: "explicit" | "implicit" | "no";
/** /**
* A URL for the license or permissions for the imagery * A URL for the license or permissions for the imagery
*/ */
readonly license_url?: string readonly license_url?: string;
/** /**
* A URL for the privacy policy of the operator or false if there is no existing privacy policy for tis imagery. * A URL for the privacy policy of the operator or false if there is no existing privacy policy for tis imagery.
*/ */
readonly privacy_policy_url?: string | boolean readonly privacy_policy_url?: string | boolean;
/** /**
* A unique identifier for the source; used in imagery_used changeset tag * A unique identifier for the source; used in imagery_used changeset tag
*/ */
readonly id: string readonly id: string;
/** /**
* A short English-language description of the source * A short English-language description of the source
*/ */
readonly description?: string readonly description?: string;
/** /**
* The ISO 3166-1 alpha-2 two letter country code in upper case. Use ZZ for unknown or multiple. * The ISO 3166-1 alpha-2 two letter country code in upper case. Use ZZ for unknown or multiple.
*/ */
readonly country_code?: string readonly country_code?: string;
/** /**
* Whether this imagery should be shown in the default world-wide menu * Whether this imagery should be shown in the default world-wide menu
*/ */
readonly default?: boolean readonly default?: boolean;
/** /**
* Whether this imagery is the best source for the region * Whether this imagery is the best source for the region
*/ */
readonly best?: boolean readonly best?: boolean;
/** /**
* The age of the oldest imagery or data in the source, as an RFC3339 date or leading portion of one * The age of the oldest imagery or data in the source, as an RFC3339 date or leading portion of one
*/ */
readonly start_date?: string readonly start_date?: string;
/** /**
* The age of the newest imagery or data in the source, as an RFC3339 date or leading portion of one * The age of the newest imagery or data in the source, as an RFC3339 date or leading portion of one
*/ */
readonly end_date?: string readonly end_date?: string;
/** /**
* HTTP header to check for information if the tile is invalid * HTTP header to check for information if the tile is invalid
*/ */
@ -226,61 +259,61 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
* via the `patternProperty` "^.*$". * via the `patternProperty` "^.*$".
*/ */
[k: string]: string[] | null [k: string]: string[] | null
} };
/** /**
* 'true' if tiles are transparent and can be overlaid on another source * 'true' if tiles are transparent and can be overlaid on another source
*/ */
readonly overlay?: boolean & string readonly overlay?: boolean & string;
readonly available_projections?: string[] readonly available_projections?: string[];
readonly attribution?: { readonly attribution?: {
readonly url?: string readonly url?: string
readonly text?: string readonly text?: string
readonly html?: string readonly html?: string
readonly required?: boolean readonly required?: boolean
} };
/** /**
* A URL for an image, that can be displayed in the list of imagery layers next to the name * A URL for an image, that can be displayed in the list of imagery layers next to the name
*/ */
readonly icon?: string readonly icon?: string;
/** /**
* A link to an EULA text that has to be accepted by the user, before the imagery source is added. Can contain {lang} to be replaced by a current user language wiki code (like FR:) or an empty string for the default English text. * A link to an EULA text that has to be accepted by the user, before the imagery source is added. Can contain {lang} to be replaced by a current user language wiki code (like FR:) or an empty string for the default English text.
*/ */
readonly eula?: string readonly eula?: string;
/** /**
* A URL for an image, that is displayed in the mapview for attribution * A URL for an image, that is displayed in the mapview for attribution
*/ */
readonly "logo-image"?: string readonly "logo-image"?: string;
/** /**
* Customized text for the terms of use link (default is "Background Terms of Use") * Customized text for the terms of use link (default is "Background Terms of Use")
*/ */
readonly "terms-of-use-text"?: string readonly "terms-of-use-text"?: string;
/** /**
* Specify a checksum for tiles, which aren't real tiles. `type` is the digest type and can be MD5, SHA-1, SHA-256, SHA-384 and SHA-512, value is the hex encoded checksum in lower case. To create a checksum save the tile as file and upload it to e.g. https://defuse.ca/checksums.htm. * Specify a checksum for tiles, which aren't real tiles. `type` is the digest type and can be MD5, SHA-1, SHA-256, SHA-384 and SHA-512, value is the hex encoded checksum in lower case. To create a checksum save the tile as file and upload it to e.g. https://defuse.ca/checksums.htm.
*/ */
readonly "no-tile-checksum"?: string readonly "no-tile-checksum"?: string;
/** /**
* header-name attribute specifies a header returned by tile server, that will be shown as `metadata-key` attribute in Show Tile Info dialog * header-name attribute specifies a header returned by tile server, that will be shown as `metadata-key` attribute in Show Tile Info dialog
*/ */
readonly "metadata-header"?: string readonly "metadata-header"?: string;
/** /**
* Set to `true` if imagery source is properly aligned and does not need imagery offset adjustments. This is used for OSM based sources too. * Set to `true` if imagery source is properly aligned and does not need imagery offset adjustments. This is used for OSM based sources too.
*/ */
readonly "valid-georeference"?: boolean readonly "valid-georeference"?: boolean;
/** /**
* Size of individual tiles delivered by a TMS service * Size of individual tiles delivered by a TMS service
*/ */
readonly "tile-size"?: number readonly "tile-size"?: number;
/** /**
* Whether tiles status can be accessed by appending /status to the tile URL and can be submitted for re-rendering by appending /dirty. * Whether tiles status can be accessed by appending /status to the tile URL and can be submitted for re-rendering by appending /dirty.
*/ */
readonly "mod-tile-features"?: string readonly "mod-tile-features"?: string;
/** /**
* HTTP headers to be sent to server. It has two attributes header-name and header-value. May be specified multiple times. * HTTP headers to be sent to server. It has two attributes header-name and header-value. May be specified multiple times.
*/ */
readonly "custom-http-headers"?: { readonly "custom-http-headers"?: {
readonly "header-name"?: string readonly "header-name"?: string
readonly "header-value"?: string readonly "header-value"?: string
} };
/** /**
* Default layer to open (when using WMS_ENDPOINT type). Contains list of layer tag with two attributes - name and style, e.g. `"default-layers": ["layer": { name="Basisdata_NP_Basiskart_JanMayen_WMTS_25829" "style":"default" } ]` (not allowed in `mirror` attribute) * Default layer to open (when using WMS_ENDPOINT type). Contains list of layer tag with two attributes - name and style, e.g. `"default-layers": ["layer": { name="Basisdata_NP_Basiskart_JanMayen_WMTS_25829" "style":"default" } ]` (not allowed in `mirror` attribute)
*/ */
@ -291,17 +324,17 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
[k: string]: unknown [k: string]: unknown
} }
[k: string]: unknown [k: string]: unknown
}[] }[];
/** /**
* format to use when connecting tile server (when using WMS_ENDPOINT type) * format to use when connecting tile server (when using WMS_ENDPOINT type)
*/ */
readonly format?: string readonly format?: string;
/** /**
* If `true` transparent tiles will be requested from WMS server * If `true` transparent tiles will be requested from WMS server
*/ */
readonly transparent?: boolean & string readonly transparent?: boolean & string;
/** /**
* minimum expiry time for tiles in seconds. The larger the value, the longer entry in cache will be considered valid * minimum expiry time for tiles in seconds. The larger the value, the longer entry in cache will be considered valid
*/ */
readonly "minimum-tile-expire"?: number readonly "minimum-tile-expire"?: number;
} }

View file

@ -4,6 +4,7 @@ import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import { DesugaringStep, Each, Fuse, On } from "./Conversion" import { DesugaringStep, Each, Fuse, On } from "./Conversion"
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson" import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
import { del } from "idb-keyval";
export class UpdateLegacyLayer extends DesugaringStep< export class UpdateLegacyLayer extends DesugaringStep<
LayerConfigJson | string | { builtin; override } LayerConfigJson | string | { builtin; override }
@ -41,7 +42,6 @@ export class UpdateLegacyLayer extends DesugaringStep<
delete preset["preciseInput"] delete preset["preciseInput"]
} else if (preciseInput !== undefined) { } else if (preciseInput !== undefined) {
delete preciseInput["preferredBackground"] delete preciseInput["preferredBackground"]
console.log("Precise input:", preciseInput)
preset.snapToLayer = preciseInput.snapToLayer preset.snapToLayer = preciseInput.snapToLayer
delete preciseInput.snapToLayer delete preciseInput.snapToLayer
if (preciseInput.maxSnapDistance) { if (preciseInput.maxSnapDistance) {
@ -146,7 +146,6 @@ export class UpdateLegacyLayer extends DesugaringStep<
} }
const pr = <PointRenderingConfigJson>rendering const pr = <PointRenderingConfigJson>rendering
let iconSize = pr.iconSize let iconSize = pr.iconSize
console.log("Iconsize is", iconSize)
if (Object.keys(pr.iconSize).length === 1 && pr.iconSize["render"] !== undefined) { if (Object.keys(pr.iconSize).length === 1 && pr.iconSize["render"] !== undefined) {
iconSize = pr.iconSize["render"] iconSize = pr.iconSize["render"]
@ -198,6 +197,10 @@ class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
delete oldThemeConfig.socialImage delete oldThemeConfig.socialImage
} }
if(oldThemeConfig.defaultBackgroundId === "osm"){
console.log("Removing old background in", json.id)
}
if (oldThemeConfig["roamingRenderings"] !== undefined) { if (oldThemeConfig["roamingRenderings"] !== undefined) {
if (oldThemeConfig["roamingRenderings"].length == 0) { if (oldThemeConfig["roamingRenderings"].length == 0) {
delete oldThemeConfig["roamingRenderings"] delete oldThemeConfig["roamingRenderings"]

View file

@ -18,6 +18,8 @@ import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRende
import Validators from "../../../UI/InputElement/Validators" import Validators from "../../../UI/InputElement/Validators"
import TagRenderingConfig from "../TagRenderingConfig" import TagRenderingConfig from "../TagRenderingConfig"
import { parse as parse_html } from "node-html-parser" import { parse as parse_html } from "node-html-parser"
import PresetConfig from "../PresetConfig"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
class ValidateLanguageCompleteness extends DesugaringStep<any> { class ValidateLanguageCompleteness extends DesugaringStep<any> {
private readonly _languages: string[] private readonly _languages: string[]
@ -167,9 +169,9 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
json: LayoutConfigJson, json: LayoutConfigJson,
context: string context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[]; information: string[] } { ): { result: LayoutConfigJson; errors: string[]; warnings: string[]; information: string[] } {
const errors = [] const errors: string[] = []
const warnings = [] const warnings: string[] = []
const information = [] const information: string[] = []
const theme = new LayoutConfig(json, this._isBuiltin) const theme = new LayoutConfig(json, this._isBuiltin)
@ -245,7 +247,7 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
information information
) )
} }
const dups = Utils.Dupiclates(json.layers.map((layer) => layer["id"])) const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"]))
if (dups.length > 0) { if (dups.length > 0) {
errors.push( errors.push(
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}` `The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`
@ -275,6 +277,10 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
errors.push(e) errors.push(e)
} }
if (theme.id !== "personal") {
new DetectDuplicatePresets().convertJoin(theme, context, errors, warnings, information)
}
return { return {
result: json, result: json,
errors, errors,
@ -838,6 +844,15 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
} }
} }
const layerConfig = new LayerConfig(json, "validation", true)
for (const [attribute, code, isStrict] of layerConfig.calculatedTags ?? []) {
try {
new Function("feat", "return " + code + ";")
} catch (e) {
throw `Invalid function definition: the custom javascript is invalid:${e} (at ${context}). The offending javascript code is:\n ${code}`
}
}
if (json.source === "special") { if (json.source === "special") {
if (!Constants.priviliged_layers.find((x) => x == json.id)) { if (!Constants.priviliged_layers.find((x) => x == json.id)) {
errors.push( errors.push(
@ -880,7 +895,7 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
{ {
// duplicate ids in tagrenderings check // duplicate ids in tagrenderings check
const duplicates = Utils.Dedup( const duplicates = Utils.Dedup(
Utils.Dupiclates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))) Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"])))
) )
if (duplicates.length > 0) { if (duplicates.length > 0) {
console.log(json.tagRenderings) console.log(json.tagRenderings)
@ -976,7 +991,7 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
) )
} }
const duplicateIds = Utils.Dupiclates( const duplicateIds = Utils.Duplicates(
(json.tagRenderings ?? []) (json.tagRenderings ?? [])
?.map((f) => f["id"]) ?.map((f) => f["id"])
.filter((id) => id !== "questions") .filter((id) => id !== "questions")
@ -1234,3 +1249,68 @@ export class DetectDuplicateFilters extends DesugaringStep<{
} }
} }
} }
export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
constructor() {
super(
"Detects mappings which have identical (english) names or identical mappings.",
["presets"],
"DetectDuplicatePresets"
)
}
convert(
json: LayoutConfig,
context: string
): {
result: LayoutConfig
errors?: string[]
warnings?: string[]
information?: string[]
} {
const presets: PresetConfig[] = [].concat(...json.layers.map((l) => l.presets))
const errors = []
const enNames = presets.map((p) => p.title.textFor("en"))
if (new Set(enNames).size != enNames.length) {
const dups = Utils.Duplicates(enNames)
const layersWithDup = json.layers.filter((l) =>
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0)
)
const layerIds = layersWithDup.map((l) => l.id)
errors.push(
`At ${context}: this themes has multiple presets which are named:${dups}, namely layers ${layerIds.join(
", "
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`
)
}
const optimizedTags = <TagsFilter[]>presets.map((p) => new And(p.tags).optimize())
for (let i = 0; i < presets.length; i++) {
const presetATags = optimizedTags[i]
const presetA = presets[i]
for (let j = i + 1; j < presets.length; j++) {
const presetBTags = optimizedTags[j]
const presetB = presets[j]
if (
Utils.SameObject(presetATags, presetBTags) &&
Utils.sameList(
presetA.preciseInput.snapToLayers,
presetB.preciseInput.snapToLayers
)
) {
errors.push(
`At ${context}: this themes has multiple presets with the same tags: ${presetATags.asHumanString(
false,
false,
{}
)}, namely the preset '${presets[i].title.textFor("en")}' and '${presets[
j
].title.textFor("en")}'`
)
}
}
}
return { errors, result: json }
}
}

View file

@ -204,12 +204,6 @@ export default class LayerConfig extends WithContextLoader {
} }
const code = kv.substring(index + 1) const code = kv.substring(index + 1)
try {
new Function("feat", "return " + code + ";")
} catch (e) {
throw `Invalid function definition: the custom javascript is invalid:${e} (at ${context}). The offending javascript code is:\n ${code}`
}
this.calculatedTags.push([key, code, isStrict]) this.calculatedTags.push([key, code, isStrict])
} }
} }
@ -365,7 +359,7 @@ export default class LayerConfig extends WithContextLoader {
} }
{ {
const duplicateIds = Utils.Dupiclates(this.filters.map((f) => f.id)) const duplicateIds = Utils.Duplicates(this.filters.map((f) => f.id))
if (duplicateIds.length > 0) { if (duplicateIds.length > 0) {
throw `Some filters have a duplicate id: ${duplicateIds} (at ${context}.filters)` throw `Some filters have a duplicate id: ${duplicateIds} (at ${context}.filters)`
} }

View file

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

View file

@ -10,12 +10,13 @@
const dispatch = createEventDispatcher<{ click }>() const dispatch = createEventDispatcher<{ click }>()
export let clss: string | undefined = undefined export let clss: string | undefined = undefined
export let imageClass: string | undefined = undefined
</script> </script>
<SubtleButton <SubtleButton
on:click={() => dispatch("click")} on:click={() => dispatch("click")}
options={{ extraClasses: twMerge("flex items-center", clss) }} options={{ extraClasses: twMerge("flex items-center", clss) }}
> >
<ChevronLeftIcon class="h-12 w-12" slot="image" /> <ChevronLeftIcon class={imageClass ?? "h-12 w-12"} slot="image" />
<slot slot="message" /> <slot slot="message" />
</SubtleButton> </SubtleButton>

View file

@ -1,32 +0,0 @@
import BaseUIElement from "../BaseUIElement"
export class CenterFlexedElement extends BaseUIElement {
private _html: string
constructor(html: string) {
super()
this._html = html ?? ""
}
InnerRender(): string {
return this._html
}
AsMarkdown(): string {
return this._html
}
protected InnerConstructElement(): HTMLElement {
const e = document.createElement("div")
e.innerHTML = this._html
e.style.display = "flex"
e.style.height = "100%"
e.style.width = "100%"
e.style.flexDirection = "column"
e.style.flexWrap = "nowrap"
e.style.alignContent = "center"
e.style.justifyContent = "center"
e.style.alignItems = "center"
return e
}
}

Some files were not shown because too many files have changed in this diff Show more