diff --git a/Rakefile b/Rakefile index f585fa4..f7891b1 100644 --- a/Rakefile +++ b/Rakefile @@ -23,3 +23,33 @@ namespace :overpass do puts "Wrote #{geojson["features"].size} POIs to #{args[:out]}" end end + +namespace :gpx do + desc "Backfill properties.id on Note-category features saved before edit/delete (and its id targeting) existed" + task :backfill_note_ids do + require "json" + require "securerandom" + + data_dir = ENV.fetch("MAPTOOL_DATA_DIR") { File.join(__dir__, "data") } + count = 0 + Dir.glob(File.join(data_dir, "gpx", "**", "*.geojson")).each do |file| + # ".gpx.geojson"/".fit.geojson" are GpxStore's own cache sidecars for + # an uploaded GPX/FIT (regenerated from the source on demand, see + # GpxStore#geojson) — never hand-drawn notes, not worth touching. + next if file.end_with?(".gpx.geojson", ".fit.geojson") + + collection = JSON.parse(File.read(file)) + changed = false + (collection["features"] || []).each do |feature| + properties = (feature["properties"] ||= {}) + next unless properties["category"] == "Note" && !properties["id"] + + properties["id"] = SecureRandom.uuid + changed = true + count += 1 + end + File.write(file, JSON.generate(collection)) if changed + end + puts "Backfilled #{count} note id(s)" + end +end diff --git a/TODO.adoc b/TODO.adoc index 960827a..5824c1d 100644 --- a/TODO.adoc +++ b/TODO.adoc @@ -44,6 +44,7 @@ independence from connectivity. ** [x] Save a distance/area shape as a note too — Measure already draws these by hand, so a "Save" button there (not a second click-to-draw UI in the notes tool) reuses that instead of duplicating it; both share one `NoteSaveForm` (notes.js). `gpx.js`'s `_addLayers` now renders Polygon and note-flagged LineString features (fill/outline, dashed line), not just points ** [x] Edit/delete individual notes from their popup (logged-in only) — targets a feature by a client-generated `properties.id` (`GpxStore#update_feature`/`#delete_feature`), not array position ** [x] Measure's vertices are draggable (mousedown + move, `event.preventDefault()` to stop the map's own drag-pan) and deletable (click to select — shown in yellow — then Delete/Backspace) +** [x] Fixed: a note saved before `properties.id` existed (the earliest test notes) had no Edit/Delete-able target — the popup no longer offers them without an id, and `rake gpx:backfill_note_ids` assigns one to any pre-existing Note feature missing it ** [x] Gated behind login: NotesControl isn't added to the map at all when logged out, Measure's "Save as note" button is hidden (Measure itself stays usable either way) ** [ ] The Unigrid Matrix category filter only re-slices the *points* source (`_applyCategoryFilter`) — toggling the "Note" category off hides point notes but not polygon/line ones sharing that category * [ ] Offline satellite tile caching (e.g. ESRI) for a selected route area — key for judging surface quality and obstacles when wild camping diff --git a/public/js/gpx.js b/public/js/gpx.js index 766c66a..2b19e8d 100644 --- a/public/js/gpx.js +++ b/public/js/gpx.js @@ -696,7 +696,10 @@ export class GpxPanelControl { this._map.on("click", layerId, (event) => { const feature = event.features[0]; if (!feature) return; - const editable = feature.properties.category === "Note" && isLoggedIn(); + // properties.id is what update/delete target by — a note saved before + // that existed (see `rake gpx:backfill_note_ids`) doesn't have one + // yet, and offering Edit/Delete for it would just 404. + const editable = feature.properties.category === "Note" && !!feature.properties.id && isLoggedIn(); const popup = new Popup({ closeButton: true, maxWidth: "260px" }) .setLngLat(event.lngLat) .setHTML(popupHtml(feature.properties, { editable }))