Let a cached file's GeoJSON be downloaded by clicking its tree name
Once a .gpx/.fit is cached, the tree shows it under its converted name
("ride.gpx" -> "ride.gpx.geojson", matching the real cache file next
to it on disk) instead of still reading as un-converted -- the
GeoJSON is what's actually on the map from that point on, and the
original is never deleted, just not re-shown as itself. The name is
now a real link (GET /api/gpx/file/<path>?download=1) instead of
plain text, the only way to get that converted form out of the app.
Verified in a real browser that an <a> nested in a <label> claims its
own click rather than the label forwarding it to the checkbox, so
downloading and toggling visibility don't fight over the same click.
This commit is contained in:
1 parent
b4f205667b
commit
cd92dadf6a
5 files changed
+70
-4
No files matched your search
@@ -191,6 +191,10 @@ module Maptool
|
||||
json paths: paths, tree: GPX_STORE.tree
|
||||
end
|
||||
|
||||
# ?download=1 exports the file's GeoJSON as an attachment instead of
|
||||
# rendering it on the map — the only way to get a .gpx/.fit's converted
|
||||
# form out of the app, since GpxStore#geojson's cache is otherwise an
|
||||
# internal implementation detail with no filename of its own to expose.
|
||||
get "/api/gpx/file/*" do
|
||||
begin
|
||||
body = GPX_STORE.geojson(params[:splat].first)
|
||||
@@ -203,6 +207,15 @@ module Maptool
|
||||
end
|
||||
|
||||
content_type :json
|
||||
if params[:download]
|
||||
# Matches the tree's own display name for a cached .gpx/.fit
|
||||
# (public/js/gpx.js's _renderFile) — "ride.gpx" downloads as
|
||||
# "ride.gpx.geojson", not "ride.geojson", so the two never disagree.
|
||||
# A .geojson source is already named right and is left alone.
|
||||
original_name = File.basename(params[:splat].first)
|
||||
download_name = original_name.downcase.end_with?(".geojson") ? original_name : "#{original_name}.geojson"
|
||||
attachment download_name
|
||||
end
|
||||
body
|
||||
end
|
||||
|
||||
|
||||
+17
-1
@@ -285,7 +285,23 @@ A `.geojson` upload skips this path entirely — `geojson` reads and
|
||||
returns it directly, no cache file, no `Gpx`/`Fit` involved; the file
|
||||
tree's cached-file icon (`GEOJSON_ICON`, `public/js/icons.js`) always
|
||||
shows for one, since there's no conversion step it could ever be
|
||||
waiting on. `Gpx` parses the GPX with REXML: `<wpt>`
|
||||
waiting on.
|
||||
+
|
||||
Once a `.gpx`/`.fit` is cached, the tree shows it under its converted
|
||||
name instead — `"ride.gpx"` reads as `"ride.gpx.geojson"`, matching
|
||||
the real cache file sitting next to it on disk — since the GeoJSON,
|
||||
not the original, is what's actually rendered on the map from that
|
||||
point on. The original is never deleted or re-parsed once cached, just
|
||||
not re-shown as itself; the tree's file name is a real link
|
||||
(`GET /api/gpx/file/<path>?download=1`), the only way to get that
|
||||
converted form out of the app as an actual file, since the cache is
|
||||
otherwise an implementation detail with no name of its own to expose.
|
||||
It's an `<a>`, not a `<span>`, specifically so clicking it downloads
|
||||
instead of also toggling the row's checkbox — a native `<a>`/`<button>`
|
||||
nested inside a `<label>` claims its own click instead of the label
|
||||
forwarding it to the associated control, so the two behaviors don't
|
||||
fight over the same click. A directly-uploaded `.geojson` keeps its
|
||||
own name (nothing to convert) and downloads unchanged. `Gpx` parses the GPX with REXML: `<wpt>`
|
||||
waypoints become `Point` features (with `name`, `desc` and `time`
|
||||
carried through as properties), each `<trk>` becomes one
|
||||
`MultiLineString` feature (one line per `<trkseg>`), and each `<rte>`
|
||||
|
||||
@@ -360,6 +360,12 @@ html, body {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.gpx-tree__file-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.gpx-tree__kind-icon {
|
||||
|
||||
+16
-3
@@ -271,10 +271,23 @@ export class GpxPanelControl {
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = this._visible.has(file.path);
|
||||
checkbox.addEventListener("change", () => this._toggleFile(file.path, checkbox.checked, checkbox));
|
||||
const name = document.createElement("span");
|
||||
// Once a .gpx/.fit is cached, its converted GeoJSON is the thing that's
|
||||
// actually shown on the map from here on (see GpxStore#geojson) — the
|
||||
// name reflects that (matching the real "<path>.geojson" cache file on
|
||||
// disk) rather than still reading as a plain, un-converted .gpx/.fit.
|
||||
// A direct .geojson upload already ends in .geojson, so it's left
|
||||
// alone. It's an <a>, not a <span>: clicking it downloads that GeoJSON
|
||||
// (?download=1) — the only way to get it out of the app, since the
|
||||
// original .gpx/.fit is never deleted but also never re-shown once
|
||||
// converted. An <a> nested in a <label> doesn't also toggle the
|
||||
// checkbox on click (unlike a plain element), so this doesn't fight
|
||||
// the row's own toggle-on-map behavior.
|
||||
const displayName = file.cached && !file.name.toLowerCase().endsWith(".geojson") ? `${file.name}.geojson` : file.name;
|
||||
const name = document.createElement("a");
|
||||
name.className = "gpx-tree__file-name";
|
||||
name.textContent = file.name;
|
||||
name.title = file.name;
|
||||
name.href = `${gpxFileUrl(file.path)}?download=1`;
|
||||
name.textContent = displayName;
|
||||
name.title = `Download ${displayName}`;
|
||||
label.appendChild(checkbox);
|
||||
(file.kinds || []).forEach((kind) => label.appendChild(this._kindIcon(kind)));
|
||||
if (file.cached) label.appendChild(this._cachedIcon());
|
||||
|
||||
@@ -72,6 +72,24 @@ class AppTest < Minitest::Test
|
||||
assert_equal "Spot", json_body(file_response)["features"][0]["properties"]["name"]
|
||||
end
|
||||
|
||||
def test_download_of_a_converted_gpx_names_it_gpx_geojson
|
||||
content = File.read(fixture_path("sample.gpx"))
|
||||
post_file("/api/gpx", filename: "ride.gpx", content: content, env: logged_in_env)
|
||||
|
||||
response = request.get("/api/gpx/file/ride.gpx?download=1")
|
||||
assert_equal 200, response.status
|
||||
assert_includes response.headers["Content-Disposition"], 'filename="ride.gpx.geojson"'
|
||||
end
|
||||
|
||||
def test_download_of_a_native_geojson_keeps_its_own_name
|
||||
content = '{"type":"FeatureCollection","features":[]}'
|
||||
post_file("/api/gpx", filename: "spots.geojson", content: content, env: logged_in_env)
|
||||
|
||||
response = request.get("/api/gpx/file/spots.geojson?download=1")
|
||||
assert_equal 200, response.status
|
||||
assert_includes response.headers["Content-Disposition"], 'filename="spots.geojson"'
|
||||
end
|
||||
|
||||
def test_unparseable_gpx_returns_422
|
||||
post_file("/api/gpx", filename: "bad.gpx", content: "<gpx><trk unclosed>", env: logged_in_env)
|
||||
|
||||
|
||||
Reference in new issue
Block a user