@mapmap/maps 0.5.0 → 0.5.1

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
@@ -372,6 +372,102 @@ puck.setLocation({ lat: 51.5074, lon: -0.1278 }, 45); // heading in degrees
372
372
  pitch/zoom as its option defaults when built from a `MapMapMap`; explicit
373
373
  options still win (option > design > built-in). Themes saved without the
374
374
  block load unchanged and stay `"version": 1`.
375
+ - `extra.nav.alerts` (optional since v1) holds the safety-camera alert
376
+ design: chip tokens (`background`, `textColor`, `outlineColor`,
377
+ `cornerRadius` 0-24, `outlineWidth` 0-4), the always-present escalated
378
+ pair (`escalatedBackground`, `escalatedTextColor`), `iconSet`
379
+ (`european` / `us` / `minimal` / `brand`) with optional per-kind
380
+ `iconUrlOverrides`, `alertSound` and `escalatedSound` from the built-in
381
+ earcon library (`cameraCalm`, `cameraUrgent`, `overspeedSoft`,
382
+ `zoneEnter`, `zoneClear`) with optional `alertSoundUrl` /
383
+ `escalatedSoundUrl` overrides, `mapIconSize` (0.75-1.5) and
384
+ `mapMinZoom` (9-14),
385
+ `corridorColor` / `corridorOpacity` for the average-speed corridor,
386
+ `leadDistance` (`short` / `standard` / `long` = 150 m / 250 m / 400 m
387
+ floors), `chipPosition` (`aboveSpeed` / `topLeading` / `topTrailing`)
388
+ and a `kinds` entry per camera kind (`fixed`, `average`, `red_light`,
389
+ `mobile_site`, `unknown`) carrying `showOnMap` (browse-mode map
390
+ display), `showOnMapWhileNavigating` (map display during guidance,
391
+ default true), `alertWhileDriving`, `showInRoutePreview`, `alertMode`
392
+ (`always` / `whenSpeeding` / `never`) and `audioMode` (`off` /
393
+ `earcon` / `earcon_and_speech`).
394
+
395
+ ```ts
396
+ import { CameraAlertChip, alertPresentation, navCameraKind } from "@mapmap/maps";
397
+
398
+ const chip = new CameraAlertChip(document.body, map.navDesign?.alerts);
399
+ // Visual is unconditional and calm; audio is the escalation channel.
400
+ chip.update({ kind: "fixed", limitKph: 30, distanceM: 240 }, { speeding: false });
401
+
402
+ // Average-speed corridors tint the route line itself.
403
+ routes.setAlertCorridors([corridorGeometry]);
404
+ ```
405
+
406
+ `alertPresentation(design, alert, { speeding })` is the pure rule behind
407
+ the chip: it returns whether to show it, whether to escalate, whether
408
+ audio fires and the lead-distance floor in metres. `alertChipContent`
409
+ returns just the title, subtitle and icon URL, which is what CarPlay's
410
+ `CPNavigationAlert` and Android Auto's `Alert` accept - colours, radius
411
+ and position do not survive a head-unit template, so styling degrades to
412
+ the platform look there rather than being assumed to render.
413
+ `alertContrastIssues(design)` runs a WCAG contrast check over both chip
414
+ colour pairs (floor 4.5:1); Studio surfaces the same warnings as you
415
+ pick colours.
416
+
417
+ **Sound.** `alertPresentation(...).soundUrl` is the earcon to play for
418
+ the state it just resolved - the calm sound, or its escalated variant
419
+ when the driver is over the limit - so a host never picks the file
420
+ itself. `alertSoundUrl(design, escalated)` is the same rule standalone.
421
+ The SDK bundles no audio: the built-ins resolve to
422
+ `https://mapmap.ai/earcons/*.mp3` (public and unmetered), and
423
+ `navAlertSoundUrl(sound, baseUrl)` re-points them at your own host.
424
+
425
+ ```ts
426
+ const p = alertPresentation(design, alert, { speeding });
427
+ if (p.audible) {
428
+ const audio = new Audio(p.soundUrl);
429
+ audio.volume = 1; // the app owns the in-car level, not the theme
430
+ void audio.play()?.catch?.(() => {});
431
+ }
432
+ ```
433
+
434
+ There are exactly two sounds, and that is deliberate. Which camera it is
435
+ travels in the icon and in the spoken line; how urgent it is travels in
436
+ the timbre. Per-kind chimes would ask a driver to learn five sounds at
437
+ motorway speed, which no shipping product does.
438
+
439
+ **Camera POIs on the map.** Cameras are not in the vector tiles - they
440
+ come from the gateway's `POST /v1/cameras/along`, behind the same
441
+ server-side jurisdiction policy - so a style draws them over a GeoJSON
442
+ source you add. `cameraSymbolLayer(design, sourceId, mode, variant)`
443
+ builds that layer: sprite icons per kind, the design's `mapIconSize` and
444
+ `mapMinZoom`, and a filter holding only the kinds that mode shows.
445
+
446
+ ```ts
447
+ map.addSource("cameras", { type: "geojson", data: camerasGeoJson });
448
+ map.addLayer(cameraSymbolLayer(design, "cameras", "navigating", "light"));
449
+ ```
450
+
451
+ The icons ship in the published sprite sheet
452
+ (`https://api.mapmap.ai/sprite/sprite`), generated from the same glyphs
453
+ the chip draws, so the pin and the chip are never two different cameras.
454
+ Image ids come from `cameraSpriteName(kind, variant)` -
455
+ `camera-fixed-dark`, `camera-redlight-light` and so on, `-dark` for
456
+ light basemaps and `-light` for dark ones. Your style must carry a
457
+ `sprite` URL for them to render; the skeleton ships none by default.
458
+
459
+ `cameraShownOnMap(design, kind, mode)` is the underlying rule.
460
+ `"browse"` reads `showOnMap` and `"navigating"` reads
461
+ `showOnMapWhileNavigating`, because showing a camera while planning and
462
+ showing it while driving are separate decisions - the market warns with
463
+ or without a planned route, so map display is not gated behind
464
+ guidance.
465
+
466
+ **Jurisdiction policy is server side and is not styleable.** Whether a
467
+ camera may be returned at all is decided per country by the gateway
468
+ before the response leaves it. This block styles what policy already
469
+ permits; it cannot widen it.
470
+
375
471
  - The nav block travels with the **theme document**, never the compiled
