import { NOTE_PIN_ICON } from "/js/icons.js"; import { notifyGpxStoreChanged } from "/js/panels.js"; const NEW_LAYER_VALUE = "__new__"; // A quick "pin it with a note" tool — click the map, write a short note, // pick which .geojson "layer" file it joins (or start a new one). Saved // server-side (POST /api/gpx/notes) into the same GpxStore everything else // in the Tracks panel already reads from, so a note shows up there with no // separate rendering path of its own: same tree, same checkbox visibility, // same popup. Points only for now — a polygon mode would need gpx.js's // point/line split (public/js/gpx.js's _addLayers) taught a third, // Polygon-shaped bucket first, so it's left for a follow-up rather than // saving areas that wouldn't render back once reloaded. export class NotesControl { constructor() { this._active = false; this._pendingLngLat = null; } onAdd(map) { this._map = map; this._container = document.createElement("div"); this._container.className = "maplibregl-ctrl maplibregl-ctrl-group notes-ctrl"; this._container.innerHTML = ` `; this._button = this._container.querySelector(".notes-ctrl__button"); this._panel = this._container.querySelector(".notes-panel"); this._hint = this._container.querySelector(".notes-panel__hint"); this._form = this._container.querySelector(".notes-panel__form"); this._text = this._container.querySelector(".notes-panel__text"); this._target = this._container.querySelector(".notes-panel__target"); this._newName = this._container.querySelector(".notes-panel__new-name"); this._button.addEventListener("click", () => this._setActive(!this._active)); this._target.addEventListener("change", () => { this._newName.hidden = this._target.value !== NEW_LAYER_VALUE; if (!this._newName.hidden) this._newName.focus(); }); this._container.querySelector(".notes-panel__save").addEventListener("click", () => this._save()); this._container.querySelector(".notes-panel__cancel").addEventListener("click", () => this._resetForm()); this._onClick = (event) => { if (!this._active) return; this._pendingLngLat = event.lngLat; this._openForm(); }; map.on("click", this._onClick); return this._container; } onRemove() { this._map.off("click", this._onClick); this._container.remove(); } getDefaultPosition() { return "top-left"; } _setActive(active) { this._active = active; this._button.classList.toggle("active", active); this._panel.hidden = !active; this._map.getCanvas().style.cursor = active ? "crosshair" : ""; if (!active) this._resetForm(); } async _openForm() { this._hint.hidden = true; this._form.hidden = false; this._text.value = ""; this._text.focus(); await this._populateTargets(); } _resetForm() { this._pendingLngLat = null; this._hint.hidden = false; this._form.hidden = true; this._newName.hidden = true; this._newName.value = ""; } async _populateTargets() { const previousValue = this._target.value; let files = []; try { const tree = await (await fetch("/api/gpx/tree")).json(); files = geojsonFiles(tree); } catch (err) { // Tree fetch failed — target select just falls back to "new layer" // only, still usable. } this._target.innerHTML = files.map((file) => ``).join("") + ``; this._target.value = files.some((file) => file.path === previousValue) ? previousValue : NEW_LAYER_VALUE; this._newName.hidden = this._target.value !== NEW_LAYER_VALUE; } async _save() { const text = this._text.value.trim(); if (!text || !this._pendingLngLat) return; const isNewLayer = this._target.value === NEW_LAYER_VALUE; const name = isNewLayer ? this._newName.value.trim() : ""; if (isNewLayer && !name) { this._newName.focus(); return; } const feature = { type: "Feature", properties: { name: text, category: "Note", time: new Date().toISOString() }, geometry: { type: "Point", coordinates: [this._pendingLngLat.lng, this._pendingLngLat.lat] }, }; const body = new URLSearchParams({ feature: JSON.stringify(feature) }); if (isNewLayer) body.set("name", name); else body.set("path", this._target.value); const saveButton = this._container.querySelector(".notes-panel__save"); saveButton.disabled = true; try { const response = await fetch("/api/gpx/notes", { method: "POST", body }); if (!response.ok) throw new Error(`save failed: ${response.status}`); notifyGpxStoreChanged(); this._resetForm(); } catch (err) { window.alert("Could not save the note — check your connection and try again."); } finally { saveButton.disabled = false; } } } function geojsonFiles(node, results = []) { if (node.type === "file" && node.path.toLowerCase().endsWith(".geojson")) { results.push({ path: node.path }); } (node.children || []).forEach((child) => geojsonFiles(child, results)); return results; } function escapeAttr(value) { return value.replace(/&/g, "&").replace(/"/g, """); } function escapeText(value) { return value.replace(/&/g, "&").replace(/