@mapmap/maps 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,17 +90,86 @@ new MapMapMap({
90
90
  ```ts
91
91
  const routes = new RouteLayer(map /* MapMapMap or maplibregl.Map */, {
92
92
  baseUrl?, apiKey?, id?, design?, // design: extra.nav route block
93
+ endpoints?, // start/end/waypoint markers, off by default
93
94
  });
94
95
 
95
96
  await routes.route(from, to, { profile, truck }); // two-point
96
97
  await routes.routePath([a, b, c], { profile, truck }); // multi-point
97
- routes.current; // last ParsedRoute
98
- routes.clear(); // remove the drawn line
98
+ routes.draw(parsedRoute); // a route parsed elsewhere
99
+ routes.current; // last ParsedRoute
100
+ routes.ids; // the source/layer ids this layer owns
101
+ routes.clear(); // remove the drawn line and everything hung off it
102
+ routes.destroy(); // clear() + detach the style.load listener
103
+
104
+ // Draw geometry you already have, with no ParsedRoute to hand
105
+ routes.drawGeometry([[lng, lat], ...]);
106
+
107
+ // Intermediate stops, for the numbered waypoint markers
108
+ routes.setWaypoints([[lng, lat]]);
109
+ routes.currentWaypoints; // read them back
110
+
111
+ // Alternatives: one selected, the rest dimmer beneath and clickable
112
+ routes.drawAlternatives(parsedRoutes, selectedIndex);
113
+ routes.onSelectAlternative((i) => routes.drawAlternatives(parsedRoutes, i));
114
+ routes.alternativeRoutes; // what is currently drawn as an alternative
115
+
116
+ // Ferry legs as dashes over the selected line, so water does not read
117
+ // as driving. Supplied explicitly: line-dasharray cannot be data-driven,
118
+ // and only you know which parts of your route are ferries.
119
+ routes.setFerrySegments([[[lng, lat], ...]]);
120
+ routes.clearFerrySegments();
121
+
122
+ // Runtime paint, over the design. Survives style reloads; `{}` clears.
123
+ // A provisional or straight-line-approximated route, for example:
124
+ routes.setLineStyle({ color: "#9096a2", dash: [1.6, 1.4], casingOpacity: 0 });
99
125
  ```
100
126
 
127
+ `ids` names every MapLibre id the layer owns, so you can restyle or reorder
128
+ them: `source`, `casing`, `line`, `maneuverSource`, `maneuver`,
129
+ `corridorSource`, `corridor`, `endpointsSource` and `endpoints`. All are
130
+ derived from the `id` option (default `mapmap-route`).
131
+
132
+ #### Start, end and waypoint markers
133
+
134
+ `endpoints: true` draws branded markers on the route: a green `dot` at the
135
+ first coordinate, a signal-blue `pin` at the last, and smaller pins
136
+ numbered 1..n at each intermediate stop. Override any of them, or pass
137
+ `false` to drop one:
138
+
139
+ ```ts
140
+ new RouteLayer(map, {
141
+ endpoints: {
142
+ start: { icon: "home", colour: "#3ecf8e", size: "m", label: "Depot" },
143
+ end: { text: "B" }, // 1–3 characters beat the glyph
144
+ waypoint: false, // no intermediate markers
145
+ numberWaypoints: true, // default
146
+ },
147
+ });
148
+ ```
149
+
150
+ The stops come from `routePath`'s via points; when you hand a route parsed
151
+ elsewhere to `draw()` or `drawGeometry()`, set them with
152
+ `routes.setWaypoints([[lng, lat], …])` and read them back from
153
+ `routes.currentWaypoints`. An empty array clears them.
154
+
155
+ The markers are a symbol layer over their own source (both ids
156
+ `<id>-endpoints`, on `routes.ids` as `endpoints` and `endpointsSource`),
157
+ not DOM markers, so they appear in canvas exports and match the native
158
+ SDKs. A pin anchors at its tip, a `dot` at its centre; `label` draws below
159
+ the marker with a white halo and is optional to the collision detector.
160
+ They survive `setStyle`, and `clear()`/`destroy()` removes the layer, the
161
+ source and the images this layer registered. Omit the option and the layer
162
+ behaves exactly as before.
163
+
101
164
  Coordinates accept `[lng, lat]`, `{ lng, lat }` or `{ lon, lat }`. A route
102
165
  resolves to `{ distanceM, durationS, geometry (GeoJSON LineString), raw }`.
103
166
 
167
+ The `design` block takes `color`, `width`, `opacity` and `casingColor`, plus
168
+ three optional fields: `casingWidth` (defaults to `width + 4`),
169
+ `casingOpacity` (defaults to `opacity`) and `dash` (omitted or `null` draws
170
+ solid). Set the first two when your casing is not exactly four wider than
171
+ your line, or when it is translucent under a solid line.
172
+
104
173
  `truck` params map onto the gateway's OSRM truck vendor extensions:
105
174
  `heightM`, `widthM`, `lengthM`, `weightT`, `hazmat`, and `tunnelCode` (ADR
106
175
  8.6.4, e.g. `"C"` or `"B/D"` - the slash is URL-encoded for you). They only
@@ -150,7 +219,7 @@ stores.select("man-01"); // list→map sync: popup + cam
150
219
  stores.deselect(); // close the popup
151
220
  stores.nearest({ lat: 51.5, lon: -0.13 }, 3); // haversine, adds distanceM
152
221
  await stores.nearestByDriveTime({ lat: 51.5, lon: -0.13 }, { n: 3, costing: "truck" });
153
- stores.ids; // { source, points, clusters, clusterCounts } - see below
222
+ stores.ids; // { source, points, clusters, clusterCounts, labels, pointsFallback }
154
223
  stores.clear(); // remove pins; stores.destroy() also detaches listeners
155
224
  ```
156
225
 
@@ -213,17 +282,116 @@ the `Place`, or `undefined` for an unknown id (nothing happens). It does
213
282
  #### Escape-hatch styling: `ids`
214
283
 
215
284
  The generated MapLibre ids are public API via `stores.ids` →
216
- `{ source, points, clusters, clusterCounts }`, so raw MapLibre calls are
217
- supported when the options don't reach far enough:
285
+ `{ source, points, clusters, clusterCounts, labels, pointsFallback }`, so raw
286
+ MapLibre calls are supported when the options don't reach far enough:
218
287
 
219
288
  ```ts
220
289
  map.setPaintProperty(stores.ids.points, "circle-radius", 9);
221
290
  map.queryRenderedFeatures({ layers: [stores.ids.points] });
222
291
  ```
223
292
 
224
- `clusters`/`clusterCounts` are only installed with `cluster: true` (the
225
- default); `points` is a circle layer by default, or a symbol layer once a
226
- custom `icon` has loaded.
293
+ Not every id is installed on the map at all times - the object always
294
+ carries all six, but a layer only exists when its feature is switched on:
295
+
296
+ - `clusters`/`clusterCounts` - only with `cluster: true` (the default).
297
+ - `labels` - only with a `label` option (see below).
298
+ - `pointsFallback` - only when `icon` is a **record** of per-place images:
299
+ it is the circle layer drawn under places whose icon hasn't loaded (or
300
+ doesn't exist).
301
+ - `points` is a circle layer by default, or a symbol layer once a single
302
+ custom `icon` has loaded.
303
+
304
+ #### Per-place labels
305
+
306
+ `label: true` writes each place's `name` beside its point; an options
307
+ object tunes it:
308
+
309
+ ```ts
310
+ new PlacesLayer(map, {
311
+ places,
312
+ label: { property: "name", size: 12, colour: "#333333", haloColour: "#ffffff" },
313
+ });
314
+ ```
315
+
316
+ Both spellings are accepted for the two colour keys - `colour`/`haloColour`
317
+ and `color`/`haloColor` (British wins if both are given) - so a
318
+ dynamically-built options object can't lose its label colours to a spelling
319
+ mismatch with the sibling `color`/`clusterColor` options. Labels only ever
320
+ draw on unclustered points, and hide before colliding (`text-optional`).
321
+
322
+ > Labels need the **`MapMap Sans Regular`** fontstack from the style's
323
+ > `glyphs` endpoint. Every MapMap style ships it; a theme pointed at a
324
+ > custom glyphs host that doesn't serve it drops the labels **silently**
325
+ > (MapLibre logs a glyph 404 and renders the points alone). The same
326
+ > applies to marker labels below.
327
+
328
+ ### `class MarkersLayer` - markers designed in Studio
329
+
330
+ Custom markers and labels that travel **with the theme**, under
331
+ `extra.markers` (schema v1): up to 200 coloured glyph pins, plain dots or
332
+ small custom images, each with an optional label. They are placed in
333
+ Studio's Markers tab, so a designer can ship "here are our depots" with the
334
+ style itself - no places data, no code change:
335
+
336
+ ```ts
337
+ import { MarkersLayer, markersFromThemeUrl } from "@mapmap/maps";
338
+
339
+ const markers = await markersFromThemeUrl(
340
+ "https://api.mapmap.ai/styles/midnight-fleet-a1b2c3/theme",
341
+ );
342
+ if (markers) new MarkersLayer(map /* MapMapMap or maplibregl.Map */, markers);
343
+ ```
344
+
345
+ For a theme object you already have (a downloaded `*.theme.json`), use
346
+ `markersFromTheme(theme)`. Both it and `markersFromThemeUrl` return
347
+ `undefined` when the theme carries no markers block, so "no markers" stays
348
+ distinguishable from "an empty designed set"; `parseMarkers(value)` is the
349
+ same lenient parse over a raw `extra.markers` value.
350
+
351
+ ```ts
352
+ layer.setMarkers(block); // replace - never dropped, even mid-style-load
353
+ layer.setMarkers(undefined); // clear the markers, keep the layer alive
354
+ layer.current; // the parsed items
355
+ layer.ids; // { source: "mm-user-markers", layer: "mm-user-markers" }
356
+ layer.clear(); // remove layer, source and this layer's images
357
+ layer.destroy(); // clear() + detach the style.load listener
358
+ ```
359
+
360
+ - **One source, one layer**, both id `mm-user-markers` (a locked contract
361
+ shared with Studio and the server-side validator). `ids` is public API
362
+ for escape-hatch styling.
363
+ - **Item fields**: `id` (unique, ≤ 64 characters), `lng`/`lat`, `icon` (one
364
+ of 21 glyphs, default `pin`; `dot` draws a plain circle), `colour`
365
+ (`#rrggbb`, default `#1a6bff`), `size` (`s`/`m`/`l` = 24/32/40 px,
366
+ default `m`), `label` (≤ 120 characters), `image` (a `data:` URI -
367
+ png/jpeg/webp/svg+xml, base64, ≤ 64 KB decoded - drawn instead of the
368
+ pin). Lengths are counted in Unicode scalars, so emoji count as one.
369
+ - **Parsing is lenient and never throws**: invalid items are skipped, bad
370
+ fields fall back to defaults, over-long labels are truncated. A block
371
+ whose `version` is not `1` parses to nothing at all - a future v2 must
372
+ not be silently drawn as v1 by an already-installed SDK.
373
+ - **Custom images load asynchronously**: the marker draws its glyph pin
374
+ until the image arrives, then swaps in place. An image MapLibre cannot
375
+ rasterise keeps the pin - pass `{ onImageError }` to hear about it rather
376
+ than wondering why a logo is a blue pin:
377
+
378
+ ```ts
379
+ new MarkersLayer(map, markers, {
380
+ onImageError: (image, error) => console.warn("marker image", image, error),
381
+ });
382
+ ```
383
+
384
+ The usual cause is an **SVG with no intrinsic `width`/`height`**: an
385
+ `<img>` renders it happily, `createImageBitmap` (what `map.loadImage`
386
+ uses) refuses it.
387
+ - **Labels need the `MapMap Sans Regular` fontstack** from the style's
388
+ `glyphs` endpoint, exactly like the places labels above; a custom glyphs
389
+ host without it drops the labels silently.
390
+ - The layer **survives `setStyle`** and re-installs itself on every
391
+ `style.load`. That is load-bearing: MapLibre diffs against a serialised
392
+ style that INCLUDES runtime sources/layers, so a diffed `setStyle`
393
+ removes `mm-user-markers` and the `style.load` that `setState` fires
394
+ afterwards is what puts it back.
227
395
 
228
396
  ### `class NavigationCamera`
229
397
 
@@ -304,6 +472,14 @@ const map = new MapMapMap({
304
472
  - **`extra.nav`** (Studio's navigation design block) is carried by the theme
305
473
  file and ignored by style compilation - see "Using your Studio design"
306
474
  below.
475
+ - **`extra.markers`** (Studio's custom markers & labels, schema v1) rides
476
+ the same way: `{ "version": 1, "items": [ { "id", "lng", "lat", "icon",
477
+ "colour", "size", "label", "image" } ] }`, up to 200 items, never part of
478
+ the compiled `style.json`. Read it with `markersFromTheme(theme)` /
479
+ `markersFromThemeUrl(url)` and draw it with `MarkersLayer` (above).
480
+ `extra` as a whole is bounded at 256 KB serialised, and each marker
481
+ `image` at 64 KB decoded - the per-image caps multiply, so a few
482
+ full-size marker images will hit the block cap first.
307
483
  - **Layer ids** (29, paint order): `background landcover landuse park water
308
484
  waterway aeroway building building-outline rail pedestrian-areas road-path
309
485
  road-minor-casing road-major-casing road-minor road-major
@@ -622,12 +798,14 @@ mapping. Available from the main entry and as a separate
622
798
  ```ts
623
799
  import {
624
800
  directionIcons, // { turn_left: "<svg…>", roundabout_right: …, ferry: … }
625
- iconNameForStep, // (step) => DirectionIconName — preferred
801
+ iconNamesForSteps, // (steps) => DirectionIconName[] — preferred for a turn list
802
+ iconNameForStep, // (step) => DirectionIconName
626
803
  iconNameForManeuver, // (type?, modifier?, drivingSide?) => DirectionIconName
627
804
  directionIconSvg, // shorthand: the SVG for a maneuver
628
805
  } from "@mapmap/maps/direction-icons";
629
806
 
630
- el.innerHTML = directionIcons[iconNameForStep(step)];
807
+ const icons = iconNamesForSteps(leg.steps);
808
+ el.innerHTML = directionIcons[icons[i]];
631
809
  ```
632
810
 
633
811
  Icons are 20×20 `viewBox` SVGs. The arrow inherits `currentColor`; secondary
@@ -636,8 +814,19 @@ filled with `var(--mm-icon-secondary, #C9CDD2)` — set that CSS custom
636
814
  property to retheme it. Unknown maneuvers degrade to the plain turn family,
637
815
  never a missing icon.
638
816
 
639
- Prefer `iconNameForStep(step)`: it reads everything off the OSRM step
640
- itself ferry legs from `step.mode`, and the u-turn direction from
817
+ Rendering a turn list? Use `iconNamesForSteps(steps)`. A roundabout's icon
818
+ has to be read from the manoeuvre's THROUGH angle — the turn between the
819
+ road you arrive on and the road you leave on — and no single step carries
820
+ that: the enter step knows the approach, the matching exit step knows the
821
+ road taken. Resolve them together and consecutive roundabouts render
822
+ distinct icons; resolve them one at a time and every roundabout on the
823
+ route draws the same generic ring. (Stock OSRM does send a roundabout
824
+ `modifier`, but it measures the entry tangent, which is "left" for every
825
+ roundabout in a left-hand-traffic country — `iconNamesForSteps`
826
+ deliberately overrides it.)
827
+
828
+ For a single step, `iconNameForStep(step)` reads everything off the OSRM
829
+ step itself — ferry legs from `step.mode`, and the u-turn direction from
641
830
  `step.driving_side` (the MapMap gateway emits it on u-turn steps), so
642
831
  left-hand-traffic markets render `uturn_right` with no configuration.
643
832
  The lower-level `iconNameForManeuver` remains for callers without a step
@@ -150,7 +150,40 @@ function iconNameForStep(step) {
150
150
  const side = step.driving_side === "left" ? "left" : "right";
151
151
  return iconNameForManeuver(step.maneuver?.type, step.maneuver?.modifier, side);
152
152
  }
153
+ var ROUNDABOUT_ENTER = /* @__PURE__ */ new Set(["roundabout", "rotary"]);
154
+ var ROUNDABOUT_EXIT = /* @__PURE__ */ new Set(["exit roundabout", "exit rotary"]);
155
+ function signedTurn(from, to) {
156
+ return ((to - from + 180) % 360 + 360) % 360 - 180;
157
+ }
158
+ function modifierForTurn(delta) {
159
+ const magnitude = Math.abs(delta);
160
+ if (magnitude <= 22.5) return "straight";
161
+ const side = delta < 0 ? "left" : "right";
162
+ if (magnitude <= 67.5) return `slight ${side}`;
163
+ if (magnitude <= 112.5) return side;
164
+ return `sharp ${side}`;
165
+ }
166
+ function iconNamesForSteps(steps) {
167
+ const names = steps.map((step) => iconNameForStep(step));
168
+ const norm = (s) => (s ?? "").trim().toLowerCase();
169
+ for (let i = 0; i < steps.length - 1; i += 1) {
170
+ const enter = steps[i]?.maneuver;
171
+ const exit = steps[i + 1]?.maneuver;
172
+ if (!enter || !exit) continue;
173
+ if (!ROUNDABOUT_ENTER.has(norm(enter.type))) continue;
174
+ if (!ROUNDABOUT_EXIT.has(norm(exit.type))) continue;
175
+ const approach = enter.bearing_before;
176
+ const taken = exit.bearing_after;
177
+ if (typeof approach !== "number" || !Number.isFinite(approach)) continue;
178
+ if (typeof taken !== "number" || !Number.isFinite(taken)) continue;
179
+ if (approach === 0 && enter.bearing_after === 0) continue;
180
+ const modifier = modifierForTurn(signedTurn(approach, taken));
181
+ names[i] = iconNameForManeuver(enter.type, modifier);
182
+ names[i + 1] = iconNameForManeuver(exit.type, modifier);
183
+ }
184
+ return names;
185
+ }
153
186
 
154
- export { directionIconSvg, directionIcons, iconNameForManeuver, iconNameForStep };
155
- //# sourceMappingURL=chunk-CH53LJR7.js.map
156
- //# sourceMappingURL=chunk-CH53LJR7.js.map
187
+ export { directionIconSvg, directionIcons, iconNameForManeuver, iconNameForStep, iconNamesForSteps };
188
+ //# sourceMappingURL=chunk-J657OWYE.js.map
189
+ //# sourceMappingURL=chunk-J657OWYE.js.map