376
472
  style: `extra` round-trips through `POST /styles` and
377
473
  `GET /styles/{id}/theme` (bounded at 256 KB serialised), but a compiled
@@ -501,20 +597,27 @@ const route = await routes.route(from, to, {
501
597
 
502
598
  const steps = extractGuidance(route);
503
599
  const banner = new GuidanceBanner(document.body, map.navDesign?.banner);
504
- banner.update(steps[0].banners[0] ?? null);
600
+ // Pass the step as the second argument so ferry legs get the ferry glyph
601
+ // and u-turns point the right way in left-hand-traffic regions.
602
+ banner.update(steps[0].banners[0] ?? null, steps[0]);
505
603
  // As the driver advances, fire each voice prompt once when its
506
604
  // distanceAlongGeometry trigger is crossed:
507
605
  speak(steps[0].voice[0]);
508
606
  ```
509
607
 
510
- Voice prompts arrive plain and as SSML; banners carry the maneuver glyph
511
- and, where OSM has `turn:lanes` data, a lane diagram (sprite icons
512
- `lane-left`, `lane-straight`, ship with the map assets).
608
+ Voice prompts arrive plain and as SSML. The banner renders the manoeuvre
609
+ glyph from the platform's direction-icon set (roundabouts, forks, ramps,
610
+ ferries see below) sized to the design's font, and, where OSM has
611
+ `turn:lanes` data, a lane diagram (sprite icons `lane-left`,
612
+ `lane-straight`, … ship with the map assets). The glyph's secondary shapes
613
+ retheme via the `--mm-icon-secondary` CSS custom property.
513
614
 
514
615
  ### Direction icons (`@mapmap/maps/direction-icons`)
515
616
 
516
- A separate entry point (so the core bundle is unaffected) with the platform's
517
- 90-icon manoeuvre set as inline SVG, plus the OSRM maneuver mapping:
617
+ The platform's 90-icon manoeuvre set as inline SVG, plus the OSRM maneuver
618
+ mapping. Available from the main entry and as a separate
619
+ `@mapmap/maps/direction-icons` entry point; apps that use neither
620
+ `GuidanceBanner` nor the icons tree-shake the set away entirely:
518
621
 
519
622
  ```ts
520
623
  import {