diff --git a/package-lock.json b/package-lock.json index 49e028734..3aac1a975 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,6 +69,7 @@ "katex": "0.16.21", "knockout": "3.5.1", "leaflet": "1.9.4", + "leaflet-gpx": "2.1.2", "mark.js": "8.11.1", "marked": "15.0.6", "mermaid": "11.4.1", @@ -132,6 +133,7 @@ "@types/jasmine": "5.1.5", "@types/jquery": "3.5.32", "@types/jsdom": "21.1.7", + "@types/leaflet-gpx": "1.3.7", "@types/mime-types": "2.1.4", "@types/multer": "1.4.12", "@types/node": "22.12.0", @@ -3839,6 +3841,16 @@ "@types/geojson": "*" } }, + "node_modules/@types/leaflet-gpx": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/leaflet-gpx/-/leaflet-gpx-1.3.7.tgz", + "integrity": "sha512-IDshIOLZ7dUUjRiCE3DuJcAGavgUCw0xQ93dc/3YvsA6jrFc+nx8eXr0tqZIf2SaWMgqiDj/n7N24WWNh/898g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/leaflet": "*" + } + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -11431,6 +11443,12 @@ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "license": "BSD-2-Clause" }, + "node_modules/leaflet-gpx": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/leaflet-gpx/-/leaflet-gpx-2.1.2.tgz", + "integrity": "sha512-lKoEPlAWel9KXn9keg6Dmyt7gmj5IYyD8CKuxivN+77GpZr2bpKliwFvZJxLUHmNu4fICmCySyxhm5qjZuvvQg==", + "license": "BSD-2-Clause" + }, "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", diff --git a/package.json b/package.json index 4216a3d26..fa88fd4fd 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "katex": "0.16.21", "knockout": "3.5.1", "leaflet": "1.9.4", + "leaflet-gpx": "2.1.2", "mark.js": "8.11.1", "marked": "15.0.6", "mermaid": "11.4.1", @@ -174,6 +175,7 @@ "@types/jasmine": "5.1.5", "@types/jquery": "3.5.32", "@types/jsdom": "21.1.7", + "@types/leaflet-gpx": "1.3.7", "@types/mime-types": "2.1.4", "@types/multer": "1.4.12", "@types/node": "22.12.0", diff --git a/src/public/app/widgets/type_widgets/geo_map.ts b/src/public/app/widgets/type_widgets/geo_map.ts index 60166cd41..5bb060ce9 100644 --- a/src/public/app/widgets/type_widgets/geo_map.ts +++ b/src/public/app/widgets/type_widgets/geo_map.ts @@ -104,6 +104,7 @@ export default class GeoMapTypeWidget extends TypeWidget { private _state: State; private L!: Leaflet; private currentMarkerData: MarkerData; + private gpxLoaded?: boolean; static getType() { return "geoMap"; @@ -159,9 +160,7 @@ export default class GeoMapTypeWidget extends TypeWidget { } async #reloadMarkers() { - const map = this.geoMapWidget.map; - - if (!this.note || !map) { + if (!this.note) { return; } @@ -173,48 +172,80 @@ export default class GeoMapTypeWidget extends TypeWidget { // Add the new markers. this.currentMarkerData = {}; const childNotes = await this.note.getChildNotes(); - const L = this.L; for (const childNote of childNotes) { - const latLng = childNote.getAttributeValue("label", LOCATION_ATTRIBUTE); - if (!latLng) { + if (childNote.mime === "application/gpx+xml") { + this.#processNoteWithGpxTrack(childNote); continue; } - const [ lat, lng ] = latLng.split(",", 2).map((el) => parseFloat(el)); - const icon = L.divIcon({ - html: `\ - - - - ${childNote.title}`, - iconSize: [ 25, 41 ], - iconAnchor: [ 12, 41 ] - }) + const latLng = childNote.getAttributeValue("label", LOCATION_ATTRIBUTE); + if (latLng) { + this.#processNoteWithMarker(childNote, latLng); + } + } + } - const marker = L.marker(L.latLng(lat, lng), { - icon, - draggable: true, - autoPan: true, - autoPanSpeed: 5, - }) - .addTo(map) - .on("moveend", e => { - this.moveMarker(childNote.noteId, (e.target as Marker).getLatLng()); - }); + async #processNoteWithGpxTrack(note: FNote) { + if (!this.L || !this.geoMapWidget.map) { + return; + } - marker.on("contextmenu", (e) => { - openContextMenu(childNote.noteId, e.originalEvent); + if (!this.gpxLoaded) { + await import("leaflet-gpx"); + this.gpxLoaded = true; + } + + // TODO: This is not very efficient as it's probably a string response that is parsed and then converted back to string and parsed again. + const xmlResponse = await server.get(`notes/${note.noteId}/open`); + const stringResponse = new XMLSerializer().serializeToString(xmlResponse); + + const track = new this.L.GPX(stringResponse, { + + }); + track.addTo(this.geoMapWidget.map); + } + + #processNoteWithMarker(note: FNote, latLng: string) { + const map = this.geoMapWidget.map; + if (!map) { + return; + } + + const [ lat, lng ] = latLng.split(",", 2).map((el) => parseFloat(el)); + const L = this.L; + const icon = L.divIcon({ + html: `\ + + + + ${note.title}`, + iconSize: [ 25, 41 ], + iconAnchor: [ 12, 41 ] + }) + + const marker = L.marker(L.latLng(lat, lng), { + icon, + draggable: true, + autoPan: true, + autoPanSpeed: 5, + }) + .addTo(map) + .on("moveend", e => { + this.moveMarker(note.noteId, (e.target as Marker).getLatLng()); }); - const el = marker.getElement(); - if (el) { - const $el = $(el); - $el.attr("data-href", `#${childNote.noteId}`); - note_tooltip.setupElementTooltip($($el)); - } + marker.on("contextmenu", (e) => { + openContextMenu(note.noteId, e.originalEvent); + }); - this.currentMarkerData[childNote.noteId] = marker; + const el = marker.getElement(); + if (el) { + const $el = $(el); + $el.attr("data-href", `#${note.noteId}`); + note_tooltip.setupElementTooltip($($el)); } + + this.currentMarkerData[note.noteId] = marker; } #changeState(newState: State) { @@ -299,6 +330,14 @@ export default class GeoMapTypeWidget extends TypeWidget { } entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { + // If any of the children branches are altered. + if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.noteId)) { + this.#reloadMarkers(); + return; + } + + // If any of note has its location attribute changed. + // TODO: Should probably filter by parent here as well. const attributeRows = loadResults.getAttributeRows(); if (attributeRows.find((at) => at.name === LOCATION_ATTRIBUTE)) { this.#reloadMarkers();