From 8d0c61ea37c178fa883db42ad0df7d78fa3aa9bb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 15 Feb 2026 19:20:08 +0200 Subject: [PATCH] refactor(collections/map): extract marker data build to separate module --- .../src/widgets/collections/geomap/index.tsx | 84 +------------------ .../widgets/collections/geomap/marker_data.ts | 84 +++++++++++++++++++ 2 files changed, 86 insertions(+), 82 deletions(-) create mode 100644 apps/client/src/widgets/collections/geomap/marker_data.ts diff --git a/apps/client/src/widgets/collections/geomap/index.tsx b/apps/client/src/widgets/collections/geomap/index.tsx index fd2ae6dfae..443dd917e0 100644 --- a/apps/client/src/widgets/collections/geomap/index.tsx +++ b/apps/client/src/widgets/collections/geomap/index.tsx @@ -1,7 +1,6 @@ import "./index.css"; import type maplibregl from "maplibre-gl"; -import { RefObject } from "preact"; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; import appContext from "../../../components/app_context"; @@ -14,7 +13,7 @@ import toast from "../../../services/toast"; import CollectionProperties from "../../note_bars/CollectionProperties"; import ActionButton from "../../react/ActionButton"; import { ButtonOrActionButton } from "../../react/Button"; -import { useChildNotes, useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTriliumEvent } from "../../react/hooks"; +import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTriliumEvent } from "../../react/hooks"; import { ParentComponent } from "../../react/react_utils"; import TouchBar, { TouchBarButton, TouchBarSlider } from "../../react/TouchBar"; import { ViewModeProps } from "../interface"; @@ -23,6 +22,7 @@ import openContextMenu, { openMapContextMenu } from "./context_menu"; import Map, { GeoMouseEvent } from "./map"; import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, MapLayer } from "./map_layer"; import Marker, { GpxTrack } from "./marker"; +import { MARKER_SVG, useMarkerData } from "./marker_data"; const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659]; const DEFAULT_ZOOM = 2; @@ -171,80 +171,6 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM ); } -function useMarkerData(note: FNote | null | undefined, apiRef: RefObject) { - const childNotes = useChildNotes(note?.noteId); - - useEffect(() => { - const map = apiRef.current as maplibregl.Map | undefined; - if (!map) return; - - svgToImage(MARKER_SVG, (img) => { - map.addImage("custom-marker", img, { - pixelRatio: window.devicePixelRatio - }); - }); - - const features: maplibregl.GeoJSONFeature[] = []; - for (const childNote of childNotes) { - const location = childNote.getLabelValue(LOCATION_ATTRIBUTE); - const latLng = location?.split(",", 2).map((el) => parseFloat(el)) as [ number, number ] | undefined; - if (!latLng) continue; - latLng.reverse(); - - features.push({ - type: "Feature", - geometry: { - type: "Point", - coordinates: latLng - }, - properties: { - id: childNote.noteId, - name: childNote.title, - } - }); - } - - map.addSource("points", { - type: "geojson", - data: { - type: "FeatureCollection", - features - } - }); - map.addLayer({ - id: "points-layer", - type: "symbol", - source: "points", - layout: { - "icon-image": "custom-marker", - "icon-size": 1, - "icon-anchor": "bottom", - "icon-allow-overlap": true - } - }); - - return () => { - map.removeLayer("points-layer"); - map.removeSource("points"); - }; - }, [ apiRef, childNotes ]); -} - -function svgToImage(svgString, callback) { - const svgBlob = new Blob([svgString], { type: "image/svg+xml" }); - const url = URL.createObjectURL(svgBlob); - - const img = new Image(); - - img.onload = () => { - URL.revokeObjectURL(url); - callback(img); - }; - - img.src = url; -} - - function useLayerData(note: FNote) { const [ layerName ] = useNoteLabel(note, "map:style"); // Memo is needed because it would generate unnecessary reloads due to layer change. @@ -373,12 +299,6 @@ function NoteGpxTrack({ note, hideLabels }: { note: FNote, hideLabels?: boolean />; } -// SVG marker pin shape (replaces the Leaflet marker PNG). -const MARKER_SVG = `` + - `` + - `` + - ``; - function buildIconHtml(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string, archived?: boolean) { let html = /*html*/`\
${MARKER_SVG}
diff --git a/apps/client/src/widgets/collections/geomap/marker_data.ts b/apps/client/src/widgets/collections/geomap/marker_data.ts new file mode 100644 index 0000000000..e3271d8f0f --- /dev/null +++ b/apps/client/src/widgets/collections/geomap/marker_data.ts @@ -0,0 +1,84 @@ +import { MutableRef, useEffect } from "preact/hooks"; + +import FNote from "../../../entities/fnote"; +import { useChildNotes } from "../../react/hooks"; +import { LOCATION_ATTRIBUTE } from "."; + +// SVG marker pin shape (replaces the Leaflet marker PNG). +export const MARKER_SVG = `` + + `` + + `` + + ``; + +export function useMarkerData(note: FNote | null | undefined, apiRef: MutableRef) { + const childNotes = useChildNotes(note?.noteId); + + useEffect(() => { + const map = apiRef.current as maplibregl.Map | undefined; + if (!map) return; + + svgToImage(MARKER_SVG, (img) => { + map.addImage("custom-marker", img, { + pixelRatio: window.devicePixelRatio + }); + }); + + const features: maplibregl.GeoJSONFeature[] = []; + for (const childNote of childNotes) { + const location = childNote.getLabelValue(LOCATION_ATTRIBUTE); + const latLng = location?.split(",", 2).map((el) => parseFloat(el)) as [ number, number ] | undefined; + if (!latLng) continue; + latLng.reverse(); + + features.push({ + type: "Feature", + geometry: { + type: "Point", + coordinates: latLng + }, + properties: { + id: childNote.noteId, + name: childNote.title, + } + }); + } + + map.addSource("points", { + type: "geojson", + data: { + type: "FeatureCollection", + features + } + }); + map.addLayer({ + id: "points-layer", + type: "symbol", + source: "points", + layout: { + "icon-image": "custom-marker", + "icon-size": 1, + "icon-anchor": "bottom", + "icon-allow-overlap": true + } + }); + + return () => { + map.removeLayer("points-layer"); + map.removeSource("points"); + }; + }, [ apiRef, childNotes ]); +} + +function svgToImage(svgString, callback) { + const svgBlob = new Blob([svgString], { type: "image/svg+xml" }); + const url = URL.createObjectURL(svgBlob); + + const img = new Image(); + + img.onload = () => { + URL.revokeObjectURL(url); + callback(img); + }; + + img.src = url; +}