From 6d03304cbb4eaf77081f36832a37375cb0a73670 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 6 Jul 2025 12:12:59 +0300 Subject: [PATCH] chore(views/geomap): reintroduce display of markers --- .../src/widgets/type_widgets/geo_map.ts | 218 +++--------------- .../widgets/view_widgets/geo_view/index.ts | 107 ++++++++- .../widgets/view_widgets/geo_view/markers.ts | 55 +++++ 3 files changed, 197 insertions(+), 183 deletions(-) create mode 100644 apps/client/src/widgets/view_widgets/geo_view/markers.ts diff --git a/apps/client/src/widgets/type_widgets/geo_map.ts b/apps/client/src/widgets/type_widgets/geo_map.ts index 65abbc094..ad7d98529 100644 --- a/apps/client/src/widgets/type_widgets/geo_map.ts +++ b/apps/client/src/widgets/type_widgets/geo_map.ts @@ -1,4 +1,4 @@ -import { GPX, Marker, type LatLng, type LeafletMouseEvent } from "leaflet"; +import { type LatLng, type LeafletMouseEvent } from "leaflet"; import type FNote from "../../entities/fnote.js"; import GeoMapWidget, { type InitCallback, type Leaflet } from "../geo_map.js"; import TypeWidget from "./type_widget.js"; @@ -8,80 +8,21 @@ import dialogService from "../../services/dialog.js"; import type { CommandListenerData, EventData } from "../../components/app_context.js"; import { t } from "../../services/i18n.js"; import attributes from "../../services/attributes.js"; -import openContextMenu from "./geo_map_context_menu.js"; import link from "../../services/link.js"; -import note_tooltip from "../../services/note_tooltip.js"; -import appContext from "../../components/app_context.js"; -import markerIcon from "leaflet/dist/images/marker-icon.png"; -import markerIconShadow from "leaflet/dist/images/marker-shadow.png"; + import { hasTouchBar } from "../../services/utils.js"; const TPL = /*html*/`\
`; const LOCATION_ATTRIBUTE = "geolocation"; const CHILD_NOTE_ICON = "bx bx-pin"; - - - // TODO: Deduplicate interface CreateChildResponse { note: { @@ -99,8 +40,6 @@ export default class GeoMapTypeWidget extends TypeWidget { private geoMapWidget: GeoMapWidget; private _state: State; private L!: Leaflet; - private currentMarkerData: Record; - private currentTrackData: Record; private gpxLoaded?: boolean; private ignoreNextZoomEvent?: boolean; @@ -112,8 +51,7 @@ export default class GeoMapTypeWidget extends TypeWidget { super(); this.geoMapWidget = new GeoMapWidget("type", (L: Leaflet) => this.#onMapInitialized(L)); - this.currentMarkerData = {}; - this.currentTrackData = {}; + this._state = State.Normal; this.child(this.geoMapWidget); @@ -148,125 +86,41 @@ export default class GeoMapTypeWidget extends TypeWidget { // } } - async #reloadMarkers() { - if (!this.note) { - return; - } - // Delete all existing markers - for (const marker of Object.values(this.currentMarkerData)) { - marker.remove(); - } - // Delete all existing tracks - for (const track of Object.values(this.currentTrackData)) { - track.remove(); - } + // async #processNoteWithGpxTrack(note: FNote) { + // if (!this.L || !this.geoMapWidget.map) { + // return; + // } - // Add the new markers. - this.currentMarkerData = {}; - const childNotes = await this.note.getChildNotes(); - for (const childNote of childNotes) { - if (childNote.mime === "application/gpx+xml") { - this.#processNoteWithGpxTrack(childNote); - continue; - } + // if (!this.gpxLoaded) { + // await import("leaflet-gpx"); + // this.gpxLoaded = true; + // } - const latLng = childNote.getAttributeValue("label", LOCATION_ATTRIBUTE); - if (latLng) { - this.#processNoteWithMarker(childNote, latLng); - } - } - } + // const xmlResponse = await server.get(`notes/${note.noteId}/open`, undefined, true); + // let stringResponse: string; + // if (xmlResponse instanceof Uint8Array) { + // stringResponse = new TextDecoder().decode(xmlResponse); + // } else { + // stringResponse = xmlResponse; + // } - async #processNoteWithGpxTrack(note: FNote) { - if (!this.L || !this.geoMapWidget.map) { - return; - } - - if (!this.gpxLoaded) { - await import("leaflet-gpx"); - this.gpxLoaded = true; - } - - const xmlResponse = await server.get(`notes/${note.noteId}/open`, undefined, true); - let stringResponse: string; - if (xmlResponse instanceof Uint8Array) { - stringResponse = new TextDecoder().decode(xmlResponse); - } else { - stringResponse = xmlResponse; - } - - const track = new this.L.GPX(stringResponse, { - markers: { - startIcon: this.#buildIcon(note.getIcon(), note.getColorClass(), note.title), - endIcon: this.#buildIcon("bxs-flag-checkered"), - wptIcons: { - "": this.#buildIcon("bx bx-pin") - } - }, - polyline_options: { - color: note.getLabelValue("color") ?? "blue" - } - }); - track.addTo(this.geoMapWidget.map); - this.currentTrackData[note.noteId] = track; - } - - #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 = this.#buildIcon(note.getIcon(), note.getColorClass(), note.title); - - 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()); - }); - marker.on("mousedown", ({ originalEvent }) => { - // Middle click to open in new tab - if (originalEvent.button === 1) { - const hoistedNoteId = this.hoistedNoteId; - //@ts-ignore, fix once tab manager is ported. - appContext.tabManager.openInNewTab(note.noteId, hoistedNoteId); - return true; - } - }); - marker.on("contextmenu", (e) => { - openContextMenu(note.noteId, e.originalEvent); - }); - - const el = marker.getElement(); - if (el) { - const $el = $(el); - $el.attr("data-href", `#${note.noteId}`); - note_tooltip.setupElementTooltip($($el)); - } - - this.currentMarkerData[note.noteId] = marker; - } - - #buildIcon(bxIconClass: string, colorClass?: string, title?: string) { - return this.L.divIcon({ - html: /*html*/`\ - - - - ${title ?? ""}`, - iconSize: [25, 41], - iconAnchor: [12, 41] - }); - } + // const track = new this.L.GPX(stringResponse, { + // markers: { + // startIcon: this.#buildIcon(note.getIcon(), note.getColorClass(), note.title), + // endIcon: this.#buildIcon("bxs-flag-checkered"), + // wptIcons: { + // "": this.#buildIcon("bx bx-pin") + // } + // }, + // polyline_options: { + // color: note.getLabelValue("color") ?? "blue" + // } + // }); + // track.addTo(this.geoMapWidget.map); + // this.currentTrackData[note.noteId] = track; + // } #changeState(newState: State) { this._state = newState; @@ -332,13 +186,13 @@ export default class GeoMapTypeWidget extends TypeWidget { async doRefresh(note: FNote) { await this.geoMapWidget.refresh(); // this.#restoreViewportAndZoom(); - await this.#reloadMarkers(); + // await this.#reloadMarkers(); } entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { // If any of the children branches are altered. if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === this.noteId)) { - this.#reloadMarkers(); + // this.#reloadMarkers(); return; } @@ -346,7 +200,7 @@ export default class GeoMapTypeWidget extends TypeWidget { // TODO: Should probably filter by parent here as well. const attributeRows = loadResults.getAttributeRows(); if (attributeRows.find((at) => [LOCATION_ATTRIBUTE, "color"].includes(at.name ?? ""))) { - this.#reloadMarkers(); + // this.#reloadMarkers(); } } diff --git a/apps/client/src/widgets/view_widgets/geo_view/index.ts b/apps/client/src/widgets/view_widgets/geo_view/index.ts index 9684094a9..6eb75c4ad 100644 --- a/apps/client/src/widgets/view_widgets/geo_view/index.ts +++ b/apps/client/src/widgets/view_widgets/geo_view/index.ts @@ -1,8 +1,10 @@ import ViewMode, { ViewModeArgs } from "../view_mode.js"; import L from "leaflet"; -import type { LatLng, Map } from "leaflet"; +import type { GPX, LatLng, Map, Marker } from "leaflet"; import SpacedUpdate from "../../../services/spaced_update.js"; import { t } from "../../../services/i18n.js"; +import processNoteWithMarker from "./markers.js"; +import froca from "../../../services/froca.js"; const TPL = /*html*/`
@@ -17,6 +19,60 @@ const TPL = /*html*/` height: 100%; overflow: hidden; } + + .leaflet-pane { + z-index: 1; + } + + .geo-map-container.placing-note { + cursor: crosshair; + } + + .geo-map-container .marker-pin { + position: relative; + } + + .geo-map-container .leaflet-div-icon { + position: relative; + background: transparent; + border: 0; + overflow: visible; + } + + .geo-map-container .leaflet-div-icon .icon-shadow { + position: absolute; + top: 0; + left: 0; + z-index: -1; + } + + .geo-map-container .leaflet-div-icon .bx { + position: absolute; + top: 3px; + left: 2px; + background-color: white; + color: black; + padding: 2px; + border-radius: 50%; + font-size: 17px; + } + + .geo-map-container .leaflet-div-icon .title-label { + display: block; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + font-size: 0.75rem; + height: 1rem; + color: black; + width: 100px; + text-align: center; + text-overflow: ellipsis; + text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white; + white-space: no-wrap; + overflow: hidden; + }
@@ -31,19 +87,30 @@ interface MapData { const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659]; const DEFAULT_ZOOM = 2; +const LOCATION_ATTRIBUTE = "geolocation"; +const CHILD_NOTE_ICON = "bx bx-pin"; export default class GeoView extends ViewMode { + private args: ViewModeArgs; private $root: JQuery; private $container!: JQuery; private map?: Map; private spacedUpdate: SpacedUpdate; + private currentMarkerData: Record; + private currentTrackData: Record; + constructor(args: ViewModeArgs) { super(args, "geoMap"); + this.args = args; this.$root = $(TPL); this.$container = this.$root.find(".geo-map-container"); this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000); + + this.currentMarkerData = {}; + this.currentTrackData = {}; + args.$parent.append(this.$root); } @@ -78,6 +145,8 @@ export default class GeoView extends ViewMode { map.on("moveend", updateFn); map.on("zoomend", updateFn); // map.on("click", (e) => this.#onMapClicked(e)); + + this.#reloadMarkers(); } async #restoreViewportAndZoom() { @@ -109,6 +178,42 @@ export default class GeoView extends ViewMode { this.viewStorage.store(data); } + async #reloadMarkers() { + if (!this.map) { + return; + } + + // Delete all existing markers + for (const marker of Object.values(this.currentMarkerData)) { + marker.remove(); + } + + // Delete all existing tracks + for (const track of Object.values(this.currentTrackData)) { + track.remove(); + } + + // Add the new markers. + this.currentMarkerData = {}; + for (const noteId of this.args.noteIds) { + const childNote = await froca.getNote(noteId); + if (!childNote) { + continue; + } + + if (childNote.mime === "application/gpx+xml") { + // this.#processNoteWithGpxTrack(childNote); + continue; + } + + const latLng = childNote.getAttributeValue("label", LOCATION_ATTRIBUTE); + if (latLng) { + const marker = processNoteWithMarker(this.map, childNote, latLng); + this.currentMarkerData[childNote.noteId] = marker; + } + } + } + get isFullHeight(): boolean { return true; } diff --git a/apps/client/src/widgets/view_widgets/geo_view/markers.ts b/apps/client/src/widgets/view_widgets/geo_view/markers.ts new file mode 100644 index 000000000..d25234650 --- /dev/null +++ b/apps/client/src/widgets/view_widgets/geo_view/markers.ts @@ -0,0 +1,55 @@ +import markerIcon from "leaflet/dist/images/marker-icon.png"; +import markerIconShadow from "leaflet/dist/images/marker-shadow.png"; +import { marker, latLng, divIcon, Map } from "leaflet"; +import type FNote from "../../../entities/fnote.js"; +import note_tooltip from "../../../services/note_tooltip.js"; +import openContextMenu from "../../type_widgets/geo_map_context_menu.js"; + +export default function processNoteWithMarker(map: Map, note: FNote, location: string) { + const [lat, lng] = location.split(",", 2).map((el) => parseFloat(el)); + const icon = buildIcon(note.getIcon(), note.getColorClass(), note.title); + + const newMarker = marker(latLng(lat, lng), { + icon, + draggable: true, + autoPan: true, + autoPanSpeed: 5 + }) + .addTo(map) + .on("moveend", (e) => { + // this.moveMarker(note.noteId, (e.target as Marker).getLatLng()); + }); + newMarker.on("mousedown", ({ originalEvent }) => { + // Middle click to open in new tab + if (originalEvent.button === 1) { + const hoistedNoteId = this.hoistedNoteId; + //@ts-ignore, fix once tab manager is ported. + appContext.tabManager.openInNewTab(note.noteId, hoistedNoteId); + return true; + } + }); + newMarker.on("contextmenu", (e) => { + openContextMenu(note.noteId, e.originalEvent); + }); + + const el = newMarker.getElement(); + if (el) { + const $el = $(el); + $el.attr("data-href", `#${note.noteId}`); + note_tooltip.setupElementTooltip($($el)); + } + + return newMarker; +} + +function buildIcon(bxIconClass: string, colorClass?: string, title?: string) { + return divIcon({ + html: /*html*/`\ + + + + ${title ?? ""}`, + iconSize: [25, 41], + iconAnchor: [12, 41] + }); +}