@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 +201 -12
- package/dist/{chunk-CH53LJR7.js → chunk-J657OWYE.js} +36 -3
- package/dist/chunk-J657OWYE.js.map +1 -0
- package/dist/direction-icons.d.ts +33 -5
- package/dist/direction-icons.js +1 -1
- package/dist/index.d.ts +799 -18
- package/dist/index.js +1341 -80
- package/dist/index.js.map +1 -1
- package/llms-sdk.txt +91 -1
- package/package.json +4 -3
- package/dist/chunk-CH53LJR7.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { directionIcons, iconNameForStep } from './chunk-
|
|
2
|
-
export { directionIconSvg, directionIcons, iconNameForManeuver, iconNameForStep } from './chunk-
|
|
1
|
+
import { directionIcons, iconNameForStep } from './chunk-J657OWYE.js';
|
|
2
|
+
export { directionIconSvg, directionIcons, iconNameForManeuver, iconNameForStep, iconNamesForSteps } from './chunk-J657OWYE.js';
|
|
3
3
|
import maplibregl from 'maplibre-gl';
|
|
4
4
|
import { Protocol } from 'pmtiles';
|
|
5
5
|
|
|
@@ -619,6 +619,10 @@ function num(v, fallback, min, max) {
|
|
|
619
619
|
function optNum(v, min, max) {
|
|
620
620
|
return typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : void 0;
|
|
621
621
|
}
|
|
622
|
+
function dashArray(v) {
|
|
623
|
+
if (!Array.isArray(v) || v.length === 0) return void 0;
|
|
624
|
+
return v.every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0) ? v : void 0;
|
|
625
|
+
}
|
|
622
626
|
function bool(v, fallback) {
|
|
623
627
|
return typeof v === "boolean" ? v : fallback;
|
|
624
628
|
}
|
|
@@ -692,13 +696,23 @@ function parseNavDesign(value) {
|
|
|
692
696
|
const route = isRecord(value.route) ? value.route : {};
|
|
693
697
|
const puck = isRecord(value.puck) ? value.puck : {};
|
|
694
698
|
const banner = isRecord(value.banner) ? value.banner : {};
|
|
699
|
+
const casingWidth = optNum(route.casingWidth, 0.5, 40);
|
|
700
|
+
const casingOpacity = optNum(route.casingOpacity, 0, 1);
|
|
701
|
+
const dash = dashArray(route.dash);
|
|
695
702
|
return {
|
|
696
703
|
version: 1,
|
|
697
704
|
route: {
|
|
698
705
|
color: str(route.color, d.route.color),
|
|
699
706
|
width: num(route.width, d.route.width, 0.5, 20),
|
|
700
707
|
opacity: num(route.opacity, d.route.opacity, 0, 1),
|
|
701
|
-
casingColor: str(route.casingColor, d.route.casingColor)
|
|
708
|
+
casingColor: str(route.casingColor, d.route.casingColor),
|
|
709
|
+
// Casing width/opacity and the dash pattern stay OPTIONAL: absent must
|
|
710
|
+
// round-trip as absent so `RouteLayer` keeps its documented `width + 4`
|
|
711
|
+
// and shared-opacity fallbacks, and a theme saved before these fields
|
|
712
|
+
// existed re-exports byte-identical.
|
|
713
|
+
...casingWidth !== void 0 ? { casingWidth } : {},
|
|
714
|
+
...casingOpacity !== void 0 ? { casingOpacity } : {},
|
|
715
|
+
...dash !== void 0 ? { dash } : {}
|
|
702
716
|
},
|
|
703
717
|
puck: {
|
|
704
718
|
color: str(puck.color, d.puck.color),
|
|
@@ -1098,8 +1112,22 @@ function landcoverColour(p) {
|
|
|
1098
1112
|
base
|
|
1099
1113
|
];
|
|
1100
1114
|
}
|
|
1115
|
+
var NOT_A_ROAD_CLASSES = [
|
|
1116
|
+
"rail",
|
|
1117
|
+
"transit",
|
|
1118
|
+
"ferry",
|
|
1119
|
+
"path",
|
|
1120
|
+
"track",
|
|
1121
|
+
"pedestrian",
|
|
1122
|
+
"aerialway",
|
|
1123
|
+
"bridge",
|
|
1124
|
+
"pier"
|
|
1125
|
+
];
|
|
1101
1126
|
function notARoadFilter() {
|
|
1102
|
-
return ["!in", "class",
|
|
1127
|
+
return ["!in", "class", ...NOT_A_ROAD_CLASSES];
|
|
1128
|
+
}
|
|
1129
|
+
function minorRoadFilter() {
|
|
1130
|
+
return ["!in", "class", "motorway", "trunk", "primary", ...NOT_A_ROAD_CLASSES];
|
|
1103
1131
|
}
|
|
1104
1132
|
function tunnelOpacity() {
|
|
1105
1133
|
return ["match", ["get", "brunnel"], "tunnel", 0.55, 1];
|
|
@@ -1346,12 +1374,23 @@ function buildStyle(options = {}) {
|
|
|
1346
1374
|
source: "territory",
|
|
1347
1375
|
"source-layer": "transportation",
|
|
1348
1376
|
minzoom: 14,
|
|
1349
|
-
//
|
|
1350
|
-
//
|
|
1351
|
-
//
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1354
|
-
|
|
1377
|
+
// The geometry-type clause is LOAD-BEARING, not belt and braces. A fill
|
|
1378
|
+
// layer does not skip line geometry: MapLibre's FillBucket tessellates
|
|
1379
|
+
// every ring `loadGeometry()` hands it, so an open footway LineString
|
|
1380
|
+
// gets closed and filled, and the chord from its last vertex back to its
|
|
1381
|
+
// first is drawn as a solid wedge. Over land that wedge hides in the
|
|
1382
|
+
// landuse beige; over water it is a grey slab across the bay. Class
|
|
1383
|
+
// alone matched 206 features at Lugano z15, 165 of them lines.
|
|
1384
|
+
//
|
|
1385
|
+
// Both Polygon and MultiPolygon are needed: MapLibre reports a plaza as
|
|
1386
|
+
// "MultiPolygon" as soon as it has more than one ring (a hole is
|
|
1387
|
+
// enough), so matching only "Polygon" silently drops the
|
|
1388
|
+
// doughnut-shaped plazas.
|
|
1389
|
+
filter: [
|
|
1390
|
+
"all",
|
|
1391
|
+
["match", ["get", "class"], ["path", "pedestrian"], true, false],
|
|
1392
|
+
["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]]
|
|
1393
|
+
],
|
|
1355
1394
|
paint: {
|
|
1356
1395
|
"fill-color": p("path"),
|
|
1357
1396
|
"fill-opacity": 0.7
|
|
@@ -1391,19 +1430,7 @@ function buildStyle(options = {}) {
|
|
|
1391
1430
|
type: "line",
|
|
1392
1431
|
source: "territory",
|
|
1393
1432
|
"source-layer": "transportation",
|
|
1394
|
-
filter:
|
|
1395
|
-
"!in",
|
|
1396
|
-
"class",
|
|
1397
|
-
"motorway",
|
|
1398
|
-
"trunk",
|
|
1399
|
-
"primary",
|
|
1400
|
-
"rail",
|
|
1401
|
-
"transit",
|
|
1402
|
-
"path",
|
|
1403
|
-
"track",
|
|
1404
|
-
"pedestrian",
|
|
1405
|
-
"ferry"
|
|
1406
|
-
],
|
|
1433
|
+
filter: minorRoadFilter(),
|
|
1407
1434
|
layout: { "line-cap": "round", "line-join": "round" },
|
|
1408
1435
|
paint: {
|
|
1409
1436
|
"line-color": mixColour(p("road"), p("textSecondary"), 0.45),
|
|
@@ -1472,19 +1499,7 @@ function buildStyle(options = {}) {
|
|
|
1472
1499
|
type: "line",
|
|
1473
1500
|
source: "territory",
|
|
1474
1501
|
"source-layer": "transportation",
|
|
1475
|
-
filter:
|
|
1476
|
-
"!in",
|
|
1477
|
-
"class",
|
|
1478
|
-
"motorway",
|
|
1479
|
-
"trunk",
|
|
1480
|
-
"primary",
|
|
1481
|
-
"rail",
|
|
1482
|
-
"transit",
|
|
1483
|
-
"path",
|
|
1484
|
-
"track",
|
|
1485
|
-
"pedestrian",
|
|
1486
|
-
"ferry"
|
|
1487
|
-
],
|
|
1502
|
+
filter: minorRoadFilter(),
|
|
1488
1503
|
layout: { "line-cap": "round", "line-join": "round" },
|
|
1489
1504
|
paint: {
|
|
1490
1505
|
"line-color": p("road"),
|
|
@@ -1870,7 +1885,14 @@ var MapMapMap = class {
|
|
|
1870
1885
|
this.effectName = null;
|
|
1871
1886
|
this.effectOptions = {};
|
|
1872
1887
|
this.effectExplicit = false;
|
|
1888
|
+
/** True while an effect re-apply is already queued behind the style. */
|
|
1889
|
+
this.effectWaiting = false;
|
|
1890
|
+
// Lifecycle: `destroy()` must silence anything still queued behind a
|
|
1891
|
+
// style load, and the cleanups let it detach those listeners.
|
|
1892
|
+
this.destroyed = false;
|
|
1893
|
+
this.pendingWaits = /* @__PURE__ */ new Set();
|
|
1873
1894
|
this.handleStyleLoadForEffects = () => {
|
|
1895
|
+
if (this.destroyed) return;
|
|
1874
1896
|
if (!this.effectExplicit) {
|
|
1875
1897
|
const block = effectsFromStyleMetadata(this.styleMetadata());
|
|
1876
1898
|
this.effectName = block?.route ?? null;
|
|
@@ -1909,6 +1931,37 @@ var MapMapMap = class {
|
|
|
1909
1931
|
apiKey: this.apiKey
|
|
1910
1932
|
});
|
|
1911
1933
|
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Run `action` as soon as the style can take layer edits, or now if it
|
|
1936
|
+
* already can.
|
|
1937
|
+
*
|
|
1938
|
+
* Never wait on `load` alone: MapLibre fires it exactly once per map, so
|
|
1939
|
+
* anything deferred after the first load is dropped for good - and
|
|
1940
|
+
* `isStyleLoaded()` reports false for the whole of every `style.load`
|
|
1941
|
+
* (the new style's sources are still loading) and whenever a source is
|
|
1942
|
+
* loading afterwards. Waiting for whichever of `style.load` / `idle` /
|
|
1943
|
+
* `load` arrives first covers the first load and every later style swap,
|
|
1944
|
+
* and the listeners detach as soon as one fires (or on `destroy()`).
|
|
1945
|
+
*/
|
|
1946
|
+
whenStyleReady(action) {
|
|
1947
|
+
if (this.destroyed) return;
|
|
1948
|
+
if (this.map.isStyleLoaded()) {
|
|
1949
|
+
action();
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
const events = ["style.load", "idle", "load"];
|
|
1953
|
+
const run = () => {
|
|
1954
|
+
cleanup();
|
|
1955
|
+
if (this.destroyed) return;
|
|
1956
|
+
action();
|
|
1957
|
+
};
|
|
1958
|
+
const cleanup = () => {
|
|
1959
|
+
this.pendingWaits.delete(cleanup);
|
|
1960
|
+
for (const event of events) this.map.off(event, run);
|
|
1961
|
+
};
|
|
1962
|
+
this.pendingWaits.add(cleanup);
|
|
1963
|
+
for (const event of events) this.map.on(event, run);
|
|
1964
|
+
}
|
|
1912
1965
|
/** The current style's `metadata`, if it can be read yet. */
|
|
1913
1966
|
styleMetadata() {
|
|
1914
1967
|
try {
|
|
@@ -1948,10 +2001,10 @@ var MapMapMap = class {
|
|
|
1948
2001
|
setRouteEffect(effect, options = {}) {
|
|
1949
2002
|
this.effectExplicit = true;
|
|
1950
2003
|
const { geometry, ...flowOptions } = options;
|
|
2004
|
+
this.removeEffectLayer();
|
|
1951
2005
|
this.effectName = effect;
|
|
1952
2006
|
this.effectOptions = flowOptions;
|
|
1953
2007
|
if (geometry) this.effectGeometry = geometry;
|
|
1954
|
-
this.removeEffectLayer();
|
|
1955
2008
|
this.applyRouteEffect();
|
|
1956
2009
|
}
|
|
1957
2010
|
/**
|
|
@@ -1984,7 +2037,12 @@ var MapMapMap = class {
|
|
|
1984
2037
|
return;
|
|
1985
2038
|
}
|
|
1986
2039
|
if (!this.map.isStyleLoaded()) {
|
|
1987
|
-
|
|
2040
|
+
if (this.effectWaiting) return;
|
|
2041
|
+
this.effectWaiting = true;
|
|
2042
|
+
this.whenStyleReady(() => {
|
|
2043
|
+
this.effectWaiting = false;
|
|
2044
|
+
this.applyRouteEffect();
|
|
2045
|
+
});
|
|
1988
2046
|
return;
|
|
1989
2047
|
}
|
|
1990
2048
|
if (this.effectLayer && this.map.getLayer(id)) {
|
|
@@ -2013,7 +2071,7 @@ var MapMapMap = class {
|
|
|
2013
2071
|
*/
|
|
2014
2072
|
setBuildings3d(enabled) {
|
|
2015
2073
|
if (!this.map.isStyleLoaded()) {
|
|
2016
|
-
this.
|
|
2074
|
+
this.whenStyleReady(() => this.setBuildings3d(enabled));
|
|
2017
2075
|
return;
|
|
2018
2076
|
}
|
|
2019
2077
|
if (enabled === (this.map.getLayer("building-3d") !== void 0)) return;
|
|
@@ -2036,18 +2094,45 @@ var MapMapMap = class {
|
|
|
2036
2094
|
this.map.setLayerZoomRange("building", minzoom, 24);
|
|
2037
2095
|
}
|
|
2038
2096
|
}
|
|
2039
|
-
/**
|
|
2097
|
+
/**
|
|
2098
|
+
* Resolves once the style and first tiles are loaded.
|
|
2099
|
+
*
|
|
2100
|
+
* Safe to call at any point in the map's life, not just before its
|
|
2101
|
+
* first load: `map.loaded()` goes false again whenever a source is
|
|
2102
|
+
* loading (a theme swap, a pan into new tiles) while `load` only ever
|
|
2103
|
+
* fires once, so this waits on `idle` too rather than hanging.
|
|
2104
|
+
*/
|
|
2040
2105
|
whenReady() {
|
|
2041
2106
|
return new Promise((resolve) => {
|
|
2042
2107
|
if (this.map.loaded()) {
|
|
2043
2108
|
resolve();
|
|
2044
2109
|
return;
|
|
2045
2110
|
}
|
|
2046
|
-
|
|
2111
|
+
const events = ["load", "idle"];
|
|
2112
|
+
const done = () => {
|
|
2113
|
+
for (const event of events) this.map.off(event, done);
|
|
2114
|
+
this.pendingWaits.delete(cleanup);
|
|
2115
|
+
resolve();
|
|
2116
|
+
};
|
|
2117
|
+
const cleanup = () => {
|
|
2118
|
+
for (const event of events) this.map.off(event, done);
|
|
2119
|
+
this.pendingWaits.delete(cleanup);
|
|
2120
|
+
};
|
|
2121
|
+
this.pendingWaits.add(cleanup);
|
|
2122
|
+
for (const event of events) this.map.on(event, done);
|
|
2047
2123
|
});
|
|
2048
2124
|
}
|
|
2049
|
-
/**
|
|
2125
|
+
/**
|
|
2126
|
+
* Remove the map and release its WebGL context. Anything still queued
|
|
2127
|
+
* behind a style load is dropped first, so nothing touches the map
|
|
2128
|
+
* after this returns.
|
|
2129
|
+
*/
|
|
2050
2130
|
destroy() {
|
|
2131
|
+
this.destroyed = true;
|
|
2132
|
+
for (const cleanup of [...this.pendingWaits]) cleanup();
|
|
2133
|
+
this.pendingWaits.clear();
|
|
2134
|
+
this.effectWaiting = false;
|
|
2135
|
+
this.effectLayer = void 0;
|
|
2051
2136
|
this.map.remove();
|
|
2052
2137
|
}
|
|
2053
2138
|
};
|
|
@@ -2153,12 +2238,544 @@ function numberOr(value, fallback) {
|
|
|
2153
2238
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2154
2239
|
}
|
|
2155
2240
|
|
|
2241
|
+
// src/marker-icons.ts
|
|
2242
|
+
var MARKER_GLYPH_IDS = [
|
|
2243
|
+
"pin",
|
|
2244
|
+
"dot",
|
|
2245
|
+
"star",
|
|
2246
|
+
"heart",
|
|
2247
|
+
"flag",
|
|
2248
|
+
"home",
|
|
2249
|
+
"work",
|
|
2250
|
+
"food",
|
|
2251
|
+
"cafe",
|
|
2252
|
+
"bar",
|
|
2253
|
+
"shop",
|
|
2254
|
+
"hotel",
|
|
2255
|
+
"parking",
|
|
2256
|
+
"fuel",
|
|
2257
|
+
"charging",
|
|
2258
|
+
"transit",
|
|
2259
|
+
"bike",
|
|
2260
|
+
"camera",
|
|
2261
|
+
"music",
|
|
2262
|
+
"warning",
|
|
2263
|
+
"info"
|
|
2264
|
+
];
|
|
2265
|
+
function isMarkerGlyphId(value) {
|
|
2266
|
+
return typeof value === "string" && MARKER_GLYPH_IDS.includes(value);
|
|
2267
|
+
}
|
|
2268
|
+
var markerGlyphPaths = {
|
|
2269
|
+
pin: "M7 12a5 5 0 1 1 10 0a5 5 0 1 1-10 0Z",
|
|
2270
|
+
dot: "M4 12a8 8 0 1 1 16 0a8 8 0 1 1-16 0Z",
|
|
2271
|
+
star: "M12 3L14.41 9.483L21.32 9.772L15.899 14.067L17.76 20.728L12 16.9L6.24 20.728L8.101 14.067L2.68 9.772L9.59 9.483Z",
|
|
2272
|
+
heart: "M12 21C6.5 16.5 3 13.2 3 9.5C3 6.5 5.4 4.5 8 4.5C9.7 4.5 11.2 5.4 12 6.8C12.8 5.4 14.3 4.5 16 4.5C18.6 4.5 21 6.5 21 9.5C21 13.2 17.5 16.5 12 21Z",
|
|
2273
|
+
flag: "M5.5 2.5h2v19h-2V2.5Zm2 2H19l-2.8 4l2.8 4H7.5v-8Z",
|
|
2274
|
+
home: "M12 3.2l9 7.6h-2.6v9.7h-4.9v-5.9h-3v5.9H5.6v-9.7H3l9-7.6Z",
|
|
2275
|
+
work: "M9.5 4h5a1.5 1.5 0 0 1 1.5 1.5V7h4a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h4V5.5A1.5 1.5 0 0 1 9.5 4Zm0.5 3h4V5.5h-4V7Z",
|
|
2276
|
+
food: "M6.75 8.25V3.75a0.75 0.75 0 0 1 1.5 0V8.25a0.75 0.75 0 0 1-1.5 0ZM20.25 3.75V21a0.75 0.75 0 0 1-1.5 0V16.5H14.25a0.75 0.75 0 0 1-0.75-0.75 25.195 25.195 0 0 1 0.677-5.333c0.917-3.796 2.655-6.34 5.028-7.357A0.75 0.75 0 0 1 20.25 3.75ZM18.75 5.053c-3.016 2.303-3.607 7.914-3.722 9.947H18.75ZM11.24 3.627a0.75 0.75 0 1 0-1.479 0.247L10.5 8.309a3 3 0 0 1-6 0l0.739-4.435a0.75 0.75 0 1 0-1.479-0.247l-0.75 4.5A0.766 0.766 0 0 0 3 8.25a4.507 4.507 0 0 0 3.75 4.436V21a0.75 0.75 0 0 0 1.5 0V12.686A4.507 4.507 0 0 0 12 8.25a0.766 0.766 0 0 0-0.01-0.123Z",
|
|
2277
|
+
cafe: "M3 6.2h13.4v6.3a5.5 5.5 0 0 1-5.5 5.5H8.5A5.5 5.5 0 0 1 3 12.5V6.2Zm13.4 2h2.4a3.2 3.2 0 0 1 0 6.4h-1.6v-2.2h1.6a1 1 0 0 0 0-2h-2.4V8.2Z",
|
|
2278
|
+
bar: "M3.5 4h17l-7.2 8.2V19h3.7v2H7v-2h3.3v-6.8L3.5 4Z",
|
|
2279
|
+
shop: "M20.25 3.75H3.75A1.5 1.5 0 0 0 2.25 5.25V18.75a1.5 1.5 0 0 0 1.5 1.5H20.25a1.5 1.5 0 0 0 1.5-1.5V5.25A1.5 1.5 0 0 0 20.25 3.75Zm0 15H3.75V5.25H20.25V18.75ZM16.5 8.25a4.5 4.5 0 0 1-9 0 0.75 0.75 0 0 1 1.5 0 3 3 0 0 0 6 0 0.75 0.75 0 0 1 1.5 0Z",
|
|
2280
|
+
hotel: "M20.25 6.75H3V4.5a0.75 0.75 0 0 0-1.5 0V19.5a0.75 0.75 0 0 0 1.5 0V16.5H22.5v3a0.75 0.75 0 0 0 1.5 0V10.5A3.75 3.75 0 0 0 20.25 6.75ZM3 8.25h6.75v6.75H3Zm8.25 6.75V8.25h9a2.25 2.25 0 0 1 2.25 2.25v4.5Z",
|
|
2281
|
+
parking: "M7.5 3.5h6.2a5.4 5.4 0 0 1 0 10.8h-3v6.2H7.5V3.5Zm3.2 3v4.8h3a2.4 2.4 0 0 0 0-4.8h-3Z",
|
|
2282
|
+
fuel: "M4.5 3h9a1 1 0 0 1 1 1v17h-11V4a1 1 0 0 1 1-1Zm2 2.5v3.8h5V5.5h-5ZM15.5 9h1.4l2.7 2.7c0.3 0.3 0.5 0.7 0.5 1.1V17a2.3 2.3 0 0 1-4.6 0V9Z",
|
|
2283
|
+
charging: "M13.4 2L5.2 13.2h4.9L9 22l8.3-11.4h-5L13.4 2Z",
|
|
2284
|
+
transit: "M17.25 2.25H6.75A3 3 0 0 0 3.75 5.25V17.25a3 3 0 0 0 3 3h0.75L6.15 22.05a0.75 0.75 0 1 0 1.2 0.9L9.375 20.25h5.25l2.025 2.7a0.75 0.75 0 1 0 1.2-0.9L16.5 20.25h0.75a3 3 0 0 0 3-3V5.25A3 3 0 0 0 17.25 2.25ZM6.75 3.75H17.25a1.5 1.5 0 0 1 1.5 1.5v6H5.25V5.25A1.5 1.5 0 0 1 6.75 3.75ZM17.25 18.75H6.75a1.5 1.5 0 0 1-1.5-1.5V12.75H18.75v4.5A1.5 1.5 0 0 1 17.25 18.75ZM9 16.125a1.125 1.125 0 1 1-1.125-1.125A1.125 1.125 0 0 1 9 16.125Zm8.25 0a1.125 1.125 0 1 1-1.125-1.125A1.125 1.125 0 0 1 17.25 16.125Z",
|
|
2285
|
+
bike: "M2.3 16.9a3.9 3.9 0 1 1 7.8 0a3.9 3.9 0 1 1-7.8 0ZM3.9 16.9a2.3 2.3 0 1 0 4.6 0a2.3 2.3 0 1 0-4.6 0ZM13.9 16.9a3.9 3.9 0 1 1 7.8 0a3.9 3.9 0 1 1-7.8 0ZM15.5 16.9a2.3 2.3 0 1 0 4.6 0a2.3 2.3 0 1 0-4.6 0ZM6.9 17.2l3-6.7h4.6l2.9 6.7l-1.4 0.6l-2.3-5.3h-2.9l-2.5 5.6l-1.4-0.6Z",
|
|
2286
|
+
camera: "M9.2 4.5h5.6l1.6 2.2H20a1.6 1.6 0 0 1 1.6 1.6v9.6a1.6 1.6 0 0 1-1.6 1.6H4a1.6 1.6 0 0 1-1.6-1.6V8.3A1.6 1.6 0 0 1 4 6.7h3.6L9.2 4.5ZM8.1 12.9a3.9 3.9 0 1 0 7.8 0a3.9 3.9 0 1 0-7.8 0ZM9.9 12.9a2.1 2.1 0 1 1 4.2 0a2.1 2.1 0 1 1-4.2 0Z",
|
|
2287
|
+
music: "M4.9 18.2a2.7 2.7 0 1 1 5.4 0a2.7 2.7 0 1 1-5.4 0ZM14.1 16.4a2.7 2.7 0 1 1 5.4 0a2.7 2.7 0 1 1-5.4 0ZM8.5 7h1.8v11.2H8.5ZM17.7 5.2h1.8v11.2h-1.8ZM8.5 7l11-1.8v3l-11 1.8V7Z",
|
|
2288
|
+
warning: "M12 2.8L22.3 20.5H1.7L12 2.8Zm-1 6.6v5.4h2V9.4h-2Zm0 7.2v2.2h2v-2.2h-2Z",
|
|
2289
|
+
info: "M2.5 12a9.5 9.5 0 1 1 19 0a9.5 9.5 0 1 1-19 0ZM11 6.4v2.4h2V6.4h-2Zm0 4.2v6.9h2v-6.9h-2Z"
|
|
2290
|
+
};
|
|
2291
|
+
var MARKER_PIN_BODY_PATH = "M12 24C7 17.2 3 13.2 3 9.4a9 9 0 0 1 18 0C21 13.2 17 17.2 12 24Z";
|
|
2292
|
+
var MARKER_GLYPH_VIEWBOX = 24;
|
|
2293
|
+
|
|
2294
|
+
// src/markers.ts
|
|
2295
|
+
var MARKER_SIZES = ["s", "m", "l"];
|
|
2296
|
+
var MARKER_SIZES_PX = {
|
|
2297
|
+
s: 24,
|
|
2298
|
+
m: 32,
|
|
2299
|
+
l: 40
|
|
2300
|
+
};
|
|
2301
|
+
var MARKER_LABEL_TEXT_SIZES = {
|
|
2302
|
+
s: 12,
|
|
2303
|
+
m: 13,
|
|
2304
|
+
l: 14
|
|
2305
|
+
};
|
|
2306
|
+
var MARKER_DEFAULT_COLOUR = "#1a6bff";
|
|
2307
|
+
var MAX_MARKER_IMAGE_BYTES = 64 * 1024;
|
|
2308
|
+
var MAX_MARKER_ITEMS = 200;
|
|
2309
|
+
var MAX_MARKER_ID_LENGTH = 64;
|
|
2310
|
+
var MAX_MARKER_LABEL_LENGTH = 120;
|
|
2311
|
+
var MAX_MARKER_TEXT_LENGTH = 3;
|
|
2312
|
+
var MARKERS_ID = "mm-user-markers";
|
|
2313
|
+
var MARKER_IMAGE_MIME = /^data:image\/(png|jpeg|webp|svg\+xml);base64,/;
|
|
2314
|
+
function isRecord3(v) {
|
|
2315
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2316
|
+
}
|
|
2317
|
+
function boundedNumber(v, min, max) {
|
|
2318
|
+
return typeof v === "number" && Number.isFinite(v) && v >= min && v <= max ? v : void 0;
|
|
2319
|
+
}
|
|
2320
|
+
function markerColour(v) {
|
|
2321
|
+
return typeof v === "string" && /^#[0-9a-fA-F]{6}$/.test(v.trim()) ? v.trim().toLowerCase() : MARKER_DEFAULT_COLOUR;
|
|
2322
|
+
}
|
|
2323
|
+
function dataUriBytes(uri) {
|
|
2324
|
+
const comma = uri.indexOf(",");
|
|
2325
|
+
const payload = comma === -1 ? "" : uri.slice(comma + 1);
|
|
2326
|
+
const meta = comma === -1 ? uri : uri.slice(0, comma);
|
|
2327
|
+
if (!/;base64$/i.test(meta) && !/;base64;/i.test(meta)) return payload.length;
|
|
2328
|
+
return Math.floor(payload.replace(/=+$/, "").length * 3 / 4);
|
|
2329
|
+
}
|
|
2330
|
+
function truncateChars(text, max) {
|
|
2331
|
+
const chars = Array.from(text);
|
|
2332
|
+
return chars.length <= max ? text : chars.slice(0, max).join("");
|
|
2333
|
+
}
|
|
2334
|
+
function markerImage(v) {
|
|
2335
|
+
if (typeof v !== "string") return void 0;
|
|
2336
|
+
const uri = v.trim();
|
|
2337
|
+
if (!MARKER_IMAGE_MIME.test(uri)) return void 0;
|
|
2338
|
+
if (dataUriBytes(uri) > MAX_MARKER_IMAGE_BYTES) return void 0;
|
|
2339
|
+
return uri;
|
|
2340
|
+
}
|
|
2341
|
+
function parseMarkers(value) {
|
|
2342
|
+
const block = isRecord3(value) ? value : {};
|
|
2343
|
+
const items = [];
|
|
2344
|
+
if (block.version !== 1) return { version: 1, items };
|
|
2345
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2346
|
+
const rawItems = Array.isArray(block.items) ? block.items : [];
|
|
2347
|
+
for (const raw of rawItems) {
|
|
2348
|
+
if (items.length >= MAX_MARKER_ITEMS) break;
|
|
2349
|
+
if (!isRecord3(raw)) continue;
|
|
2350
|
+
const id = typeof raw.id === "string" ? raw.id : void 0;
|
|
2351
|
+
if (!id || id.length === 0 || Array.from(id).length > MAX_MARKER_ID_LENGTH) continue;
|
|
2352
|
+
if (seen.has(id)) continue;
|
|
2353
|
+
const lng = boundedNumber(raw.lng, -180, 180);
|
|
2354
|
+
const lat = boundedNumber(raw.lat, -90, 90);
|
|
2355
|
+
if (lng === void 0 || lat === void 0) continue;
|
|
2356
|
+
seen.add(id);
|
|
2357
|
+
const item = {
|
|
2358
|
+
id,
|
|
2359
|
+
lng,
|
|
2360
|
+
lat,
|
|
2361
|
+
icon: isMarkerGlyphId(raw.icon) ? raw.icon : "pin",
|
|
2362
|
+
colour: markerColour(raw.colour),
|
|
2363
|
+
size: raw.size === "s" || raw.size === "l" ? raw.size : "m"
|
|
2364
|
+
};
|
|
2365
|
+
const image = markerImage(raw.image);
|
|
2366
|
+
if (image !== void 0) item.image = image;
|
|
2367
|
+
if (typeof raw.label === "string" && raw.label.length > 0) {
|
|
2368
|
+
item.label = truncateChars(raw.label, MAX_MARKER_LABEL_LENGTH);
|
|
2369
|
+
}
|
|
2370
|
+
items.push(item);
|
|
2371
|
+
}
|
|
2372
|
+
return { version: 1, items };
|
|
2373
|
+
}
|
|
2374
|
+
function markersFromTheme(theme) {
|
|
2375
|
+
const extra = theme?.extra;
|
|
2376
|
+
if (typeof extra !== "object" || extra === null || !("markers" in extra)) {
|
|
2377
|
+
return void 0;
|
|
2378
|
+
}
|
|
2379
|
+
return parseMarkers(extra.markers);
|
|
2380
|
+
}
|
|
2381
|
+
async function markersFromThemeUrl(url, fetchImpl) {
|
|
2382
|
+
const doFetch = fetchImpl ?? ((input) => globalThis.fetch(input));
|
|
2383
|
+
const response = await doFetch(url);
|
|
2384
|
+
if (!response.ok) {
|
|
2385
|
+
throw new Error(`fetching theme ${url}: HTTP ${response.status}`);
|
|
2386
|
+
}
|
|
2387
|
+
const theme = await response.json();
|
|
2388
|
+
return markersFromTheme(theme);
|
|
2389
|
+
}
|
|
2390
|
+
function markerImageKey(icon, colour2, size) {
|
|
2391
|
+
return `mm-marker-${icon}-${colour2.toLowerCase()}-${size}`;
|
|
2392
|
+
}
|
|
2393
|
+
function markerText(value) {
|
|
2394
|
+
if (typeof value !== "string") return void 0;
|
|
2395
|
+
const text = truncateChars(value.trim(), MAX_MARKER_TEXT_LENGTH);
|
|
2396
|
+
return text.length > 0 ? text : void 0;
|
|
2397
|
+
}
|
|
2398
|
+
function markerTextImageKey(text, colour2, size) {
|
|
2399
|
+
return `mm-marker-text-${text}-${colour2.toLowerCase()}-${size}`;
|
|
2400
|
+
}
|
|
2401
|
+
function fnv1a(text) {
|
|
2402
|
+
let hash = 2166136261;
|
|
2403
|
+
for (let i = 0; i < text.length; i++) {
|
|
2404
|
+
hash ^= text.charCodeAt(i);
|
|
2405
|
+
hash = Math.imul(hash, 16777619);
|
|
2406
|
+
}
|
|
2407
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
2408
|
+
}
|
|
2409
|
+
function markerImageDataKey(dataUri) {
|
|
2410
|
+
return `mm-marker-image-${fnv1a(dataUri)}`;
|
|
2411
|
+
}
|
|
2412
|
+
var GLYPH_FRACTION = 0.62;
|
|
2413
|
+
var PIN_HEAD_CENTRE_Y = 9.4;
|
|
2414
|
+
var PIN_HEAD_RADIUS = 9;
|
|
2415
|
+
var DOT_RADIUS = 8;
|
|
2416
|
+
var DOT_STROKE = 2;
|
|
2417
|
+
var PIN_TEXT_FONT_STACK = "system-ui, sans-serif";
|
|
2418
|
+
function pinTextStartSize(length) {
|
|
2419
|
+
if (length <= 1) return 10;
|
|
2420
|
+
return length === 2 ? 8.2 : 6.6;
|
|
2421
|
+
}
|
|
2422
|
+
var PIN_TEXT_MIN_SIZE = 4.5;
|
|
2423
|
+
var PIN_TEXT_WIDTH_FRACTION = 0.78;
|
|
2424
|
+
function renderMarkerImage(options = {}) {
|
|
2425
|
+
const text = markerText(options.text);
|
|
2426
|
+
const icon = options.icon ?? "pin";
|
|
2427
|
+
const colour2 = options.colour ?? MARKER_DEFAULT_COLOUR;
|
|
2428
|
+
const sizePx = MARKER_SIZES_PX[options.size ?? "m"];
|
|
2429
|
+
const g = globalThis;
|
|
2430
|
+
if (!g.document || !g.Path2D) return null;
|
|
2431
|
+
const side = sizePx * 2;
|
|
2432
|
+
const canvas = g.document.createElement("canvas");
|
|
2433
|
+
canvas.width = side;
|
|
2434
|
+
canvas.height = side;
|
|
2435
|
+
const ctx = canvas.getContext("2d");
|
|
2436
|
+
if (!ctx) return null;
|
|
2437
|
+
const scale = side / MARKER_GLYPH_VIEWBOX;
|
|
2438
|
+
if (icon === "dot" && text === void 0) {
|
|
2439
|
+
const cx = side / 2;
|
|
2440
|
+
const cy = side / 2;
|
|
2441
|
+
const r = DOT_RADIUS * scale;
|
|
2442
|
+
ctx.beginPath();
|
|
2443
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
2444
|
+
ctx.fillStyle = colour2;
|
|
2445
|
+
ctx.fill();
|
|
2446
|
+
ctx.lineWidth = DOT_STROKE * scale;
|
|
2447
|
+
ctx.strokeStyle = "#ffffff";
|
|
2448
|
+
ctx.stroke();
|
|
2449
|
+
return { image: ctx.getImageData(0, 0, side, side), pixelRatio: 2 };
|
|
2450
|
+
}
|
|
2451
|
+
ctx.save();
|
|
2452
|
+
ctx.scale(scale, scale);
|
|
2453
|
+
ctx.fillStyle = colour2;
|
|
2454
|
+
ctx.fill(new g.Path2D(MARKER_PIN_BODY_PATH));
|
|
2455
|
+
ctx.restore();
|
|
2456
|
+
if (text !== void 0) {
|
|
2457
|
+
const maxWidth = PIN_HEAD_RADIUS * 2 * PIN_TEXT_WIDTH_FRACTION * scale;
|
|
2458
|
+
let unitSize = pinTextStartSize(text.length);
|
|
2459
|
+
ctx.textAlign = "center";
|
|
2460
|
+
ctx.textBaseline = "middle";
|
|
2461
|
+
ctx.fillStyle = "#ffffff";
|
|
2462
|
+
const canMeasure = typeof ctx.measureText === "function";
|
|
2463
|
+
for (; ; ) {
|
|
2464
|
+
ctx.font = `bold ${unitSize * scale}px ${PIN_TEXT_FONT_STACK}`;
|
|
2465
|
+
if (!canMeasure || unitSize <= PIN_TEXT_MIN_SIZE) break;
|
|
2466
|
+
if (ctx.measureText(text).width <= maxWidth) break;
|
|
2467
|
+
unitSize -= 0.5;
|
|
2468
|
+
}
|
|
2469
|
+
ctx.fillText(text, side / 2, PIN_HEAD_CENTRE_Y * scale);
|
|
2470
|
+
return { image: ctx.getImageData(0, 0, side, side), pixelRatio: 2 };
|
|
2471
|
+
}
|
|
2472
|
+
const glyphSide = PIN_HEAD_RADIUS * 2 * GLYPH_FRACTION * scale;
|
|
2473
|
+
const glyphScale = glyphSide / MARKER_GLYPH_VIEWBOX;
|
|
2474
|
+
ctx.save();
|
|
2475
|
+
ctx.translate(
|
|
2476
|
+
side / 2 - glyphSide / 2,
|
|
2477
|
+
PIN_HEAD_CENTRE_Y * scale - glyphSide / 2
|
|
2478
|
+
);
|
|
2479
|
+
ctx.scale(glyphScale, glyphScale);
|
|
2480
|
+
ctx.fillStyle = "#ffffff";
|
|
2481
|
+
ctx.fill(new g.Path2D(markerGlyphPaths[icon]));
|
|
2482
|
+
ctx.restore();
|
|
2483
|
+
return { image: ctx.getImageData(0, 0, side, side), pixelRatio: 2 };
|
|
2484
|
+
}
|
|
2485
|
+
var TEXT_OFFSET_EXPRESSION = [
|
|
2486
|
+
"match",
|
|
2487
|
+
["get", "offsetKey"],
|
|
2488
|
+
"tip",
|
|
2489
|
+
["literal", [0, 0.4]],
|
|
2490
|
+
"cs",
|
|
2491
|
+
["literal", [0, 1.3]],
|
|
2492
|
+
"cm",
|
|
2493
|
+
["literal", [0, 1.55]],
|
|
2494
|
+
"cl",
|
|
2495
|
+
["literal", [0, 1.75]],
|
|
2496
|
+
["literal", [0, 0.4]]
|
|
2497
|
+
];
|
|
2498
|
+
var MARKER_TEXT_FONT = "MapMap Sans Regular";
|
|
2499
|
+
var MarkersLayer = class {
|
|
2500
|
+
constructor(map, markers, options = {}) {
|
|
2501
|
+
this.items = [];
|
|
2502
|
+
this.destroyed = false;
|
|
2503
|
+
/**
|
|
2504
|
+
* Bumped by {@link clear} (and {@link destroy}). An image load started
|
|
2505
|
+
* before the bump resolves into a cleared layer, so its handler compares
|
|
2506
|
+
* the generation it was started in and drops out — without this a
|
|
2507
|
+
* pending `loadImage` would rebuild and RE-INSTALL the layer on a map the
|
|
2508
|
+
* caller had already cleaned up.
|
|
2509
|
+
*/
|
|
2510
|
+
this.generation = 0;
|
|
2511
|
+
/** Custom images by cache key, once loaded (kept across style swaps). */
|
|
2512
|
+
this.loadedImages = /* @__PURE__ */ new Map();
|
|
2513
|
+
/** Custom-image cache keys that failed to load (pin fallback, once). */
|
|
2514
|
+
this.failedImages = /* @__PURE__ */ new Set();
|
|
2515
|
+
/** Custom-image cache keys with a load in flight. */
|
|
2516
|
+
this.pendingImages = /* @__PURE__ */ new Set();
|
|
2517
|
+
/**
|
|
2518
|
+
* Every `map.addImage` key this layer currently owns, so a rebuild can
|
|
2519
|
+
* drop the ones nothing references any more and teardown removes exactly
|
|
2520
|
+
* what it added (the RouteLayer endpoints pattern). Without it every
|
|
2521
|
+
* icon/colour/size change leaked a 2x RGBA image — 25 KB per click at
|
|
2522
|
+
* the 40px preset — and re-invalidated the sprite atlas.
|
|
2523
|
+
*/
|
|
2524
|
+
this.registeredImageKeys = /* @__PURE__ */ new Set();
|
|
2525
|
+
this.handleStyleLoad = () => {
|
|
2526
|
+
if (this.data) this.install();
|
|
2527
|
+
};
|
|
2528
|
+
this.map = map instanceof MapMapMap ? map.map : map;
|
|
2529
|
+
this.options = options;
|
|
2530
|
+
this.map.on("style.load", this.handleStyleLoad);
|
|
2531
|
+
if (markers) this.setMarkers(markers);
|
|
2532
|
+
}
|
|
2533
|
+
/**
|
|
2534
|
+
* Replace the layer's markers with a parsed block (`undefined` or an
|
|
2535
|
+
* empty block clears the map of markers but keeps the layer alive).
|
|
2536
|
+
* Custom images load asynchronously: their markers render as default
|
|
2537
|
+
* pins until the image arrives, then swap in place; a failed or
|
|
2538
|
+
* oversized image keeps the pin fallback.
|
|
2539
|
+
*/
|
|
2540
|
+
setMarkers(markers) {
|
|
2541
|
+
this.items = markers?.items ?? [];
|
|
2542
|
+
this.rebuild();
|
|
2543
|
+
}
|
|
2544
|
+
/** The layer's current markers. */
|
|
2545
|
+
get current() {
|
|
2546
|
+
return this.items;
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* The MapLibre source/layer ids - public API for escape-hatch styling.
|
|
2550
|
+
* Both are the contract id `mm-user-markers`.
|
|
2551
|
+
*/
|
|
2552
|
+
get ids() {
|
|
2553
|
+
return { source: MARKERS_ID, layer: MARKERS_ID };
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Rebuild the source data from the items and (re)install. Mirrors
|
|
2557
|
+
* PlacesLayer's never-drop-an-update rule: once the source exists the
|
|
2558
|
+
* data is applied immediately (even while `isStyleLoaded()` is
|
|
2559
|
+
* transiently false mid-render); before first installation the latest
|
|
2560
|
+
* data is stashed for the next `style.load`.
|
|
2561
|
+
*/
|
|
2562
|
+
rebuild() {
|
|
2563
|
+
this.data = {
|
|
2564
|
+
type: "FeatureCollection",
|
|
2565
|
+
features: this.items.map((item) => this.featureFor(item))
|
|
2566
|
+
};
|
|
2567
|
+
const source = this.map.getSource(MARKERS_ID);
|
|
2568
|
+
if (source) {
|
|
2569
|
+
this.registerImages();
|
|
2570
|
+
source.setData(this.data);
|
|
2571
|
+
} else if (this.map.isStyleLoaded()) {
|
|
2572
|
+
this.install();
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
/** Build one feature, kicking off the custom-image load if needed. */
|
|
2576
|
+
featureFor(item) {
|
|
2577
|
+
const sizePx = MARKER_SIZES_PX[item.size];
|
|
2578
|
+
const textSize = MARKER_LABEL_TEXT_SIZES[item.size];
|
|
2579
|
+
let iconImage;
|
|
2580
|
+
let iconSize;
|
|
2581
|
+
let anchor;
|
|
2582
|
+
const imageKey = item.image !== void 0 ? markerImageDataKey(item.image) : void 0;
|
|
2583
|
+
const loaded = imageKey !== void 0 ? this.loadedImages.get(imageKey) : void 0;
|
|
2584
|
+
if (item.image !== void 0 && imageKey !== void 0 && !this.failedImages.has(imageKey)) {
|
|
2585
|
+
if (loaded) {
|
|
2586
|
+
iconImage = imageKey;
|
|
2587
|
+
iconSize = loaded.data.width > 0 ? sizePx / loaded.data.width : 1;
|
|
2588
|
+
anchor = "center";
|
|
2589
|
+
} else {
|
|
2590
|
+
this.loadCustomImage(imageKey, item.image);
|
|
2591
|
+
iconImage = markerImageKey(item.icon, item.colour, item.size);
|
|
2592
|
+
iconSize = 1;
|
|
2593
|
+
anchor = item.icon === "dot" ? "center" : "bottom";
|
|
2594
|
+
}
|
|
2595
|
+
} else {
|
|
2596
|
+
iconImage = markerImageKey(item.icon, item.colour, item.size);
|
|
2597
|
+
iconSize = 1;
|
|
2598
|
+
anchor = item.icon === "dot" ? "center" : "bottom";
|
|
2599
|
+
}
|
|
2600
|
+
const properties = {
|
|
2601
|
+
id: item.id,
|
|
2602
|
+
iconImage,
|
|
2603
|
+
iconSize,
|
|
2604
|
+
iconAnchor: anchor,
|
|
2605
|
+
textSize,
|
|
2606
|
+
offsetKey: anchor === "bottom" ? "tip" : `c${item.size}`
|
|
2607
|
+
};
|
|
2608
|
+
if (item.label !== void 0) properties["label"] = item.label;
|
|
2609
|
+
return {
|
|
2610
|
+
type: "Feature",
|
|
2611
|
+
geometry: { type: "Point", coordinates: [item.lng, item.lat] },
|
|
2612
|
+
properties
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
/** Load a custom `data:` image once; rebuild the features when done. */
|
|
2616
|
+
loadCustomImage(key, uri) {
|
|
2617
|
+
if (this.pendingImages.has(key)) return;
|
|
2618
|
+
this.pendingImages.add(key);
|
|
2619
|
+
const generation = this.generation;
|
|
2620
|
+
const stale = () => this.destroyed || generation !== this.generation;
|
|
2621
|
+
void this.map.loadImage(uri).then((image) => {
|
|
2622
|
+
this.pendingImages.delete(key);
|
|
2623
|
+
if (stale()) return;
|
|
2624
|
+
this.loadedImages.set(key, {
|
|
2625
|
+
data: image.data,
|
|
2626
|
+
raw: image.data
|
|
2627
|
+
});
|
|
2628
|
+
this.rebuild();
|
|
2629
|
+
}).catch((error) => {
|
|
2630
|
+
this.pendingImages.delete(key);
|
|
2631
|
+
if (stale()) return;
|
|
2632
|
+
this.failedImages.add(key);
|
|
2633
|
+
this.options.onImageError?.(uri, error);
|
|
2634
|
+
this.rebuild();
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Register every image the current features reference and drop the ones
|
|
2639
|
+
* they no longer do (idempotent). Runtime images survive a diffed
|
|
2640
|
+
* `setStyle` and are rebuilt after a full reload, so this runs on every
|
|
2641
|
+
* install as well as every data rebuild.
|
|
2642
|
+
*/
|
|
2643
|
+
registerImages() {
|
|
2644
|
+
const needed = /* @__PURE__ */ new Set();
|
|
2645
|
+
for (const item of this.items) {
|
|
2646
|
+
const imageKey = item.image !== void 0 ? markerImageDataKey(item.image) : void 0;
|
|
2647
|
+
const loaded = imageKey !== void 0 ? this.loadedImages.get(imageKey) : void 0;
|
|
2648
|
+
if (imageKey !== void 0 && loaded && !this.failedImages.has(imageKey)) {
|
|
2649
|
+
needed.add(imageKey);
|
|
2650
|
+
if (!this.map.hasImage(imageKey)) {
|
|
2651
|
+
this.map.addImage(imageKey, loaded.raw);
|
|
2652
|
+
}
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2655
|
+
const key = markerImageKey(item.icon, item.colour, item.size);
|
|
2656
|
+
needed.add(key);
|
|
2657
|
+
if (this.map.hasImage(key)) continue;
|
|
2658
|
+
const rendered = renderMarkerImage({
|
|
2659
|
+
icon: item.icon,
|
|
2660
|
+
colour: item.colour,
|
|
2661
|
+
size: item.size
|
|
2662
|
+
});
|
|
2663
|
+
if (rendered) {
|
|
2664
|
+
this.map.addImage(key, rendered.image, { pixelRatio: rendered.pixelRatio });
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
for (const key of this.registeredImageKeys) {
|
|
2668
|
+
if (needed.has(key)) continue;
|
|
2669
|
+
if (this.map.hasImage(key)) this.map.removeImage(key);
|
|
2670
|
+
this.registeredImageKeys.delete(key);
|
|
2671
|
+
}
|
|
2672
|
+
for (const key of needed) this.registeredImageKeys.add(key);
|
|
2673
|
+
}
|
|
2674
|
+
/** Add-or-update the images, source and layer on the current style. */
|
|
2675
|
+
install() {
|
|
2676
|
+
if (!this.data) return;
|
|
2677
|
+
this.registerImages();
|
|
2678
|
+
const existing = this.map.getSource(MARKERS_ID);
|
|
2679
|
+
if (existing) {
|
|
2680
|
+
existing.setData(this.data);
|
|
2681
|
+
} else {
|
|
2682
|
+
this.map.addSource(MARKERS_ID, { type: "geojson", data: this.data });
|
|
2683
|
+
}
|
|
2684
|
+
if (!this.map.getLayer(MARKERS_ID)) {
|
|
2685
|
+
const layer = {
|
|
2686
|
+
id: MARKERS_ID,
|
|
2687
|
+
type: "symbol",
|
|
2688
|
+
source: MARKERS_ID,
|
|
2689
|
+
layout: {
|
|
2690
|
+
"icon-image": ["get", "iconImage"],
|
|
2691
|
+
"icon-size": ["get", "iconSize"],
|
|
2692
|
+
"icon-anchor": ["get", "iconAnchor"],
|
|
2693
|
+
"icon-allow-overlap": true,
|
|
2694
|
+
"text-field": ["get", "label"],
|
|
2695
|
+
"text-font": [MARKER_TEXT_FONT],
|
|
2696
|
+
"text-size": ["get", "textSize"],
|
|
2697
|
+
"text-anchor": "top",
|
|
2698
|
+
"text-offset": TEXT_OFFSET_EXPRESSION,
|
|
2699
|
+
"text-optional": true
|
|
2700
|
+
},
|
|
2701
|
+
paint: {
|
|
2702
|
+
"text-color": "#333333",
|
|
2703
|
+
"text-halo-color": "#ffffff",
|
|
2704
|
+
"text-halo-width": 1.5
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
this.map.addLayer(layer);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Remove the markers' layer, source and registered images from the map.
|
|
2712
|
+
* Images are removed by the keys this layer REGISTERED, not by whatever
|
|
2713
|
+
* `items` happens to hold now — deriving them from the items meant
|
|
2714
|
+
* `setMarkers(undefined)` followed by `clear()`/`destroy()` removed
|
|
2715
|
+
* nothing at all.
|
|
2716
|
+
*/
|
|
2717
|
+
clear() {
|
|
2718
|
+
if (this.map.getLayer(MARKERS_ID)) this.map.removeLayer(MARKERS_ID);
|
|
2719
|
+
if (this.map.getSource(MARKERS_ID)) this.map.removeSource(MARKERS_ID);
|
|
2720
|
+
for (const key of this.registeredImageKeys) {
|
|
2721
|
+
if (this.map.hasImage(key)) this.map.removeImage(key);
|
|
2722
|
+
}
|
|
2723
|
+
this.registeredImageKeys.clear();
|
|
2724
|
+
this.items = [];
|
|
2725
|
+
this.data = void 0;
|
|
2726
|
+
this.loadedImages.clear();
|
|
2727
|
+
this.failedImages.clear();
|
|
2728
|
+
this.pendingImages.clear();
|
|
2729
|
+
this.generation++;
|
|
2730
|
+
}
|
|
2731
|
+
/**
|
|
2732
|
+
* Remove the markers and detach the style.load listener. Call when
|
|
2733
|
+
* disposing of the layer (the map is left untouched). Any image load
|
|
2734
|
+
* still in flight resolves into a no-op.
|
|
2735
|
+
*/
|
|
2736
|
+
destroy() {
|
|
2737
|
+
this.destroyed = true;
|
|
2738
|
+
this.map.off("style.load", this.handleStyleLoad);
|
|
2739
|
+
this.clear();
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
|
|
2156
2743
|
// src/route.ts
|
|
2157
2744
|
var SIGNAL_BLUE = "#3a86ff";
|
|
2158
2745
|
var CASING_COLOR = "#1f438a";
|
|
2159
2746
|
var CORRIDOR_COLOR = "#e05c6c";
|
|
2160
2747
|
var CORRIDOR_OPACITY = 0.55;
|
|
2748
|
+
var ENDPOINT_START_COLOUR = "#3ecf8e";
|
|
2749
|
+
var DEFAULT_START = {
|
|
2750
|
+
icon: "dot",
|
|
2751
|
+
colour: ENDPOINT_START_COLOUR,
|
|
2752
|
+
size: "m"
|
|
2753
|
+
};
|
|
2754
|
+
var DEFAULT_END = {
|
|
2755
|
+
icon: "pin",
|
|
2756
|
+
colour: SIGNAL_BLUE,
|
|
2757
|
+
size: "m"
|
|
2758
|
+
};
|
|
2759
|
+
var DEFAULT_WAYPOINT = {
|
|
2760
|
+
icon: "pin",
|
|
2761
|
+
colour: SIGNAL_BLUE,
|
|
2762
|
+
size: "s"
|
|
2763
|
+
};
|
|
2764
|
+
function resolveEndpoints(option) {
|
|
2765
|
+
if (option === void 0 || option === false) return void 0;
|
|
2766
|
+
const design = option === true ? {} : option;
|
|
2767
|
+
const merge = (value, fallback) => value === false ? false : { ...fallback, ...value };
|
|
2768
|
+
return {
|
|
2769
|
+
start: merge(design.start, DEFAULT_START),
|
|
2770
|
+
end: merge(design.end, DEFAULT_END),
|
|
2771
|
+
waypoint: merge(design.waypoint, DEFAULT_WAYPOINT),
|
|
2772
|
+
numberWaypoints: design.numberWaypoints ?? true
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2161
2775
|
var PROGRESS_COLOR = "#b0b0b0";
|
|
2776
|
+
var ALT_CASING_COLOR = "#5a6b84";
|
|
2777
|
+
var ALT_LINE_COLOR = "#aebdd2";
|
|
2778
|
+
var FERRY_COLOR = "#eaf1fb";
|
|
2162
2779
|
function arrowImage() {
|
|
2163
2780
|
const size = 24;
|
|
2164
2781
|
const data = new Uint8Array(size * size * 4);
|
|
@@ -2184,15 +2801,81 @@ function arrowImage() {
|
|
|
2184
2801
|
}
|
|
2185
2802
|
return { width: size, height: size, data };
|
|
2186
2803
|
}
|
|
2804
|
+
var ENDPOINT_TEXT_FONT = "MapMap Sans Regular";
|
|
2805
|
+
var ENDPOINT_TEXT_OFFSET = [
|
|
2806
|
+
"match",
|
|
2807
|
+
["get", "offsetKey"],
|
|
2808
|
+
"tip",
|
|
2809
|
+
["literal", [0, 0.4]],
|
|
2810
|
+
"cs",
|
|
2811
|
+
["literal", [0, 1.3]],
|
|
2812
|
+
"cm",
|
|
2813
|
+
["literal", [0, 1.55]],
|
|
2814
|
+
"cl",
|
|
2815
|
+
["literal", [0, 1.75]],
|
|
2816
|
+
["literal", [0, 0.4]]
|
|
2817
|
+
];
|
|
2818
|
+
function endpointRender(marker, coordinates, role, numbered) {
|
|
2819
|
+
const size = marker.size ?? "m";
|
|
2820
|
+
const icon = marker.icon ?? "pin";
|
|
2821
|
+
const colour2 = marker.colour ?? SIGNAL_BLUE;
|
|
2822
|
+
const text = markerText(marker.text) ?? markerText(numbered);
|
|
2823
|
+
const key = text !== void 0 ? markerTextImageKey(text, colour2, size) : markerImageKey(icon, colour2, size);
|
|
2824
|
+
const anchor = icon === "dot" && text === void 0 ? "center" : "bottom";
|
|
2825
|
+
const properties = {
|
|
2826
|
+
role,
|
|
2827
|
+
iconImage: key,
|
|
2828
|
+
iconAnchor: anchor,
|
|
2829
|
+
textSize: MARKER_LABEL_TEXT_SIZES[size],
|
|
2830
|
+
offsetKey: anchor === "bottom" ? "tip" : `c${size}`
|
|
2831
|
+
};
|
|
2832
|
+
if (marker.label !== void 0) properties["label"] = marker.label;
|
|
2833
|
+
return {
|
|
2834
|
+
feature: {
|
|
2835
|
+
type: "Feature",
|
|
2836
|
+
geometry: { type: "Point", coordinates },
|
|
2837
|
+
properties
|
|
2838
|
+
},
|
|
2839
|
+
key,
|
|
2840
|
+
render: text !== void 0 ? { text, colour: colour2, size } : { icon, colour: colour2, size }
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2187
2843
|
var RouteLayer = class {
|
|
2188
2844
|
constructor(map, options = {}) {
|
|
2845
|
+
/** Intermediate stops for the endpoint markers, `[lng, lat]` each. */
|
|
2846
|
+
this.waypoints = [];
|
|
2847
|
+
/** Marker images this layer registered, so teardown can remove them. */
|
|
2848
|
+
this.endpointImageKeys = /* @__PURE__ */ new Set();
|
|
2849
|
+
/** Unselected alternatives, drawn beneath the selected route. */
|
|
2850
|
+
this.alternatives = [];
|
|
2851
|
+
/** Ferry legs of the selected route, drawn as dashes over its line. */
|
|
2852
|
+
this.ferrySegments = [];
|
|
2189
2853
|
this.progress = 0;
|
|
2190
2854
|
this.corridors = [];
|
|
2855
|
+
/**
|
|
2856
|
+
* Monotonic id of the newest route request. A response only draws when it
|
|
2857
|
+
* still owns this value - see {@link RouteLayer.routePath}.
|
|
2858
|
+
*/
|
|
2859
|
+
this.requestSeq = 0;
|
|
2860
|
+
/** Whether the alternative-line click listener is currently attached. */
|
|
2861
|
+
this.altClickBound = false;
|
|
2191
2862
|
this.handleStyleLoad = () => {
|
|
2863
|
+
this.installAlternatives();
|
|
2192
2864
|
if (this.lastRoute) this.install(this.lastRoute);
|
|
2193
2865
|
else this.installManeuver();
|
|
2194
2866
|
this.installCorridors();
|
|
2867
|
+
this.installFerry();
|
|
2868
|
+
};
|
|
2869
|
+
this.handleAlternativeClick = (event) => {
|
|
2870
|
+
if (!this.selectHandler) return;
|
|
2871
|
+
const raw = event.features?.[0]?.properties?.["index"];
|
|
2872
|
+
const position = typeof raw === "number" ? raw : Number(raw);
|
|
2873
|
+
if (!Number.isInteger(position)) return;
|
|
2874
|
+
const original = this.altOriginalIndices[position];
|
|
2875
|
+
if (original !== void 0) this.selectHandler(original);
|
|
2195
2876
|
};
|
|
2877
|
+
/** Original array position of each drawn alternative, by its own index. */
|
|
2878
|
+
this.altOriginalIndices = [];
|
|
2196
2879
|
if (map instanceof MapMapMap) {
|
|
2197
2880
|
this.map = map.map;
|
|
2198
2881
|
this.owner = map;
|
|
@@ -2219,9 +2902,38 @@ var RouteLayer = class {
|
|
|
2219
2902
|
this.arrowImageId = `${id}-arrow`;
|
|
2220
2903
|
this.corridorSourceId = `${id}-corridor-src`;
|
|
2221
2904
|
this.corridorLayerId = `${id}-corridor`;
|
|
2905
|
+
this.altSourceId = `${id}-alt-src`;
|
|
2906
|
+
this.altCasingLayerId = `${id}-alt-casing`;
|
|
2907
|
+
this.altLineLayerId = `${id}-alt-line`;
|
|
2908
|
+
this.ferrySourceId = `${id}-ferry-src`;
|
|
2909
|
+
this.ferryLayerId = `${id}-ferry`;
|
|
2910
|
+
this.endpointsSourceId = `${id}-endpoints`;
|
|
2911
|
+
this.endpointsLayerId = `${id}-endpoints`;
|
|
2912
|
+
this.endpoints = resolveEndpoints(options.endpoints);
|
|
2222
2913
|
this.progressColor = options.progressColor ?? PROGRESS_COLOR;
|
|
2914
|
+
this.altCasingColor = options.alternativeCasingColor ?? ALT_CASING_COLOR;
|
|
2915
|
+
this.altColor = options.alternativeColor ?? ALT_LINE_COLOR;
|
|
2916
|
+
this.ferryColor = options.ferryColor ?? FERRY_COLOR;
|
|
2223
2917
|
this.map.on("style.load", this.handleStyleLoad);
|
|
2224
2918
|
}
|
|
2919
|
+
/**
|
|
2920
|
+
* The MapLibre source and layer ids this layer owns - public API for
|
|
2921
|
+
* escape-hatch styling and layer ordering (`map.moveLayer`). All are
|
|
2922
|
+
* derived from the `id` option (default `mapmap-route`).
|
|
2923
|
+
*/
|
|
2924
|
+
get ids() {
|
|
2925
|
+
return {
|
|
2926
|
+
source: this.sourceId,
|
|
2927
|
+
casing: this.casingLayerId,
|
|
2928
|
+
line: this.lineLayerId,
|
|
2929
|
+
maneuverSource: this.maneuverSourceId,
|
|
2930
|
+
maneuver: this.maneuverLayerId,
|
|
2931
|
+
corridorSource: this.corridorSourceId,
|
|
2932
|
+
corridor: this.corridorLayerId,
|
|
2933
|
+
endpointsSource: this.endpointsSourceId,
|
|
2934
|
+
endpoints: this.endpointsLayerId
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2225
2937
|
/**
|
|
2226
2938
|
* Route from `from` to `to` (with optional intermediate `via` points passed
|
|
2227
2939
|
* through `options` is not supported here - pass a coordinate list to
|
|
@@ -2230,8 +2942,21 @@ var RouteLayer = class {
|
|
|
2230
2942
|
async route(from, to, options = {}) {
|
|
2231
2943
|
return this.routePath([from, to], options);
|
|
2232
2944
|
}
|
|
2233
|
-
/**
|
|
2945
|
+
/**
|
|
2946
|
+
* Route through an ordered list of coordinates and draw the result.
|
|
2947
|
+
*
|
|
2948
|
+
* Concurrent calls are ordered by ISSUE, not by arrival: only the newest
|
|
2949
|
+
* request draws. A destination retyped while the first request is still in
|
|
2950
|
+
* flight used to leave the map showing whichever response happened to land
|
|
2951
|
+
* last, so a driver could be looking at the line to the address they had
|
|
2952
|
+
* already replaced. Each caller still receives its own parsed route, so a
|
|
2953
|
+
* caller that wants the data of a superseded request keeps getting it -
|
|
2954
|
+
* only the drawing is suppressed. {@link RouteLayer.clear} likewise
|
|
2955
|
+
* supersedes anything in flight.
|
|
2956
|
+
*/
|
|
2234
2957
|
async routePath(points, options = {}) {
|
|
2958
|
+
const seq = ++this.requestSeq;
|
|
2959
|
+
this.waypoints = points.slice(1, -1).map(toLngLat);
|
|
2235
2960
|
const url = buildRouteUrl(
|
|
2236
2961
|
this.baseUrl,
|
|
2237
2962
|
options.profile ?? "driving",
|
|
@@ -2253,7 +2978,7 @@ var RouteLayer = class {
|
|
|
2253
2978
|
}
|
|
2254
2979
|
}
|
|
2255
2980
|
const parsed = parseOsrmRoute(body);
|
|
2256
|
-
this.draw(parsed);
|
|
2981
|
+
if (seq === this.requestSeq) this.draw(parsed);
|
|
2257
2982
|
return parsed;
|
|
2258
2983
|
}
|
|
2259
2984
|
/** The most recently drawn route, if any. */
|
|
@@ -2285,7 +3010,9 @@ var RouteLayer = class {
|
|
|
2285
3010
|
}
|
|
2286
3011
|
if (this.map.getLayer(this.casingLayerId) && this.map.getLayer(this.lineLayerId)) {
|
|
2287
3012
|
this.applyProgress();
|
|
3013
|
+
this.applyLineStyle();
|
|
2288
3014
|
this.installManeuver();
|
|
3015
|
+
this.installEndpoints();
|
|
2289
3016
|
return;
|
|
2290
3017
|
}
|
|
2291
3018
|
const casing = {
|
|
@@ -2295,8 +3022,8 @@ var RouteLayer = class {
|
|
|
2295
3022
|
layout: { "line-cap": "round", "line-join": "round" },
|
|
2296
3023
|
paint: this.design ? {
|
|
2297
3024
|
"line-color": this.design.casingColor,
|
|
2298
|
-
"line-width": this.design.width + 4,
|
|
2299
|
-
"line-opacity": this.design.opacity
|
|
3025
|
+
"line-width": this.design.casingWidth ?? this.design.width + 4,
|
|
3026
|
+
"line-opacity": this.design.casingOpacity ?? this.design.opacity
|
|
2300
3027
|
} : {
|
|
2301
3028
|
"line-color": CASING_COLOR,
|
|
2302
3029
|
"line-width": ["interpolate", ["linear"], ["zoom"], 8, 6, 16, 12]
|
|
@@ -2310,7 +3037,8 @@ var RouteLayer = class {
|
|
|
2310
3037
|
paint: this.design ? {
|
|
2311
3038
|
"line-color": this.design.color,
|
|
2312
3039
|
"line-width": this.design.width,
|
|
2313
|
-
"line-opacity": this.design.opacity
|
|
3040
|
+
"line-opacity": this.design.opacity,
|
|
3041
|
+
...this.design.dash ? { "line-dasharray": this.design.dash } : {}
|
|
2314
3042
|
} : {
|
|
2315
3043
|
"line-color": SIGNAL_BLUE,
|
|
2316
3044
|
"line-width": ["interpolate", ["linear"], ["zoom"], 8, 3, 16, 8]
|
|
@@ -2319,7 +3047,316 @@ var RouteLayer = class {
|
|
|
2319
3047
|
if (!this.map.getLayer(this.casingLayerId)) this.map.addLayer(casing);
|
|
2320
3048
|
if (!this.map.getLayer(this.lineLayerId)) this.map.addLayer(line);
|
|
2321
3049
|
this.applyProgress();
|
|
3050
|
+
this.applyLineStyle();
|
|
2322
3051
|
this.installManeuver();
|
|
3052
|
+
this.installEndpoints();
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Set the intermediate stops drawn by the endpoint markers, in order.
|
|
3056
|
+
* {@link routePath} fills these in from its own via points, so this is
|
|
3057
|
+
* only needed when drawing a route parsed elsewhere (e.g. a route fetched
|
|
3058
|
+
* by the app and handed to {@link draw}). An empty array clears them.
|
|
3059
|
+
*/
|
|
3060
|
+
setWaypoints(waypoints) {
|
|
3061
|
+
this.waypoints = waypoints.map(toLngLat);
|
|
3062
|
+
if (this.lastRoute && this.map.isStyleLoaded()) this.installEndpoints();
|
|
3063
|
+
}
|
|
3064
|
+
/** The intermediate stops currently drawn, `[lng, lat]` each. */
|
|
3065
|
+
get currentWaypoints() {
|
|
3066
|
+
return this.waypoints;
|
|
3067
|
+
}
|
|
3068
|
+
/**
|
|
3069
|
+
* Build one Point feature per endpoint marker: start (first route
|
|
3070
|
+
* coordinate), each waypoint in order, then end (last coordinate). The
|
|
3071
|
+
* end is added last so it wins MapLibre's symbol-collision ordering when
|
|
3072
|
+
* markers overlap on a short route.
|
|
3073
|
+
*/
|
|
3074
|
+
endpointFeatures(route) {
|
|
3075
|
+
const design = this.endpoints;
|
|
3076
|
+
if (!design) return [];
|
|
3077
|
+
const line = route.geometry.coordinates;
|
|
3078
|
+
const first = line[0];
|
|
3079
|
+
const last = line[line.length - 1];
|
|
3080
|
+
if (!first || !last) return [];
|
|
3081
|
+
const rendered = [];
|
|
3082
|
+
const push = (marker, coordinates, role, numbered) => {
|
|
3083
|
+
if (marker === false) return;
|
|
3084
|
+
rendered.push(endpointRender(marker, coordinates, role, numbered));
|
|
3085
|
+
};
|
|
3086
|
+
push(design.start, first, "start");
|
|
3087
|
+
this.waypoints.forEach((coordinates, index) => {
|
|
3088
|
+
const numbered = design.numberWaypoints ? String(index + 1) : void 0;
|
|
3089
|
+
push(design.waypoint, coordinates, "waypoint", numbered);
|
|
3090
|
+
});
|
|
3091
|
+
push(design.end, last, "end");
|
|
3092
|
+
return rendered;
|
|
3093
|
+
}
|
|
3094
|
+
/**
|
|
3095
|
+
* Register every endpoint marker image the current features need
|
|
3096
|
+
* (idempotent; `setStyle` wipes runtime images, so this runs on each
|
|
3097
|
+
* install). Keys are remembered so teardown removes exactly what this
|
|
3098
|
+
* layer added.
|
|
3099
|
+
*/
|
|
3100
|
+
registerEndpointImages(rendered) {
|
|
3101
|
+
for (const item of rendered) {
|
|
3102
|
+
this.endpointImageKeys.add(item.key);
|
|
3103
|
+
if (this.map.hasImage(item.key)) continue;
|
|
3104
|
+
const image = renderMarkerImage(item.render);
|
|
3105
|
+
if (image) {
|
|
3106
|
+
this.map.addImage(item.key, image.image, { pixelRatio: image.pixelRatio });
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
/** Add-or-update the endpoint markers' source and symbol layer. */
|
|
3111
|
+
installEndpoints() {
|
|
3112
|
+
if (!this.endpoints || !this.lastRoute) return;
|
|
3113
|
+
const rendered = this.endpointFeatures(this.lastRoute);
|
|
3114
|
+
const data = {
|
|
3115
|
+
type: "FeatureCollection",
|
|
3116
|
+
features: rendered.map((item) => item.feature)
|
|
3117
|
+
};
|
|
3118
|
+
this.registerEndpointImages(rendered);
|
|
3119
|
+
const source = this.map.getSource(this.endpointsSourceId);
|
|
3120
|
+
if (source) {
|
|
3121
|
+
source.setData(data);
|
|
3122
|
+
} else {
|
|
3123
|
+
this.map.addSource(this.endpointsSourceId, { type: "geojson", data });
|
|
3124
|
+
}
|
|
3125
|
+
if (this.map.getLayer(this.endpointsLayerId)) return;
|
|
3126
|
+
const layer = {
|
|
3127
|
+
id: this.endpointsLayerId,
|
|
3128
|
+
type: "symbol",
|
|
3129
|
+
source: this.endpointsSourceId,
|
|
3130
|
+
layout: {
|
|
3131
|
+
"icon-image": ["get", "iconImage"],
|
|
3132
|
+
"icon-anchor": ["get", "iconAnchor"],
|
|
3133
|
+
"icon-allow-overlap": true,
|
|
3134
|
+
"text-field": ["get", "label"],
|
|
3135
|
+
"text-font": [ENDPOINT_TEXT_FONT],
|
|
3136
|
+
"text-size": ["get", "textSize"],
|
|
3137
|
+
"text-anchor": "top",
|
|
3138
|
+
"text-offset": ENDPOINT_TEXT_OFFSET,
|
|
3139
|
+
"text-optional": true
|
|
3140
|
+
},
|
|
3141
|
+
paint: {
|
|
3142
|
+
"text-color": "#333333",
|
|
3143
|
+
"text-halo-color": "#ffffff",
|
|
3144
|
+
"text-halo-width": 1.5
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3147
|
+
this.map.addLayer(layer);
|
|
3148
|
+
}
|
|
3149
|
+
/** Remove the endpoint markers, their source and their images. */
|
|
3150
|
+
clearEndpoints() {
|
|
3151
|
+
if (this.map.getLayer(this.endpointsLayerId)) {
|
|
3152
|
+
this.map.removeLayer(this.endpointsLayerId);
|
|
3153
|
+
}
|
|
3154
|
+
if (this.map.getSource(this.endpointsSourceId)) {
|
|
3155
|
+
this.map.removeSource(this.endpointsSourceId);
|
|
3156
|
+
}
|
|
3157
|
+
for (const key of this.endpointImageKeys) {
|
|
3158
|
+
if (this.map.hasImage(key)) this.map.removeImage(key);
|
|
3159
|
+
}
|
|
3160
|
+
this.endpointImageKeys.clear();
|
|
3161
|
+
}
|
|
3162
|
+
/**
|
|
3163
|
+
* Draw a bare line from coordinates, without a parsed OSRM route.
|
|
3164
|
+
*
|
|
3165
|
+
* {@link RouteLayer.draw} expects a {@link ParsedRoute} because it is
|
|
3166
|
+
* normally fed by {@link RouteLayer.route}. Callers that already have
|
|
3167
|
+
* geometry and nothing else — an agent tool result, a stored polyline,
|
|
3168
|
+
* a hand-built preview — had to invent a `ParsedRoute` with zeroed
|
|
3169
|
+
* distance and duration that `draw` never reads. This is that path,
|
|
3170
|
+
* named honestly.
|
|
3171
|
+
*/
|
|
3172
|
+
drawGeometry(coordinates) {
|
|
3173
|
+
this.draw({
|
|
3174
|
+
distanceM: 0,
|
|
3175
|
+
durationS: 0,
|
|
3176
|
+
geometry: { type: "LineString", coordinates },
|
|
3177
|
+
raw: {}
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Draw a set of routes: one selected, the rest as dimmer alternatives
|
|
3182
|
+
* beneath it. Offering alternatives is table stakes for a navigation UI,
|
|
3183
|
+
* and the selected route keeps every feature of {@link RouteLayer.draw}
|
|
3184
|
+
* (progress, manoeuvre arrow, corridors, route effects).
|
|
3185
|
+
*
|
|
3186
|
+
* Pass the index of the route to select; out-of-range values clamp to the
|
|
3187
|
+
* first route. Calling with a single route is equivalent to `draw`, and
|
|
3188
|
+
* calling with an empty array clears everything.
|
|
3189
|
+
*/
|
|
3190
|
+
drawAlternatives(routes, selectedIndex = 0) {
|
|
3191
|
+
if (routes.length === 0) {
|
|
3192
|
+
this.clear();
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
const index = selectedIndex >= 0 && selectedIndex < routes.length ? selectedIndex : 0;
|
|
3196
|
+
this.alternatives = routes.filter((_, i) => i !== index);
|
|
3197
|
+
this.altOriginalIndices = routes.map((_, i) => i).filter((i) => i !== index);
|
|
3198
|
+
this.installAlternatives();
|
|
3199
|
+
this.draw(routes[index]);
|
|
3200
|
+
}
|
|
3201
|
+
/** Which alternatives are currently drawn, in the order given. */
|
|
3202
|
+
get alternativeRoutes() {
|
|
3203
|
+
return this.alternatives;
|
|
3204
|
+
}
|
|
3205
|
+
/**
|
|
3206
|
+
* Register a click handler for the alternative lines. The index is the
|
|
3207
|
+
* position within the array last passed to
|
|
3208
|
+
* {@link RouteLayer.drawAlternatives}, so a caller can re-issue that call
|
|
3209
|
+
* with the new selection. Passing `undefined` removes the handler.
|
|
3210
|
+
*/
|
|
3211
|
+
onSelectAlternative(handler) {
|
|
3212
|
+
this.selectHandler = handler;
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* Draw ferry legs of the selected route as dashes over its line, so water
|
|
3216
|
+
* crossings do not read as driving.
|
|
3217
|
+
*
|
|
3218
|
+
* Supplied as explicit geometries rather than derived from the route,
|
|
3219
|
+
* because `line-dasharray` cannot be data-driven: a ferry leg needs its
|
|
3220
|
+
* own layer, and only the caller knows which parts of their route are
|
|
3221
|
+
* ferries.
|
|
3222
|
+
*/
|
|
3223
|
+
setFerrySegments(segments) {
|
|
3224
|
+
this.ferrySegments = segments;
|
|
3225
|
+
if (this.map.isStyleLoaded()) this.installFerry();
|
|
3226
|
+
}
|
|
3227
|
+
/** Remove the ferry dashes. */
|
|
3228
|
+
clearFerrySegments() {
|
|
3229
|
+
this.ferrySegments = [];
|
|
3230
|
+
if (this.map.getLayer(this.ferryLayerId)) this.map.removeLayer(this.ferryLayerId);
|
|
3231
|
+
if (this.map.getSource(this.ferrySourceId)) this.map.removeSource(this.ferrySourceId);
|
|
3232
|
+
}
|
|
3233
|
+
/** Add-or-update the alternative-route source and its two layers. */
|
|
3234
|
+
installAlternatives() {
|
|
3235
|
+
if (this.alternatives.length === 0) {
|
|
3236
|
+
this.removeAlternativeLayers();
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
if (!this.map.isStyleLoaded()) return;
|
|
3240
|
+
const data = {
|
|
3241
|
+
type: "FeatureCollection",
|
|
3242
|
+
features: this.alternatives.map((route, i) => ({
|
|
3243
|
+
type: "Feature",
|
|
3244
|
+
properties: { index: i },
|
|
3245
|
+
geometry: route.geometry
|
|
3246
|
+
}))
|
|
3247
|
+
};
|
|
3248
|
+
const existing = this.map.getSource(this.altSourceId);
|
|
3249
|
+
if (existing) existing.setData(data);
|
|
3250
|
+
else this.map.addSource(this.altSourceId, { type: "geojson", data });
|
|
3251
|
+
const below = this.map.getLayer(this.casingLayerId) ? this.casingLayerId : void 0;
|
|
3252
|
+
if (!this.map.getLayer(this.altCasingLayerId)) {
|
|
3253
|
+
this.map.addLayer({
|
|
3254
|
+
id: this.altCasingLayerId,
|
|
3255
|
+
type: "line",
|
|
3256
|
+
source: this.altSourceId,
|
|
3257
|
+
layout: { "line-cap": "round", "line-join": "round" },
|
|
3258
|
+
paint: {
|
|
3259
|
+
"line-color": this.altCasingColor,
|
|
3260
|
+
"line-width": 7,
|
|
3261
|
+
"line-opacity": 0.55
|
|
3262
|
+
}
|
|
3263
|
+
}, below);
|
|
3264
|
+
}
|
|
3265
|
+
if (!this.map.getLayer(this.altLineLayerId)) {
|
|
3266
|
+
this.map.addLayer({
|
|
3267
|
+
id: this.altLineLayerId,
|
|
3268
|
+
type: "line",
|
|
3269
|
+
source: this.altSourceId,
|
|
3270
|
+
layout: { "line-cap": "round", "line-join": "round" },
|
|
3271
|
+
paint: {
|
|
3272
|
+
"line-color": this.altColor,
|
|
3273
|
+
"line-width": 4,
|
|
3274
|
+
"line-opacity": 0.9
|
|
3275
|
+
}
|
|
3276
|
+
}, below);
|
|
3277
|
+
}
|
|
3278
|
+
if (!this.altClickBound) {
|
|
3279
|
+
this.map.on("click", this.altLineLayerId, this.handleAlternativeClick);
|
|
3280
|
+
this.altClickBound = true;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
removeAlternativeLayers() {
|
|
3284
|
+
if (this.altClickBound) {
|
|
3285
|
+
this.map.off("click", this.altLineLayerId, this.handleAlternativeClick);
|
|
3286
|
+
this.altClickBound = false;
|
|
3287
|
+
}
|
|
3288
|
+
if (this.map.getLayer(this.altLineLayerId)) {
|
|
3289
|
+
this.map.removeLayer(this.altLineLayerId);
|
|
3290
|
+
}
|
|
3291
|
+
if (this.map.getLayer(this.altCasingLayerId)) this.map.removeLayer(this.altCasingLayerId);
|
|
3292
|
+
if (this.map.getSource(this.altSourceId)) this.map.removeSource(this.altSourceId);
|
|
3293
|
+
}
|
|
3294
|
+
/** Add-or-update the ferry-dash overlay above the selected route. */
|
|
3295
|
+
installFerry() {
|
|
3296
|
+
if (this.ferrySegments.length === 0) {
|
|
3297
|
+
this.clearFerrySegments();
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
if (!this.map.isStyleLoaded()) return;
|
|
3301
|
+
const data = {
|
|
3302
|
+
type: "FeatureCollection",
|
|
3303
|
+
features: this.ferrySegments.map((coordinates) => ({
|
|
3304
|
+
type: "Feature",
|
|
3305
|
+
properties: {},
|
|
3306
|
+
geometry: { type: "LineString", coordinates }
|
|
3307
|
+
}))
|
|
3308
|
+
};
|
|
3309
|
+
const existing = this.map.getSource(this.ferrySourceId);
|
|
3310
|
+
if (existing) existing.setData(data);
|
|
3311
|
+
else this.map.addSource(this.ferrySourceId, { type: "geojson", data });
|
|
3312
|
+
if (!this.map.getLayer(this.ferryLayerId)) {
|
|
3313
|
+
this.map.addLayer({
|
|
3314
|
+
id: this.ferryLayerId,
|
|
3315
|
+
type: "line",
|
|
3316
|
+
source: this.ferrySourceId,
|
|
3317
|
+
layout: { "line-cap": "butt", "line-join": "round" },
|
|
3318
|
+
paint: {
|
|
3319
|
+
"line-color": this.ferryColor,
|
|
3320
|
+
"line-width": 2.5,
|
|
3321
|
+
"line-dasharray": [1.2, 1.6]
|
|
3322
|
+
}
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3327
|
+
* Override the drawn line's paint at runtime, over whatever the design or
|
|
3328
|
+
* the built-in look set.
|
|
3329
|
+
*
|
|
3330
|
+
* Route styling is not always static: a route can be provisional, or a
|
|
3331
|
+
* straight-line approximation that must not be mistaken for a surveyed
|
|
3332
|
+
* one. That is a paint change on a route already drawn, so it belongs
|
|
3333
|
+
* here rather than in the constructor's design.
|
|
3334
|
+
*
|
|
3335
|
+
* Overrides survive style reloads. Pass `{}` to clear them.
|
|
3336
|
+
*/
|
|
3337
|
+
setLineStyle(style) {
|
|
3338
|
+
this.lineStyle = Object.keys(style).length > 0 ? style : void 0;
|
|
3339
|
+
this.applyLineStyle();
|
|
3340
|
+
}
|
|
3341
|
+
/** Apply any runtime override; a no-op when the layers are absent. */
|
|
3342
|
+
applyLineStyle() {
|
|
3343
|
+
const style = this.lineStyle;
|
|
3344
|
+
if (!style) return;
|
|
3345
|
+
if (this.map.getLayer(this.lineLayerId)) {
|
|
3346
|
+
if (style.color !== void 0) {
|
|
3347
|
+
this.map.setPaintProperty(this.lineLayerId, "line-color", style.color);
|
|
3348
|
+
}
|
|
3349
|
+
if (style.dash !== void 0) {
|
|
3350
|
+
this.map.setPaintProperty(
|
|
3351
|
+
this.lineLayerId,
|
|
3352
|
+
"line-dasharray",
|
|
3353
|
+
style.dash ?? [1, 0]
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
if (style.casingOpacity !== void 0 && this.map.getLayer(this.casingLayerId)) {
|
|
3358
|
+
this.map.setPaintProperty(this.casingLayerId, "line-opacity", style.casingOpacity);
|
|
3359
|
+
}
|
|
2323
3360
|
}
|
|
2324
3361
|
/**
|
|
2325
3362
|
* Sets how much of the route has been travelled, as a fraction in `[0, 1]`
|
|
@@ -2457,10 +3494,21 @@ var RouteLayer = class {
|
|
|
2457
3494
|
this.map.setPaintProperty(this.corridorLayerId, "line-width", width);
|
|
2458
3495
|
}
|
|
2459
3496
|
}
|
|
2460
|
-
/**
|
|
3497
|
+
/**
|
|
3498
|
+
* Remove the route's layers and source from the map. Any route request
|
|
3499
|
+
* still in flight is superseded, so a late response cannot redraw a route
|
|
3500
|
+
* the caller has already cleared.
|
|
3501
|
+
*/
|
|
2461
3502
|
clear() {
|
|
3503
|
+
this.requestSeq += 1;
|
|
2462
3504
|
this.clearManeuver();
|
|
2463
3505
|
this.clearAlertCorridors();
|
|
3506
|
+
this.clearFerrySegments();
|
|
3507
|
+
this.alternatives = [];
|
|
3508
|
+
this.altOriginalIndices = [];
|
|
3509
|
+
this.removeAlternativeLayers();
|
|
3510
|
+
this.clearEndpoints();
|
|
3511
|
+
this.waypoints = [];
|
|
2464
3512
|
for (const layerId of [this.lineLayerId, this.casingLayerId]) {
|
|
2465
3513
|
if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
|
|
2466
3514
|
}
|
|
@@ -2530,7 +3578,7 @@ function alertContrastIssues(design) {
|
|
|
2530
3578
|
return issues;
|
|
2531
3579
|
}
|
|
2532
3580
|
var MAX_PUCK_IMAGE_BYTES = 64 * 1024;
|
|
2533
|
-
function
|
|
3581
|
+
function dataUriBytes2(uri) {
|
|
2534
3582
|
const comma = uri.indexOf(",");
|
|
2535
3583
|
const payload = comma === -1 ? "" : uri.slice(comma + 1);
|
|
2536
3584
|
const meta = comma === -1 ? uri : uri.slice(0, comma);
|
|
@@ -2539,7 +3587,7 @@ function dataUriBytes(uri) {
|
|
|
2539
3587
|
function effectiveImageUrl(design) {
|
|
2540
3588
|
const url = design.imageUrl?.trim();
|
|
2541
3589
|
if (!url) return void 0;
|
|
2542
|
-
if (/^data:/i.test(url) &&
|
|
3590
|
+
if (/^data:/i.test(url) && dataUriBytes2(url) > MAX_PUCK_IMAGE_BYTES) {
|
|
2543
3591
|
return void 0;
|
|
2544
3592
|
}
|
|
2545
3593
|
return url;
|
|
@@ -2858,7 +3906,7 @@ async function uploadProbeBatch(baseUrl, apiKey, body, options = {}) {
|
|
|
2858
3906
|
} catch {
|
|
2859
3907
|
outcome = "gaveUp";
|
|
2860
3908
|
}
|
|
2861
|
-
if (outcome === "gaveUp" && attempt < maxAttempts) {
|
|
3909
|
+
if (outcome === "gaveUp" && attempt < maxAttempts && options.signal?.aborted !== true) {
|
|
2862
3910
|
await sleep(backoffMs(attempt));
|
|
2863
3911
|
continue;
|
|
2864
3912
|
}
|
|
@@ -2904,6 +3952,8 @@ function haversineDistanceM(a, b) {
|
|
|
2904
3952
|
}
|
|
2905
3953
|
var PlacesLayer = class {
|
|
2906
3954
|
constructor(map, options = {}) {
|
|
3955
|
+
/** Per-place icon keys whose image failed to load (circle fallback). */
|
|
3956
|
+
this.failedIconKeys = /* @__PURE__ */ new Set();
|
|
2907
3957
|
this.places = [];
|
|
2908
3958
|
this.fitted = false;
|
|
2909
3959
|
this.destroyed = false;
|
|
@@ -2956,13 +4006,21 @@ var PlacesLayer = class {
|
|
|
2956
4006
|
this.clustersLayerId = `${id}-clusters`;
|
|
2957
4007
|
this.clusterCountLayerId = `${id}-cluster-count`;
|
|
2958
4008
|
this.pointsLayerId = `${id}-points`;
|
|
4009
|
+
this.pointsFallbackLayerId = `${id}-points-fallback`;
|
|
4010
|
+
this.labelsLayerId = `${id}-labels`;
|
|
2959
4011
|
this.iconImageId = `${id}-icon`;
|
|
2960
4012
|
this.cluster = options.cluster ?? true;
|
|
2961
4013
|
this.clusterRadius = options.clusterRadius;
|
|
2962
4014
|
this.clusterMaxZoom = options.clusterMaxZoom;
|
|
2963
4015
|
this.color = options.color ?? SIGNAL_BLUE;
|
|
2964
4016
|
this.clusterColor = options.clusterColor ?? (typeof this.color === "string" ? this.color : SIGNAL_BLUE);
|
|
2965
|
-
|
|
4017
|
+
if (options.icon && typeof options.icon.url === "string") {
|
|
4018
|
+
this.singleIcon = options.icon;
|
|
4019
|
+
} else if (options.icon) {
|
|
4020
|
+
this.iconRecord = options.icon;
|
|
4021
|
+
}
|
|
4022
|
+
this.iconProperty = options.iconProperty ?? "kind";
|
|
4023
|
+
this.label = options.label === true ? {} : options.label ? options.label : void 0;
|
|
2966
4024
|
this.wantFitBounds = options.fitBounds ?? false;
|
|
2967
4025
|
this.onPlaceClick = options.onPlaceClick;
|
|
2968
4026
|
this.popupFn = options.popup;
|
|
@@ -2970,6 +4028,11 @@ var PlacesLayer = class {
|
|
|
2970
4028
|
this.map.on("click", this.pointsLayerId, this.handlePlaceClick);
|
|
2971
4029
|
this.map.on("mouseenter", this.pointsLayerId, this.handleMouseEnter);
|
|
2972
4030
|
this.map.on("mouseleave", this.pointsLayerId, this.handleMouseLeave);
|
|
4031
|
+
if (this.iconRecord) {
|
|
4032
|
+
this.map.on("click", this.pointsFallbackLayerId, this.handlePlaceClick);
|
|
4033
|
+
this.map.on("mouseenter", this.pointsFallbackLayerId, this.handleMouseEnter);
|
|
4034
|
+
this.map.on("mouseleave", this.pointsFallbackLayerId, this.handleMouseLeave);
|
|
4035
|
+
}
|
|
2973
4036
|
if (this.cluster) {
|
|
2974
4037
|
this.map.on("click", this.clustersLayerId, this.handleClusterClick);
|
|
2975
4038
|
this.map.on("mouseenter", this.clustersLayerId, this.handleMouseEnter);
|
|
@@ -3029,7 +4092,9 @@ var PlacesLayer = class {
|
|
|
3029
4092
|
source: this.sourceId,
|
|
3030
4093
|
points: this.pointsLayerId,
|
|
3031
4094
|
clusters: this.clustersLayerId,
|
|
3032
|
-
clusterCounts: this.clusterCountLayerId
|
|
4095
|
+
clusterCounts: this.clusterCountLayerId,
|
|
4096
|
+
labels: this.labelsLayerId,
|
|
4097
|
+
pointsFallback: this.pointsFallbackLayerId
|
|
3033
4098
|
};
|
|
3034
4099
|
}
|
|
3035
4100
|
/**
|
|
@@ -3073,6 +4138,11 @@ var PlacesLayer = class {
|
|
|
3073
4138
|
* attached; unreachable places (`null` duration) are dropped. Needs the
|
|
3074
4139
|
* gateway `baseUrl`/`apiKey` - picked up from a `MapMapMap` automatically,
|
|
3075
4140
|
* or pass them as layer options.
|
|
4141
|
+
*
|
|
4142
|
+
* Answers always describe the places that were asked about: the list is
|
|
4143
|
+
* snapshotted at call time, so a `setPlaces` landing mid-flight (a
|
|
4144
|
+
* search-as-you-type filter, say) cannot mis-pair the matrix rows with a
|
|
4145
|
+
* different set of places.
|
|
3076
4146
|
*/
|
|
3077
4147
|
async nearestByDriveTime(origin, options = {}) {
|
|
3078
4148
|
if (!this.baseUrl) {
|
|
@@ -3080,7 +4150,8 @@ var PlacesLayer = class {
|
|
|
3080
4150
|
"PlacesLayer needs a baseUrl (or a MapMapMap) for nearestByDriveTime"
|
|
3081
4151
|
);
|
|
3082
4152
|
}
|
|
3083
|
-
|
|
4153
|
+
const places = this.places;
|
|
4154
|
+
if (places.length === 0) return [];
|
|
3084
4155
|
const headers = {
|
|
3085
4156
|
"Content-Type": "application/json",
|
|
3086
4157
|
Accept: "application/json"
|
|
@@ -3088,7 +4159,7 @@ var PlacesLayer = class {
|
|
|
3088
4159
|
if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
3089
4160
|
const body = {
|
|
3090
4161
|
sources: [{ lat: origin.lat, lon: origin.lon }],
|
|
3091
|
-
targets:
|
|
4162
|
+
targets: places.map((p) => ({ lat: p.lat, lon: p.lon })),
|
|
3092
4163
|
costing: options.costing ?? "auto"
|
|
3093
4164
|
};
|
|
3094
4165
|
if (options.costingOptions) body["costing_options"] = options.costingOptions;
|
|
@@ -3107,7 +4178,7 @@ var PlacesLayer = class {
|
|
|
3107
4178
|
}
|
|
3108
4179
|
const { durations, distances } = matrixRows(payload);
|
|
3109
4180
|
const reachable = [];
|
|
3110
|
-
|
|
4181
|
+
places.forEach((place, index) => {
|
|
3111
4182
|
const durationS = durations[index];
|
|
3112
4183
|
if (typeof durationS !== "number") return;
|
|
3113
4184
|
const distanceM = distances?.[index];
|
|
@@ -3136,6 +4207,38 @@ var PlacesLayer = class {
|
|
|
3136
4207
|
}
|
|
3137
4208
|
if (this.cluster) this.installClusterLayers();
|
|
3138
4209
|
this.installPointsLayer();
|
|
4210
|
+
this.installLabelsLayer();
|
|
4211
|
+
}
|
|
4212
|
+
/**
|
|
4213
|
+
* The per-place name labels layer (only with a `label` option): the map
|
|
4214
|
+
* font with a halo, anchored just below each unclustered point, hidden
|
|
4215
|
+
* before colliding (`text-optional`). Re-installed with the rest of the
|
|
4216
|
+
* layers on every `style.load`.
|
|
4217
|
+
*/
|
|
4218
|
+
installLabelsLayer() {
|
|
4219
|
+
if (!this.label || this.map.getLayer(this.labelsLayerId)) return;
|
|
4220
|
+
const labels = {
|
|
4221
|
+
id: this.labelsLayerId,
|
|
4222
|
+
type: "symbol",
|
|
4223
|
+
source: this.sourceId,
|
|
4224
|
+
...this.cluster ? { filter: ["!", ["has", "point_count"]] } : {},
|
|
4225
|
+
layout: {
|
|
4226
|
+
"text-field": ["get", this.label.property ?? "name"],
|
|
4227
|
+
"text-font": [CLUSTER_TEXT_FONT],
|
|
4228
|
+
"text-size": this.label.size ?? 12,
|
|
4229
|
+
"text-anchor": "top",
|
|
4230
|
+
"text-offset": [0, 0.9],
|
|
4231
|
+
"text-optional": true
|
|
4232
|
+
},
|
|
4233
|
+
paint: {
|
|
4234
|
+
// Both spellings are accepted (British wins) — see
|
|
4235
|
+
// PlacesLabelOptions.
|
|
4236
|
+
"text-color": this.label.colour ?? this.label.color ?? "#333333",
|
|
4237
|
+
"text-halo-color": this.label.haloColour ?? this.label.haloColor ?? "#ffffff",
|
|
4238
|
+
"text-halo-width": 1.5
|
|
4239
|
+
}
|
|
4240
|
+
};
|
|
4241
|
+
this.map.addLayer(labels);
|
|
3139
4242
|
}
|
|
3140
4243
|
/** Cluster circles (sized by count) and the count badge on top. */
|
|
3141
4244
|
installClusterLayers() {
|
|
@@ -3177,8 +4280,12 @@ var PlacesLayer = class {
|
|
|
3177
4280
|
* circle look if it fails, like PositionPuck's image fallback).
|
|
3178
4281
|
*/
|
|
3179
4282
|
installPointsLayer() {
|
|
4283
|
+
if (this.iconRecord) {
|
|
4284
|
+
this.installPerPlaceIconLayers(this.iconRecord);
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
3180
4287
|
if (this.map.getLayer(this.pointsLayerId)) return;
|
|
3181
|
-
if (!this.
|
|
4288
|
+
if (!this.singleIcon) {
|
|
3182
4289
|
this.addCirclePointsLayer();
|
|
3183
4290
|
return;
|
|
3184
4291
|
}
|
|
@@ -3186,7 +4293,7 @@ var PlacesLayer = class {
|
|
|
3186
4293
|
this.addIconPointsLayer();
|
|
3187
4294
|
return;
|
|
3188
4295
|
}
|
|
3189
|
-
void this.map.loadImage(this.
|
|
4296
|
+
void this.map.loadImage(this.singleIcon.url).then((image) => {
|
|
3190
4297
|
if (this.destroyed || this.map.getLayer(this.pointsLayerId)) return;
|
|
3191
4298
|
if (!this.map.hasImage(this.iconImageId)) {
|
|
3192
4299
|
this.map.addImage(this.iconImageId, image.data);
|
|
@@ -3197,6 +4304,103 @@ var PlacesLayer = class {
|
|
|
3197
4304
|
this.addCirclePointsLayer();
|
|
3198
4305
|
});
|
|
3199
4306
|
}
|
|
4307
|
+
/** The `map.addImage` id for one key of the per-place icon record. */
|
|
4308
|
+
iconImageIdFor(key) {
|
|
4309
|
+
return `${this.iconImageId}-${key}`;
|
|
4310
|
+
}
|
|
4311
|
+
/**
|
|
4312
|
+
* Per-place icon mode: load every record image (each key independently -
|
|
4313
|
+
* one bad URL never sinks the rest), then install a symbol layer whose
|
|
4314
|
+
* `icon-image` is a match expression on `iconProperty`, plus a circle
|
|
4315
|
+
* fallback layer for places whose property value has no loaded icon.
|
|
4316
|
+
*/
|
|
4317
|
+
installPerPlaceIconLayers(record) {
|
|
4318
|
+
const keys = Object.keys(record);
|
|
4319
|
+
if (keys.length === 0) {
|
|
4320
|
+
if (!this.map.getLayer(this.pointsLayerId)) this.addCirclePointsLayer();
|
|
4321
|
+
return;
|
|
4322
|
+
}
|
|
4323
|
+
const toLoad = keys.filter(
|
|
4324
|
+
(key) => !this.map.hasImage(this.iconImageIdFor(key)) && !this.failedIconKeys.has(key)
|
|
4325
|
+
);
|
|
4326
|
+
if (toLoad.length === 0) {
|
|
4327
|
+
this.addPerPlaceLayers(record);
|
|
4328
|
+
return;
|
|
4329
|
+
}
|
|
4330
|
+
void Promise.all(
|
|
4331
|
+
toLoad.map(async (key) => {
|
|
4332
|
+
try {
|
|
4333
|
+
const image = await this.map.loadImage(record[key].url);
|
|
4334
|
+
if (this.destroyed) return;
|
|
4335
|
+
if (!this.map.hasImage(this.iconImageIdFor(key))) {
|
|
4336
|
+
this.map.addImage(this.iconImageIdFor(key), image.data);
|
|
4337
|
+
}
|
|
4338
|
+
} catch {
|
|
4339
|
+
this.failedIconKeys.add(key);
|
|
4340
|
+
}
|
|
4341
|
+
})
|
|
4342
|
+
).then(() => {
|
|
4343
|
+
if (!this.destroyed) this.addPerPlaceLayers(record);
|
|
4344
|
+
});
|
|
4345
|
+
}
|
|
4346
|
+
/** Install the per-place icon symbol layer + circle fallback layer. */
|
|
4347
|
+
addPerPlaceLayers(record) {
|
|
4348
|
+
const loaded = Object.keys(record).filter(
|
|
4349
|
+
(key) => this.map.hasImage(this.iconImageIdFor(key))
|
|
4350
|
+
);
|
|
4351
|
+
const unclustered = ["!", ["has", "point_count"]];
|
|
4352
|
+
if (!this.map.getLayer(this.pointsFallbackLayerId)) {
|
|
4353
|
+
const notCovered = [
|
|
4354
|
+
"!",
|
|
4355
|
+
["in", ["get", this.iconProperty], ["literal", loaded]]
|
|
4356
|
+
];
|
|
4357
|
+
const fallback = {
|
|
4358
|
+
id: this.pointsFallbackLayerId,
|
|
4359
|
+
type: "circle",
|
|
4360
|
+
source: this.sourceId,
|
|
4361
|
+
filter: this.cluster ? ["all", unclustered, notCovered] : notCovered,
|
|
4362
|
+
paint: {
|
|
4363
|
+
"circle-color": this.color,
|
|
4364
|
+
"circle-radius": 7,
|
|
4365
|
+
"circle-stroke-color": "#ffffff",
|
|
4366
|
+
"circle-stroke-width": 2
|
|
4367
|
+
}
|
|
4368
|
+
};
|
|
4369
|
+
this.map.addLayer(fallback);
|
|
4370
|
+
}
|
|
4371
|
+
if (loaded.length === 0) return;
|
|
4372
|
+
if (this.map.getLayer(this.pointsLayerId)) return;
|
|
4373
|
+
const iconImage = [
|
|
4374
|
+
"match",
|
|
4375
|
+
["get", this.iconProperty],
|
|
4376
|
+
...loaded.flatMap((key) => [key, this.iconImageIdFor(key)]),
|
|
4377
|
+
this.iconImageIdFor(loaded[0])
|
|
4378
|
+
// unreachable under the filter
|
|
4379
|
+
];
|
|
4380
|
+
const iconSize = [
|
|
4381
|
+
"match",
|
|
4382
|
+
["get", this.iconProperty],
|
|
4383
|
+
...loaded.flatMap((key) => [key, record[key]?.size ?? 1]),
|
|
4384
|
+
1
|
|
4385
|
+
];
|
|
4386
|
+
const covered = [
|
|
4387
|
+
"in",
|
|
4388
|
+
["get", this.iconProperty],
|
|
4389
|
+
["literal", loaded]
|
|
4390
|
+
];
|
|
4391
|
+
const points = {
|
|
4392
|
+
id: this.pointsLayerId,
|
|
4393
|
+
type: "symbol",
|
|
4394
|
+
source: this.sourceId,
|
|
4395
|
+
filter: this.cluster ? ["all", unclustered, covered] : covered,
|
|
4396
|
+
layout: {
|
|
4397
|
+
"icon-image": iconImage,
|
|
4398
|
+
"icon-size": iconSize,
|
|
4399
|
+
"icon-allow-overlap": true
|
|
4400
|
+
}
|
|
4401
|
+
};
|
|
4402
|
+
this.map.addLayer(points);
|
|
4403
|
+
}
|
|
3200
4404
|
addCirclePointsLayer() {
|
|
3201
4405
|
const points = {
|
|
3202
4406
|
id: this.pointsLayerId,
|
|
@@ -3220,7 +4424,7 @@ var PlacesLayer = class {
|
|
|
3220
4424
|
...this.cluster ? { filter: ["!", ["has", "point_count"]] } : {},
|
|
3221
4425
|
layout: {
|
|
3222
4426
|
"icon-image": this.iconImageId,
|
|
3223
|
-
"icon-size": this.
|
|
4427
|
+
"icon-size": this.singleIcon?.size ?? 1,
|
|
3224
4428
|
"icon-allow-overlap": true
|
|
3225
4429
|
}
|
|
3226
4430
|
};
|
|
@@ -3244,7 +4448,9 @@ var PlacesLayer = class {
|
|
|
3244
4448
|
this.popup?.remove();
|
|
3245
4449
|
this.popup = void 0;
|
|
3246
4450
|
for (const layerId of [
|
|
4451
|
+
this.labelsLayerId,
|
|
3247
4452
|
this.pointsLayerId,
|
|
4453
|
+
this.pointsFallbackLayerId,
|
|
3248
4454
|
this.clusterCountLayerId,
|
|
3249
4455
|
this.clustersLayerId
|
|
3250
4456
|
]) {
|
|
@@ -3254,6 +4460,11 @@ var PlacesLayer = class {
|
|
|
3254
4460
|
if (this.map.hasImage(this.iconImageId)) {
|
|
3255
4461
|
this.map.removeImage(this.iconImageId);
|
|
3256
4462
|
}
|
|
4463
|
+
for (const key of Object.keys(this.iconRecord ?? {})) {
|
|
4464
|
+
if (this.map.hasImage(this.iconImageIdFor(key))) {
|
|
4465
|
+
this.map.removeImage(this.iconImageIdFor(key));
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
3257
4468
|
this.places = [];
|
|
3258
4469
|
this.data = void 0;
|
|
3259
4470
|
}
|
|
@@ -3267,6 +4478,11 @@ var PlacesLayer = class {
|
|
|
3267
4478
|
this.map.off("click", this.pointsLayerId, this.handlePlaceClick);
|
|
3268
4479
|
this.map.off("mouseenter", this.pointsLayerId, this.handleMouseEnter);
|
|
3269
4480
|
this.map.off("mouseleave", this.pointsLayerId, this.handleMouseLeave);
|
|
4481
|
+
if (this.iconRecord) {
|
|
4482
|
+
this.map.off("click", this.pointsFallbackLayerId, this.handlePlaceClick);
|
|
4483
|
+
this.map.off("mouseenter", this.pointsFallbackLayerId, this.handleMouseEnter);
|
|
4484
|
+
this.map.off("mouseleave", this.pointsFallbackLayerId, this.handleMouseLeave);
|
|
4485
|
+
}
|
|
3270
4486
|
if (this.cluster) {
|
|
3271
4487
|
this.map.off("click", this.clustersLayerId, this.handleClusterClick);
|
|
3272
4488
|
this.map.off("mouseenter", this.clustersLayerId, this.handleMouseEnter);
|
|
@@ -3276,16 +4492,26 @@ var PlacesLayer = class {
|
|
|
3276
4492
|
}
|
|
3277
4493
|
};
|
|
3278
4494
|
function boundsOf(places) {
|
|
3279
|
-
let minLng = Infinity;
|
|
3280
4495
|
let minLat = Infinity;
|
|
3281
|
-
let maxLng = -Infinity;
|
|
3282
4496
|
let maxLat = -Infinity;
|
|
3283
|
-
for (const {
|
|
3284
|
-
if (lon < minLng) minLng = lon;
|
|
4497
|
+
for (const { lat } of places) {
|
|
3285
4498
|
if (lat < minLat) minLat = lat;
|
|
3286
|
-
if (lon > maxLng) maxLng = lon;
|
|
3287
4499
|
if (lat > maxLat) maxLat = lat;
|
|
3288
4500
|
}
|
|
4501
|
+
const lngs = places.map((p) => p.lon).sort((a, b) => a - b);
|
|
4502
|
+
let widestGap = -Infinity;
|
|
4503
|
+
let cutAfter = lngs.length - 1;
|
|
4504
|
+
for (let i = 0; i < lngs.length; i++) {
|
|
4505
|
+
const here = lngs[i];
|
|
4506
|
+
const next = i === lngs.length - 1 ? lngs[0] + 360 : lngs[i + 1];
|
|
4507
|
+
if (next - here > widestGap) {
|
|
4508
|
+
widestGap = next - here;
|
|
4509
|
+
cutAfter = i;
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
const minLng = lngs[(cutAfter + 1) % lngs.length];
|
|
4513
|
+
let maxLng = lngs[cutAfter];
|
|
4514
|
+
if (maxLng < minLng) maxLng += 360;
|
|
3289
4515
|
return [
|
|
3290
4516
|
[minLng, minLat],
|
|
3291
4517
|
[maxLng, maxLat]
|
|
@@ -3404,7 +4630,7 @@ var NavigationCamera = class {
|
|
|
3404
4630
|
if (!route || route.coordinates.length === 0) return;
|
|
3405
4631
|
this.clearRecentreTimer();
|
|
3406
4632
|
this.currentMode = "overview";
|
|
3407
|
-
this.m.fitBounds(boundsOf2(route.coordinates), {
|
|
4633
|
+
this.m.fitBounds(boundsOf2(unwrapLngs(route.coordinates)), {
|
|
3408
4634
|
padding: OVERVIEW_PADDING_PX,
|
|
3409
4635
|
pitch: 0,
|
|
3410
4636
|
bearing: 0,
|
|
@@ -3767,10 +4993,20 @@ var IsochroneLayer = class {
|
|
|
3767
4993
|
get current() {
|
|
3768
4994
|
return this.lastData;
|
|
3769
4995
|
}
|
|
3770
|
-
/**
|
|
4996
|
+
/**
|
|
4997
|
+
* Draw (or update) a ring collection. Safe before the style has loaded.
|
|
4998
|
+
* Never silently drops an update: once the source exists the data is
|
|
4999
|
+
* applied immediately, even while `isStyleLoaded()` is transiently
|
|
5000
|
+
* `false` mid-render (PlacesLayer has the same rule) - otherwise the map
|
|
5001
|
+
* would keep showing the previous rings until the next style swap.
|
|
5002
|
+
* Calls made before the source has first been installed are stashed in
|
|
5003
|
+
* `lastData` - the latest wins - and installed on the next `style.load`.
|
|
5004
|
+
*/
|
|
3771
5005
|
draw(collection) {
|
|
3772
5006
|
this.lastData = collection;
|
|
3773
|
-
if (!this.map.isStyleLoaded())
|
|
5007
|
+
if (!this.map.getSource(this.sourceId) && !this.map.isStyleLoaded()) {
|
|
5008
|
+
return;
|
|
5009
|
+
}
|
|
3774
5010
|
this.install(collection);
|
|
3775
5011
|
}
|
|
3776
5012
|
/** Add-or-update the source and ring layers on the current style. */
|
|
@@ -3926,10 +5162,13 @@ var AdrCheck = class {
|
|
|
3926
5162
|
throw new Error("ADR check returned a non-object response");
|
|
3927
5163
|
}
|
|
3928
5164
|
const obj = body;
|
|
3929
|
-
const
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
5165
|
+
const status = obj["status"];
|
|
5166
|
+
if (status !== "allowed" && status !== "blocked") {
|
|
5167
|
+
throw new Error(
|
|
5168
|
+
`ADR check returned an undecodable decision (status ${JSON.stringify(status)}) - refusing to report a non-blocking result for a compliance check`
|
|
5169
|
+
);
|
|
5170
|
+
}
|
|
5171
|
+
const result = { status, raw: obj };
|
|
3933
5172
|
if (typeof obj["reason"] === "string") result.reason = obj["reason"];
|
|
3934
5173
|
return result;
|
|
3935
5174
|
}
|
|
@@ -4008,9 +5247,15 @@ function bannerLanes(banner) {
|
|
|
4008
5247
|
const lane = { directions: c.directions ?? [] };
|
|
4009
5248
|
if (c.active !== void 0) lane.active = c.active;
|
|
4010
5249
|
if (c.active === true) lane.valid = true;
|
|
5250
|
+
if (typeof c.active_direction === "string" && c.active_direction.length > 0) {
|
|
5251
|
+
lane.activeDirection = c.active_direction;
|
|
5252
|
+
}
|
|
4011
5253
|
return lane;
|
|
4012
5254
|
});
|
|
4013
5255
|
}
|
|
5256
|
+
function laneArrowDirection(lane) {
|
|
5257
|
+
return lane.activeDirection ?? lane.directions[0] ?? "none";
|
|
5258
|
+
}
|
|
4014
5259
|
function directionArrow(direction) {
|
|
4015
5260
|
switch (direction) {
|
|
4016
5261
|
case "left":
|
|
@@ -4101,13 +5346,13 @@ var GuidanceBanner = class {
|
|
|
4101
5346
|
laneRow.style.cssText = `margin-left:auto;letter-spacing:${spacing}px;white-space:nowrap`;
|
|
4102
5347
|
for (const lane of lanes) {
|
|
4103
5348
|
const glyph = doc.createElement("span");
|
|
4104
|
-
glyph.textContent = directionArrow(lane
|
|
5349
|
+
glyph.textContent = directionArrow(laneArrowDirection(lane));
|
|
4105
5350
|
glyph.style.opacity = lane.active ? "0.95" : "0.35";
|
|
4106
5351
|
laneRow.appendChild(glyph);
|
|
4107
5352
|
}
|
|
4108
5353
|
} else {
|
|
4109
5354
|
laneRow.style.cssText = "margin-left:auto;letter-spacing:4px";
|
|
4110
|
-
laneRow.textContent = lanes.map((lane) => directionArrow(lane
|
|
5355
|
+
laneRow.textContent = lanes.map((lane) => directionArrow(laneArrowDirection(lane))).join("");
|
|
4111
5356
|
laneRow.style.opacity = "0.9";
|
|
4112
5357
|
}
|
|
4113
5358
|
children.push(laneRow);
|
|
@@ -4143,11 +5388,11 @@ function cameraKindsShownOnMap(design, mode = "browse") {
|
|
|
4143
5388
|
}
|
|
4144
5389
|
function cameraSymbolLayer(design, source, mode = "browse", variant = "dark", id = "safety-cameras") {
|
|
4145
5390
|
const kinds = cameraKindsShownOnMap(design, mode);
|
|
4146
|
-
const
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
iconImage.push(
|
|
5391
|
+
const kind = cameraKindExpression();
|
|
5392
|
+
const iconImage = ["match", kind];
|
|
5393
|
+
for (const canonical of NAV_CAMERA_KINDS) {
|
|
5394
|
+
if (canonical === "unknown") continue;
|
|
5395
|
+
iconImage.push(canonical, cameraSpriteName(canonical, variant));
|
|
4151
5396
|
}
|
|
4152
5397
|
iconImage.push(cameraSpriteName("unknown", variant));
|
|
4153
5398
|
return {
|
|
@@ -4155,7 +5400,7 @@ function cameraSymbolLayer(design, source, mode = "browse", variant = "dark", id
|
|
|
4155
5400
|
type: "symbol",
|
|
4156
5401
|
source,
|
|
4157
5402
|
minzoom: design.mapMinZoom,
|
|
4158
|
-
filter: ["in",
|
|
5403
|
+
filter: ["in", kind, ["literal", kinds]],
|
|
4159
5404
|
layout: {
|
|
4160
5405
|
"icon-image": iconImage,
|
|
4161
5406
|
"icon-size": design.mapIconSize,
|
|
@@ -4164,6 +5409,22 @@ function cameraSymbolLayer(design, source, mode = "browse", variant = "dark", id
|
|
|
4164
5409
|
}
|
|
4165
5410
|
};
|
|
4166
5411
|
}
|
|
5412
|
+
var CAMERA_KIND_WIRE_SPELLINGS = {
|
|
5413
|
+
fixed: ["fixed", "permanent"],
|
|
5414
|
+
average: ["average", "average_speed", "section", "section_control"],
|
|
5415
|
+
red_light: ["red_light", "redlight", "traffic_signals"],
|
|
5416
|
+
mobile_site: ["mobile_site", "mobile"],
|
|
5417
|
+
unknown: ["unknown"]
|
|
5418
|
+
};
|
|
5419
|
+
function cameraKindExpression(property = "kind") {
|
|
5420
|
+
const match = ["match", ["to-string", ["get", property]]];
|
|
5421
|
+
for (const canonical of NAV_CAMERA_KINDS) {
|
|
5422
|
+
if (canonical === "unknown") continue;
|
|
5423
|
+
match.push(CAMERA_KIND_WIRE_SPELLINGS[canonical], canonical);
|
|
5424
|
+
}
|
|
5425
|
+
match.push("unknown");
|
|
5426
|
+
return match;
|
|
5427
|
+
}
|
|
4167
5428
|
function cameraSpriteName(kind, variant = "dark") {
|
|
4168
5429
|
const normalised = navCameraKind(kind);
|
|
4169
5430
|
const slug = normalised === "red_light" ? "redlight" : normalised.replace(/_/g, "-");
|
|
@@ -4426,6 +5687,6 @@ function clampVolume(volume) {
|
|
|
4426
5687
|
return Math.min(1, Math.max(0, volume));
|
|
4427
5688
|
}
|
|
4428
5689
|
|
|
4429
|
-
export { ALERT_CONTRAST_MIN, AdrCheck, CameraAlertChip, 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_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, 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, 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 };
|
|
5690
|
+
export { ALERT_CONTRAST_MIN, AdrCheck, CameraAlertChip, DEFAULT_GLYPHS_URL, DEFAULT_TERRITORY_TILES_URL, EFFECTS_METADATA_KEY, FLOW_DEFAULTS, FULL_ATTRIBUTION, FlowRouteEffectLayer, GuidanceBanner, IsochroneLayer, LOGO_SVG, LogoControl, MARKERS_ID, MARKER_DEFAULT_COLOUR, MARKER_GLYPH_IDS, MARKER_GLYPH_VIEWBOX, MARKER_LABEL_TEXT_SIZES, MARKER_PIN_BODY_PATH, MARKER_SIZES, MARKER_SIZES_PX, MAX_MARKER_ID_LENGTH, MAX_MARKER_IMAGE_BYTES, MAX_MARKER_ITEMS, MAX_MARKER_LABEL_LENGTH, MAX_MARKER_TEXT_LENGTH, MAX_PUCK_IMAGE_BYTES, MapMapMap, MarkersLayer, 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, 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, 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, isMarkerGlyphId, isNameTextField, laneArrowDirection, languageTextField, lngLatToMercator, markerGlyphPaths, markerImage, markerImageDataKey, markerImageKey, markerText, markerTextImageKey, markersFromTheme, markersFromThemeUrl, navAlertLeadDistanceM, navAlertSoundUrl, navCameraKind, navDesignFromTheme, navDesignFromThemeUrl, parseCssColour, parseHexColor, parseMarkers, parseNavAlertsDesign, parseNavDesign, parseOsrmRoute, parsePoiDesign, placesFromGeoJSON, poiDesignFromTheme, poiDesignFromThemeUrl, poiDesignIsDefault, poiTextColorExpression, prefersReducedMotion, registerPmtilesProtocol, relativeLuminance, renderMarkerImage, resetDiagnostics, resolveTheme, runMapDiagnostics, setMapLanguage, severityProsody, shortestArcDeg, shortestArcDelta, speak, ssmlToText, sunTimes, tessellateRouteRibbon, toLngLat, toPmtilesUrl, truncateChars, uploadProbeBatch };
|
|
4430
5691
|
//# sourceMappingURL=index.js.map
|
|
4431
5692
|
//# sourceMappingURL=index.js.map
|