Add file-kind icons to the GPX tree, fix drop into an empty panel

Each tree entry now shows a small waypoint/track icon based on a
cheap substring check for <wpt>/<trk> (Maptool::Gpx.kinds) rather than
a full REXML parse, since re-parsing a several-thousand-waypoint file
on every tree fetch would make the panel noticeably slower to open.

Also fix a real drop-target bug: .gpx-tree had no flex:1, so a
short/empty tree left blank panel space below it that wasn't part of
the drag & drop target — dropping a file there silently did nothing.
This commit is contained in:
randy committed 2026-08-25 22:38:41 +02:00
1 parent 87df90f75c
commit 06ea8ea141
5 files changed
+58 -3

No files matched your search

+13
View File
@@ -8,6 +8,19 @@ module Maptool
module Gpx module Gpx
class ParseError < StandardError; end class ParseError < StandardError; end
# Cheap content-type check for the file tree's icons: a plain substring
# search rather than a full REXML parse, since some uploaded files (e.g.
# a several-thousand-waypoint POI export) are large enough that parsing
# the whole DOM just to answer "does it have any of these tags" would
# make every tree fetch noticeably slower.
def self.kinds(path)
content = File.read(path)
kinds = []
kinds << "waypoint" if content.include?("<wpt")
kinds << "track" if content.include?("<trk")
kinds
end
def self.to_geojson(path) def self.to_geojson(path)
root = REXML::Document.new(File.read(path)).root root = REXML::Document.new(File.read(path)).root
raise ParseError, "not a GPX file" unless root && root.name == "gpx" raise ParseError, "not a GPX file" unless root && root.name == "gpx"
+2 -1
View File
@@ -1,4 +1,5 @@
require "fileutils" require "fileutils"
require_relative "gpx"
module Maptool module Maptool
# Filesystem-backed store for uploaded GPX files, organized in # Filesystem-backed store for uploaded GPX files, organized in
@@ -117,7 +118,7 @@ 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 } { "type" => "file", "name" => entry, "path" => rel, "kinds" => Gpx.kinds(full) }
end end
end end
end end
+17 -1
View File
@@ -292,6 +292,11 @@ html, body {
} }
.gpx-tree { .gpx-tree {
/* flex:1 (not just overflow-y) so the tree fills the rest of the panel
even when short/empty — otherwise the blank space below it wouldn't
be part of the drop target and dropping a file there would silently
do nothing. */
flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 8px 12px; padding: 8px 12px;
} }
@@ -342,12 +347,23 @@ html, body {
cursor: pointer; cursor: pointer;
} }
.gpx-tree__file label span { .gpx-tree__file-name {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.gpx-tree__kind-icon {
flex-shrink: 0;
display: flex;
color: #999;
}
.gpx-tree__kind-icon svg {
width: 14px;
height: 14px;
}
.gpx-tree__delete { .gpx-tree__delete {
flex-shrink: 0; flex-shrink: 0;
border: none; border: none;
+16 -1
View File
@@ -1,5 +1,5 @@
import { Popup } from "/vendor/maplibre-gl/maplibre-gl.mjs"; import { Popup } from "/vendor/maplibre-gl/maplibre-gl.mjs";
import { GPX_ICON, NEW_FOLDER_ICON, UPLOAD_ICON } from "/js/icons.js"; import { GPX_ICON, NEW_FOLDER_ICON, UPLOAD_ICON, WAYPOINT_ICON, TRACK_ICON } from "/js/icons.js";
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js"; import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
const TREE_URL = "/api/gpx/tree"; const TREE_URL = "/api/gpx/tree";
@@ -205,9 +205,11 @@ export class GpxPanelControl {
checkbox.checked = this._visible.has(file.path); checkbox.checked = this._visible.has(file.path);
checkbox.addEventListener("change", () => this._toggleFile(file.path, checkbox.checked)); checkbox.addEventListener("change", () => this._toggleFile(file.path, checkbox.checked));
const name = document.createElement("span"); const name = document.createElement("span");
name.className = "gpx-tree__file-name";
name.textContent = file.name; name.textContent = file.name;
name.title = file.name; name.title = file.name;
label.appendChild(checkbox); label.appendChild(checkbox);
(file.kinds || []).forEach((kind) => label.appendChild(this._kindIcon(kind)));
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)));
@@ -215,6 +217,19 @@ export class GpxPanelControl {
return row; return row;
} }
_kindIcon(kind) {
const icon = document.createElement("span");
icon.className = "gpx-tree__kind-icon";
if (kind === "waypoint") {
icon.innerHTML = WAYPOINT_ICON;
icon.title = "Has waypoints";
} else {
icon.innerHTML = TRACK_ICON;
icon.title = "Has a track";
}
return icon;
}
_deleteButton(onClick) { _deleteButton(onClick) {
const button = document.createElement("button"); const button = document.createElement("button");
button.type = "button"; button.type = "button";
+10
View File
@@ -37,3 +37,13 @@ export const UPLOAD_ICON = `
<path d="M12 15V4M8 8l4-4 4 4"/> <path d="M12 15V4M8 8l4-4 4 4"/>
<path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/> <path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/>
</svg>`; </svg>`;
// Shown next to each file in the GPX tree, indicating whether it has
// waypoints, a track, or both.
export const WAYPOINT_ICON = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 21s7-7.58 7-12a7 7 0 0 0-14 0c0 4.42 7 12 7 12z"/>
<circle cx="12" cy="9" r="2.2"/>
</svg>`;
export const TRACK_ICON = GPX_ICON;