Support uploading .geojson files directly
CI / check (push) Successful in 45s
CI / deploy (push) Successful in 15s

A .geojson file needs no GPX/FIT conversion — it's already the shape
gpx.js expects — so GpxStore#geojson now just reads and returns it
as-is, no cache sidecar. save/tree/the upload dropzone and its zip
handling all pick it up through the existing TRACK_EXTENSIONS list.

Also fixes a bug the new "cached" tree icon would have shipped with:
a GPX/FIT's own .geojson cache sidecar (e.g. "ride.gpx.geojson") also
matches the new .geojson branch in list_children, which would have
listed it as a second, phantom tree entry — excluded explicitly, with
a regression test.
This commit is contained in:
randy committed 2026-09-07 22:48:58 +02:00
1 parent 44d235bbcf
commit b4f205667b
8 files changed
+110 -19

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
module Maptool
# A directly-uploaded .geojson file needs no conversion — GpxStore serves
# it straight from disk (see GpxStore#geojson) — so unlike Gpx/Fit there's
# no to_geojson here. Only kind-detection lives here, for the tree's badge
# icons, mirroring Gpx.kinds' cheap substring-search rather than a full
# JSON parse: a POI export can be tens of MB, geojson or not, and a full
# parse just to answer "does this have any points/lines" would cost the
# same either way.
module Geojson
def self.kinds(path)
content = File.read(path)
kinds = []
kinds << "waypoint" if content.include?('"Point"')
kinds << "track" if content.include?('"LineString"') # matches MultiLineString too
kinds
end
end
end
+17 -1
View File
@@ -2,6 +2,7 @@ require "fileutils"
require "json"
require_relative "gpx"
require_relative "fit"
require_relative "geojson"
module Maptool
# Filesystem-backed store for uploaded GPX/FIT files, organized in
@@ -110,9 +111,14 @@ module Maptool
# can take tens of seconds, every single time the file is toggled
# visible. Cached alongside the source file and invalidated by mtime,
# so only the first read after a save (or edit) pays that cost.
#
# A directly-uploaded .geojson file skips all of this — it's already in
# the shape gpx.js expects, so there's nothing to convert or cache;
# it's just read and served as-is.
def geojson(relative_path)
path = resolve_file(relative_path)
raise Errno::ENOENT, path unless File.file?(path)
return File.read(path) if path.downcase.end_with?(".geojson")
cache = geojson_cache_path(path)
unless geojson_cached?(path)
@@ -139,12 +145,16 @@ module Maptool
"#{path}.geojson"
end
# A .geojson source needs no separate cache file — it already is the
# target format, so it's always "ready".
def geojson_cached?(path)
return true if path.downcase.end_with?(".geojson")
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 .geojson].freeze
def safe_filename(filename)
base = File.basename(filename.to_s)
@@ -176,6 +186,12 @@ module Maptool
{ "type" => "file", "name" => entry, "path" => rel, "kinds" => Gpx.kinds(full), "cached" => geojson_cached?(full) }
elsif entry.downcase.end_with?(".fit")
{ "type" => "file", "name" => entry, "path" => rel, "kinds" => Fit.kinds(full), "cached" => geojson_cached?(full) }
# A GPX/FIT's own cache sidecar (e.g. "ride.gpx.geojson", written by
# `geojson` above) also ends in ".geojson" — excluded here so it
# doesn't show up as a second, phantom tree entry next to the file
# it was derived from.
elsif entry.downcase.end_with?(".geojson") && !entry.downcase.end_with?(".gpx.geojson", ".fit.geojson")
{ "type" => "file", "name" => entry, "path" => rel, "kinds" => Geojson.kinds(full), "cached" => true }
end
end
end