Swap the pin marker for a rounded-square badge
CI / check (push) Successful in 43s
CI / deploy (push) Successful in 14s

The National Park Service frames its own map symbols (the icons already
vendored in for POI categories) in a rounded square, not a pin — pairing a
pin shape with NPS-style pictograms looked like two mismatched icon
conventions. Switched the marker to a rounded square to match.

This also simplifies the placement code: a pin has to anchor at its tip, so
the category glyph on top needed an offset computed from the pin/glyph
geometry to land in the pin's head. A square has no tip — both the badge and
the glyph layer just center directly on the coordinate with no offset math
at all.
This commit is contained in:
randy committed 2026-09-07 16:45:22 +02:00
1 parent 2a83bb10ca
commit 7de8428b05
3 files changed
+50 -64

No files matched your search

+22 -23
View File
@@ -219,38 +219,37 @@ 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). Individual waypoints render as a
pin (`public/js/icons.js`'s `MAP_PIN_ICON`, the same path as the
existing `WAYPOINT_ICON` outline, filled instead of stroked) rather
rounded-square badge (`public/js/icons.js`'s `MAP_MARKER_ICON`) rather
than a plain circle — a flat colored dot turned out to look too much
like a base map style's own small circular POI icons, blending in
instead of reading as "your data"; a pin is a shape convention no base
map uses for its own points. It's colored via
`["coalesce", ["get", "color"], TRACK_COLOR]` (falling back to the
instead of reading as "your data". A rounded square rather than a
pin/teardrop: it matches how the National Park Service itself frames
these exact category symbols (below) on its own maps, so the two look
native together instead of like mismatched icon styles. It's colored
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 anchored at its tip (`icon-anchor: "bottom"`, the
standard map-pin convention — the tip, not the shape's center, lands on
the actual coordinate). A small curated icon set
(`public/js/icons.js`'s `POI_ICONS`, covering the categories p4n2gpx
actually uses, not an exhaustive OsmAnd icon set) draws a white glyph
on top of the pin's head, in a symbol layer offset up from the shared
tip anchor by a size computed from the pin and glyph geometry
(`POI_ICON_OFFSET_Y` — both layers anchor to the same raster canvas
edge, so the offset is "how far above that shared anchor is the pin's
head-center, minus how far above it the glyph's own visual center
already sits at its own icon-size"), filtered to just the known keys
(anything else keeps the plain pin with no glyph, rather than a
GNSS pin) and — unlike a pin, which has to anchor at its tip — centered
directly on the coordinate with plain default anchoring, which also
means the category-glyph layer on top needs no offset math: both
layers just share the same center anchor at `[0, 0]`. A small curated
icon set (`public/js/icons.js`'s `POI_ICONS`, covering the categories
p4n2gpx actually uses, not an exhaustive OsmAnd icon set) draws a white
glyph on top of the badge 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 badge rather than a
broken/missing image warning). Six of the seven glyphs are the
National Park Service's own map symbols
(`github.com/nationalparkservice/symbol-library`, BSD-licensed,
originally forked from Mapbox's Maki project) — a professional,
purpose-built icon set beats hand-drawing "fuel pump"/"picnic table"
glyphs from scratch; `forest` has no NPS equivalent (that set covers
facilities, not land cover) and stays hand-drawn. Both the pin and the
category glyphs are 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. The points source itself
facilities, not land cover) and stays hand-drawn. Both the badge and
the category glyphs are 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. 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`,
+17 -33
View File
@@ -6,7 +6,7 @@ import {
WAYPOINT_ICON,
TRACK_ICON,
ROUTE_ICON,
MAP_PIN_ICON,
MAP_MARKER_ICON,
POI_ICONS,
} from "/js/icons.js";
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
@@ -19,24 +19,9 @@ const MOVE_URL = "/api/gpx/move";
const TRACK_COLOR = "#e63946";
const POI_ICON_KEYS = Object.keys(POI_ICONS);
const POI_ICON_IMAGE_SIZE = 48; // rasterized once at this size, rendered much smaller by icon-size below
const POI_ICON_SIZE = 0.3; // 48 * 0.3 = ~14px, matching the pin head's diameter below
// The pin glyph (icons.js's MAP_PIN_ICON) is a 24x24 viewBox with its head
// circle centered at y=9, rasterized onto the same POI_ICON_IMAGE_SIZE
// canvas as every other icon. icon-anchor: "bottom" anchors both this layer
// and the category-glyph layer below to the *raster canvas's* bottom edge
// (not the pin's visual tip, which sits a little above that edge) — so both
// layers share one screen anchor point, and placing the glyph in the pin's
// head is just "how far above that shared anchor is each icon's own visual
// center, and what's the gap between them":
const MAP_PIN_ID = "map-pin";
const MAP_PIN_SIZE = 0.55;
const MAP_PIN_HEAD_CENTER_SVG_Y = 9;
const MAP_PIN_VIEWBOX = 24;
const pinHeadCenterAboveAnchorPx =
(MAP_PIN_VIEWBOX - MAP_PIN_HEAD_CENTER_SVG_Y) * (POI_ICON_IMAGE_SIZE / MAP_PIN_VIEWBOX) * MAP_PIN_SIZE;
const glyphOwnCenterAboveAnchorPx = (POI_ICON_IMAGE_SIZE * POI_ICON_SIZE) / 2; // glyph content roughly fills its own canvas
const POI_ICON_OFFSET_Y = -((pinHeadCenterAboveAnchorPx - glyphOwnCenterAboveAnchorPx) / POI_ICON_SIZE);
const POI_ICON_SIZE = 0.3; // 48 * 0.3 = ~14px, sized to sit inside the marker badge below
const MAP_MARKER_ID = "map-marker";
const MAP_MARKER_SIZE = 0.55;
// Custom drag payload MIME type for moving an already-uploaded file between
// folders, as opposed to an OS file drag (type "Files") for uploading a new
@@ -71,7 +56,7 @@ export class GpxPanelControl {
// color information worth preserving, and SDF lets the same sprite be
// reused if a future category needs a different tint.
async _registerPoiIcons() {
const sources = { ...POI_ICONS, [MAP_PIN_ID]: MAP_PIN_ICON };
const sources = { ...POI_ICONS, [MAP_MARKER_ID]: MAP_MARKER_ICON };
await Promise.all(
Object.keys(sources).map(async (key) => {
if (this._map.hasImage(key)) return;
@@ -424,20 +409,23 @@ export class GpxPanelControl {
layout: { "text-field": ["get", "point_count_abbreviated"], "text-font": ["Noto Sans Regular"], "text-size": 11 },
paint: { "text-color": "#fff" },
});
// A pin (icons.js's MAP_PIN_ICON), not a plain circle: a flat colored
// dot looks too much like a base map style's own small circular POI
// icons, so individual waypoints blended in and were hard to spot as
// "your data" versus the map's own points. Anchored at its tip (the
// standard map-pin convention), not centered on the coordinate.
// A rounded-square badge (icons.js's MAP_MARKER_ICON), not a plain
// circle: a flat colored dot looked too much like a base map style's
// own small circular POI icons, so individual waypoints blended in
// instead of standing out as "your data". A rounded square rather than
// a pin/teardrop specifically to match how the NPS itself frames these
// exact category symbols (POI_ICONS below) on its own maps. Centered on
// the coordinate — a square has no natural "tip" the way a pin does, so
// default center anchoring is simplest and also means the glyph layer
// below needs no offset, just the same center anchor.
this._map.addLayer({
id: `${pointsId}-unclustered`,
type: "symbol",
source: pointsId,
filter: ["!", ["has", "point_count"]],
layout: {
"icon-image": MAP_PIN_ID,
"icon-size": MAP_PIN_SIZE,
"icon-anchor": "bottom",
"icon-image": MAP_MARKER_ID,
"icon-size": MAP_MARKER_SIZE,
"icon-allow-overlap": true,
"icon-ignore-placement": true,
},
@@ -445,9 +433,7 @@ export class GpxPanelControl {
});
// Only for the curated POI_ICON_KEYS (see icons.js) — anything else
// (a plain hand-placed waypoint, or a category p4n2gpx/OsmAnd hasn't
// been taught here) just keeps the plain pin above with no glyph.
// icon-offset moves it up from the anchor (the pin's tip) into the
// pin's round head — see POI_ICON_OFFSET_Y's comment above.
// been taught here) just keeps the plain badge above with no glyph.
this._map.addLayer({
id: `${pointsId}-unclustered-icon`,
type: "symbol",
@@ -456,8 +442,6 @@ export class GpxPanelControl {
layout: {
"icon-image": ["get", "icon"],
"icon-size": POI_ICON_SIZE,
"icon-anchor": "bottom",
"icon-offset": [0, POI_ICON_OFFSET_Y],
"icon-allow-overlap": true,
"icon-ignore-placement": true,
},
+11 -8
View File
@@ -58,14 +58,17 @@ export const ROUTE_ICON = `
<path d="M6.5 7.5C10 11 8 13 12 13s2 5 6.5 5" stroke-dasharray="2 2.2"/>
</svg>`;
// Filled version of WAYPOINT_ICON's outline (same path, fill instead of
// stroke) — used as the map marker for individual waypoints, tinted by
// category color via icon-color (see gpx.js). A pin/teardrop shape rather
// than a plain dot specifically so it doesn't blend in with a base map
// style's own small circular POI icons; the tip (not the head center) is
// the anchor point, the standard map-pin convention.
export const MAP_PIN_ICON = `
<svg viewBox="0 0 24 24"><path d="M12 21s7-7.58 7-12a7 7 0 0 0-14 0c0 4.42 7 12 7 12z" fill="currentColor"/></svg>`;
// A rounded-square badge — used as the map marker for individual waypoints,
// tinted by category color via icon-color (see gpx.js). Not a plain dot,
// which blended in with a base map style's own small circular POI icons,
// and not a pin/teardrop either — a rounded square is the National Park
// Service's own convention for framing these exact category symbols
// (POI_ICONS below), so pairing the two looks native rather than like two
// mismatched icon styles. Centered on the coordinate (no tip to anchor at),
// which also means the category glyph on top needs no offset — both layers
// just share the same center anchor at [0, 0].
export const MAP_MARKER_ICON = `
<svg viewBox="0 0 24 24"><rect x="1" y="1" width="22" height="22" rx="5" fill="currentColor"/></svg>`;
// POI category glyphs (park4night/OsmAnd exports), keyed by the OsmAnd icon
// name a file's <osmand:icon> extension carries — a small curated set for