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
- name: REUSE compliance check
uses: fsfe/reuse-action@v2
uses: fsfe/reuse-action@952281636420dd0b691786c93e9d3af06032f138
- name: create generated dir
run: mkdir ./assets/generated

View file

@ -89,7 +89,7 @@ jobs:
env:
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
if: ${{ success() && github.ref != 'refs/heads/develop' && github.ref != 'refs/heads/master' }}
with:

View file

@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<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/tagrendering.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": [
{
"if": "tourism=artwork",
"addExtraTags": [
"not:tourism:artwork="
],
"then": {
"en": "This bench has an integrated artwork",
"nl": "Deze bank heeft een geïntegreerd kunstwerk",
@ -902,7 +905,7 @@
}
},
{
"if": "tourism=",
"if": "not:tourism:artwork=yes",
"then": {
"en": "This bench does not have an integrated artwork",
"nl": "Deze bank heeft geen geïntegreerd kunstwerk",
@ -913,7 +916,18 @@
"cs": "Tato lavička nemá integrované umělecké dílo",
"he": "לספסל זה אין יצירת אמנות משולבת",
"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": {

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.",
"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, …"
}
},
"deletion": true
}

View file

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

View file

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

View file

@ -23,7 +23,7 @@
"osmTags": "amenity=recycling"
},
"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,
"title": {
@ -1569,4 +1569,4 @@
"enableRelocation": true,
"enableImproveAccuracy": true
}
}
}

View file

