Merge branch 'develop'

This commit is contained in:
Pieter Vander Vennet 2024-01-07 18:23:12 +01:00
commit be0154bfe5
86 changed files with 1669 additions and 512 deletions

2
.gitignore vendored
View file

@ -25,6 +25,8 @@ index_*.ts
*.doctest.ts
service-worker.js
.env
src/assets/editor-layer-index.json
.vscode/*
!.vscode/settings.json

View file

@ -51,7 +51,7 @@
</div>
<script type="module" src="./src/notfound.ts"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-nx5O+otcqJoqMhdDt8jUzmia6ng81Z5zZozYr69TzPkOLjVhLKMxu5zHCV9/0MPn"></script>
</body>
</html>

View file

@ -22,7 +22,7 @@
]
},
"icon": {
"render": "addSmall:#000",
"render": "addSmall",
"mappings": [
{
"if": "detach=yes",

View file

@ -0,0 +1,59 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="251.000000pt" height="250.000000pt" viewBox="0 0 251.000000 250.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,250.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1410 2282 c-48 -24 -80 -75 -80 -127 l0 -42 -73 -5 c-108 -7 -147
-41 -147 -128 l0 -40 -321 0 c-326 0 -348 -2 -376 -39 -21 -27 -15 -79 12
-106 l24 -25 331 0 c326 0 330 0 330 -20 0 -32 26 -74 54 -89 22 -12 25 -21
28 -75 3 -54 5 -61 25 -64 17 -3 26 -16 40 -58 10 -33 25 -60 38 -66 11 -6 25
-18 31 -27 18 -24 71 32 91 96 12 38 22 53 37 55 18 3 21 11 24 63 3 55 6 62
35 80 21 13 37 36 47 63 11 33 19 42 37 42 l23 0 0 -296 c0 -192 4 -302 11
-316 14 -26 67 -48 101 -41 14 3 37 16 50 30 21 23 23 35 28 168 4 129 7 146
25 159 28 21 58 20 89 -5 l26 -20 0 -270 c0 -173 4 -269 10 -269 6 0 10 97 10
271 l0 271 -29 29 c-33 32 -67 37 -110 14 -41 -21 -51 -61 -51 -200 0 -105 -2
-124 -18 -138 -25 -23 -78 -21 -102 3 -19 19 -20 33 -20 315 l0 295 164 0
c159 0 166 1 192 24 35 30 37 87 5 120 -20 19 -34 21 -190 24 l-169 3 -4 129
c-3 112 -6 132 -24 157 -52 70 -132 92 -204 55z m157 -33 c43 -40 53 -78 53
-199 l0 -110 -25 0 c-21 0 -25 5 -25 30 0 44 -24 96 -54 116 -16 10 -55 19
-96 22 l-70 5 0 34 c0 46 24 90 63 114 43 27 117 21 154 -12z m-67 -209 c18
-18 20 -33 20 -165 0 -178 -4 -185 -105 -185 l-65 0 0 114 c0 66 -4 117 -10
121 -6 4 -10 -37 -10 -114 l0 -121 -67 0 c-55 0 -71 4 -90 22 -22 20 -23 28
-23 162 0 191 -5 186 187 186 130 0 145 -2 163 -20z m-390 -190 l0 -40 -319 0
c-277 0 -322 2 -335 16 -9 8 -16 21 -16 28 0 34 21 36 347 36 l323 0 0 -40z
m868 11 c7 -46 -16 -51 -220 -51 l-188 0 0 40 0 40 203 -2 202 -3 3 -24z
m-538 -261 l0 -40 -105 0 -105 0 0 40 0 40 105 0 105 0 0 -40z m-54 -96 c-3
-9 -9 -27 -12 -40 -5 -21 -12 -25 -37 -22 -25 2 -32 10 -43 41 l-13 37 56 0
c46 0 54 -3 49 -16z"/>
<path d="M2035 1419 c-10 -26 -1 -504 10 -502 14 4 19 493 6 506 -7 7 -12 5
-16 -4z"/>
<path d="M2120 1165 c0 -163 4 -255 10 -255 6 0 10 92 10 255 0 163 -4 255
-10 255 -6 0 -10 -92 -10 -255z"/>
<path d="M1324 1307 c-9 -25 0 -62 16 -62 10 0 15 10 15 34 0 36 -21 54 -31
28z"/>
<path d="M1304 1192 c-6 -4 -16 -17 -23 -30 -10 -18 -9 -23 4 -28 21 -9 45 14
45 42 0 25 -7 29 -26 16z"/>
<path d="M902 1123 c2 -13 13 -18 38 -18 25 0 36 5 38 18 3 14 -4 17 -38 17
-34 0 -41 -3 -38 -17z"/>
<path d="M1030 1119 c0 -17 5 -20 37 -17 27 2 39 8 41 21 3 14 -4 17 -37 17
-35 0 -41 -3 -41 -21z"/>
<path d="M1152 1123 c2 -13 13 -18 38 -18 25 0 36 5 38 18 3 14 -4 17 -38 17
-34 0 -41 -3 -38 -17z"/>
<path d="M796 990 c-57 -59 -66 -73 -56 -85 11 -13 67 -15 380 -15 203 0 375
3 384 6 29 11 16 39 -47 102 l-63 62 -265 0 -266 0 -67 -70z m609 -10 l39 -40
-319 0 -319 0 39 40 39 40 241 0 241 0 39 -40z"/>
<path d="M741 851 c-8 -5 -11 -16 -8 -25 6 -14 48 -16 396 -16 321 0 390 2
394 14 13 34 -13 36 -392 36 -207 0 -382 -4 -390 -9z"/>
<path d="M735 760 c-17 -27 18 -30 396 -30 382 0 389 0 389 20 0 20 -7 20
-389 20 -249 0 -392 -4 -396 -10z"/>
<path d="M734 677 c-3 -8 -4 -54 -2 -103 l3 -89 389 -3 c305 -2 391 1 398 10
13 21 10 172 -4 186 -18 18 -777 17 -784 -1z m746 -97 l0 -60 -350 0 -350 0 0
60 0 60 350 0 350 0 0 -60z"/>
<path d="M580 420 c0 -7 197 -10 571 -10 377 0 568 3 564 10 -4 6 -205 10
-571 10 -369 0 -564 -3 -564 -10z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Verbund Offener Werkstätten
SPDX-License-Identifier: CC-By-SA

View file

@ -0,0 +1,57 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="251.000000pt" height="251.000000pt" viewBox="0 0 251.000000 251.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,251.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M734 2059 c-11 -19 9 -28 71 -31 l60 -3 38 -66 38 -67 -36 -72 c-25
-49 -41 -70 -50 -66 -57 21 -139 29 -201 19 -175 -30 -302 -155 -333 -328 -46
-257 191 -496 451 -454 163 26 284 139 324 302 l17 67 76 0 76 0 120 239 c65
132 122 238 126 235 4 -2 21 -29 38 -59 l31 -54 -55 -52 c-141 -133 -166 -344
-60 -505 88 -134 249 -202 403 -170 81 17 120 35 180 84 132 106 180 311 109
464 -30 67 -101 147 -162 183 -95 55 -228 68 -332 31 -50 -18 -39 -28 -128
132 -72 130 -105 167 -158 177 -52 10 -102 2 -102 -15 0 -11 15 -17 53 -21 65
-8 96 -28 122 -79 11 -21 20 -42 20 -45 0 -3 -109 -4 -242 -3 l-242 3 -32 55
c-18 30 -33 58 -34 63 0 4 14 7 30 7 23 0 30 4 30 20 0 19 -7 20 -119 20 -76
0 -122 -4 -127 -11z m630 -411 c-58 -117 -107 -214 -109 -216 -1 -2 -49 79
-105 180 -56 101 -111 198 -122 216 l-20 32 231 0 231 0 -106 -212z m-275 -20
c61 -108 114 -203 117 -212 5 -13 -2 -16 -43 -16 l-50 0 -12 58 c-23 103 -99
217 -175 259 -16 9 -16 13 10 66 16 31 31 54 35 49 4 -4 57 -95 118 -204z
m-277 92 c35 -10 36 -12 26 -34 -9 -21 -14 -23 -42 -15 -17 5 -56 9 -87 9
-208 0 -352 -209 -279 -404 81 -213 357 -262 508 -91 38 44 72 117 72 157 0
13 8 18 26 18 26 0 27 -1 21 -45 -14 -95 -98 -204 -196 -252 -49 -25 -67 -28
-151 -28 -84 1 -101 4 -148 28 -75 39 -124 87 -163 159 -31 59 -34 69 -34 158
0 76 4 104 22 142 38 84 125 163 212 194 44 16 162 18 213 4z m1112 -11 c113
-42 208 -166 222 -289 13 -119 -51 -259 -150 -327 -163 -111 -390 -68 -495 95
-89 139 -73 314 39 436 46 49 63 56 79 29 7 -14 2 -24 -29 -51 -80 -68 -116
-193 -85 -298 86 -298 494 -298 580 0 40 139 -23 279 -157 346 -50 24 -74 30
-128 30 -37 0 -82 -5 -100 -11 -27 -10 -34 -9 -42 4 -14 25 -10 31 25 44 54
20 179 16 241 -8z m-971 -73 c54 -52 95 -128 104 -191 5 -43 5 -45 -19 -45
-22 0 -26 6 -32 41 -9 59 -41 114 -92 163 -31 30 -43 48 -38 60 10 26 29 19
77 -28z m-147 11 c5 -4 -21 -65 -55 -136 -35 -70 -61 -133 -57 -140 5 -7 54
-11 153 -11 l145 0 -7 -37 c-16 -91 -101 -183 -194 -210 -207 -60 -402 126
-351 336 20 85 93 166 177 197 39 15 175 15 189 1z m1117 -17 c206 -104 198
-408 -12 -504 -63 -28 -169 -28 -232 0 -140 65 -203 238 -136 376 28 58 79
114 95 104 5 -3 38 -57 73 -120 36 -64 72 -117 82 -120 32 -8 19 32 -49 150
-36 64 -64 118 -62 120 33 32 173 29 241 -6z m-1018 -47 c40 -39 85 -123 85
-160 0 -23 -2 -23 -120 -23 -66 0 -120 2 -120 5 0 11 105 215 111 215 3 0 23
-17 44 -37z"/>
<path d="M765 881 c-45 -21 -97 -69 -92 -85 2 -6 43 -13 97 -16 l93 -5 26 -47
26 -48 -26 -47 -26 -48 -98 -5 c-69 -3 -100 -9 -103 -18 -8 -24 75 -90 135
-108 87 -25 149 -8 221 62 l56 54 179 0 180 0 47 -49 c37 -39 60 -53 106 -67
218 -63 384 194 236 367 -46 53 -101 79 -172 79 -70 0 -124 -25 -172 -78 l-38
-42 -187 0 -186 0 -35 41 c-20 23 -54 50 -76 60 -53 24 -139 24 -191 0z m171
-40 c23 -10 50 -31 60 -46 48 -68 34 -65 258 -65 l204 0 37 46 c42 51 101 83
153 84 64 0 138 -44 168 -102 18 -35 18 -131 0 -166 -66 -127 -243 -138 -321
-20 l-25 38 -213 0 -213 0 -38 -46 c-21 -25 -54 -52 -72 -60 -53 -22 -123 -18
-169 11 l-40 24 75 1 c41 0 80 4 86 8 6 4 26 35 45 69 l34 62 -29 57 c-42 80
-48 84 -124 85 l-67 1 40 19 c51 24 99 24 151 0z"/>
<path d="M1556 744 c-14 -26 -26 -54 -26 -64 0 -10 12 -38 26 -64 l26 -46 63
0 c69 0 87 12 115 80 14 33 4 70 -32 115 -17 22 -27 25 -83 25 l-63 0 -26 -46z
m151 -29 c16 -33 16 -37 0 -70 -15 -31 -21 -35 -56 -35 -34 0 -42 4 -60 36
l-21 36 21 34 c18 29 26 34 60 34 35 0 41 -4 56 -35z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Verbund Offener Werkstätten
SPDX-License-Identifier: CC-By-SA

View file

@ -0,0 +1,52 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="250.000000pt" height="250.000000pt" viewBox="0 0 250.000000 250.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,250.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M995 2133 c-139 -22 -328 -97 -426 -169 l-49 -36 0 -638 c0 -572 2
-638 16 -644 9 -3 195 -6 415 -6 348 0 399 -2 399 -15 0 -12 -14 -15 -64 -15
-35 0 -71 -3 -80 -6 -27 -10 -20 -62 13 -95 29 -29 31 -29 145 -29 l115 0 3
-82 3 -83 100 0 100 0 3 83 3 82 104 0 c59 0 115 5 130 12 46 21 73 99 39 112
-9 3 -47 6 -85 6 l-69 0 0 36 c0 28 9 46 36 77 62 69 89 139 89 237 0 100 -25
165 -94 242 -64 72 -67 95 -26 197 49 119 48 121 -25 121 l-60 0 0 204 0 204
-49 36 c-70 52 -222 121 -321 147 -84 21 -291 34 -365 22z m362 -68 c96 -25
206 -73 285 -128 l48 -32 0 -192 0 -193 -25 0 -25 0 0 174 c0 148 -3 177 -16
191 -32 31 -179 97 -273 122 -137 37 -315 37 -453 0 -118 -31 -276 -109 -283
-141 -3 -11 -4 -268 -3 -571 l3 -550 344 -3 344 -2 23 -25 23 -25 -394 0 -395
0 0 608 0 607 48 32 c216 147 488 193 749 128z m-109 -60 c85 -8 179 -38 280
-87 l82 -41 0 -179 0 -178 -25 0 c-24 0 -24 2 -27 83 l-3 82 -68 3 c-91 4 -97
-2 -97 -94 l0 -74 -34 0 c-19 0 -38 -4 -41 -10 -3 -5 10 -47 30 -92 19 -45 35
-96 35 -115 l0 -33 -95 0 c-84 0 -95 2 -95 18 0 9 8 22 19 29 28 17 61 94 61
141 0 56 -15 90 -57 134 l-37 38 28 59 c16 32 25 64 22 70 -5 7 -55 11 -147
11 -159 0 -155 3 -112 -85 l27 -55 -31 -29 c-60 -54 -80 -124 -58 -198 8 -27
28 -62 45 -78 47 -45 40 -55 -35 -55 l-64 0 3 133 c2 72 1 138 -1 145 -4 8
-30 12 -84 12 l-78 0 0 -165 0 -165 331 0 330 0 -37 -41 c-52 -58 -77 -121
-83 -211 l-5 -78 -103 0 c-114 0 -125 7 -78 51 31 29 45 53 55 97 13 57 -1 62
-181 62 -87 0 -165 -3 -174 -6 -34 -13 -7 -113 43 -158 36 -32 27 -45 -36 -48
-50 -3 -58 -6 -58 -23 0 -19 8 -20 275 -23 l275 -3 21 -45 21 -44 -323 0 -324
0 0 558 0 559 83 41 c45 22 114 50 153 62 74 22 214 39 274 34 19 -2 64 -6 98
-9z m-78 -280 c0 -3 -9 -23 -20 -45 -28 -54 -26 -66 20 -107 70 -63 77 -142
18 -208 -29 -34 -32 -35 -102 -35 -79 0 -106 12 -131 60 -31 60 -15 128 44
184 35 33 40 74 16 116 -8 14 -14 28 -15 33 0 4 38 7 85 7 47 0 85 -2 85 -5z
m350 -145 l0 -60 -40 0 -40 0 0 60 0 60 40 0 40 0 0 -60z m-714 -175 c-1 -125
-9 -146 -48 -128 -21 9 -22 16 -24 126 l-1 117 37 0 38 0 -2 -115z m985 53
c-8 -23 -11 -23 -202 -26 -174 -2 -195 -1 -206 15 -7 9 -13 20 -13 25 0 4 97
8 215 8 l214 0 -8 -22z m-38 -96 c-22 -68 -12 -110 43 -174 40 -46 84 -116 84
-133 0 -3 -133 -5 -295 -5 -199 0 -295 3 -295 10 0 21 31 70 75 120 57 65 72
115 51 178 -9 25 -16 47 -16 49 0 2 83 3 184 3 l184 0 -15 -48z m-613 -72 c0
-18 -7 -20 -60 -20 -53 0 -60 2 -60 20 0 18 7 20 60 20 53 0 60 -2 60 -20z
m-80 -236 c0 -11 -23 -50 -43 -71 -18 -20 -30 -23 -103 -23 -82 0 -83 0 -107
33 -51 68 -53 67 108 67 80 0 145 -3 145 -6z m830 -96 c0 -83 -25 -149 -80
-211 l-32 -37 -192 0 -192 0 -36 38 c-40 40 -72 104 -82 165 l-7 37 286 0
c183 0 285 4 285 10 0 6 -102 10 -285 10 -157 0 -285 3 -285 8 0 4 3 17 6 30
l6 22 304 0 304 0 0 -72z m-910 -43 c0 -12 -13 -15 -60 -15 -47 0 -60 3 -60
15 0 12 13 15 60 15 47 0 60 -3 60 -15z m790 -265 l0 -40 -185 0 -185 0 0 40
0 40 185 0 185 0 0 -40z m141 -110 c-22 -19 -40 -20 -328 -20 -292 0 -343 4
-343 30 0 6 131 10 348 10 346 -1 347 -1 323 -20z m-271 -120 l0 -60 -60 0
-60 0 0 60 0 60 60 0 60 0 0 -60z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Verbund Offener Werkstätten
SPDX-License-Identifier: CC-By-SA

View file

@ -0,0 +1,64 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="250.000000pt" height="250.000000pt" viewBox="0 0 250.000000 250.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,250.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M612 2138 c-8 -8 -9 -15 -1 -25 9 -10 113 -13 535 -13 601 0 544 11
544 -104 l0 -66 -610 0 c-400 0 -610 -3 -610 -10 0 -7 210 -10 610 -10 l610 0
0 -33 c0 -70 34 -67 -662 -67 -343 0 -627 -3 -631 -6 -3 -3 -3 -12 0 -20 4
-12 64 -14 353 -14 l348 0 4 -74 c4 -79 19 -109 66 -127 19 -7 22 -16 22 -67
0 -54 2 -60 40 -97 l40 -39 0 -311 0 -311 48 -47 c26 -26 52 -47 58 -47 5 0
31 22 57 48 l47 48 0 310 0 310 40 39 c38 37 40 43 40 97 0 51 3 60 23 67 46
18 61 48 65 126 4 69 6 74 32 85 42 17 50 48 50 185 0 117 -1 126 -24 152
l-24 28 -529 3 c-401 2 -531 0 -541 -10z m988 -436 c0 -40 -5 -73 -12 -80 -17
-17 -409 -17 -426 0 -7 7 -12 40 -12 80 l0 68 225 0 225 0 0 -68z m-80 -189
c0 -39 -5 -52 -32 -80 -31 -32 -35 -33 -112 -33 -76 0 -82 1 -113 32 -28 27
-33 39 -33 80 l0 48 145 0 145 0 0 -47z m-80 -196 c0 -27 -11 -43 -58 -90 -32
-31 -61 -57 -65 -57 -4 0 -7 41 -7 90 l0 90 65 0 65 0 0 -33z m-64 -216 l-66
-66 0 45 c0 41 5 49 63 108 l62 63 3 -42 c3 -40 0 -46 -62 -108z m7 -123 c-31
-32 -61 -58 -65 -58 -4 0 -8 19 -8 42 0 39 6 48 63 106 l62 63 3 -47 c3 -46 1
-49 -55 -106z m-5 -126 l-63 -61 -3 41 c-3 39 0 45 60 105 l63 64 3 -44 c3
-42 1 -47 -60 -105z m30 -119 l-32 -33 -30 30 -30 30 59 60 60 61 3 -57 c3
-53 1 -59 -30 -91z"/>
<path d="M879 1449 c-11 -11 -18 -27 -15 -36 3 -8 6 -19 6 -24 0 -17 50 -30
71 -19 28 15 35 57 14 80 -23 26 -50 25 -76 -1z m61 -14 c18 -21 5 -45 -25
-45 -20 0 -25 5 -25 23 0 39 26 50 50 22z"/>
<path d="M1784 1459 c-16 -21 -164 -396 -164 -416 0 -35 20 7 100 206 74 185
92 246 64 210z"/>
<path d="M870 1209 c0 -18 281 -374 298 -378 13 -2 14 0 6 10 -6 8 -75 96
-153 197 -78 100 -144 182 -147 182 -2 0 -4 -5 -4 -11z"/>
<path d="M1907 1154 c-70 -35 -125 -69 -122 -74 7 -11 265 115 265 130 0 16
-7 13 -143 -56z"/>
<path d="M831 996 c-15 -18 -4 -46 19 -46 28 0 45 27 30 45 -16 19 -34 19 -49
1z"/>
<path d="M273 924 c-11 -28 18 -34 174 -34 l153 0 5 -27 c40 -192 104 -296
239 -384 167 -110 395 -110 562 0 135 88 199 192 239 384 l5 27 278 0 c152 0
283 4 291 9 8 5 11 16 8 25 -6 14 -40 16 -305 16 -218 0 -301 -3 -310 -12 -7
-7 -12 -28 -12 -47 0 -97 -57 -218 -140 -301 -187 -187 -483 -187 -670 0 -83
83 -140 204 -140 301 0 19 -5 40 -12 47 -8 8 -64 12 -185 12 -150 0 -174 -2
-180 -16z"/>
<path d="M421 826 c-16 -19 -10 -66 10 -79 26 -16 68 0 75 30 4 14 2 33 -3 42
-13 21 -66 26 -82 7z m65 -21 c7 -18 -13 -45 -33 -45 -17 0 -27 24 -19 45 7
20 45 19 52 0z"/>
<path d="M2040 821 c-15 -28 -12 -46 8 -64 27 -24 59 -21 78 8 15 23 15 27 0
50 -21 31 -70 35 -86 6z m68 -29 c4 -28 -24 -41 -44 -20 -20 20 -7 50 20 46
13 -2 22 -12 24 -26z"/>
<path d="M870 791 c0 -10 277 -131 300 -131 29 0 -8 20 -147 79 -151 65 -153
66 -153 52z"/>
<path d="M1748 775 c-17 -37 3 -75 40 -75 35 0 52 16 52 50 0 34 -17 50 -52
50 -21 0 -32 -7 -40 -25z m67 -25 c0 -18 -6 -26 -23 -28 -13 -2 -25 3 -28 12
-10 26 4 48 28 44 17 -2 23 -10 23 -28z"/>
<path d="M1923 633 c-19 -7 -16 -50 3 -57 19 -7 44 10 44 30 0 16 -30 33 -47
27z"/>
<path d="M1635 570 c-26 -29 -24 -79 4 -102 70 -56 164 39 101 102 -26 26 -81
26 -105 0z m89 -16 c21 -20 20 -43 -1 -66 -32 -36 -83 -17 -83 32 0 14 5 31
12 38 17 17 54 15 72 -4z"/>
<path d="M1426 414 c-29 -29 -10 -94 27 -94 25 0 57 32 57 55 0 23 -32 55 -57
55 -6 0 -19 -7 -27 -16z m58 -27 c8 -12 7 -21 -5 -32 -21 -21 -49 -9 -49 20 0
36 34 44 54 12z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Verbund Offener Werkstätten
SPDX-License-Identifier: CC-By-SA

View file

@ -243,7 +243,8 @@
"sourceString": [
"device-key",
"{device-name}",
"{negative-name}"
"{negative-name}",
"{icon}"
],
"into": [
[
@ -261,7 +262,8 @@
"de": "3D-Drucker",
"ca": "Impressora 3D",
"cs": "3D-tiskárna"
}
},
"./assets/layers/hackerspace/3d_printer.svg"
],
[
"lasercutter",
@ -278,7 +280,8 @@
"de": "Laserschneider",
"ca": "tallador laser",
"cs": "laserová řezačka"
}
},
"./assets/layers/hackerspace/lasercutter.svg"
],
[
"cnc_drilling_machine",
@ -295,7 +298,67 @@
"de": "CNC-Fräse",
"ca": "trepant CNC",
"cs": "CNC vrtačka"
}
},
"./assets/layers/hackerspace/cnc.svg"
],
[
"media_studio",
{
"en": "a multimedia studio"
},
{
"en": "multimedia studio"
},
"./assets/layers/hackerspace/media_studio.svg"
],
[
"sewing_machine",
{
"en": "a sewing machine"
},
{
"en": "sewing machine"
},
"./assets/layers/hackerspace/sewing_machine.svg"
],
[
"workshop:wood",
{
"en": "a woodworking workshop"
},
{
"en": "woodworking workshop"
},
"./assets/layers/hackerspace/woodworking.svg"
], [
"workshop:ceramics",
{
"en": "a ceramics workshop"
},
{
"en": "ceramics workshop"
},
"./assets/layers/hackerspace/ceramics.svg"
],
[
"workshop:metal",
{
"en": "a metal workshop"
},
{
"en": "meta workshop"
},
"./assets/layers/hackerspace/metal.svg"
],
[
"bicycle:diy",
{
"en": "a bicycle repair workshop"
},
{
"en": "bicycle repair workshop"
},
"./assets/layers/hackerspace/bicycle.svg"
]
]
},
@ -308,9 +371,11 @@
"ca": "Hi ha {device-name} disponible a aquest espai hacker?",
"cs": "Je {device-name} dostupné v tomto hackerspace?"
},
"#iconsize": "large",
"mappings": [
{
"if": "service:device-key=yes",
"icon": "{icon}",
"then": {
"en": "There is {device-name} available at this hackerspace",
"nl": "Er is {device-name} beschikbaar in deze hackerspace",

View file

@ -0,0 +1,57 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="251.000000pt" height="250.000000pt" viewBox="0 0 251.000000 250.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,250.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1030 2000 l0 -60 -264 0 c-264 0 -265 0 -292 -24 -39 -33 -39 -89 0
-122 27 -24 28 -24 292 -24 l264 0 0 -40 0 -40 40 0 40 0 0 -58 c0 -49 5 -65
36 -112 84 -127 118 -139 175 -62 77 102 79 107 79 170 l0 61 43 3 c41 3 42 4
45 41 l3 37 254 0 c269 0 294 4 314 47 18 39 13 63 -18 94 l-29 29 -261 0
-260 0 -3 58 -3 57 -227 3 -228 2 0 -60z m410 -125 l0 -145 -185 0 -185 0 0
145 0 145 185 0 185 0 0 -145z m-410 -25 l0 -40 -251 0 c-153 0 -258 4 -270
10 -18 10 -26 50 -12 63 3 4 125 7 270 7 l263 0 0 -40z m993 18 c9 -13 7 -22
-7 -37 -18 -20 -29 -21 -273 -21 l-253 0 0 40 0 40 261 -2 c233 -3 263 -5 272
-20z m-663 -228 l0 -50 -105 0 -105 0 0 50 0 50 105 0 105 0 0 -50z m-20 -75
c0 -3 -12 -23 -26 -45 -22 -32 -33 -40 -58 -40 -22 0 -35 7 -45 23 -8 12 -21
32 -29 45 l-14 22 86 0 c47 0 86 -2 86 -5z m-73 -131 c-3 -3 -12 -4 -19 -1 -8
3 -5 6 6 6 11 1 17 -2 13 -5z"/>
<path d="M1120 1875 l0 -96 53 3 52 3 0 90 0 90 -52 3 -53 3 0 -96z m80 0 l0
-75 -30 0 -30 0 0 75 0 75 30 0 30 0 0 -75z"/>
<path d="M1248 1133 l-3 -246 -39 97 c-37 90 -51 114 -62 104 -2 -3 14 -47 36
-98 22 -52 40 -96 40 -99 0 -3 -25 19 -55 49 -30 29 -58 51 -62 47 -4 -4 18
-33 48 -63 61 -63 69 -63 -79 0 -94 40 -100 24 -7 -18 157 -71 151 -65 53 -65
-52 -1 -88 -5 -88 -11 0 -6 40 -10 100 -10 l100 0 0 -56 c0 -48 -4 -59 -29
-85 l-29 -29 -326 0 c-179 0 -326 3 -325 8 0 4 56 77 123 162 l122 155 87 3
c79 3 87 5 87 22 0 19 -7 20 -98 20 l-99 0 -147 -190 c-89 -115 -146 -197
-144 -208 3 -16 29 -17 331 -20 l327 -2 0 -45 0 -45 -240 0 c-153 0 -240 -4
-240 -10 0 -7 223 -10 650 -10 427 0 650 3 650 10 0 6 -137 10 -391 10 l-390
0 3 46 3 45 441 -1 c242 0 448 2 457 6 10 3 17 10 17 15 0 5 -59 97 -130 205
l-131 195 -97 -3 c-78 -2 -97 -6 -97 -18 0 -12 18 -16 86 -18 l86 -3 107 -159
c58 -88 106 -162 106 -165 0 -3 -171 -5 -379 -5 l-379 0 19 37 c11 21 19 58
19 85 l0 48 124 0 c166 0 177 18 14 22 l-123 3 50 19 c98 38 116 47 110 56 -3
5 -28 1 -58 -11 -29 -12 -59 -23 -67 -26 -8 -3 32 42 90 101 57 58 98 106 90
106 -9 0 -58 -43 -110 -95 -52 -52 -95 -93 -97 -91 -2 2 12 40 32 86 20 46 33
85 30 88 -10 10 -22 -12 -59 -98 l-35 -85 0 243 c-1 151 -5 242 -11 242 -6 0
-11 -94 -12 -247z"/>
<path d="M1040 1330 c0 -16 23 -60 32 -60 11 0 10 14 -4 45 -11 25 -28 34 -28
15z"/>
<path d="M1430 1281 c-17 -43 -7 -63 13 -24 19 36 21 53 8 53 -5 0 -14 -13
-21 -29z"/>
<path d="M1090 1210 c0 -16 23 -60 32 -60 11 0 10 14 -4 45 -11 25 -28 34 -28
15z"/>
<path d="M1377 1148 c-4 -12 -4 -24 -1 -27 6 -7 24 20 24 37 0 21 -17 13 -23
-10z"/>
<path d="M960 1122 c0 -14 59 -66 67 -59 3 4 -6 21 -22 37 -28 30 -45 38 -45
22z"/>
<path d="M1522 959 c-13 -5 -21 -13 -17 -18 7 -12 47 1 53 17 4 14 -4 14 -36
1z"/>
<path d="M875 830 c3 -5 26 -10 51 -10 24 0 44 5 44 10 0 6 -23 10 -51 10 -31
0 -48 -4 -44 -10z"/>
<path d="M1585 830 c3 -5 15 -10 25 -10 10 0 22 5 25 10 4 6 -7 10 -25 10 -18
0 -29 -4 -25 -10z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Verbund Offener Werkstätten
SPDX-License-Identifier: CC-By-SA

View file

@ -0,0 +1,92 @@
[
{
"path": "3d_printer.svg",
"license": "CC-By-SA",
"authors": [
"Verbund Offener Werkstätten"
],
"sources": [
"https://www.offene-werkstaetten.org/de/werkstatt-suche"
]
},
{
"path": "bicycle.svg",
"license": "CC-By-SA",
"authors": [
"Verbund Offener Werkstätten"
],
"sources": [
"https://www.offene-werkstaetten.org/de/werkstatt-suche"
]
},
{
"path": "ceramics.svg",
"license": "CC-By-SA",
"authors": [
"Verbund Offener Werkstätten"
],
"sources": [
"https://www.offene-werkstaetten.org/de/werkstatt-suche"
]
},
{
"path": "cnc.svg",
"license": "CC-By-SA",
"authors": [
"Verbund Offener Werkstätten"
],
"sources": [
"https://www.offene-werkstaetten.org/de/werkstatt-suche"
]
},
{
"path": "lasercutter.svg",
"license": "CC-By-SA",
"authors": [
"Verbund Offener Werkstätten"
],
"sources": [
"https://www.offene-werkstaetten.org/de/werkstatt-suche"
]
},
{
"path": "media_studio.svg",
"license": "CC-By-SA",
"authors": [
"Verbund Offener Werkstätten"
],
"sources": [
"https://www.offene-werkstaetten.org/de/werkstatt-suche"
]
},
{
"path": "metal.svg",
"license": "CC-By-SA",
"authors": [
"Verbund Offener Werkstätten"
],
"sources": [
"https://www.offene-werkstaetten.org/de/werkstatt-suche"
]
},
{
"path": "sewing_machine.svg",
"license": "CC-By-SA",
"authors": [
"Verbund Offener Werkstätten"
],
"sources": [
"https://www.offene-werkstaetten.org/de/werkstatt-suche"
]
},
{
"path": "woodworking.svg",
"license": "CC-By-SA",
"authors": [
"Verbund Offener Werkstätten"
],
"sources": [
"https://www.offene-werkstaetten.org/de/werkstatt-suche"
]
}
]

View file

@ -0,0 +1,107 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="250.000000pt" height="250.000000pt" viewBox="0 0 250.000000 250.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,250.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M567 2053 c-4 -3 -7 -104 -7 -223 l0 -217 -52 -6 c-40 -5 -59 -14
-83 -37 l-30 -30 -3 -463 c-2 -425 -1 -465 15 -497 29 -56 46 -60 265 -60
l198 0 32 29 33 29 3 181 c3 171 4 181 23 181 15 0 19 -7 19 -39 0 -26 5 -41
16 -45 24 -9 497 -7 512 2 9 7 12 55 10 208 l-3 199 -265 0 -265 0 -3 -37 c-3
-30 -7 -38 -23 -38 -18 0 -19 8 -19 100 l0 100 350 0 c304 0 350 -2 350 -15 0
-10 -10 -15 -29 -15 -51 0 -52 -8 -49 -280 l3 -252 33 -29 c29 -26 39 -29 102
-29 l70 0 0 -60 0 -60 -55 0 c-75 0 -121 -21 -141 -64 -32 -67 -35 -66 261
-66 205 0 265 3 266 13 7 74 -46 117 -145 117 l-56 0 0 60 0 60 63 0 c137 0
147 22 147 327 0 255 -2 263 -61 263 l-29 0 0 88 c0 65 -4 94 -16 111 -20 28
-72 51 -114 51 l-30 0 0 209 c0 115 -3 216 -6 225 -6 14 -72 16 -643 16 -351
0 -641 -3 -644 -7z m1243 -238 l0 -205 -32 0 c-76 0 -138 -62 -138 -137 l0
-33 -350 0 -350 0 0 40 c0 23 -7 55 -16 71 -24 46 -66 59 -199 59 l-115 0 0
205 0 205 600 0 600 0 0 -205z m-936 -271 c14 -14 16 -71 16 -484 l0 -470 -24
-15 c-21 -14 -54 -16 -211 -13 -171 3 -188 5 -201 22 -12 16 -14 99 -14 480 0
413 2 461 17 478 14 16 35 18 209 18 162 0 195 -3 208 -16z m1086 -4 c11 -11
20 -31 20 -45 0 -22 -4 -25 -35 -25 -19 0 -35 -4 -35 -10 0 -5 16 -10 35 -10
32 0 35 -2 35 -29 0 -26 -4 -30 -32 -33 -18 -2 -33 -7 -33 -13 0 -5 15 -11 33
-13 28 -3 32 -7 32 -33 0 -27 -3 -29 -35 -29 -19 0 -35 -4 -35 -10 0 -5 16
-10 35 -10 32 0 35 -2 35 -30 0 -28 -3 -30 -35 -30 -19 0 -35 -4 -35 -10 0 -5
16 -10 35 -10 32 0 35 -2 35 -29 0 -26 -4 -30 -32 -33 -18 -2 -33 -7 -33 -13
0 -5 15 -11 33 -13 28 -3 32 -7 32 -33 0 -27 -3 -29 -35 -29 -19 0 -35 -4 -35
-10 0 -5 16 -10 35 -10 31 0 35 -3 35 -25 0 -14 -9 -34 -20 -45 -18 -18 -33
-20 -127 -20 -119 0 -143 10 -143 62 0 24 4 28 29 28 17 0 33 5 36 10 4 6 -8
10 -29 10 -33 0 -36 2 -36 29 0 26 4 30 33 33 17 2 32 8 32 13 0 6 -15 11 -32
13 -29 3 -33 7 -33 33 0 27 3 29 36 29 21 0 33 4 29 10 -3 6 -19 10 -36 10
-26 0 -29 3 -29 30 0 27 3 30 29 30 17 0 33 5 36 10 4 6 -8 10 -29 10 -33 0
-36 2 -36 29 0 26 4 30 33 33 17 2 32 8 32 13 0 6 -15 11 -32 13 -29 3 -33 7
-33 33 0 27 3 29 36 29 21 0 33 4 29 10 -3 6 -19 10 -36 10 -25 0 -29 4 -29
27 0 53 22 63 142 63 95 0 110 -2 128 -20z m-320 -290 c0 -47 -3 -60 -15 -60
-12 0 -15 13 -15 60 0 47 3 60 15 60 12 0 15 -13 15 -60z m420 0 c0 -53 -2
-60 -20 -60 -18 0 -20 7 -20 60 0 53 2 60 20 60 18 0 20 -7 20 -60z m-580
-185 l0 -165 -230 0 -230 0 0 165 0 165 230 0 230 0 0 -165z m-500 -5 c0 -73
-2 -80 -20 -80 -18 0 -20 7 -20 80 0 73 2 80 20 80 18 0 20 -7 20 -80z m660 8
c0 -138 44 -178 193 -178 156 0 187 27 187 162 0 81 2 88 20 88 19 0 20 -7 20
-145 0 -197 14 -185 -223 -185 -243 0 -227 -13 -227 181 0 126 2 149 15 149
12 0 15 -15 15 -72z m210 -371 c0 -43 5 -78 12 -85 7 -7 42 -12 89 -12 56 0
80 -4 89 -15 7 -9 11 -18 8 -20 -3 -3 -102 -4 -221 -3 -189 3 -215 5 -207 18
7 11 34 16 97 20 l88 5 3 83 c3 74 5 82 22 82 18 0 20 -7 20 -73z"/>
<path d="M700 1886 c0 -79 -2 -86 -20 -86 -18 0 -20 -7 -20 -70 l0 -70 50 0
50 0 0 70 c0 63 -2 70 -20 70 -18 0 -20 7 -20 79 0 44 -4 83 -10 86 -6 4 -10
-25 -10 -79z m40 -156 c0 -49 -1 -50 -30 -50 -29 0 -30 1 -30 50 0 49 1 50 30
50 29 0 30 -1 30 -50z"/>
<path d="M866 1963 c-3 -3 -6 -42 -6 -85 0 -71 -2 -78 -20 -78 -18 0 -20 -7
-20 -70 l0 -70 55 0 55 0 0 70 c0 63 -2 70 -20 70 -18 0 -20 7 -20 79 0 74 -7
100 -24 84z m44 -233 l0 -50 -35 0 -35 0 0 50 0 50 35 0 35 0 0 -50z"/>
<path d="M990 1899 c0 -62 2 -71 20 -76 18 -5 20 -14 20 -84 0 -46 4 -79 10
-79 6 0 10 33 10 80 0 73 2 80 20 80 18 0 20 7 20 75 l0 75 -50 0 -50 0 0 -71z
m80 -4 c0 -54 0 -55 -30 -55 -30 0 -30 1 -30 55 0 54 0 55 30 55 30 0 30 -1
30 -55z"/>
<path d="M1200 1951 c0 -14 -6 -21 -20 -21 -18 0 -20 -7 -20 -75 0 -68 2 -75
20 -75 18 0 20 -7 20 -60 0 -33 4 -60 10 -60 6 0 10 27 10 60 0 53 2 60 20 60
18 0 20 7 20 71 0 62 -2 71 -20 76 -11 3 -20 11 -20 19 0 7 -4 16 -10 19 -5 3
-10 -3 -10 -14z m40 -96 c0 -54 0 -55 -30 -55 -30 0 -30 1 -30 55 0 54 0 55
30 55 30 0 30 -1 30 -55z"/>
<path d="M1366 1963 c-3 -3 -6 -21 -6 -39 0 -23 -5 -33 -20 -37 -17 -4 -20
-14 -20 -72 0 -58 3 -68 19 -72 14 -4 21 -15 23 -42 4 -47 22 -47 26 0 2 27 9
38 23 42 16 4 19 14 19 72 0 58 -3 68 -20 72 -15 4 -20 14 -20 39 0 32 -11 50
-24 37z m44 -153 l0 -50 -35 0 -35 0 0 50 0 50 35 0 35 0 0 -50z"/>
<path d="M1490 1899 c0 -62 2 -71 20 -76 18 -5 20 -14 20 -84 0 -46 4 -79 10
-79 6 0 10 33 10 80 0 73 2 80 20 80 18 0 20 7 20 75 l0 75 -50 0 -50 0 0 -71z
m80 -4 c0 -54 0 -55 -30 -55 -30 0 -30 1 -30 55 0 54 0 55 30 55 30 0 30 -1
30 -55z"/>
<path d="M1700 1886 c0 -79 -2 -86 -20 -86 -18 0 -20 -7 -20 -70 l0 -70 50 0
50 0 0 70 c0 63 -2 70 -20 70 -18 0 -20 7 -20 79 0 44 -4 83 -10 86 -6 4 -10
-25 -10 -79z m40 -156 c0 -49 -1 -50 -30 -50 -29 0 -30 1 -30 50 0 49 1 50 30
50 29 0 30 -1 30 -50z"/>
<path d="M1002 1578 c-30 -30 -2 -88 42 -88 29 0 46 18 46 50 0 33 -17 50 -50
50 -14 0 -31 -5 -38 -12z m66 -36 c4 -28 -24 -40 -45 -19 -21 21 -9 49 19 45
15 -2 24 -11 26 -26z"/>
<path d="M1171 1576 c-16 -19 -10 -66 10 -79 26 -16 68 0 75 30 4 14 2 33 -3
42 -13 21 -66 26 -82 7z m65 -21 c7 -18 -13 -45 -33 -45 -17 0 -27 24 -19 45
7 20 45 19 52 0z"/>
<path d="M1336 1574 c-9 -8 -16 -21 -16 -27 0 -25 32 -57 55 -57 23 0 55 32
55 57 0 19 -30 43 -55 43 -13 0 -31 -7 -39 -16z m68 -20 c7 -19 -10 -44 -29
-44 -19 0 -36 25 -29 44 3 9 16 16 29 16 13 0 26 -7 29 -16z"/>
<path d="M1502 1578 c-17 -17 -15 -54 4 -72 18 -19 55 -21 72 -4 7 7 12 24 12
38 0 14 -5 31 -12 38 -7 7 -24 12 -38 12 -14 0 -31 -5 -38 -12z m66 -36 c4
-28 -24 -40 -45 -19 -21 21 -9 49 19 45 15 -2 24 -11 26 -26z"/>
<path d="M614 1496 c-3 -8 -4 -29 -2 -48 3 -32 4 -33 56 -36 l52 -3 0 50 0 51
-50 0 c-34 0 -52 -5 -56 -14z m86 -36 c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30
30 0 27 3 30 30 30 27 0 30 -3 30 -30z"/>
<path d="M611 1345 c-110 -34 -161 -153 -112 -261 63 -139 266 -138 335 1 72
145 -68 308 -223 260z m127 -55 c90 -55 97 -180 12 -240 -45 -32 -89 -36 -144
-12 -37 17 -51 30 -66 64 -25 54 -25 84 -1 128 39 75 130 102 199 60z"/>
<path d="M625 1271 c-75 -31 -91 -141 -28 -191 26 -20 90 -29 78 -10 -3 6 -15
10 -26 10 -11 0 -31 11 -44 25 -48 47 -21 135 45 150 17 4 28 11 24 16 -6 11
-22 11 -49 0z"/>
<path d="M755 1151 c-3 -17 -11 -33 -16 -36 -5 -4 -9 -13 -9 -22 0 -14 2 -14
19 1 23 21 36 65 23 78 -6 6 -12 -1 -17 -21z"/>
<path d="M490 750 c0 -6 65 -10 175 -10 110 0 175 4 175 10 0 6 -65 10 -175
10 -110 0 -175 -4 -175 -10z"/>
<path d="M495 670 c-4 -7 55 -10 169 -10 111 0 176 4 176 10 0 14 -337 14
-345 0z"/>
<path d="M1324 1166 c-14 -37 0 -51 51 -51 49 0 50 1 50 30 0 29 -2 30 -48 33
-35 2 -49 -1 -53 -12z m86 -16 c0 -5 -16 -10 -35 -10 -19 0 -35 5 -35 10 0 6
16 10 35 10 19 0 35 -4 35 -10z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Verbund Offener Werkstätten
SPDX-License-Identifier: CC-By-SA

View file

@ -0,0 +1,68 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="250.000000pt" height="251.000000pt" viewBox="0 0 250.000000 251.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,251.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M860 2267 c-28 -14 -390 -605 -409 -668 -16 -52 -14 -138 5 -191 29
-87 109 -153 332 -276 l64 -35 -121 -121 c-67 -67 -121 -126 -121 -132 0 -15
94 -104 110 -104 9 0 78 63 155 140 l140 140 185 0 c174 0 186 -1 198 -20 12
-19 23 -20 415 -20 398 0 403 0 447 23 51 26 78 59 90 110 16 72 -16 144 -81
182 -32 19 -52 20 -314 20 -268 0 -282 -1 -328 -22 -26 -12 -60 -31 -74 -43
-20 -15 -40 -20 -89 -20 -54 0 -64 -3 -69 -20 -6 -18 -15 -20 -116 -20 -61 0
-108 3 -106 8 3 4 89 145 191 315 124 205 186 317 186 335 0 17 -9 37 -22 49
-28 25 -595 368 -623 376 -11 4 -31 1 -45 -6z m342 -218 c165 -100 302 -187
304 -194 5 -12 -319 -560 -373 -632 -24 -33 -25 -33 -105 -33 -75 0 -84 -2
-109 -26 -20 -20 -34 -25 -52 -20 -13 3 -86 43 -163 89 -189 114 -234 176
-221 306 5 48 27 89 197 372 122 204 197 319 207 319 9 0 150 -82 315 -181z
m998 -899 l0 -120 -74 0 -74 0 -11 33 c-22 65 -96 117 -166 117 -70 0 -144
-52 -166 -117 l-11 -33 -69 0 -69 0 0 83 c0 95 4 102 80 136 41 19 65 21 303
21 l257 0 0 -120z m95 60 c29 -57 12 -130 -39 -164 -14 -9 -28 -16 -31 -16 -3
0 -5 54 -5 120 l0 121 30 -16 c16 -8 36 -29 45 -45z m-775 -100 l0 -80 -40 0
-40 0 0 80 0 80 40 0 40 0 0 -80z m439 23 c26 -20 71 -79 71 -94 0 -5 -70 -9
-155 -9 -85 0 -155 2 -155 5 0 30 58 97 100 114 36 15 108 7 139 -16z m-569
-23 l0 -40 -197 0 -196 0 -142 -140 -141 -140 -27 27 -27 28 152 152 153 153
212 0 213 0 0 -40z"/>
<path d="M877 2083 c-107 -171 -197 -332 -192 -344 8 -21 460 -291 481 -287
11 2 61 74 128 186 107 179 109 183 90 201 -29 27 -453 281 -469 281 -8 0 -25
-17 -38 -37z m202 -105 c64 -39 122 -75 130 -81 10 -9 -40 -25 -215 -69 -126
-32 -234 -58 -241 -58 -7 0 28 68 78 150 83 139 92 149 111 139 11 -6 73 -42
137 -81z m216 -132 l39 -24 -39 -10 c-22 -6 -91 -23 -152 -37 -62 -15 -113
-31 -113 -36 0 -13 9 -12 160 26 76 19 141 35 144 35 3 0 -36 -68 -85 -151
-55 -91 -96 -148 -103 -146 -45 18 -397 242 -385 246 19 6 472 119 484 120 6
0 28 -10 50 -23z"/>
<path d="M911 1718 c-20 -5 -30 -13 -25 -18 10 -10 82 5 99 21 12 12 -29 10
-74 -3z"/>
<path d="M632 1639 c5 -16 445 -287 468 -288 30 -2 -28 38 -240 164 -118 71
-219 130 -224 133 -5 3 -7 -1 -4 -9z"/>
<path d="M660 1518 c14 -15 316 -198 326 -198 24 0 -20 33 -147 110 -145 89
-214 123 -179 88z"/>
<path d="M1720 1700 c-45 -45 -11 -120 53 -120 42 0 67 26 67 68 0 41 -31 72
-70 72 -17 0 -39 -9 -50 -20z m81 -10 c22 -12 26 -59 7 -78 -19 -19 -66 -15
-78 7 -14 27 -13 47 6 65 18 19 38 20 65 6z"/>
<path d="M374 1195 c-15 -23 -15 -27 0 -50 28 -42 96 -24 96 25 0 49 -68 67
-96 25z m66 -10 c10 -12 10 -18 0 -30 -25 -30 -61 -7 -46 30 3 8 12 15 19 15
8 0 20 -7 27 -15z"/>
<path d="M577 1133 c-11 -10 -8 -171 3 -178 6 -4 10 28 10 89 0 96 -1 102 -13
89z"/>
<path d="M367 958 c73 -73 136 -129 140 -125 9 10 -237 257 -257 257 -8 0 45
-59 117 -132z"/>
<path d="M1030 885 c-24 -24 -41 -48 -37 -52 8 -7 97 75 97 89 0 17 -18 6 -60
-37z"/>
<path d="M1256 914 c-19 -18 -21 -55 -4 -72 15 -15 61 -15 76 0 7 7 12 24 12
38 0 14 -5 31 -12 38 -17 17 -54 15 -72 -4z m59 -34 c0 -18 -6 -26 -23 -28
-27 -4 -40 22 -22 44 19 22 45 13 45 -16z"/>
<path d="M1075 750 c4 -6 67 -10 156 -10 93 0 149 4 149 10 0 6 -59 10 -156
10 -101 0 -153 -3 -149 -10z"/>
<path d="M370 710 c0 -5 20 -10 44 -10 25 0 48 5 51 10 4 6 -13 10 -44 10 -28
0 -51 -4 -51 -10z"/>
<path d="M564 637 c-3 -8 -4 -45 -2 -83 l3 -69 683 -3 c619 -2 683 -1 689 14
3 9 0 20 -8 25 -8 5 -186 9 -396 9 l-383 0 0 44 c0 82 22 76 -296 76 -230 0
-286 -3 -290 -13z m536 -67 l0 -40 -245 0 -245 0 0 40 0 40 245 0 245 0 0 -40z"/>
<path d="M740 420 c0 -7 168 -10 485 -10 317 0 485 3 485 10 0 7 -168 10 -485
10 -317 0 -485 -3 -485 -10z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Verbund Offener Werkstätten
SPDX-License-Identifier: CC-By-SA

View file

@ -0,0 +1,43 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="250.000000pt" height="250.000000pt" viewBox="0 0 250.000000 250.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,250.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1527 2144 c-4 -4 -7 -25 -7 -46 0 -31 -3 -38 -20 -38 -18 0 -20 -7
-20 -80 l0 -80 -335 0 -335 0 0 41 c0 33 -3 40 -17 37 -13 -2 -19 -14 -21 -41
-3 -35 -5 -37 -40 -37 -89 0 -160 -42 -193 -115 -16 -33 -19 -67 -19 -202 l0
-163 28 -31 c24 -27 37 -33 85 -37 l56 -4 3 -37 c3 -34 5 -36 41 -39 l37 -3 0
-60 c0 -52 2 -59 20 -59 18 0 20 7 20 60 l0 60 39 0 c41 0 51 11 51 56 0 21 5
24 34 24 77 0 126 39 126 100 l0 30 210 0 210 0 0 -213 c0 -237 -5 -260 -70
-304 -33 -23 -37 -23 -384 -23 -387 0 -398 -2 -458 -66 -42 -46 -48 -75 -48
-243 0 -140 2 -160 18 -174 17 -16 81 -17 714 -17 660 0 696 1 711 18 15 17
17 76 17 653 0 593 -2 637 -19 674 -33 73 -104 115 -193 115 l-37 0 -3 123
c-3 100 -6 122 -18 122 -12 0 -15 -22 -18 -122 l-3 -123 -39 0 -40 0 0 64 c0
77 -6 96 -30 96 -15 0 -20 9 -22 42 -3 39 -16 57 -31 42z m33 -184 c0 -53 -2
-60 -20 -60 -18 0 -20 7 -20 60 0 53 2 60 20 60 18 0 20 -7 20 -60z m-818
-247 l3 -138 48 -3 47 -3 0 140 0 141 90 0 90 0 0 -207 c0 -178 -2 -209 -17
-225 -15 -16 -35 -18 -214 -18 l-199 0 -16 25 c-24 37 -17 319 9 358 25 39 75
67 119 67 l37 0 3 -137z m78 52 l0 -85 -30 0 -30 0 0 85 0 85 30 0 30 0 0 -85z
m660 -80 l0 -165 -210 0 -210 0 0 45 0 45 183 2 c113 2 182 7 182 13 0 6 -69
11 -182 13 l-183 2 0 105 0 105 210 0 210 0 0 -165z m387 145 c70 -42 68 -23
71 -637 l3 -553 -692 0 -692 0 5 79 c5 88 20 120 71 151 31 19 54 20 552 20
286 0 526 4 534 9 8 5 11 16 8 25 -5 14 -27 16 -138 16 -125 0 -131 1 -117 18
48 54 48 57 48 487 l0 405 158 0 c138 0 161 -2 189 -20z m-1047 -205 c0 -32
-2 -35 -30 -35 -28 0 -30 3 -30 35 0 32 2 35 30 35 28 0 30 -3 30 -35z m30
-295 c0 -18 -7 -20 -60 -20 -53 0 -60 2 -60 20 0 18 7 20 60 20 53 0 60 -2 60
-20z m1090 -773 c0 -29 -5 -58 -12 -65 -17 -17 -1339 -17 -1356 0 -7 7 -12 36
-12 65 l0 53 690 0 690 0 0 -53z"/>
<path d="M1577 1634 c-4 -4 -7 -38 -7 -76 l0 -69 158 3 157 3 0 70 0 70 -151
3 c-82 1 -153 -1 -157 -4z m283 -74 l0 -50 -135 0 -135 0 0 50 0 50 135 0 135
0 0 -50z"/>
<path d="M1664 1330 c-33 -13 -82 -77 -90 -115 -9 -50 19 -119 62 -151 46 -36
100 -41 157 -16 153 68 103 294 -65 291 -24 0 -52 -4 -64 -9z m163 -53 c18
-20 33 -46 33 -57 0 -19 -6 -20 -130 -20 -126 0 -130 1 -130 21 0 12 18 39 40
61 39 39 42 40 97 36 49 -4 60 -9 90 -41z m33 -114 c0 -49 -73 -113 -129 -113
-59 0 -131 60 -131 109 0 20 5 21 130 21 117 0 130 -2 130 -17z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Verbund Offener Werkstätten
SPDX-License-Identifier: CC-By-SA

View file

@ -0,0 +1,62 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="250.000000pt" height="251.000000pt" viewBox="0 0 250.000000 251.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,251.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M750 2120 c0 -37 36 -239 44 -247 15 -15 14 11 -5 128 -20 116 -39
176 -39 119z"/>
<path d="M540 2043 c0 -13 82 -133 91 -133 15 0 10 12 -32 77 -37 56 -59 77
-59 56z"/>
<path d="M986 2008 c-8 -13 -36 -71 -62 -130 -26 -60 -53 -108 -60 -108 -7 0
-111 20 -230 45 -129 27 -221 42 -227 36 -5 -5 -19 -57 -32 -117 l-23 -107 26
-56 c28 -59 34 -62 98 -48 29 5 33 3 45 -26 22 -57 35 -66 80 -56 22 5 44 7
49 4 5 -3 16 -24 25 -48 l16 -42 -126 -280 -126 -279 3 -136 3 -135 451 -3
c354 -2 454 1 461 10 6 7 50 99 98 205 83 183 88 191 104 173 13 -16 22 -18
53 -11 30 7 37 6 41 -8 6 -25 389 -104 423 -89 29 14 29 13 101 364 l58 283
-21 22 c-16 16 -63 29 -195 56 -96 20 -177 39 -182 42 -4 4 9 46 30 92 l38 85
-3 137 -3 137 -434 0 c-238 0 -440 3 -448 6 -10 3 -21 -4 -31 -18z m399 -60
c-7 -18 -38 -85 -68 -149 l-54 -116 -164 34 c-90 18 -167 34 -170 36 -4 1 16
53 43 115 l51 112 188 0 188 0 -14 -32z m345 28 c0 -2 -13 -33 -30 -69 -33
-73 -34 -77 -21 -77 5 0 25 34 45 76 l36 76 38 -7 c20 -4 38 -8 39 -9 3 -3
-154 -357 -161 -364 -4 -3 -266 47 -274 53 -1 1 20 50 48 110 27 59 50 111 50
117 0 26 -27 -15 -72 -113 l-51 -109 -36 6 c-20 4 -39 10 -44 13 -4 4 23 73
60 154 l67 147 153 0 c84 0 153 -2 153 -4z m91 -301 c-39 -87 -56 -105 -84
-88 -11 6 0 38 52 155 l66 148 3 -63 c2 -55 -2 -74 -37 -152z m-383 -73 c183
-38 334 -71 337 -74 3 -3 -83 -437 -110 -554 -5 -24 -62 -39 -80 -22 -5 5 -12
77 -15 161 l-5 152 -281 3 -280 2 -18 36 -17 37 -52 -7 -53 -6 -19 43 -20 42
-52 -3 -53 -3 -16 40 c-18 46 -34 54 -87 42 l-38 -9 -19 44 c-22 49 -26 52
-91 40 -35 -6 -37 -5 -52 32 -14 34 -14 46 -1 108 8 38 17 76 19 85 6 17 67 6
1003 -189z m420 -91 c11 -6 1 -67 -49 -308 -61 -296 -62 -300 -85 -295 -13 2
-24 8 -24 12 0 4 27 137 60 295 33 158 60 292 60 296 0 11 21 11 38 0z m187
-37 c66 -14 124 -29 129 -34 12 -12 -100 -569 -117 -587 -9 -9 -42 -6 -144 16
-73 15 -135 30 -138 33 -3 3 22 135 55 294 33 159 60 292 60 297 0 12 28 8
155 -19z m-1237 -134 c2 -15 -40 -122 -107 -270 l-110 -245 -45 -3 -44 -3 102
228 c57 125 112 250 124 276 19 43 25 48 49 45 21 -2 29 -9 31 -28z m91 -51
c22 5 42 6 46 3 12 -12 3 -22 -21 -22 -19 0 -25 -4 -22 -17 3 -16 25 -18 250
-23 l246 -5 -92 -202 -91 -203 -93 0 -92 0 13 33 c8 17 33 76 56 131 23 54 40
101 36 104 -10 10 -21 -10 -76 -140 l-53 -128 -193 0 c-106 0 -193 3 -193 8 0
4 46 111 102 238 89 201 105 231 120 223 10 -6 33 -6 57 0z m597 -91 c-7 -18
-48 -111 -91 -205 l-78 -173 -44 0 c-24 0 -42 3 -40 8 2 4 44 96 93 205 l89
197 42 0 43 0 -14 -32z m-48 -361 l-83 -181 -3 64 c-3 61 0 71 80 248 l83 185
3 -67 c3 -66 1 -72 -80 -249z m-868 -167 l0 -100 -45 0 -45 0 0 100 0 100 45
0 45 0 0 -100z m420 44 c0 -34 4 -53 10 -49 6 3 10 28 10 56 l0 49 90 0 90 0
0 -100 0 -100 -215 0 -215 0 0 49 c0 28 -4 53 -10 56 -6 4 -10 -15 -10 -49 l0
-56 -75 0 -75 0 0 100 0 100 200 0 200 0 0 -56z m320 -44 l0 -100 -45 0 -45 0
0 100 0 100 45 0 45 0 0 -100z"/>
<path d="M1927 1403 c-11 -18 -88 -415 -84 -427 6 -15 143 -44 153 -33 12 12
95 430 87 438 -9 8 -104 29 -133 29 -10 0 -21 -3 -23 -7z m109 -56 c2 -2 -13
-83 -33 -180 -36 -174 -37 -177 -62 -177 -61 1 -61 0 6 304 l15 68 35 -5 c20
-4 37 -8 39 -10z"/>
<path d="M472 1334 c-39 -27 -19 -94 27 -94 24 0 51 31 51 58 0 18 -33 52 -50
52 -3 0 -16 -7 -28 -16z m55 -30 c8 -21 -19 -46 -40 -38 -17 6 -23 35 -10 47
12 13 44 7 50 -9z"/>
<path d="M1767 648 c62 -62 116 -109 120 -105 9 10 -197 217 -217 217 -8 0 36
-50 97 -112z"/>
<path d="M1580 611 c0 -41 4 -71 10 -71 6 0 10 28 10 64 0 36 -4 68 -10 71 -6
4 -10 -20 -10 -64z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Verbund Offener Werkstätten
SPDX-License-Identifier: CC-By-SA

View file

@ -289,6 +289,35 @@
}
],
"condition": "conveying!=yes"
},
{
"id": "incline",
"render": {
"en": "These stairs have an incline of {incline}"
},
"freeform": {
"key": "incline",
"type": "slope"
},
"question": {
"en": "What is the incline of these stairs?"
},
"mappings": [
{
"if": "incline=up",
"then": {
"en": "The upward direction is {direction_absolute()}"
},
"hideInAnswer": true
},
{
"if": "incline=down",
"then": {
"en": "The downward direction is {direction_absolute()}"
},
"hideInAnswer": true
}
]
}
]
}

View file

@ -113,6 +113,33 @@
"*": "{logout()}"
}
},
{
"id": "a11y-features",
"question": {
"en": "What accessibility features should be applied?"
},
"mappings": [
{
"if": "mapcomplete-a11y=default",
"alsoShowIf": "mapcomplete-a11y=",
"then": {
"en": "Enable accessibility features when arrow keys are used to navigate the map"
}
},
{
"if": "mapcomplete-a11y=always",
"then": {
"en": "Always enable accessibility features"
}
},
{
"if": "mapcomplete-a11y=never",
"then": {
"en": "Never enable accessibility features"
}
}
]
},
{
"id": "background-layer-readonly",
"condition": {

View file

@ -22,7 +22,8 @@
"startZoom": 9,
"startLat": 51.0249,
"startLon": 4.026489,
"defaultBackgroundId": "AGIVFlandersGRB",
"defaultBackgroundId": "osm",
"credits": [
"Pieter Vander Vennet"
],

View file

@ -63,7 +63,7 @@
"startZoom": 8,
"startLat": 50.642,
"startLon": 4.482,
"defaultBackgroundId": "AGIV",
"defaultBackgroundId": "photo",
"credits": [
"Midgard"
],
@ -76,4 +76,4 @@
"maxZoom": 19,
"minNeededElements": 25
}
}
}

View file

@ -53,7 +53,7 @@
<div id="main"></div>
<script type="module" src="./src/all_themes_index.ts"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous"
integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
integrity="sha384-nx5O+otcqJoqMhdDt8jUzmia6ng81Z5zZozYr69TzPkOLjVhLKMxu5zHCV9/0MPn"></script>
<script>
window.addEventListener('load', () => {

View file

@ -8543,6 +8543,18 @@
},
"question": "Does this stair have a handrail?"
},
"incline": {
"mappings": {
"0": {
"then": "The upward direction is {direction_absolute()}"
},
"1": {
"then": "The downward direction is {direction_absolute()}"
}
},
"question": "What is the incline of these stairs?",
"render": "These stairs have an incline of {incline}"
},
"multilevels": {
"override": {
"question": "Between which levels are these stairs?",
@ -9722,6 +9734,20 @@
"usersettings": {
"description": "A special layer which is not meant to be shown on a map, but which is used to set user settings",
"tagRenderings": {
"a11y-features": {
"mappings": {
"0": {
"then": "Enable accessibility features when arrow keys are used to navigate the map"
},
"1": {
"then": "Always enable accessibility features"
},
"2": {
"then": "Never enable accessibility features"
}
},
"question": "What accessibility features should be applied?"
},
"all-questions-at-once": {
"mappings": {
"0": {

View file

@ -370,14 +370,15 @@
"useSearch": "Gebruik de zoekfunctie hierboven om meer opties te zien",
"useSearchForMore": "Gebruik de zoekfunctie om {total} meer waarden te vinden…",
"visualFeedback": {
"closestFeaturesAre": "{n} object in beeld.",
"closestFeaturesAre": "{n} objecten in beeld.",
"east": "Naar het oosten",
"in": "Aan het inzoomen naar zoomlevel {z}",
"islocked": "Bewegen vergrendeld rond je huidige locatie. Duw op de geolocatie-knop om te ontgrendelen.",
"locked": "Bewegen vergrendeld rond jouw huidige locatie.",
"navigation": "Gebruik de pijltjestoetsen om te bewegen. Druk op spatie om het meest centrale punt te selecteren. Druk op een cijfertoets om andere items te selecteren.",
"noCloseFeatures": "Niet in beeld",
"noCloseFeatures": "Geen objecten in beeld",
"north": "Naar het noorden",
"oneFeatureInView": "Eén object in beeld.",
"out": "Aan het uitzoomen naar zoomlevel {z}",
"south": "Naar het zuiden",
"unlocked": "Bewegen ontgrendeld",

View file

@ -53,7 +53,7 @@
<div id="main"></div>
<script type="module" src="./src/leaderboard.ts"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous"
integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
integrity="sha384-nx5O+otcqJoqMhdDt8jUzmia6ng81Z5zZozYr69TzPkOLjVhLKMxu5zHCV9/0MPn"></script>
<script>
window.addEventListener('load', () => {

5
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "mapcomplete",
"version": "0.36.9",
"version": "0.36.11",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mapcomplete",
"version": "0.36.9",
"version": "0.36.11",
"license": "GPL-3.0-or-later",
"dependencies": {
"@rgossiaux/svelte-headlessui": "^1.0.2",
@ -15,6 +15,7 @@
"@turf/boolean-intersects": "^6.5.0",
"@turf/buffer": "^6.5.0",
"@turf/collect": "^6.5.0",
"@turf/difference": "^6.5.0",
"@turf/distance": "^6.5.0",
"@turf/length": "^6.5.0",
"@turf/turf": "^6.5.0",

View file

@ -1,6 +1,6 @@
{
"name": "mapcomplete",
"version": "0.36.9",
"version": "0.36.11",
"repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
@ -103,6 +103,7 @@
"@turf/boolean-intersects": "^6.5.0",
"@turf/buffer": "^6.5.0",
"@turf/collect": "^6.5.0",
"@turf/difference": "^6.5.0",
"@turf/distance": "^6.5.0",
"@turf/length": "^6.5.0",
"@turf/turf": "^6.5.0",

View file

@ -39,7 +39,7 @@
<div id="main"></div>
<script type="module" src="./src/privacy_index.ts"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous"
integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
integrity="sha384-nx5O+otcqJoqMhdDt8jUzmia6ng81Z5zZozYr69TzPkOLjVhLKMxu5zHCV9/0MPn"></script>
</body>

View file

@ -1071,14 +1071,14 @@ video {
height: 6rem;
}
.h-full {
height: 100%;
}
.h-screen {
height: 100vh;
}
.h-full {
height: 100%;
}
.h-32 {
height: 8rem;
}
@ -2318,14 +2318,6 @@ input[type=text] {
width: 100%;
}
.debug input, .debug textarea {
border: 6px solid red
}
.debug label input, .debug label textarea {
border: 1px solid grey;
}
/************************* BIG CATEGORIES ********************************/
/**

View file

@ -284,7 +284,7 @@ async function generateCsp(
if (typeof sv.needsUrls === "function") {
return
}
apiUrls.push(...sv.needsUrls)
apiUrls.push(...(sv.needsUrls ?? []))
})
const usedSpecialVisualisations = ValidationUtils.getSpecialVisualisationsWithArgs(layoutJson)
@ -292,7 +292,7 @@ async function generateCsp(
if (typeof usedSpecialVisualisation === "string") {
continue
}
const neededUrls = usedSpecialVisualisation.func.needsUrls
const neededUrls = usedSpecialVisualisation.func.needsUrls ?? []
if (typeof neededUrls === "function") {
apiUrls.push(...neededUrls(usedSpecialVisualisation.args))
}

View file

@ -4,6 +4,10 @@ import { Review } from "mangrove-reviews-typescript"
import { parse } from "csv-parse"
import { Feature, FeatureCollection, Point } from "geojson"
/**
* To be run from the repository root, e.g.
* vite-node scripts/generateReviewsAnalysis.ts -- ~/Downloads/mangrove.reviews_1704031255.csv
*/
export default class GenerateReviewsAnalysis extends Script {
constructor() {
super("Analyses a CSV-file with Mangrove reviews")
@ -104,6 +108,11 @@ export default class GenerateReviewsAnalysis extends Script {
}
async main(args: string[]): Promise<void> {
if (args.length === 0) {
console.log(
"Usage: enter file path of mangrove.reviews_timestamp.csv as first argument"
)
}
const datapath = args[0] ?? "../MapComplete-data/mangrove.reviews_1674234503.csv"
await this.analyze(datapath)
}

View file

@ -28,3 +28,15 @@ studio.mapcomplete.org {
to http://127.0.0.1:1235
}
}
bounce.mapcomplete.org {
reverse_proxy {
to http://127.0.0.1:1236
}
}
mapcomplete.osm.be {
reverse_proxy {
to http://127.0.0.1:1236
}
}

View file

@ -0,0 +1,45 @@
import * as http from "node:http"
/**
* Redirect people from
* "mapcomplete.osm.be/path?query=parameter#id" to "mapcomplete.org/path?query=parameter#id"
*/
const PORT = 1236
const CORS = "http://localhost:1234,https://mapcomplete.org,https://dev.mapcomplete.org"
async function redirect(req: http.IncomingMessage, res: http.ServerResponse) {
try {
console.log(
req.method + " " + req.url,
"from:",
req.headers.origin,
new Date().toISOString()
)
res.setHeader(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
)
res.setHeader("Access-Control-Allow-Origin", req.headers.origin ?? "*")
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, UPDATE")
res.writeHead(204, { "Content-Type": "text/html" })
res.end()
return
}
console.log("Request url:", req.url)
const oldUrl = new URL("https://127.0.0.1:8080" + req.url)
const newUrl = "https://mapcomplete.org" + oldUrl.pathname + oldUrl.search + oldUrl.hash
res.writeHead(301, { "Content-Type": "text/html", Location: newUrl })
res.write("Moved permantently")
res.end()
} catch (e) {
console.error(e)
}
}
http.createServer(redirect).listen(PORT)
console.log(
`Server started at http://127.0.0.1:${PORT}/, the time is ${new Date().toISOString()}, version from package.json is`
)

