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:
1 parent
87df90f75c
commit
06ea8ea141
5 files changed
+58
-3
No files matched your search
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in new issue
Block a user