forked from MapComplete/MapComplete
Merge branch 'develop'
This commit is contained in:
commit
7ace25d377
161 changed files with 3351 additions and 2461 deletions
|
@ -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
|
||||
|
|
2
.github/workflows/deploy_pietervdvn.yml
vendored
2
.github/workflows/deploy_pietervdvn.yml
vendored
|
@ -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:
|
||||
|
|
1
404.html
1
404.html
|
@ -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"/>
|
||||
|
|
|
@ -504,4 +504,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -363,5 +363,6 @@
|
|||
"fr": "Un vélo café est un café à destination des cyclistes avec, par exemple, des services tels qu’une 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
|
||||
}
|
||||
|
|
|
@ -5294,4 +5294,4 @@
|
|||
},
|
||||
"neededChangesets": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -386,4 +386,4 @@
|
|||
"accepts_debit_cards",
|
||||
"accepts_credit_cards"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -134,4 +134,4 @@
|
|||
"lineCap": "square"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -244,4 +244,4 @@
|
|||
"fr": "Une couche affichant les douches (publiques)",
|
||||
"ca": "Una capa que mostra dutxes (públiques)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
129
assets/layers/surveillance_camera/ALPR.svg
Normal file
129
assets/layers/surveillance_camera/ALPR.svg
Normal 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 |
2
assets/layers/surveillance_camera/ALPR.svg.license
Normal file
2
assets/layers/surveillance_camera/ALPR.svg.license
Normal file
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: Pieter Vander Vennet
|
||||
SPDX-License-Identifier: CC0-1.0
|
BIN
assets/layers/surveillance_camera/ALPR_Example.jpg
Normal file
BIN
assets/layers/surveillance_camera/ALPR_Example.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: synx508
|
||||
SPDX-License-Identifier: CC-BY-NC 2.0
|
BIN
assets/layers/surveillance_camera/ALPR_Example2.jpg
Normal file
BIN
assets/layers/surveillance_camera/ALPR_Example2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 115 KiB |
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: https://commons.wikimedia.org/wiki/User:Mbrickn
|
||||
SPDX-License-Identifier: CC-BY 4.0
|
30
assets/layers/surveillance_camera/license_info.json
Normal file
30
assets/layers/surveillance_camera/license_info.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -74,8 +74,7 @@
|
|||
"images",
|
||||
{
|
||||
"id": "plantnet",
|
||||
"render": "{plantnet_detection()}",
|
||||
"condition": "species:wikidata="
|
||||
"render": "{plantnet_detection()}"
|
||||
},
|
||||
{
|
||||
"id": "tree-species-wikidata",
|
||||
|
|
|
@ -23,7 +23,8 @@
|
|||
"_d=feat.properties._description?.replace(/</g,'<')?.replace(/>/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",
|
||||
|
|
|
@ -172,4 +172,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@
|
|||
"startLat": 52.99238,
|
||||
"startLon": 6.570614,
|
||||
"startZoom": 20,
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"defaultBackgroundId": "maptiler.backdrop",
|
||||
"layers": [
|
||||
{
|
||||
"builtin": "cycleways_and_roads",
|
||||
|
|
|
@ -1607,4 +1607,4 @@
|
|||
]
|
||||
},
|
||||
"credits": "joost schouppe"
|
||||
}
|
||||
}
|
|
@ -57,7 +57,6 @@
|
|||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"widenFactor": 1.5,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"layers": [
|
||||
"charging_station"
|
||||
]
|
||||
|
|
|
@ -465,4 +465,4 @@
|
|||
"toilet"
|
||||
],
|
||||
"credits": "Christian Neumann <christian@utopicode.de>"
|
||||
}
|
||||
}
|
|
@ -265,6 +265,6 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"defaultBackgroundId": "maptiler.backdrop",
|
||||
"credits": "L'imaginaire"
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
},
|
||||
"icon": "./assets/themes/drinking_water/logo.svg",
|
||||
"startLat": 50.8465573,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.351697,
|
||||
"startZoom": 16,
|
||||
"widenFactor": 2,
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
"eu": "Hezkuntza",
|
||||
"pl": "Edukacja"
|
||||
},
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLat": 0,
|
||||
"startLon": 0,
|
||||
"startZoom": 0,
|
||||
|
|
|
@ -19,4 +19,4 @@
|
|||
"startLat": 53.0565,
|
||||
"startLon": 8.7492,
|
||||
"startZoom": 11
|
||||
}
|
||||
}
|
|
@ -288,4 +288,4 @@
|
|||
}
|
||||
],
|
||||
"hideFromOverview": false
|
||||
}
|
||||
}
|
|
@ -69,4 +69,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -44,7 +44,7 @@
|
|||
"layers": [
|
||||
"ghost_bike"
|
||||
],
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"defaultBackgroundId": "maptiler.backdrop",
|
||||
"clustering": {
|
||||
"maxZoom": 0
|
||||
}
|
||||
|
|
|
@ -773,4 +773,4 @@
|
|||
"overpassMaxZoom": 15,
|
||||
"osmApiTileSize": 17,
|
||||
"credits": "Pieter Vander Vennet"
|
||||
}
|
||||
}
|
|
@ -27,7 +27,6 @@
|
|||
},
|
||||
"icon": "./assets/layers/doctors/doctors.svg",
|
||||
"startLat": 50.8465573,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.351697,
|
||||
"startZoom": 16,
|
||||
"widenFactor": 2,
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
},
|
||||
"icon": "./assets/layers/entrance/entrance.svg",
|
||||
"startLat": 51.17181,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.144383,
|
||||
"startZoom": 14,
|
||||
"widenFactor": 2,
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"widenFactor": 5,
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"defaultBackgroundId": "maptiler.backdrop",
|
||||
"layers": [
|
||||
"map"
|
||||
]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -41,6 +41,5 @@
|
|||
"layers": [
|
||||
"windturbine"
|
||||
],
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"credits": "Seppe Santens"
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -144,14 +144,7 @@
|
|||
"width": 5
|
||||
}
|
||||
],
|
||||
"presets": [
|
||||
{
|
||||
"tags": [
|
||||
"shop=yes",
|
||||
"dog=yes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"=presets": [],
|
||||
"source": {
|
||||
"=osmTags": {
|
||||
"and": [
|
||||
|
|
|
@ -71,4 +71,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -46,7 +46,6 @@
|
|||
"startLon": 9.9937,
|
||||
"startZoom": 13,
|
||||
"widenFactor": 1.5,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"clustering": {
|
||||
"maxZoom": 14,
|
||||
"minNeededElements": 100
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
},
|
||||
"icon": "./assets/themes/rainbow_crossings/logo.svg",
|
||||
"startLat": 50.8465573,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.351697,
|
||||
"startZoom": 16,
|
||||
"widenFactor": 2,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"startLon": 0,
|
||||
"startZoom": 0,
|
||||
"hideFromOverview": true,
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"defaultBackgroundId": "maptiler.backdrop",
|
||||
"layers": [
|
||||
{
|
||||
"builtin": "indoors",
|
||||
|
@ -412,4 +412,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -54,8 +54,8 @@
|
|||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"widenFactor": 2,
|
||||
"defaultBackgroundId": "osm",
|
||||
"defaultBackgroundId": "maptiler.carto",
|
||||
"layers": [
|
||||
"surveillance_camera"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -239,4 +239,4 @@
|
|||
"hideFromOverview": true,
|
||||
"enableMoreQuests": false,
|
||||
"enableShareScreen": false
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@
|
|||
"sameAs": "vending_machine"
|
||||
},
|
||||
"minzoom": 18,
|
||||
"=presets": [],
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -271,4 +271,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
3
config.json
Normal file
3
config.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"#": "Settings in this file override the `config`-section of `package.json`"
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
58
package-lock.json
generated
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
4
scripts/hetzner/config.json
Normal file
4
scripts/hetzner/config.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"#":"Some configuration tweaks specifically for hetzner",
|
||||
"country_coder_host": "https://countrycoder.mapcomplete.org/"
|
||||
}
|
21
scripts/hetzner/config/Caddyfile
Normal file
21
scripts/hetzner/config/Caddyfile
Normal 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
|
||||
}
|
||||
}
|
7
scripts/hetzner/config/csp-logger-config.json
Normal file
7
scripts/hetzner/config/csp-logger-config.json
Normal 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"]
|
||||
}
|
23
scripts/hetzner/deployHetzner.sh
Executable file
23
scripts/hetzner/deployHetzner.sh
Executable 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
|
|
@ -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
|
||||
|
|
67
src/Logic/Actors/PreferredRasterLayerSelector.ts
Normal file
67
src/Logic/Actors/PreferredRasterLayerSelector.ts
Normal 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
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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([
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
159
src/Logic/ImageProviders/ImageUploadManager.ts
Normal file
159
src/Logic/ImageProviders/ImageUploadManager.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
15
src/Logic/ImageProviders/ImageUploader.ts
Normal file
15
src/Logic/ImageProviders/ImageUploader.ts
Normal 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 }>;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
54
src/Logic/Osm/Actions/LinkImageAction.ts
Normal file
54
src/Logic/Osm/Actions/LinkImageAction.ts
Normal 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()
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue