Color-code, cluster and add icons to POI waypoints
A park4night/OsmAnd POI export (e.g. a p4n2gpx area export) tags every waypoint with a category and a per-category color/icon in its <extensions> block, all of which was previously thrown away — every waypoint rendered as the same plain red dot, unreadable once a file has a few thousand of them. Maptool::Gpx now captures category/color/icon (waypoint_style), matched by namespace URI rather than local element name: a p4n2gpx file carries both <osmand:icon> and <locus:icon> side by side, and REXML strips prefixes from Element#name, so both report as just "icon" — matching on that alone was silently picking the (useless) Locus drawable path instead of the OsmAnd category key. Regression test added for this. On the map: waypoints are colored by category (falling back to the plain track color for anything uncategorized), a small hand-drawn icon set (public/js/icons.js's POI_ICONS, rasterized to SDF sprites at runtime) draws a glyph on top for the categories it covers, the points source is clustered so a dense export collapses into count bubbles instead of a solid blob, and a legend in the side panel lists whichever categories are currently visible. Splitting each file's GeoJSON into a lines source and a points source (rather than one mixed source with a geometry-type filter per layer) was needed for clustering, since MapLibre's cluster support only understands a source made entirely of points.
This commit is contained in:
1 parent
708c08259b
commit
0b18dc018a
8 files changed
+434
-35
No files matched your search
+58
-12
@@ -193,17 +193,63 @@ route icon next to a route file and a solid one for a track
|
||||
`Fit` reads Garmin's binary FIT format directly
|
||||
(hand-rolled — see below) into a single `MultiLineString` `track`
|
||||
feature; FIT activity files don't really have a GPX-waypoint
|
||||
equivalent, so it never produces `Point` features. MapLibre's
|
||||
`geometry-type` expression treats `MultiLineString` the same as
|
||||
`LineString`, so a single GeoJSON source per file with a `line` layer
|
||||
and a `circle` layer (each filtered by geometry type) is enough to
|
||||
render both. Checking a file fetches its GeoJSON itself
|
||||
(`public/js/gpx.js`'s `_addLayers`) and hands the parsed result to
|
||||
`addSource` — rather than passing MapLibre a URL and letting it fetch
|
||||
internally — specifically so a `422` (a file REXML or `Fit` can't
|
||||
parse) surfaces as an alert and un-checks the box instead of the layer
|
||||
just silently never appearing, which is what a `data: url` source does
|
||||
on a failed fetch.
|
||||
equivalent, so it never produces `Point` features. Checking a file
|
||||
fetches its GeoJSON itself (`public/js/gpx.js`'s `_addLayers`) and
|
||||
hands the parsed result to `addSource` — rather than passing MapLibre a
|
||||
URL and letting it fetch internally — specifically so a `422` (a file
|
||||
REXML or `Fit` can't parse) surfaces as an alert and un-checks the box
|
||||
instead of the layer just silently never appearing, which is what a
|
||||
`data: url` source does on a failed fetch. The fetched features are
|
||||
then split client-side into two GeoJSON sources per file, a
|
||||
lines-and-routes one and a points-only one, rather than one mixed
|
||||
source with a `geometry-type` filter per layer (the previous design):
|
||||
MapLibre's built-in point clustering (below) only understands a source
|
||||
made entirely of `Point`/`MultiPoint` features, so a file mixing a
|
||||
track with waypoints — or a several-thousand-waypoint POI export with
|
||||
no line at all — needs the points on their own source regardless of
|
||||
what else the file's GeoJSON also carries.
|
||||
|
||||
POI category styling & clustering:: A POI export (park4night via
|
||||
p4n2gpx, OsmAnd) tags each waypoint with a category (`<type>`) and, in
|
||||
a vendor `<extensions>` block, a per-category color and OsmAnd icon key
|
||||
(e.g. `amenity_parking`) — `Maptool::Gpx.waypoint_style` reads both
|
||||
into `category`/`color` properties (an OsmAnd export can carry a
|
||||
`<locus:icon>` alongside `<osmand:icon>`, both of which strip down to
|
||||
the same local element name "icon" once REXML drops namespace
|
||||
prefixes, so this specifically matches on namespace *URI*
|
||||
(`https://osmand.net`), not local name, to avoid picking whichever
|
||||
happens to come last in the file). The unclustered-points map layer
|
||||
colors each dot via `["coalesce", ["get", "color"], TRACK_COLOR]`
|
||||
(falling back to the plain track color for a waypoint with no
|
||||
category, e.g. a hand-placed GNSS pin), and a small curated icon set
|
||||
(`public/js/icons.js`'s `POI_ICONS`, ~7 hand-drawn glyphs covering the
|
||||
categories p4n2gpx actually uses, not an exhaustive OsmAnd icon set) is
|
||||
rasterized once per style load (`_registerPoiIcons`, an SVG →
|
||||
`<img>` → `<canvas>` → `ImageData` round trip, since these need to be
|
||||
real raster images for `map.addImage`) and registered as SDF images —
|
||||
a single alpha shape MapLibre tints at render time via `icon-color`
|
||||
rather than a fixed-color image — so a symbol layer can draw a white
|
||||
glyph on top of each dot for any waypoint whose `icon` matches a known
|
||||
key (filtered out entirely for anything else, so an unrecognized icon
|
||||
key just falls back to a plain colored dot rather than a broken/missing
|
||||
image warning). The points source itself is created with
|
||||
`cluster: true`; nearby points collapse into a stepped-radius,
|
||||
sequential-blue circle sized by `point_count` below `clusterMaxZoom`,
|
||||
clicking one zooms to its `getClusterExpansionZoom()`, and this is
|
||||
harmless to enable unconditionally — a source with nothing dense enough
|
||||
to cluster just renders every point individually as usual. A small
|
||||
legend (`.gpx-legend` in the side panel) lists every distinct category
|
||||
+ color currently visible across all checked files, rebuilt from the
|
||||
same points GeoJSON kept around for exactly this
|
||||
(`GpxPanelControl#_pointsByPath`) whenever a file is toggled on/off; it
|
||||
stays hidden for files with no categorized waypoints (a plain GPS
|
||||
track has none). The legend swatch color is set via
|
||||
`swatch.style.background = color` (a DOM property assignment) rather
|
||||
than interpolating the untrusted color string into an `innerHTML`
|
||||
attribute — `escapeHtml` (used elsewhere on this page) only guards
|
||||
element content, not attribute-quote escaping, so building
|
||||
`style="background:${color}"` as a string would have been an
|
||||
injection risk for a crafted GPX file.
|
||||
|
||||
FIT parsing:: No gem was added for this — the one Ruby option
|
||||
(`fit-parser`, tjwallace/fit) hasn't been touched since 2022 and pulls
|
||||
@@ -234,7 +280,7 @@ Like the radar overlay, GPX layers are removed by `map.setStyle()`
|
||||
whichever files were checked.
|
||||
|
||||
Waypoint popups:: Clicking a waypoint circle opens a MapLibre popup
|
||||
with its name, description and time. Click/hover handling is
|
||||
with its name, category (if any), description and time. Click/hover handling is
|
||||
registered once per file (via `map.on(type, layerId, handler)`, a
|
||||
delegated listener that survives `setStyle()`) rather than on every
|
||||
`style.load` re-add, which would otherwise stack duplicate handlers
|
||||
|
||||
Reference in new issue
Block a user