Show a GeoJSON icon next to files with a ready cache
GpxStore#tree now reports a "cached" flag per file (reusing the same mtime freshness check geojson() already does); the file tree shows a small curly-braces icon next to any file whose conversion is already on disk, so it's visible which files will toggle on instantly versus which still need to parse.
This commit is contained in:
1 parent
dd0ad6d845
commit
44d235bbcf
4 files changed
+56
-4
No files matched your search
@@ -22,7 +22,10 @@ module Maptool
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Nested { type:, name:, path:, children: } hash describing every
|
# Nested { type:, name:, path:, children: } hash describing every
|
||||||
# folder and .gpx/.fit file under the store root.
|
# folder and .gpx/.fit file under the store root. Each file also
|
||||||
|
# carries "cached" — whether its GeoJSON conversion is ready on disk
|
||||||
|
# (see `geojson` below) — so the tree UI can show a file's ready to
|
||||||
|
# view instantly, without a round trip just to find out.
|
||||||
def tree
|
def tree
|
||||||
{ "type" => "folder", "name" => "root", "path" => "", "children" => list_children(@root, "") }
|
{ "type" => "folder", "name" => "root", "path" => "", "children" => list_children(@root, "") }
|
||||||
end
|
end
|
||||||
@@ -112,7 +115,7 @@ module Maptool
|
|||||||
raise Errno::ENOENT, path unless File.file?(path)
|
raise Errno::ENOENT, path unless File.file?(path)
|
||||||
|
|
||||||
cache = geojson_cache_path(path)
|
cache = geojson_cache_path(path)
|
||||||
if !File.file?(cache) || File.mtime(cache) < File.mtime(path)
|
unless geojson_cached?(path)
|
||||||
data = path.downcase.end_with?(".fit") ? Fit.to_geojson(path) : Gpx.to_geojson(path)
|
data = path.downcase.end_with?(".fit") ? Fit.to_geojson(path) : Gpx.to_geojson(path)
|
||||||
File.write(cache, JSON.generate(data))
|
File.write(cache, JSON.generate(data))
|
||||||
end
|
end
|
||||||
@@ -136,6 +139,11 @@ module Maptool
|
|||||||
"#{path}.geojson"
|
"#{path}.geojson"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def geojson_cached?(path)
|
||||||
|
cache = geojson_cache_path(path)
|
||||||
|
File.file?(cache) && File.mtime(cache) >= File.mtime(path)
|
||||||
|
end
|
||||||
|
|
||||||
TRACK_EXTENSIONS = %w[.gpx .fit].freeze
|
TRACK_EXTENSIONS = %w[.gpx .fit].freeze
|
||||||
|
|
||||||
def safe_filename(filename)
|
def safe_filename(filename)
|
||||||
@@ -165,9 +173,9 @@ module Maptool
|
|||||||
if File.directory?(full)
|
if File.directory?(full)
|
||||||
{ "type" => "folder", "name" => entry, "path" => rel, "children" => list_children(full, rel) }
|
{ "type" => "folder", "name" => entry, "path" => rel, "children" => list_children(full, rel) }
|
||||||
elsif entry.downcase.end_with?(".gpx")
|
elsif entry.downcase.end_with?(".gpx")
|
||||||
{ "type" => "file", "name" => entry, "path" => rel, "kinds" => Gpx.kinds(full) }
|
{ "type" => "file", "name" => entry, "path" => rel, "kinds" => Gpx.kinds(full), "cached" => geojson_cached?(full) }
|
||||||
elsif entry.downcase.end_with?(".fit")
|
elsif entry.downcase.end_with?(".fit")
|
||||||
{ "type" => "file", "name" => entry, "path" => rel, "kinds" => Fit.kinds(full) }
|
{ "type" => "file", "name" => entry, "path" => rel, "kinds" => Fit.kinds(full), "cached" => geojson_cached?(full) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
WAYPOINT_ICON,
|
WAYPOINT_ICON,
|
||||||
TRACK_ICON,
|
TRACK_ICON,
|
||||||
ROUTE_ICON,
|
ROUTE_ICON,
|
||||||
|
GEOJSON_ICON,
|
||||||
MAP_MARKER_ICON,
|
MAP_MARKER_ICON,
|
||||||
POI_ICONS,
|
POI_ICONS,
|
||||||
PARK4NIGHT_TAGS,
|
PARK4NIGHT_TAGS,
|
||||||
@@ -276,6 +277,7 @@ export class GpxPanelControl {
|
|||||||
name.title = file.name;
|
name.title = file.name;
|
||||||
label.appendChild(checkbox);
|
label.appendChild(checkbox);
|
||||||
(file.kinds || []).forEach((kind) => label.appendChild(this._kindIcon(kind)));
|
(file.kinds || []).forEach((kind) => label.appendChild(this._kindIcon(kind)));
|
||||||
|
if (file.cached) label.appendChild(this._cachedIcon());
|
||||||
label.appendChild(name);
|
label.appendChild(name);
|
||||||
row.appendChild(label);
|
row.appendChild(label);
|
||||||
row.appendChild(this._deleteButton(() => this._deleteFile(file.path)));
|
row.appendChild(this._deleteButton(() => this._deleteFile(file.path)));
|
||||||
@@ -299,6 +301,14 @@ export class GpxPanelControl {
|
|||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_cachedIcon() {
|
||||||
|
const icon = document.createElement("span");
|
||||||
|
icon.className = "gpx-tree__kind-icon";
|
||||||
|
icon.innerHTML = GEOJSON_ICON;
|
||||||
|
icon.title = "Ready to view instantly (GeoJSON cached)";
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
_deleteButton(onClick) {
|
_deleteButton(onClick) {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
|
|||||||
@@ -54,6 +54,16 @@ export const WAYPOINT_ICON = `
|
|||||||
|
|
||||||
export const TRACK_ICON = GPX_ICON;
|
export const TRACK_ICON = GPX_ICON;
|
||||||
|
|
||||||
|
// Shown next to a file whose GeoJSON conversion is already cached on disk
|
||||||
|
// (GpxStore#geojson) — ready to view instantly instead of paying REXML's
|
||||||
|
// parse cost on click. Curly braces read as "structured data ready", the
|
||||||
|
// same shorthand most editors/tools use for JSON.
|
||||||
|
export const GEOJSON_ICON = `
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M9 4c-1.5 0-2 .8-2 2v3c0 1-.5 1.5-1.5 1.5 1 0 1.5.5 1.5 1.5v3c0 1.2.5 2 2 2"/>
|
||||||
|
<path d="M15 4c1.5 0 2 .8 2 2v3c0 1 .5 1.5 1.5 1.5-1 0-1.5.5-1.5 1.5v3c0 1.2-.5 2-2 2"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
// Same shape as TRACK_ICON, dashed — a route is planned rather than
|
// Same shape as TRACK_ICON, dashed — a route is planned rather than
|
||||||
// recorded, the same visual convention most map apps use for the
|
// recorded, the same visual convention most map apps use for the
|
||||||
// distinction.
|
// distinction.
|
||||||
|
|||||||
@@ -121,6 +121,30 @@ class GpxStoreTest < Minitest::Test
|
|||||||
assert_raises(Errno::ENOENT) { @store.geojson("nope.gpx") }
|
assert_raises(Errno::ENOENT) { @store.geojson("nope.gpx") }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_tree_reports_cached_true_after_a_successful_save
|
||||||
|
@store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
||||||
|
|
||||||
|
file = @store.tree["children"].find { |c| c["name"] == "ride.gpx" }
|
||||||
|
assert file["cached"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tree_reports_cached_false_for_an_unparseable_file
|
||||||
|
@store.save(folder: "", filename: "bad.gpx", io: StringIO.new("not gpx at all"))
|
||||||
|
|
||||||
|
file = @store.tree["children"].find { |c| c["name"] == "bad.gpx" }
|
||||||
|
refute file["cached"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tree_reports_cached_false_once_the_source_is_resaved
|
||||||
|
path = @store.save(folder: "", filename: "ride.gpx", io: StringIO.new(sample_gpx))
|
||||||
|
FileUtils.touch(@store.resolve_file(path) + ".geojson", mtime: Time.now - 10)
|
||||||
|
File.write(@store.resolve_file(path), sample_gpx)
|
||||||
|
FileUtils.touch(@store.resolve_file(path), mtime: Time.now)
|
||||||
|
|
||||||
|
file = @store.tree["children"].find { |c| c["name"] == "ride.gpx" }
|
||||||
|
refute file["cached"]
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def sample_gpx
|
def sample_gpx
|
||||||
|
|||||||
Reference in new issue
Block a user