diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d2e224fb6e..6acc1b1eee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,22 +6,22 @@ Hi! Thanks for checking out how to contribute to MapComplete! There are multiple ways to contribute: - Translating MapComplete to your own language can be done - on [this website](https://hosted.weblate.org/projects/mapcomplete/) + on [the Weblate website](https://hosted.weblate.org/projects/mapcomplete/) - If you encounter a bug, the [issue tracker](https://github.com/pietervdvn/MapComplete/issues) is the place to be - A good start to contribute is to create a single map layer showing features which interest you. Read more about [making your own theme](/Docs/Making_Your_Own_Theme.md). - If you want to improve a theme, create a new theme, spot a typo in the repo... the best way is to open a pull request. -People who stick around and contribute in a meaningful way, _might_ be granted write access to the repository (except . This is +People who stick around and contribute in a meaningful way, _might_ be granted write access to the repository (except the branches *master* and *develop*). This is done on a purely subjective basis, e.g. after a few pull requests and if you are a member of the OSM community. Rights of contributors ----------------------- If you have write access to the repository, you can make a fork of an already existing branch and push this new branch -to github. This means that this branch will be _automatically built_ and be **deployed** +to GitHub. This means that this branch will be _automatically built_ and be **deployed** to `https://pietervdvn.github.io/mc/`. You can see the deploy process -on [Github Actions](https://github.com/pietervdvn/MapComplete/actions). Don't worry about pushing too much. These -deploys are free and totally automatic. They might fail if something is wrong, but this will hinder no-one. +on [GitHub Actions](https://github.com/pietervdvn/MapComplete/actions). Don't worry about pushing too much. These +deploys are free and totally automatic. They might fail if something is wrong, but this will hinder no one. Additionaly, some other maintainer might step in and merge the latest develop with your branch, making later pull requests easier. @@ -58,6 +58,6 @@ again to start fresh. What not to contribute ---------------------- -I'm currently _not_ accepting files for integration with some editor. There are hundreds of editors out there, if every single one of them needs a file in the repo, this ends up as a mess. -Furthermore, MapComplete doesn't want to encourage or discourage some editors. +I'm currently _not_ accepting files for integration with some text editor. There are hundreds of editors out there, if every single one of them needs a file in the repo, this ends up as a mess. +Furthermore, MapComplete doesn't want to encourage or discourage some text editors. At last, these files are hard to maintain and are hard to detect if they have fallen out of use. diff --git a/assets/layers/guidepost/guidepost.json b/assets/layers/guidepost/guidepost.json new file mode 100644 index 0000000000..6fd27bcc42 --- /dev/null +++ b/assets/layers/guidepost/guidepost.json @@ -0,0 +1,53 @@ +{ + "id": "guidepost", + "name": { + "en": "Guideposts" + }, + "description": { + "en": "Guideposts (also known as fingerposts or finger posts) are often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations" + }, + "source": { + "osmTags": "information=guidepost" + }, + "minzoom": 14, + "presets": [ + { + "title": { + "en": "a guidepost" + }, + "tags": [ + "information=guidepost", + "tourism=information" + ], + "description": { + "en": "A guidepost (also known as fingerpost) is often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations" + }, + "exampleImages": [ + "./assets/layers/guidepost/guidepost_example.jpg" + ] + } + ], + "deletion": true, + "allowMove": { + "enableImproveAccuracy": "true", + "enableRelocation": "false" + }, + "title": { + "render": { + "en": "Guidepost" + } + }, + "mapRendering": [ + { + "location": [ + "centroid", + "point" + ], + "icon": "./assets/layers/guidepost/guidepost.svg", + "anchor": "bottom" + } + ], + "tagRenderings": [ + "images" + ] +} diff --git a/assets/layers/guidepost/guidepost.svg b/assets/layers/guidepost/guidepost.svg new file mode 100644 index 0000000000..61d75072db --- /dev/null +++ b/assets/layers/guidepost/guidepost.svg @@ -0,0 +1,61 @@ + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/assets/layers/guidepost/guidepost.svg.license b/assets/layers/guidepost/guidepost.svg.license new file mode 100644 index 0000000000..bb226dbab0 --- /dev/null +++ b/assets/layers/guidepost/guidepost.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: OSM Carto +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/layers/guidepost/guidepost_example.jpg b/assets/layers/guidepost/guidepost_example.jpg new file mode 100644 index 0000000000..8e06fecf44 Binary files /dev/null and b/assets/layers/guidepost/guidepost_example.jpg differ diff --git a/assets/layers/guidepost/guidepost_example.jpg.license b/assets/layers/guidepost/guidepost_example.jpg.license new file mode 100644 index 0000000000..cd1b487693 --- /dev/null +++ b/assets/layers/guidepost/guidepost_example.jpg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Mschaeuble +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/layers/guidepost/license_info.json b/assets/layers/guidepost/license_info.json new file mode 100644 index 0000000000..e622ffc30a --- /dev/null +++ b/assets/layers/guidepost/license_info.json @@ -0,0 +1,22 @@ +[ + { + "path": "guidepost.svg", + "license": "CC0-1.0", + "authors": [ + "OSM Carto" + ], + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Guidepost-14.svg" + ] + }, + { + "path": "guidepost_example.jpg", + "license": "CC0-1.0", + "authors": [ + "Mschaeuble" + ], + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Signpost.jpg" + ] + } +] \ No newline at end of file diff --git a/assets/layers/guidepost/signpost_example.jpg.license b/assets/layers/guidepost/signpost_example.jpg.license new file mode 100644 index 0000000000..cd1b487693 --- /dev/null +++ b/assets/layers/guidepost/signpost_example.jpg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Mschaeuble +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/layers/ticket_machine/ticket_machine.json b/assets/layers/ticket_machine/ticket_machine.json index 6292fc6f92..8f60ae2345 100644 --- a/assets/layers/ticket_machine/ticket_machine.json +++ b/assets/layers/ticket_machine/ticket_machine.json @@ -20,7 +20,7 @@ ] } }, - "minzoom": 19, + "minzoom": 18, "title": { "render": { "en": "Ticket Machine", diff --git a/assets/layers/ticket_validator/ticket_validator.json b/assets/layers/ticket_validator/ticket_validator.json index b0b93552a5..16520ec3a1 100644 --- a/assets/layers/ticket_validator/ticket_validator.json +++ b/assets/layers/ticket_validator/ticket_validator.json @@ -13,7 +13,7 @@ "source": { "osmTags": "amenity=ticket_validator" }, - "minzoom": 19, + "minzoom": 18, "title": { "render": { "en": "Ticket Validator", diff --git a/assets/themes/bag/bag.json b/assets/themes/bag/bag.json index 8f33320cbf..204226ed31 100644 --- a/assets/themes/bag/bag.json +++ b/assets/themes/bag/bag.json @@ -47,7 +47,7 @@ "osmTags": "building~*", "maxCacheAge": 0 }, - "minzoom": 19, + "minzoom": 18, "calculatedTags": [ "_surface:strict:=feat(get)('_surface')" ], @@ -154,7 +154,7 @@ }, "maxCacheAge": 0 }, - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "label": { @@ -194,7 +194,7 @@ "osmTags": "identificatie~*", "maxCacheAge": 0 }, - "minzoom": 19, + "minzoom": 18, "calculatedTags": [ "_overlaps_with_buildings=overlapWith(feat)('osm:buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)", "_overlaps_with=feat(get)('_overlaps_with_buildings').find(f => f.overlap > 1 /* square meter */ )", @@ -379,7 +379,7 @@ "osmTags": "identificatie~*", "maxCacheAge": 0 }, - "minzoom": 19, + "minzoom": 18, "calculatedTags": [ "_closed_osm_addr:=closest(feat)('osm:adresses').properties", "_bag_obj:addr:housenumber=`${feat.properties.huisnummer}${feat.properties.huisletter}${(feat.properties.toevoeging != '') ? '-' : ''}${feat.properties.toevoeging}`", @@ -427,4 +427,4 @@ } ], "hideFromOverview": true -} \ No newline at end of file +} diff --git a/assets/themes/blind_osm/blind_osm.json b/assets/themes/blind_osm/blind_osm.json index 9532bd0cf2..29acc68352 100644 --- a/assets/themes/blind_osm/blind_osm.json +++ b/assets/themes/blind_osm/blind_osm.json @@ -68,7 +68,7 @@ { "builtin": "kerbs", "override": { - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "iconBadges": [ @@ -112,4 +112,4 @@ }, "stairs" ] -} \ No newline at end of file +} diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json index f5bb2c2dd1..f34d3e69c4 100644 --- a/assets/themes/climbing/climbing.json +++ b/assets/themes/climbing/climbing.json @@ -464,10 +464,14 @@ { "builtin": [ "toilet", - "drinking_water" + "drinking_water", + "guidepost" ], "override": { - "minzoom": 15 + "minzoom": 15, + "mapRendering": [{ + "iconSize": "30,30" + }] } } ], diff --git a/assets/themes/guideposts/guideposts.json b/assets/themes/guideposts/guideposts.json new file mode 100644 index 0000000000..30a857b7be --- /dev/null +++ b/assets/themes/guideposts/guideposts.json @@ -0,0 +1,14 @@ +{ + "id": "guideposts", + "title": { + "en": "Guideposts" + }, + "description": { + "en": "Guideposts (also known as fingerposts or finger posts) are often found along official hiking, cycling, skiing or horseback riding routes to indicate the directions to different destinations. Additionally, they are often named after a region or place and show the altitude.\n\nThe position of a signpost can be used by a hiker/biker/rider/skier as a confirmation of the current position, especially if they use a printed map without a GPS receiver. " + }, + "icon": "./assets/layers/guidepost/guidepost.svg", + "startZoom": 2, + "layers": [ + "guidepost" + ] +} diff --git a/assets/themes/healthcare/healthcare.json b/assets/themes/healthcare/healthcare.json index 488a88fe3c..bcc2c2d96b 100644 --- a/assets/themes/healthcare/healthcare.json +++ b/assets/themes/healthcare/healthcare.json @@ -111,8 +111,8 @@ "=presets": [], "=name": null, "override": { - "minzoom": 19 + "minzoom": 18 } } ] -} \ No newline at end of file +} diff --git a/assets/themes/onwheels/onwheels.json b/assets/themes/onwheels/onwheels.json index 38544e0b7b..047ade494c 100644 --- a/assets/themes/onwheels/onwheels.json +++ b/assets/themes/onwheels/onwheels.json @@ -32,7 +32,7 @@ { "builtin": "indoors", "override": { - "minzoom": 19, + "minzoom": 18, "name": null, "passAllFeatures": true } @@ -43,7 +43,7 @@ "name": null, "tagRendering": null, "title": "null", - "minzoom": 19, + "minzoom": 18, "shownByDefault": false } }, @@ -71,7 +71,7 @@ { "builtin": "entrance", "override": { - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "circle:white;./assets/themes/onwheels/entrance.svg" @@ -131,7 +131,7 @@ { "builtin": "kerbs", "override": { - "minzoom": 19, + "minzoom": 18, "syncSelection": "theme-only", "mapRendering": [ { @@ -289,7 +289,7 @@ { "builtin": "toilet", "override": { - "minzoom": 19, + "minzoom": 18, "syncSelection": "theme-only", "mapRendering": [ { @@ -349,7 +349,7 @@ { "builtin": "reception_desk", "override": { - "minzoom": 19, + "minzoom": 18, "syncSelection": "theme-only" } }, @@ -357,7 +357,7 @@ { "builtin": "elevator", "override": { - "minzoom": 19, + "minzoom": 18, "syncSelection": "theme-only", "mapRendering": [ { @@ -524,4 +524,4 @@ ] }, "enableDownload": true -} \ No newline at end of file +} diff --git a/assets/themes/pets/pets.json b/assets/themes/pets/pets.json index 650095516c..d9eaa8963d 100644 --- a/assets/themes/pets/pets.json +++ b/assets/themes/pets/pets.json @@ -165,7 +165,7 @@ { "builtin": "food", "override": { - "minzoom": 19, + "minzoom": 18, "filter": null, "name": null } @@ -181,7 +181,7 @@ { "builtin": "shops", "override": { - "minzoom": 19, + "minzoom": 18, "filter": null, "presets": [ { @@ -220,4 +220,4 @@ } ], "credits": "Niels Elgaard Larsen" -} \ No newline at end of file +} diff --git a/assets/themes/stations/stations.json b/assets/themes/stations/stations.json index 8b242d1af4..c358d0a3be 100644 --- a/assets/themes/stations/stations.json +++ b/assets/themes/stations/stations.json @@ -32,7 +32,7 @@ { "builtin": "indoors", "override": { - "minzoom": 19, + "minzoom": 18, "passAllFeatures": true, "mapRendering": [ {}, @@ -50,7 +50,7 @@ { "builtin": "stairs", "override": { - "minzoom": 19 + "minzoom": 18 } }, { @@ -130,7 +130,7 @@ ] }, "presets": null, - "minzoom": 19 + "minzoom": 18 } }, { @@ -143,7 +143,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "circle:white;./assets/themes/stations/bicycle_parking.svg" @@ -161,7 +161,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "circle:white;./assets/themes/stations/rental_bicycle.svg" @@ -179,7 +179,7 @@ ] }, "presets": null, - "minzoom": 19 + "minzoom": 18 } }, { @@ -195,7 +195,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering+": [ { "color": "#00f", @@ -214,7 +214,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering+": [ { "color": "yellow", @@ -235,13 +235,13 @@ "clock" ], "override": { - "minzoom": 19 + "minzoom": 18 } }, { "builtin": "bench", "override": { - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "./assets/themes/stations/bench.svg" @@ -252,7 +252,7 @@ { "builtin": "drinking_water", "override": { - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "circle:white;./assets/themes/stations/drinking_water.svg" @@ -293,7 +293,7 @@ "zh_Hant": "時刻表" } }, - "minzoom": 19, + "minzoom": 18, "source": { "osmTags": { "and": [ @@ -412,4 +412,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/assets/themes/street_lighting/street_lighting.json b/assets/themes/street_lighting/street_lighting.json index 0ba9c2ab49..7d1c8da7fc 100644 --- a/assets/themes/street_lighting/street_lighting.json +++ b/assets/themes/street_lighting/street_lighting.json @@ -221,7 +221,7 @@ ] } }, - "minzoom": 19, + "minzoom": 18, "title": { "render": { "en": "Street", @@ -361,4 +361,4 @@ } ], "credits": "Robin van der Linde" -} \ No newline at end of file +} diff --git a/assets/themes/transit/transit.json b/assets/themes/transit/transit.json index f2d2af6650..3f98526768 100644 --- a/assets/themes/transit/transit.json +++ b/assets/themes/transit/transit.json @@ -36,22 +36,19 @@ { "builtin": "bike_parking", "override": { - "minzoom": 19, - "minzoomVisible": 19 + "minzoom": 18 } }, { "builtin": "parking", "override": { - "minzoom": 19, - "minzoomVisible": 19 + "minzoom": 18 } }, { "builtin": "shelter", "override": { - "minzoom": 19, - "minzoomVisible": 19, + "minzoom": 18, "source": { "osmTags": { "and": [ @@ -67,4 +64,4 @@ } ], "credits": "Robin van der Linde" -} \ No newline at end of file +} diff --git a/assets/themes/width/width.json b/assets/themes/width/width.json index 6d0bfb1147..9778da3293 100644 --- a/assets/themes/width/width.json +++ b/assets/themes/width/width.json @@ -32,7 +32,7 @@ 51.190748429411705 ] ], - "defaultBackgroundId": "CartoDB.DarkMatterNoLabels", + "defaultBackgroundId": "alidade.smooth_dark", "layers": [ { "id": "street_with_width", @@ -271,4 +271,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/langs/en.json b/langs/en.json index 7167afb957..7bd2338e5b 100644 --- a/langs/en.json +++ b/langs/en.json @@ -124,7 +124,7 @@ "pleaseLogin": "Please log in to add a new feature", "presetInfo": "The new POI will have {tags}", "stillLoading": "The data is still loading. Please wait a bit before you add a new feature.", - "title": "Add a new feature?", + "title": "Add a new feature", "warnVisibleForEveryone": "Your addition will be visible for everyone", "wrongType": "This feature is not a node or a way and can not be imported", "zoomInFurther": "Zoom in further to add a feature.", diff --git a/langs/nl.json b/langs/nl.json index b3feb8d923..f86e8d96d2 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -124,7 +124,7 @@ "pleaseLogin": "Gelieve je aan te melden om een object toe te voegen", "presetInfo": "Het nieuwe object krijgt de attributen {tags}", "stillLoading": "De data worden nog geladen. Nog even geduld en dan kan je een object toevoegen.", - "title": "Nieuw object toevoegen?", + "title": "Nieuw object toevoegen", "warnVisibleForEveryone": "Je toevoeging is voor iedereen zichtbaar", "wrongType": "Dit object is geen punt of lijn en kan daarom niet geïmporteerd worden", "zoomInFurther": "Gelieve verder in te zoomen om een object toe te voegen.", diff --git a/package-lock.json b/package-lock.json index e325aa81fe..b8edfaf5b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9612,9 +9612,9 @@ } }, "node_modules/postcss": { - "version": "8.4.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz", - "integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -20529,9 +20529,9 @@ } }, "postcss": { - "version": "8.4.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz", - "integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "requires": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", diff --git a/package.json b/package.json index c843af3732..f1c7b90a01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.33.7", + "version": "0.33.8", "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", "bugs": "https://github.com/pietervdvn/MapComplete/issues", @@ -37,7 +37,7 @@ }, "scripts": { "start": "npm run generate:layeroverview && npm run strt", - "strt": "vite --host", + "strt": "vite --host | sed 's/localhost:/127.0.0.1:/g'", "strttest": "export NODE_OPTIONS=--max_old_space_size=8364 && parcel serve test.html assets/templates/*.svg assets/templates/fonts/*.ttf", "watch:css": "tailwindcss -i index.css -o public/css/index-tailwind-output.css --watch", "generate:css": "tailwindcss -i src/index.css -o public/css/index-tailwind-output.css", diff --git a/scripts/build.sh b/scripts/build.sh index 6649d61cab..b530e79eac 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,8 +8,7 @@ rm -rf dist/* rm -rf .cache mkdir dist 2> /dev/null mkdir dist/assets 2> /dev/null -mkdir dist/assets/langs 2> /dev/null -mkdir dist/assets/langs/layers 2> /dev/null + export NODE_OPTIONS="--max-old-space-size=8192" @@ -48,11 +47,12 @@ fi export NODE_OPTIONS=--max-old-space-size=7000 vite build $SRC_MAPS - - # Copy the layer files, as these might contain assets (e.g. svgs) cp -r assets/layers/ dist/assets/layers/ cp -r assets/themes/ dist/assets/themes/ cp -r assets/svg/ dist/assets/svg/ -cp -r langs/layers/ dist/assets/langs/layers/ +mkdir dist/assets/langs +mkdir dist/assets/langs/layers +cp -r langs/layers/ dist/assets/langs/ +ls dist/assets/langs/layers/ export NODE_OPTIONS="" diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index f3fea1d981..b8ea07fdb5 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -61,9 +61,9 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P if (!layout.icon.endsWith(".svg")) { console.warn( "Not creating a social image for " + - layout.id + - " as it is _not_ a .svg: " + - layout.icon + layout.id + + " as it is _not_ a .svg: " + + layout.icon, ) return undefined } @@ -85,7 +85,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P delete svg["defs"] delete svg["$"] let templateSvg = await ScriptUtils.ReadSvg( - "./public/assets/SocialImageTemplate" + template + ".svg" + "./public/assets/SocialImageTemplate" + template + ".svg", ) templateSvg = Utils.WalkJson( templateSvg, @@ -104,7 +104,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P return false } return mightBeTokenToReplace.circle[0]?.$?.style?.indexOf("fill:#ff00ff") >= 0 - } + }, ) const builder = new xml2js.Builder() @@ -116,7 +116,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P async function createManifest( layout: LayoutConfig, - alreadyWritten: string[] + alreadyWritten: string[], ): Promise<{ manifest: any whiteIcons: string[] @@ -210,19 +210,17 @@ function asLangSpan(t: Translation, tag = "span"): string { let previousSrc: Set = new Set() let eliUrlsCached: string[] -function eliUrls(): string[] { + +async function eliUrls(): Promise { if (eliUrlsCached) { return eliUrlsCached } const urls: string[] = [] const regex = /{switch:([^}]+)}/ - const rasterLayers = [ - ...AvailableRasterLayers.vectorLayers, - ...eli.features, - ...eli_global.layers.map((properties) => ({ properties })), - ] + const rasterLayers = [AvailableRasterLayers.maptilerDefaultLayer, ...eli.features, ...eli_global.layers.map(properties => ({ properties }))] for (const feature of rasterLayers) { - const url = (feature).properties.url + const f = feature + const url = f.properties.url const match = url.match(regex) if (match) { const domains = match[1].split(",") @@ -231,17 +229,43 @@ function eliUrls(): string[] { } else { urls.push(url) } + + if (f.properties.type === "vector") { + // We also need to whitelist eventual sources + const styleSpec = await Utils.downloadJsonCached(f.properties.url, 1000 * 120) + for (const key of Object.keys(styleSpec.sources)) { + const url = styleSpec.sources[key].url + if(!url){ + continue + } + let urlClipped = url + if(url.indexOf("?") > 0){ + urlClipped = url?.substring(0, url.indexOf("?")) + } + console.log("Source url ",key,url) + urls.push(url) + if(urlClipped.endsWith(".json")){ + const tileInfo = await Utils.downloadJsonCached(url, 1000*120) + urls.push(tileInfo["tiles"] ?? []) + } + + } + urls.push(...(styleSpec["tiles"] ?? [])) + urls.push(styleSpec["sprite"]) + urls.push(styleSpec["glyphs"]) + } + } eliUrlsCached = urls - return urls + return Utils.NoNull(urls).sort() } -function generateCsp( +async function generateCsp( layout: LayoutConfig, options: { scriptSrcs: string[] - } -): string { + }, +): Promise { const apiUrls: string[] = [ "'self'", ...Constants.defaultOverpassUrls, @@ -251,12 +275,12 @@ function generateCsp( "https://pietervdvn.goatcounter.com", ] .concat(...SpecialVisualizations.specialVisualizations.map((sv) => sv.needsUrls)) - .concat(...eliUrls()) + .concat(...await eliUrls()) const geojsonSources: string[] = layout.layers.map((l) => l.source?.geojsonSource) const hosts = new Set() const eliLayers: RasterLayerPolygon[] = AvailableRasterLayers.layersAvailableAt( - new ImmutableStore({ lon: 0, lat: 0 }) + new ImmutableStore({ lon: 0, lat: 0 }), ).data const vectorLayers = eliLayers.filter((l) => l.properties.type === "vector") const vectorSources = vectorLayers.map((l) => l.properties.url) @@ -283,14 +307,14 @@ function generateCsp( "connect-src items for theme", layout.id, "(extra sources: ", - newSrcs.join(" ") + ")" + newSrcs.join(" ") + ")", ) previousSrc = hosts const csp: Record = { "default-src": "'self'", "script-src": ["'self'", "https://gc.zgo.at/count.js", ...(options?.scriptSrcs ?? [])].join( - " " + " ", ), "img-src": "* data:", // maplibre depends on 'data:' to load "connect-src": connectSrc.join(" "), @@ -320,12 +344,12 @@ const removeOtherLanguagesHash = crypto async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) { Locale.language.setData(layout.language[0]) const targetLanguage = layout.language[0] - const ogTitle = Translations.T(layout.title).textFor(targetLanguage).replace(/"/g, '\\"') + const ogTitle = Translations.T(layout.title).textFor(targetLanguage).replace(/"/g, "\\\"") const ogDescr = Translations.T( - layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap" + layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap", ) .textFor(targetLanguage) - .replace(/"/g, '\\"') + .replace(/"/g, "\\\"") let ogImage = layout.socialImage let twitterImage = ogImage if (ogImage === LayoutConfig.defaultSocialImage && layout.official) { @@ -390,34 +414,34 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title }) const templateLines = template.split("\n") const removeOtherLanguagesReference = templateLines.find( - (line) => line.indexOf("./src/UI/RemoveOtherLanguages.js") >= 0 + (line) => line.indexOf("./src/UI/RemoveOtherLanguages.js") >= 0, ) let output = template .replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1")) .replace( "Made with OpenStreetMap", - Translations.t.general.poweredByOsm.textFor(targetLanguage) + Translations.t.general.poweredByOsm.textFor(targetLanguage), ) .replace(/.*/s, themeSpecific) .replace( //, - generateCsp(layout, { + await generateCsp(layout, { scriptSrcs: [`'sha256-${removeOtherLanguagesHash}'`], - }) + }), ) .replace(removeOtherLanguagesReference, "") .replace( /.*/s, - asLangSpan(layout.shortDescription) + asLangSpan(layout.shortDescription), ) .replace( /.*/s, - "" + "", ) .replace( /.*\/src\/index\.ts.*/, - `` + ``, ) return output @@ -508,13 +532,14 @@ async function main(): Promise { title: { en: "MapComplete" }, description: { en: "A thematic map viewer and editor based on OpenStreetMap" }, }), - alreadyWritten + alreadyWritten, ) const manif = JSON.stringify(manifest, undefined, 2) writeFileSync("public/index.webmanifest", manif) } +ScriptUtils.fixUtils() main().then(() => { console.log("All done!") }) diff --git a/scripts/hetzner/config/Caddyfile b/scripts/hetzner/config/Caddyfile index 27b328008f..139a801862 100644 --- a/scripts/hetzner/config/Caddyfile +++ b/scripts/hetzner/config/Caddyfile @@ -22,3 +22,9 @@ report.mapcomplete.org { to http://127.0.0.1:2600 } } + +studio.mapcomplete.org { + reverse_proxy { + to http://127.0.0.1:1235 + } +} diff --git a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts index ecd8d82884..40705891f0 100644 --- a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts @@ -1,60 +1,68 @@ -import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" -import { WritableFeatureSource } from "../FeatureSource" -import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" -import { Feature, Point } from "geojson" -import { TagUtils } from "../../Tags/TagUtils" -import BaseUIElement from "../../../UI/BaseUIElement" -import { Utils } from "../../../Utils" +import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; +import { WritableFeatureSource } from "../FeatureSource"; +import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"; +import { Feature, Point } from "geojson"; +import { TagUtils } from "../../Tags/TagUtils"; +import BaseUIElement from "../../../UI/BaseUIElement"; +import { Utils } from "../../../Utils"; +import { OsmTags } from "../../../Models/OsmFeature"; /** * Highly specialized feature source. * Based on a lon/lat UIEVentSource, will generate the corresponding feature with the correct properties */ export class LastClickFeatureSource implements WritableFeatureSource { - public readonly features: UIEventSource = new UIEventSource([]) + public readonly features: UIEventSource = new UIEventSource([]); + public readonly hasNoteLayer: boolean; + public readonly renderings: string[]; + public readonly hasPresets: boolean; + private i: number = 0; constructor(location: Store<{ lon: number; lat: number }>, layout: LayoutConfig) { - const allPresets: BaseUIElement[] = [] + this.hasNoteLayer = layout.layers.some((l) => l.id === "note"); + this.hasPresets = layout.layers.some((l) => l.presets?.length > 0); + const allPresets: BaseUIElement[] = []; for (const layer of layout.layers) for (let i = 0; i < (layer.presets ?? []).length; i++) { - const preset = layer.presets[i] - const tags = new ImmutableStore(TagUtils.KVtoProperties(preset.tags)) + const preset = layer.presets[i]; + const tags = new ImmutableStore(TagUtils.KVtoProperties(preset.tags)); const { html } = layer.mapRendering[0].RenderIcon(tags, false, { noSize: true, - includeBadges: false, - }) - allPresets.push(html) + includeBadges: false + }); + allPresets.push(html); } - const renderings = Utils.Dedup( + this.renderings = Utils.Dedup( allPresets.map((uiElem) => Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML ) - ) - - let i = 0 + ); location.addCallbackAndRunD(({ lon, lat }) => { - const properties = { - lastclick: "yes", - id: "last_click_" + i, - has_note_layer: layout.layers.some((l) => l.id === "note") ? "yes" : "no", - has_presets: layout.layers.some((l) => l.presets?.length > 0) ? "yes" : "no", - renderings: renderings.join(""), - number_of_presets: "" + renderings.length, - first_preset: renderings[0], - } - i++ + this.features.setData([this.createFeature(lon, lat)]); + }); + } - const point = >{ - type: "Feature", - properties, - geometry: { - type: "Point", - coordinates: [lon, lat], - }, + public createFeature(lon: number, lat: number): Feature { + const properties: OsmTags = { + lastclick: "yes", + id: "last_click_" + this.i, + has_note_layer: this.hasNoteLayer ? "yes" : "no", + has_presets: this.hasPresets ? "yes" : "no", + renderings: this.renderings.join(""), + number_of_presets: "" + this.renderings.length, + first_preset: this.renderings[0] + }; + this.i++; + + return >{ + type: "Feature", + properties, + geometry: { + type: "Point", + coordinates: [lon, lat] } - this.features.setData([point]) - }) + }; } } diff --git a/src/Logic/State/FeatureSwitchState.ts b/src/Logic/State/FeatureSwitchState.ts index 3fda13afd6..a12835fbb1 100644 --- a/src/Logic/State/FeatureSwitchState.ts +++ b/src/Logic/State/FeatureSwitchState.ts @@ -99,7 +99,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches { ) this.featureSwitchCommunityIndex = FeatureSwitchUtils.initSwitch( "fs-community-index", - true, + this.featureSwitchEnableLogin.data, "Disables/enables the button to get in touch with the community" ) this.featureSwitchExtraLinkEnabled = FeatureSwitchUtils.initSwitch( diff --git a/src/Logic/State/UserSettingsMetaTagging.ts b/src/Logic/State/UserSettingsMetaTagging.ts index 6e568c5c32..33a5ae85b5 100644 --- a/src/Logic/State/UserSettingsMetaTagging.ts +++ b/src/Logic/State/UserSettingsMetaTagging.ts @@ -1,42 +1,14 @@ import { Utils } from "../../Utils" /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ export class ThemeMetaTagging { - public static readonly themeName = "usersettings" + public static readonly themeName = "usersettings" - public metaTaggging_for_usersettings(feat: { properties: Record }) { - Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => - feat.properties._description - .match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) - ?.at(1) - ) - Utils.AddLazyProperty( - feat.properties, - "_d", - () => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? "" - ) - Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () => - ((feat) => { - const e = document.createElement("div") - e.innerHTML = feat.properties._d - return Array.from(e.getElementsByTagName("a")).filter( - (a) => a.href.match(/mastodon|en.osm.town/) !== null - )[0]?.href - })(feat) - ) - Utils.AddLazyProperty(feat.properties, "_mastodon_link", () => - ((feat) => { - const e = document.createElement("div") - e.innerHTML = feat.properties._d - return Array.from(e.getElementsByTagName("a")).filter( - (a) => a.getAttribute("rel")?.indexOf("me") >= 0 - )[0]?.href - })(feat) - ) - Utils.AddLazyProperty( - feat.properties, - "_mastodon_candidate", - () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a - ) - feat.properties["__current_backgroun"] = "initial_value" - } -} + public metaTaggging_for_usersettings(feat: {properties: Record}) { + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) ) + Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' ) + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) ) + Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) ) + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a ) + feat.properties['__current_backgroun'] = 'initial_value' + } +} \ No newline at end of file diff --git a/src/Logic/Web/ThemeViewStateHashActor.ts b/src/Logic/Web/ThemeViewStateHashActor.ts index b10208f09e..8527d8d796 100644 --- a/src/Logic/Web/ThemeViewStateHashActor.ts +++ b/src/Logic/Web/ThemeViewStateHashActor.ts @@ -175,7 +175,6 @@ export default class ThemeViewStateHashActor { } private back() { - console.log("Got a back event") const state = this._state // history.pushState(null, null, window.location.pathname); if (state.selectedElement.data) { diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts index ad68f6979e..39aa182ad7 100644 --- a/src/Models/Constants.ts +++ b/src/Models/Constants.ts @@ -58,7 +58,7 @@ export default class Constants { importHelperUnlock: 5000, } - static readonly minZoomLevelToAddNewPoint = Constants.isRetina() ? 18 : 19 + static readonly minZoomLevelToAddNewPoint = Constants.isRetina() ? 17 : 18 /** * Used by 'PendingChangesUploader', which waits this amount of seconds to upload changes. * (Note that pendingChanges might upload sooner if the popup is closed or similar) diff --git a/src/Models/RasterLayers.ts b/src/Models/RasterLayers.ts index 89e5b61a45..95ded771c0 100644 --- a/src/Models/RasterLayers.ts +++ b/src/Models/RasterLayers.ts @@ -53,61 +53,6 @@ export class AvailableRasterLayers { geometry: BBox.global.asGeometry(), } - public static readonly maptilerCarto: RasterLayerPolygon = { - type: "Feature", - properties: { - name: "MapTiler Carto", - url: "https://api.maptiler.com/maps/openstreetmap/style.json?key=GvoVAJgu46I5rZapJuAy", - category: "osmbasedmap", - id: "maptiler.carto", - type: "vector", - attribution: { - text: "Maptiler", - url: "https://www.maptiler.com/copyright/", - }, - }, - geometry: BBox.global.asGeometry(), - } - - public static readonly maptilerBackdrop: RasterLayerPolygon = { - type: "Feature", - properties: { - name: "MapTiler Backdrop", - url: "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy", - category: "osmbasedmap", - id: "maptiler.backdrop", - type: "vector", - attribution: { - text: "Maptiler", - url: "https://www.maptiler.com/copyright/", - }, - }, - geometry: BBox.global.asGeometry(), - } - public static readonly americana: RasterLayerPolygon = { - type: "Feature", - properties: { - name: "Americana", - url: "https://zelonewolf.github.io/openstreetmap-americana/style.json", - category: "osmbasedmap", - id: "americana", - type: "vector", - attribution: { - text: "Americana", - url: "https://github.com/ZeLonewolf/openstreetmap-americana/", - }, - }, - geometry: BBox.global.asGeometry(), - } - - public static readonly vectorLayers = [ - AvailableRasterLayers.maptilerDefaultLayer, - AvailableRasterLayers.osmCarto, - AvailableRasterLayers.maptilerCarto, - AvailableRasterLayers.maptilerBackdrop, - AvailableRasterLayers.americana, - ] - public static layersAvailableAt( location: Store<{ lon: number; lat: number }> ): Store { @@ -119,7 +64,7 @@ export class AvailableRasterLayers { ) }) ) - const available = Stores.ListStabilized( + return Stores.ListStabilized( availableLayersBboxes.map((eliPolygons) => { const loc = location.data const lonlat: [number, number] = [loc.lon, loc.lat] @@ -129,12 +74,11 @@ export class AvailableRasterLayers { } return GeoOperations.inside(lonlat, eliPolygon) }) + matching.push(AvailableRasterLayers.maptilerDefaultLayer) matching.push(...AvailableRasterLayers.globalLayers) - matching.unshift(...AvailableRasterLayers.vectorLayers) return matching }) ) - return available } } diff --git a/src/Models/ThemeConfig/LayoutConfig.ts b/src/Models/ThemeConfig/LayoutConfig.ts index 9781734dd8..3a88dc569d 100644 --- a/src/Models/ThemeConfig/LayoutConfig.ts +++ b/src/Models/ThemeConfig/LayoutConfig.ts @@ -94,6 +94,9 @@ export default class LayoutConfig implements LayoutInformation { } const context = this.id this.credits = json.credits + if(!json.title){ + throw `The theme ${json.id} does not have a title defined.` + } this.language = json.mustHaveLanguage ?? Object.keys(json.title) this.usedImages = Array.from( new ExtractImages(official, undefined) diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index a15f6b0569..f52da26f50 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -1,62 +1,58 @@ -import LayoutConfig from "./ThemeConfig/LayoutConfig" -import { SpecialVisualizationState } from "../UI/SpecialVisualization" -import { Changes } from "../Logic/Osm/Changes" -import { Store, UIEventSource } from "../Logic/UIEventSource" +import LayoutConfig from "./ThemeConfig/LayoutConfig"; +import { SpecialVisualizationState } from "../UI/SpecialVisualization"; +import { Changes } from "../Logic/Osm/Changes"; +import { Store, UIEventSource } from "../Logic/UIEventSource"; +import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"; +import { OsmConnection } from "../Logic/Osm/OsmConnection"; +import { ExportableMap, MapProperties } from "./MapProperties"; +import LayerState from "../Logic/State/LayerState"; +import { Feature, Point, Polygon } from "geojson"; +import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; +import { Map as MlMap } from "maplibre-gl"; +import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"; +import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"; +import { GeoLocationState } from "../Logic/State/GeoLocationState"; +import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; +import { QueryParameters } from "../Logic/Web/QueryParameters"; +import UserRelatedState from "../Logic/State/UserRelatedState"; +import LayerConfig from "./ThemeConfig/LayerConfig"; +import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"; +import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers"; +import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"; +import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; +import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"; +import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; +import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"; +import ShowDataLayer from "../UI/Map/ShowDataLayer"; +import TitleHandler from "../Logic/Actors/TitleHandler"; +import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"; +import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"; +import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater"; +import { BBox } from "../Logic/BBox"; +import Constants from "./Constants"; +import Hotkeys from "../UI/Base/Hotkeys"; +import Translations from "../UI/i18n/Translations"; +import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"; +import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"; +import { MenuState } from "./MenuState"; +import MetaTagging from "../Logic/MetaTagging"; +import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"; import { - FeatureSource, - IndexedFeatureSource, - WritableFeatureSource, -} from "../Logic/FeatureSource/FeatureSource" -import { OsmConnection } from "../Logic/Osm/OsmConnection" -import { ExportableMap, MapProperties } from "./MapProperties" -import LayerState from "../Logic/State/LayerState" -import { Feature, Point, Polygon } from "geojson" -import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" -import { Map as MlMap } from "maplibre-gl" -import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning" -import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor" -import { GeoLocationState } from "../Logic/State/GeoLocationState" -import FeatureSwitchState from "../Logic/State/FeatureSwitchState" -import { QueryParameters } from "../Logic/Web/QueryParameters" -import UserRelatedState from "../Logic/State/UserRelatedState" -import LayerConfig from "./ThemeConfig/LayerConfig" -import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" -import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers" -import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" -import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" -import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore" -import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter" -import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource" -import ShowDataLayer from "../UI/Map/ShowDataLayer" -import TitleHandler from "../Logic/Actors/TitleHandler" -import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor" -import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader" -import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater" -import { BBox } from "../Logic/BBox" -import Constants from "./Constants" -import Hotkeys from "../UI/Base/Hotkeys" -import Translations from "../UI/i18n/Translations" -import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" -import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource" -import { MenuState } from "./MenuState" -import MetaTagging from "../Logic/MetaTagging" -import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator" -import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource" -import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" -import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer" -import { Utils } from "../Utils" -import { EliCategory } from "./RasterLayerProperties" -import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter" -import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" -import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" -import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" -import NoElementsInViewDetector, { - FeatureViewState, -} from "../Logic/Actors/NoElementsInViewDetector" -import FilteredLayer from "./FilteredLayer" -import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector" -import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" -import { Imgur } from "../Logic/ImageProviders/Imgur" + NewGeometryFromChangesFeatureSource +} from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"; +import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; +import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer"; +import { Utils } from "../Utils"; +import { EliCategory } from "./RasterLayerProperties"; +import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"; +import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"; +import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"; +import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"; +import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector"; +import FilteredLayer from "./FilteredLayer"; +import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"; +import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"; +import { Imgur } from "../Logic/ImageProviders/Imgur"; /** * @@ -67,559 +63,581 @@ import { Imgur } from "../Logic/ImageProviders/Imgur" * It ties up all the needed elements and starts some actors. */ export default class ThemeViewState implements SpecialVisualizationState { - readonly layout: LayoutConfig - readonly map: UIEventSource - readonly changes: Changes - readonly featureSwitches: FeatureSwitchState - readonly featureSwitchIsTesting: Store - readonly featureSwitchUserbadge: Store + readonly layout: LayoutConfig; + readonly map: UIEventSource; + readonly changes: Changes; + readonly featureSwitches: FeatureSwitchState; + readonly featureSwitchIsTesting: Store; + readonly featureSwitchUserbadge: Store; - readonly featureProperties: FeaturePropertiesStore + readonly featureProperties: FeaturePropertiesStore; - readonly osmConnection: OsmConnection - readonly selectedElement: UIEventSource - readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> - readonly mapProperties: MapProperties & ExportableMap - readonly osmObjectDownloader: OsmObjectDownloader + readonly osmConnection: OsmConnection; + readonly selectedElement: UIEventSource; + readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>; + readonly mapProperties: MapProperties & ExportableMap; + readonly osmObjectDownloader: OsmObjectDownloader; - readonly dataIsLoading: Store - /** - * Indicates if there is _some_ data in view, even if it is not shown due to the filters - */ - readonly hasDataInView: Store + readonly dataIsLoading: Store; + /** + * Indicates if there is _some_ data in view, even if it is not shown due to the filters + */ + readonly hasDataInView: Store; - readonly guistate: MenuState - readonly fullNodeDatabase?: FullNodeDatabaseSource + readonly guistate: MenuState; + readonly fullNodeDatabase?: FullNodeDatabaseSource; - readonly historicalUserLocations: WritableFeatureSource> - readonly indexedFeatures: IndexedFeatureSource & LayoutSource - readonly currentView: FeatureSource> - readonly featuresInView: FeatureSource - readonly newFeatures: WritableFeatureSource - readonly layerState: LayerState - readonly perLayer: ReadonlyMap - readonly perLayerFiltered: ReadonlyMap + readonly historicalUserLocations: WritableFeatureSource>; + readonly indexedFeatures: IndexedFeatureSource & LayoutSource; + readonly currentView: FeatureSource>; + readonly featuresInView: FeatureSource; + readonly newFeatures: WritableFeatureSource; + readonly layerState: LayerState; + readonly perLayer: ReadonlyMap; + readonly perLayerFiltered: ReadonlyMap; - readonly availableLayers: Store - readonly selectedLayer: UIEventSource - readonly userRelatedState: UserRelatedState - readonly geolocation: GeoLocationHandler + readonly availableLayers: Store; + readonly selectedLayer: UIEventSource; + readonly userRelatedState: UserRelatedState; + readonly geolocation: GeoLocationHandler; - readonly imageUploadManager: ImageUploadManager + readonly imageUploadManager: ImageUploadManager; - readonly lastClickObject: WritableFeatureSource - readonly overlayLayerStates: ReadonlyMap< - string, - { readonly isDisplayed: UIEventSource } - > - /** - * All 'level'-tags that are available with the current features - */ - readonly floors: Store + readonly addNewPoint: UIEventSource = new UIEventSource(false); - constructor(layout: LayoutConfig) { - Utils.initDomPurify() - this.layout = layout - this.featureSwitches = new FeatureSwitchState(layout) - this.guistate = new MenuState( - this.featureSwitches.featureSwitchWelcomeMessage.data, - layout.id - ) - this.map = new UIEventSource(undefined) - const initial = new InitialMapPositioning(layout) - this.mapProperties = new MapLibreAdaptor(this.map, initial) - const geolocationState = new GeoLocationState() + readonly lastClickObject: LastClickFeatureSource; + readonly overlayLayerStates: ReadonlyMap< + string, + { readonly isDisplayed: UIEventSource } + >; + /** + * All 'level'-tags that are available with the current features + */ + readonly floors: Store; + private readonly newPointDialog: FilteredLayer; - this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting - this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin + constructor(layout: LayoutConfig) { + Utils.initDomPurify(); + this.layout = layout; + this.featureSwitches = new FeatureSwitchState(layout); + this.guistate = new MenuState( + this.featureSwitches.featureSwitchWelcomeMessage.data, + layout.id + ); + this.map = new UIEventSource(undefined); + const initial = new InitialMapPositioning(layout); + this.mapProperties = new MapLibreAdaptor(this.map, initial); + const geolocationState = new GeoLocationState(); - this.osmConnection = new OsmConnection({ - dryRun: this.featureSwitches.featureSwitchIsTesting, - fakeUser: this.featureSwitches.featureSwitchFakeUser.data, - oauth_token: QueryParameters.GetQueryParameter( - "oauth_token", - undefined, - "Used to complete the login" - ), - }) - this.userRelatedState = new UserRelatedState( - this.osmConnection, - layout?.language, - layout, - this.featureSwitches, - this.mapProperties - ) - this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => { - this.mapProperties.allowRotating.setData(fixated !== "yes") - }) - this.selectedElement = new UIEventSource(undefined, "Selected element") - this.selectedLayer = new UIEventSource(undefined, "Selected layer") + this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting; + this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin; - this.selectedElementAndLayer = this.selectedElement.mapD( - (feature) => { - const layer = this.selectedLayer.data - if (!layer) { - return undefined - } - return { layer, feature } - }, - [this.selectedLayer] - ) + this.osmConnection = new OsmConnection({ + dryRun: this.featureSwitches.featureSwitchIsTesting, + fakeUser: this.featureSwitches.featureSwitchFakeUser.data, + oauth_token: QueryParameters.GetQueryParameter( + "oauth_token", + undefined, + "Used to complete the login" + ) + }); + this.userRelatedState = new UserRelatedState( + this.osmConnection, + layout?.language, + layout, + this.featureSwitches, + this.mapProperties + ); + this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => { + this.mapProperties.allowRotating.setData(fixated !== "yes"); + }); + this.selectedElement = new UIEventSource(undefined, "Selected element"); + this.selectedLayer = new UIEventSource(undefined, "Selected layer"); - this.geolocation = new GeoLocationHandler( - geolocationState, - this.selectedElement, - this.mapProperties, - this.userRelatedState.gpsLocationHistoryRetentionTime - ) - - this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location) - - const self = this - this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id) - - { - const overlayLayerStates = new Map }>() - for (const rasterInfo of this.layout.tileLayerSources) { - const isDisplayed = QueryParameters.GetBooleanQueryParameter( - "overlay-" + rasterInfo.id, - rasterInfo.defaultState ?? true, - "Wether or not overlayer layer " + rasterInfo.id + " is shown" - ) - const state = { isDisplayed } - overlayLayerStates.set(rasterInfo.id, state) - new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state) - } - this.overlayLayerStates = overlayLayerStates + this.selectedElementAndLayer = this.selectedElement.mapD( + (feature) => { + const layer = this.selectedLayer.data; + if (!layer) { + return undefined; } + return { layer, feature }; + }, + [this.selectedLayer] + ); - { - /* Setup the layout source - * A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too - */ + this.geolocation = new GeoLocationHandler( + geolocationState, + this.selectedElement, + this.mapProperties, + this.userRelatedState.gpsLocationHistoryRetentionTime + ); - if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) { - this.fullNodeDatabase = new FullNodeDatabaseSource() - } + this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location); - const layoutSource = new LayoutSource( - layout.layers, - this.featureSwitches, - this.mapProperties, - this.osmConnection.Backend(), - (id) => self.layerState.filteredLayers.get(id).isDisplayed, - this.fullNodeDatabase - ) + const self = this; + this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id); - this.indexedFeatures = layoutSource + { + const overlayLayerStates = new Map }>(); + for (const rasterInfo of this.layout.tileLayerSources) { + const isDisplayed = QueryParameters.GetBooleanQueryParameter( + "overlay-" + rasterInfo.id, + rasterInfo.defaultState ?? true, + "Wether or not overlayer layer " + rasterInfo.id + " is shown" + ); + const state = { isDisplayed }; + overlayLayerStates.set(rasterInfo.id, state); + new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state); + } + this.overlayLayerStates = overlayLayerStates; + } - const empty = [] - let currentViewIndex = 0 - this.currentView = new StaticFeatureSource( - this.mapProperties.bounds.map((bbox) => { - if (!bbox) { - return empty - } - currentViewIndex++ - return [ - bbox.asGeoJson({ - zoom: this.mapProperties.zoom.data, - ...this.mapProperties.location.data, - id: "current_view", - }), - ] - }) - ) - this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) - this.dataIsLoading = layoutSource.isLoading + { + /* Setup the layout source + * A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too + */ - const indexedElements = this.indexedFeatures - this.featureProperties = new FeaturePropertiesStore(indexedElements) - this.changes = new Changes( - { - dryRun: this.featureSwitches.featureSwitchIsTesting, - allElements: indexedElements, - featurePropertiesStore: this.featureProperties, - osmConnection: this.osmConnection, - historicalUserLocations: this.geolocation.historicalUserLocations, - }, - layout?.isLeftRightSensitive() ?? false - ) - this.historicalUserLocations = this.geolocation.historicalUserLocations - this.newFeatures = new NewGeometryFromChangesFeatureSource( - this.changes, - indexedElements, - this.featureProperties - ) - layoutSource.addSource(this.newFeatures) + if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) { + this.fullNodeDatabase = new FullNodeDatabaseSource(); + } - const perLayer = new PerLayerFeatureSourceSplitter( - Array.from(this.layerState.filteredLayers.values()).filter( - (l) => l.layerDef?.source !== null - ), - new ChangeGeometryApplicator(this.indexedFeatures, this.changes), - { - constructStore: (features, layer) => - new GeoIndexedStoreForLayer(features, layer), - handleLeftovers: (features) => { - console.warn( - "Got ", - features.length, - "leftover features, such as", - features[0].properties - ) - }, - } - ) - this.perLayer = perLayer.perLayer - } - this.perLayer.forEach((fs) => { - new SaveFeatureSourceToLocalStorage( - this.osmConnection.Backend(), - fs.layer.layerDef.id, - 15, - fs, - this.featureProperties, - fs.layer.layerDef.maxAgeOfCache - ) - }) + const layoutSource = new LayoutSource( + layout.layers, + this.featureSwitches, + this.mapProperties, + this.osmConnection.Backend(), + (id) => self.layerState.filteredLayers.get(id).isDisplayed, + this.fullNodeDatabase + ); - this.floors = this.featuresInView.features.stabilized(500).map((features) => { - if (!features) { - return [] - } - const floors = new Set() - for (const feature of features) { - let level = feature.properties["_level"] - if (level) { - const levels = level.split(";") - for (const l of levels) { - floors.add(l) - } - } else { - floors.add("0") // '0' is the default and is thus _always_ present - } - } - const sorted = Array.from(floors) - // Sort alphabetically first, to deal with floor "A", "B" and "C" - sorted.sort() - sorted.sort((a, b) => { - // We use the laxer 'parseInt' to deal with floor '1A' - const na = parseInt(a) - const nb = parseInt(b) - if (isNaN(na) || isNaN(nb)) { - return 0 - } - return na - nb + this.indexedFeatures = layoutSource; + + let currentViewIndex = 0 + const empty = []; + this.currentView = new StaticFeatureSource( + this.mapProperties.bounds.map((bbox) => { + if (!bbox) { + return empty; + } + currentViewIndex++; + return [ + bbox.asGeoJson({ + zoom: this.mapProperties.zoom.data, + ...this.mapProperties.location.data, + id: "current_view_"+currentViewIndex }) - sorted.reverse(/* new list, no side-effects */) - return sorted + ]; }) + ); + this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds); + this.dataIsLoading = layoutSource.isLoading; - const lastClick = (this.lastClickObject = new LastClickFeatureSource( - this.mapProperties.lastClickLocation, - this.layout - )) + const indexedElements = this.indexedFeatures; + this.featureProperties = new FeaturePropertiesStore(indexedElements); + this.changes = new Changes( + { + dryRun: this.featureSwitches.featureSwitchIsTesting, + allElements: indexedElements, + featurePropertiesStore: this.featureProperties, + osmConnection: this.osmConnection, + historicalUserLocations: this.geolocation.historicalUserLocations + }, + layout?.isLeftRightSensitive() ?? false + ); + this.historicalUserLocations = this.geolocation.historicalUserLocations; + this.newFeatures = new NewGeometryFromChangesFeatureSource( + this.changes, + indexedElements, + this.featureProperties + ); + layoutSource.addSource(this.newFeatures); - this.osmObjectDownloader = new OsmObjectDownloader( - this.osmConnection.Backend(), - this.changes - ) - - this.perLayerFiltered = this.showNormalDataOn(this.map) - - this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView - this.imageUploadManager = new ImageUploadManager( - layout, - Imgur.singleton, - this.featureProperties, - this.osmConnection, - this.changes - ) - - this.initActors() - this.addLastClick(lastClick) - this.drawSpecialLayers() - this.initHotkeys() - this.miscSetup() - if (!Utils.runningFromConsole) { - console.log("State setup completed", this) + const perLayer = new PerLayerFeatureSourceSplitter( + Array.from(this.layerState.filteredLayers.values()).filter( + (l) => l.layerDef?.source !== null + ), + new ChangeGeometryApplicator(this.indexedFeatures, this.changes), + { + constructStore: (features, layer) => + new GeoIndexedStoreForLayer(features, layer), + handleLeftovers: (features) => { + console.warn( + "Got ", + features.length, + "leftover features, such as", + features[0].properties + ); + } } + ); + this.perLayer = perLayer.perLayer; } + this.perLayer.forEach((fs) => { + new SaveFeatureSourceToLocalStorage( + this.osmConnection.Backend(), + fs.layer.layerDef.id, + 15, + fs, + this.featureProperties, + fs.layer.layerDef.maxAgeOfCache + ); + }); + this.newPointDialog = this.layerState.filteredLayers.get("last_click"); - public showNormalDataOn(map: Store): ReadonlyMap { - const filteringFeatureSource = new Map() - this.perLayer.forEach((fs, layerName) => { - const doShowLayer = this.mapProperties.zoom.map( - (z) => - (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), - [fs.layer.isDisplayed] - ) - - if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) { - /* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined) - * - * This means that we don't have to filter it, nor do we have to display it - * - * Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden. - * However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer! - * */ - return - } - const filtered = new FilteringFeatureSource( - fs.layer, - fs, - (id) => this.featureProperties.getStore(id), - this.layerState.globalFilters - ) - filteringFeatureSource.set(layerName, filtered) - - new ShowDataLayer(map, { - layer: fs.layer.layerDef, - features: filtered, - doShowLayer, - selectedElement: this.selectedElement, - selectedLayer: this.selectedLayer, - fetchStore: (id) => this.featureProperties.getStore(id), - }) - }) - return filteringFeatureSource - } - - /** - * Various small methods that need to be called - */ - private miscSetup() { - this.userRelatedState.markLayoutAsVisited(this.layout) - - this.selectedElement.addCallbackAndRunD((feature) => { - // As soon as we have a selected element, we clear the selected element - // This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature - // The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear - if (feature.properties.id === "last_click") { - return - } - this.lastClickObject.features.setData([]) - }) - - if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) { - Utils.LoadCustomCss(this.layout.customCss) + this.floors = this.featuresInView.features.stabilized(500).map((features) => { + if (!features) { + return []; + } + const floors = new Set(); + for (const feature of features) { + let level = feature.properties["_level"]; + if (level) { + const levels = level.split(";"); + for (const l of levels) { + floors.add(l); + } + } else { + floors.add("0"); // '0' is the default and is thus _always_ present } + } + const sorted = Array.from(floors); + // Sort alphabetically first, to deal with floor "A", "B" and "C" + sorted.sort(); + sorted.sort((a, b) => { + // We use the laxer 'parseInt' to deal with floor '1A' + const na = parseInt(a); + const nb = parseInt(b); + if (isNaN(na) || isNaN(nb)) { + return 0; + } + return na - nb; + }); + sorted.reverse(/* new list, no side-effects */); + return sorted; + }); + + const lastClick = (this.lastClickObject = new LastClickFeatureSource( + this.mapProperties.lastClickLocation, + this.layout + )); + + this.osmObjectDownloader = new OsmObjectDownloader( + this.osmConnection.Backend(), + this.changes + ); + + this.perLayerFiltered = this.showNormalDataOn(this.map); + + this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView; + this.imageUploadManager = new ImageUploadManager( + layout, + Imgur.singleton, + this.featureProperties, + this.osmConnection, + this.changes + ); + + this.initActors(); + this.drawSpecialLayers(); + this.initHotkeys(); + this.miscSetup(); + if (!Utils.runningFromConsole) { + console.log("State setup completed", this); } + } - private initHotkeys() { - Hotkeys.RegisterHotkey( - { nomod: "Escape", onUp: true }, - Translations.t.hotkeyDocumentation.closeSidebar, - () => { - this.selectedElement.setData(undefined) - this.guistate.closeAll() - } - ) + public showNormalDataOn(map: Store): ReadonlyMap { + const filteringFeatureSource = new Map(); + this.perLayer.forEach((fs, layerName) => { + const doShowLayer = this.mapProperties.zoom.map( + (z) => + (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), + [fs.layer.isDisplayed] + ); + if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) { + /* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined) + * + * This means that we don't have to filter it, nor do we have to display it + * + * Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden. + * However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer! + * */ + return; + } + const filtered = new FilteringFeatureSource( + fs.layer, + fs, + (id) => this.featureProperties.getStore(id), + this.layerState.globalFilters + ); + filteringFeatureSource.set(layerName, filtered); + + new ShowDataLayer(map, { + layer: fs.layer.layerDef, + features: filtered, + doShowLayer, + selectedElement: this.selectedElement, + selectedLayer: this.selectedLayer, + fetchStore: (id) => this.featureProperties.getStore(id) + }); + }); + return filteringFeatureSource; + } + + /** + * Various small methods that need to be called + */ + private miscSetup() { + this.userRelatedState.markLayoutAsVisited(this.layout); + + this.selectedElement.addCallbackAndRunD((feature) => { + // As soon as we have a selected element, we clear the selected element + // This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature + // The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear + if (feature.properties.id === "last_click") { + return; + } + this.lastClickObject.features.setData([]); + }); + + if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) { + Utils.LoadCustomCss(this.layout.customCss); + } + } + + private initHotkeys() { + Hotkeys.RegisterHotkey( + { nomod: "Escape", onUp: true }, + Translations.t.hotkeyDocumentation.closeSidebar, + () => { + this.selectedElement.setData(undefined); + this.guistate.closeAll(); + } + ); + + this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun(enable => { + if(!enable){ + return + } Hotkeys.RegisterHotkey( { - nomod: "b", + nomod: "b" }, Translations.t.hotkeyDocumentation.openLayersPanel, () => { if (this.featureSwitches.featureSwitchFilter.data) { - this.guistate.openFilterView() + this.guistate.openFilterView(); } } - ) - + ); Hotkeys.RegisterHotkey( { shift: "O" }, Translations.t.hotkeyDocumentation.selectMapnik, () => { - this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto) + this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto); } - ) + ); const setLayerCategory = (category: EliCategory) => { - const available = this.availableLayers.data - const current = this.mapProperties.rasterLayer + const available = this.availableLayers.data; + const current = this.mapProperties.rasterLayer; const best = RasterLayerUtils.SelectBestLayerAccordingTo( available, category, current.data - ) - console.log("Best layer for category", category, "is", best.properties.id) - current.setData(best) - } + ); + console.log("Best layer for category", category, "is", best.properties.id); + current.setData(best); + }; Hotkeys.RegisterHotkey( { nomod: "O" }, Translations.t.hotkeyDocumentation.selectOsmbasedmap, () => setLayerCategory("osmbasedmap") - ) + ); Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () => setLayerCategory("map") - ) + ); Hotkeys.RegisterHotkey( { nomod: "P" }, Translations.t.hotkeyDocumentation.selectAerial, () => setLayerCategory("photo") - ) - } + ); + return true + }) - private addLastClick(last_click: LastClickFeatureSource) { - // The last_click gets a _very_ special treatment as it interacts with various parts - const last_click_layer = this.layerState.filteredLayers.get("last_click") - this.featureProperties.trackFeatureSource(last_click) - this.indexedFeatures.addSource(last_click) - last_click.features.addCallbackAndRunD((features) => { - if (this.selectedLayer.data?.id === "last_click") { - // The last-click location moved, but we have selected the last click of the previous location - // So, we update _after_ clearing the selection to make sure no stray data is sticking around - this.selectedElement.setData(undefined) - this.selectedElement.setData(features[0]) - } - }) - new ShowDataLayer(this.map, { - features: new FilteringFeatureSource(last_click_layer, last_click), - doShowLayer: this.featureSwitches.featureSwitchEnableLogin, - layer: last_click_layer.layerDef, - selectedElement: this.selectedElement, - selectedLayer: this.selectedLayer, - onClick: (feature: Feature) => { - if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) { - this.map.data.flyTo({ - zoom: Constants.minZoomLevelToAddNewPoint, - center: this.mapProperties.lastClickLocation.data, - }) - return - } - // We first clear the selection to make sure no weird state is around - this.selectedLayer.setData(undefined) - this.selectedElement.setData(undefined) + } - this.selectedElement.setData(feature) - this.selectedLayer.setData(last_click_layer.layerDef) - }, - }) - } + private addLastClick(last_click: LastClickFeatureSource) { + // The last_click gets a _very_ special treatment as it interacts with various parts + this.featureProperties.trackFeatureSource(last_click); + this.indexedFeatures.addSource(last_click); + + last_click.features.addCallbackAndRunD((features) => { + if (this.selectedLayer.data?.id === "last_click") { + // The last-click location moved, but we have selected the last click of the previous location + // So, we update _after_ clearing the selection to make sure no stray data is sticking around + this.selectedElement.setData(undefined); + this.selectedElement.setData(features[0]); + } + }); + + new ShowDataLayer(this.map, { + features: new FilteringFeatureSource(this.newPointDialog, last_click), + doShowLayer: this.featureSwitches.featureSwitchEnableLogin, + layer: this.newPointDialog.layerDef, + selectedElement: this.selectedElement, + selectedLayer: this.selectedLayer, + onClick: (feature: Feature) => { + if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) { + this.map.data.flyTo({ + zoom: Constants.minZoomLevelToAddNewPoint, + center: this.mapProperties.lastClickLocation.data + }); + return; + } + // We first clear the selection to make sure no weird state is around + this.selectedLayer.setData(undefined); + this.selectedElement.setData(undefined); + + this.selectedElement.setData(feature); + this.selectedLayer.setData(this.newPointDialog.layerDef); + } + }); + } + + public openNewDialog() { + this.selectedLayer.setData(undefined); + this.selectedElement.setData(undefined); + + const { lon, lat } = this.mapProperties.location.data; + const feature = this.lastClickObject.createFeature(lon, lat) + this.featureProperties.trackFeature(feature) + this.selectedElement.setData(feature); + this.selectedLayer.setData(this.newPointDialog.layerDef); + } + + /** + * Add the special layers to the map + */ + private drawSpecialLayers() { + type AddedByDefaultTypes = (typeof Constants.added_by_default)[number] + const empty = []; /** - * Add the special layers to the map + * A listing which maps the layerId onto the featureSource */ - private drawSpecialLayers() { - type AddedByDefaultTypes = (typeof Constants.added_by_default)[number] - const empty = [] - /** - * A listing which maps the layerId onto the featureSource - */ - const specialLayers: Record< - Exclude | "current_view", - FeatureSource - > = { - home_location: this.userRelatedState.homeLocation, - gps_location: this.geolocation.currentUserLocation, - gps_location_history: this.geolocation.historicalUserLocations, - gps_track: this.geolocation.historicalUserLocationsTrack, - selected_element: new StaticFeatureSource( - this.selectedElement.map((f) => (f === undefined ? empty : [f])) - ), - range: new StaticFeatureSource( - this.mapProperties.maxbounds.map((bbox) => - bbox === undefined ? empty : [bbox.asGeoJson({ id: "range" })] - ) - ), - current_view: this.currentView, - } - if (this.layout?.lockLocation) { - const bbox = new BBox(this.layout.lockLocation) - this.mapProperties.maxbounds.setData(bbox) - ShowDataLayer.showRange( - this.map, - new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]), - this.featureSwitches.featureSwitchIsTesting - ) - } - const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view") - if (currentViewLayer?.tagRenderings?.length > 0) { - const params = MetaTagging.createExtraFuncParams(this) - this.featureProperties.trackFeatureSource(specialLayers.current_view) - specialLayers.current_view.features.addCallbackAndRunD((features) => { - MetaTagging.addMetatags( - features, - params, - currentViewLayer, - this.layout, - this.osmObjectDownloader, - this.featureProperties - ) - }) - } - - const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range") - - const rangeIsDisplayed = rangeFLayer?.isDisplayed - - if ( - !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef)) - ) { - rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true) - } - - this.layerState.filteredLayers.forEach((flayer) => { - const id = flayer.layerDef.id - const features: FeatureSource = specialLayers[id] - if (features === undefined) { - return - } - - this.featureProperties.trackFeatureSource(features) - new ShowDataLayer(this.map, { - features, - doShowLayer: flayer.isDisplayed, - layer: flayer.layerDef, - selectedElement: this.selectedElement, - selectedLayer: this.selectedLayer, - }) - }) - } - - /** - * Setup various services for which no reference are needed - */ - private initActors() { - // Unselect the selected element if it is panned out of view - this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => { - const selected = this.selectedElement.data - if (selected === undefined) { - return - } - const bbox = BBox.get(selected) - if (!bbox.overlapsWith(bounds)) { - this.selectedElement.setData(undefined) - } - }) - - this.selectedElement.addCallback((selected) => { - if (selected === undefined) { - // We did _unselect_ an item - we always remove the lastclick-object - this.lastClickObject.features.setData([]) - this.selectedLayer.setData(undefined) - } - }) - new ThemeViewStateHashActor(this) - new MetaTagging(this) - new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this) - new ChangeToElementsActor(this.changes, this.featureProperties) - new PendingChangesUploader(this.changes, this.selectedElement) - new SelectedElementTagsUpdater(this) - new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers) - new PreferredRasterLayerSelector( - this.mapProperties.rasterLayer, - this.availableLayers, - this.featureSwitches.backgroundLayerId, - this.userRelatedState.preferredBackgroundLayer + const specialLayers: Record< + Exclude | "current_view", + FeatureSource + > = { + home_location: this.userRelatedState.homeLocation, + gps_location: this.geolocation.currentUserLocation, + gps_location_history: this.geolocation.historicalUserLocations, + gps_track: this.geolocation.historicalUserLocationsTrack, + selected_element: new StaticFeatureSource( + this.selectedElement.map((f) => (f === undefined ? empty : [f])) + ), + range: new StaticFeatureSource( + this.mapProperties.maxbounds.map((bbox) => + bbox === undefined ? empty : [bbox.asGeoJson({ id: "range" })] ) + ), + current_view: this.currentView + }; + if (this.layout?.lockLocation) { + const bbox = new BBox(this.layout.lockLocation); + this.mapProperties.maxbounds.setData(bbox); + ShowDataLayer.showRange( + this.map, + new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]), + this.featureSwitches.featureSwitchIsTesting + ); } + const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view"); + if (currentViewLayer?.tagRenderings?.length > 0) { + const params = MetaTagging.createExtraFuncParams(this); + this.featureProperties.trackFeatureSource(specialLayers.current_view); + specialLayers.current_view.features.addCallbackAndRunD((features) => { + MetaTagging.addMetatags( + features, + params, + currentViewLayer, + this.layout, + this.osmObjectDownloader, + this.featureProperties + ); + }); + } + + const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range"); + + const rangeIsDisplayed = rangeFLayer?.isDisplayed; + + if ( + !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef)) + ) { + rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true); + } + + this.layerState.filteredLayers.forEach((flayer) => { + const id = flayer.layerDef.id; + const features: FeatureSource = specialLayers[id]; + if (features === undefined) { + return; + } + + this.featureProperties.trackFeatureSource(features); + new ShowDataLayer(this.map, { + features, + doShowLayer: flayer.isDisplayed, + layer: flayer.layerDef, + selectedElement: this.selectedElement, + selectedLayer: this.selectedLayer + }); + }); + } + + /** + * Setup various services for which no reference are needed + */ + private initActors() { + // Unselect the selected element if it is panned out of view + this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => { + const selected = this.selectedElement.data; + if (selected === undefined) { + return; + } + const bbox = BBox.get(selected); + if (!bbox.overlapsWith(bounds)) { + this.selectedElement.setData(undefined); + } + }); + + this.selectedElement.addCallback((selected) => { + if (selected === undefined) { + // We did _unselect_ an item - we always remove the lastclick-object + this.lastClickObject.features.setData([]); + this.selectedLayer.setData(undefined); + } + }); + new ThemeViewStateHashActor(this); + new MetaTagging(this); + new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this); + new ChangeToElementsActor(this.changes, this.featureProperties); + new PendingChangesUploader(this.changes, this.selectedElement); + new SelectedElementTagsUpdater(this); + new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers); + new PreferredRasterLayerSelector( + this.mapProperties.rasterLayer, + this.availableLayers, + this.featureSwitches.backgroundLayerId, + this.userRelatedState.preferredBackgroundLayer + ); + } } diff --git a/src/UI/Base/FileSelector.svelte b/src/UI/Base/FileSelector.svelte index 3a63f86539..9bd5b3f8e6 100644 --- a/src/UI/Base/FileSelector.svelte +++ b/src/UI/Base/FileSelector.svelte @@ -12,7 +12,28 @@ let id = Math.random() * 1000000000 + "" -
+ { + drawAttention = false + dispatcher("submit", inputElement.files) + }} + on:dragend={() => { + console.log("Drag end") + drawAttention = false + }} + on:dragenter|preventDefault|stopPropagation={(e) => { + console.log("Dragging enter") + drawAttention = true + e.dataTransfer.drop = "copy" + }} + on:dragstart={() => { + console.log("DragStart") + drawAttention = false + }} + on:drop|preventDefault|stopPropagation={(e) => { + console.log("Got a 'drop'") + drawAttention = false + dispatcher("submit", e.dataTransfer.files) + }}> @@ -23,26 +44,7 @@ id={"fileinput" + id} {multiple} name="file-input" - on:change|preventDefault={() => { - drawAttention = false - dispatcher("submit", inputElement.files) - }} - on:dragend={() => { - drawAttention = false - }} - on:dragover|preventDefault|stopPropagation={(e) => { - console.log("Dragging over!") - drawAttention = true - e.dataTransfer.drop = "copy" - }} - on:dragstart={() => { - drawAttention = false - }} - on:drop|preventDefault|stopPropagation={(e) => { - console.log("Got a 'drop'") - drawAttention = false - dispatcher("submit", e.dataTransfer.files) - }} + type="file" />
diff --git a/src/UI/Base/FloatOver.svelte b/src/UI/Base/FloatOver.svelte index 701b19b397..b86baf78d3 100644 --- a/src/UI/Base/FloatOver.svelte +++ b/src/UI/Base/FloatOver.svelte @@ -11,8 +11,9 @@
{dispatch("close")}} > -
+
{}}>
diff --git a/src/UI/Base/Hotkeys.ts b/src/UI/Base/Hotkeys.ts index 6b4f073650..e40d921664 100644 --- a/src/UI/Base/Hotkeys.ts +++ b/src/UI/Base/Hotkeys.ts @@ -22,7 +22,14 @@ export default class Hotkeys { }[] >([]) - private static textElementSelected(): boolean { + private static textElementSelected(event: KeyboardEvent): boolean { + if(event.ctrlKey || event.altKey){ + // This is an event with a modifier-key, lets not ignore it + return false + } + if(event.key === "Escape"){ + return false // Another not-printable character that should not be ignored + } return ["input", "textarea"].includes(document?.activeElement?.tagName?.toLowerCase()) } public static RegisterHotkey( @@ -68,7 +75,7 @@ export default class Hotkeys { }) } else if (key["shift"] !== undefined) { document.addEventListener(type, function (event) { - if (Hotkeys.textElementSelected()) { + if (Hotkeys.textElementSelected(event)) { // A text element is selected, we don't do anything special return } @@ -86,7 +93,7 @@ export default class Hotkeys { }) } else if (key["nomod"] !== undefined) { document.addEventListener(type, function (event) { - if (Hotkeys.textElementSelected()) { + if (Hotkeys.textElementSelected(event)) { // A text element is selected, we don't do anything special return } diff --git a/src/UI/Base/TabbedGroup.svelte b/src/UI/Base/TabbedGroup.svelte index 7e33100e70..35d9a865b1 100644 --- a/src/UI/Base/TabbedGroup.svelte +++ b/src/UI/Base/TabbedGroup.svelte @@ -1,20 +1,32 @@
@@ -29,41 +41,31 @@ >
- {#if $$slots.title1} - twJoin("tab", selected && "primary")}> -
- Tab 0 -
-
- {/if} - {#if $$slots.title1} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} - {#if $$slots.title2} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} - {#if $$slots.title3} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} - {#if $$slots.title4} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} + twJoin("tab", selected && "primary", !$condition0 && "hidden")}> +
+ Tab 0 +
+
+ twJoin("tab", selected && "primary", !$condition1 && "hidden")}> +
+ +
+
+ twJoin("tab", selected && "primary", !$condition2 && "hidden")}> +
+ +
+
+ twJoin("tab", selected && "primary", !$condition3 && "hidden")}> +
+ +
+
+ twJoin("tab", selected && "primary", !$condition4 && "hidden")}> +
+ +
+
@@ -75,16 +77,24 @@ - + +
+ - + +
+ - + +
+ - + +
+
@@ -92,44 +102,44 @@
diff --git a/src/UI/BigComponents/ThemeIntroPanel.svelte b/src/UI/BigComponents/ThemeIntroPanel.svelte index a109862d57..a65c28906b 100644 --- a/src/UI/BigComponents/ThemeIntroPanel.svelte +++ b/src/UI/BigComponents/ThemeIntroPanel.svelte @@ -1,47 +1,48 @@
@@ -62,61 +63,67 @@
-
- {#if $currentGPSLocation !== undefined || $geopermission === "prompt"} - - - {:else if $geopermission === "requested"} - - {:else if $geopermission === "denied"} - - {:else} - - {/if} -
-
- state.guistate.themeIsOpened.setData(false)} - on:searchIsValid={(isValid) => { +
+ + {#if $currentGPSLocation !== undefined || $geopermission === "prompt"} + + + {:else if $geopermission === "requested"} + + {:else if $geopermission === "denied"} + + {:else} + + {/if} + + + + +
+
+ state.guistate.themeIsOpened.setData(false)} + on:searchIsValid={(isValid) => { searchEnabled = isValid }} - perLayer={state.perLayer} - {selectedElement} - {selectedLayer} - {triggerSearch} - /> + perLayer={state.perLayer} + {selectedElement} + {selectedLayer} + {triggerSearch} + /> +
+
- -
+
diff --git a/src/UI/Input/Checkboxes.ts b/src/UI/Input/Checkboxes.ts deleted file mode 100644 index 222d39fb1f..0000000000 --- a/src/UI/Input/Checkboxes.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { InputElement } from "./InputElement" -import { UIEventSource } from "../../Logic/UIEventSource" -import { Utils } from "../../Utils" -import BaseUIElement from "../BaseUIElement" -import InputElementMap from "./InputElementMap" -import Translations from "../i18n/Translations" - -/** - * @deprecated - */ -export class CheckBox extends InputElementMap { - constructor(el: BaseUIElement | string, defaultValue?: boolean) { - super( - new CheckBoxes([Translations.W(el)]), - (x0, x1) => x0 === x1, - (t) => t.length > 0, - (x) => (x ? [0] : []) - ) - if (defaultValue !== undefined) { - this.GetValue().setData(defaultValue) - } - } -} - -/** - * A list of individual checkboxes - * The value will contain the indexes of the selected checkboxes - */ -export default class CheckBoxes extends InputElement { - private static _nextId = 0 - private readonly value: UIEventSource - private readonly _elements: BaseUIElement[] - - constructor(elements: BaseUIElement[], value = new UIEventSource([])) { - super() - this.value = value - this._elements = Utils.NoNull(elements) - this.SetClass("flex flex-col") - } - - IsValid(ts: number[]): boolean { - return ts !== undefined - } - - GetValue(): UIEventSource { - return this.value - } - - protected InnerConstructElement(): HTMLElement { - const formTag = document.createElement("form") - - const value = this.value - const elements = this._elements - - for (let i = 0; i < elements.length; i++) { - let inputI = elements[i] - const input = document.createElement("input") - const id = CheckBoxes._nextId - CheckBoxes._nextId++ - input.id = "checkbox" + id - - input.type = "checkbox" - input.classList.add("p-1", "cursor-pointer", "m-3", "pl-3", "mr-0") - - const label = document.createElement("label") - label.htmlFor = input.id - label.appendChild(input) - label.appendChild(inputI.ConstructElement()) - label.classList.add("block", "w-full", "p-2", "cursor-pointer") - - formTag.appendChild(label) - - value.addCallbackAndRunD((selectedValues) => { - input.checked = selectedValues.indexOf(i) >= 0 - - if (input.checked) { - label.classList.add("checked") - } else { - label.classList.remove("checked") - } - }) - - input.onchange = () => { - // Index = index in the list of already checked items - const index = value.data.indexOf(i) - if (input.checked && index < 0) { - value.data.push(i) - value.ping() - } else if (index >= 0) { - value.data.splice(index, 1) - value.ping() - } - } - } - - return formTag - } -} diff --git a/src/UI/Input/InputElementMap.ts b/src/UI/Input/InputElementMap.ts deleted file mode 100644 index 4e49c38b51..0000000000 --- a/src/UI/Input/InputElementMap.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { InputElement } from "./InputElement" -import { Store, UIEventSource } from "../../Logic/UIEventSource" - -/** - * @deprecated - */ -export default class InputElementMap extends InputElement { - private readonly _inputElement: InputElement - private isSame: (x0: X, x1: X) => boolean - private readonly fromX: (x: X) => T - private readonly toX: (t: T) => X - private readonly _value: UIEventSource - - constructor( - inputElement: InputElement, - isSame: (x0: X, x1: X) => boolean, - toX: (t: T) => X, - fromX: (x: X) => T, - extraSources: Store[] = [] - ) { - super() - this.isSame = isSame - this.fromX = fromX - this.toX = toX - this._inputElement = inputElement - const self = this - this._value = inputElement.GetValue().sync( - (t) => { - const newX = toX(t) - const currentX = self.GetValue()?.data - if (isSame(currentX, newX)) { - return currentX - } - return newX - }, - extraSources, - (x) => { - return fromX(x) - } - ) - } - - GetValue(): UIEventSource { - return this._value - } - - IsValid(x: X): boolean { - if (x === undefined) { - return false - } - const t = this.fromX(x) - if (t === undefined) { - return false - } - return this._inputElement.IsValid(t) - } - - protected InnerConstructElement(): HTMLElement { - return this._inputElement.ConstructElement() - } -} diff --git a/src/UI/LanguagePicker.ts b/src/UI/LanguagePicker.ts index 497e0024b5..350822a1c8 100644 --- a/src/UI/LanguagePicker.ts +++ b/src/UI/LanguagePicker.ts @@ -12,7 +12,7 @@ import { QueryParameters } from "../Logic/Web/QueryParameters" export default class LanguagePicker extends Toggle { constructor(languages: string[], assignTo: UIEventSource) { - console.log("Constructing a language pîcker for languages", languages) + console.log("Constructing a language picker for languages", languages) if ( languages === undefined || languages.length <= 1 || diff --git a/src/UI/Map/MapLibreAdaptor.ts b/src/UI/Map/MapLibreAdaptor.ts index 73e7dc0d28..dadd1781bd 100644 --- a/src/UI/Map/MapLibreAdaptor.ts +++ b/src/UI/Map/MapLibreAdaptor.ts @@ -1,14 +1,14 @@ -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import type { Map as MLMap } from "maplibre-gl" -import { Map as MlMap, SourceSpecification } from "maplibre-gl" -import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" -import { Utils } from "../../Utils" -import { BBox } from "../../Logic/BBox" -import { ExportableMap, MapProperties } from "../../Models/MapProperties" -import SvelteUIElement from "../Base/SvelteUIElement" -import MaplibreMap from "./MaplibreMap.svelte" -import { RasterLayerProperties } from "../../Models/RasterLayerProperties" -import * as htmltoimage from "html-to-image" +import { Store, UIEventSource } from "../../Logic/UIEventSource"; +import type { Map as MLMap } from "maplibre-gl"; +import { Map as MlMap, SourceSpecification } from "maplibre-gl"; +import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"; +import { Utils } from "../../Utils"; +import { BBox } from "../../Logic/BBox"; +import { ExportableMap, MapProperties } from "../../Models/MapProperties"; +import SvelteUIElement from "../Base/SvelteUIElement"; +import MaplibreMap from "./MaplibreMap.svelte"; +import { RasterLayerProperties } from "../../Models/RasterLayerProperties"; +import * as htmltoimage from "html-to-image"; /** * The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties` diff --git a/src/UI/Map/ShowDataLayer.ts b/src/UI/Map/ShowDataLayer.ts index 1e6bedfd87..7373a40422 100644 --- a/src/UI/Map/ShowDataLayer.ts +++ b/src/UI/Map/ShowDataLayer.ts @@ -16,6 +16,7 @@ import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" import FilteredLayer from "../../Models/FilteredLayer" import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource" +import { CLIENT_RENEG_LIMIT } from "tls"; class PointRenderingLayer { private readonly _config: PointRenderingConfig @@ -406,13 +407,10 @@ class LineRenderingLayer { } else { const tags = this._fetchStore(id) this._listenerInstalledOn.add(id) - map.setFeatureState( - { source: this._layername, id }, - this.calculatePropsFor(feature.properties) - ) - tags.addCallbackD((properties) => { - if (!map.getLayer(this._layername)) { - return + tags.addCallbackAndRunD((properties) => { + // Make sure to use 'getSource' here, the layer names are different! + if(map.getSource(this._layername) === undefined){ + return true } map.setFeatureState( { source: this._layername, id }, diff --git a/src/UI/Popup/DeleteFlow/DeleteWizard.svelte b/src/UI/Popup/DeleteFlow/DeleteWizard.svelte index 66781eabba..a92e4de38e 100644 --- a/src/UI/Popup/DeleteFlow/DeleteWizard.svelte +++ b/src/UI/Popup/DeleteFlow/DeleteWizard.svelte @@ -92,7 +92,7 @@ {#if currentState === "start"} - - import { Store, UIEventSource } from "../Logic/UIEventSource" - import { Map as MlMap } from "maplibre-gl" - import MaplibreMap from "./Map/MaplibreMap.svelte" - import FeatureSwitchState from "../Logic/State/FeatureSwitchState" - import MapControlButton from "./Base/MapControlButton.svelte" - import ToSvelte from "./Base/ToSvelte.svelte" - import If from "./Base/If.svelte" - import { GeolocationControl } from "./BigComponents/GeolocationControl" - import type { Feature } from "geojson" - import SelectedElementView from "./BigComponents/SelectedElementView.svelte" - import LayerConfig from "../Models/ThemeConfig/LayerConfig" - import Filterview from "./BigComponents/Filterview.svelte" - import ThemeViewState from "../Models/ThemeViewState" - import type { MapProperties } from "../Models/MapProperties" - import Geosearch from "./BigComponents/Geosearch.svelte" - import Translations from "./i18n/Translations" - import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid" + import { Store, UIEventSource } from "../Logic/UIEventSource"; + import { Map as MlMap } from "maplibre-gl"; + import MaplibreMap from "./Map/MaplibreMap.svelte"; + import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; + import MapControlButton from "./Base/MapControlButton.svelte"; + import ToSvelte from "./Base/ToSvelte.svelte"; + import If from "./Base/If.svelte"; + import { GeolocationControl } from "./BigComponents/GeolocationControl"; + import type { Feature } from "geojson"; + import SelectedElementView from "./BigComponents/SelectedElementView.svelte"; + import LayerConfig from "../Models/ThemeConfig/LayerConfig"; + import Filterview from "./BigComponents/Filterview.svelte"; + import ThemeViewState from "../Models/ThemeViewState"; + import type { MapProperties } from "../Models/MapProperties"; + import Geosearch from "./BigComponents/Geosearch.svelte"; + import Translations from "./i18n/Translations"; + import { CogIcon, EyeIcon, 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" - import PrivacyPolicy from "./BigComponents/PrivacyPolicy" - import Constants from "../Models/Constants" - import TabbedGroup from "./Base/TabbedGroup.svelte" - import UserRelatedState from "../Logic/State/UserRelatedState" - import LoginToggle from "./Base/LoginToggle.svelte" - import LoginButton from "./Base/LoginButton.svelte" - import CopyrightPanel from "./BigComponents/CopyrightPanel" - import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte" - import ModalRight from "./Base/ModalRight.svelte" - import { Utils } from "../Utils" - import Hotkeys from "./Base/Hotkeys" - import { VariableUiElement } from "./Base/VariableUIElement" - import SvelteUIElement from "./Base/SvelteUIElement" - import OverlayToggle from "./BigComponents/OverlayToggle.svelte" - import LevelSelector from "./BigComponents/LevelSelector.svelte" - import ExtraLinkButton from "./BigComponents/ExtraLinkButton" - import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte" - import Svg from "../Svg" - import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte" - import type { RasterLayerPolygon } from "../Models/RasterLayers" - import { AvailableRasterLayers } from "../Models/RasterLayers" - import RasterLayerOverview from "./Map/RasterLayerOverview.svelte" - import IfHidden from "./Base/IfHidden.svelte" - import { onDestroy } from "svelte" - import { OpenJosm } from "./BigComponents/OpenJosm" - import MapillaryLink from "./BigComponents/MapillaryLink.svelte" - import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte" - import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte" - import StateIndicator from "./BigComponents/StateIndicator.svelte" - import LanguagePicker from "./LanguagePicker" - import Locale from "./i18n/Locale" - import ShareScreen from "./BigComponents/ShareScreen.svelte" + import Tr from "./Base/Tr.svelte"; + import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"; + import FloatOver from "./Base/FloatOver.svelte"; + import PrivacyPolicy from "./BigComponents/PrivacyPolicy"; + import Constants from "../Models/Constants"; + import TabbedGroup from "./Base/TabbedGroup.svelte"; + import UserRelatedState from "../Logic/State/UserRelatedState"; + import LoginToggle from "./Base/LoginToggle.svelte"; + import LoginButton from "./Base/LoginButton.svelte"; + import CopyrightPanel from "./BigComponents/CopyrightPanel"; + import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"; + import ModalRight from "./Base/ModalRight.svelte"; + import { Utils } from "../Utils"; + import Hotkeys from "./Base/Hotkeys"; + import { VariableUiElement } from "./Base/VariableUIElement"; + import SvelteUIElement from "./Base/SvelteUIElement"; + import OverlayToggle from "./BigComponents/OverlayToggle.svelte"; + import LevelSelector from "./BigComponents/LevelSelector.svelte"; + import ExtraLinkButton from "./BigComponents/ExtraLinkButton"; + import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"; + import Svg from "../Svg"; + import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"; + import type { RasterLayerPolygon } from "../Models/RasterLayers"; + import { AvailableRasterLayers } from "../Models/RasterLayers"; + import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"; + import IfHidden from "./Base/IfHidden.svelte"; + import { onDestroy } from "svelte"; + import { OpenJosm } from "./BigComponents/OpenJosm"; + import MapillaryLink from "./BigComponents/MapillaryLink.svelte"; + import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"; + import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"; + import StateIndicator from "./BigComponents/StateIndicator.svelte"; + import LanguagePicker from "./LanguagePicker"; + import Locale from "./i18n/Locale"; + import ShareScreen from "./BigComponents/ShareScreen.svelte"; - export let state: ThemeViewState - let layout = state.layout + export let state: ThemeViewState; + let layout = state.layout; - let maplibremap: UIEventSource = state.map - let selectedElement: UIEventSource = state.selectedElement - let selectedLayer: UIEventSource = state.selectedLayer + let maplibremap: UIEventSource = state.map; + let selectedElement: UIEventSource = state.selectedElement; + let selectedLayer: UIEventSource = state.selectedLayer; - const selectedElementView = selectedElement.map( - (selectedElement) => { - // Svelte doesn't properly reload some of the legacy UI-elements - // As such, we _reconstruct_ the selectedElementView every time a new feature is selected - // This is a bit wasteful, but until everything is a svelte-component, this should do the trick - const layer = selectedLayer.data - if (selectedElement === undefined || layer === undefined) { - return undefined - } + const selectedElementView = selectedElement.map( + (selectedElement) => { + // Svelte doesn't properly reload some of the legacy UI-elements + // As such, we _reconstruct_ the selectedElementView every time a new feature is selected + // This is a bit wasteful, but until everything is a svelte-component, this should do the trick + const layer = selectedLayer.data; + if (selectedElement === undefined || layer === undefined) { + return undefined; + } - if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) { - return undefined - } + if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) { + return undefined; + } - const tags = state.featureProperties.getStore(selectedElement.properties.id) - return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags }) - }, - [selectedLayer] - ) + const tags = state.featureProperties.getStore(selectedElement.properties.id); + return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags }); + }, + [selectedLayer] + ); - const selectedElementTitle = selectedElement.map( - (selectedElement) => { - // Svelte doesn't properly reload some of the legacy UI-elements - // As such, we _reconstruct_ the selectedElementView every time a new feature is selected - // This is a bit wasteful, but until everything is a svelte-component, this should do the trick - const layer = selectedLayer.data - if (selectedElement === undefined || layer === undefined) { - return undefined - } + const selectedElementTitle = selectedElement.map( + (selectedElement) => { + // Svelte doesn't properly reload some of the legacy UI-elements + // As such, we _reconstruct_ the selectedElementView every time a new feature is selected + // This is a bit wasteful, but until everything is a svelte-component, this should do the trick + const layer = selectedLayer.data; + if (selectedElement === undefined || layer === undefined) { + return undefined; + } - const tags = state.featureProperties.getStore(selectedElement.properties.id) - return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags }) - }, - [selectedLayer] - ) + const tags = state.featureProperties.getStore(selectedElement.properties.id); + return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags }); + }, + [selectedLayer] + ); - let mapproperties: MapProperties = state.mapProperties - let featureSwitches: FeatureSwitchState = state.featureSwitches - let availableLayers = state.availableLayers - let userdetails = state.osmConnection.userDetails - let currentViewLayer = layout.layers.find((l) => l.id === "current_view") - let rasterLayer: Store = state.mapProperties.rasterLayer - let rasterLayerName = - rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name - onDestroy( - rasterLayer.addCallbackAndRunD((l) => { - rasterLayerName = l.properties.name - }) - ) + let mapproperties: MapProperties = state.mapProperties; + let featureSwitches: FeatureSwitchState = state.featureSwitches; + let availableLayers = state.availableLayers; + let userdetails = state.osmConnection.userDetails; + let currentViewLayer = layout.layers.find((l) => l.id === "current_view"); + let rasterLayer: Store = state.mapProperties.rasterLayer; + let rasterLayerName = + rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name; + onDestroy( + rasterLayer.addCallbackAndRunD((l) => { + rasterLayerName = l.properties.name; + }) + );
@@ -168,18 +168,39 @@
-
- - - { +
@@ -255,9 +276,9 @@ - + state.guistate.themeIsOpened.setData(false)}> - +
- - - - + +
@@ -298,6 +317,7 @@ /> {/each}
+
@@ -314,7 +334,7 @@ new CopyrightPanel(state)} slot="content3" /> -
+
@@ -327,7 +347,7 @@ - state.guistate.backgroundLayerSelectionIsOpened.setData(false)}> + {state.guistate.backgroundLayerSelectionIsOpened.setData(false)}}>
- + state.guistate.menuIsOpened.setData(false) }> - +
-
@@ -430,12 +450,15 @@
- - + + + new OpenJosm(state.osmConnection, state.mapProperties.bounds).SetClass("w-full")} - /> - + /> + + +
diff --git a/src/Utils.ts b/src/Utils.ts index 1f196de6ef..180c727cbc 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1098,7 +1098,16 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } return { content: data } } catch (e) { - console.error("Could not parse ", data, "due to", e, "\n", e.stack) + console.error( + "Could not parse the response of", + url, + "which contains", + data, + "due to", + e, + "\n", + e.stack + ) return { error: "malformed", url } } } diff --git a/src/assets/global-raster-layers.json b/src/assets/global-raster-layers.json index c33f71e080..e23152532d 100644 --- a/src/assets/global-raster-layers.json +++ b/src/assets/global-raster-layers.json @@ -1,97 +1,169 @@ { "layers": [ { - "id": "Stamen.TonerLite", - "name": "Toner Lite (by Stamen)", - "url": "https://stamen-tiles-{switch:a,b,c,d}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png", + "name": "Americana", + "url": "https://zelonewolf.github.io/openstreetmap-americana/style.json", "category": "osmbasedmap", + "id": "americana", + "type": "vector", "attribution": { - "html": "Map tiles by Stamen Design, CC BY 3.0 — Map data {attribution.OpenStreetMap}" - }, - "min_zoom": 0, - "max_zoom": 20 + "text": "Americana", + "url": "https://github.com/ZeLonewolf/openstreetmap-americana/" + } }, { - "id": "Stamen.TonerBackground", - "name": "Toner Background - no labels (by Stamen)", + "name": "MapTiler Backdrop", + "url": "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy", "category": "osmbasedmap", - "url": "https://stamen-tiles-{switch:a,b,c,d}.a.ssl.fastly.net/toner-background/{z}/{x}/{y}.png", + "id": "maptiler.backdrop", + "type": "vector", "attribution": { - "html": "Map tiles by Stamen Design, CC BY 3.0 — Map data {attribution.OpenStreetMap}" - }, - "min_zoom": 0, - "max_zoom": 20 + "text": "Maptiler", + "url": "https://www.maptiler.com/copyright/" + } }, { - "id": "Stamen.Watercolor", - "name": "Watercolor (by Stamen)", + "name": "MapTiler Carto", + "url": "https://api.maptiler.com/maps/openstreetmap/style.json?key=GvoVAJgu46I5rZapJuAy", "category": "osmbasedmap", - "url": "https://stamen-tiles-{switch:a,b,c,d}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.png", + "id": "maptiler.carto", + "type": "vector", "attribution": { - "html": "Map tiles by Stamen Design, CC BY 3.0 — Map data {attribution.OpenStreetMap}" - }, - "min_zoom": 0, - "max_zoom": 20 + "text": "Maptiler", + "url": "https://www.maptiler.com/copyright/" + } }, { - "id": "CartoDB.Positron", - "name": "Positron (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png", + "name": "Alidade Smooth", + "url": "https://tiles-eu.stadiamaps.com/styles/alidade_smooth.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75", + "category": "osmbasedmap", + "id": "alidade.smooth", + "type": "vector", "attribution": { - "html": "CARTO" - }, - "max_zoom": 20, - "category": "osmbasedmap" + "text": "Alidade", + "url": "https://stadiamaps.com/" + } }, { - "id": "CartoDB.PositronNoLabels", - "name": "Positron - no labels (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png", + "name": "Alidade Smooth Dark", + "url": "https://tiles-eu.stadiamaps.com/styles/alidade_smooth_dark.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75", "category": "osmbasedmap", + "id": "alidade.smooth_dark", + "type": "vector", "attribution": { - "html": "CARTO" - }, - "max_zoom": 20 + "text": "Alidade/Stadiamaps", + "url": "https://stadiamaps.com/" + } }, { - "id": "CartoDB.Voyager", - "name": "Voyager (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png", + "name": "Stamen Terrain", + "url": "https://tiles-eu.stadiamaps.com/styles/stamen_terrain.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75", "category": "osmbasedmap", + "id": "stamen.terrain", + "type": "vector", "attribution": { - "html": "CARTO" - }, - "max_zoom": 20 + "text": "Stamen/Stadiamaps", + "url": "https://stadiamaps.com/" + } }, { - "id": "CartoDB.VoyagerNoLabels", - "name": "Voyager - no labels (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}.png", + "name": "Stamen Toner", + "url": "https://tiles-eu.stadiamaps.com/styles/stamen_toner.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75", "category": "osmbasedmap", + "id": "stamen.toner", + "type": "vector", "attribution": { - "html": "CARTO" - }, - "max_zoom": 20 + "text": "Stamen/Stadiamaps", + "url": "https://stadiamaps.com/" + } + }, { + "name": "Stamen Watercolor", + "url": "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg?key=14c5a900-7137-42f7-9cb9-fff0f4696f75", + "category": "osmbasedmap", + "id": "stamen.watercolor", + "attribution": { + "text": "Stamen/Stadiamaps", + "url": "https://stadiamaps.com/" + } }, { - "id": "CartoDB.DarkMatter", - "name": "Dark Matter (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png", + "url": "https://tiles-eu.stadiamaps.com/styles/osm_bright.json", + "name": "StadiaMaps OSM Bright", "category": "osmbasedmap", + "id": "stadia.bright", + "type": "vector", "attribution": { - "html": "CARTO" - }, - "max_zoom": 20 + "text": "Stadiamaps", + "url": "https://stadiamaps.com/" + } }, { - "id": "CartoDB.DarkMatterNoLabels", - "name": "Dark Matter - no labels (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png", + "url": "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Positron", "category": "osmbasedmap", + "id": "carto.positron", + "type": "vector", "attribution": { - "html": "CARTO" - }, - "max_zoom": 20 + "text": "CARTO", + "url": "https://carto.com/" + } + }, + { + "url": "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Dark Matter", + "category": "osmbasedmap", + "id": "carto.dark_matter", + "type": "vector", + "attribution": { + "text": "CARTO", + "url": "https://carto.com/" + } + }, + { + "url": "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Voyager", + "category": "osmbasedmap", + "id": "carto.voyager", + "type": "vector", + "attribution": { + "text": "CARTO", + "url": "https://carto.com/" + } + }, + + + { + "url": "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Positron (no labels)", + "category": "osmbasedmap", + "id": "carto.positron_no_labels", + "type": "vector", + "attribution": { + "text": "CARTO", + "url": "https://carto.com/" + } + }, + { + "url": "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Dark Matter (no labels)", + "category": "osmbasedmap", + "id": "carto.dark_matter_no_labels", + "type": "vector", + "attribution": { + "text": "CARTO", + "url": "https://carto.com/" + } + }, + { + "url": "https://basemaps.cartocdn.com/gl/voyager-nolabels-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Voyager (no labels)", + "category": "osmbasedmap", + "id": "carto.voyager_no_labels", + "type": "vector", + "attribution": { + "text": "CARTO", + "url": "https://carto.com/" + } } ] }