diff --git a/.forgejo/workflows/deploy_hosted.yml b/.forgejo/workflows/deploy_hosted.yml index 77698c8a7..e11635f0f 100644 --- a/.forgejo/workflows/deploy_hosted.yml +++ b/.forgejo/workflows/deploy_hosted.yml @@ -16,7 +16,7 @@ jobs: node-version: "20" cache: "npm" cache-dependency-path: package-lock.json - + - name: install deps run: npm ci shell: bash @@ -43,8 +43,7 @@ jobs: export NODE_OPTIONS="--max-old-space-size=8192" npm run clean:tests npm run generate:doctests 2>&1 | grep -v "No doctests found in" - vitest --run test - npm run clean:tests + vitest --run test && npm run clean:tests shell: bash @@ -53,11 +52,16 @@ jobs: - name: Zipping dist file run: | - mv dist ${{ github.ref_name }} + mv dist /tmp/${{ github.ref_name }} + cd /tmp zip ${{ github.ref_name }}.zip -r ${{ github.ref_name }}/* + cd - - name: uploading file - run: scp ${{ github.ref_name }}.zip hetzner:/root/staging/ + run: scp /tmp/${{ github.ref_name }}.zip hetzner:/root/staging/ + + - name: cleanup files + run: rm /tmp/${{ github.ref_name }}.zip && rm -rf /tmp/${{ github.ref_name }}/ - name: unzipping remote file run: ssh hetzner "cd /root/staging && rm -rf ${{ github.ref_name }} && unzip ${{ github.ref_name }}.zip && rm -rf /root/public/${{ github.ref_name }} && mv /root/staging/${{ github.ref_name }}/ /root/public/ && rm ${{ github.ref_name }}.zip" diff --git a/.gitignore b/.gitignore index 6c69ad958..14f62e468 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ dist-full/ public/assets/icons/*.webp uploaded_images.json /app/dist/ +src/assets/bing.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 3588ba422..235b0a712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ All notable changes to this project will be documented in this file. See [standa * **onwheels:** move building magic back to onwheels theme, decrease minzoom for entrances ([98e8adf](https://github.com/pietervdvn/mapcomplete/commits/98e8adf41b3e296dcdf26ac9c6df21c1f5259f46)) * **wayside_shrine,artwork:** add artwork as adjacent concept to shrines ([266e91e](https://github.com/pietervdvn/mapcomplete/commits/266e91e45db85d27d5271267a1db2175e2588c1d)) -* **wayside_shrine:** add preset to snap to a wal ([e8951e2](https://github.com/pietervdvn/mapcomplete/commits/e8951e2b80ff2e4a9818452bc0c0f03f058e7cf2)) +* **wayside_shrine:** add preset to snap to a + wall ([e8951e2](https://github.com/pietervdvn/mapcomplete/commits/e8951e2b80ff2e4a9818452bc0c0f03f058e7cf2)) * **wayside_shrines:** integrate wayside shrines ([1f7b156](https://github.com/pietervdvn/mapcomplete/commits/1f7b156a2c18200ece3c3ec9aa3d86f814d2590b)) * **wayside_shrines:** small language fixes ([aaf46cd](https://github.com/pietervdvn/mapcomplete/commits/aaf46cd7b01a9f11720d5e47ac6ffcde15b29add)) diff --git a/assets/layers/entrance/entrance.json b/assets/layers/entrance/entrance.json index 50148fc9c..e044044ff 100644 --- a/assets/layers/entrance/entrance.json +++ b/assets/layers/entrance/entrance.json @@ -33,17 +33,17 @@ "minzoom": 14, "title": { "render": { - "en": "Entrance", - "nl": "Ingang", - "ca": "Entrada", - "da": "Indgang", - "de": "Eingang", - "es": "Entrada", - "fr": "Entrée", - "pa_PK": "دروازہ", - "pl": "Wejście", - "cs": "Vchod", - "cy": "Mynedfa" + "en": "Entrance {ref}", + "nl": "Ingang {ref}", + "ca": "Entrada {ref}", + "da": "Indgang {ref}", + "de": "Eingang {ref}", + "es": "Entrada {ref}", + "fr": "Entrée {ref}", + "pa_PK": "دروازہ{ref}", + "pl": "Wejście {ref}", + "cs": "Vchod {ref}", + "cy": "Mynedfa {ref}" } }, "pointRendering": [ @@ -68,7 +68,16 @@ ] } } - ] + ], + "label": { + "mappings": [ + { + "if": "ref~*", + "then": "{ref}" + } + ] + }, + "labelCssClasses": "rounded bg-white px-2" } ], "lineRendering": [], @@ -578,6 +587,29 @@ } } ] + }, + { + "id": "ref", + "question": { + "en": "Does this door have a reference number?", + "nl": "Heeft deze toegang een referentienummer?" + }, + "render": { + "en": "This door has {ref} as reference number", + "nl": "Deze deur heeft {ref} als referentienummer" + }, + "mappings": [ + { + "if": "ref=", + "then": { + "en": "No reference number", + "nl": "Geen referentienummer" + } + } + ], + "freeform": { + "key": "ref" + } } ], "filter": [ diff --git a/assets/layers/ice_cream/ice_cream.svg b/assets/layers/ice_cream/ice_cream.svg index f40ca6c48..5cd349812 100644 --- a/assets/layers/ice_cream/ice_cream.svg +++ b/assets/layers/ice_cream/ice_cream.svg @@ -1,40 +1,61 @@ - - - - - - - - image/svg+xml - - - - - - - - - + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/assets/layers/item_with_image/camera.svg b/assets/layers/item_with_image/camera.svg index a29853276..5dae59d81 100644 --- a/assets/layers/item_with_image/camera.svg +++ b/assets/layers/item_with_image/camera.svg @@ -1,64 +1,316 @@ - - Adwaita Icon Template - - - - + + + Adwaita Icon Template + + + + - - - - - - - + + + + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - + + - - + + + + Adwaita Icon Template + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/assets/layers/toilet/toilet.json b/assets/layers/toilet/toilet.json index aeced6e97..ff4ff196b 100644 --- a/assets/layers/toilet/toilet.json +++ b/assets/layers/toilet/toilet.json @@ -954,48 +954,48 @@ "id": "toilet-changing_table:location" }, { - "labels": [ - "relevant-questions" - ], - "id": "toilet-handwashing", + "id": "toilet-supervised", "question": { - "en": "Do these toilets have a sink to wash your hands?", - "nl": "Hebben deze toiletten een lavabo om de handen te wassen?", - "de": "Gibt es hier ein Handwaschbecken?", - "es": "¿Estos baños tienen un lavabo para lavarse las manos?", - "fr": "Ces toilettes ont-ils un lavabo pour se laver les mains ?", - "da": "Har disse toiletter en håndvask til at vaske hænder?", - "ca": "Aquests lavabos tenen una pica per a rentar-se les mans?", - "cs": "Mají tyto toalety umyvadlo na mytí rukou?" + "en": "Is this toilets supervised by a person?", + "nl": "Is er toezicht op deze toilet?" + }, + "questionHint": { + "en": "This is typically a person tasked with keeping the toilets clean and collecting the fee", + "nl": "Deze persoon houdt typisch de toiletten proper en int het toiletgeld" }, "mappings": [ { - "if": "toilets:handwashing=yes", + "if": "supervised=yes", "then": { - "en": "These toilets have a sink to wash your hands", - "nl": "Deze toiletten hebben een lavabo waar men de handen kan wassen", - "de": "Die Toilette hat ein Handwaschbecken", - "es": "Estos baños tienen un lavabo para lavarse las manos", - "fr": "Ces toilettes ont un lavabo pour se laver les mains", - "da": "Dette toilet har en vask til at vaske dine hænder", - "ca": "Aquests lavabos tenen una pica per a rentar-se les mans", - "cs": "Tyto toalety mají umyvadlo na mytí rukou" + "en": "There is a person supervising these toilets during (most of) the opening hours", + "nl": "Er is een persoon die toezicht houdt op deze toiletten" } }, { - "if": "toilets:handwashing=no", + "if": "supervised=interval", "then": { - "en": "These toilets don't have a sink to wash your hands", - "nl": "Deze toiletten hebben geen lavabo waar men de handen kan wassen", - "de": "Die Toilette hat kein Handwaschbecken", - "es": "Estos baños no tienen un lavabo para lavarse las manos", - "fr": "Ces toilettes n'ont pas de lavabo pour se laver les mains", - "da": "Disse toiletter har ikke en vask til at vaske dine hænder", - "ca": "Aquests lavabos no tenen una pica per a rentar-se les mans", - "cs": "Tyto toalety nemají umyvadlo na mytí rukou" + "en": "There is a person supervising these toilets, but they are present only during certain times of the opening hours", + "nl": "Er is een persoon die toezicht houdt op deze toiletten, maar deze is slechts op enkele vaste momenten aanwezig" + } + }, + { + "if": "supervised=no", + "then": { + "en": "These toilets are not supervised", + "nl": "Er is geen persoon die toezicht houdt" } } - ] + ], + "condition": { + "and": [ + { + "or": [ + "access=yes", + "access=" + ] + } + ] + } }, { "id": "toilet-has-paper", @@ -1047,6 +1047,96 @@ ] } }, + { + "labels": [ + "relevant-questions" + ], + "id": "toilet-handwashing", + "question": { + "en": "Do these toilets have a sink to wash your hands?", + "nl": "Hebben deze toiletten een lavabo om de handen te wassen?", + "de": "Gibt es hier ein Handwaschbecken?", + "es": "¿Estos baños tienen un lavabo para lavarse las manos?", + "fr": "Ces toilettes ont-ils un lavabo pour se laver les mains ?", + "da": "Har disse toiletter en håndvask til at vaske hænder?", + "ca": "Aquests lavabos tenen una pica per a rentar-se les mans?", + "cs": "Mají tyto toalety umyvadlo na mytí rukou?" + }, + "mappings": [ + { + "if": "toilets:handwashing=yes", + "then": { + "en": "These toilets have a sink to wash your hands", + "nl": "Deze toiletten hebben een lavabo waar men de handen kan wassen", + "de": "Die Toilette hat ein Handwaschbecken", + "es": "Estos baños tienen un lavabo para lavarse las manos", + "fr": "Ces toilettes ont un lavabo pour se laver les mains", + "da": "Dette toilet har en vask til at vaske dine hænder", + "ca": "Aquests lavabos tenen una pica per a rentar-se les mans", + "cs": "Tyto toalety mají umyvadlo na mytí rukou" + } + }, + { + "if": "toilets:handwashing=no", + "then": { + "en": "These toilets don't have a sink to wash your hands", + "nl": "Deze toiletten hebben geen lavabo waar men de handen kan wassen", + "de": "Die Toilette hat kein Handwaschbecken", + "es": "Estos baños no tienen un lavabo para lavarse las manos", + "fr": "Ces toilettes n'ont pas de lavabo pour se laver les mains", + "da": "Disse toiletter har ikke en vask til at vaske dine hænder", + "ca": "Aquests lavabos no tenen una pica per a rentar-se les mans", + "cs": "Tyto toalety nemají umyvadlo na mytí rukou" + } + } + ] + }, + { + "id": "toilet-drying", + "question": { + "en": "Do these toilets have a device to dry your hands?", + "nl": "Hebben deze toiletten een apparaat om je handen te drogen?" + }, + "multiAnswer": true, + "mappings": [ + { + "if": "toilets:hands_drying=electric_hand_dryer", + "then": { + "en": "Electric hand dryers are available for drying hands.", + "nl": "Elektrische handdrogers zijn beschikbaar om handen te drogen." + } + }, + { + "if": "toilets:hands_drying=paper_towel", + "then": { + "en": "Paper towels are available for drying hands.", + "nl": "Papieren wegwerphanddoeken zijn beschikbaar om handen te drogen." + } + }, + { + "if": "toilets:hands_drying=towel_cabinet", + "then": { + "en": "A towel roll cabinet is available for drying hands", + "nl": "Een apparaat met een handdoekrol is beschikbaar om handen te drogen" + } + }, + { + "if": "toilets:hands_drying=towel", + "then": { + "en": "A fabric towel available to dry your hands.", + "nl": "Een stoffen handdoek is beschikbaar om handen te drogen." + } + }, + { + "if": "toilets:hands_drying=no", + "then": { + "en": "There are no hand drying facilities available.", + "nl": "Er zijn geen handdroogvoorzieningen beschikbaar." + } + } + ], + "condition": "toilets:handwashing=yes" + }, "description" ], "filter": [ diff --git a/assets/layers/wayside_shrine/wayside_shrine.json b/assets/layers/wayside_shrine/wayside_shrine.json index ebe563e70..4bbbcc29f 100644 --- a/assets/layers/wayside_shrine/wayside_shrine.json +++ b/assets/layers/wayside_shrine/wayside_shrine.json @@ -379,7 +379,8 @@ "religion!=" ] } - } + }, + "level" ], "deletion": { "softDeletionTags": { diff --git a/assets/themes/advertising/wall_painting.svg b/assets/themes/advertising/wall_painting.svg index fbf462c64..b1b1908a4 100644 --- a/assets/themes/advertising/wall_painting.svg +++ b/assets/themes/advertising/wall_painting.svg @@ -2,12 +2,12 @@ + inkscape:current-layer="layer1" + inkscape:showpageshadow="2" + inkscape:deskcolor="#d1d1d1"> + id="guide1027" + inkscape:locked="false" /> + id="guide1029" + inkscape:locked="false" /> + id="guide1031" + inkscape:locked="false" /> + id="guide1033" + inkscape:locked="false" /> + id="guide1035" + inkscape:locked="false" /> + id="guide1037" + inkscape:locked="false" /> + id="guide1039" + inkscape:locked="false" /> + id="guide1041" + inkscape:locked="false" /> @@ -86,26 +96,25 @@ + style="display:inline"> + rx="12.591385" /> + mask="url(#mask1309)" + transform="matrix(12.591377,0,0,12.591377,0.30278402,0.86556738)"> diff --git a/assets/themes/grb/grb.json b/assets/themes/grb/grb.json index 58409d303..5508a22ed 100644 --- a/assets/themes/grb/grb.json +++ b/assets/themes/grb/grb.json @@ -19,6 +19,50 @@ "shortDescription": { "nl": "Grb import helper tool" }, + "popup": [ + { + "id": "wikilink-needed", + "condition": "_description!~.*https://wiki.openstreetmap.org/wiki/WikiProject_Belgium/Building_and_address_import.*", + "dismissable": false, + "title": { + "render": { + "en": "Profile mention obligated", + "nl": "Link op profiel verplicht" + } + }, + "body": [ + { + "render": { + "special": { + "type": "link", + "href": "https://www.openstreetmap.org/profile/edit", + "text": { + "en": "Edit your user profile", + "nl": "Pas je profiel aan" + } + }, + "after": { + "en": "to include the link https://wiki.openstreetmap.org/wiki/WikiProject_Belgium/Building_and_address_import", + "nl": " en voeg deze link toe: https://wiki.openstreetmap.org/wiki/WikiProject_Belgium/Building_and_address_import" + } + } + }, + { + "id": "reload_profile", + "render": { + "special": { + "type": "login_button", + "force": "yes", + "message": { + "en": "Reload your profile", + "nl": "Herlaad je profiel" + } + } + } + } + ] + } + ], "icon": "./assets/themes/grb/logo.svg", "startZoom": 9, "startLat": 51.0249, diff --git a/assets/themes/wayside_shrines/wayside_shrines.json b/assets/themes/wayside_shrines/wayside_shrines.json index 4392d787d..6894fd1ed 100644 --- a/assets/themes/wayside_shrines/wayside_shrines.json +++ b/assets/themes/wayside_shrines/wayside_shrines.json @@ -14,6 +14,7 @@ "artwork" ], "override": { + "isCounted": false, "minzoom": 18 } } diff --git a/langs/layers/ca.json b/langs/layers/ca.json index 64a94bc08..3fdf28f25 100644 --- a/langs/layers/ca.json +++ b/langs/layers/ca.json @@ -5245,7 +5245,7 @@ } }, "title": { - "render": "Entrada" + "render": "Entrada {ref}" } }, "etymology": { diff --git a/langs/layers/cs.json b/langs/layers/cs.json index 4d19e10c0..5ad1e02b9 100644 --- a/langs/layers/cs.json +++ b/langs/layers/cs.json @@ -5049,7 +5049,7 @@ } }, "title": { - "render": "Vchod" + "render": "Vchod {ref}" } }, "etymology": { diff --git a/langs/layers/cy.json b/langs/layers/cy.json index aed6d66f8..5aaca51be 100644 --- a/langs/layers/cy.json +++ b/langs/layers/cy.json @@ -356,7 +356,7 @@ "entrance": { "name": "Mynedfa", "title": { - "render": "Mynedfa" + "render": "Mynedfa {ref}" } }, "extinguisher": { diff --git a/langs/layers/da.json b/langs/layers/da.json index c784113f9..3418120da 100644 --- a/langs/layers/da.json +++ b/langs/layers/da.json @@ -1564,7 +1564,7 @@ } }, "title": { - "render": "Indgang" + "render": "Indgang {ref}" } }, "etymology": { diff --git a/langs/layers/de.json b/langs/layers/de.json index b122b8aed..497d741b4 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -5204,7 +5204,7 @@ } }, "title": { - "render": "Eingang" + "render": "Eingang {ref}" } }, "etymology": { diff --git a/langs/layers/en.json b/langs/layers/en.json index f68d3443f..e1356aa1f 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -5321,13 +5321,22 @@ "question": "What is the height of this kerb?", "render": "The kerb height of this door is {kerb:height}" }, + "ref": { + "mappings": { + "0": { + "then": "No reference number" + } + }, + "question": "Does this door have a reference number?", + "render": "This door has {ref} as reference number" + }, "width": { "question": "What is the width of this door/entrance?", "render": "This door has a width of {canonical(width)}" } }, "title": { - "render": "Entrance" + "render": "Entrance {ref}" } }, "etymology": { @@ -12133,6 +12142,26 @@ "question": "How much does one have to pay for these toilets?", "render": "The fee is {charge}" }, + "toilet-drying": { + "mappings": { + "0": { + "then": "Electric hand dryers are available for drying hands." + }, + "1": { + "then": "Paper towels are available for drying hands." + }, + "2": { + "then": "A towel roll cabinet is available for drying hands" + }, + "3": { + "then": "A fabric towel available to dry your hands." + }, + "4": { + "then": "There are no hand drying facilities available." + } + }, + "question": "Do these toilets have a device to dry your hands?" + }, "toilet-handwashing": { "mappings": { "0": { @@ -12162,6 +12191,21 @@ } } }, + "toilet-supervised": { + "mappings": { + "0": { + "then": "There is a person supervising these toilets during (most of) the opening hours" + }, + "1": { + "then": "There is a person supervising these toilets, but they are present only during certain times of the opening hours" + }, + "2": { + "then": "These toilets are not supervised" + } + }, + "question": "Is this toilets supervised by a person?", + "questionHint": "This is typically a person tasked with keeping the toilets clean and collecting the fee" + }, "toilets-changing-table": { "mappings": { "0": { diff --git a/langs/layers/es.json b/langs/layers/es.json index 6ece3aa66..59387bb15 100644 --- a/langs/layers/es.json +++ b/langs/layers/es.json @@ -4903,7 +4903,7 @@ } }, "title": { - "render": "Entrada" + "render": "Entrada {ref}" } }, "etymology": { diff --git a/langs/layers/fr.json b/langs/layers/fr.json index fa7aace69..5b2cfcac2 100644 --- a/langs/layers/fr.json +++ b/langs/layers/fr.json @@ -3467,7 +3467,7 @@ } }, "title": { - "render": "Entrée" + "render": "Entrée {ref}" } }, "etymology": { diff --git a/langs/layers/nl.json b/langs/layers/nl.json index adb678192..a58a6d128 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -4990,13 +4990,22 @@ "question": "Hoe hoog is de drempel?", "render": "De drempel bij deze deur is {kerb:height}" }, + "ref": { + "mappings": { + "0": { + "then": "Geen referentienummer" + } + }, + "question": "Heeft deze toegang een referentienummer?", + "render": "Deze deur heeft {ref} als referentienummer" + }, "width": { "question": "Wat is de breedte van deze deur/toegang?", "render": "Deze deur heeft een breedte van {canonical(width)}" } }, "title": { - "render": "Ingang" + "render": "Ingang {ref}" } }, "etymology": { @@ -9757,6 +9766,26 @@ "question": "Hoeveel moet men betalen om deze toiletten te gebruiken?", "render": "De toiletten gebruiken kost {charge}" }, + "toilet-drying": { + "mappings": { + "0": { + "then": "Elektrische handdrogers zijn beschikbaar om handen te drogen." + }, + "1": { + "then": "Papieren wegwerphanddoeken zijn beschikbaar om handen te drogen." + }, + "2": { + "then": "Een apparaat met een handdoekrol is beschikbaar om handen te drogen" + }, + "3": { + "then": "Een stoffen handdoek is beschikbaar om handen te drogen." + }, + "4": { + "then": "Er zijn geen handdroogvoorzieningen beschikbaar." + } + }, + "question": "Hebben deze toiletten een apparaat om je handen te drogen?" + }, "toilet-handwashing": { "mappings": { "0": { @@ -9786,6 +9815,21 @@ } } }, + "toilet-supervised": { + "mappings": { + "0": { + "then": "Er is een persoon die toezicht houdt op deze toiletten" + }, + "1": { + "then": "Er is een persoon die toezicht houdt op deze toiletten, maar deze is slechts op enkele vaste momenten aanwezig" + }, + "2": { + "then": "Er is geen persoon die toezicht houdt" + } + }, + "question": "Is er toezicht op deze toilet?", + "questionHint": "Deze persoon houdt typisch de toiletten proper en int het toiletgeld" + }, "toilets-changing-table": { "mappings": { "0": { diff --git a/langs/layers/pa_PK.json b/langs/layers/pa_PK.json index 5e7360bba..a870525b6 100644 --- a/langs/layers/pa_PK.json +++ b/langs/layers/pa_PK.json @@ -151,7 +151,7 @@ "entrance": { "name": "دروازہ", "title": { - "render": "دروازہ" + "render": "دروازہ{ref}" } }, "food": { diff --git a/langs/layers/pl.json b/langs/layers/pl.json index c5d216106..fc45953e5 100644 --- a/langs/layers/pl.json +++ b/langs/layers/pl.json @@ -1881,7 +1881,7 @@ } }, "title": { - "render": "Wejście" + "render": "Wejście {ref}" } }, "etymology": { diff --git a/langs/themes/en.json b/langs/themes/en.json index 8a91f3192..8744e0448 100644 --- a/langs/themes/en.json +++ b/langs/themes/en.json @@ -623,6 +623,30 @@ } } } + }, + "popup": { + "0": { + "body": { + "0": { + "render": { + "special": { + "after": "to include the link https://wiki.openstreetmap.org/wiki/WikiProject_Belgium/Building_and_address_import", + "text": "Edit your user profile" + } + } + }, + "1": { + "render": { + "special": { + "msg": "Reload your profile" + } + } + } + }, + "title": { + "render": "Profile mention obligated" + } + } } }, "guideposts": { diff --git a/langs/themes/nl.json b/langs/themes/nl.json index 49c9e8b22..56088c853 100644 --- a/langs/themes/nl.json +++ b/langs/themes/nl.json @@ -669,6 +669,30 @@ } } }, + "popup": { + "0": { + "body": { + "0": { + "render": { + "special": { + "after": " en voeg deze link toe: https://wiki.openstreetmap.org/wiki/WikiProject_Belgium/Building_and_address_import", + "text": "Pas je profiel aan" + } + } + }, + "1": { + "render": { + "special": { + "msg": "Herlaad je profiel" + } + } + } + }, + "title": { + "render": "Link op profiel verplicht" + } + } + }, "shortDescription": "Grb import helper tool", "title": "GRB import helper" }, diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index d3f4dd4f9..cf4673643 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -9,16 +9,12 @@ import { DoesImageExist, PrevalidateTheme, ValidateLayer, - ValidateThemeEnsemble, + ValidateThemeEnsemble } from "../src/Models/ThemeConfig/Conversion/Validation" import { Translation } from "../src/UI/i18n/Translation" import { PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer" import { PrepareTheme } from "../src/Models/ThemeConfig/Conversion/PrepareTheme" -import { - Conversion, - DesugaringContext, - DesugaringStep, -} from "../src/Models/ThemeConfig/Conversion/Conversion" +import { Conversion, DesugaringContext, DesugaringStep } from "../src/Models/ThemeConfig/Conversion/Conversion" import { Utils } from "../src/Utils" import Script from "./Script" import { AllSharedLayers } from "../src/Customizations/AllSharedLayers" @@ -501,7 +497,7 @@ class LayerOverviewUtils extends Script { priviliged.delete(key) }) - // These two get a free pass + // These get a free pass priviliged.delete("summary") priviliged.delete("last_click") priviliged.delete("search") diff --git a/src/Logic/Osm/OsmConnection.ts b/src/Logic/Osm/OsmConnection.ts index 0230aa4ee..c8dea75dd 100644 --- a/src/Logic/Osm/OsmConnection.ts +++ b/src/Logic/Osm/OsmConnection.ts @@ -65,6 +65,16 @@ interface OsmUserInfo { } } +interface UserBlock { + "id": number, + "created_at": string, + "updated_at": string, + "ends_at": string, + "needs_view": boolean, + "user": { "uid": number, "user": string }, + "creator": { "uid": number, "user": string } +} + export default interface UserDetails { name: string uid: number @@ -77,7 +87,8 @@ export default interface UserDetails { account_created: string tracesCount: number description?: string - languages: string[] + languages: string[], + active_blocks: number } export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable" @@ -309,6 +320,7 @@ export class OsmConnection { account_created: user.account_created, tracesCount: user.traces?.count ?? 0, unreadMessages: user.messages.received?.unread ?? 0, + active_blocks: user.blocks.received.active ?? 0 } this.userDetails.set(userdetails) this.loadingStatus.setData("logged-in") @@ -559,6 +571,14 @@ export class OsmConnection { }) } + /** + * Have you been banned by the DWG? + */ + public async getUserBlocks(): Promise { + const raw = await this.interact("/user/blocks/active.json") + return JSON.parse(raw)["user_blocks"] + } + /** * To be called by land.html */ diff --git a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts index 4ad260ce1..3e28b85df 100644 --- a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts +++ b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts @@ -1,16 +1,6 @@ -import { - Concat, - Conversion, - DesugaringContext, - DesugaringStep, - Each, - Fuse, - On, - Pass, - SetDefault, -} from "./Conversion" +import { Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault } from "./Conversion" import { ThemeConfigJson } from "../Json/ThemeConfigJson" -import { PrepareLayer } from "./PrepareLayer" +import { PrepareLayer, RewriteSpecial } from "./PrepareLayer" import { LayerConfigJson } from "../Json/LayerConfigJson" import { Utils } from "../../../Utils" import Constants from "../../Constants" @@ -40,7 +30,7 @@ class SubstituteLayer extends Conversion [ lname, - Utils.levenshteinDistance(name, lname), + Utils.levenshteinDistance(name, lname) ]) withDistance.sort((a, b) => a[1] - b[1]) const ids = withDistance.map((n) => n[0]) @@ -130,9 +120,9 @@ class SubstituteLayer extends Conversion 0) { context.err( "This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " + - unused.join(", ") + - "\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore" + unused.join(", ") + + "\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore" ) } found.tagRenderings = filtered @@ -205,10 +195,10 @@ export class AddDefaultLayers extends DesugaringStep { if (alreadyLoaded.has(v.id)) { context.warn( "Layout " + - context + - " already has a layer with name " + - v.id + - "; skipping inclusion of this builtin layer" + context + + " already has a layer with name " + + v.id + + "; skipping inclusion of this builtin layer" ) continue } @@ -352,10 +342,10 @@ class AddDependencyLayersToTheme extends DesugaringStep { .enters("layer dependency") .err( "Layer " + - dependency.neededLayer + - " is loaded because " + - dependency.reason + - "; so it must specify a `snapName`. This is used in the sentence `move this point to snap it to {snapName}`" + dependency.neededLayer + + " is loaded because " + + dependency.reason + + "; so it must specify a `snapName`. This is used in the sentence `move this point to snap it to {snapName}`" ) } } @@ -380,12 +370,12 @@ class AddDependencyLayersToTheme extends DesugaringStep { if (dep === undefined) { const message = [ "Loading a dependency failed: layer " + - unmetDependency.neededLayer + - " is not found, neither as layer of " + - themeId + - " nor as builtin layer.", + unmetDependency.neededLayer + + " is not found, neither as layer of " + + themeId + + " nor as builtin layer.", reason, - "Loaded layers are: " + alreadyLoaded.map((l) => l.id).join(","), + "Loaded layers are: " + alreadyLoaded.map((l) => l.id).join(",") ] throw message.join("\n\t") } @@ -395,7 +385,7 @@ class AddDependencyLayersToTheme extends DesugaringStep { dep.description = reason dependenciesToAdd.unshift({ config: dep, - reason, + reason }) loadedLayerIds.add(dep.id) unmetDependencies = unmetDependencies.filter( @@ -440,7 +430,7 @@ class AddDependencyLayersToTheme extends DesugaringStep { return { ...theme, - layers: layers, + layers: layers } } } @@ -510,10 +500,10 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep context.warn( "The theme " + - json.id + - " has an inline layer: " + - layer["id"] + - ". This is discouraged." + json.id + + " has an inline layer: " + + layer["id"] + + ". This is discouraged." ) } return json @@ -555,12 +545,12 @@ class PostvalidateTheme extends DesugaringStep { if (minZoomAll < layer.minzoom) { context.err( "There are multiple layers based on " + - basedOn + - ". The layer with id " + - layer.id + - " has a minzoom of " + - layer.minzoom + - ", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer." + basedOn + + ". The layer with id " + + layer.id + + " has a minzoom of " + + layer.minzoom + + ", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer." ) } } @@ -586,11 +576,11 @@ class PostvalidateTheme extends DesugaringStep { .enters("layers", config.id, "filter", "sameAs") .err( "The layer " + - config.id + - " follows the filter state of layer " + - sameAs + - ", but no layer with this name was found.\n\tDid you perhaps mean one of: " + - closeLayers.slice(0, 3).join(", ") + config.id + + " follows the filter state of layer " + + sameAs + + ", but no layer with this name was found.\n\tDid you perhaps mean one of: " + + closeLayers.slice(0, 3).join(", ") ) } } @@ -618,6 +608,13 @@ export class PrepareTheme extends Fuse { new SetDefault("socialImage", "assets/SocialImage.png", true), // We expand all tagrenderings first... new On("layers", new Each(new PrepareLayer(state))), + new On("popup", new Each( + new Fuse("Prepare popups", + new On("body", new Each(new RewriteSpecial())), + new On("title", new RewriteSpecial()) + ) + )), + // Then we apply the override all. We must first expand everything in case that we override something in an expanded tag // Note that it'll cheat with tagRenderings+ new ApplyOverrideAll(), diff --git a/src/Models/ThemeConfig/Json/ThemeConfigJson.ts b/src/Models/ThemeConfig/Json/ThemeConfigJson.ts index cbdeef07b..654a6b746 100644 --- a/src/Models/ThemeConfig/Json/ThemeConfigJson.ts +++ b/src/Models/ThemeConfig/Json/ThemeConfigJson.ts @@ -3,6 +3,8 @@ import ExtraLinkConfigJson from "./ExtraLinkConfigJson" import { RasterLayerProperties } from "../../RasterLayerProperties" import { Translatable } from "./Translatable" +import { TagConfigJson } from "./TagConfigJson" +import { TagRenderingConfigJson } from "./TagRenderingConfigJson" /** * Defines the entire theme. @@ -468,4 +470,24 @@ export interface ThemeConfigJson { * group: hidden */ _usedImages?: string[] + + /** + * If set, an _additional_ popup will be shown under the theme introduction page. + * + * The embedded tagRenderingConfigs will be run against the settings-state of the contributor. + * If multiple popups are set, the first popup of the list will be rendered on top (and thus be seen first). + */ + popup?: { + /** + * ifset: the user can dismiss this message + */ + dismissible?: boolean + condition?: TagConfigJson + title: TagRenderingConfigJson, + body: TagRenderingConfigJson[], + /** + * id of the popup, mostly to keep the translations in check + */ + id: string, + }[] } diff --git a/src/Models/ThemeConfig/TagRenderingConfig.ts b/src/Models/ThemeConfig/TagRenderingConfig.ts index 3d083299f..313cd1bd0 100644 --- a/src/Models/ThemeConfig/TagRenderingConfig.ts +++ b/src/Models/ThemeConfig/TagRenderingConfig.ts @@ -701,12 +701,12 @@ export default class TagRenderingConfig { * * const config = new TagRenderingConfig({"id":"capacity", "render": "Fits {capcity} books",freeform: {"key":"capacity",type:"pnat"} }) * config.constructChangeSpecification("", undefined, undefined, {}) // => undefined - * config.constructChangeSpecification("5", undefined, undefined, {}).optimize() // => [new Tag("capacity", "5")] + * config.constructChangeSpecification("5", undefined, undefined, {}) // => [new Tag("capacity", "5")] * * // Should pick a mapping, even if freeform is usedconstructChange * const config = new TagRenderingConfig({"id": "shop-types", render: "Shop type is {shop}", freeform: {key: "shop", addExtraTags:["fixme=freeform shop type used"]}, mappings:[{if: "shop=second_hand", then: "Second hand shop"}]}) - * config.constructChangeSpecification("freeform", 1, undefined, {}).asHumanString(false, false, {}) // => [new Tag("shop","freeform",new Tag("fixme","freeform shop type used")] - * config.constructChangeSpecification("freeform", undefined, undefined, {}) // => [new Tag("shop","freeform), new Tag("fixme","freeform shop type used")] + * config.constructChangeSpecification("freeform", 1, undefined, {}) // => [new Tag("shop","freeform"),new Tag("fixme","freeform shop type used")] + * config.constructChangeSpecification("freeform", undefined, undefined, {}) // => [new Tag("shop","freeform"), new Tag("fixme","freeform shop type used")] * config.constructChangeSpecification("second_hand", 1, undefined, {}) // => [new Tag("shop","second_hand")] * * diff --git a/src/Models/ThemeConfig/ThemeConfig.ts b/src/Models/ThemeConfig/ThemeConfig.ts index de1851fc6..ad677a380 100644 --- a/src/Models/ThemeConfig/ThemeConfig.ts +++ b/src/Models/ThemeConfig/ThemeConfig.ts @@ -9,6 +9,10 @@ import LanguageUtils from "../../Utils/LanguageUtils" import { RasterLayerProperties } from "../RasterLayerProperties" import { Translatable } from "./Json/Translatable" +import { TagsFilter } from "../../Logic/Tags/TagsFilter" +import TagRenderingConfig from "./TagRenderingConfig" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" /** * Minimal information about a theme @@ -93,6 +97,14 @@ export default class ThemeConfig implements ThemeInformation { public readonly source: ThemeConfigJson public readonly enableCache: boolean + public readonly popups: Readonly<{ + id: string, + dismissible?: boolean, + condition: TagsFilter, + title: TagRenderingConfig, + body: TagRenderingConfig[] + }>[] + constructor( json: ThemeConfigJson, official = true, @@ -193,11 +205,26 @@ export default class ThemeConfig implements ThemeInformation { icon: "./assets/svg/pop-out.svg", href: "https://{basepath}/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}", newTab: true, - requirements: ["iframe", "no-welcome-message"], + requirements: ["iframe", "no-welcome-message"] }, context + ".extraLink" ) + this.popups = (json.popup ?? []).map((p, i) => { + const ctx = context + ".popup." + i + if (!p.id) { + throw (ctx + ": an id is required") + } + const body: TagRenderingConfigJson[] = Array.isArray(p.body) ? p.body : [p.body] + return { + id: p.id, + dismissible: p.dismissible ?? false, + condition: TagUtils.Tag(p.condition), + title: new TagRenderingConfig(p.title, ctx + ".title"), + body: body.map((body, i) => new TagRenderingConfig(body, ctx + ".body." + i)) + } + }) + this.hideFromOverview = json.hideFromOverview ?? false this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined this.enableUserBadge = json.enableUserBadge ?? true @@ -351,7 +378,7 @@ export default class ThemeConfig implements ThemeInformation { // The 'favourite'-layer contains pretty much all images as it bundles all layers, so we exclude it const jsonNoFavourites = { ...json, - layers: json.layers.filter((l) => l["id"] !== "favourite"), + layers: json.layers.filter((l) => l["id"] !== "favourite") } const usedImages = jsonNoFavourites._usedImages usedImages.sort() diff --git a/src/Models/ThemeConfig/WithContextLoader.ts b/src/Models/ThemeConfig/WithContextLoader.ts index 0fee9d6db..46d16e281 100644 --- a/src/Models/ThemeConfig/WithContextLoader.ts +++ b/src/Models/ThemeConfig/WithContextLoader.ts @@ -12,7 +12,7 @@ export default class WithContextLoader { this._context = context } - /** Given a key, gets the corresponding property from the json (or the default if not found + /** Given a key, gets the corresponding property from the json (or the default if not found) * * The found value is interpreted as a tagrendering and fetched/parsed * */ diff --git a/src/UI/Base/LoginButton.svelte b/src/UI/Base/LoginButton.svelte index 21d332726..011732003 100644 --- a/src/UI/Base/LoginButton.svelte +++ b/src/UI/Base/LoginButton.svelte @@ -6,18 +6,26 @@ export let osmConnection: OsmConnection export let clss: string | undefined = undefined - + /** + * Show the button, even though we are logged in + */ + export let forceShow: boolean = false + export let msg: String = undefined if (osmConnection === undefined) { console.error("No osmConnection passed into loginButton") } let isLoggedIn = osmConnection.isLoggedIn -{#if !$isLoggedIn} +{#if !$isLoggedIn || forceShow} osmConnection.AttemptLogin()} style="margin-left: 0"> - + {#if msg} + {msg} + {:else} + + {/if} {/if} diff --git a/src/UI/Base/Popup.svelte b/src/UI/Base/Popup.svelte index 0670c5e7b..96df2ac07 100644 --- a/src/UI/Base/Popup.svelte +++ b/src/UI/Base/Popup.svelte @@ -42,8 +42,8 @@ shown.set(false)} - outsideclose + on:close={() =>shown.set(false)} + outsideclose={dismissable} size="xl" {dismissable} {defaultClass} diff --git a/src/UI/Map/MapLibreAdaptor.ts b/src/UI/Map/MapLibreAdaptor.ts index 9973beaaf..643d1ea32 100644 --- a/src/UI/Map/MapLibreAdaptor.ts +++ b/src/UI/Map/MapLibreAdaptor.ts @@ -611,7 +611,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { if (!map) { return } - console.log("Bounds are", bbox?.asGeometry()) if (bbox) { if (GeoOperations.surfaceAreaInSqMeters(bbox.asGeojsonCached()) > 1) { map?.setMaxBounds(bbox.toLngLat()) diff --git a/src/UI/SpecialVisualisations/SettingsVisualisations.ts b/src/UI/SpecialVisualisations/SettingsVisualisations.ts index 8d1242039..b7ff0890c 100644 --- a/src/UI/SpecialVisualisations/SettingsVisualisations.ts +++ b/src/UI/SpecialVisualisations/SettingsVisualisations.ts @@ -111,12 +111,27 @@ export class SettingsVisualisations { }, { funcName: "login_button", - args: [], + args: [{ + name: "force", + doc: "Always show this button, even if logged in" + }, { + name: "message", + doc: "Message to display on the button" + }], docs: "Show a login button", needsUrls: [], group: "settings", - constr(state: SpecialVisualizationState): SvelteUIElement { - return new SvelteUIElement(LoginButton, { osmConnection: state.osmConnection }) + constr(state: SpecialVisualizationState, _, args): SvelteUIElement { + const force = args[0].toLowerCase() + let msg = args[1] + if (msg === "") { + msg = undefined + } + return new SvelteUIElement(LoginButton, { + osmConnection: state.osmConnection, + msg, + forceShow: force === "yes" || force === "true" + }) }, }, diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index a7d54765f..bff3dac80 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -49,6 +49,8 @@ import Loading from "./Base/Loading.svelte" import { WithSearchState } from "../Models/ThemeViewState/WithSearchState" import TitleHandler from "../Logic/Actors/TitleHandler" + import Popup from "./Base/Popup.svelte" + import TagRenderingAnswer from "./Popup/TagRendering/TagRenderingAnswer.svelte" export let state: WithSearchState new TitleHandler(state.selectedElement, state) @@ -76,6 +78,7 @@ let mapproperties: MapProperties = state.mapProperties let searchOpened = state.searchState.showSearchDrawer + let metatags = state.userRelatedState.preferencesAsTags Orientation.singleton.startMeasurements() let slideDuration = 150 // ms @@ -148,7 +151,7 @@ const bottomRight = mlmap.unproject([rect.right, rect.bottom]) const bbox = new BBox([ [topLeft.lng, topLeft.lat], - [bottomRight.lng, bottomRight.lat], + [bottomRight.lng, bottomRight.lat] ]) state.visualFeedbackViewportBounds.setData(bbox) } @@ -500,5 +503,24 @@ {/if} {/if} + {#each theme.popups as popup} + {#if popup.condition.matchesProperties($metatags)} + + + + {#each popup.body as body} + + {/each} + {popup.id} + + + {/if} + {/each}