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:
1 parent
5caf865fc8
commit
aa2b96ac70
11 files changed
+437
-2
No files matched your search
@@ -176,6 +176,47 @@ class AppTest < Minitest::Test
|
||||
assert_equal 400, response.status
|
||||
end
|
||||
|
||||
# --- Notes (hand-drawn points, notes.js) ---
|
||||
|
||||
def test_notes_requires_login
|
||||
assert_equal 401, request.post("/api/gpx/notes", params: { feature: sample_note_feature, name: "Notes" }).status
|
||||
end
|
||||
|
||||
def test_notes_creates_a_new_layer_file
|
||||
response = request.post("/api/gpx/notes", params: { feature: sample_note_feature, name: "My Notes" }, **logged_in_env)
|
||||
|
||||
assert_equal 200, response.status
|
||||
body = json_body(response)
|
||||
assert_equal "My Notes.geojson", body["path"]
|
||||
assert_equal ["My Notes.geojson"], body["tree"]["children"].map { |c| c["name"] }
|
||||
end
|
||||
|
||||
def test_notes_appends_to_an_existing_layer_file_instead_of_disambiguating
|
||||
first = json_body(request.post("/api/gpx/notes", params: { feature: sample_note_feature("Turning spot"), name: "My Notes" }, **logged_in_env))
|
||||
|
||||
second = request.post("/api/gpx/notes", params: { feature: sample_note_feature("Narrow gate"), path: first["path"] }, **logged_in_env)
|
||||
assert_equal 200, second.status
|
||||
assert_equal first["path"], json_body(second)["path"]
|
||||
|
||||
features = json_body(request.get("/api/gpx/file/#{Rack::Utils.escape_path(first["path"])}"))["features"]
|
||||
assert_equal ["Turning spot", "Narrow gate"], features.map { |f| f["properties"]["name"] }
|
||||
end
|
||||
|
||||
def test_notes_rejects_malformed_feature_json
|
||||
response = request.post("/api/gpx/notes", params: { feature: "not json", name: "Notes" }, **logged_in_env)
|
||||
assert_equal 400, response.status
|
||||
end
|
||||
|
||||
def test_notes_rejects_a_feature_with_no_geometry
|
||||
response = request.post("/api/gpx/notes", params: { feature: '{"type":"Feature","properties":{}}', name: "Notes" }, **logged_in_env)
|
||||
assert_equal 400, response.status
|
||||
end
|
||||
|
||||
def test_notes_to_a_missing_path_is_404
|
||||
response = request.post("/api/gpx/notes", params: { feature: sample_note_feature, path: "nope.geojson" }, **logged_in_env)
|
||||
assert_equal 404, response.status
|
||||
end
|
||||
|
||||
# --- LiveTrack webhook ---
|
||||
|
||||
def test_livetrack_webhook_requires_the_configured_secret
|
||||
@@ -214,6 +255,14 @@ class AppTest < Minitest::Test
|
||||
|
||||
private
|
||||
|
||||
def sample_note_feature(name = "Turning spot")
|
||||
JSON.generate(
|
||||
"type" => "Feature",
|
||||
"properties" => { "name" => name, "category" => "Note" },
|
||||
"geometry" => { "type" => "Point", "coordinates" => [17.1, 48.1] },
|
||||
)
|
||||
end
|
||||
|
||||
def le32(n)
|
||||
[n].pack("l<")
|
||||
end
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user