@mapmap/maps 0.2.0 → 0.4.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.js CHANGED
@@ -85,7 +85,7 @@ function watchForAuthFailures(map, apiKey) {
85
85
  on.call(map, "error", (ev) => {
86
86
  const status = ev?.error?.status;
87
87
  const message = ev?.error?.message ?? "";
88
- const unauthorised = status === 401 || /\b401\b|unauthori[sz]ed/i.test(message);
88
+ const unauthorised = status === 401 || /unauthori[sz]ed/i.test(message);
89
89
  if (!unauthorised) return;
90
90
  report(
91
91
  "invalid-api-key",
@@ -96,6 +96,52 @@ function watchForAuthFailures(map, apiKey) {
96
96
  }
97
97
  }
98
98
 
99
+ // src/coords.ts
100
+ function toLngLat(point) {
101
+ if (Array.isArray(point)) {
102
+ const [lng2, lat2] = point;
103
+ assertFinite(lng2, lat2);
104
+ return [lng2, lat2];
105
+ }
106
+ const lng = "lng" in point ? point.lng : point.lon;
107
+ const lat = point.lat;
108
+ assertFinite(lng, lat);
109
+ return [lng, lat];
110
+ }
111
+ function formatCoord(point) {
112
+ const [lng, lat] = toLngLat(point);
113
+ return `${lng},${lat}`;
114
+ }
115
+ function formatCoords(points) {
116
+ if (points.length < 2) {
117
+ throw new Error("at least two coordinates are required for a route");
118
+ }
119
+ return points.map(formatCoord).join(";");
120
+ }
121
+ function unwrapLngs(coordinates) {
122
+ const out = [];
123
+ for (const [lng, lat] of coordinates) {
124
+ const prev = out[out.length - 1];
125
+ let unwrapped = lng;
126
+ if (prev) {
127
+ while (unwrapped - prev[0] > 180) unwrapped -= 360;
128
+ while (unwrapped - prev[0] < -180) unwrapped += 360;
129
+ }
130
+ out.push([unwrapped, lat]);
131
+ }
132
+ return out;
133
+ }
134
+ function assertFinite(lng, lat) {
135
+ if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
136
+ throw new Error(`invalid coordinate: lng=${lng}, lat=${lat}`);
137
+ }
138
+ if (lng < -180 || lng > 180 || lat < -90 || lat > 90) {
139
+ throw new Error(
140
+ `coordinate out of range: lng=${lng} (\xB1180), lat=${lat} (\xB190) - check lng/lat order`
141
+ );
142
+ }
143
+ }
144
+
99
145
  // src/effects.ts
100
146
  var FLOW_COLOUR = "#3a86ff";
101
147
  var EARTH_RADIUS_M = 63710088e-1;
@@ -115,7 +161,7 @@ function lngLatToMercator(lngLat) {
115
161
  }
116
162
  function tessellateRouteRibbon(coordinates) {
117
163
  const points = [];
118
- for (const lngLat of coordinates) {
164
+ for (const lngLat of unwrapLngs(coordinates)) {
119
165
  const p = lngLatToMercator(lngLat);
120
166
  const last = points[points.length - 1];
121
167
  if (!last || last[0] !== p[0] || last[1] !== p[1]) points.push(p);
@@ -732,8 +778,8 @@ var PALETTE_SLOTS = [
732
778
  ["waterway", "#a8c8e8", "#1b2c40"],
733
779
  ["landcover", "#e3e8dd", "#182029"],
734
780
  ["landuse", "#ece8e1", "#171e26"],
735
- ["park", "#cfe4c8", "#1a2a1f"],
736
- ["building", "#e2ddd4", "#20272f"],
781
+ ["park", "#c8e2bf", "#1a2a1f"],
782
+ ["building", "#e0d6c4", "#252d37"],
737
783
  ["aeroway", "#dcd9d2", "#232b34"],
738
784
  ["road", "#ffffff", "#2b333d"],
739
785
  ["roadMajor", "#f6d9a0", "#4a5461"],
@@ -822,6 +868,73 @@ function resolvePalette(theme) {
822
868
  }
823
869
  return palette;
824
870
  }
871
+ function mixColour(a, b, t) {
872
+ const parse = (s) => /^#[0-9a-fA-F]{6}$/.test(s) ? [
873
+ parseInt(s.slice(1, 3), 16),
874
+ parseInt(s.slice(3, 5), 16),
875
+ parseInt(s.slice(5, 7), 16)
876
+ ] : null;
877
+ const ca = parse(a);
878
+ const cb = parse(b);
879
+ if (!ca || !cb) return a;
880
+ const k = Math.min(1, Math.max(0, t));
881
+ const hex = (i) => Math.min(255, Math.max(0, Math.round(ca[i] + (cb[i] - ca[i]) * k))).toString(16).padStart(2, "0");
882
+ return `#${hex(0)}${hex(1)}${hex(2)}`;
883
+ }
884
+ function landuseColour(p) {
885
+ const base = p("landuse");
886
+ return [
887
+ "match",
888
+ ["get", "class"],
889
+ // Civic/medical: leans towards the building tone.
890
+ ["hospital"],
891
+ mixColour(base, p("building"), 0.55),
892
+ // Education campuses read as semi-green grounds.
893
+ ["school", "university", "college", "kindergarten", "library"],
894
+ mixColour(base, p("park"), 0.35),
895
+ ["cemetery"],
896
+ mixColour(base, p("park"), 0.6),
897
+ // Sports/play surfaces borrow the park green outright.
898
+ ["pitch", "playground"],
899
+ mixColour(base, p("park"), 0.75),
900
+ // Restricted land greys off.
901
+ ["military"],
902
+ mixColour(base, p("textSecondary"), 0.3),
903
+ ["retail", "commercial"],
904
+ mixColour(base, p("building"), 0.35),
905
+ ["residential", "suburb", "neighbourhood"],
906
+ mixColour(base, p("building"), 0.18),
907
+ ["railway"],
908
+ mixColour(base, p("textSecondary"), 0.2),
909
+ base
910
+ ];
911
+ }
912
+ function landcoverColour(p) {
913
+ const base = p("landcover");
914
+ return [
915
+ "match",
916
+ ["get", "class"],
917
+ ["wood", "forest"],
918
+ mixColour(base, p("park"), 0.75),
919
+ ["grass"],
920
+ mixColour(base, p("park"), 0.45),
921
+ ["wetland"],
922
+ mixColour(base, p("park"), 0.3),
923
+ ["farmland"],
924
+ mixColour(base, p("landuse"), 0.4),
925
+ ["sand"],
926
+ mixColour(base, p("building"), 0.5),
927
+ ["rock"],
928
+ mixColour(base, p("textSecondary"), 0.25),
929
+ base
930
+ ];
931
+ }
932
+ function notARoadFilter() {
933
+ return ["!in", "class", "rail", "transit", "ferry", "path", "track", "pedestrian", "aerialway"];
934
+ }
935
+ function tunnelOpacity() {
936
+ return ["match", ["get", "brunnel"], "tunnel", 0.55, 1];
937
+ }
825
938
  function fill(id, sourceLayer, colour2, opacity, minzoom) {
826
939
  const layer = {
827
940
  id,
@@ -980,8 +1093,8 @@ function buildStyle(options = {}) {
980
1093
  type: "background",
981
1094
  paint: { "background-color": p("background") }
982
1095
  },
983
- fill("landcover", "landcover", p("landcover"), 0.6),
984
- fill("landuse", "landuse", p("landuse"), 0.45),
1096
+ fill("landcover", "landcover", landcoverColour(p), 0.6),
1097
+ fill("landuse", "landuse", landuseColour(p), 0.45),
985
1098
  fill("park", "park", p("park"), 0.7),
986
1099
  fill("water", "water", p("water"), 1),
987
1100
  {
@@ -1006,7 +1119,32 @@ function buildStyle(options = {}) {
1006
1119
  "line-width": ["interpolate", ["linear"], ["zoom"], 10, 1, 16, 6]
1007
1120
  }
1008
1121
  },
1009
- fill("building", "building", p("building"), 0.9, 13),
1122
+ fill("building", "building", p("building"), 1, 13),
1123
+ // Building footprints sit at barely 1.2:1 against the background, so at
1124
+ // street zooms the fill alone reads as nothing. An outline restores the
1125
+ // block structure without touching the `building` slot default or the
1126
+ // fill opacity, which would alter every saved theme and preset. Starts
1127
+ // at BUILDINGS_3D_MINZOOM so it hands off to the extrusion at exactly
1128
+ // the zoom the flat fill does.
1129
+ {
1130
+ id: "building-outline",
1131
+ type: "line",
1132
+ source: "territory",
1133
+ "source-layer": "building",
1134
+ minzoom: BUILDINGS_3D_MINZOOM,
1135
+ paint: {
1136
+ "line-color": mixColour(p("building"), p("textSecondary"), 0.6),
1137
+ "line-width": [
1138
+ "interpolate",
1139
+ ["exponential", 1.2],
1140
+ ["zoom"],
1141
+ 15,
1142
+ 0.3,
1143
+ 20,
1144
+ 1.6
1145
+ ]
1146
+ }
1147
+ },
1010
1148
  {
1011
1149
  id: "rail",
1012
1150
  type: "line",
@@ -1016,7 +1154,38 @@ function buildStyle(options = {}) {
1016
1154
  filter: ["in", "class", "rail", "transit"],
1017
1155
  paint: {
1018
1156
  "line-color": p("rail"),
1019
- "line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.4, 16, 2]
1157
+ "line-width": [
1158
+ "interpolate",
1159
+ ["exponential", 1.2],
1160
+ ["zoom"],
1161
+ 8,
1162
+ 0.4,
1163
+ 14,
1164
+ 1.6,
1165
+ 20,
1166
+ 6
1167
+ ]
1168
+ }
1169
+ },
1170
+ // The `transportation` layer carries POLYGON features for pedestrian
1171
+ // plazas and footway areas (61 in a single Soho tile). Without a fill
1172
+ // they were only ever hairline-dashed as outlines, leaving town centres
1173
+ // empty.
1174
+ {
1175
+ id: "pedestrian-areas",
1176
+ type: "fill",
1177
+ source: "territory",
1178
+ "source-layer": "transportation",
1179
+ minzoom: 14,
1180
+ // A fill layer only ever draws polygonal geometry, so the class match
1181
+ // is the whole filter. An explicit ["==", ["geometry-type"], "Polygon"]
1182
+ // clause looks right but matches NOTHING here: MapLibre reports these
1183
+ // multi-ring plazas as "MultiPolygon" (verified live — the clause
1184
+ // rendered 0 of 73 features).
1185
+ filter: ["match", ["get", "class"], ["path", "pedestrian"], true, false],
1186
+ paint: {
1187
+ "fill-color": p("path"),
1188
+ "fill-opacity": 0.7
1020
1189
  }
1021
1190
  },
1022
1191
  {
@@ -1028,10 +1197,104 @@ function buildStyle(options = {}) {
1028
1197
  filter: ["in", "class", "path", "track", "pedestrian"],
1029
1198
  paint: {
1030
1199
  "line-color": p("path"),
1031
- "line-width": ["interpolate", ["linear"], ["zoom"], 13, 0.5, 16, 2],
1032
- "line-dasharray": [2, 1]
1200
+ "line-width": [
1201
+ "interpolate",
1202
+ ["exponential", 1.2],
1203
+ ["zoom"],
1204
+ 13,
1205
+ 0.8,
1206
+ 16,
1207
+ 2.5,
1208
+ 18,
1209
+ 6,
1210
+ 20,
1211
+ 18
1212
+ ],
1213
+ "line-dasharray": [3, 1.5]
1033
1214
  }
1034
1215
  },
1216
+ // Road casings. ALL casings are drawn beneath ALL road fills (rather
1217
+ // than casing/fill pairs) so that at junctions a minor road's casing
1218
+ // never paints over a major road's fill. Colours are derived, not new
1219
+ // palette slots — see `mixColour`.
1220
+ {
1221
+ id: "road-minor-casing",
1222
+ type: "line",
1223
+ source: "territory",
1224
+ "source-layer": "transportation",
1225
+ filter: [
1226
+ "!in",
1227
+ "class",
1228
+ "motorway",
1229
+ "trunk",
1230
+ "primary",
1231
+ "rail",
1232
+ "transit",
1233
+ "path",
1234
+ "track",
1235
+ "pedestrian",
1236
+ "ferry"
1237
+ ],
1238
+ layout: { "line-cap": "round", "line-join": "round" },
1239
+ paint: {
1240
+ "line-color": mixColour(p("road"), p("textSecondary"), 0.45),
1241
+ "line-width": [
1242
+ "interpolate",
1243
+ ["exponential", 1.2],
1244
+ ["zoom"],
1245
+ 12,
1246
+ 0.5,
1247
+ 14,
1248
+ 5,
1249
+ 16,
1250
+ 10,
1251
+ 17,
1252
+ 14,
1253
+ 18,
1254
+ 21,
1255
+ 19,
1256
+ 37,
1257
+ 20,
1258
+ 64
1259
+ ],
1260
+ "line-opacity": tunnelOpacity()
1261
+ }
1262
+ },
1263
+ {
1264
+ id: "road-major-casing",
1265
+ type: "line",
1266
+ source: "territory",
1267
+ "source-layer": "transportation",
1268
+ minzoom: 6,
1269
+ filter: ["in", "class", "motorway", "trunk", "primary"],
1270
+ layout: { "line-cap": "round", "line-join": "round" },
1271
+ paint: {
1272
+ "line-color": mixColour(p("roadMajor"), p("textSecondary"), 0.45),
1273
+ "line-width": [
1274
+ "interpolate",
1275
+ ["exponential", 1.2],
1276
+ ["zoom"],
1277
+ 6,
1278
+ 2,
1279
+ 14,
1280
+ 8,
1281
+ 16,
1282
+ 13,
1283
+ 17,
1284
+ 19,
1285
+ 18,
1286
+ 30,
1287
+ 19,
1288
+ 54,
1289
+ 20,
1290
+ 96
1291
+ ],
1292
+ "line-opacity": tunnelOpacity()
1293
+ }
1294
+ },
1295
+ // Road widths ramp all the way to z20. They previously ended at z16, so
1296
+ // z17+ rendered frozen at z16 weights (4 px minor) against a 15-30 px
1297
+ // reference — the "schematic lines" bug.
1035
1298
  {
1036
1299
  id: "road-minor",
1037
1300
  type: "line",
@@ -1052,7 +1315,28 @@ function buildStyle(options = {}) {
1052
1315
  ],
1053
1316
  paint: {
1054
1317
  "line-color": p("road"),
1055
- "line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.5, 16, 4]
1318
+ "line-width": [
1319
+ "interpolate",
1320
+ ["exponential", 1.2],
1321
+ ["zoom"],
1322
+ 8,
1323
+ 0.5,
1324
+ 13,
1325
+ 1.5,
1326
+ 14,
1327
+ 3,
1328
+ 16,
1329
+ 7,
1330
+ 17,
1331
+ 10,
1332
+ 18,
1333
+ 16,
1334
+ 19,
1335
+ 29,
1336
+ 20,
1337
+ 52
1338
+ ],
1339
+ "line-opacity": tunnelOpacity()
1056
1340
  }
1057
1341
  },
1058
1342
  {
@@ -1063,7 +1347,64 @@ function buildStyle(options = {}) {
1063
1347
  filter: ["in", "class", "motorway", "trunk", "primary"],
1064
1348
  paint: {
1065
1349
  "line-color": p("roadMajor"),
1066
- "line-width": ["interpolate", ["linear"], ["zoom"], 6, 1, 16, 8]
1350
+ "line-width": [
1351
+ "interpolate",
1352
+ ["exponential", 1.2],
1353
+ ["zoom"],
1354
+ 6,
1355
+ 1,
1356
+ 14,
1357
+ 5.5,
1358
+ 16,
1359
+ 10,
1360
+ 17,
1361
+ 15,
1362
+ 18,
1363
+ 24,
1364
+ 19,
1365
+ 44,
1366
+ 20,
1367
+ 80
1368
+ ],
1369
+ "line-opacity": tunnelOpacity()
1370
+ }
1371
+ },
1372
+ // Bridges are REDRAWN on top of the whole flat road network (casing
1373
+ // then fill) so an overpass reads as crossing over what it spans
1374
+ // instead of as a flat junction. This is the proportionate treatment:
1375
+ // true multi-level ordering would need one layer pair per distinct
1376
+ // `layer` value, which is unbounded. The extra-wide casing supplies the
1377
+ // shadow edge that sells the crossing.
1378
+ {
1379
+ id: "road-bridge-casing",
1380
+ type: "line",
1381
+ source: "territory",
1382
+ "source-layer": "transportation",
1383
+ minzoom: 13,
1384
+ filter: ["all", ["==", "brunnel", "bridge"], notARoadFilter()],
1385
+ layout: { "line-cap": "butt", "line-join": "round" },
1386
+ paint: {
1387
+ "line-color": mixColour(p("road"), p("textSecondary"), 0.8),
1388
+ "line-width": ["interpolate", ["exponential", 1.2], ["zoom"], 13, 3, 16, 13, 17, 19, 18, 30, 19, 54, 20, 96]
1389
+ }
1390
+ },
1391
+ {
1392
+ id: "road-bridge",
1393
+ type: "line",
1394
+ source: "territory",
1395
+ "source-layer": "transportation",
1396
+ minzoom: 13,
1397
+ filter: ["all", ["==", "brunnel", "bridge"], notARoadFilter()],
1398
+ layout: { "line-cap": "round", "line-join": "round" },
1399
+ paint: {
1400
+ "line-color": [
1401
+ "match",
1402
+ ["get", "class"],
1403
+ ["motorway", "trunk", "primary"],
1404
+ p("roadMajor"),
1405
+ p("road")
1406
+ ],
1407
+ "line-width": ["interpolate", ["exponential", 1.2], ["zoom"], 13, 1.5, 16, 8, 17, 15, 18, 24, 19, 44, 20, 80]
1067
1408
  }
1068
1409
  },
1069
1410
  {
@@ -1093,14 +1434,77 @@ function buildStyle(options = {}) {
1093
1434
  }
1094
1435
  ];
1095
1436
  if (theme.buildings_3d) {
1096
- const flat = layers.find((l) => l["id"] === "building");
1097
- flat["maxzoom"] = BUILDINGS_3D_MINZOOM;
1437
+ for (const id of ["building", "building-outline"]) {
1438
+ const flat = layers.find((l) => l["id"] === id);
1439
+ flat["maxzoom"] = BUILDINGS_3D_MINZOOM;
1440
+ }
1098
1441
  layers.push(buildings3dLayer(p("building")));
1099
1442
  }
1100
1443
  (theme.extra_layers ?? []).forEach((extra, index) => {
1101
1444
  layers.push(structuredClone(validateExtraLayer(index, extra)));
1102
1445
  });
1103
1446
  layers.push(
1447
+ // One-way arrows, drawn as a TEXT symbol rather than a sprite icon. The
1448
+ // skeleton ships no sprite at all (`sprite` is an optional user-supplied
1449
+ // URL), so an `icon-image` arrow would render nothing for every default
1450
+ // style - a text glyph needs only the glyph server the labels already use.
1451
+ //
1452
+ // The font is PINNED rather than following `theme.fonts`: U+2192 is absent
1453
+ // from 5 of the 15 bundled fontstacks (Barlow, Noto Serif, Nunito, Open
1454
+ // Sans, Rubik - verified by decoding their 8448-8703 glyph ranges), so a
1455
+ // themed font would silently drop every arrow. "Noto Sans Regular" is the
1456
+ // default fontstack and does carry it.
1457
+ //
1458
+ // `text-keep-upright` MUST be false: left at its `true` default MapLibre
1459
+ // flips glyphs on right-to-left line segments to keep them readable, which
1460
+ // would reverse the very thing the arrow encodes.
1461
+ {
1462
+ id: "road-oneway",
1463
+ type: "symbol",
1464
+ source: "territory",
1465
+ "source-layer": "transportation",
1466
+ minzoom: 16,
1467
+ // `oneway` is 1 (forward), -1 (reverse) or 0/absent. The string forms
1468
+ // are matched too: legacy `==` is strictly typed, and a tile build that
1469
+ // encodes the attribute as a string would otherwise match nothing.
1470
+ filter: [
1471
+ "all",
1472
+ [
1473
+ "any",
1474
+ ["==", "oneway", 1],
1475
+ ["==", "oneway", -1],
1476
+ ["==", "oneway", "1"],
1477
+ ["==", "oneway", "-1"]
1478
+ ],
1479
+ notARoadFilter()
1480
+ ],
1481
+ layout: {
1482
+ "symbol-placement": "line",
1483
+ "symbol-spacing": 200,
1484
+ "text-field": "\u2192",
1485
+ "text-font": ["Noto Sans Regular"],
1486
+ "text-size": ["interpolate", ["linear"], ["zoom"], 16, 8, 20, 12],
1487
+ "text-rotation-alignment": "map",
1488
+ "text-pitch-alignment": "map",
1489
+ "text-keep-upright": false,
1490
+ // -1 means the arrow points against the digitisation direction.
1491
+ "text-rotate": [
1492
+ "case",
1493
+ ["any", ["==", ["get", "oneway"], -1], ["==", ["get", "oneway"], "-1"]],
1494
+ 180,
1495
+ 0
1496
+ ],
1497
+ // Arrows are road furniture, not labels: they should not lose
1498
+ // collisions with names, nor push them out.
1499
+ "text-allow-overlap": true,
1500
+ "text-ignore-placement": true
1501
+ },
1502
+ paint: {
1503
+ "text-color": mixColour(p("road"), p("textSecondary"), 0.55),
1504
+ "text-halo-color": p("textHalo"),
1505
+ "text-halo-width": 1
1506
+ }
1507
+ },
1104
1508
  symbol(
1105
1509
  "housenumber",
1106
1510
  "housenumber",
@@ -1116,11 +1520,18 @@ function buildStyle(options = {}) {
1116
1520
  "transportation_name",
1117
1521
  nameField,
1118
1522
  font,
1119
- 11,
1523
+ ["interpolate", ["linear"], ["zoom"], 12, 11, 16, 12.5, 20, 15],
1120
1524
  p("textSecondary"),
1121
1525
  p("textHalo"),
1122
1526
  12,
1123
- { "symbol-placement": "line" }
1527
+ {
1528
+ "symbol-placement": "line",
1529
+ // Street names must be able to REPEAT along a long road: at z18 the
1530
+ // map was drawing 80 POI labels to 11 road labels, reading as a pin
1531
+ // cloud rather than a street network. Spacing tightens as the same
1532
+ // street comes to fill more of the screen.
1533
+ "symbol-spacing": ["interpolate", ["linear"], ["zoom"], 12, 400, 16, 260, 18, 320, 20, 520]
1534
+ }
1124
1535
  ),
1125
1536
  symbol(
1126
1537
  "water-name",
@@ -1131,17 +1542,34 @@ function buildStyle(options = {}) {
1131
1542
  p("textSecondary"),
1132
1543
  p("textHalo")
1133
1544
  ),
1134
- symbol(
1135
- "poi-labels",
1136
- "poi",
1137
- nameField,
1138
- font,
1139
- 11,
1140
- p("textSecondary"),
1141
- p("textHalo"),
1142
- 14,
1143
- { "text-anchor": "top" }
1144
- ),
1545
+ {
1546
+ // POI rebalancing. `poi.rank` is populated on 100% of features (1 = most
1547
+ // prominent), so it can do two jobs: a zoom-stepped ceiling admits only the
1548
+ // headline POIs at mid zooms and opens right up by z18, where the user has
1549
+ // asked for detail; and `symbol-sort-key` makes the survivors compete by
1550
+ // rank, so when labels do collide the landmark wins and the vape shop
1551
+ // loses. Lower sort key = placed first, and lower rank = more important, so
1552
+ // `rank` can be used directly. This rebalances rather than hides.
1553
+ ...symbol(
1554
+ "poi-labels",
1555
+ "poi",
1556
+ nameField,
1557
+ font,
1558
+ 11,
1559
+ p("textSecondary"),
1560
+ p("textHalo"),
1561
+ 14,
1562
+ {
1563
+ "text-anchor": "top",
1564
+ "symbol-sort-key": ["coalesce", ["get", "rank"], 99]
1565
+ }
1566
+ ),
1567
+ filter: [
1568
+ "<=",
1569
+ ["coalesce", ["get", "rank"], 99],
1570
+ ["step", ["zoom"], 6, 15, 10, 16, 15, 17, 22, 18, 40, 19, 120]
1571
+ ]
1572
+ },
1145
1573
  symbol(
1146
1574
  "mountain-peak-labels",
1147
1575
  "mountain_peak",
@@ -1423,7 +1851,7 @@ var MapMapMap = class {
1423
1851
  }
1424
1852
  const minzoom = building.minzoom ?? 13;
1425
1853
  if (enabled) {
1426
- const colour2 = this.map.getPaintProperty("building", "fill-color") ?? "#e2ddd4";
1854
+ const colour2 = this.map.getPaintProperty("building", "fill-color") ?? "#e0d6c4";
1427
1855
  this.map.addLayer(
1428
1856
  buildings3dLayer(colour2),
1429
1857
  buildings3dBeforeId(this.map)
@@ -1481,39 +1909,6 @@ function resolveStyle(style, territoryTilesUrl) {
1481
1909
  return style;
1482
1910
  }
1483
1911
 
1484
- // src/coords.ts
1485
- function toLngLat(point) {
1486
- if (Array.isArray(point)) {
1487
- const [lng2, lat2] = point;
1488
- assertFinite(lng2, lat2);
1489
- return [lng2, lat2];
1490
- }
1491
- const lng = "lng" in point ? point.lng : point.lon;
1492
- const lat = point.lat;
1493
- assertFinite(lng, lat);
1494
- return [lng, lat];
1495
- }
1496
- function formatCoord(point) {
1497
- const [lng, lat] = toLngLat(point);
1498
- return `${lng},${lat}`;
1499
- }
1500
- function formatCoords(points) {
1501
- if (points.length < 2) {
1502
- throw new Error("at least two coordinates are required for a route");
1503
- }
1504
- return points.map(formatCoord).join(";");
1505
- }
1506
- function assertFinite(lng, lat) {
1507
- if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
1508
- throw new Error(`invalid coordinate: lng=${lng}, lat=${lat}`);
1509
- }
1510
- if (lng < -180 || lng > 180 || lat < -90 || lat > 90) {
1511
- throw new Error(
1512
- `coordinate out of range: lng=${lng} (\xB1180), lat=${lat} (\xB190) - check lng/lat order`
1513
- );
1514
- }
1515
- }
1516
-
1517
1912
  // src/osrm.ts
1518
1913
  function buildRouteQuery(truck, guidance) {
1519
1914
  const params = new URLSearchParams({
@@ -1587,10 +1982,38 @@ function numberOr(value, fallback) {
1587
1982
  // src/route.ts
1588
1983
  var SIGNAL_BLUE = "#3a86ff";
1589
1984
  var CASING_COLOR = "#1f438a";
1985
+ var PROGRESS_COLOR = "#b0b0b0";
1986
+ function arrowImage() {
1987
+ const size = 24;
1988
+ const data = new Uint8Array(size * size * 4);
1989
+ const head = 13;
1990
+ const put = (x, y, edge) => {
1991
+ const i = (y * size + x) * 4;
1992
+ const v = edge ? 31 : 255;
1993
+ data[i] = v;
1994
+ data[i + 1] = v;
1995
+ data[i + 2] = edge ? 58 : 255;
1996
+ data[i + 3] = 255;
1997
+ };
1998
+ for (let y = 2; y < head; y++) {
1999
+ const half = Math.round((y - 2) / (head - 3) * 9);
2000
+ for (let x = 11 - half; x <= 12 + half; x++) {
2001
+ put(x, y, x === 11 - half || x === 12 + half || y === 2);
2002
+ }
2003
+ }
2004
+ for (let y = head; y < 22; y++) {
2005
+ for (let x = 9; x <= 14; x++) {
2006
+ put(x, y, x === 9 || x === 14 || y === 21);
2007
+ }
2008
+ }
2009
+ return { width: size, height: size, data };
2010
+ }
1590
2011
  var RouteLayer = class {
1591
2012
  constructor(map, options = {}) {
2013
+ this.progress = 0;
1592
2014
  this.handleStyleLoad = () => {
1593
2015
  if (this.lastRoute) this.install(this.lastRoute);
2016
+ else this.installManeuver();
1594
2017
  };
1595
2018
  if (map instanceof MapMapMap) {
1596
2019
  this.map = map.map;
@@ -1611,6 +2034,10 @@ var RouteLayer = class {
1611
2034
  this.sourceId = `${id}-src`;
1612
2035
  this.casingLayerId = `${id}-casing`;
1613
2036
  this.lineLayerId = `${id}-line`;
2037
+ this.maneuverSourceId = `${id}-maneuver-src`;
2038
+ this.maneuverLayerId = `${id}-maneuver`;
2039
+ this.arrowImageId = `${id}-arrow`;
2040
+ this.progressColor = options.progressColor ?? PROGRESS_COLOR;
1614
2041
  this.map.on("style.load", this.handleStyleLoad);
1615
2042
  }
1616
2043
  /**
@@ -1672,9 +2099,11 @@ var RouteLayer = class {
1672
2099
  if (existing) {
1673
2100
  existing.setData(data);
1674
2101
  } else {
1675
- this.map.addSource(this.sourceId, { type: "geojson", data });
2102
+ this.map.addSource(this.sourceId, { type: "geojson", data, lineMetrics: true });
1676
2103
  }
1677
2104
  if (this.map.getLayer(this.casingLayerId) && this.map.getLayer(this.lineLayerId)) {
2105
+ this.applyProgress();
2106
+ this.installManeuver();
1678
2107
  return;
1679
2108
  }
1680
2109
  const casing = {
@@ -1707,15 +2136,86 @@ var RouteLayer = class {
1707
2136
  };
1708
2137
  if (!this.map.getLayer(this.casingLayerId)) this.map.addLayer(casing);
1709
2138
  if (!this.map.getLayer(this.lineLayerId)) this.map.addLayer(line);
2139
+ this.applyProgress();
2140
+ this.installManeuver();
2141
+ }
2142
+ /**
2143
+ * Sets how much of the route has been travelled, as a fraction in `[0, 1]`
2144
+ * of the line's length. The travelled part dims to `progressColor` (the
2145
+ * "vanishing route line"); `0` restores the untinted line. The value is
2146
+ * remembered across {@link draw} calls and style swaps. Pair with the
2147
+ * guidance module's distance-remaining to derive the fraction.
2148
+ */
2149
+ setProgress(fraction) {
2150
+ this.progress = Math.min(1, Math.max(0, fraction));
2151
+ if (this.map.getLayer(this.lineLayerId)) this.applyProgress();
2152
+ }
2153
+ /** Applies the current progress fraction to the line layer's gradient. */
2154
+ applyProgress() {
2155
+ const routeColor = this.design?.color ?? SIGNAL_BLUE;
2156
+ const gradient = this.progress > 0 ? ["step", ["line-progress"], this.progressColor, this.progress, routeColor] : void 0;
2157
+ this.map.setPaintProperty(this.lineLayerId, "line-gradient", gradient);
2158
+ }
2159
+ /**
2160
+ * Shows (or moves) the upcoming-manoeuvre arrow: a small map-aligned
2161
+ * arrow at `lngLat` rotated to `bearingDeg` (clockwise from north).
2162
+ * Survives style swaps until {@link clearManeuver}.
2163
+ */
2164
+ setManeuver(lngLat, bearingDeg) {
2165
+ this.maneuver = { lngLat, bearingDeg };
2166
+ if (this.map.isStyleLoaded()) this.installManeuver();
2167
+ }
2168
+ /** Hides the manoeuvre arrow. */
2169
+ clearManeuver() {
2170
+ this.maneuver = void 0;
2171
+ if (this.map.getLayer(this.maneuverLayerId)) this.map.removeLayer(this.maneuverLayerId);
2172
+ if (this.map.getSource(this.maneuverSourceId)) this.map.removeSource(this.maneuverSourceId);
2173
+ }
2174
+ /** Add-or-update the manoeuvre arrow source/layer for the current style. */
2175
+ installManeuver() {
2176
+ if (!this.maneuver) return;
2177
+ const data = {
2178
+ type: "Feature",
2179
+ properties: { bearing: this.maneuver.bearingDeg },
2180
+ geometry: { type: "Point", coordinates: this.maneuver.lngLat }
2181
+ };
2182
+ const source = this.map.getSource(this.maneuverSourceId);
2183
+ if (source) {
2184
+ source.setData(data);
2185
+ } else {
2186
+ this.map.addSource(this.maneuverSourceId, { type: "geojson", data });
2187
+ }
2188
+ if (!this.map.hasImage(this.arrowImageId)) {
2189
+ this.map.addImage(this.arrowImageId, arrowImage());
2190
+ }
2191
+ if (!this.map.getLayer(this.maneuverLayerId)) {
2192
+ this.map.addLayer({
2193
+ id: this.maneuverLayerId,
2194
+ type: "symbol",
2195
+ source: this.maneuverSourceId,
2196
+ layout: {
2197
+ "icon-image": this.arrowImageId,
2198
+ "icon-rotate": ["get", "bearing"],
2199
+ "icon-rotation-alignment": "map",
2200
+ "icon-allow-overlap": true,
2201
+ "icon-ignore-placement": true,
2202
+ "icon-size": ["interpolate", ["linear"], ["zoom"], 12, 0.7, 18, 1.4]
2203
+ }
2204
+ });
2205
+ } else {
2206
+ this.map.setLayoutProperty(this.maneuverLayerId, "icon-rotate", ["get", "bearing"]);
2207
+ }
1710
2208
  }
1711
2209
  /** Remove the route's layers and source from the map. */
1712
2210
  clear() {
2211
+ this.clearManeuver();
1713
2212
  for (const layerId of [this.lineLayerId, this.casingLayerId]) {
1714
2213
  if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
1715
2214
  }
1716
2215
  if (this.map.getSource(this.sourceId)) this.map.removeSource(this.sourceId);
1717
2216
  this.lastRoute = void 0;
1718
2217
  this.owner?.setRouteEffectGeometry(null);
2218
+ this.progress = 0;
1719
2219
  }
1720
2220
  /**
1721
2221
  * Remove the route and detach the layer's `style.load` listener. Call
@@ -1741,6 +2241,12 @@ function effectiveImageUrl(design) {
1741
2241
  }
1742
2242
  return url;
1743
2243
  }
2244
+ function shortestArcDeg(fromDeg, toDeg) {
2245
+ const raw = ((toDeg - fromDeg) % 360 + 360) % 360;
2246
+ return raw > 180 ? raw - 360 : raw;
2247
+ }
2248
+ var MAX_TWEEN_MS = 900;
2249
+ var MIN_TWEEN_MS = 100;
1744
2250
  var PositionPuck = class {
1745
2251
  /**
1746
2252
  * Creates the puck (not yet on the map - it appears on the first
@@ -1748,12 +2254,17 @@ var PositionPuck = class {
1748
2254
  * `navDesign.puck` when given a `MapMapMap` whose theme carried an
1749
2255
  * `extra.nav` block, then to the built-in blue puck.
1750
2256
  */
1751
- constructor(map, design) {
2257
+ constructor(map, design, options) {
1752
2258
  this.added = false;
1753
2259
  this.map = map instanceof MapMapMap ? map.map : map;
1754
2260
  this.design = design ?? (map instanceof MapMapMap ? map.navDesign?.puck : void 0) ?? defaultNavDesign().puck;
1755
2261
  this.element = createPuckElement();
1756
2262
  stylePuckElement(this.element, this.design);
2263
+ this.interpolate = options?.interpolate ?? true;
2264
+ this.now = options?.now ?? Date.now;
2265
+ const g = globalThis;
2266
+ this.requestFrame = options?.requestFrame ?? (g.requestAnimationFrame ? (cb) => g.requestAnimationFrame(() => cb()) : void 0);
2267
+ this.cancelFrame = options?.cancelFrame ?? (g.cancelAnimationFrame ? (h) => g.cancelAnimationFrame(h) : void 0);
1757
2268
  this.marker = new maplibregl.Marker({
1758
2269
  element: this.element,
1759
2270
  rotationAlignment: "map",
@@ -1764,20 +2275,72 @@ var PositionPuck = class {
1764
2275
  * Moves the puck (adding it to the map on the first call). `headingDeg`
1765
2276
  * rotates the whole element - arrow or custom image - clockwise from
1766
2277
  * north; omit it to keep the previous heading.
2278
+ *
2279
+ * With interpolation on (the default) every call after the first glides
2280
+ * from the currently rendered position - a fix arriving mid-tween
2281
+ * retargets smoothly rather than jumping.
1767
2282
  */
1768
2283
  setLocation(location, headingDeg) {
1769
- this.marker.setLngLat([location.lon, location.lat]);
1770
- if (headingDeg !== void 0) this.marker.setRotation(headingDeg);
1771
- if (!this.added) {
2284
+ const first = !this.added;
2285
+ const nowMs = this.now();
2286
+ const interval = this.lastFixAt === void 0 ? MAX_TWEEN_MS : nowMs - this.lastFixAt;
2287
+ this.lastFixAt = nowMs;
2288
+ this.cancelTween();
2289
+ const from = this.rendered;
2290
+ const target = {
2291
+ lat: location.lat,
2292
+ lon: location.lon,
2293
+ heading: headingDeg ?? from?.heading ?? 0
2294
+ };
2295
+ const duration = Math.min(MAX_TWEEN_MS, interval);
2296
+ if (first || !this.interpolate || !this.requestFrame || !from || duration < MIN_TWEEN_MS) {
2297
+ this.render(target);
2298
+ } else {
2299
+ this.tween(from, target, nowMs, duration);
2300
+ }
2301
+ if (first) {
1772
2302
  this.marker.addTo(this.map);
1773
2303
  this.added = true;
1774
2304
  }
1775
2305
  }
1776
- /** Removes the puck from the map. `setLocation` re-adds it. */
2306
+ /** Removes the puck from the map (cancelling any tween). `setLocation` re-adds it. */
1777
2307
  remove() {
2308
+ this.cancelTween();
1778
2309
  this.marker.remove();
1779
2310
  this.added = false;
1780
2311
  }
2312
+ /** Applies a position/heading to the marker immediately. */
2313
+ render(state) {
2314
+ this.marker.setLngLat([state.lon, state.lat]);
2315
+ this.marker.setRotation(state.heading);
2316
+ this.rendered = state;
2317
+ }
2318
+ /** Runs a linear position lerp + shortest-arc heading tween via rAF. */
2319
+ tween(from, to, startedAt, duration) {
2320
+ const headingDelta = shortestArcDeg(from.heading, to.heading);
2321
+ const step = () => {
2322
+ const t = Math.min(1, (this.now() - startedAt) / duration);
2323
+ this.render({
2324
+ lat: from.lat + (to.lat - from.lat) * t,
2325
+ lon: from.lon + (to.lon - from.lon) * t,
2326
+ heading: from.heading + headingDelta * t
2327
+ });
2328
+ if (t < 1) {
2329
+ this.frameHandle = this.requestFrame(step);
2330
+ } else {
2331
+ this.frameHandle = void 0;
2332
+ this.render(to);
2333
+ }
2334
+ };
2335
+ this.frameHandle = this.requestFrame(step);
2336
+ }
2337
+ /** Cancels an in-flight tween, leaving the marker where it rendered last. */
2338
+ cancelTween() {
2339
+ if (this.frameHandle !== void 0) {
2340
+ this.cancelFrame?.(this.frameHandle);
2341
+ this.frameHandle = void 0;
2342
+ }
2343
+ }
1781
2344
  };
1782
2345
  function createPuckElement() {
1783
2346
  const doc = globalThis.document;
@@ -1826,6 +2389,186 @@ function stylePuckElement(el, puck) {
1826
2389
  dot.style.cssText = `position:absolute;inset:0;border-radius:50%;background:${puck.color};border:2px solid #ffffff;box-shadow:0 1px 6px rgba(0,0,0,0.45)`;
1827
2390
  arrow.style.cssText = puck.headingArrow ? `position:absolute;left:50%;top:${(-s * 0.55).toFixed(1)}px;transform:translateX(-50%);width:0;height:0;border-left:${(s * 0.35).toFixed(1)}px solid transparent;border-right:${(s * 0.35).toFixed(1)}px solid transparent;border-bottom:${(s * 0.6).toFixed(1)}px solid ${puck.color}` : "display:none";
1828
2391
  }
2392
+
2393
+ // src/daynight.ts
2394
+ var DEG = Math.PI / 180;
2395
+ var DAY_MS = 864e5;
2396
+ var JULIAN_EPOCH = 24405875e-1;
2397
+ var J2000 = 2451545;
2398
+ function fromJulian(julian) {
2399
+ return new Date((julian - JULIAN_EPOCH) * DAY_MS);
2400
+ }
2401
+ function sunTimes(date, lat, lng) {
2402
+ const julianDay = Math.ceil(date.getTime() / DAY_MS + JULIAN_EPOCH - J2000 - 9e-4 + lng / 360);
2403
+ const meanSolarTime = julianDay + 9e-4 - lng / 360;
2404
+ const meanAnomalyDeg = (357.5291 + 0.98560028 * meanSolarTime) % 360;
2405
+ const m = meanAnomalyDeg * DEG;
2406
+ const centreDeg = 1.9148 * Math.sin(m) + 0.02 * Math.sin(2 * m) + 3e-4 * Math.sin(3 * m);
2407
+ const eclipticLngDeg = (meanAnomalyDeg + centreDeg + 180 + 102.9372) % 360;
2408
+ const l = eclipticLngDeg * DEG;
2409
+ const transit = J2000 + meanSolarTime + 53e-4 * Math.sin(m) - 69e-4 * Math.sin(2 * l);
2410
+ const sinDeclination = Math.sin(l) * Math.sin(23.4397 * DEG);
2411
+ const cosDeclination = Math.cos(Math.asin(sinDeclination));
2412
+ const cosHourAngle = (Math.sin(-0.833 * DEG) - Math.sin(lat * DEG) * sinDeclination) / (Math.cos(lat * DEG) * cosDeclination);
2413
+ if (cosHourAngle < -1) return "polarDay";
2414
+ if (cosHourAngle > 1) return "polarNight";
2415
+ const hourAngleDeg = Math.acos(cosHourAngle) / DEG;
2416
+ return {
2417
+ sunrise: fromJulian(transit - hourAngleDeg / 360),
2418
+ sunset: fromJulian(transit + hourAngleDeg / 360)
2419
+ };
2420
+ }
2421
+ function resolveTheme(date, lat, lng) {
2422
+ const times = sunTimes(date, lat, lng);
2423
+ if (times === "polarDay") return "light";
2424
+ if (times === "polarNight") return "dark";
2425
+ return date >= times.sunrise && date < times.sunset ? "light" : "dark";
2426
+ }
2427
+ var POLAR_RECHECK_MS = 6 * 36e5;
2428
+ var BOUNDARY_MARGIN_MS = 1e3;
2429
+ var ThemeScheduler = class {
2430
+ constructor(options) {
2431
+ this.disposed = false;
2432
+ this.lat = options.lat;
2433
+ this.lng = options.lng;
2434
+ this.onLight = options.onLight;
2435
+ this.onDark = options.onDark;
2436
+ this.now = options.now ?? Date.now;
2437
+ this.setTimeoutFn = options.setTimeoutFn ?? ((cb, ms) => setTimeout(cb, ms));
2438
+ this.clearTimeoutFn = options.clearTimeoutFn ?? ((h) => clearTimeout(h));
2439
+ this.evaluate();
2440
+ }
2441
+ /** The theme most recently applied, if any. */
2442
+ get current() {
2443
+ return this.applied;
2444
+ }
2445
+ /** Moves the observer (e.g. a new GPS fix region) and re-evaluates. */
2446
+ setPosition(lat, lng) {
2447
+ this.lat = lat;
2448
+ this.lng = lng;
2449
+ this.evaluate();
2450
+ }
2451
+ /** Stops all future flips. */
2452
+ dispose() {
2453
+ this.disposed = true;
2454
+ if (this.handle !== void 0) this.clearTimeoutFn(this.handle);
2455
+ this.handle = void 0;
2456
+ }
2457
+ /** Applies the theme for now and arms the timer for the next boundary. */
2458
+ evaluate() {
2459
+ if (this.disposed) return;
2460
+ if (this.handle !== void 0) {
2461
+ this.clearTimeoutFn(this.handle);
2462
+ this.handle = void 0;
2463
+ }
2464
+ const nowDate = new Date(this.now());
2465
+ const theme = resolveTheme(nowDate, this.lat, this.lng);
2466
+ if (theme !== this.applied) {
2467
+ this.applied = theme;
2468
+ (theme === "light" ? this.onLight : this.onDark)();
2469
+ }
2470
+ const delay = this.nextBoundaryDelay(nowDate);
2471
+ this.handle = this.setTimeoutFn(() => this.evaluate(), delay);
2472
+ }
2473
+ /** Milliseconds until the next sunrise/sunset (or the polar re-check). */
2474
+ nextBoundaryDelay(nowDate) {
2475
+ const nowMs = nowDate.getTime();
2476
+ for (const dayOffset of [0, 1]) {
2477
+ const times = sunTimes(new Date(nowMs + dayOffset * DAY_MS), this.lat, this.lng);
2478
+ if (times === "polarDay" || times === "polarNight") continue;
2479
+ for (const event of [times.sunrise, times.sunset]) {
2480
+ const delta = event.getTime() - nowMs;
2481
+ if (delta > 0) return delta + BOUNDARY_MARGIN_MS;
2482
+ }
2483
+ }
2484
+ return POLAR_RECHECK_MS;
2485
+ }
2486
+ };
2487
+
2488
+ // src/language.ts
2489
+ var LANGUAGE_TAG = /^[a-z]{2,3}(-[A-Za-z0-9]{2,4})?$/;
2490
+ function languageTextField(language) {
2491
+ if (language === null) {
2492
+ return ["coalesce", ["get", "name:en"], ["get", "name"]];
2493
+ }
2494
+ return [
2495
+ "coalesce",
2496
+ ["get", `name:${language}`],
2497
+ ["get", "name:latin"],
2498
+ ["get", "name"]
2499
+ ];
2500
+ }
2501
+ function isNameTextField(textField) {
2502
+ if (typeof textField === "string") {
2503
+ return /\{name(?::[A-Za-z0-9-]+)?\}/.test(textField);
2504
+ }
2505
+ if (Array.isArray(textField)) {
2506
+ if (textField.length === 2 && textField[0] === "get" && typeof textField[1] === "string") {
2507
+ return textField[1] === "name" || textField[1].startsWith("name:");
2508
+ }
2509
+ return textField.some((part) => isNameTextField(part));
2510
+ }
2511
+ return false;
2512
+ }
2513
+ function setMapLanguage(map, language) {
2514
+ if (language !== null && !LANGUAGE_TAG.test(language)) {
2515
+ throw new Error(
2516
+ `invalid language tag ${JSON.stringify(language)}; use e.g. "de", "pt-BR", "zh-Hans"`
2517
+ );
2518
+ }
2519
+ const ml = map instanceof MapMapMap ? map.map : map;
2520
+ const layers = ml.getStyle()?.layers ?? [];
2521
+ const changed = [];
2522
+ for (const layer of layers) {
2523
+ if (layer.type !== "symbol") continue;
2524
+ const textField = layer.layout?.["text-field"];
2525
+ if (textField === void 0 || !isNameTextField(textField)) continue;
2526
+ ml.setLayoutProperty(layer.id, "text-field", languageTextField(language));
2527
+ changed.push(layer.id);
2528
+ }
2529
+ return changed;
2530
+ }
2531
+
2532
+ // src/probe.ts
2533
+ function buildProbeUrl(baseUrl) {
2534
+ return `${baseUrl.replace(/\/+$/, "")}/v1/probe`;
2535
+ }
2536
+ async function uploadProbeBatch(baseUrl, apiKey, body, options = {}) {
2537
+ const doFetch = options.fetch ?? globalThis.fetch;
2538
+ const maxAttempts = options.maxAttempts ?? 4;
2539
+ const backoffMs = options.backoffMs ?? ((attempt) => attempt * 1e3);
2540
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2541
+ const url = buildProbeUrl(baseUrl);
2542
+ for (let attempt = 1; ; attempt++) {
2543
+ let outcome;
2544
+ try {
2545
+ const response = await doFetch(url, {
2546
+ method: "POST",
2547
+ headers: {
2548
+ Authorization: `Bearer ${apiKey}`,
2549
+ "Content-Type": "application/json"
2550
+ },
2551
+ body,
2552
+ signal: options.signal
2553
+ });
2554
+ outcome = classifyStatus(response.status);
2555
+ } catch {
2556
+ outcome = "gaveUp";
2557
+ }
2558
+ if (outcome === "gaveUp" && attempt < maxAttempts) {
2559
+ await sleep(backoffMs(attempt));
2560
+ continue;
2561
+ }
2562
+ return outcome;
2563
+ }
2564
+ }
2565
+ function classifyStatus(status) {
2566
+ if (status === 202) return "accepted";
2567
+ if (status === 403) return "refused";
2568
+ if (status === 501) return "notEnabled";
2569
+ if (status >= 500 && status <= 599) return "gaveUp";
2570
+ return "rejected";
2571
+ }
1829
2572
  var EARTH_RADIUS_M2 = 63710088e-1;
1830
2573
  var CLUSTER_TEXT_FONT = "Noto Sans Regular";
1831
2574
  function placesFromGeoJSON(collection) {
@@ -2464,8 +3207,9 @@ function shortestArcDelta(from, to) {
2464
3207
  }
2465
3208
  var PoseSampler = class {
2466
3209
  constructor(coordinates) {
2467
- this.points = coordinates.filter(
2468
- (p, i) => i === 0 || p[0] !== coordinates[i - 1][0] || p[1] !== coordinates[i - 1][1]
3210
+ const unwrapped = unwrapLngs(coordinates);
3211
+ this.points = unwrapped.filter(
3212
+ (p, i) => i === 0 || p[0] !== unwrapped[i - 1][0] || p[1] !== unwrapped[i - 1][1]
2469
3213
  );
2470
3214
  if (this.points.length < 2) {
2471
3215
  throw new Error(
@@ -2576,6 +3320,7 @@ function flythrough(map, route, options = {}) {
2576
3320
  seek(to) {
2577
3321
  if (destroyed) return;
2578
3322
  t = Math.min(1, Math.max(0, to));
3323
+ bearing = void 0;
2579
3324
  applyPose(1 / 60);
2580
3325
  },
2581
3326
  get speed() {
@@ -2748,6 +3493,7 @@ var IsochroneLayer = class {
2748
3493
  this.fillOpacity()
2749
3494
  );
2750
3495
  this.map.setPaintProperty(this.lineLayerId, "line-color", this.lastColor);
3496
+ this.map.setPaintProperty(this.labelLayerId, "text-color", this.lastColor);
2751
3497
  return;
2752
3498
  }
2753
3499
  const fill2 = {
@@ -3138,6 +3884,6 @@ function clampVolume(volume) {
3138
3884
  return Math.min(1, Math.max(0, volume));
3139
3885
  }
3140
3886
 
3141
- export { AdrCheck, DEFAULT_GLYPHS_URL, DEFAULT_TERRITORY_TILES_URL, EFFECTS_METADATA_KEY, FLOW_DEFAULTS, FULL_ATTRIBUTION, FlowRouteEffectLayer, GuidanceBanner, IsochroneLayer, LOGO_SVG, LogoControl, MAX_PUCK_IMAGE_BYTES, MapMapMap, NAV_CAMERA_DEFAULTS, NavigationCamera, OPENMAPTILES_ATTRIBUTION, OSM_ATTRIBUTION, PALETTE_SLOTS, POI_CATEGORY_COLORS, POI_CATEGORY_IDS, POI_CLASS_CATEGORIES, PlacesLayer, PositionPuck, RIBBON_FLOATS_PER_VERTEX, ROUTE_EFFECTS, RouteLayer, SIGNAL_BLUE, SOURCE_LAYERS, VoiceGuidance, applyPoiDesign, bannerLanes, bearingBetween, bindFlythroughToScroll, buildAdrCheckBody, buildRouteQuery, buildRouteUrl, buildStyle, builtInPoiColor, createMap, createRouteEffect, defaultNavDesign, defaultPoiDesign, directionArrow, effectsFromStyleMetadata, extractGuidance, flythrough, flythroughPose, formatCoord, formatCoords, haversineDistanceM, lngLatToMercator, navDesignFromTheme, navDesignFromThemeUrl, parseCssColour, parseNavDesign, parseOsrmRoute, parsePoiDesign, placesFromGeoJSON, poiDesignFromTheme, poiDesignFromThemeUrl, poiDesignIsDefault, poiTextColorExpression, prefersReducedMotion, registerPmtilesProtocol, resetDiagnostics, runMapDiagnostics, severityProsody, shortestArcDelta, speak, ssmlToText, tessellateRouteRibbon, toLngLat, toPmtilesUrl };
3887
+ export { AdrCheck, DEFAULT_GLYPHS_URL, DEFAULT_TERRITORY_TILES_URL, EFFECTS_METADATA_KEY, FLOW_DEFAULTS, FULL_ATTRIBUTION, FlowRouteEffectLayer, GuidanceBanner, IsochroneLayer, LOGO_SVG, LogoControl, MAX_PUCK_IMAGE_BYTES, MapMapMap, NAV_CAMERA_DEFAULTS, NavigationCamera, OPENMAPTILES_ATTRIBUTION, OSM_ATTRIBUTION, PALETTE_SLOTS, POI_CATEGORY_COLORS, POI_CATEGORY_IDS, POI_CLASS_CATEGORIES, PlacesLayer, PositionPuck, RIBBON_FLOATS_PER_VERTEX, ROUTE_EFFECTS, RouteLayer, SIGNAL_BLUE, SOURCE_LAYERS, ThemeScheduler, VoiceGuidance, applyPoiDesign, bannerLanes, bearingBetween, bindFlythroughToScroll, buildAdrCheckBody, buildProbeUrl, buildRouteQuery, buildRouteUrl, buildStyle, builtInPoiColor, createMap, createRouteEffect, defaultNavDesign, defaultPoiDesign, directionArrow, effectsFromStyleMetadata, extractGuidance, flythrough, flythroughPose, formatCoord, formatCoords, haversineDistanceM, isNameTextField, languageTextField, lngLatToMercator, navDesignFromTheme, navDesignFromThemeUrl, parseCssColour, parseNavDesign, parseOsrmRoute, parsePoiDesign, placesFromGeoJSON, poiDesignFromTheme, poiDesignFromThemeUrl, poiDesignIsDefault, poiTextColorExpression, prefersReducedMotion, registerPmtilesProtocol, resetDiagnostics, resolveTheme, runMapDiagnostics, setMapLanguage, severityProsody, shortestArcDeg, shortestArcDelta, speak, ssmlToText, sunTimes, tessellateRouteRibbon, toLngLat, toPmtilesUrl, uploadProbeBatch };
3142
3888
  //# sourceMappingURL=index.js.map
3143
3889
  //# sourceMappingURL=index.js.map