View file

@ -264,7 +264,8 @@ class ClosestNObjectFunc implements ExtraFunction {
const bbox = GeoOperations.bbox(
GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance)
)
allFeatures = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates))
const coors = <[number, number][]>bbox.geometry.coordinates
allFeatures = params.getFeaturesWithin(name, new BBox(coors))
} else {
allFeatures = [features]
}

View file

@ -39,8 +39,8 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
})
}
protected addData(featuress: Feature[][]) {
featuress = Utils.NoNull(featuress)
protected addData(sources: Feature[][]) {
sources = Utils.NoNull(sources)
let somethingChanged = false
const all: Map<string, Feature> = new Map()
const unseen = new Set<string>()
@ -51,7 +51,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
unseen.add(oldValue.properties.id)
}
for (const features of featuress) {
for (const features of sources) {
for (const f of features) {
const id = f.properties.id
unseen.delete(id)

View file

@ -6,7 +6,7 @@ import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
/***
* A tiled source which dynamically loads the required tiles at a fixed zoom level.
* A single featureSource will be initiliased for every tile in view; which will alter be merged into this featureSource
* A single featureSource will be initialized for every tile in view; which will later be merged into this featureSource
*/
export default class DynamicTileSource extends FeatureSourceMerger {
constructor(

View file

@ -19,6 +19,35 @@ import { Utils } from "../Utils"
export class GeoOperations {
private static readonly _earthRadius = 6378137
private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2
private static readonly directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const
private static readonly directionsRelative = [
"straight",
"slight_right",
"right",
"sharp_right",
"behind",
"sharp_left",
"left",
"slight_left",
] as const
private static reverseBearing = {
N: 0,
NNE: 22.5,
NE: 45,
ENE: 67.5,
E: 90,
ESE: 112.5,
SE: 135,
SSE: 157.5,
S: 180,
SSW: 202.5,
SW: 225,
WSW: 247.5,
W: 270,
WNW: 292.5,
NW: 315,
NNW: 337.5,
}
/**
* Create a union between two features
@ -124,7 +153,7 @@ export class GeoOperations {
continue
}
const intersection = GeoOperations.calculateInstersection(
const intersection = GeoOperations.calculateIntersection(
feature,
otherFeature,
featureBBox
@ -155,7 +184,7 @@ export class GeoOperations {
// Calculate the surface area of the intersection
const intersection = this.calculateInstersection(feature, otherFeature, featureBBox)
const intersection = this.calculateIntersection(feature, otherFeature, featureBBox)
if (intersection === null) {
continue
}
@ -261,16 +290,16 @@ export class GeoOperations {
}
/**
* Generates the closest point on a way from a given point.
* If the passed-in geojson object is a polygon, the outer ring will be used as linestring
*
* The properties object will contain three values:
// - `index`: closest point was found on nth line part,
// - `dist`: distance between pt and the closest point (in kilometer),
// `location`: distance along the line between start (of the line) and the closest point.
* @param way The road on which you want to find a point
* @param point Point defined as [lon, lat]
*/
* Generates the closest point on a way from a given point.
* If the passed-in geojson object is a polygon, the outer ring will be used as linestring
*
* The properties object will contain three values:
// - `index`: closest point was found on nth line part,
// - `dist`: distance between pt and the closest point (in kilometer),
// `location`: distance along the line between start (of the line) and the closest point.
* @param way The road on which you want to find a point
* @param point Point defined as [lon, lat]
*/
public static nearestPoint(
way: Feature<LineString>,
point: [number, number]
@ -293,9 +322,11 @@ export class GeoOperations {
* @param way
*/
public static forceLineString(way: Feature<LineString | Polygon>): Feature<LineString>
public static forceLineString(
way: Feature<MultiLineString | MultiPolygon>
): Feature<MultiLineString>
public static forceLineString(
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
): Feature<LineString | MultiLineString> {
@ -800,6 +831,150 @@ export class GeoOperations {
return { lon, lat }
}
public static SplitSelfIntersectingWays(features: Feature[]): Feature[] {
const result: Feature[] = []
for (const feature of features) {
if (feature.geometry.type === "LineString") {
let coors = feature.geometry.coordinates
for (let i = coors.length - 1; i >= 0; i--) {
// Go back, to nick of the back when needed
const ci = coors[i]
for (let j = i + 1; j < coors.length; j++) {
const cj = coors[j]
if (
Math.abs(ci[0] - cj[0]) <= 0.000001 &&
Math.abs(ci[1] - cj[1]) <= 0.0000001
) {
// Found a self-intersecting way!
console.debug("SPlitting way", feature.properties.id)
result.push({
...feature,
geometry: { ...feature.geometry, coordinates: coors.slice(i + 1) },
})
coors = coors.slice(0, i + 1)
break
}
}
}
result.push({
...feature,
geometry: { ...feature.geometry, coordinates: coors },
})
}
}
return result
}
/**
* GeoOperations.distanceToHuman(52.8) // => "53m"
* GeoOperations.distanceToHuman(2800) // => "2.8km"
* GeoOperations.distanceToHuman(12800) // => "13km"
*
* @param meters
*/
public static distanceToHuman(meters: number): string {
if (meters === undefined) {
return ""
}
meters = Math.round(meters)
if (meters < 1000) {
return meters + "m"
}
if (meters >= 10000) {
const km = Math.round(meters / 1000)
return km + "km"
}
meters = Math.round(meters / 100)
const kmStr = "" + meters
return kmStr.substring(0, kmStr.length - 1) + "." + kmStr.substring(kmStr.length - 1) + "km"
}
/**
* GeoOperations.parseBearing("N") // => 0
* GeoOperations.parseBearing("E") // => 90
* GeoOperations.parseBearing("NE") // => 45
* GeoOperations.parseBearing("NNE") // => 22.5
*
* GeoOperations.parseBearing("90") // => 90
* GeoOperations.parseBearing("-90°") // => 270
* GeoOperations.parseBearing("180 °") // => 180
*
* GeoOperations.parseBearing(180) // => 180
* GeoOperations.parseBearing(-270) // => 90
*
*/
public static parseBearing(str: string | number) {
let n: number
if (typeof str === "string") {
str = str.trim()
if (str.endsWith("°")) {
str = str.substring(0, str.length - 1).trim()
}
n = Number(str)
} else {
n = str
}
if (!isNaN(n)) {
while (n < 0) {
n += 360
}
return n % 360
}
return GeoOperations.reverseBearing[str]
}
/**
* GeoOperations.bearingToHuman(0) // => "N"
* GeoOperations.bearingToHuman(-9) // => "N"
* GeoOperations.bearingToHuman(-10) // => "N"
* GeoOperations.bearingToHuman(-180) // => "S"
* GeoOperations.bearingToHuman(181) // => "S"
* GeoOperations.bearingToHuman(46) // => "NE"
*/
public static bearingToHuman(
bearing: number
): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" {
while (bearing < 0) {
bearing += 360
}
bearing %= 360
bearing += 22.5
const segment = Math.floor(bearing / 45) % GeoOperations.directions.length
return GeoOperations.directions[segment]
}
/**
* GeoOperations.bearingToHuman(0) // => "N"
* GeoOperations.bearingToHuman(-10) // => "N"
* GeoOperations.bearingToHuman(-180) // => "S"
* GeoOperations.bearingToHuman(181) // => "S"
* GeoOperations.bearingToHuman(46) // => "NE"
*/
public static bearingToHumanRelative(
bearing: number
):
| "straight"
| "slight_right"
| "right"
| "sharp_right"
| "behind"
| "sharp_left"
| "left"
| "slight_left" {
while (bearing < 0) {
bearing += 360
}
bearing %= 360
bearing += 22.5
const segment = Math.floor(bearing / 45) % GeoOperations.directionsRelative.length
return GeoOperations.directionsRelative[segment]
}
/**
* Helper function which does the heavy lifting for 'inside'
*/
@ -854,7 +1029,7 @@ export class GeoOperations {
* Returns 0 if both are linestrings
* Returns null if the features are not intersecting
*/
private static calculateInstersection(
private static calculateIntersection(
feature,
otherFeature,
featureBBox: BBox,
@ -924,7 +1099,7 @@ export class GeoOperations {
return null
}
if (otherFeature.geometry.type === "LineString") {
return this.calculateInstersection(
return this.calculateIntersection(
otherFeature,
feature,
otherFeatureBBox,
@ -944,131 +1119,22 @@ export class GeoOperations {
// See https://github.com/Turfjs/turf/pull/2238
return null
}
if (e.message.indexOf("SweepLine tree") >= 0) {
console.log("Applying fallback intersection...")
const intersection = turf.intersect(
turf.truncate(feature),
turf.truncate(otherFeature)
)
if (intersection == null) {
return null
}
return turf.area(intersection) // in m²
// Another workaround: https://github.com/Turfjs/turf/issues/2258
}
throw e
}
}
throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
}
public static SplitSelfIntersectingWays(features: Feature[]): Feature[] {
const result: Feature[] = []
for (const feature of features) {
if (feature.geometry.type === "LineString") {
let coors = feature.geometry.coordinates
for (let i = coors.length - 1; i >= 0; i--) {
// Go back, to nick of the back when needed
const ci = coors[i]
for (let j = i + 1; j < coors.length; j++) {
const cj = coors[j]
if (
Math.abs(ci[0] - cj[0]) <= 0.000001 &&
Math.abs(ci[1] - cj[1]) <= 0.0000001
) {
// Found a self-intersecting way!
console.debug("SPlitting way", feature.properties.id)
result.push({
...feature,
geometry: { ...feature.geometry, coordinates: coors.slice(i + 1) },
})
coors = coors.slice(0, i + 1)
break
}
}
}
result.push({
...feature,
geometry: { ...feature.geometry, coordinates: coors },
})
}
}
return result
}
/**
* GeoOperations.distanceToHuman(52.8) // => "53m"
* GeoOperations.distanceToHuman(2800) // => "2.8km"
* GeoOperations.distanceToHuman(12800) // => "13km"
*
* @param meters
*/
public static distanceToHuman(meters: number): string {
if (meters === undefined) {
return ""
}
meters = Math.round(meters)
if (meters < 1000) {
return meters + "m"
}
if (meters >= 10000) {
const km = Math.round(meters / 1000)
return km + "km"
}
meters = Math.round(meters / 100)
const kmStr = "" + meters
return kmStr.substring(0, kmStr.length - 1) + "." + kmStr.substring(kmStr.length - 1) + "km"
}
private static readonly directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] as const
private static readonly directionsRelative = [
"straight",
"slight_right",
"right",
"sharp_right",
"behind",
"sharp_left",
"left",
"slight_left",
] as const
/**
* GeoOperations.bearingToHuman(0) // => "N"
* GeoOperations.bearingToHuman(-9) // => "N"
* GeoOperations.bearingToHuman(-10) // => "N"
* GeoOperations.bearingToHuman(-180) // => "S"
* GeoOperations.bearingToHuman(181) // => "S"
* GeoOperations.bearingToHuman(46) // => "NE"
*/
public static bearingToHuman(
bearing: number
): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" {
while (bearing < 0) {
bearing += 360
}
bearing %= 360
bearing += 22.5
const segment = Math.floor(bearing / 45) % GeoOperations.directions.length
return GeoOperations.directions[segment]
}
/**
* GeoOperations.bearingToHuman(0) // => "N"
* GeoOperations.bearingToHuman(-10) // => "N"
* GeoOperations.bearingToHuman(-180) // => "S"
* GeoOperations.bearingToHuman(181) // => "S"
* GeoOperations.bearingToHuman(46) // => "NE"
*/
public static bearingToHumanRelative(
bearing: number
):
| "straight"
| "slight_right"
| "right"
| "sharp_right"
| "behind"
| "sharp_left"
| "left"
| "slight_left" {
while (bearing < 0) {
bearing += 360
}
bearing %= 360
bearing += 22.5
const segment = Math.floor(bearing / 45) % GeoOperations.directionsRelative.length
return GeoOperations.directionsRelative[segment]
}
}

View file

@ -84,10 +84,20 @@ export class Mapillary extends ImageProvider {
private static ExtractKeyFromURL(value: string): number {
let key: string
const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/)
if (newApiFormat !== null) {
key = newApiFormat[1]
} else if (value.startsWith(Mapillary.valuePrefix)) {
if (value.startsWith("http")) {
try {
const url = new URL(value.toLowerCase())
if (url.searchParams.has("pkey")) {
const pkey = Number(url.searchParams.get("pkey"))
if (!isNaN(pkey)) {
return pkey
}
}
} catch (e) {
console.log("Could not parse value for mapillary:", value)
}
}
if (value.startsWith(Mapillary.valuePrefix)) {
key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1)
} else if (value.match("[0-9]*")) {
key = value

View file

@ -274,17 +274,20 @@ export default class MetaTagging {
console.warn(
"Could not calculate a " +
(isStrict ? "strict " : "") +
" calculated tag for key " +
key +
" defined by " +
code +
" (in layer" +
layerId +
"calculated tag for key",
key,
"for feature",
feat.properties.id,
" defined by",
code,
"(in layer",
layerId +
") due to \n" +
e +
"\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features",
e,
e.stack
e.stack,
{ feat }
)
MetaTagging.errorPrintCount++
if (MetaTagging.errorPrintCount == MetaTagging.stopErrorOutputAt) {

View file

@ -148,7 +148,6 @@ export class Changes {
}
public applyChanges(changes: ChangeDescription[]) {
console.log("Received changes:", changes)
this.pendingChanges.data.push(...changes)
this.pendingChanges.ping()
this.allChanges.data.push(...changes)
@ -191,7 +190,6 @@ export class Changes {
}
// This is a new object that should be created
states.set(id, "created")
console.log("Creating object for changeDescription", change)
let osmObj: OsmObject = undefined
switch (change.type) {
case "node":
@ -255,7 +253,6 @@ export class Changes {
const nlon = Utils.Round7(change.changes.lon)
const n = <OsmNode>obj
if (n.lat !== nlat || n.lon !== nlon) {
console.log("Node moved:", n.lat, nlat, n.lon, nlon)
n.lat = nlat
n.lon = nlon
changed = true
@ -443,7 +440,6 @@ export class Changes {
objects.forEach((obj) => SimpleMetaTagger.removeBothTagging(obj.tags))
}
console.log("Got the fresh objects!", objects, "pending: ", pending)
if (pending.length == 0) {
console.log("No pending changes...")
return true
@ -528,9 +524,7 @@ export class Changes {
await this._changesetHandler.UploadChangeset(
(csId, remappings) => {
if (remappings.size > 0) {
console.log("Rewriting pending changes from", pending, "with", remappings)
pending = pending.map((ch) => ChangeDescriptionTools.rewriteIds(ch, remappings))
console.log("Result is", pending)
}
const changes: {

View file

@ -144,6 +144,9 @@ class CountryTagger extends SimpleMetaTagger {
tagsSource.data["_country"] = newCountry
tagsSource?.ping()
} else {
// We set, be we don't ping... this is for later
tagsSource.data["_country"] = newCountry
/**
* What is this weird construction?
*
@ -160,7 +163,6 @@ class CountryTagger extends SimpleMetaTagger {
*/
window.requestIdleCallback(() => {
tagsSource.data["_country"] = newCountry
tagsSource?.ping()
})
}
@ -478,10 +480,19 @@ export default class SimpleMetaTaggers {
// isOpen is irrelevant
return false
}
if (feature.properties.opening_hours === undefined) {
return false
}
if (feature.properties.opening_hours === "24/7") {
feature.properties._isOpen = "yes"
return true
}
console.log(
"Calculating opening hours for",
feature.properties.name,
":",
feature.properties.opening_hours
)
// _isOpen is calculated dynamically on every call
Object.defineProperty(feature.properties, "_isOpen", {
@ -492,7 +503,8 @@ export default class SimpleMetaTaggers {
if (tags.opening_hours === undefined) {
return
}
if (tags._country === undefined) {
const country = tags._country
if (country === undefined) {
return
}

View file

@ -41,6 +41,7 @@ export default class UserRelatedState {
public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
public readonly showCrosshair: UIEventSource<"yes" | "always" | "no" | undefined>
public readonly fixateNorth: UIEventSource<undefined | "yes">
public readonly a11y: UIEventSource<undefined | "always" | "never" | "default">
public readonly homeLocation: FeatureSource
/**
* The language as saved into the preferences of the user, if logged in.
@ -109,6 +110,10 @@ export default class UserRelatedState {
this.showTags = <UIEventSource<any>>this.osmConnection.GetPreference("show_tags")
this.showCrosshair = <UIEventSource<any>>this.osmConnection.GetPreference("show_crosshair")
this.fixateNorth = <UIEventSource<"yes">>this.osmConnection.GetPreference("fixate-north")
this.a11y = <UIEventSource<"always" | "never" | "default">>(
this.osmConnection.GetPreference("a11y")
)
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove")
)

View file

@ -252,7 +252,7 @@ export default class FeatureReviews {
// `u` stands for `uncertainty`, https://www.rfc-editor.org/rfc/rfc5870#section-3.4.3
const self = this
return this._name.map(function (name) {
let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}`
let uri = `geo:${self._lat},${self._lon}?u=${Math.round(self._uncertainty)}`
if (name) {
uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name))
}

View file

@ -83,6 +83,15 @@ export class AvailableRasterLayers {
})
)
}
public static allIds(): Set<string> {
const all: string[] = []
all.push(...AvailableRasterLayers.globalLayers.map((l) => l.properties.id))
all.push(...AvailableRasterLayers.EditorLayerIndex.map((l) => l.properties.id))
all.push(this.osmCarto.properties.id)
all.push(this.maptilerDefaultLayer.properties.id)
return new Set<string>(all)
}
}
export class RasterLayerUtils {

View file

@ -55,12 +55,19 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
for (const key in obj) {
let subtarget = target
if (isTr && target[key] !== undefined) {
if (isTr) {
// The target is a translation AND the current object is a translation
// This means we should recursively replace with the translated value
subtarget = target[key]
if (target[key]) {
// A translation is available!
subtarget = target[key]
} else if (target["en"]) {
subtarget = target["en"]
} else {
// Take the first
subtarget = target[Object.keys(target)[0]]
}
}
obj[key] = replaceRecursive(obj[key], subtarget)
}
return obj

View file

@ -32,6 +32,7 @@ import { ConfigMeta } from "../../../UI/Studio/configMeta"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
import { ConversionContext } from "./ConversionContext"
import { ExpandRewrite } from "./ExpandRewrite"
import { ALL } from "node:dns"
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
private static readonly predefinedFilters = ExpandFilter.load_filters()
@ -1133,9 +1134,43 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
)
}
private createTitleIconsBasedOn(
tr: QuestionableTagRenderingConfigJson
): TagRenderingConfigJson | undefined {
const mappings: { if: TagConfigJson; then: string }[] = tr.mappings
?.filter((m) => m.icon !== undefined)
.map((m) => {
const path: string = typeof m.icon === "string" ? m.icon : m.icon.path
const img = `<img class="m-1 h-6 w-6 low-interaction rounded" src='${path}'/>`
return { if: m.if, then: img }
})
if (!mappings || mappings.length === 0) {
return undefined
}
return <TagRenderingConfigJson>{
id: "title_icon_auto_" + tr.id,
mappings,
}
}
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
json = { ...json }
json.titleIcons = [...json.titleIcons]
const allAutoIndex = json.titleIcons.indexOf(<any>"auto:*")
if (allAutoIndex >= 0) {
const generated = Utils.NoNull(
json.tagRenderings.map((tr) => {
if (typeof tr === "string") {
return undefined
}
return this.createTitleIconsBasedOn(<any>tr)
})
)
json.titleIcons.splice(allAutoIndex, 1, ...generated)
return json
}
for (let i = 0; i < json.titleIcons.length; i++) {
const titleIcon = json.titleIcons[i]
if (typeof titleIcon !== "string") {
@ -1152,14 +1187,9 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
context.enters("titleIcons", i).err("TagRendering with id " + trId + " not found")
continue
}
const mappings: { if: TagConfigJson; then: string }[] = tr.mappings
?.filter((m) => m.icon !== undefined)
.map((m) => {
const path: string = typeof m.icon === "string" ? m.icon : m.icon.path
const img = `<img class="m-1 h-6 w-6 low-interaction rounded" src='${path}'/>`
return { if: m.if, then: img }
})
if (mappings.length === 0) {
const generated = this.createTitleIconsBasedOn(tr)
if (!generated) {
context
.enters("titleIcons", i)
.warn(
@ -1169,10 +1199,7 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
)
continue
}
json.titleIcons[i] = <TagRenderingConfigJson>{
id: "title_icon_auto_" + trId,
mappings,
}
json.titleIcons[i] = generated
}
return json
}

View file

@ -21,6 +21,9 @@ import PresetConfig from "../PresetConfig"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { Translatable } from "../Json/Translatable"
import { ConversionContext } from "./ConversionContext"
import * as eli from "../../../assets/editor-layer-index.json"
import { AvailableRasterLayers } from "../../RasterLayers"
import Back from "../../../assets/svg/Back.svelte"
class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> {
private readonly _languages: string[]
@ -124,6 +127,7 @@ export class DoesImageExist extends DesugaringStep<string> {
}
export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
private static readonly _availableLayers = AvailableRasterLayers.allIds()
/**
* The paths where this layer is originally saved. Triggers some extra checks
* @private
@ -260,6 +264,19 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
.err("The overpassURL is a string, use a list of strings instead. Wrap it with [ ]")
}
if (json.defaultBackgroundId) {
const backgroundId = json.defaultBackgroundId
const isCategory =
backgroundId === "photo" || backgroundId === "map" || backgroundId === "osmbasedmap"
if (!isCategory && !ValidateTheme._availableLayers.has(backgroundId)) {
context
.enter("defaultBackgroundId")
.err("This layer ID is not known: " + backgroundId)
}
}
return json
}
}
@ -421,6 +438,7 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
// No need to check the writable tags, as this cannot write
return json
}
function addAll(keys: { forEach: (f: (s: string) => void) => void }, addTo: Set<string>) {
keys?.forEach((k) => addTo.add(k))
}
@ -1359,6 +1377,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
private readonly validator: ValidateLayer
constructor(
path: string,
isBuiltin: boolean,
@ -1385,6 +1404,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
return prepared?.raw
}
}
export class ValidateLayer extends Conversion<
LayerConfigJson,
{ parsed: LayerConfig; raw: LayerConfigJson }
@ -1462,6 +1482,19 @@ export class ValidateLayer extends Conversion<
}
}
for (let i = 0; i < json.presets?.length; i++) {
const preset = json.presets[i]
if (
preset.snapToLayer === undefined &&
preset.maxSnapDistance !== undefined &&
preset.maxSnapDistance !== null
) {
context
.enters("presets", i, "maxSnapDistance")
.err("A maxSnapDistance is given, but there is no layer given to snap to")
}
}
return { raw: json, parsed: layerConfig }
}
}

View file

@ -145,7 +145,7 @@ export interface LayerConfigJson {
* There are a few extra functions available. Refer to <a>Docs/CalculatedTags.md</a> for more information
* The functions will be run in order, e.g.
* [
Not found... * "_max_overlap_m2=Math.max(...feat.overlapsWith("someOtherLayer").map(o => o.overlap))
* "_max_overlap_m2=Math.max(...feat.overlapsWith("someOtherLayer").map(o => o.overlap))
* "_max_overlap_ratio=Number(feat._max_overlap_m2)/feat.area
* ]
*

View file

@ -184,14 +184,6 @@ export default class LayerConfig extends WithContextLoader {
snapToLayers,
maxSnapDistance: pr.maxSnapDistance ?? 10,
}
} else if (pr.maxSnapDistance !== undefined) {
throw (
"Layer " +
this.id +
" defines a maxSnapDistance, but does not include a `snapToLayer` (at " +
context +
")"
)
}
const config: PresetConfig = {

View file

@ -314,6 +314,7 @@ export default class LayoutConfig implements LayoutInformation {
return layer
}
}
console.log("Fallthrough", this, tags)
return undefined
}
}

View file

@ -79,7 +79,7 @@ export default class TagRenderingConfig {
public readonly mappings?: Mapping[]
public readonly editButtonAriaLabel?: Translation
public readonly labels: string[]
public readonly classes: string[]
public readonly classes: string[] | undefined
constructor(
config: string | TagRenderingConfigJson | QuestionableTagRenderingConfigJson,
@ -131,6 +131,9 @@ export default class TagRenderingConfig {
this.classes = json.classes ?? []
}
this.classes = [].concat(...this.classes.map((cl) => cl.split(" ")))
if (this.classes.length === 0) {
this.classes = undefined
}
this.render = Translations.T(<any>json.render, translationKey + ".render")
this.question = Translations.T(json.question, translationKey + ".question")
@ -233,8 +236,10 @@ export default class TagRenderingConfig {
const commonIconSize =
Utils.NoNull(
json.mappings.map((m) => (m.icon !== undefined ? m.icon["class"] : undefined))
)[0] ?? "small"
json.mappings.map((m) => (!!m.icon ? m.icon["class"] : undefined))
)[0] ??
json["#iconsize"] ??
"small"
this.mappings = json.mappings.map((m, i) =>
TagRenderingConfig.ExtractMapping(
m,
@ -364,7 +369,7 @@ export default class TagRenderingConfig {
let icon = undefined
let iconClass = commonSize
if (mapping.icon !== undefined) {
if (!!mapping.icon) {
if (typeof mapping.icon === "string" && mapping.icon !== "") {
let stripped = mapping.icon
if (stripped.endsWith(".svg")) {
@ -378,7 +383,7 @@ export default class TagRenderingConfig {
} else {
icon = mapping.icon
}
} else {
} else if (mapping.icon["path"]) {
icon = mapping.icon["path"]
iconClass = mapping.icon["class"] ?? iconClass
}
@ -647,12 +652,6 @@ export default class TagRenderingConfig {
multiSelectedMapping: boolean[] | undefined,
currentProperties: Record<string, string>
): UploadableTag {
console.log("Constructing change spec", {
freeformValue,
singleSelectedMapping,
multiSelectedMapping,
currentProperties,
})
if (typeof freeformValue === "string") {
freeformValue = freeformValue?.trim()
}

View file

@ -452,7 +452,17 @@ export default class ThemeViewState implements SpecialVisualizationState {
* Various small methods that need to be called
*/
private miscSetup() {
this.userRelatedState.a11y.addCallbackAndRunD((a11y) => {
if (a11y === "always") {
this.visualFeedback.setData(true)
} else if (a11y === "never") {
this.visualFeedback.setData(false)
}
})
this.mapProperties.onKeyNavigationEvent((keyEvent) => {
if (this.userRelatedState.a11y.data === "never") {
return
}
if (["north", "east", "south", "west"].indexOf(keyEvent.key) >= 0) {
this.visualFeedback.setData(true)
return true // Our job is done, unregister
@ -482,7 +492,9 @@ export default class ThemeViewState implements SpecialVisualizationState {
* @private
*/
private selectClosestAtCenter(i: number = 0) {
this.visualFeedback.setData(true)
if (this.userRelatedState.a11y.data !== "never") {
this.visualFeedback.setData(true)
}
const toSelect = this.closestFeatures.features?.data?.[i]
if (!toSelect) {
window.requestAnimationFrame(() => {

View file

@ -9,7 +9,7 @@
export let clss: string | undefined = undefined
</script>
<button class={clss} on:click={() => osmConnection.AttemptLogin()}>
<button class={clss} on:click={() => osmConnection.AttemptLogin()} style="margin-left: 0">
<ToSvelte construct={Svg.login_svg().SetClass("w-12 m-1")} />
<slot>
<Tr t={Translations.t.general.loginWithOpenStreetMap} />

View file

@ -8,6 +8,7 @@
onMount(() => {
const uiElem = typeof construct === "function" ? construct() : construct
html = uiElem?.ConstructElement()
if (html !== undefined) {
elem?.replaceWith(html)
}

View file

@ -10,6 +10,7 @@
import { createEventDispatcher, onDestroy } from "svelte"
import { placeholder } from "../../Utils/placeholder"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import { ariaLabel } from "../../Utils/ariaLabel"
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
export let bounds: UIEventSource<BBox>
@ -116,6 +117,7 @@
}}
bind:value={searchContents}
use:placeholder={Translations.t.general.search.search}
use:ariaLabel={Translations.t.general.search.search}
/>
{#if feedback !== undefined}
<!-- The feedback is _always_ shown for screenreaders and to make sure that the searchfield can still be selected by tabbing-->

View file

@ -30,6 +30,7 @@
import Direction_gradient from "../../assets/svg/Direction_gradient.svelte"
import Mastodon from "../../assets/svg/Mastodon.svelte"
import Party from "../../assets/svg/Party.svelte"
import AddSmall from "../../assets/svg/AddSmall.svelte"
/**
* Renders a single icon.
@ -111,6 +112,8 @@
<Mastodon {color} class={clss} />
{:else if icon === "party"}
<Party {color} class={clss} />
{:else if icon === "addSmall"}
<AddSmall {color} class={clss} />
{:else}
<img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true" alt="" />
{/if}

View file

@ -511,15 +511,25 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
await Utils.waitFor(250)
}
}
public installCustomKeyboardHandler(viewport: Store<HTMLDivElement>) {
viewport.mapD(
public installCustomKeyboardHandler(viewportStore: UIEventSource<HTMLDivElement>) {
viewportStore.mapD(
(viewport) => {
const map = this._maplibreMap.data
if (!map) {
return
}
const oldKeyboard = map.keyboard
oldKeyboard._panStep = viewport.getBoundingClientRect().width
const w = viewport.getBoundingClientRect().width
if (w < 10) {
/// this is weird, but definitively wrong!
console.log("Got a very small bound", w, viewport)
// We try again later on
window.requestAnimationFrame(() => {
viewportStore.ping()
})
return
}
oldKeyboard._panStep = w
},
[this._maplibreMap]
)

View file

@ -159,7 +159,7 @@ class ApplyButton extends UIElement {
private async Run() {
try {
console.log("Applying auto-action on " + this.target_feature_ids.length + " features")
const appliedOn: string[] = []
for (let i = 0; i < this.target_feature_ids.length; i++) {
const targetFeatureId = this.target_feature_ids[i]
const feature = this.state.indexedFeatures.featuresById.data.get(targetFeatureId)
@ -190,6 +190,7 @@ class ApplyButton extends UIElement {
specialRendering.args
)
}
appliedOn.push(targetFeatureId)
if (i % 50 === 0) {
await this.state.changes.flushChanges("Auto button: intermediate save")
}
@ -198,6 +199,12 @@ class ApplyButton extends UIElement {
console.log("Flushing changes...")
await this.state.changes.flushChanges("Auto button: done")
this.buttonState.setData("done")
console.log(
"Applied changes onto",
appliedOn.length,
"items, unique IDs:",
new Set(appliedOn).size
)
} catch (e) {
console.error("Error while running autoApply: ", e)
this.buttonState.setData({ error: e })

View file

@ -127,13 +127,6 @@
<button slot="cancel" class="items-center" on:click={() => (currentState = "start")}>
<Tr t={t.cancel} />
</button>
<XCircleIcon
slot="upper-right"
class="h-8 w-8 cursor-pointer"
on:click={() => {
currentState = "start"
}}
/>
<div slot="under-buttons">
{#if selectedTags !== undefined}

View file

@ -20,6 +20,7 @@ export class ShareLinkViz implements SpecialVisualization {
},
]
needsUrls = []
svelteBased = true
public constr(
state: SpecialVisualizationState,
@ -52,6 +53,8 @@ export class ShareLinkViz implements SpecialVisualization {
}
}
return new SvelteUIElement(ShareButton, { generateShareData, text })
return new SvelteUIElement(ShareButton, { generateShareData, text }).SetClass(
"w-full h-full"
)
}
}

View file

@ -41,7 +41,7 @@
}
let skippedQuestions = new UIEventSource<Set<string>>(new Set<string>())
let questionboxElem: HTMLBaseElement
let questionboxElem: HTMLDivElement
let questionsToAsk = tags.map(
(tags) => {
const baseQuestions = (layer.tagRenderings ?? [])?.filter(
@ -49,7 +49,7 @@
)
const questionsToAsk: TagRenderingConfig[] = []
for (const baseQuestion of baseQuestions) {
if (skippedQuestions.data.has(baseQuestion.id) > 0) {
if (skippedQuestions.data.has(baseQuestion.id)) {
continue
}
if (
@ -88,6 +88,7 @@
<div
bind:this={questionboxElem}
aria-live="polite"
class="marker-questionbox-root"
class:hidden={$questionsToAsk.length === 0 && skipped === 0 && answered === 0}
>

View file

@ -29,7 +29,7 @@
</script>
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties($tags))}
<div {id} class={twMerge("link-underline inline-block w-full", config?.classes, extraClasses)}>
<div {id} class={twMerge("link-underline inline-block w-full", config?.classes , extraClasses)}>
{#if $trs.length === 1}
<TagRenderingMapping mapping={$trs[0]} {tags} {state} {selectedElement} {layer} />
{/if}

View file

@ -23,7 +23,7 @@
export let editingEnabled: Store<boolean> | undefined = state?.featureSwitchUserbadge
export let highlightedRendering: UIEventSource<string> = undefined
export let clss
export let clss = undefined
/**
* Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property
*/
@ -98,16 +98,6 @@
>
<Tr t={Translations.t.general.cancel} />
</button>
<button
slot="upper-right"
class="h-8 w-8 cursor-pointer border-none p-0"
use:ariaLabel={Translations.t.general.cancel}
on:click={() => {
editMode = false
}}
>
<XCircleIcon />
</button>
</TagRenderingQuestion>
{:else}
<div class="low-interaction flex items-center justify-between overflow-hidden rounded px-2">

View file

@ -12,7 +12,6 @@
export let tags: UIEventSource<Record<string, string>>
export let state: SpecialVisualizationState
export let layer: LayerConfig
export let mapping: {
readonly then: Translation
readonly searchTerms?: Record<string, string[]>
@ -30,7 +29,7 @@
{#if mapping.icon !== undefined}
<div class="inline-flex items-center">
<Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass}`, "mx-2")} />
<Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-2")} />
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement} />
</div>
{:else if mapping.then !== undefined}

View file

@ -83,7 +83,7 @@
</script>
{#if $matchesTerm && !$mappingIsHidden}
<label class={twJoin("flex", mappingIsSelected && "checked")}>
<label class={twJoin("flex gap-x-1", mappingIsSelected && "checked")}>
<slot />
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer} />
</label>

View file

@ -151,7 +151,7 @@
$freeformInput,
selectedMapping,
checkedMappings,
tags.data
tags.data,
)
} catch (e) {
console.error("Could not calculate changeSpecification:", e)
@ -213,136 +213,53 @@
onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount
})
}),
)
}
</script>
{#if question !== undefined}
<form
class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
style="max-height: 75vh"
on:submit|preventDefault={() => onSave()}
>
<label class="neutral-label">
<div class="interactive sticky top-0 flex justify-between pt-1" style="z-index: 11">
<span class="font-bold">
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
</span>
<slot name="upper-right" />
</div>
<div class="relative">
{#if config.questionhint}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{/if}
<form
class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
style="max-height: 75vh"
on:submit|preventDefault={() => onSave()}
>
<fieldset>
{#if config.mappings?.length >= 8}
<div class="sticky flex w-full" aria-hidden="true">
<Search class="h-6 w-6" />
<input
type="text"
bind:value={$searchTerm}
class="w-full"
use:placeholder={Translations.t.general.searchAnswer}
/>
</div>
{/if}
<legend>
<div class="interactive sticky top-0 justify-between pt-1 font-bold" style="z-index: 11">
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
</div>
{#if config.freeform?.key && !(mappings?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:submit={onSave}
/>
{:else if mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
<TagRenderingMappingInput
{mapping}
{tags}
{state}
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={selectedMapping === i}
>
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={i}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex">
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={config.mappings?.length}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:selected={() => (selectedMapping = config.mappings?.length)}
on:submit={onSave}
/>
</label>
{/if}
</div>
{:else if mappings !== undefined && config.multiAnswer}
<!-- Multiple answers can be chosen: checkboxes -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<TagRenderingMappingInput
{mapping}
{tags}
{state}
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={checkedMappings[i]}
>
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + i}
bind:checked={checkedMappings[i]}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex">
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]}
on:keypress={(e) => onInputKeypress(e)}
/>
{#if config.questionhint}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{/if}
</legend>
{#if config.mappings?.length >= 8}
<div class="sticky flex w-full" aria-hidden="true">
<Search class="h-6 w-6" />
<input
type="text"
bind:value={$searchTerm}
class="w-full"
use:placeholder={Translations.t.general.searchAnswer}
/>
</div>
{/if}
{#if config.freeform?.key && !(mappings?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
<FreeformInput
{config}
{tags}
@ -353,39 +270,122 @@
value={freeformInput}
on:submit={onSave}
/>
</label>
{:else if mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
<TagRenderingMappingInput
{mapping}
{tags}
{state}
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={selectedMapping === i}
>
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={i}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex gap-x-1">
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={config.mappings?.length}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:selected={() => (selectedMapping = config.mappings?.length)}
on:submit={onSave}
/>
</label>
{/if}
</div>
{:else if mappings !== undefined && config.multiAnswer}
<!-- Multiple answers can be chosen: checkboxes -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<TagRenderingMappingInput
{mapping}
{tags}
{state}
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={checkedMappings[i]}
>
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + i}
bind:checked={checkedMappings[i]}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex gap-x-1">
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:submit={onSave}
/>
</label>
{/if}
</div>
{/if}
<LoginToggle {state}>
<Loading slot="loading" />
<SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}>
<Login slot="image" class="h-8 w-8" />
<Tr t={Translations.t.general.loginToStart} slot="message" />
</SubtleButton>
{#if $feedback !== undefined}
<div class="alert" aria-live="assertive" role="alert">
<Tr t={$feedback} />
</div>
{/if}
</div>
{/if}
</label>
<LoginToggle {state}>
<Loading slot="loading" />
<SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}>
<Login slot="image" class="h-8 w-8" />
<Tr t={Translations.t.general.loginToStart} slot="message" />
</SubtleButton>
{#if $feedback !== undefined}
<div class="alert" aria-live="assertive" role="alert">
<Tr t={$feedback} />
</div>
{/if}
<div
class="interactive sticky bottom-0 flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap"
style="z-index: 11"
>
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel" />
<slot name="save-button" {selectedTags}>
<button
on:click={onSave}
class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")}
<div
class="interactive sticky bottom-0 flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap"
style="z-index: 11"
>
<Tr t={Translations.t.general.save} />
</button>
</slot>
</div>
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel" />
<slot name="save-button" {selectedTags}>
<button
on:click={onSave}
class={twJoin(selectedTags === undefined ? "disabled" : "button-shadow", "primary")}
>
<Tr t={Translations.t.general.save} />
</button>
</slot>
</div>
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}
<span class="flex flex-wrap justify-between">
<TagHint {state} tags={selectedTags} currentProperties={$tags} />
<span class="flex flex-wrap">
@ -397,8 +397,12 @@
{/if}
</span>
</span>
{/if}
<slot name="under-buttons" />
</LoginToggle>
</form>
{/if}
<slot name="under-buttons" />
</LoginToggle>
</fieldset>
</form>
</div>
{/if}

View file

@ -98,7 +98,7 @@ export interface SpecialVisualization {
readonly funcName: string
readonly docs: string | BaseUIElement
readonly example?: string
readonly needsUrls: string[] | ((args: string[]) => string)
readonly needsUrls?: string[] | ((args: string[]) => string)
/**
* Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included

View file

@ -102,6 +102,7 @@ class NearbyImageVis implements SpecialVisualization {
"A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature"
funcName = "nearby_images"
needsUrls = NearbyImagesSearch.apiUrls
svelteBased = true
constr(
state: SpecialVisualizationState,
@ -141,6 +142,7 @@ class StealViz implements SpecialVisualization {
},
]
needsUrls = []
svelteBased = true
constr(state: SpecialVisualizationState, featureTags, args) {
const [featureIdKey, layerAndtagRenderingIds] = args
@ -213,6 +215,7 @@ export class QuestionViz implements SpecialVisualization {
doc: "One or more ';'-separated labels of questions which should _not_ be included",
},
]
svelteBased = true
constr(
state: SpecialVisualizationState,
@ -236,7 +239,7 @@ export class QuestionViz implements SpecialVisualization {
state,
onlyForLabels: labels,
notForLabels: blacklist,
})
}).SetClass("w-full")
}
}
@ -437,7 +440,7 @@ export default class SpecialVisualizations {
funcName: "add_new_point",
docs: "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`",
args: [],
needsUrls: [],
constr(state: SpecialVisualizationState, _, __, feature): BaseUIElement {
let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(AddNewPoint, {
@ -449,7 +452,7 @@ export default class SpecialVisualizations {
{
funcName: "user_profile",
args: [],
needsUrls: [],
docs: "A component showing information about the currently logged in user (username, profile description, profile picture + link to edit them). Mostly meant to be used in the 'user-settings'",
constr(state: SpecialVisualizationState): BaseUIElement {
return new SvelteUIElement(UserProfile, {
@ -460,7 +463,6 @@ export default class SpecialVisualizations {
{
funcName: "language_picker",
args: [],
needsUrls: [],
docs: "A component to set the language of the user interface",
constr(state: SpecialVisualizationState): BaseUIElement {
return new SvelteUIElement(LanguagePicker, {
@ -477,6 +479,7 @@ export default class SpecialVisualizations {
args: [],
needsUrls: [Constants.osmAuthConfig.url],
docs: "Shows a button where the user can log out",
constr(state: SpecialVisualizationState): BaseUIElement {
return new SvelteUIElement(LogoutButton, { osmConnection: state.osmConnection })
},
@ -488,7 +491,7 @@ export default class SpecialVisualizations {
funcName: "split_button",
docs: "Adds a button which allows to split a way",
args: [],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>
@ -509,7 +512,7 @@ export default class SpecialVisualizations {
funcName: "move_button",
docs: "Adds a button which allows to move the object to another location. The config will be read from the layer config",
args: [],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
@ -532,7 +535,7 @@ export default class SpecialVisualizations {
funcName: "delete_button",
docs: "Adds a button which allows to delete the object at this location. The config will be read from the layer config",
args: [],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
@ -597,6 +600,7 @@ export default class SpecialVisualizations {
},
],
needsUrls: [...Wikidata.neededUrls, ...Wikipedia.neededUrls],
example:
"`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height",
constr: (_, tagsSource, args) => {
@ -650,7 +654,6 @@ export default class SpecialVisualizations {
funcName: "all_tags",
docs: "Prints all key-value pairs of the object - used for debugging",
args: [],
needsUrls: [],
constr: (state, tags: UIEventSource<any>) =>
new SvelteUIElement(AllTagsPanel, { tags, state }),
},
@ -820,7 +823,7 @@ export default class SpecialVisualizations {
doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__",
},
],
needsUrls: [],
example:
"A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`",
constr: (state, tagSource: UIEventSource<any>, args) => {
@ -848,14 +851,13 @@ export default class SpecialVisualizations {
doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__",
},
],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
): SvelteUIElement {
const keyToUse = args[0]
const prefix = args[1]
const postfix = args[2]
@ -870,7 +872,7 @@ export default class SpecialVisualizations {
},
{
funcName: "canonical",
needsUrls: [],
docs: "Converts a short, canonical value into the long, translated text including the unit. This only works if a `unit` is defined for the corresponding value. The unit specification will be included in the text. ",
example:
"If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ...",
@ -910,7 +912,7 @@ export default class SpecialVisualizations {
funcName: "export_as_geojson",
docs: "Exports the selected feature as GeoJson-file",
args: [],
needsUrls: [],
constr: (state, tagSource, tagsSource, feature, layer) => {
const t = Translations.t.general.download
@ -942,7 +944,7 @@ export default class SpecialVisualizations {
funcName: "open_in_iD",
docs: "Opens the current view in the iD-editor",
args: [],
needsUrls: [],
constr: (state, feature) => {
return new SvelteUIElement(OpenIdEditor, {
mapProperties: state.mapProperties,
@ -964,7 +966,7 @@ export default class SpecialVisualizations {
funcName: "clear_location_history",
docs: "A button to remove the travelled track information from the device",
args: [],
needsUrls: [],
constr: (state) => {
return new SubtleButton(
Svg.delete_icon_svg().SetStyle("height: 1.5rem"),
@ -1023,6 +1025,7 @@ export default class SpecialVisualizations {
},
],
needsUrls: [Imgur.apiUrl],
constr: (state, tags, args) => {
const id = tags.data[args[0] ?? "id"]
tags = state.featureProperties.getStore(id)
@ -1033,7 +1036,7 @@ export default class SpecialVisualizations {
{
funcName: "title",
args: [],
needsUrls: [],
docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'",
example:
"`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.",
@ -1145,6 +1148,7 @@ export default class SpecialVisualizations {
defaultValue: "mr_taskId",
},
],
constr: (state, tagsSource, args) => {
let [message, image, message_closed, statusToSet, maproulette_id_key] = args
if (image === "") {
@ -1168,7 +1172,7 @@ export default class SpecialVisualizations {
funcName: "statistics",
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
args: [],
needsUrls: [],
constr: (state) => {
return new Combine(
state.layout.layers
@ -1211,7 +1215,6 @@ export default class SpecialVisualizations {
required: true,
},
],
needsUrls: [],
constr(__, tags, args) {
return new SvelteUIElement(SendEmail, { args, tags })
@ -1244,7 +1247,7 @@ export default class SpecialVisualizations {
doc: "If set, this text will be used as aria-label",
},
],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
@ -1273,7 +1276,7 @@ export default class SpecialVisualizations {
{
funcName: "multi",
docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering",
needsUrls: [],
example:
"```json\n" +
JSON.stringify(
@ -1327,7 +1330,7 @@ export default class SpecialVisualizations {
{
funcName: "translated",
docs: "If the given key can be interpreted as a JSON, only show the key containing the current language (or 'en'). This specialRendering is meant to be used by MapComplete studio and is not useful in map themes",
needsUrls: [],
args: [
{
name: "key",
@ -1366,7 +1369,7 @@ export default class SpecialVisualizations {
required: true,
},
],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
@ -1396,7 +1399,7 @@ export default class SpecialVisualizations {
{
funcName: "braced",
docs: "Show a literal text within braces",
needsUrls: [],
args: [
{
name: "text",
@ -1417,7 +1420,7 @@ export default class SpecialVisualizations {
{
funcName: "tags",
docs: "Shows a (json of) tags in a human-readable way + links to the wiki",
needsUrls: [],
args: [
{
name: "key",
@ -1468,6 +1471,7 @@ export default class SpecialVisualizations {
],
docs: "Shows events that are happening based on a Giggity URL",
needsUrls: (args) => args[0],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
@ -1481,7 +1485,7 @@ export default class SpecialVisualizations {
},
{
funcName: "gps_all_tags",
needsUrls: [],
docs: "Shows the current tags of the GPS-representing object, used for debugging",
args: [],
constr(
@ -1507,9 +1511,10 @@ export default class SpecialVisualizations {
},
{
funcName: "favourite_status",
needsUrls: [],
docs: "A button that allows a (logged in) contributor to mark a location as a favourite location",
args: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
@ -1527,7 +1532,7 @@ export default class SpecialVisualizations {
},
{
funcName: "favourite_icon",
needsUrls: [],
docs: "A small button that allows a (logged in) contributor to mark a location as a favourite location, sized to fit a title-icon",
args: [],
constr(
@ -1542,13 +1547,13 @@ export default class SpecialVisualizations {
state,
layer,
feature,
})
}).SetClass("w-full h-full")
},
},
{
funcName: "direction_indicator",
args: [],
needsUrls: [],
docs: "Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object",
constr(
state: SpecialVisualizationState,
@ -1563,7 +1568,7 @@ export default class SpecialVisualizations {
{
funcName: "qr_code",
args: [],
needsUrls: [],
docs: "Generates a QR-code to share the selected object",
constr(
state: SpecialVisualizationState,
@ -1598,6 +1603,41 @@ export default class SpecialVisualizations {
)
},
},
{
funcName: "direction_absolute",
docs: "Converts compass degrees (with 0° being north, 90° being east, ...) into a human readable, translated direction such as 'north', 'northeast'",
args: [
{
name: "key",
doc: "The attribute containing the degrees",
defaultValue: "_direction:centerpoint",
},
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
return new VariableUiElement(
tagSource
.map((tags) => {
console.log("Direction value", tags[key], key)
return tags[key]
})
.mapD((value) => {
const dir = GeoOperations.bearingToHuman(
GeoOperations.parseBearing(value)
)
console.log("Human dir", dir)
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
})
)
},
},
]
specialVisualizations.push(new AutoApplyButton(specialVisualizations))

View file

@ -24,7 +24,7 @@
export let backToStudio: () => void
let messages = state.messages
let hasErrors = messages.mapD(
(m: ConversionMessage[]) => m.filter((m) => m.level === "error").length
(m: ConversionMessage[]) => m.filter((m) => m.level === "error").length,
)
const configuration = state.configuration
@ -73,6 +73,7 @@
})
let highlightedItem: UIEventSource<HighlightedTagRendering> = state.highlightedItem
function deleteLayer() {
state.delete()
backToStudio()
@ -93,15 +94,32 @@
{:else if $hasErrors > 0}
<div class="alert">{$hasErrors} errors detected</div>
{:else}
<a
class="primary button"
href={baseUrl + state.server.layerUrl(title.data)}
target="_blank"
rel="noopener"
>
Try it out
<ChevronRightIcon class="h-6 w-6 shrink-0" />
</a>
<div class="flex">
<a
class="button small"
href={baseUrl + state.server.layerUrl(title.data) + "&test=true"}
target="_blank"
rel="noopener"
>
<div class="flex flex-col">
<b>
Test in safe mode
</b>
<div>No changes are recoded to OSM</div>
</div>
<ChevronRightIcon class="h-6 w-6 shrink-0" />
</a>
<a
class="primary button"
href={baseUrl + state.server.layerUrl(title.data)}
target="_blank"
rel="noopener"
>
Try it out
<ChevronRightIcon class="h-6 w-6 shrink-0" />
</a>
</div>
{/if}
</div>
@ -120,7 +138,8 @@
<Region {state} configs={perRegion["Basic"]} />
<div class="mt-12">
<button on:click={() => deleteLayer()} class="small">
<TrashIcon class="h-6 w-6" /> Delete this layer
<TrashIcon class="h-6 w-6" />
Delete this layer
</button>
</div>
</div>

View file

@ -8,15 +8,14 @@
import EditLayerState from "./EditLayerState"
import { onDestroy } from "svelte"
import type { JsonSchemaType } from "./jsonSchema"
import { ConfigMetaUtils } from "./configMeta.ts"
import { ConfigMetaUtils } from "./configMeta"
import ShowConversionMessage from "./ShowConversionMessage.svelte"
export let state: EditLayerState
export let path: (string | number)[] = []
export let schema: ConfigMeta
export let startInEditModeIfUnset: boolean = schema.hints && !schema.hints.ifunset
let value = new UIEventSource<string | any>(undefined)
const isTranslation =
schema.hints?.typehint === "translation" ||
schema.hints?.typehint === "rendered" ||
@ -144,10 +143,10 @@
path,
tags.map((tgs) => {
const v = tgs["value"]
if (typeof v !== "string") {
return { ...v }
if (typeof v === "object") {
return { ...<object>v }
}
if (schema.type === "boolan") {
if (schema.type === "boolean") {
return v === "true" || v === "yes" || v === "1"
}
if (mightBeBoolean(schema.type)) {
@ -159,7 +158,7 @@
}
}
if (schema.type === "number") {
if (v === "") {
if (v === "" || v === null || isNaN(Number(v))) {
return undefined
}
return Number(v)

View file

@ -95,7 +95,7 @@
const version = meta.version
async function editLayer(event: Event) {
const layerId: { owner: number; id: string } = event.detail
const layerId: { owner: number; id: string } = event["detail"]
state = "loading"
editLayerState.startSavingUpdates(false)
editLayerState.configuration.setData(await studio.fetch(layerId.id, "layers", layerId.owner))
@ -104,7 +104,7 @@
}
async function editTheme(event: Event) {
const id: { id: string; owner: number } = event.detail
const id: { id: string; owner: number } = event["detail"]
state = "loading"
editThemeState.startSavingUpdates(false)
editThemeState.configuration.setData(await studio.fetch(id.id, "themes", id.owner))

View file

@ -13,13 +13,7 @@
import type { MapProperties } from "../Models/MapProperties"
import Geosearch from "./BigComponents/Geosearch.svelte"
import Translations from "./i18n/Translations"
import {
CogIcon,
EyeIcon,
HeartIcon,
MenuIcon,
XCircleIcon,
} from "@rgossiaux/svelte-heroicons/solid"
import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import Tr from "./Base/Tr.svelte"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
import FloatOver from "./Base/FloatOver.svelte"
@ -72,14 +66,12 @@
import FilterPanel from "./BigComponents/FilterPanel.svelte"
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
import { BBox } from "../Logic/BBox"
import { MapLibreAdaptor } from "./Map/MapLibreAdaptor.js"
import { QueryParameters } from "../Logic/Web/QueryParameters"
export let state: ThemeViewState
let layout = state.layout
let maplibremap: UIEventSource<MlMap> = state.map
let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined)
selectedElement.addCallbackAndRun(se => console.log("Selected element", se))
let compass = Orientation.singleton.alpha
let compassLoaded = Orientation.singleton.gotMeasurement
Orientation.singleton.startMeasurements()
@ -100,8 +92,12 @@
})
})
let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) =>
state.layout.getMatchingLayer(element.properties)
let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => {
if (element.properties.id.startsWith("current_view")) {
return currentViewLayer
}
return state.layout.getMatchingLayer(element.properties)
},
)
let currentZoom = state.mapProperties.zoom
let showCrosshair = state.userRelatedState.showCrosshair
@ -109,6 +105,7 @@
let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined)
let mapproperties: MapProperties = state.mapProperties
state.mapProperties.installCustomKeyboardHandler(viewport)
function updateViewport() {
const rect = viewport.data?.getBoundingClientRect()
if (!rect) {
@ -142,7 +139,7 @@
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name
})
}),
)
let previewedImage = state.previewedImage
@ -189,9 +186,11 @@
<div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2">
<If condition={state.visualFeedback}>
<div class="w-fit">
<VisualFeedbackPanel {state} />
</div>
{#if $selectedElement === undefined}
<div class="w-fit">
<VisualFeedbackPanel {state} />
</div>
{/if}
</If>
<If condition={state.featureSwitches.featureSwitchSearch}>
<Geosearch
@ -226,7 +225,7 @@
{#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()}
<MapControlButton
on:click={() => {
selectedElement.setData(state.currentView.features?.data?.[0])
state.selectedElement.setData(state.currentView.features?.data?.[0])
}}
on:keydown={forwardEventToMap}
>

View file

@ -1448,7 +1448,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
d.setUTCMinutes(0)
}
public static scrollIntoView(element: HTMLBaseElement) {
public static scrollIntoView(element: HTMLBaseElement | HTMLDivElement) {
// Is the element completely in the view?
const parentRect = Utils.findParentWithScrolling(element).getBoundingClientRect()
const elementRect = element.getBoundingClientRect()
@ -1680,7 +1680,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
})
}
private static findParentWithScrolling(element: HTMLBaseElement): HTMLBaseElement {
private static findParentWithScrolling(
element: HTMLBaseElement | HTMLDivElement
): HTMLBaseElement | HTMLDivElement {
// Check if the element itself has scrolling
if (element.scrollHeight > element.clientHeight) {
return element

File diff suppressed because one or more lines are too long

View file

@ -121,16 +121,6 @@ input[type=text] {
width: 100%;
}
.debug input, .debug textarea {
border: 6px solid red
}
.debug label input, .debug label textarea {
border: 1px solid grey;
}
/************************* BIG CATEGORIES ********************************/
/**

View file

@ -14,7 +14,7 @@
<body>
<div id="main">Loading statistics...</div>
<script src="./src/UI/StatisticsGUI.ts" type="module"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-nx5O+otcqJoqMhdDt8jUzmia6ng81Z5zZozYr69TzPkOLjVhLKMxu5zHCV9/0MPn"></script>
</body>
</html>

View file

@ -14,7 +14,7 @@
<body>
<div id="main" class="h-full">Initing studio...</div>
<script src="./src/UI/StudioGui.ts" type="module"></script>
<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 async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-nx5O+otcqJoqMhdDt8jUzmia6ng81Z5zZozYr69TzPkOLjVhLKMxu5zHCV9/0MPn"></script>
</body>
</html>

View file

@ -1,15 +1,17 @@
import { exec } from "child_process"
import { describe, it } from "vitest"
import { describe, expect, it, test } from "vitest"
import { webcrypto } from "node:crypto"
import { parse as parse_html } from "node-html-parser"
import { readFileSync } from "fs"
import ScriptUtils from "../scripts/ScriptUtils"
function detectInCode(forbidden: string, reason: string) {
return wrap(detectInCodeUnwrapped(forbidden, reason))
}
/**
*
* @param forbidden: a GREP-regex. This means that '.' is a wildcard and should be escaped to match a literal dot
* @param forbidden a GREP-regex. This means that '.' is a wildcard and should be escaped to match a literal dot
* @param reason
* @private
*/
@ -64,13 +66,23 @@ function wrap(promise: Promise<void>): (done: () => void) => void {
}
}
function validateScriptIntegrityOf(path: string) {
function _arrayBufferToBase64(buffer) {
var binary = ""
var bytes = new Uint8Array(buffer)
var len = bytes.byteLength
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
}
async function validateScriptIntegrityOf(path: string): Promise<void> {
const htmlContents = readFileSync(path, "utf8")
const doc = parse_html(htmlContents)
// @ts-ignore
const scripts = Array.from(doc.getElementsByTagName("script"))
for (const script of scripts) {
const src = script.getAttribute("src")
let src = script.getAttribute("src")
if (src === undefined) {
continue
}
@ -87,6 +99,18 @@ function validateScriptIntegrityOf(path: string) {
if (crossorigin !== "anonymous") {
throw new Error(ctx + " has crossorigin missing or not set to 'anonymous'")
}
if (src.startsWith("//")) {
src = "https:" + src
}
// Using 'scriptUtils' actually fetches data from the internet, it is not prohibited by the testHooks
const data: string = (await ScriptUtils.Download(src))["content"]
const hashed = await webcrypto.subtle.digest("SHA-384", new TextEncoder().encode(data))
const hashedStr = _arrayBufferToBase64(hashed)
console.log(src, hashedStr, integrity)
expect(integrity).to.equal(
"sha384-" + hashedStr,
"Loading a script from '" + src + "' in the file " + path + " has a mismatched checksum"
)
}
}
@ -112,10 +136,10 @@ describe("Code quality", () => {
)
)
it("scripts with external sources should have an integrity hash", () => {
test("scripts with external sources should have an integrity hash", async () => {
const htmlFiles = ScriptUtils.readDirRecSync(".", 1).filter((f) => f.endsWith(".html"))
for (const htmlFile of htmlFiles) {
validateScriptIntegrityOf(htmlFile)
await validateScriptIntegrityOf(htmlFile)
}
})
/*

View file

@ -76,7 +76,7 @@
<script src="./src/UI/RemoveOtherLanguages.js"></script>
<script async src="./src/InstallServiceWorker.ts" type="module"></script>
<script defer src="./src/index.ts" type="module"></script>
<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 async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-nx5O+otcqJoqMhdDt8jUzmia6ng81Z5zZozYr69TzPkOLjVhLKMxu5zHCV9/0MPn"></script>

View file

@ -5,6 +5,8 @@ export default defineConfig({
plugins: [svelte({ hot: !process.env.VITEST, preprocess: [autoPreprocess()] })],
test: {
globals: true,
maxThreads: 16,
minThreads: 1,
setupFiles: ["./test/testhooks.ts"],
include: ["./test/*.spec.ts", "./test/**/*.spec.ts", "./*.doctest.ts", "./**/*.doctest.ts"],
},