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

@ -504,4 +504,4 @@
] ]
} }
] ]
} }

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

@ -5294,4 +5294,4 @@
}, },
"neededChangesets": 10 "neededChangesets": 10
} }
} }

View file

@ -386,4 +386,4 @@
"accepts_debit_cards", "accepts_debit_cards",
"accepts_credit_cards" "accepts_credit_cards"
] ]
} }

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

@ -134,4 +134,4 @@
"lineCap": "square" "lineCap": "square"
} }
] ]
} }

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": {
@ -1569,4 +1569,4 @@
"enableRelocation": true, "enableRelocation": true,
"enableImproveAccuracy": true "enableImproveAccuracy": true
} }
} }

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

@ -244,4 +244,4 @@
"fr": "Une couche affichant les douches (publiques)", "fr": "Une couche affichant les douches (publiques)",
"ca": "Una capa que mostra dutxes (públiques)" "ca": "Una capa que mostra dutxes (públiques)"
} }
} }

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

@ -172,4 +172,4 @@
} }
} }
] ]
} }

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

@ -1607,4 +1607,4 @@
] ]
}, },
"credits": "joost schouppe" "credits": "joost schouppe"
} }

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

@ -465,4 +465,4 @@
"toilet" "toilet"
], ],
"credits": "Christian Neumann <christian@utopicode.de>" "credits": "Christian Neumann <christian@utopicode.de>"
} }

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

@ -19,4 +19,4 @@
"startLat": 53.0565, "startLat": 53.0565,
"startLon": 8.7492, "startLon": 8.7492,
"startZoom": 11 "startZoom": 11
} }

View file

@ -288,4 +288,4 @@
} }
], ],
"hideFromOverview": false "hideFromOverview": false
} }

View file

@ -69,4 +69,4 @@
} }
} }
] ]
} }

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

@ -773,4 +773,4 @@
"overpassMaxZoom": 15, "overpassMaxZoom": 15,
"osmApiTileSize": 17, "osmApiTileSize": 17,
"credits": "Pieter Vander Vennet" "credits": "Pieter Vander Vennet"
} }

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,
@ -525,4 +524,4 @@
] ]
}, },
"enableDownload": true "enableDownload": true
} }

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

@ -71,4 +71,4 @@
} }
} }
] ]
} }

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",
@ -412,4 +412,4 @@
] ]
} }
] ]
} }

View file

@ -54,8 +54,8 @@
"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

@ -239,4 +239,4 @@
"hideFromOverview": true, "hideFromOverview": true,
"enableMoreQuests": false, "enableMoreQuests": false,
"enableShareScreen": false "enableShareScreen": false
} }

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,

View file

@ -271,4 +271,4 @@
] ]
} }
] ]
} }

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"
) )
} }

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