From b03fe206a17b13a83d3dfc89056b4f67d859ed95 Mon Sep 17 00:00:00 2001 From: Ismail Sunni Date: Tue, 28 Oct 2025 17:08:41 +0700 Subject: [PATCH 1/3] PB-1760: Implement GPX export to route and track. --- .../drawing/components/DrawingExporter.vue | 14 +++++++----- .../src/modules/drawing/lib/export-utils.js | 11 ++++++++-- .../src/modules/i18n/locales/de.json | 2 ++ .../src/modules/i18n/locales/en.json | 2 ++ .../src/modules/i18n/locales/fr.json | 2 ++ .../src/modules/i18n/locales/it.json | 2 ++ .../src/modules/i18n/locales/rm.json | 2 ++ .../tests/cypress/tests-e2e/drawing.cy.js | 22 +++++++++++++++---- 8 files changed, 46 insertions(+), 11 deletions(-) diff --git a/packages/mapviewer/src/modules/drawing/components/DrawingExporter.vue b/packages/mapviewer/src/modules/drawing/components/DrawingExporter.vue index c620c834f1..10833d1af7 100644 --- a/packages/mapviewer/src/modules/drawing/components/DrawingExporter.vue +++ b/packages/mapviewer/src/modules/drawing/components/DrawingExporter.vue @@ -9,12 +9,13 @@ import { downloadFile, generateFilename } from '@/utils/utils' /** @type {DropdownItem[]} */ const exportOptions = [ { id: 'KML', title: 'KML', value: 'KML' }, - { id: 'GPX', title: 'GPX', value: 'GPX' }, + { id: 'GPX-Track', title: 'gpx_track', value: 'GPX_TRACK' }, + { id: 'GPX-Route', title: 'gpx_route', value: 'GPX_ROUTE' }, ] const drawingLayer = inject('drawingLayer') -const exportSelection = ref(exportOptions[0].title) +const exportSelection = ref(exportOptions[0].value) const store = useStore() @@ -23,7 +24,7 @@ const isDrawingEmpty = computed(() => store.getters.isDrawingEmpty) const activeKmlLayer = computed(() => store.getters.activeKmlLayer) function onExportOptionSelected(dropdownItem) { - exportSelection.value = dropdownItem.title + exportSelection.value = dropdownItem.value exportDrawing() } function exportDrawing() { @@ -33,9 +34,12 @@ function exportDrawing() { } const features = drawingLayer.getSource().getFeatures() let content, fileName - if (exportSelection.value === 'GPX') { + if (exportSelection.value === 'GPX_TRACK') { fileName = generateFilename('.gpx') - content = generateGpxString(projection.value, features) + content = generateGpxString(projection.value, features, true) + } else if (exportSelection.value === 'GPX_ROUTE') { + fileName = generateFilename('.gpx') + content = generateGpxString(projection.value, features, false) } else { fileName = generateFilename('.kml') content = generateKmlString(projection.value, features, activeKmlLayer.value?.name) diff --git a/packages/mapviewer/src/modules/drawing/lib/export-utils.js b/packages/mapviewer/src/modules/drawing/lib/export-utils.js index e861fb2046..112ff280ec 100644 --- a/packages/mapviewer/src/modules/drawing/lib/export-utils.js +++ b/packages/mapviewer/src/modules/drawing/lib/export-utils.js @@ -2,7 +2,7 @@ import { WGS84 } from '@geoadmin/coordinates' import log from '@geoadmin/log' import Feature from 'ol/Feature' import { GPX } from 'ol/format' -import { LineString, Polygon } from 'ol/geom' +import { LineString, MultiLineString, Polygon } from 'ol/geom' import { Circle, Icon } from 'ol/style' import Style from 'ol/style/Style' @@ -44,9 +44,10 @@ export const DrawingState = Object.freeze({ * * @param {CoordinateSystem} projection Coordinate system of the features * @param features {Feature[]} Features (OpenLayers) to be converted to GPX format + * @param asTrack {boolean} If true, exports as track (trk/trkpt), otherwise as route (rte/rtept) * @returns {string} */ -export function generateGpxString(projection, features = []) { +export function generateGpxString(projection, features = [], asTrack = false) { const normalizedFeatures = features.map((feature) => { const clone = feature.clone() const geom = clone.getGeometry() @@ -55,6 +56,12 @@ export function generateGpxString(projection, features = []) { const coordinates = geom.getLinearRing().getCoordinates() clone.setGeometry(new LineString(coordinates)) } + // If track mode requested, convert LineString to MultiLineString + // OpenLayers outputs LineString as and MultiLineString as + if (asTrack && clone.getGeometry() instanceof LineString) { + const coords = clone.getGeometry().getCoordinates() + clone.setGeometry(new MultiLineString([coords])) + } // Set the desc attribute from description property so that it is exported to GPX in desc tag if (clone.getProperties().description) { clone.set('desc', clone.getProperties().description) diff --git a/packages/mapviewer/src/modules/i18n/locales/de.json b/packages/mapviewer/src/modules/i18n/locales/de.json index 09aaa4a7ca..1edadc9cd1 100644 --- a/packages/mapviewer/src/modules/i18n/locales/de.json +++ b/packages/mapviewer/src/modules/i18n/locales/de.json @@ -228,6 +228,8 @@ "export": "Exportieren", "export_kml": "Exportieren", "export_kml_notsupported": "Ihr Browser unterstützt die Speicherfunktion nicht. Benutzen Sie IE10, Firefox oder Chrome.", + "gpx_route": "GPX (Route)", + "gpx_track": "GPX (Track)", "external_data_tooltip": "Daten und/oder Stil eines Drittanbieters ", "external_data_warning": "Warnung: Diese Daten und/oder Stil kommen von einem Drittanbieter (--URL--). Verfügbarkeit wird durch Drittanbieter gewährleistet. Es gelten zusätzlich die Bedingungen der entsprechenden Datenherren.", "extra_large_size": "Extra Gross", diff --git a/packages/mapviewer/src/modules/i18n/locales/en.json b/packages/mapviewer/src/modules/i18n/locales/en.json index a99300874d..d992f189b4 100644 --- a/packages/mapviewer/src/modules/i18n/locales/en.json +++ b/packages/mapviewer/src/modules/i18n/locales/en.json @@ -228,6 +228,8 @@ "export": "Export", "export_kml": "Export", "export_kml_notsupported": "Your browser does not support the save function. Use IE10, Firefox or Chrome.", + "gpx_route": "GPX (Route)", + "gpx_track": "GPX (Track)", "external_data_tooltip": "Dataset and/or style provided by third party", "external_data_warning": "Warning: Third party data and/or style shown (--URL--). Availability is ensured by the third party data provider. The terms and conditions of the third party data owner do apply and have to be respected.", "extra_large_size": "Extra Large", diff --git a/packages/mapviewer/src/modules/i18n/locales/fr.json b/packages/mapviewer/src/modules/i18n/locales/fr.json index f325a93c7a..eb85c552cd 100644 --- a/packages/mapviewer/src/modules/i18n/locales/fr.json +++ b/packages/mapviewer/src/modules/i18n/locales/fr.json @@ -228,6 +228,8 @@ "export": "Exporter", "export_kml": "Exporter", "export_kml_notsupported": "Votre browser ne prend pas en charge la fonction de sauvegarde. Veuillez employer IE10, Firefox ou Chrome.", + "gpx_route": "GPX (Itinéraire)", + "gpx_track": "GPX (Tracé)", "external_data_tooltip": "Données et/ou style d’un fournisseur tiers", "external_data_warning": "Attention: Ces données et/ou styles proviennent d’un fournisseur tiers (--URL--). La disponibilité des données est assurée par le tiers. Les conditions d’utilisation du propriétaire respectif des données doivent être respectées.", "extra_large_size": "Très Grande", diff --git a/packages/mapviewer/src/modules/i18n/locales/it.json b/packages/mapviewer/src/modules/i18n/locales/it.json index 32c85fec87..c914b30dcf 100644 --- a/packages/mapviewer/src/modules/i18n/locales/it.json +++ b/packages/mapviewer/src/modules/i18n/locales/it.json @@ -228,6 +228,8 @@ "export": "Esportare", "export_kml": "Esportare", "export_kml_notsupported": "Il suobrowser non supporta la funzione Salva. Usare IE10, Firefox o Chrome.", + "gpx_route": "GPX (Percorso)", + "gpx_track": "GPX (Traccia)", "external_data_tooltip": "Dati e/o stile di un fornitore di terze parti", "external_data_warning": "Attenzione: questi dati e/o stile provengono da terze parti (--URL--). La loro disponibilità è soggetta al fornitore esterno. I termini e le condizioni dei rispettivi proprietari dei dati devono essere rispettate.", "extra_large_size": "Extra Grande", diff --git a/packages/mapviewer/src/modules/i18n/locales/rm.json b/packages/mapviewer/src/modules/i18n/locales/rm.json index e730fa5555..25fc6f5878 100644 --- a/packages/mapviewer/src/modules/i18n/locales/rm.json +++ b/packages/mapviewer/src/modules/i18n/locales/rm.json @@ -228,6 +228,8 @@ "export": "Exportar", "export_kml": "Exportar", "export_kml_notsupported": "Voss browser na sustegna betg arcunar. Duvrai IE10, Firefox u Chrome.", + "gpx_route": "GPX (Route)", + "gpx_track": "GPX (Track)", "external_data_tooltip": "Datas e/u stil da terzas partidas", "external_data_warning": "Attenziun: questas datas e/u stil derivan da partidas terzas (--URL--). La disponibladad vegn garantida tras partidas terzas. Ultra da quai valan las cundiziuns dals patruns da las datas correspundentas.", "extra_large_size": "Extra Grond", diff --git a/packages/mapviewer/tests/cypress/tests-e2e/drawing.cy.js b/packages/mapviewer/tests/cypress/tests-e2e/drawing.cy.js index acdf42a860..07025db743 100644 --- a/packages/mapviewer/tests/cypress/tests-e2e/drawing.cy.js +++ b/packages/mapviewer/tests/cypress/tests-e2e/drawing.cy.js @@ -1396,16 +1396,30 @@ describe('Drawing module tests', () => { }) cy.task('clearFolder', downloadsFolder) - cy.log('it exports a GPX if chosen in the dropdown') + cy.log('it exports a GPX Route if chosen in the dropdown') cy.get( '[data-cy="drawing-toolbox-export-button"] [data-cy="dropdown-toggle-button"]' ).click() cy.get( - '[data-cy="drawing-toolbox-export-button"] [data-cy="dropdown-item-GPX"]' + '[data-cy="drawing-toolbox-export-button"] [data-cy="dropdown-item-GPX-Route"]' ).click() checkFiles('gpx', (content) => { - // 1 (routes), for the single LINEPOLYGON - expect(content).to.match(/.*<\/rte>.*<\/gpx>/) + // 1 (route), for the single LINEPOLYGON + expect(content).to.match(/.*.*<\/gpx>/) + }) + + cy.task('clearFolder', downloadsFolder) + + cy.log('it exports a GPX Track if chosen in the dropdown') + cy.get( + '[data-cy="drawing-toolbox-export-button"] [data-cy="dropdown-toggle-button"]' + ).click() + cy.get( + '[data-cy="drawing-toolbox-export-button"] [data-cy="dropdown-item-GPX-Track"]' + ).click() + checkFiles('gpx', (content) => { + // 1 (track), for the single LINEPOLYGON + expect(content).to.match(/.*.*.*<\/trk>.*<\/gpx>/) }) cy.task('clearFolder', downloadsFolder) From 468292e457348e296c7767b982b1fbe6573c9dee Mon Sep 17 00:00:00 2001 From: Ismail Sunni Date: Tue, 28 Oct 2025 17:12:57 +0700 Subject: [PATCH 2/3] PB-1760: Better logic to convert between track or route GPX. --- .../src/modules/drawing/lib/export-utils.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/mapviewer/src/modules/drawing/lib/export-utils.js b/packages/mapviewer/src/modules/drawing/lib/export-utils.js index 112ff280ec..b8deb2b500 100644 --- a/packages/mapviewer/src/modules/drawing/lib/export-utils.js +++ b/packages/mapviewer/src/modules/drawing/lib/export-utils.js @@ -56,11 +56,20 @@ export function generateGpxString(projection, features = [], asTrack = false) { const coordinates = geom.getLinearRing().getCoordinates() clone.setGeometry(new LineString(coordinates)) } - // If track mode requested, convert LineString to MultiLineString + // Convert between LineString and MultiLineString based on export mode // OpenLayers outputs LineString as and MultiLineString as - if (asTrack && clone.getGeometry() instanceof LineString) { - const coords = clone.getGeometry().getCoordinates() - clone.setGeometry(new MultiLineString([coords])) + if (asTrack) { + // Track mode: convert LineString to MultiLineString + if (clone.getGeometry() instanceof LineString) { + const coords = clone.getGeometry().getCoordinates() + clone.setGeometry(new MultiLineString([coords])) + } + } else { + // Route mode: convert MultiLineString to LineString (take first line segment) + if (clone.getGeometry() instanceof MultiLineString) { + const coords = clone.getGeometry().getCoordinates()[0] + clone.setGeometry(new LineString(coords)) + } } // Set the desc attribute from description property so that it is exported to GPX in desc tag if (clone.getProperties().description) { From 780b9f527009c74f9b7bff7777770f8bac7bd8c3 Mon Sep 17 00:00:00 2001 From: Ismail Sunni Date: Tue, 28 Oct 2025 17:56:36 +0700 Subject: [PATCH 3/3] PB-1760: Fix lint. --- .../src/modules/infobox/components/styling/FeatureStyleEdit.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mapviewer/src/modules/infobox/components/styling/FeatureStyleEdit.vue b/packages/mapviewer/src/modules/infobox/components/styling/FeatureStyleEdit.vue index 4ec98ea59a..ebdc5fe2d2 100644 --- a/packages/mapviewer/src/modules/infobox/components/styling/FeatureStyleEdit.vue +++ b/packages/mapviewer/src/modules/infobox/components/styling/FeatureStyleEdit.vue @@ -258,8 +258,8 @@ function mediaTypes() { {{ t('modify_description') }}