@mapmap/maps 0.5.1 → 0.6.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/dist/index.d.ts CHANGED
@@ -518,6 +518,26 @@ interface NavRouteDesign {
518
518
  opacity: number;
519
519
  /** Casing (outline) colour drawn under the line. */
520
520
  casingColor: string;
521
+ /**
522
+ * Casing width in px. Defaults to `width + 4`.
523
+ *
524
+ * Optional because the casing was previously always `width + 4`, which
525
+ * cannot express a design whose casing is not exactly four wider than
526
+ * its line (the playground's is 9 over a 4.5 line).
527
+ */
528
+ casingWidth?: number;
529
+ /**
530
+ * Casing opacity, 0-1. Defaults to {@link NavRouteDesign.opacity}.
531
+ *
532
+ * Optional because casing and line previously shared one opacity, which
533
+ * cannot express a translucent casing under a solid line.
534
+ */
535
+ casingOpacity?: number;
536
+ /**
537
+ * Line dash pattern, in line-widths, as MapLibre's `line-dasharray`.
538
+ * Omitted or `null` draws a solid line.
539
+ */
540
+ dash?: number[] | null;
521
541
  }
522
542
  /** Current-position puck tokens. */
523
543
  interface NavPuckDesign {
@@ -1165,6 +1185,18 @@ declare function createMap(options: MapMapOptions): MapMapMap;
1165
1185
 
1166
1186
  /** MapMap brand signal blue (see website `--signal`). */
1167
1187
  declare const SIGNAL_BLUE = "#3a86ff";
1188
+ /**
1189
+ * Runtime overrides for the drawn line, applied over the design or the
1190
+ * built-in look. Every field is optional; omitted ones are left alone.
1191
+ */
1192
+ interface RouteLineStyle {
1193
+ /** Line colour. */
1194
+ color?: string;
1195
+ /** Dash pattern in line-widths, or `null` for solid. */
1196
+ dash?: number[] | null;
1197
+ /** Casing opacity, 0-1. Set 0 to hide the casing entirely. */
1198
+ casingOpacity?: number;
1199
+ }
1168
1200
  interface RouteLayerOptions {
1169
1201
  /** Gateway base URL. Defaults to the map's `baseUrl` when given a map. */
1170
1202
  baseUrl?: string;
@@ -1191,6 +1223,12 @@ interface RouteLayerOptions {
1191
1223
  * route line"). Defaults to a dimmed grey.
1192
1224
  */
1193
1225
  progressColor?: string;
1226
+ /** Casing colour for unselected alternative routes. */
1227
+ alternativeCasingColor?: string;
1228
+ /** Line colour for unselected alternative routes. */
1229
+ alternativeColor?: string;
1230
+ /** Colour of the ferry dashes drawn over the selected route. */
1231
+ ferryColor?: string;
1194
1232
  }
1195
1233
  /** Draws MapMap routes on a MapLibre map. */
1196
1234
  declare class RouteLayer {
@@ -1211,7 +1249,22 @@ declare class RouteLayer {
1211
1249
  private readonly design;
1212
1250
  private alertsDesign;
1213
1251
  private readonly progressColor;
1252
+ private readonly altSourceId;
1253
+ private readonly altCasingLayerId;
1254
+ private readonly altLineLayerId;
1255
+ private readonly ferrySourceId;
1256
+ private readonly ferryLayerId;
1257
+ private readonly altCasingColor;
1258
+ private readonly altColor;
1259
+ private readonly ferryColor;
1214
1260
  private lastRoute;
1261
+ /** Unselected alternatives, drawn beneath the selected route. */
1262
+ private alternatives;
1263
+ /** Ferry legs of the selected route, drawn as dashes over its line. */
1264
+ private ferrySegments;
1265
+ private selectHandler;
1266
+ /** Runtime paint override, re-applied after every style reload. */
1267
+ private lineStyle;
1215
1268
  private progress;
1216
1269
  private maneuver;
1217
1270
  private corridors;
@@ -1235,6 +1288,71 @@ declare class RouteLayer {
1235
1288
  draw(route: ParsedRoute): void;
1236
1289
  /** Add-or-update the source and layers for a route on the current style. */
1237
1290
  private install;
1291
+ /**
1292
+ * Draw a bare line from coordinates, without a parsed OSRM route.
1293
+ *
1294
+ * {@link RouteLayer.draw} expects a {@link ParsedRoute} because it is
1295
+ * normally fed by {@link RouteLayer.route}. Callers that already have
1296
+ * geometry and nothing else — an agent tool result, a stored polyline,
1297
+ * a hand-built preview — had to invent a `ParsedRoute` with zeroed
1298
+ * distance and duration that `draw` never reads. This is that path,
1299
+ * named honestly.
1300
+ */
1301
+ drawGeometry(coordinates: [number, number][]): void;
1302
+ /**
1303
+ * Draw a set of routes: one selected, the rest as dimmer alternatives
1304
+ * beneath it. Offering alternatives is table stakes for a navigation UI,
1305
+ * and the selected route keeps every feature of {@link RouteLayer.draw}
1306
+ * (progress, manoeuvre arrow, corridors, route effects).
1307
+ *
1308
+ * Pass the index of the route to select; out-of-range values clamp to the
1309
+ * first route. Calling with a single route is equivalent to `draw`, and
1310
+ * calling with an empty array clears everything.
1311
+ */
1312
+ drawAlternatives(routes: ParsedRoute[], selectedIndex?: number): void;
1313
+ /** Which alternatives are currently drawn, in the order given. */
1314
+ get alternativeRoutes(): readonly ParsedRoute[];
1315
+ /**
1316
+ * Register a click handler for the alternative lines. The index is the
1317
+ * position within the array last passed to
1318
+ * {@link RouteLayer.drawAlternatives}, so a caller can re-issue that call
1319
+ * with the new selection. Passing `undefined` removes the handler.
1320
+ */
1321
+ onSelectAlternative(handler: ((index: number) => void) | undefined): void;
1322
+ /**
1323
+ * Draw ferry legs of the selected route as dashes over its line, so water
1324
+ * crossings do not read as driving.
1325
+ *
1326
+ * Supplied as explicit geometries rather than derived from the route,
1327
+ * because `line-dasharray` cannot be data-driven: a ferry leg needs its
1328
+ * own layer, and only the caller knows which parts of their route are
1329
+ * ferries.
1330
+ */
1331
+ setFerrySegments(segments: [number, number][][]): void;
1332
+ /** Remove the ferry dashes. */
1333
+ clearFerrySegments(): void;
1334
+ /** Add-or-update the alternative-route source and its two layers. */
1335
+ private installAlternatives;
1336
+ private readonly handleAlternativeClick;
1337
+ /** Original array position of each drawn alternative, by its own index. */
1338
+ private altOriginalIndices;
1339
+ private removeAlternativeLayers;
1340
+ /** Add-or-update the ferry-dash overlay above the selected route. */
1341
+ private installFerry;
1342
+ /**
1343
+ * Override the drawn line's paint at runtime, over whatever the design or
1344
+ * the built-in look set.
1345
+ *
1346
+ * Route styling is not always static: a route can be provisional, or a
1347
+ * straight-line approximation that must not be mistaken for a surveyed
1348
+ * one. That is a paint change on a route already drawn, so it belongs
1349
+ * here rather than in the constructor's design.
1350
+ *
1351
+ * Overrides survive style reloads. Pass `{}` to clear them.
1352
+ */
1353
+ setLineStyle(style: RouteLineStyle): void;
1354
+ /** Apply any runtime override; a no-op when the layers are absent. */
1355
+ private applyLineStyle;
1238
1356
  /**
1239
1357
  * Sets how much of the route has been travelled, as a fraction in `[0, 1]`
1240
1358
  * of the line's length. The travelled part dims to `progressColor` (the
@@ -2783,4 +2901,4 @@ declare class VoiceGuidance {
2783
2901
  private playEarcon;
2784
2902
  }
2785
2903
 
2786
- export { ALERT_CONTRAST_MIN, AdrCheck, type AdrCheckOptions, type AdrCheckRequest, type AdrCheckResult, type AdrDimensions, type AdrTunnelCategory, type AlertChipContent, type AlertContrastIssue, type AlertPresentation, type BannerComponent, type BannerContent, type BannerInstruction, type BuildStyleOptions, type CameraAlert, CameraAlertChip, type CameraFix, DEFAULT_GLYPHS_URL, DEFAULT_TERRITORY_TILES_URL, type DiagnosticIssue, EFFECTS_METADATA_KEY, FLOW_DEFAULTS, FULL_ATTRIBUTION, FlowRouteEffectLayer, type FlythroughController, type FlythroughOptions, type FlythroughPose, GuidanceBanner, type GuidanceSeverity, type GuidanceSpokenPrompt, type IsochroneFeatureCollection, IsochroneLayer, type IsochroneLayerOptions, LOGO_SVG, type LaneIndication, type LayerOverride, type LngLatLike, LogoControl, type LogoOptions, type LogoPosition, MAX_PUCK_IMAGE_BYTES, type MapDiagnosticsInput, type MapDisplayMode, MapMapMap, type MapMapOptions, type MapMapTheme, NAV_ALERT_AUDIO_MODES, NAV_ALERT_CHIP_POSITIONS, NAV_ALERT_ICON_SETS, NAV_ALERT_LEAD_DISTANCES, NAV_ALERT_LEAD_DISTANCE_M, NAV_ALERT_MODES, NAV_ALERT_SOUNDS, NAV_ALERT_SOUND_BASE_URL, NAV_ALERT_SOUND_FILES, NAV_CAMERA_DEFAULTS, NAV_CAMERA_KINDS, type NavAlertAudioMode, type NavAlertChipPosition, type NavAlertIconSet, type NavAlertKindDesign, type NavAlertLeadDistance, type NavAlertMode, type NavAlertSound, type NavAlertsDesign, type NavBannerDesign, type NavCameraDesign, type NavCameraKind, type NavDesign, type NavPuckDesign, type NavRouteDesign, NavigationCamera, type NavigationCameraMode, type NavigationCameraOptions, type NearestByDriveTimeOptions, OPENMAPTILES_ATTRIBUTION, OSM_ATTRIBUTION, PALETTE_SLOTS, POI_CATEGORY_COLORS, POI_CATEGORY_IDS, POI_CLASS_CATEGORIES, type ParsedRoute, type Place, type PlacePointFeature, type PlaceWithDistance, type PlaceWithDriveTime, type PlacesFeatureCollection, type PlacesIcon, type PlacesInput, PlacesLayer, type PlacesLayerIds, type PlacesLayerOptions, type PlacesSelectOptions, type PoiCategoryDesign, type PoiDesign, PositionPuck, type PositionPuckOptions, type ProbeUploadOutcome, RIBBON_FLOATS_PER_VERTEX, ROUTE_EFFECTS, type ReachabilityMode, type RibbonMesh, type RouteEffectLayer, type RouteEffectName, type RouteFlowOptions, type RouteGeometry, RouteLayer, type RouteLayerOptions, type RouteOptions, type RouteProfile, SIGNAL_BLUE, SOURCE_LAYERS, type SetRouteEffectOptions, type SeverityProsody, type ShowReachabilityOptions, type SpeakOptions, type StepGuidance, type SunTimes, type Theme, type ThemeEffects, ThemeScheduler, type ThemeSchedulerOptions, type TruckParams, type UploadProbeOptions, VoiceGuidance, type VoiceGuidanceOptions, type VoiceGuidanceUpdate, type VoiceInstruction, alertChipContent, alertContrastIssues, alertIconSvg, alertIconUrl, alertPresentation, alertSoundUrl, alertSpeedReadoutColor, applyPoiDesign, bannerLanes, bearingBetween, bindFlythroughToScroll, buildAdrCheckBody, buildProbeUrl, buildRouteQuery, buildRouteUrl, buildStyle, builtInPoiColor, cameraKindLabel, cameraKindsShownOnMap, cameraShownOnMap, cameraSpriteName, cameraSymbolLayer, contrastRatio, createMap, createRouteEffect, defaultNavAlertsDesign, defaultNavDesign, defaultPoiDesign, directionArrow, effectsFromStyleMetadata, extractGuidance, flythrough, flythroughPose, formatCoord, formatCoords, haversineDistanceM, isNameTextField, languageTextField, lngLatToMercator, navAlertLeadDistanceM, navAlertSoundUrl, navCameraKind, navDesignFromTheme, navDesignFromThemeUrl, parseCssColour, parseHexColor, parseNavAlertsDesign, parseNavDesign, parseOsrmRoute, parsePoiDesign, placesFromGeoJSON, poiDesignFromTheme, poiDesignFromThemeUrl, poiDesignIsDefault, poiTextColorExpression, prefersReducedMotion, registerPmtilesProtocol, relativeLuminance, resetDiagnostics, resolveTheme, runMapDiagnostics, setMapLanguage, severityProsody, shortestArcDeg, shortestArcDelta, speak, ssmlToText, sunTimes, tessellateRouteRibbon, toLngLat, toPmtilesUrl, uploadProbeBatch };
2904
+ export { ALERT_CONTRAST_MIN, AdrCheck, type AdrCheckOptions, type AdrCheckRequest, type AdrCheckResult, type AdrDimensions, type AdrTunnelCategory, type AlertChipContent, type AlertContrastIssue, type AlertPresentation, type BannerComponent, type BannerContent, type BannerInstruction, type BuildStyleOptions, type CameraAlert, CameraAlertChip, type CameraFix, DEFAULT_GLYPHS_URL, DEFAULT_TERRITORY_TILES_URL, type DiagnosticIssue, EFFECTS_METADATA_KEY, FLOW_DEFAULTS, FULL_ATTRIBUTION, FlowRouteEffectLayer, type FlythroughController, type FlythroughOptions, type FlythroughPose, GuidanceBanner, type GuidanceSeverity, type GuidanceSpokenPrompt, type IsochroneFeatureCollection, IsochroneLayer, type IsochroneLayerOptions, LOGO_SVG, type LaneIndication, type LayerOverride, type LngLatLike, LogoControl, type LogoOptions, type LogoPosition, MAX_PUCK_IMAGE_BYTES, type MapDiagnosticsInput, type MapDisplayMode, MapMapMap, type MapMapOptions, type MapMapTheme, NAV_ALERT_AUDIO_MODES, NAV_ALERT_CHIP_POSITIONS, NAV_ALERT_ICON_SETS, NAV_ALERT_LEAD_DISTANCES, NAV_ALERT_LEAD_DISTANCE_M, NAV_ALERT_MODES, NAV_ALERT_SOUNDS, NAV_ALERT_SOUND_BASE_URL, NAV_ALERT_SOUND_FILES, NAV_CAMERA_DEFAULTS, NAV_CAMERA_KINDS, type NavAlertAudioMode, type NavAlertChipPosition, type NavAlertIconSet, type NavAlertKindDesign, type NavAlertLeadDistance, type NavAlertMode, type NavAlertSound, type NavAlertsDesign, type NavBannerDesign, type NavCameraDesign, type NavCameraKind, type NavDesign, type NavPuckDesign, type NavRouteDesign, NavigationCamera, type NavigationCameraMode, type NavigationCameraOptions, type NearestByDriveTimeOptions, OPENMAPTILES_ATTRIBUTION, OSM_ATTRIBUTION, PALETTE_SLOTS, POI_CATEGORY_COLORS, POI_CATEGORY_IDS, POI_CLASS_CATEGORIES, type ParsedRoute, type Place, type PlacePointFeature, type PlaceWithDistance, type PlaceWithDriveTime, type PlacesFeatureCollection, type PlacesIcon, type PlacesInput, PlacesLayer, type PlacesLayerIds, type PlacesLayerOptions, type PlacesSelectOptions, type PoiCategoryDesign, type PoiDesign, PositionPuck, type PositionPuckOptions, type ProbeUploadOutcome, RIBBON_FLOATS_PER_VERTEX, ROUTE_EFFECTS, type ReachabilityMode, type RibbonMesh, type RouteEffectLayer, type RouteEffectName, type RouteFlowOptions, type RouteGeometry, RouteLayer, type RouteLayerOptions, type RouteLineStyle, type RouteOptions, type RouteProfile, SIGNAL_BLUE, SOURCE_LAYERS, type SetRouteEffectOptions, type SeverityProsody, type ShowReachabilityOptions, type SpeakOptions, type StepGuidance, type SunTimes, type Theme, type ThemeEffects, ThemeScheduler, type ThemeSchedulerOptions, type TruckParams, type UploadProbeOptions, VoiceGuidance, type VoiceGuidanceOptions, type VoiceGuidanceUpdate, type VoiceInstruction, alertChipContent, alertContrastIssues, alertIconSvg, alertIconUrl, alertPresentation, alertSoundUrl, alertSpeedReadoutColor, applyPoiDesign, bannerLanes, bearingBetween, bindFlythroughToScroll, buildAdrCheckBody, buildProbeUrl, buildRouteQuery, buildRouteUrl, buildStyle, builtInPoiColor, cameraKindLabel, cameraKindsShownOnMap, cameraShownOnMap, cameraSpriteName, cameraSymbolLayer, contrastRatio, createMap, createRouteEffect, defaultNavAlertsDesign, defaultNavDesign, defaultPoiDesign, directionArrow, effectsFromStyleMetadata, extractGuidance, flythrough, flythroughPose, formatCoord, formatCoords, haversineDistanceM, isNameTextField, languageTextField, lngLatToMercator, navAlertLeadDistanceM, navAlertSoundUrl, navCameraKind, navDesignFromTheme, navDesignFromThemeUrl, parseCssColour, parseHexColor, parseNavAlertsDesign, parseNavDesign, parseOsrmRoute, parsePoiDesign, placesFromGeoJSON, poiDesignFromTheme, poiDesignFromThemeUrl, poiDesignIsDefault, poiTextColorExpression, prefersReducedMotion, registerPmtilesProtocol, relativeLuminance, resetDiagnostics, resolveTheme, runMapDiagnostics, setMapLanguage, severityProsody, shortestArcDeg, shortestArcDelta, speak, ssmlToText, sunTimes, tessellateRouteRibbon, toLngLat, toPmtilesUrl, uploadProbeBatch };
package/dist/index.js CHANGED
@@ -2159,6 +2159,9 @@ var CASING_COLOR = "#1f438a";
2159
2159
  var CORRIDOR_COLOR = "#e05c6c";
2160
2160
  var CORRIDOR_OPACITY = 0.55;
2161
2161
  var PROGRESS_COLOR = "#b0b0b0";
2162
+ var ALT_CASING_COLOR = "#5a6b84";
2163
+ var ALT_LINE_COLOR = "#aebdd2";
2164
+ var FERRY_COLOR = "#eaf1fb";
2162
2165
  function arrowImage() {
2163
2166
  const size = 24;
2164
2167
  const data = new Uint8Array(size * size * 4);
@@ -2186,13 +2189,29 @@ function arrowImage() {
2186
2189
  }
2187
2190
  var RouteLayer = class {
2188
2191
  constructor(map, options = {}) {
2192
+ /** Unselected alternatives, drawn beneath the selected route. */
2193
+ this.alternatives = [];
2194
+ /** Ferry legs of the selected route, drawn as dashes over its line. */
2195
+ this.ferrySegments = [];
2189
2196
  this.progress = 0;
2190
2197
  this.corridors = [];
2191
2198
  this.handleStyleLoad = () => {
2199
+ this.installAlternatives();
2192
2200
  if (this.lastRoute) this.install(this.lastRoute);
2193
2201
  else this.installManeuver();
2194
2202
  this.installCorridors();
2203
+ this.installFerry();
2195
2204
  };
2205
+ this.handleAlternativeClick = (event) => {
2206
+ if (!this.selectHandler) return;
2207
+ const raw = event.features?.[0]?.properties?.["index"];
2208
+ const position = typeof raw === "number" ? raw : Number(raw);
2209
+ if (!Number.isInteger(position)) return;
2210
+ const original = this.altOriginalIndices[position];
2211
+ if (original !== void 0) this.selectHandler(original);
2212
+ };
2213
+ /** Original array position of each drawn alternative, by its own index. */
2214
+ this.altOriginalIndices = [];
2196
2215
  if (map instanceof MapMapMap) {
2197
2216
  this.map = map.map;
2198
2217
  this.owner = map;
@@ -2219,7 +2238,15 @@ var RouteLayer = class {
2219
2238
  this.arrowImageId = `${id}-arrow`;
2220
2239
  this.corridorSourceId = `${id}-corridor-src`;
2221
2240
  this.corridorLayerId = `${id}-corridor`;
2241
+ this.altSourceId = `${id}-alt-src`;
2242
+ this.altCasingLayerId = `${id}-alt-casing`;
2243
+ this.altLineLayerId = `${id}-alt-line`;
2244
+ this.ferrySourceId = `${id}-ferry-src`;
2245
+ this.ferryLayerId = `${id}-ferry`;
2222
2246
  this.progressColor = options.progressColor ?? PROGRESS_COLOR;
2247
+ this.altCasingColor = options.alternativeCasingColor ?? ALT_CASING_COLOR;
2248
+ this.altColor = options.alternativeColor ?? ALT_LINE_COLOR;
2249
+ this.ferryColor = options.ferryColor ?? FERRY_COLOR;
2223
2250
  this.map.on("style.load", this.handleStyleLoad);
2224
2251
  }
2225
2252
  /**
@@ -2285,6 +2312,7 @@ var RouteLayer = class {
2285
2312
  }
2286
2313
  if (this.map.getLayer(this.casingLayerId) && this.map.getLayer(this.lineLayerId)) {
2287
2314
  this.applyProgress();
2315
+ this.applyLineStyle();
2288
2316
  this.installManeuver();
2289
2317
  return;
2290
2318
  }
@@ -2295,8 +2323,8 @@ var RouteLayer = class {
2295
2323
  layout: { "line-cap": "round", "line-join": "round" },
2296
2324
  paint: this.design ? {
2297
2325
  "line-color": this.design.casingColor,
2298
- "line-width": this.design.width + 4,
2299
- "line-opacity": this.design.opacity
2326
+ "line-width": this.design.casingWidth ?? this.design.width + 4,
2327
+ "line-opacity": this.design.casingOpacity ?? this.design.opacity
2300
2328
  } : {
2301
2329
  "line-color": CASING_COLOR,
2302
2330
  "line-width": ["interpolate", ["linear"], ["zoom"], 8, 6, 16, 12]
@@ -2310,7 +2338,8 @@ var RouteLayer = class {
2310
2338
  paint: this.design ? {
2311
2339
  "line-color": this.design.color,
2312
2340
  "line-width": this.design.width,
2313
- "line-opacity": this.design.opacity
2341
+ "line-opacity": this.design.opacity,
2342
+ ...this.design.dash ? { "line-dasharray": this.design.dash } : {}
2314
2343
  } : {
2315
2344
  "line-color": SIGNAL_BLUE,
2316
2345
  "line-width": ["interpolate", ["linear"], ["zoom"], 8, 3, 16, 8]
@@ -2319,8 +2348,202 @@ var RouteLayer = class {
2319
2348
  if (!this.map.getLayer(this.casingLayerId)) this.map.addLayer(casing);
2320
2349
  if (!this.map.getLayer(this.lineLayerId)) this.map.addLayer(line);
2321
2350
  this.applyProgress();
2351
+ this.applyLineStyle();
2322
2352
  this.installManeuver();
2323
2353
  }
2354
+ /**
2355
+ * Draw a bare line from coordinates, without a parsed OSRM route.
2356
+ *
2357
+ * {@link RouteLayer.draw} expects a {@link ParsedRoute} because it is
2358
+ * normally fed by {@link RouteLayer.route}. Callers that already have
2359
+ * geometry and nothing else — an agent tool result, a stored polyline,
2360
+ * a hand-built preview — had to invent a `ParsedRoute` with zeroed
2361
+ * distance and duration that `draw` never reads. This is that path,
2362
+ * named honestly.
2363
+ */
2364
+ drawGeometry(coordinates) {
2365
+ this.draw({
2366
+ distanceM: 0,
2367
+ durationS: 0,
2368
+ geometry: { type: "LineString", coordinates },
2369
+ raw: {}
2370
+ });
2371
+ }
2372
+ /**
2373
+ * Draw a set of routes: one selected, the rest as dimmer alternatives
2374
+ * beneath it. Offering alternatives is table stakes for a navigation UI,
2375
+ * and the selected route keeps every feature of {@link RouteLayer.draw}
2376
+ * (progress, manoeuvre arrow, corridors, route effects).
2377
+ *
2378
+ * Pass the index of the route to select; out-of-range values clamp to the
2379
+ * first route. Calling with a single route is equivalent to `draw`, and
2380
+ * calling with an empty array clears everything.
2381
+ */
2382
+ drawAlternatives(routes, selectedIndex = 0) {
2383
+ if (routes.length === 0) {
2384
+ this.clear();
2385
+ return;
2386
+ }
2387
+ const index = selectedIndex >= 0 && selectedIndex < routes.length ? selectedIndex : 0;
2388
+ this.alternatives = routes.filter((_, i) => i !== index);
2389
+ this.altOriginalIndices = routes.map((_, i) => i).filter((i) => i !== index);
2390
+ this.installAlternatives();
2391
+ this.draw(routes[index]);
2392
+ }
2393
+ /** Which alternatives are currently drawn, in the order given. */
2394
+ get alternativeRoutes() {
2395
+ return this.alternatives;
2396
+ }
2397
+ /**
2398
+ * Register a click handler for the alternative lines. The index is the
2399
+ * position within the array last passed to
2400
+ * {@link RouteLayer.drawAlternatives}, so a caller can re-issue that call
2401
+ * with the new selection. Passing `undefined` removes the handler.
2402
+ */
2403
+ onSelectAlternative(handler) {
2404
+ this.selectHandler = handler;
2405
+ }
2406
+ /**
2407
+ * Draw ferry legs of the selected route as dashes over its line, so water
2408
+ * crossings do not read as driving.
2409
+ *
2410
+ * Supplied as explicit geometries rather than derived from the route,
2411
+ * because `line-dasharray` cannot be data-driven: a ferry leg needs its
2412
+ * own layer, and only the caller knows which parts of their route are
2413
+ * ferries.
2414
+ */
2415
+ setFerrySegments(segments) {
2416
+ this.ferrySegments = segments;
2417
+ if (this.map.isStyleLoaded()) this.installFerry();
2418
+ }
2419
+ /** Remove the ferry dashes. */
2420
+ clearFerrySegments() {
2421
+ this.ferrySegments = [];
2422
+ if (this.map.getLayer(this.ferryLayerId)) this.map.removeLayer(this.ferryLayerId);
2423
+ if (this.map.getSource(this.ferrySourceId)) this.map.removeSource(this.ferrySourceId);
2424
+ }
2425
+ /** Add-or-update the alternative-route source and its two layers. */
2426
+ installAlternatives() {
2427
+ if (this.alternatives.length === 0) {
2428
+ this.removeAlternativeLayers();
2429
+ return;
2430
+ }
2431
+ if (!this.map.isStyleLoaded()) return;
2432
+ const data = {
2433
+ type: "FeatureCollection",
2434
+ features: this.alternatives.map((route, i) => ({
2435
+ type: "Feature",
2436
+ properties: { index: i },
2437
+ geometry: route.geometry
2438
+ }))
2439
+ };
2440
+ const existing = this.map.getSource(this.altSourceId);
2441
+ if (existing) existing.setData(data);
2442
+ else this.map.addSource(this.altSourceId, { type: "geojson", data });
2443
+ const below = this.map.getLayer(this.casingLayerId) ? this.casingLayerId : void 0;
2444
+ if (!this.map.getLayer(this.altCasingLayerId)) {
2445
+ this.map.addLayer({
2446
+ id: this.altCasingLayerId,
2447
+ type: "line",
2448
+ source: this.altSourceId,
2449
+ layout: { "line-cap": "round", "line-join": "round" },
2450
+ paint: {
2451
+ "line-color": this.altCasingColor,
2452
+ "line-width": 7,
2453
+ "line-opacity": 0.55
2454
+ }
2455
+ }, below);
2456
+ }
2457
+ if (!this.map.getLayer(this.altLineLayerId)) {
2458
+ this.map.addLayer({
2459
+ id: this.altLineLayerId,
2460
+ type: "line",
2461
+ source: this.altSourceId,
2462
+ layout: { "line-cap": "round", "line-join": "round" },
2463
+ paint: {
2464
+ "line-color": this.altColor,
2465
+ "line-width": 4,
2466
+ "line-opacity": 0.9
2467
+ }
2468
+ }, below);
2469
+ this.map.on("click", this.altLineLayerId, this.handleAlternativeClick);
2470
+ }
2471
+ }
2472
+ removeAlternativeLayers() {
2473
+ if (this.map.getLayer(this.altLineLayerId)) {
2474
+ this.map.off("click", this.altLineLayerId, this.handleAlternativeClick);
2475
+ this.map.removeLayer(this.altLineLayerId);
2476
+ }
2477
+ if (this.map.getLayer(this.altCasingLayerId)) this.map.removeLayer(this.altCasingLayerId);
2478
+ if (this.map.getSource(this.altSourceId)) this.map.removeSource(this.altSourceId);
2479
+ }
2480
+ /** Add-or-update the ferry-dash overlay above the selected route. */
2481
+ installFerry() {
2482
+ if (this.ferrySegments.length === 0) {
2483
+ this.clearFerrySegments();
2484
+ return;
2485
+ }
2486
+ if (!this.map.isStyleLoaded()) return;
2487
+ const data = {
2488
+ type: "FeatureCollection",
2489
+ features: this.ferrySegments.map((coordinates) => ({
2490
+ type: "Feature",
2491
+ properties: {},
2492
+ geometry: { type: "LineString", coordinates }
2493
+ }))
2494
+ };
2495
+ const existing = this.map.getSource(this.ferrySourceId);
2496
+ if (existing) existing.setData(data);
2497
+ else this.map.addSource(this.ferrySourceId, { type: "geojson", data });
2498
+ if (!this.map.getLayer(this.ferryLayerId)) {
2499
+ this.map.addLayer({
2500
+ id: this.ferryLayerId,
2501
+ type: "line",
2502
+ source: this.ferrySourceId,
2503
+ layout: { "line-cap": "butt", "line-join": "round" },
2504
+ paint: {
2505
+ "line-color": this.ferryColor,
2506
+ "line-width": 2.5,
2507
+ "line-dasharray": [1.2, 1.6]
2508
+ }
2509
+ });
2510
+ }
2511
+ }
2512
+ /**
2513
+ * Override the drawn line's paint at runtime, over whatever the design or
2514
+ * the built-in look set.
2515
+ *
2516
+ * Route styling is not always static: a route can be provisional, or a
2517
+ * straight-line approximation that must not be mistaken for a surveyed
2518
+ * one. That is a paint change on a route already drawn, so it belongs
2519
+ * here rather than in the constructor's design.
2520
+ *
2521
+ * Overrides survive style reloads. Pass `{}` to clear them.
2522
+ */
2523
+ setLineStyle(style) {
2524
+ this.lineStyle = Object.keys(style).length > 0 ? style : void 0;
2525
+ this.applyLineStyle();
2526
+ }
2527
+ /** Apply any runtime override; a no-op when the layers are absent. */
2528
+ applyLineStyle() {
2529
+ const style = this.lineStyle;
2530
+ if (!style) return;
2531
+ if (this.map.getLayer(this.lineLayerId)) {
2532
+ if (style.color !== void 0) {
2533
+ this.map.setPaintProperty(this.lineLayerId, "line-color", style.color);
2534
+ }
2535
+ if (style.dash !== void 0) {
2536
+ this.map.setPaintProperty(
2537
+ this.lineLayerId,
2538
+ "line-dasharray",
2539
+ style.dash ?? [1, 0]
2540
+ );
2541
+ }
2542
+ }
2543
+ if (style.casingOpacity !== void 0 && this.map.getLayer(this.casingLayerId)) {
2544
+ this.map.setPaintProperty(this.casingLayerId, "line-opacity", style.casingOpacity);
2545
+ }
2546
+ }
2324
2547
  /**
2325
2548
  * Sets how much of the route has been travelled, as a fraction in `[0, 1]`
2326
2549
  * of the line's length. The travelled part dims to `progressColor` (the
@@ -2461,6 +2684,10 @@ var RouteLayer = class {
2461
2684
  clear() {
2462
2685
  this.clearManeuver();
2463
2686
  this.clearAlertCorridors();
2687
+ this.clearFerrySegments();
2688
+ this.alternatives = [];
2689
+ this.altOriginalIndices = [];
2690
+ this.removeAlternativeLayers();
2464
2691
  for (const layerId of [this.lineLayerId, this.casingLayerId]) {
2465
2692
  if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
2466
2693
  }
@@ -3936,6 +4163,16 @@ var AdrCheck = class {
3936
4163
  };
3937
4164
 
3938
4165
  // src/guidance.ts
4166
+ var CSS_COLOUR = /^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color|color-mix|var)\((?:[\w\s.,%/#-]|\([\w\s.,%/#-]*\))*\)$/i;
4167
+ function safeColour(value, fallback) {
4168
+ const v = value.trim();
4169
+ if (v === "" || /[;{}]|url\(|\/\*/i.test(v)) return fallback;
4170
+ if (v.startsWith("#")) {
4171
+ return /^[0-9a-fA-F]+$/.test(v.slice(1)) && [3, 4, 6, 8].includes(v.length - 1) ? v : fallback;
4172
+ }
4173
+ if (/^[a-zA-Z][a-zA-Z-]*$/.test(v)) return v;
4174
+ return CSS_COLOUR.test(v) ? v : fallback;
4175
+ }
3939
4176
  function extractGuidance(route) {
3940
4177
  const legs = route.raw["legs"] ?? [];
3941
4178
  const out = [];
@@ -3986,10 +4223,11 @@ var XML_ENTITIES = {
3986
4223
  "&apos;": "'"
3987
4224
  };
3988
4225
  function ssmlToText(ssml) {
3989
- return ssml.replace(/<[^>]+>/g, "").replace(
3990
- /&(?:amp|lt|gt|quot|apos);|&#(\d+);/g,
3991
- (entity, decimal) => decimal !== void 0 ? String.fromCodePoint(Number(decimal)) : XML_ENTITIES[entity] ?? entity
3992
- ).trim();
4226
+ return ssml.replace(/<[^>]+>/g, "").replace(/&(?:amp|lt|gt|quot|apos);|&#(\d+);/g, (entity, decimal) => {
4227
+ if (decimal === void 0) return XML_ENTITIES[entity] ?? entity;
4228
+ const n = Number(decimal);
4229
+ return n <= 1114111 ? String.fromCodePoint(n) : entity;
4230
+ }).trim();
3993
4231
  }
3994
4232
  function bannerLanes(banner) {
3995
4233
  const components = banner.sub?.components ?? [];
@@ -4045,7 +4283,7 @@ var GuidanceBanner = class {
4045
4283
  this.design = design;
4046
4284
  this.element = doc.createElement("div");
4047
4285
  this.element.className = "mapmap-banner";
4048
- this.element.style.cssText = design ? `display:none;align-items:center;box-sizing:border-box;gap:${Math.round((design.padding ?? 10) * 1.2)}px;padding:${design.padding ?? 10}px ${Math.round((design.padding ?? 10) * 1.4)}px;background:${design.background};color:${design.textColor};border-radius:${design.cornerRadius ?? 10}px;max-width:${design.maxWidth ?? 340}px;` + (design.height !== void 0 ? `min-height:${design.height}px;` : "") + `font:600 ${design.fontSize}px/1.3 system-ui,sans-serif` : "display:none;gap:12px;align-items:center;padding:10px 14px;background:#101418;color:#fff;border-radius:10px;font:600 16px/1.3 system-ui,sans-serif";
4286
+ this.element.style.cssText = design ? `display:none;align-items:center;box-sizing:border-box;gap:${Math.round((design.padding ?? 10) * 1.2)}px;padding:${design.padding ?? 10}px ${Math.round((design.padding ?? 10) * 1.4)}px;background:${safeColour(design.background, "#101418")};color:${safeColour(design.textColor, "#ffffff")};border-radius:${design.cornerRadius ?? 10}px;max-width:${design.maxWidth ?? 340}px;` + (design.height !== void 0 ? `min-height:${design.height}px;` : "") + `font:600 ${design.fontSize}px/1.3 system-ui,sans-serif` : "display:none;gap:12px;align-items:center;padding:10px 14px;background:#101418;color:#fff;border-radius:10px;font:600 16px/1.3 system-ui,sans-serif";
4049
4287
  container?.appendChild(this.element);
4050
4288
  }
4051
4289
  /**
@@ -4304,9 +4542,9 @@ var CameraAlertChip = class {
4304
4542
  this.element.style.cssText = `display:${shown};gap:8px;align-items:center;padding:8px 12px;background:${this.escalated ? "#c0392b" : "#101418"};color:#fff;border-radius:10px;font:600 15px/1.3 system-ui,sans-serif`;
4305
4543
  return;
4306
4544
  }
4307
- const background = this.escalated ? design.escalatedBackground : design.background;
4308
- const color = this.escalated ? design.escalatedTextColor : design.textColor;
4309
- this.element.style.cssText = `display:${shown};gap:8px;align-items:center;box-sizing:border-box;padding:8px 12px;background:${background};color:${color};border-radius:${design.cornerRadius}px;` + (design.outlineWidth > 0 ? `border:${design.outlineWidth}px solid ${design.outlineColor};` : "") + (CHIP_SLOT_CSS[design.chipPosition] ?? "") + "font:600 15px/1.3 system-ui,sans-serif";
4545
+ const background = this.escalated ? safeColour(design.escalatedBackground, "#c0392b") : safeColour(design.background, "#101418");
4546
+ const color = this.escalated ? safeColour(design.escalatedTextColor, "#ffffff") : safeColour(design.textColor, "#ffffff");
4547
+ this.element.style.cssText = `display:${shown};gap:8px;align-items:center;box-sizing:border-box;padding:8px 12px;background:${background};color:${color};border-radius:${design.cornerRadius}px;` + (design.outlineWidth > 0 ? `border:${design.outlineWidth}px solid ${safeColour(design.outlineColor, "#ffffff")};` : "") + (CHIP_SLOT_CSS[design.chipPosition] ?? "") + "font:600 15px/1.3 system-ui,sans-serif";
4310
4548
  }
4311
4549
  };
4312
4550