diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 14f4f261b7..b3ad288e82 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -1,5 +1,9 @@ import * as turf from '@turf/turf' import {BBox} from "./BBox"; +import togpx from "togpx" +import Constants from "../Models/Constants"; +import LayerConfig from "../Models/ThemeConfig/LayerConfig"; +import {meta} from "@turf/turf"; export class GeoOperations { @@ -436,6 +440,31 @@ export class GeoOperations { return undefined; } + public static AsGpx(feature, generatedWithLayer?: LayerConfig){ + + const metadata = {} + const tags = feature.properties + + if(generatedWithLayer !== undefined){ + + metadata["name"] = generatedWithLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt + metadata["desc"] = "Generated with MapComplete layer "+generatedWithLayer.id + if(tags._backend?.contains("openstreetmap")){ + metadata["copyright"]= "Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright" + metadata["author"] = tags["_last_edit:contributor"] + metadata["link"]= "https://www.openstreetmap.org/"+tags.id + metadata["time"] = tags["_last_edit:timestamp"] + }else{ + metadata["time"] = new Date().toISOString() + } + } + + return togpx(feature, { + creator: "MapComplete "+Constants.vNumber, + metadata + }) + } + } diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index b1f8caae73..61efd85203 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -300,5 +300,17 @@ export default class LayoutConfig { public isLeftRightSensitive() { return this.layers.some(l => l.isLeftRightSensitive()) } + + public getMatchingLayer(tags: any) : LayerConfig | undefined{ + if(tags === undefined){ + return undefined + } + for (const layer of this.layers) { + if (layer.source.osmTags.matchesProperties(tags)) { + return layer + } + } + return undefined + } } \ No newline at end of file diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index c5a825574c..bc8575ec66 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -38,10 +38,11 @@ import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"; import {And} from "../Logic/Tags/And"; import Toggle from "./Input/Toggle"; import {DefaultGuiState} from "./DefaultGuiState"; +import {GeoOperations} from "../Logic/GeoOperations"; export interface SpecialVisualization { funcName: string, - constr: ((state: State, tagSource: UIEventSource, argument: string[], guistate: DefaultGuiState) => BaseUIElement), + constr: ((state: State, tagSource: UIEventSource, argument: string[], guistate: DefaultGuiState, ) => BaseUIElement), docs: string, example?: string, args: { name: string, defaultValue?: string, doc: string }[] @@ -172,7 +173,7 @@ export default class SpecialVisualizations { // This is a list of values idList = JSON.parse(value) } - + for (const id of idList) { features.push({ @@ -425,12 +426,7 @@ export default class SpecialVisualizations { const title = state?.layoutToUse?.title?.txt ?? "MapComplete"; - let matchingLayer: LayerConfig = undefined; - for (const layer of (state?.layoutToUse?.layers ?? [])) { - if (layer.source.osmTags.matchesProperties(tagSource?.data)) { - matchingLayer = layer - } - } + let matchingLayer: LayerConfig = state?.layoutToUse?.getMatchingLayer(tagSource?.data); let name = matchingLayer?.title?.GetRenderValue(tagSource.data)?.txt ?? tagSource.data?.name ?? "POI"; if (name) { name = `${name} (${title})` @@ -603,6 +599,31 @@ export default class SpecialVisualizations { ) , undefined, state.osmConnection.isLoggedIn) } + }, + { + funcName: "export_as_gpx", + docs: "Exports the selected feature as GPX-file", + args: [], + constr: (state, tagSource, args) => { + const t = Translations.t.general.download; + + return new SubtleButton(Svg.download_ui(), + new Combine([t.downloadGpx.SetClass("font-bold text-lg"), + t.downloadGpxHelper.SetClass("subtle")]).SetClass("flex flex-col") + ).onClick(() => { + console.log("Exporting as GPX!") + const tags = tagSource.data + const feature = state.allElements.ContainingFeatures.get(tags.id) + const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags) + const gpx = GeoOperations.AsGpx(feature, matchingLayer) + const title = matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" + Utils.offerContentsAsDownloadableFile(gpx, title+"_mapcomplete_export.gpx", { + mimetype: "{gpx=application/gpx+xml}" + }) + + + }) + } } ] diff --git a/assets/layers/gps_track/gps_track.json b/assets/layers/gps_track/gps_track.json index 4462d3fc46..08a3260b00 100644 --- a/assets/layers/gps_track/gps_track.json +++ b/assets/layers/gps_track/gps_track.json @@ -6,6 +6,18 @@ "osmTags": "user:location=yes", "maxCacheAge": 0 }, + "title": { + "render": "Your travelled path" + }, + "tagRenderings": [ + { + "id": "Privacy notice", + "render": { + "en": "This is the path you've travelled since this website is opened. Don't worry - this is only visible to you and no one else. Your location data is never sent off-device." + } + }, + "export_as_gpx" + ], "name": "Your track", "mapRendering": [ { diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index 0b0d674e7d..b460e4ca62 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -2,6 +2,9 @@ "images": { "render": "{image_carousel()}{image_upload()}" }, +"export_as_gpx":{ + "render": "{export_as_gpx()}" +}, "wikipedia": { "render": "{wikipedia():max-height:25rem}", "question": { diff --git a/assets/themes/natuurpunt/natuurpunt.json b/assets/themes/natuurpunt/natuurpunt.json index 3ed14ee9ec..de0ac462c2 100644 --- a/assets/themes/natuurpunt/natuurpunt.json +++ b/assets/themes/natuurpunt/natuurpunt.json @@ -64,9 +64,13 @@ }, "minzoom": 13, "minzoomVisible": 0, - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" - } + "mapRendering": [ + { + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" + } + } + ] } }, { @@ -84,10 +88,14 @@ "isOsmCache": "duplicate" }, "minzoom": 1, - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" - }, - "presets": [] + "mapRendering": [ + { + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" + }, + "presets": [] + } + ] } }, { @@ -103,9 +111,13 @@ "isOsmCache": true }, "minzoom": 1, - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/information.svg" - } + "mapRendering": [ + { + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/information.svg" + } + } + ] } }, { @@ -122,19 +134,23 @@ "isOsmCache": true }, "minzoom": 10, - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/trail.svg", - "mappings": [ - { - "if": "wheelchair=yes", - "then": "circle:#FE6F32;./assets/themes/natuurpunt/walk_wheelchair.svg" - }, - { - "if": "pushchair=yes", - "then": "circle:#FE6F32;./assets/themes/natuurpunt/pushchair.svg" + "mapRendering": [ + { + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/trail.svg", + "mappings": [ + { + "if": "wheelchair=yes", + "then": "circle:#FE6F32;./assets/themes/natuurpunt/walk_wheelchair.svg" + }, + { + "if": "pushchair=yes", + "then": "circle:#FE6F32;./assets/themes/natuurpunt/pushchair.svg" + } + ] } - ] - } + } + ] } }, { @@ -146,19 +162,23 @@ "geoJsonZoomLevel": 12, "isOsmCache": true }, - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/toilets.svg", - "mappings": [ - { - "if": "wheelchair=yes", - "then": "circle:#FE6F32;./assets/themes/natuurpunt/wheelchair.svg" - }, - { - "if": "toilets:position=urinals", - "then": "circle:#FE6F32;./assets/themes/natuurpunt/urinal.svg" + "mapRendering": [ + { + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/toilets.svg", + "mappings": [ + { + "if": "wheelchair=yes", + "then": "circle:#FE6F32;./assets/themes/natuurpunt/wheelchair.svg" + }, + { + "if": "toilets:position=urinals", + "then": "circle:#FE6F32;./assets/themes/natuurpunt/urinal.svg" + } + ] } - ] - } + } + ] } }, { @@ -170,10 +190,14 @@ "geoJsonZoomLevel": 12, "isOsmCache": true }, - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/birdhide.svg", - "mappings": null - } + "mapRendering": [ + { + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/birdhide.svg", + "mappings": null + } + } + ] } }, { @@ -185,9 +209,13 @@ "geoJsonZoomLevel": 12, "isOsmCache": true }, - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/picnic_table.svg" - } + "mapRendering": [ + { + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/picnic_table.svg" + } + } + ] } }, { @@ -199,34 +227,37 @@ "geoJsonZoomLevel": 12, "isOsmCache": true }, - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/drips.svg" - } + "mapRendering": [ + { + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/drips.svg" + } + } + ] } }, { "builtin": "parking", "override": { "minzoom": "16", - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/parking.svg", - "mappings": [ - { - "if": "amenity=bicycle_parking", - "then": "circle:#FE6F32;./assets/themes/natuurpunt/parkingbike.svg" - } - ] - }, - "iconOverlays": [ + "mapRendering": [ { - "if": "amenity=motorcycle_parking", - "then": "circle:#335D9F;./assets/themes/natuurpunt/parkingmotor.svg", - "badge": true - }, - { - "if": "capacity:disabled=yes", - "then": "circle:#335D9F;./assets/themes/natuurpunt/parkingwheels.svg", - "badge": true + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/parking.svg", + "mappings": [ + { + "if": "amenity=bicycle_parking", + "then": "circle:#FE6F32;./assets/themes/natuurpunt/parkingbike.svg" + } + ] + }, + "iconOverlays": [ + { + "if": "capacity:disabled=yes", + "then": "circle:#335D9F;./assets/themes/natuurpunt/parkingwheels.svg", + "badge": true + } + ] } ] } @@ -240,9 +271,13 @@ "geoJsonZoomLevel": 12, "isOsmCache": true }, - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/information_board.svg" - } + "mapRendering": [ + { + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/information_board.svg" + } + } + ] } }, { @@ -254,9 +289,13 @@ "geoJsonZoomLevel": 12, "isOsmCache": true }, - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/bench.svg" - } + "mapRendering": [ + { + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/bench.svg" + } + } + ] } }, { @@ -268,9 +307,13 @@ "geoJsonZoomLevel": 12, "isOsmCache": true }, - "icon": { - "render": "circle:#FE6F32;./assets/themes/natuurpunt/watermill.svg" - } + "mapRendering": [ + { + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/watermill.svg" + } + } + ] } } ], diff --git a/langs/en.json b/langs/en.json index d414af49c7..3072eb1ed9 100644 --- a/langs/en.json +++ b/langs/en.json @@ -190,6 +190,8 @@ "downloadAsPdf": "Download a PDF of the current map", "downloadAsPdfHelper": "Ideal to print the current map", "downloadGeojson": "Download visible data as GeoJSON", + "downloadGpx":"Download as GPX-file", + "downloadGpxHelper":"A GPX-file can be used with most navigation devices and applications", "exporting": "Exporting…", "downloadGeoJsonHelper": "Compatible with QGIS, ArcGIS, ESRI, …", "downloadCSV": "Download visible data as CSV", diff --git a/package-lock.json b/package-lock.json index a5662a4f7a..11c576967e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "parcel": "^1.2.4", "prompt-sync": "^4.2.0", "tailwindcss": "^2.2.15", + "togpx": "^0.5.4", "tslint": "^6.1.3", "wikibase-sdk": "^7.14.0", "wikidata-sdk": "^7.14.0" @@ -3756,6 +3757,23 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "node_modules/bops": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz", + "integrity": "sha1-CC0dVfoB5g29wuvC26N/ZZVUzzo=", + "dependencies": { + "base64-js": "0.0.2", + "to-utf8": "0.0.1" + } + }, + "node_modules/bops/node_modules/base64-js": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.2.tgz", + "integrity": "sha1-Ak8Pcq+iW3X5wO5zzU9V7Bvtl4Q=", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -8619,6 +8637,14 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jxon": { + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/jxon/-/jxon-2.0.0-beta.5.tgz", + "integrity": "sha1-O2qUEE+YAe5oL9BWZF/1Rz2bND4=", + "dependencies": { + "xmldom": "^0.1.21" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -15930,6 +15956,36 @@ "node": ">=8.0" } }, + "node_modules/to-utf8": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", + "integrity": "sha1-0Xrqcv8vujm55DYBvns/9y4ImFI=" + }, + "node_modules/togpx": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/togpx/-/togpx-0.5.4.tgz", + "integrity": "sha1-sz27BUHfBL1rpPULhtqVNCS7d3M=", + "dependencies": { + "concat-stream": "~1.0.1", + "jxon": "~2.0.0-beta.5", + "optimist": "~0.3.5", + "xmldom": "~0.1.17" + }, + "bin": { + "togpx": "togpx" + } + }, + "node_modules/togpx/node_modules/concat-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.0.1.tgz", + "integrity": "sha1-AYsYvBx9BzotyCqkhEI0GixN158=", + "engines": [ + "node >= 0.8.0" + ], + "dependencies": { + "bops": "0.0.6" + } + }, "node_modules/toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -21029,6 +21085,22 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "bops": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz", + "integrity": "sha1-CC0dVfoB5g29wuvC26N/ZZVUzzo=", + "requires": { + "base64-js": "0.0.2", + "to-utf8": "0.0.1" + }, + "dependencies": { + "base64-js": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.2.tgz", + "integrity": "sha1-Ak8Pcq+iW3X5wO5zzU9V7Bvtl4Q=" + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -24823,6 +24895,14 @@ "safe-buffer": "^5.0.1" } }, + "jxon": { + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/jxon/-/jxon-2.0.0-beta.5.tgz", + "integrity": "sha1-O2qUEE+YAe5oL9BWZF/1Rz2bND4=", + "requires": { + "xmldom": "^0.1.21" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -30870,6 +30950,32 @@ "is-number": "^7.0.0" } }, + "to-utf8": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", + "integrity": "sha1-0Xrqcv8vujm55DYBvns/9y4ImFI=" + }, + "togpx": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/togpx/-/togpx-0.5.4.tgz", + "integrity": "sha1-sz27BUHfBL1rpPULhtqVNCS7d3M=", + "requires": { + "concat-stream": "~1.0.1", + "jxon": "~2.0.0-beta.5", + "optimist": "~0.3.5", + "xmldom": "~0.1.17" + }, + "dependencies": { + "concat-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.0.1.tgz", + "integrity": "sha1-AYsYvBx9BzotyCqkhEI0GixN158=", + "requires": { + "bops": "0.0.6" + } + } + } + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", diff --git a/package.json b/package.json index 945d6e7859..58ff23009c 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "parcel": "^1.2.4", "prompt-sync": "^4.2.0", "tailwindcss": "^2.2.15", + "togpx": "^0.5.4", "tslint": "^6.1.3", "wikibase-sdk": "^7.14.0", "wikidata-sdk": "^7.14.0"