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* prefixes, so this specifically matches on namespace *URI*
(`https://osmand.net`), not local name, to avoid picking whichever (`https://osmand.net`), not local name, to avoid picking whichever
happens to come last in the file). Individual waypoints render as a 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 rounded-square badge (`public/js/icons.js`'s `MAP_MARKER_ICON`) rather
existing `WAYPOINT_ICON` outline, filled instead of stroked) rather
than a plain circle — a flat colored dot turned out to look too much 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 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 instead of reading as "your data". A rounded square rather than a
map uses for its own points. It's colored via pin/teardrop: it matches how the National Park Service itself frames
`["coalesce", ["get", "color"], TRACK_COLOR]` (falling back to the 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 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 GNSS pin) and — unlike a pin, which has to anchor at its tip — centered
standard map-pin convention — the tip, not the shape's center, lands on directly on the coordinate with plain default anchoring, which also
the actual coordinate). A small curated icon set means the category-glyph layer on top needs no offset math: both
(`public/js/icons.js`'s `POI_ICONS`, covering the categories p4n2gpx layers just share the same center anchor at `[0, 0]`. A small curated
actually uses, not an exhaustive OsmAnd icon set) draws a white glyph icon set (`public/js/icons.js`'s `POI_ICONS`, covering the categories
on top of the pin's head, in a symbol layer offset up from the shared p4n2gpx actually uses, not an exhaustive OsmAnd icon set) draws a white
tip anchor by a size computed from the pin and glyph geometry glyph on top of the badge for any waypoint whose `icon` matches a known
(`POI_ICON_OFFSET_Y` — both layers anchor to the same raster canvas key (filtered out entirely for anything else, so an unrecognized icon
edge, so the offset is "how far above that shared anchor is the pin's key just falls back to a plain colored badge rather than a
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
broken/missing image warning). Six of the seven glyphs are the broken/missing image warning). Six of the seven glyphs are the
National Park Service's own map symbols National Park Service's own map symbols
(`github.com/nationalparkservice/symbol-library`, BSD-licensed, (`github.com/nationalparkservice/symbol-library`, BSD-licensed,
originally forked from Mapbox's Maki project) — a professional, originally forked from Mapbox's Maki project) — a professional,
purpose-built icon set beats hand-drawing "fuel pump"/"picnic table" purpose-built icon set beats hand-drawing "fuel pump"/"picnic table"
glyphs from scratch; `forest` has no NPS equivalent (that set covers glyphs from scratch; `forest` has no NPS equivalent (that set covers
facilities, not land cover) and stays hand-drawn. Both the pin and the facilities, not land cover) and stays hand-drawn. Both the badge and
category glyphs are rasterized once per style load (`_registerPoiIcons`, the category glyphs are rasterized once per style load
an SVG → `<img>` → `<canvas>` → `ImageData` round trip, since these (`_registerPoiIcons`, an SVG → `<img>` → `<canvas>` → `ImageData` round
need to be real raster images for `map.addImage`) and registered as SDF trip, since these need to be real raster images for `map.addImage`) and
images — a single alpha shape MapLibre tints at render time via registered as SDF images — a single alpha shape MapLibre tints at
`icon-color` rather than a fixed-color image. The points source itself render time via `icon-color` rather than a fixed-color image. The
points source itself
is created with is created with
`cluster: true`; nearby points collapse into a stepped-radius, `cluster: true`; nearby points collapse into a stepped-radius,
sequential-blue circle sized by `point_count` below `clusterMaxZoom`, sequential-blue circle sized by `point_count` below `clusterMaxZoom`,
+17 -33
View File
@@ -6,7 +6,7 @@ import {
WAYPOINT_ICON, WAYPOINT_ICON,
TRACK_ICON, TRACK_ICON,
ROUTE_ICON, ROUTE_ICON,
MAP_PIN_ICON, MAP_MARKER_ICON,
POI_ICONS, POI_ICONS,
} from "/js/icons.js"; } from "/js/icons.js";
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js"; import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
@@ -19,24 +19,9 @@ const MOVE_URL = "/api/gpx/move";
const TRACK_COLOR = "#e63946"; const TRACK_COLOR = "#e63946";
const POI_ICON_KEYS = Object.keys(POI_ICONS); 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_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 const POI_ICON_SIZE = 0.3; // 48 * 0.3 = ~14px, sized to sit inside the marker badge below
const MAP_MARKER_ID = "map-marker";
// The pin glyph (icons.js's MAP_PIN_ICON) is a 24x24 viewBox with its head const MAP_MARKER_SIZE = 0.55;
// 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);
// Custom drag payload MIME type for moving an already-uploaded file between // 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 // 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 // color information worth preserving, and SDF lets the same sprite be
// reused if a future category needs a different tint. // reused if a future category needs a different tint.
async _registerPoiIcons() { 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( await Promise.all(
Object.keys(sources).map(async (key) => { Object.keys(sources).map(async (key) => {
if (this._map.hasImage(key)) return; 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 }, layout: { "text-field": ["get", "point_count_abbreviated"], "text-font": ["Noto Sans Regular"], "text-size": 11 },
paint: { "text-color": "#fff" }, paint: { "text-color": "#fff" },
}); });
// A pin (icons.js's MAP_PIN_ICON), not a plain circle: a flat colored // A rounded-square badge (icons.js's MAP_MARKER_ICON), not a plain
// dot looks too much like a base map style's own small circular POI // circle: a flat colored dot looked too much like a base map style's
// icons, so individual waypoints blended in and were hard to spot as // own small circular POI icons, so individual waypoints blended in
// "your data" versus the map's own points. Anchored at its tip (the // instead of standing out as "your data". A rounded square rather than
// standard map-pin convention), not centered on the coordinate. // 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({ this._map.addLayer({
id: `${pointsId}-unclustered`, id: `${pointsId}-unclustered`,
type: "symbol", type: "symbol",
source: pointsId, source: pointsId,
filter: ["!", ["has", "point_count"]], filter: ["!", ["has", "point_count"]],
layout: { layout: {
"icon-image": MAP_PIN_ID, "icon-image": MAP_MARKER_ID,
"icon-size": MAP_PIN_SIZE, "icon-size": MAP_MARKER_SIZE,
"icon-anchor": "bottom",
"icon-allow-overlap": true, "icon-allow-overlap": true,
"icon-ignore-placement": true, "icon-ignore-placement": true,
}, },
@@ -445,9 +433,7 @@ export class GpxPanelControl {
}); });
// Only for the curated POI_ICON_KEYS (see icons.js) — anything else // Only for the curated POI_ICON_KEYS (see icons.js) — anything else
// (a plain hand-placed waypoint, or a category p4n2gpx/OsmAnd hasn't // (a plain hand-placed waypoint, or a category p4n2gpx/OsmAnd hasn't
// been taught here) just keeps the plain pin above with no glyph. // been taught here) just keeps the plain badge 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.
this._map.addLayer({ this._map.addLayer({
id: `${pointsId}-unclustered-icon`, id: `${pointsId}-unclustered-icon`,
type: "symbol", type: "symbol",
@@ -456,8 +442,6 @@ export class GpxPanelControl {
layout: { layout: {
"icon-image": ["get", "icon"], "icon-image": ["get", "icon"],
"icon-size": POI_ICON_SIZE, "icon-size": POI_ICON_SIZE,
"icon-anchor": "bottom",
"icon-offset": [0, POI_ICON_OFFSET_Y],
"icon-allow-overlap": true, "icon-allow-overlap": true,
"icon-ignore-placement": 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"/> <path d="M6.5 7.5C10 11 8 13 12 13s2 5 6.5 5" stroke-dasharray="2 2.2"/>
</svg>`; </svg>`;
// Filled version of WAYPOINT_ICON's outline (same path, fill instead of // A rounded-square badge — used as the map marker for individual waypoints,
// stroke) — used as the map marker for individual waypoints, tinted by // tinted by category color via icon-color (see gpx.js). Not a plain dot,
// category color via icon-color (see gpx.js). A pin/teardrop shape rather // which blended in with a base map style's own small circular POI icons,
// than a plain dot specifically so it doesn't blend in with a base map // and not a pin/teardrop either — a rounded square is the National Park
// style's own small circular POI icons; the tip (not the head center) is // Service's own convention for framing these exact category symbols
// the anchor point, the standard map-pin convention. // (POI_ICONS below), so pairing the two looks native rather than like two
export const MAP_PIN_ICON = ` // mismatched icon styles. Centered on the coordinate (no tip to anchor at),
<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>`; // 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 // POI category glyphs (park4night/OsmAnd exports), keyed by the OsmAnd icon
// name a file's <osmand:icon> extension carries — a small curated set for // name a file's <osmand:icon> extension carries — a small curated set for