@ -20,7 +20,7 @@
}
},
"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 != '[]'"
],
"isShown": {

View file

@ -244,4 +244,4 @@
"fr": "Une couche affichant les douches (publiques)",
"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": [
"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": {
"en": "What kind of camera is this?",
@ -53,11 +79,7 @@
},
"mappings": [
{
"if": {
"and": [
"camera:type=fixed"
]
},
"if": "camera:type=fixed",
"then": {
"en": "A fixed (non-moving) camera",
"nl": "Een vaste camera",
@ -66,14 +88,11 @@
"de": "Eine fest montierte (nicht bewegliche) Kamera",
"ca": "Una càmera fixa (no movible)",
"es": "Cámara fija (no móvil)"
}
},
"icon": "./assets/themes/surveillance/cam_right.svg"
},
{
"if": {
"and": [
"camera:type=dome"
]
},
"if": "camera:type=dome",
"then": {
"en": "A dome camera (which can turn)",
"nl": "Een dome (bolvormige camera die kan draaien)",
@ -83,7 +102,8 @@
"de": "Eine Kuppelkamera (drehbar)",
"ca": "Càmera de cúpula (que pot girar)",
"es": "Cámara con domo (que se puede girar)"
}
},
"icon": "./assets/themes/surveillance/dome.svg"
},
{
"if": {
@ -595,6 +615,40 @@
"fr": "une caméra de surveillance fixée au mur"
},
"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": [
@ -602,6 +656,10 @@
"icon": {
"render": "./assets/themes/surveillance/logo.svg",
"mappings": [
{
"if": "surveillance:type=ALPR",
"then": "./assets/layers/surveillance_camera/ALPR.svg"
},
{
"if": "camera:type=dome",
"then": "./assets/themes/surveillance/dome.svg"
@ -619,15 +677,17 @@
"iconSize": {
"mappings": [
{
"if": "camera:type=dome",
"then": "50,50,center"
},
{
"if": "_direction:leftright~*",
"if": {
"and": [
"camera:type=fixed",
"surveillance:type=camera",
"_direction:leftright~*"
]
},
"then": "100,35,center"
}
],
"render": "50,50,center"
"render": "35,35,center"
},
"location": [
"point",
@ -638,7 +698,12 @@
"render": "calc({_direction:numerical}deg + 90deg)",
"mappings": [
{
"if": "camera:type=dome",
"if": {
"or": [
"camera:type=dome",
"surveillance:type=ALPR"
]
},
"then": "0"
},
{

View file

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

View file

@ -23,7 +23,8 @@
"_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_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": [
{
@ -103,6 +104,72 @@
"*": "{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",
"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,
"startLon": 6.570614,
"startZoom": 20,
"defaultBackgroundId": "CartoDB.Positron",
"defaultBackgroundId": "maptiler.backdrop",
"layers": [
{
"builtin": "cycleways_and_roads",

View file

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

View file

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

View file

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

View file

@ -265,6 +265,6 @@
]
}
],
"defaultBackgroundId": "CartoDB.Positron",
"defaultBackgroundId": "maptiler.backdrop",
"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."
},
"hideFromOverview": false,
"defaultBackgroundId": "CartoDB.Voyager",
"icon": "./assets/themes/cycle_infra/cycle-infra.svg",
"startLat": 51,
"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",
"icon": "./assets/themes/cyclofix/logo.svg",
"startLat": 0,
"defaultBackgroundId": "CartoDB.Voyager",
"startLon": 0,
"startZoom": 1,
"widenFactor": 2,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,6 @@
},
"icon": "./assets/layers/walls_and_buildings/walls_and_buildings.png",
"startLat": 50.8465573,
"defaultBackgroundId": "CartoDB.Voyager",
"startLon": 4.351697,
"startZoom": 16,
"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>
<meta charset="UTF-8">
<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/openinghourstable.css" rel="stylesheet"/>
<link href="./css/tagrendering.css" rel="stylesheet"/>
@ -16,8 +17,6 @@
<title>MapComplete</title>
<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">
<meta content="./assets/SocialImage.png" property="og:image">
<meta content="MapComplete - editable, thematic maps with OpenStreetMap" property="og:title">
@ -48,10 +47,12 @@
</head>
<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>
<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>
<script>

View file

@ -344,6 +344,8 @@
},
"useSearch": "Use the search above to see presets",
"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": {
"abbreviations": {
"friday": "Fri",
@ -380,6 +382,7 @@
"born": "Born: {value}",
"died": "Died: {value}"
},
"readMore": "Read the rest of the article",
"searchToShort": "Your search query is too short, enter a longer text",
"searchWikidata": "Search on Wikidata",
"wikipediaboxTitle": "Wikipedia"
@ -413,6 +416,22 @@
"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.",
"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!",
"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!",
@ -498,7 +517,9 @@
},
"plantDetection": {
"back": "Back to species overview",
"button": "Automatically detect the plant species using the AI of Plantnet.org",
"confirm": "Select species",
"done": "The species has been applied",
"error": "Something went wrong while detecting the tree species: {error}",
"howTo": {
"intro": "For optimal results,",
@ -515,7 +536,8 @@
"poweredByPlantnet": "Powered by <a href='https://plantnet.org' target='_blank'>plantnet.org</a>",
"querying": "Querying plantnet.org with {length} images",
"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": {
"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": {
"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?",
@ -8765,6 +8768,14 @@
},
"1": {
"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": {
@ -8858,6 +8869,18 @@
"question": "In which geographical direction does this camera film?",
"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": {
"mappings": {
"0": {
@ -9629,6 +9652,32 @@
},
"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": {
"mappings": {
"0": {

View file

@ -568,6 +568,9 @@
},
"1": {
"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?",

58
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "mapcomplete",
"version": "0.32.0",
"version": "0.33.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mapcomplete",
"version": "0.32.0",
"version": "0.33.1",
"license": "GPL-3.0-or-later",
"dependencies": {
"@rgossiaux/svelte-headlessui": "^1.0.2",
@ -18,12 +18,14 @@
"@turf/distance": "^6.5.0",
"@turf/length": "^6.5.0",
"@turf/turf": "^6.5.0",
"@types/dompurify": "^3.0.2",
"@types/showdown": "^2.0.0",
"chart.js": "^3.8.0",
"country-language": "^0.1.7",
"country-to-currency": "^1.0.10",
"csv-parse": "^5.1.0",
"doctest-ts-improved": "^0.8.8",
"dompurify": "^3.0.5",
"email-validator": "^2.0.4",
"escape-html": "^1.0.3",
"fake-dom": "^1.0.4",
@ -3799,6 +3801,14 @@
"@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": {
"version": "1.0.0",
"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",
"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": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/wikidata-sdk/-/wikidata-sdk-6.1.0.tgz",
@ -6009,10 +6024,9 @@
}
},
"node_modules/dompurify": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz",
"integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==",
"optional": true
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A=="
},
"node_modules/domutils": {
"version": "1.3.0",
@ -8394,6 +8408,12 @@
"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": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@ -16125,6 +16145,14 @@
"@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": {
"version": "1.0.0",
"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",
"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": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/wikidata-sdk/-/wikidata-sdk-6.1.0.tgz",
@ -17770,10 +17803,9 @@
}
},
"dompurify": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz",
"integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==",
"optional": true
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A=="
},
"domutils": {
"version": "1.3.0",
@ -19559,6 +19591,12 @@
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.27.1.tgz",
"integrity": "sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==",
"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",
"version": "0.32.0",
"version": "0.33.4",
"repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
@ -8,7 +8,8 @@
"main": "index.ts",
"type": "module",
"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` are the OAuth-2 credentials for the production-OSM server and the test-server.",
"Are you deploying your own instance? Register your application too.",
@ -115,12 +116,14 @@
"@turf/distance": "^6.5.0",
"@turf/length": "^6.5.0",
"@turf/turf": "^6.5.0",
"@types/dompurify": "^3.0.2",
"@types/showdown": "^2.0.0",
"chart.js": "^3.8.0",
"country-language": "^0.1.7",
"country-to-currency": "^1.0.10",
"csv-parse": "^5.1.0",
"doctest-ts-improved": "^0.8.8",
"dompurify": "^3.0.5",
"email-validator": "^2.0.4",
"escape-html": "^1.0.3",
"fake-dom": "^1.0.4",

View file

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

View file

@ -273,7 +273,6 @@ class GenerateSeries extends Script {
allFeatures = allFeatures.filter((f) => f.properties.metadata?.theme !== "EMPTY CS")
const centerpoints = allFeatures.map((f) => GeoOperations.centerpoint(f))
console.log("Found", centerpoints.length, " changesets in total")
const path = `${targetDir}/all_centerpoints.geojson`
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"
# This script ends every line with '&&' to chain everything. A failure will thus stop the build
npm run generate:editor-layer-index &&
npm run generate &&
# npm run generate:editor-layer-index &&
# npm run generate &&
npm run generate:layouts
if [ $? -ne 0 ]; then

View file

@ -50,8 +50,11 @@ async function fetchRegularLanguages() {
const result = await Utils.downloadJson(url, { "User-Agent": "MapComplete script" })
const bindings = <LanguageSpecResult[]>result.results.bindings
// Traditional chinese = 繁體中文 or 正體中文
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 punjabi = await fetchSpecial(58635, "pa_PK")
const Shahmukhi = await Wikidata.LoadWikidataEntryAsync(133800)

View file

@ -26,7 +26,7 @@ function asList(hist: Map<string, number>): ContributorList {
}
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 codeContributors = 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 { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
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.
// It spits out an overview of those to be used to load them
@ -395,10 +396,133 @@ class LayerOverviewUtils extends Script {
skippedLayers.length +
" 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
}
/**
* 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(
licensePaths: Set<string>,
sharedLayers: Map<string, LayerConfigJson>,
@ -436,6 +560,7 @@ class LayerOverviewUtils extends Script {
})
const skippedThemes: string[] = []
for (let i = 0; i < themeFiles.length; i++) {
const themeInfo = themeFiles[i]
const themePath = themeInfo.path
@ -443,6 +568,7 @@ class LayerOverviewUtils extends Script {
const targetPath =
LayerOverviewUtils.themePath + "/" + themePath.substring(themePath.lastIndexOf("/"))
const usedLayers = Array.from(
LayerOverviewUtils.extractLayerIdsFrom(themeFile, false)
).map((id) => LayerOverviewUtils.layerPath + id + ".json")
@ -504,6 +630,8 @@ class LayerOverviewUtils extends Script {
this.writeTheme(themeFile)
fixed.set(themeFile.id, themeFile)
this.extractJavascriptCode(themeFile)
} catch (e) {
console.error("ERROR: could not prepare theme " + themePath + " due to " + e)
throw e

View file

@ -200,6 +200,26 @@ function asLangSpan(t: Translation, tag = "span"): string {
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) {
Locale.language.setData(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)
)
.replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific)
.replace(/<!-- CSP -->/, generateCsp())
.replace(
/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s,
asLangSpan(layout.shortDescription)
@ -298,7 +319,12 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
async function createIndexFor(theme: LayoutConfig) {
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)
}

View file

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

View file

@ -34,8 +34,6 @@ function generateTagOverview(
return overview
}
function tagrenderingToTaginfoDescription(tr: TagRenderingConfig) {}
function generateLayerUsage(layer: LayerConfig, layout: LayoutConfig): any[] {
if (layer.name === undefined) {
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 {
FixLegacyTheme,
UpdateLegacyLayer,
} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"
import Translations from "../UI/i18n/Translations"
import { Translation } from "../UI/i18n/Translation"
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
} from "../src/Models/ThemeConfig/Conversion/LegacyJsonConvert"
import Translations from "../src/UI/i18n/Translations"
import { Translation } from "../src/UI/i18n/Translation"
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
/*
* 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.:",
"````",
'"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",',
" \"_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:",
new List([

View file

@ -1,52 +1,19 @@
import { FeatureSource } from "../FeatureSource"
import { UIEventSource } from "../../UIEventSource"
import { OsmTags } from "../../../Models/OsmFeature"
/**
* Constructs a UIEventStore for the properties of every Feature, indexed by id
*/
export default class FeaturePropertiesStore {
private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>()
public readonly aliases = new Map<string, string>()
constructor(...sources: FeatureSource[]) {
for (const source of sources) {
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.
* 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) {
changeMade = true
delete oldProperties[oldPropertiesKey]
// delete oldProperties[oldPropertiesKey]
}
}
@ -83,7 +50,48 @@ export default class FeaturePropertiesStore {
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 {
if (newId === undefined) {
// 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
this._elements.set(newId, element)
this.aliases.set(newId, oldId)
element.ping()
}

View file

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

View file

@ -4,8 +4,9 @@ import { IndexedFeatureSource, WritableFeatureSource } from "../FeatureSource"
import { UIEventSource } from "../../UIEventSource"
import { ChangeDescription } from "../../Osm/Actions/ChangeDescription"
import { OsmId, OsmTags } from "../../../Models/OsmFeature"
import { Feature } from "geojson"
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
import { Feature, Point } from "geojson"
import { TagUtils } from "../../Tags/TagUtils"
import FeaturePropertiesStore from "../Actors/FeaturePropertiesStore"
export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource {
// 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.
* 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[]>([])
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) {
const seenChanges = new Set<ChangeDescription>()
const features = this.features.data
constructor(
changes: Changes,
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 backend = changes.backend
changes.pendingChanges.addCallbackAndRunD((changes) => {
if (changes.length === 0) {
return
changes.pendingChanges.addCallbackAndRunD((changes) => self.handleChanges(changes))
}
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) {
feature.id = feature.properties.id
features.push(feature)
somethingChanged = true
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()
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) {
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
}
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()
}
})
somethingChanged ||= this.handleChange(change)
}
if (somethingChanged) {
this.features.ping()
}
}
}

View file

@ -20,7 +20,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
private options: {
bounds: Store<BBox>
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'
*/
@ -41,7 +41,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
constructor(options: {
bounds: Store<BBox>
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'
*/
@ -54,7 +54,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
this._bounds = options.bounds
this.allowedTags = options.allowedFeatures
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._patchRelations = options?.patchRelations ?? true
}

View file

@ -1,7 +1,6 @@
import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject"
import { UIEventSource } from "../../UIEventSource"
import { BBox } from "../../BBox"
import StaticFeatureSource from "../Sources/StaticFeatureSource"
import { Tiles } from "../../../Models/TileRange"
export default class FullNodeDatabaseSource {
@ -48,11 +47,7 @@ export default class FullNodeDatabaseSource {
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)
this.loadedTiles.set(tileId, nodesById)
}

View file

@ -771,7 +771,6 @@ export class GeoOperations {
const splitup = turf.lineSplit(<Feature<LineString>>toSplit, boundary)
const kept = []
for (const f of splitup.features) {
const ls = <Feature<LineString>>f
if (!GeoOperations.inside(GeoOperations.centerpointCoordinates(f), boundary)) {
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 BaseUIElement from "../../UI/BaseUIElement"
import { Utils } from "../../Utils"
import Constants from "../../Models/Constants"
import { LicenseInfo } from "./LicenseInfo"
import ImageProvider, { ProvidedImage } from "./ImageProvider";
import BaseUIElement from "../../UI/BaseUIElement";
import { Utils } from "../../Utils";
import Constants from "../../Models/Constants";
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 singleton = new Imgur()
public readonly defaultKeyPrefixes: string[] = ["image"]
public readonly maxFileSizeInMegabytes = 10
private constructor() {
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,
description: string,
blobs: FileList,
handleSuccessfullUpload: (imageURL: string) => Promise<void>,
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
) {
blob: File
): Promise<{ key: string, value: string }> {
const apiUrl = "https://api.imgur.com/3/image"
const apiKey = Constants.ImgurApiKey
@ -63,6 +33,7 @@ export class Imgur extends ImageProvider {
formData.append("title", title)
formData.append("description", description)
const settings: RequestInit = {
method: "POST",
body: formData,
@ -74,17 +45,9 @@ export class Imgur extends ImageProvider {
}
// Response contains stringified JSON
// Image URL available at response.data.link
fetch(apiUrl, settings)
.then(async function (response) {
const content = await response.json()
await handleSuccessfullUpload(content.data.link)
})
.catch((reason) => {
console.log("Uploading to IMGUR failed", reason)
// @ts-ignore
onFail(reason)
})
const response = await fetch(apiUrl, settings)
const content = await response.json()
return { key: "image", value: content.data.link }
}
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 { Utils } from "../Utils"
import { Store, UIEventSource } from "./UIEventSource"
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
/**
* 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 {
private static errorPrintCount = 0
private static readonly stopErrorOutputAt = 10
private static metataggingObject: any = undefined
private static retaggingFuncCache = new Map<
string,
((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.
* The given features should be part of the given layer
@ -298,6 +315,40 @@ export default class MetaTagging {
layer: LayerConfig,
helpers: Record<ExtraFuncType, (feature: Feature) => Function>
): (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
if (calculatedTags === undefined || calculatedTags.length === 0) {
return undefined

View file

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

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) {
this.trackStatistics = trackStatistics
this.mainObjectId = mainObjectId
if(mainObjectId === undefined || mainObjectId === null){
throw "OsmObject received '"+mainObjectId+"' as mainObjectId"
}
}
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
}
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`
const rawData = await Utils.downloadJsonCached(url, 1000)
parsed = OsmObject.ParseObjects(rawData.elements)

View file

@ -5,6 +5,7 @@ import Locale from "../../UI/i18n/Locale"
import Constants from "../../Models/Constants"
import { Changes } from "./Changes"
import { Utils } from "../../Utils"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore";
export interface ChangesetTag {
key: string
@ -13,7 +14,7 @@ export interface ChangesetTag {
}
export class ChangesetHandler {
private readonly allElements: { addAlias: (id0: String, id1: string) => void }
private readonly allElements: FeaturePropertiesStore
private osmConnection: OsmConnection
private readonly changes: Changes
private readonly _dryRun: Store<boolean>
@ -29,11 +30,11 @@ export class ChangesetHandler {
constructor(
dryRun: Store<boolean>,
osmConnection: OsmConnection,
allElements: { addAlias: (id0: string, id1: string) => void } | undefined,
allElements: FeaturePropertiesStore | { addAlias: (id0: string, id1: string) => void } | undefined,
changes: Changes
) {
this.osmConnection = osmConnection
this.allElements = allElements
this.allElements = <FeaturePropertiesStore> allElements
this.changes = changes
this._dryRun = dryRun
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"
export abstract class OsmObject {
private static defaultBackend = "https://www.openstreetmap.org/"
private static defaultBackend = "https://api.openstreetmap.org/"
protected static backendURL = OsmObject.defaultBackend
private static polygonFeatures = OsmObject.constructPolygonFeatures()
type: "node" | "way" | "relation"

View file

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

View file

@ -219,7 +219,7 @@ class RewriteMetaInfoTags extends SimpleMetaTagger {
move("changeset", "_last_edit:changeset")
move("timestamp", "_last_edit:timestamp")
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
}
}

View file

@ -198,7 +198,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
this.backgroundLayerId = QueryParameters.GetQueryParameter(
"background",
layoutToUse?.defaultBackgroundId ?? "osm",
layoutToUse?.defaultBackgroundId,
"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