Turn POI markers into pins and use a real icon set
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s

Colored circles for waypoints looked too similar to a base map style's own
small circular POI icons, so uploaded POI data blended into the map instead
of standing out. Switched the marker shape to a pin/teardrop (reusing the
existing WAYPOINT_ICON path, filled instead of stroked) — no base map style
uses that shape for its own points, so it reads immediately as "your data".
Anchored at its tip, the standard map-pin convention, rather than centered
on the coordinate.

Also swapped the hand-drawn category glyphs for the National Park Service's
own map symbols (github.com/nationalparkservice/symbol-library, BSD
licensed, forked from Mapbox's Maki) for the six categories it covers —
professional, purpose-built pictograms beat ad-hoc "fuel pump"/"picnic
table" line art. `forest` has no NPS equivalent and stays hand-drawn.

The glyph now sits in the pin's head rather than centered on a flat dot,
which needed real geometry (both layers anchor to the same point, so the
offset is the gap between where the pin's head-center ends up and where the
glyph's own visual center already sits) rather than the old anchor="center"
zero-offset placement.
This commit is contained in:
randy committed 2026-09-07 16:37:31 +02:00
1 parent 0b18dc018a
commit 2a83bb10ca
3 files changed
+143 -50

No files matched your search

+49 -11
View File
@@ -1,5 +1,14 @@
import { Popup } from "/vendor/maplibre-gl/maplibre-gl.mjs";
import { GPX_ICON, NEW_FOLDER_ICON, UPLOAD_ICON, WAYPOINT_ICON, TRACK_ICON, ROUTE_ICON, POI_ICONS } from "/js/icons.js";
import {
GPX_ICON,
NEW_FOLDER_ICON,
UPLOAD_ICON,
WAYPOINT_ICON,
TRACK_ICON,
ROUTE_ICON,
MAP_PIN_ICON,
POI_ICONS,
} from "/js/icons.js";
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
import { formatActivityType } from "/js/track_profile.js";
@@ -10,7 +19,24 @@ 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 unclustered circle's 14px diameter
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);
// 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
@@ -45,10 +71,11 @@ 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 };
await Promise.all(
POI_ICON_KEYS.map(async (key) => {
Object.keys(sources).map(async (key) => {
if (this._map.hasImage(key)) return;
const imageData = await svgToImageData(POI_ICONS[key], POI_ICON_IMAGE_SIZE);
const imageData = await svgToImageData(sources[key], POI_ICON_IMAGE_SIZE);
if (!this._map.hasImage(key)) this._map.addImage(key, imageData, { sdf: true });
}),
);
@@ -397,21 +424,30 @@ 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.
this._map.addLayer({
id: `${pointsId}-unclustered`,
type: "circle",
type: "symbol",
source: pointsId,
filter: ["!", ["has", "point_count"]],
paint: {
"circle-radius": 7,
"circle-color": ["coalesce", ["get", "color"], TRACK_COLOR],
"circle-stroke-color": "#fff",
"circle-stroke-width": 1.5,
layout: {
"icon-image": MAP_PIN_ID,
"icon-size": MAP_PIN_SIZE,
"icon-anchor": "bottom",
"icon-allow-overlap": true,
"icon-ignore-placement": true,
},
paint: { "icon-color": ["coalesce", ["get", "color"], TRACK_COLOR] },
});
// 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 colored dot above with no icon.
// 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.
this._map.addLayer({
id: `${pointsId}-unclustered-icon`,
type: "symbol",
@@ -420,6 +456,8 @@ 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,
},