Add a "user overlay" tool: click the map, save a note

New map-corner "Add note" control (public/js/notes.js), mirroring
measure.js's click-to-place UX. Click the map, write a short note,
pick an existing .geojson layer file or name a new one — saved via a
new GpxStore#append_feature (grows the target file in place, unlike
save()'s always-disambiguate-the-filename behavior) through a new
POST /api/gpx/notes route.

No new rendering path: a note is just another point in a .geojson
file the Tracks panel's tree already lists, checkboxes, and pops up —
visibility toggling ("layers") comes for free from that existing
machinery, per-file, with no changes needed there.

Points only for now — a polygon mode would need gpx.js's point/line
split taught a third Polygon bucket first, or a saved polygon
wouldn't render back on reload (see TODO.adoc).

Covered by new tests: GpxStore#append_feature (create/append/missing-
path/filename-safety) and the /api/gpx/notes route (auth gate, create,
append, malformed/missing-geometry rejection, 404 on a missing
target). Full suite: 97 runs, 0 failures.
This commit is contained in:
randy committed 2026-09-09 10:18:50 +02:00
1 parent 5caf865fc8
commit aa2b96ac70
11 files changed
+437 -2

No files matched your search

+36
View File
@@ -174,8 +174,44 @@ class GpxStoreTest < Minitest::Test
assert_equal ["ride.gpx"], @store.tree["children"].map { |c| c["name"] }
end
def test_append_feature_creates_a_new_geojson_file
path = @store.append_feature(feature: sample_feature("Turning spot"), folder: "", name: "My Notes")
assert_equal "My Notes.geojson", path
file = @store.tree["children"].find { |c| c["name"] == "My Notes.geojson" }
assert_equal ["waypoint"], file["kinds"]
end
def test_append_feature_grows_the_same_file_instead_of_disambiguating
first_path = @store.append_feature(feature: sample_feature("Turning spot"), folder: "", name: "My Notes")
second_path = @store.append_feature(feature: sample_feature("Narrow gate"), path: first_path)
assert_equal first_path, second_path
features = JSON.parse(@store.geojson(first_path))["features"]
assert_equal ["Turning spot", "Narrow gate"], features.map { |f| f["properties"]["name"] }
end
def test_append_feature_to_a_missing_path_raises_enoent
assert_raises(Errno::ENOENT) { @store.append_feature(feature: sample_feature("x"), path: "nope.geojson") }
end
def test_append_feature_name_always_gets_a_geojson_extension
path = @store.append_feature(feature: sample_feature("x"), folder: "", name: "notes.txt")
assert_equal "notes.txt.geojson", path
end
def test_append_feature_blank_name_falls_back_to_notes
path = @store.append_feature(feature: sample_feature("x"), folder: "", name: "")
assert_equal "notes.geojson", path
end
private
def sample_feature(name)
{ "type" => "Feature", "properties" => { "name" => name, "category" => "Note" },
"geometry" => { "type" => "Point", "coordinates" => [17.1, 48.1] } }
end
def sample_gpx
<<~GPX
<gpx><wpt lat="48.1" lon="17.1"><name>Waypoint</name></wpt></gpx>