@mapmap/maps 0.6.0 → 0.8.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 +221 -16
- 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 +721 -24
- package/dist/index.js +1151 -85
- 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,20 @@ 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();
|
|
1894
|
+
// Enforcement notices already logged for THIS map. Per instance, not
|
|
1895
|
+
// per page (diagnostics.ts dedups per page because a duplicated
|
|
1896
|
+
// maplibre-gl is one page-level fault): each map that asks for a
|
|
1897
|
+
// removal is its own licence problem and deserves its own line, but a
|
|
1898
|
+
// theme swap or re-render must never repeat one.
|
|
1899
|
+
this.notices = /* @__PURE__ */ new Set();
|
|
1873
1900
|
this.handleStyleLoadForEffects = () => {
|
|
1901
|
+
if (this.destroyed) return;
|
|
1874
1902
|
if (!this.effectExplicit) {
|
|
1875
1903
|
const block = effectsFromStyleMetadata(this.styleMetadata());
|
|
1876
1904
|
this.effectName = block?.route ?? null;
|
|
@@ -1886,21 +1914,36 @@ var MapMapMap = class {
|
|
|
1886
1914
|
this.navDesign = navDesignFromTheme(theme);
|
|
1887
1915
|
this.poiDesign = poiDesignFromTheme(theme);
|
|
1888
1916
|
const style = resolveStyle(options.style, options.territoryTilesUrl);
|
|
1917
|
+
const requestedAttribution = options.mapOptions?.attributionControl;
|
|
1918
|
+
if (requestedAttribution === false) {
|
|
1919
|
+
this.notice(
|
|
1920
|
+
"attribution-removal",
|
|
1921
|
+
"mapOptions.attributionControl: false is ignored - the OpenStreetMap attribution stays on. The credit is a condition of the ODbL that the map data is served under, for MapMap and for you. Pass { compact: true } to collapse it, or { customAttribution } to add your own credit alongside it."
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
const attributionControl = {
|
|
1925
|
+
compact: false,
|
|
1926
|
+
...requestedAttribution === false ? void 0 : requestedAttribution
|
|
1927
|
+
};
|
|
1889
1928
|
this.map = new maplibregl.Map({
|
|
1890
1929
|
container: options.container,
|
|
1891
1930
|
style,
|
|
1892
1931
|
center: options.center ?? DEFAULT_CENTER,
|
|
1893
1932
|
zoom: options.zoom ?? DEFAULT_ZOOM,
|
|
1894
|
-
|
|
1895
|
-
|
|
1933
|
+
...options.mapOptions,
|
|
1934
|
+
attributionControl
|
|
1896
1935
|
});
|
|
1897
|
-
if (options.logo
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
logoOptions.position ?? "bottom-right"
|
|
1936
|
+
if (options.logo === false) {
|
|
1937
|
+
this.notice(
|
|
1938
|
+
"logo-removal",
|
|
1939
|
+
"logo: false is ignored - the MapMap mark stays on the map. Carrying the mark is a condition of the @mapmap/maps licence. Pass logo: { position } to move it to another corner."
|
|
1902
1940
|
);
|
|
1903
1941
|
}
|
|
1942
|
+
const logoOptions = typeof options.logo === "object" && options.logo !== null ? options.logo : {};
|
|
1943
|
+
this.map.addControl(
|
|
1944
|
+
new LogoControl(logoOptions),
|
|
1945
|
+
logoOptions.position ?? "bottom-right"
|
|
1946
|
+
);
|
|
1904
1947
|
this.map.on("style.load", this.handleStyleLoadForEffects);
|
|
1905
1948
|
runMapDiagnostics({
|
|
1906
1949
|
container: options.container,
|
|
@@ -1909,6 +1952,48 @@ var MapMapMap = class {
|
|
|
1909
1952
|
apiKey: this.apiKey
|
|
1910
1953
|
});
|
|
1911
1954
|
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Log an enforcement notice at most once for this map. A warning, not
|
|
1957
|
+
* an error and never a throw: `logo: false` and
|
|
1958
|
+
* `attributionControl: false` are published, documented options, and a
|
|
1959
|
+
* hard failure would break a customer's build on a patch upgrade.
|
|
1960
|
+
*/
|
|
1961
|
+
notice(notice, message) {
|
|
1962
|
+
if (this.notices.has(notice)) return;
|
|
1963
|
+
this.notices.add(notice);
|
|
1964
|
+
console.warn(`MapMap [${notice}]: ${message}`);
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Run `action` as soon as the style can take layer edits, or now if it
|
|
1968
|
+
* already can.
|
|
1969
|
+
*
|
|
1970
|
+
* Never wait on `load` alone: MapLibre fires it exactly once per map, so
|
|
1971
|
+
* anything deferred after the first load is dropped for good - and
|
|
1972
|
+
* `isStyleLoaded()` reports false for the whole of every `style.load`
|
|
1973
|
+
* (the new style's sources are still loading) and whenever a source is
|
|
1974
|
+
* loading afterwards. Waiting for whichever of `style.load` / `idle` /
|
|
1975
|
+
* `load` arrives first covers the first load and every later style swap,
|
|
1976
|
+
* and the listeners detach as soon as one fires (or on `destroy()`).
|
|
1977
|
+
*/
|
|
1978
|
+
whenStyleReady(action) {
|
|
1979
|
+
if (this.destroyed) return;
|
|
1980
|
+
if (this.map.isStyleLoaded()) {
|
|
1981
|
+
action();
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
const events = ["style.load", "idle", "load"];
|
|
1985
|
+
const run = () => {
|
|
1986
|
+
cleanup();
|
|
1987
|
+
if (this.destroyed) return;
|
|
1988
|
+
action();
|
|
1989
|
+
};
|
|
1990
|
+
const cleanup = () => {
|
|
1991
|
+
this.pendingWaits.delete(cleanup);
|
|
1992
|
+
for (const event of events) this.map.off(event, run);
|
|
1993
|
+
};
|
|
1994
|
+
this.pendingWaits.add(cleanup);
|
|
1995
|
+
for (const event of events) this.map.on(event, run);
|
|
1996
|
+
}
|
|
1912
1997
|
/** The current style's `metadata`, if it can be read yet. */
|
|
1913
1998
|
styleMetadata() {
|
|
1914
1999
|
try {
|
|
@@ -1948,10 +2033,10 @@ var MapMapMap = class {
|
|
|
1948
2033
|
setRouteEffect(effect, options = {}) {
|
|
1949
2034
|
this.effectExplicit = true;
|
|
1950
2035
|
const { geometry, ...flowOptions } = options;
|
|
2036
|
+
this.removeEffectLayer();
|
|
1951
2037
|
this.effectName = effect;
|
|
1952
2038
|
this.effectOptions = flowOptions;
|
|
1953
2039
|
if (geometry) this.effectGeometry = geometry;
|
|
1954
|
-
this.removeEffectLayer();
|
|
1955
2040
|
this.applyRouteEffect();
|
|
1956
2041
|
}
|
|
1957
2042
|
/**
|
|
@@ -1984,7 +2069,12 @@ var MapMapMap = class {
|
|
|
1984
2069
|
return;
|
|
1985
2070
|
}
|
|
1986
2071
|
if (!this.map.isStyleLoaded()) {
|
|
1987
|
-
|
|
2072
|
+
if (this.effectWaiting) return;
|
|
2073
|
+
this.effectWaiting = true;
|
|
2074
|
+
this.whenStyleReady(() => {
|
|
2075
|
+
this.effectWaiting = false;
|
|
2076
|
+
this.applyRouteEffect();
|
|
2077
|
+
});
|
|
1988
2078
|
return;
|
|
1989
2079
|
}
|
|
1990
2080
|
if (this.effectLayer && this.map.getLayer(id)) {
|
|
@@ -2013,7 +2103,7 @@ var MapMapMap = class {
|
|
|
2013
2103
|
*/
|
|
2014
2104
|
setBuildings3d(enabled) {
|
|
2015
2105
|
if (!this.map.isStyleLoaded()) {
|
|
2016
|
-
this.
|
|
2106
|
+
this.whenStyleReady(() => this.setBuildings3d(enabled));
|
|
2017
2107
|
return;
|
|
2018
2108
|
}
|
|
2019
2109
|
if (enabled === (this.map.getLayer("building-3d") !== void 0)) return;
|
|
@@ -2036,18 +2126,45 @@ var MapMapMap = class {
|
|
|
2036
2126
|
this.map.setLayerZoomRange("building", minzoom, 24);
|
|
2037
2127
|
}
|
|
2038
2128
|
}
|
|
2039
|
-
/**
|
|
2129
|
+
/**
|
|
2130
|
+
* Resolves once the style and first tiles are loaded.
|
|
2131
|
+
*
|
|
2132
|
+
* Safe to call at any point in the map's life, not just before its
|
|
2133
|
+
* first load: `map.loaded()` goes false again whenever a source is
|
|
2134
|
+
* loading (a theme swap, a pan into new tiles) while `load` only ever
|
|
2135
|
+
* fires once, so this waits on `idle` too rather than hanging.
|
|
2136
|
+
*/
|
|
2040
2137
|
whenReady() {
|
|
2041
2138
|
return new Promise((resolve) => {
|
|
2042
2139
|
if (this.map.loaded()) {
|
|
2043
2140
|
resolve();
|
|
2044
2141
|
return;
|
|
2045
2142
|
}
|
|
2046
|
-
|
|
2143
|
+
const events = ["load", "idle"];
|
|
2144
|
+
const done = () => {
|
|
2145
|
+
for (const event of events) this.map.off(event, done);
|
|
2146
|
+
this.pendingWaits.delete(cleanup);
|
|
2147
|
+
resolve();
|
|
2148
|
+
};
|
|
2149
|
+
const cleanup = () => {
|
|
2150
|
+
for (const event of events) this.map.off(event, done);
|
|
2151
|
+
this.pendingWaits.delete(cleanup);
|
|
2152
|
+
};
|
|
2153
|
+
this.pendingWaits.add(cleanup);
|
|
2154
|
+
for (const event of events) this.map.on(event, done);
|
|
2047
2155
|
});
|
|
2048
2156
|
}
|
|
2049
|
-
/**
|
|
2157
|
+
/**
|
|
2158
|
+
* Remove the map and release its WebGL context. Anything still queued
|
|
2159
|
+
* behind a style load is dropped first, so nothing touches the map
|
|
2160
|
+
* after this returns.
|
|
2161
|
+
*/
|
|
2050
2162
|
destroy() {
|
|
2163
|
+
this.destroyed = true;
|
|
2164
|
+
for (const cleanup of [...this.pendingWaits]) cleanup();
|
|
2165
|
+
this.pendingWaits.clear();
|
|
2166
|
+
this.effectWaiting = false;
|
|
2167
|
+
this.effectLayer = void 0;
|
|
2051
2168
|
this.map.remove();
|
|
2052
2169
|
}
|
|
2053
2170
|
};
|
|
@@ -2153,11 +2270,540 @@ function numberOr(value, fallback) {
|
|
|
2153
2270
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2154
2271
|
}
|
|
2155
2272
|
|
|
2273
|
+
// src/marker-icons.ts
|
|
2274
|
+
var MARKER_GLYPH_IDS = [
|
|
2275
|
+
"pin",
|
|
2276
|
+
"dot",
|
|
2277
|
+
"star",
|
|
2278
|
+
"heart",
|
|
2279
|
+
"flag",
|
|
2280
|
+
"home",
|
|
2281
|
+
"work",
|
|
2282
|
+
"food",
|
|
2283
|
+
"cafe",
|
|
2284
|
+
"bar",
|
|
2285
|
+
"shop",
|
|
2286
|
+
"hotel",
|
|
2287
|
+
"parking",
|
|
2288
|
+
"fuel",
|
|
2289
|
+
"charging",
|
|
2290
|
+
"transit",
|
|
2291
|
+
"bike",
|
|
2292
|
+
"camera",
|
|
2293
|
+
"music",
|
|
2294
|
+
"warning",
|
|
2295
|
+
"info"
|
|
2296
|
+
];
|
|
2297
|
+
function isMarkerGlyphId(value) {
|
|
2298
|
+
return typeof value === "string" && MARKER_GLYPH_IDS.includes(value);
|
|
2299
|
+
}
|
|
2300
|
+
var markerGlyphPaths = {
|
|
2301
|
+
pin: "M7 12a5 5 0 1 1 10 0a5 5 0 1 1-10 0Z",
|
|
2302
|
+
dot: "M4 12a8 8 0 1 1 16 0a8 8 0 1 1-16 0Z",
|
|
2303
|
+
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",
|
|
2304
|
+
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",
|
|
2305
|
+
flag: "M5.5 2.5h2v19h-2V2.5Zm2 2H19l-2.8 4l2.8 4H7.5v-8Z",
|
|
2306
|
+
home: "M12 3.2l9 7.6h-2.6v9.7h-4.9v-5.9h-3v5.9H5.6v-9.7H3l9-7.6Z",
|
|
2307
|
+
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",
|
|
2308
|
+
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",
|
|
2309
|
+
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",
|
|
2310
|
+
bar: "M3.5 4h17l-7.2 8.2V19h3.7v2H7v-2h3.3v-6.8L3.5 4Z",
|
|
2311
|
+
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",
|
|
2312
|
+
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",
|
|
2313
|
+
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",
|
|
2314
|
+
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",
|
|
2315
|
+
charging: "M13.4 2L5.2 13.2h4.9L9 22l8.3-11.4h-5L13.4 2Z",
|
|
2316
|
+
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",
|
|
2317
|
+
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",
|
|
2318
|
+
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",
|
|
2319
|
+
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",
|
|
2320
|
+
warning: "M12 2.8L22.3 20.5H1.7L12 2.8Zm-1 6.6v5.4h2V9.4h-2Zm0 7.2v2.2h2v-2.2h-2Z",
|
|
2321
|
+
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"
|
|
2322
|
+
};
|
|
2323
|
+
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";
|
|
2324
|
+
var MARKER_GLYPH_VIEWBOX = 24;
|
|
2325
|
+
|
|
2326
|
+
// src/markers.ts
|
|
2327
|
+
var MARKER_SIZES = ["s", "m", "l"];
|
|
2328
|
+
var MARKER_SIZES_PX = {
|
|
2329
|
+
s: 24,
|
|
2330
|
+
m: 32,
|
|
2331
|
+
l: 40
|
|
2332
|
+
};
|
|
2333
|
+
var MARKER_LABEL_TEXT_SIZES = {
|
|
2334
|
+
s: 12,
|
|
2335
|
+
m: 13,
|
|
2336
|
+
l: 14
|
|
2337
|
+
};
|
|
2338
|
+
var MARKER_DEFAULT_COLOUR = "#1a6bff";
|
|
2339
|
+
var MAX_MARKER_IMAGE_BYTES = 64 * 1024;
|
|
2340
|
+
var MAX_MARKER_ITEMS = 200;
|
|
2341
|
+
var MAX_MARKER_ID_LENGTH = 64;
|
|
2342
|
+
var MAX_MARKER_LABEL_LENGTH = 120;
|
|
2343
|
+
var MAX_MARKER_TEXT_LENGTH = 3;
|
|
2344
|
+
var MARKERS_ID = "mm-user-markers";
|
|
2345
|
+
var MARKER_IMAGE_MIME = /^data:image\/(png|jpeg|webp|svg\+xml);base64,/;
|
|
2346
|
+
function isRecord3(v) {
|
|
2347
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2348
|
+
}
|
|
2349
|
+
function boundedNumber(v, min, max) {
|
|
2350
|
+
return typeof v === "number" && Number.isFinite(v) && v >= min && v <= max ? v : void 0;
|
|
2351
|
+
}
|
|
2352
|
+
function markerColour(v) {
|
|
2353
|
+
return typeof v === "string" && /^#[0-9a-fA-F]{6}$/.test(v.trim()) ? v.trim().toLowerCase() : MARKER_DEFAULT_COLOUR;
|
|
2354
|
+
}
|
|
2355
|
+
function dataUriBytes(uri) {
|
|
2356
|
+
const comma = uri.indexOf(",");
|
|
2357
|
+
const payload = comma === -1 ? "" : uri.slice(comma + 1);
|
|
2358
|
+
const meta = comma === -1 ? uri : uri.slice(0, comma);
|
|
2359
|
+
if (!/;base64$/i.test(meta) && !/;base64;/i.test(meta)) return payload.length;
|
|
2360
|
+
return Math.floor(payload.replace(/=+$/, "").length * 3 / 4);
|
|
2361
|
+
}
|
|
2362
|
+
function truncateChars(text, max) {
|
|
2363
|
+
const chars = Array.from(text);
|
|
2364
|
+
return chars.length <= max ? text : chars.slice(0, max).join("");
|
|
2365
|
+
}
|
|
2366
|
+
function markerImage(v) {
|
|
2367
|
+
if (typeof v !== "string") return void 0;
|
|
2368
|
+
const uri = v.trim();
|
|
2369
|
+
if (!MARKER_IMAGE_MIME.test(uri)) return void 0;
|
|
2370
|
+
if (dataUriBytes(uri) > MAX_MARKER_IMAGE_BYTES) return void 0;
|
|
2371
|
+
return uri;
|
|
2372
|
+
}
|
|
2373
|
+
function parseMarkers(value) {
|
|
2374
|
+
const block = isRecord3(value) ? value : {};
|
|
2375
|
+
const items = [];
|
|
2376
|
+
if (block.version !== 1) return { version: 1, items };
|
|
2377
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2378
|
+
const rawItems = Array.isArray(block.items) ? block.items : [];
|
|
2379
|
+
for (const raw of rawItems) {
|
|
2380
|
+
if (items.length >= MAX_MARKER_ITEMS) break;
|
|
2381
|
+
if (!isRecord3(raw)) continue;
|
|
2382
|
+
const id = typeof raw.id === "string" ? raw.id : void 0;
|
|
2383
|
+
if (!id || id.length === 0 || Array.from(id).length > MAX_MARKER_ID_LENGTH) continue;
|
|
2384
|
+
if (seen.has(id)) continue;
|
|
2385
|
+
const lng = boundedNumber(raw.lng, -180, 180);
|
|
2386
|
+
const lat = boundedNumber(raw.lat, -90, 90);
|
|
2387
|
+
if (lng === void 0 || lat === void 0) continue;
|
|
2388
|
+
seen.add(id);
|
|
2389
|
+
const item = {
|
|
2390
|
+
id,
|
|
2391
|
+
lng,
|
|
2392
|
+
lat,
|
|
2393
|
+
icon: isMarkerGlyphId(raw.icon) ? raw.icon : "pin",
|
|
2394
|
+
colour: markerColour(raw.colour),
|
|
2395
|
+
size: raw.size === "s" || raw.size === "l" ? raw.size : "m"
|
|
2396
|
+
};
|
|
2397
|
+
const image = markerImage(raw.image);
|
|
2398
|
+
if (image !== void 0) item.image = image;
|
|
2399
|
+
if (typeof raw.label === "string" && raw.label.length > 0) {
|
|
2400
|
+
item.label = truncateChars(raw.label, MAX_MARKER_LABEL_LENGTH);
|
|
2401
|
+
}
|
|
2402
|
+
items.push(item);
|
|
2403
|
+
}
|
|
2404
|
+
return { version: 1, items };
|
|
2405
|
+
}
|
|
2406
|
+
function markersFromTheme(theme) {
|
|
2407
|
+
const extra = theme?.extra;
|
|
2408
|
+
if (typeof extra !== "object" || extra === null || !("markers" in extra)) {
|
|
2409
|
+
return void 0;
|
|
2410
|
+
}
|
|
2411
|
+
return parseMarkers(extra.markers);
|
|
2412
|
+
}
|
|
2413
|
+
async function markersFromThemeUrl(url, fetchImpl) {
|
|
2414
|
+
const doFetch = fetchImpl ?? ((input) => globalThis.fetch(input));
|
|
2415
|
+
const response = await doFetch(url);
|
|
2416
|
+
if (!response.ok) {
|
|
2417
|
+
throw new Error(`fetching theme ${url}: HTTP ${response.status}`);
|
|
2418
|
+
}
|
|
2419
|
+
const theme = await response.json();
|
|
2420
|
+
return markersFromTheme(theme);
|
|
2421
|
+
}
|
|
2422
|
+
function markerImageKey(icon, colour2, size) {
|
|
2423
|
+
return `mm-marker-${icon}-${colour2.toLowerCase()}-${size}`;
|
|
2424
|
+
}
|
|
2425
|
+
function markerText(value) {
|
|
2426
|
+
if (typeof value !== "string") return void 0;
|
|
2427
|
+
const text = truncateChars(value.trim(), MAX_MARKER_TEXT_LENGTH);
|
|
2428
|
+
return text.length > 0 ? text : void 0;
|
|
2429
|
+
}
|
|
2430
|
+
function markerTextImageKey(text, colour2, size) {
|
|
2431
|
+
return `mm-marker-text-${text}-${colour2.toLowerCase()}-${size}`;
|
|
2432
|
+
}
|
|
2433
|
+
function fnv1a(text) {
|
|
2434
|
+
let hash = 2166136261;
|
|
2435
|
+
for (let i = 0; i < text.length; i++) {
|
|
2436
|
+
hash ^= text.charCodeAt(i);
|
|
2437
|
+
hash = Math.imul(hash, 16777619);
|
|
2438
|
+
}
|
|
2439
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
2440
|
+
}
|
|
2441
|
+
function markerImageDataKey(dataUri) {
|
|
2442
|
+
return `mm-marker-image-${fnv1a(dataUri)}`;
|
|
2443
|
+
}
|
|
2444
|
+
var GLYPH_FRACTION = 0.62;
|
|
2445
|
+
var PIN_HEAD_CENTRE_Y = 9.4;
|
|
2446
|
+
var PIN_HEAD_RADIUS = 9;
|
|
2447
|
+
var DOT_RADIUS = 8;
|
|
2448
|
+
var DOT_STROKE = 2;
|
|
2449
|
+
var PIN_TEXT_FONT_STACK = "system-ui, sans-serif";
|
|
2450
|
+
function pinTextStartSize(length) {
|
|
2451
|
+
if (length <= 1) return 10;
|
|
2452
|
+
return length === 2 ? 8.2 : 6.6;
|
|
2453
|
+
}
|
|
2454
|
+
var PIN_TEXT_MIN_SIZE = 4.5;
|
|
2455
|
+
var PIN_TEXT_WIDTH_FRACTION = 0.78;
|
|
2456
|
+
function renderMarkerImage(options = {}) {
|
|
2457
|
+
const text = markerText(options.text);
|
|
2458
|
+
const icon = options.icon ?? "pin";
|
|
2459
|
+
const colour2 = options.colour ?? MARKER_DEFAULT_COLOUR;
|
|
2460
|
+
const sizePx = MARKER_SIZES_PX[options.size ?? "m"];
|
|
2461
|
+
const g = globalThis;
|
|
2462
|
+
if (!g.document || !g.Path2D) return null;
|
|
2463
|
+
const side = sizePx * 2;
|
|
2464
|
+
const canvas = g.document.createElement("canvas");
|
|
2465
|
+
canvas.width = side;
|
|
2466
|
+
canvas.height = side;
|
|
2467
|
+
const ctx = canvas.getContext("2d");
|
|
2468
|
+
if (!ctx) return null;
|
|
2469
|
+
const scale = side / MARKER_GLYPH_VIEWBOX;
|
|
2470
|
+
if (icon === "dot" && text === void 0) {
|
|
2471
|
+
const cx = side / 2;
|
|
2472
|
+
const cy = side / 2;
|
|
2473
|
+
const r = DOT_RADIUS * scale;
|
|
2474
|
+
ctx.beginPath();
|
|
2475
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
2476
|
+
ctx.fillStyle = colour2;
|
|
2477
|
+
ctx.fill();
|
|
2478
|
+
ctx.lineWidth = DOT_STROKE * scale;
|
|
2479
|
+
ctx.strokeStyle = "#ffffff";
|
|
2480
|
+
ctx.stroke();
|
|
2481
|
+
return { image: ctx.getImageData(0, 0, side, side), pixelRatio: 2 };
|
|
2482
|
+
}
|
|
2483
|
+
ctx.save();
|
|
2484
|
+
ctx.scale(scale, scale);
|
|
2485
|
+
ctx.fillStyle = colour2;
|
|
2486
|
+
ctx.fill(new g.Path2D(MARKER_PIN_BODY_PATH));
|
|
2487
|
+
ctx.restore();
|
|
2488
|
+
if (text !== void 0) {
|
|
2489
|
+
const maxWidth = PIN_HEAD_RADIUS * 2 * PIN_TEXT_WIDTH_FRACTION * scale;
|
|
2490
|
+
let unitSize = pinTextStartSize(text.length);
|
|
2491
|
+
ctx.textAlign = "center";
|
|
2492
|
+
ctx.textBaseline = "middle";
|
|
2493
|
+
ctx.fillStyle = "#ffffff";
|
|
2494
|
+
const canMeasure = typeof ctx.measureText === "function";
|
|
2495
|
+
for (; ; ) {
|
|
2496
|
+
ctx.font = `bold ${unitSize * scale}px ${PIN_TEXT_FONT_STACK}`;
|
|
2497
|
+
if (!canMeasure || unitSize <= PIN_TEXT_MIN_SIZE) break;
|
|
2498
|
+
if (ctx.measureText(text).width <= maxWidth) break;
|
|
2499
|
+
unitSize -= 0.5;
|
|
2500
|
+
}
|
|
2501
|
+
ctx.fillText(text, side / 2, PIN_HEAD_CENTRE_Y * scale);
|
|
2502
|
+
return { image: ctx.getImageData(0, 0, side, side), pixelRatio: 2 };
|
|
2503
|
+
}
|
|
2504
|
+
const glyphSide = PIN_HEAD_RADIUS * 2 * GLYPH_FRACTION * scale;
|
|
2505
|
+
const glyphScale = glyphSide / MARKER_GLYPH_VIEWBOX;
|
|
2506
|
+
ctx.save();
|
|
2507
|
+
ctx.translate(
|
|
2508
|
+
side / 2 - glyphSide / 2,
|
|
2509
|
+
PIN_HEAD_CENTRE_Y * scale - glyphSide / 2
|
|
2510
|
+
);
|
|
2511
|
+
ctx.scale(glyphScale, glyphScale);
|
|
2512
|
+
ctx.fillStyle = "#ffffff";
|
|
2513
|
+
ctx.fill(new g.Path2D(markerGlyphPaths[icon]));
|
|
2514
|
+
ctx.restore();
|
|
2515
|
+
return { image: ctx.getImageData(0, 0, side, side), pixelRatio: 2 };
|
|
2516
|
+
}
|
|
2517
|
+
var TEXT_OFFSET_EXPRESSION = [
|
|
2518
|
+
"match",
|
|
2519
|
+
["get", "offsetKey"],
|
|
2520
|
+
"tip",
|
|
2521
|
+
["literal", [0, 0.4]],
|
|
2522
|
+
"cs",
|
|
2523
|
+
["literal", [0, 1.3]],
|
|
2524
|
+
"cm",
|
|
2525
|
+
["literal", [0, 1.55]],
|
|
2526
|
+
"cl",
|
|
2527
|
+
["literal", [0, 1.75]],
|
|
2528
|
+
["literal", [0, 0.4]]
|
|
2529
|
+
];
|
|
2530
|
+
var MARKER_TEXT_FONT = "MapMap Sans Regular";
|
|
2531
|
+
var MarkersLayer = class {
|
|
2532
|
+
constructor(map, markers, options = {}) {
|
|
2533
|
+
this.items = [];
|
|
2534
|
+
this.destroyed = false;
|
|
2535
|
+
/**
|
|
2536
|
+
* Bumped by {@link clear} (and {@link destroy}). An image load started
|
|
2537
|
+
* before the bump resolves into a cleared layer, so its handler compares
|
|
2538
|
+
* the generation it was started in and drops out — without this a
|
|
2539
|
+
* pending `loadImage` would rebuild and RE-INSTALL the layer on a map the
|
|
2540
|
+
* caller had already cleaned up.
|
|
2541
|
+
*/
|
|
2542
|
+
this.generation = 0;
|
|
2543
|
+
/** Custom images by cache key, once loaded (kept across style swaps). */
|
|
2544
|
+
this.loadedImages = /* @__PURE__ */ new Map();
|
|
2545
|
+
/** Custom-image cache keys that failed to load (pin fallback, once). */
|
|
2546
|
+
this.failedImages = /* @__PURE__ */ new Set();
|
|
2547
|
+
/** Custom-image cache keys with a load in flight. */
|
|
2548
|
+
this.pendingImages = /* @__PURE__ */ new Set();
|
|
2549
|
+
/**
|
|
2550
|
+
* Every `map.addImage` key this layer currently owns, so a rebuild can
|
|
2551
|
+
* drop the ones nothing references any more and teardown removes exactly
|
|
2552
|
+
* what it added (the RouteLayer endpoints pattern). Without it every
|
|
2553
|
+
* icon/colour/size change leaked a 2x RGBA image — 25 KB per click at
|
|
2554
|
+
* the 40px preset — and re-invalidated the sprite atlas.
|
|
2555
|
+
*/
|
|
2556
|
+
this.registeredImageKeys = /* @__PURE__ */ new Set();
|
|
2557
|
+
this.handleStyleLoad = () => {
|
|
2558
|
+
if (this.data) this.install();
|
|
2559
|
+
};
|
|
2560
|
+
this.map = map instanceof MapMapMap ? map.map : map;
|
|
2561
|
+
this.options = options;
|
|
2562
|
+
this.map.on("style.load", this.handleStyleLoad);
|
|
2563
|
+
if (markers) this.setMarkers(markers);
|
|
2564
|
+
}
|
|
2565
|
+
/**
|
|
2566
|
+
* Replace the layer's markers with a parsed block (`undefined` or an
|
|
2567
|
+
* empty block clears the map of markers but keeps the layer alive).
|
|
2568
|
+
* Custom images load asynchronously: their markers render as default
|
|
2569
|
+
* pins until the image arrives, then swap in place; a failed or
|
|
2570
|
+
* oversized image keeps the pin fallback.
|
|
2571
|
+
*/
|
|
2572
|
+
setMarkers(markers) {
|
|
2573
|
+
this.items = markers?.items ?? [];
|
|
2574
|
+
this.rebuild();
|
|
2575
|
+
}
|
|
2576
|
+
/** The layer's current markers. */
|
|
2577
|
+
get current() {
|
|
2578
|
+
return this.items;
|
|
2579
|
+
}
|
|
2580
|
+
/**
|
|
2581
|
+
* The MapLibre source/layer ids - public API for escape-hatch styling.
|
|
2582
|
+
* Both are the contract id `mm-user-markers`.
|
|
2583
|
+
*/
|
|
2584
|
+
get ids() {
|
|
2585
|
+
return { source: MARKERS_ID, layer: MARKERS_ID };
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Rebuild the source data from the items and (re)install. Mirrors
|
|
2589
|
+
* PlacesLayer's never-drop-an-update rule: once the source exists the
|
|
2590
|
+
* data is applied immediately (even while `isStyleLoaded()` is
|
|
2591
|
+
* transiently false mid-render); before first installation the latest
|
|
2592
|
+
* data is stashed for the next `style.load`.
|
|
2593
|
+
*/
|
|
2594
|
+
rebuild() {
|
|
2595
|
+
this.data = {
|
|
2596
|
+
type: "FeatureCollection",
|
|
2597
|
+
features: this.items.map((item) => this.featureFor(item))
|
|
2598
|
+
};
|
|
2599
|
+
const source = this.map.getSource(MARKERS_ID);
|
|
2600
|
+
if (source) {
|
|
2601
|
+
this.registerImages();
|
|
2602
|
+
source.setData(this.data);
|
|
2603
|
+
} else if (this.map.isStyleLoaded()) {
|
|
2604
|
+
this.install();
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
/** Build one feature, kicking off the custom-image load if needed. */
|
|
2608
|
+
featureFor(item) {
|
|
2609
|
+
const sizePx = MARKER_SIZES_PX[item.size];
|
|
2610
|
+
const textSize = MARKER_LABEL_TEXT_SIZES[item.size];
|
|
2611
|
+
let iconImage;
|
|
2612
|
+
let iconSize;
|
|
2613
|
+
let anchor;
|
|
2614
|
+
const imageKey = item.image !== void 0 ? markerImageDataKey(item.image) : void 0;
|
|
2615
|
+
const loaded = imageKey !== void 0 ? this.loadedImages.get(imageKey) : void 0;
|
|
2616
|
+
if (item.image !== void 0 && imageKey !== void 0 && !this.failedImages.has(imageKey)) {
|
|
2617
|
+
if (loaded) {
|
|
2618
|
+
iconImage = imageKey;
|
|
2619
|
+
iconSize = loaded.data.width > 0 ? sizePx / loaded.data.width : 1;
|
|
2620
|
+
anchor = "center";
|
|
2621
|
+
} else {
|
|
2622
|
+
this.loadCustomImage(imageKey, item.image);
|
|
2623
|
+
iconImage = markerImageKey(item.icon, item.colour, item.size);
|
|
2624
|
+
iconSize = 1;
|
|
2625
|
+
anchor = item.icon === "dot" ? "center" : "bottom";
|
|
2626
|
+
}
|
|
2627
|
+
} else {
|
|
2628
|
+
iconImage = markerImageKey(item.icon, item.colour, item.size);
|
|
2629
|
+
iconSize = 1;
|
|
2630
|
+
anchor = item.icon === "dot" ? "center" : "bottom";
|
|
2631
|
+
}
|
|
2632
|
+
const properties = {
|
|
2633
|
+
id: item.id,
|
|
2634
|
+
iconImage,
|
|
2635
|
+
iconSize,
|
|
2636
|
+
iconAnchor: anchor,
|
|
2637
|
+
textSize,
|
|
2638
|
+
offsetKey: anchor === "bottom" ? "tip" : `c${item.size}`
|
|
2639
|
+
};
|
|
2640
|
+
if (item.label !== void 0) properties["label"] = item.label;
|
|
2641
|
+
return {
|
|
2642
|
+
type: "Feature",
|
|
2643
|
+
geometry: { type: "Point", coordinates: [item.lng, item.lat] },
|
|
2644
|
+
properties
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
/** Load a custom `data:` image once; rebuild the features when done. */
|
|
2648
|
+
loadCustomImage(key, uri) {
|
|
2649
|
+
if (this.pendingImages.has(key)) return;
|
|
2650
|
+
this.pendingImages.add(key);
|
|
2651
|
+
const generation = this.generation;
|
|
2652
|
+
const stale = () => this.destroyed || generation !== this.generation;
|
|
2653
|
+
void this.map.loadImage(uri).then((image) => {
|
|
2654
|
+
this.pendingImages.delete(key);
|
|
2655
|
+
if (stale()) return;
|
|
2656
|
+
this.loadedImages.set(key, {
|
|
2657
|
+
data: image.data,
|
|
2658
|
+
raw: image.data
|
|
2659
|
+
});
|
|
2660
|
+
this.rebuild();
|
|
2661
|
+
}).catch((error) => {
|
|
2662
|
+
this.pendingImages.delete(key);
|
|
2663
|
+
if (stale()) return;
|
|
2664
|
+
this.failedImages.add(key);
|
|
2665
|
+
this.options.onImageError?.(uri, error);
|
|
2666
|
+
this.rebuild();
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* Register every image the current features reference and drop the ones
|
|
2671
|
+
* they no longer do (idempotent). Runtime images survive a diffed
|
|
2672
|
+
* `setStyle` and are rebuilt after a full reload, so this runs on every
|
|
2673
|
+
* install as well as every data rebuild.
|
|
2674
|
+
*/
|
|
2675
|
+
registerImages() {
|
|
2676
|
+
const needed = /* @__PURE__ */ new Set();
|
|
2677
|
+
for (const item of this.items) {
|
|
2678
|
+
const imageKey = item.image !== void 0 ? markerImageDataKey(item.image) : void 0;
|
|
2679
|
+
const loaded = imageKey !== void 0 ? this.loadedImages.get(imageKey) : void 0;
|
|
2680
|
+
if (imageKey !== void 0 && loaded && !this.failedImages.has(imageKey)) {
|
|
2681
|
+
needed.add(imageKey);
|
|
2682
|
+
if (!this.map.hasImage(imageKey)) {
|
|
2683
|
+
this.map.addImage(imageKey, loaded.raw);
|
|
2684
|
+
}
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
const key = markerImageKey(item.icon, item.colour, item.size);
|
|
2688
|
+
needed.add(key);
|
|
2689
|
+
if (this.map.hasImage(key)) continue;
|
|
2690
|
+
const rendered = renderMarkerImage({
|
|
2691
|
+
icon: item.icon,
|
|
2692
|
+
colour: item.colour,
|
|
2693
|
+
size: item.size
|
|
2694
|
+
});
|
|
2695
|
+
if (rendered) {
|
|
2696
|
+
this.map.addImage(key, rendered.image, { pixelRatio: rendered.pixelRatio });
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
for (const key of this.registeredImageKeys) {
|
|
2700
|
+
if (needed.has(key)) continue;
|
|
2701
|
+
if (this.map.hasImage(key)) this.map.removeImage(key);
|
|
2702
|
+
this.registeredImageKeys.delete(key);
|
|
2703
|
+
}
|
|
2704
|
+
for (const key of needed) this.registeredImageKeys.add(key);
|
|
2705
|
+
}
|
|
2706
|
+
/** Add-or-update the images, source and layer on the current style. */
|
|
2707
|
+
install() {
|
|
2708
|
+
if (!this.data) return;
|
|
2709
|
+
this.registerImages();
|
|
2710
|
+
const existing = this.map.getSource(MARKERS_ID);
|
|
2711
|
+
if (existing) {
|
|
2712
|
+
existing.setData(this.data);
|
|
2713
|
+
} else {
|
|
2714
|
+
this.map.addSource(MARKERS_ID, { type: "geojson", data: this.data });
|
|
2715
|
+
}
|
|
2716
|
+
if (!this.map.getLayer(MARKERS_ID)) {
|
|
2717
|
+
const layer = {
|
|
2718
|
+
id: MARKERS_ID,
|
|
2719
|
+
type: "symbol",
|
|
2720
|
+
source: MARKERS_ID,
|
|
2721
|
+
layout: {
|
|
2722
|
+
"icon-image": ["get", "iconImage"],
|
|
2723
|
+
"icon-size": ["get", "iconSize"],
|
|
2724
|
+
"icon-anchor": ["get", "iconAnchor"],
|
|
2725
|
+
"icon-allow-overlap": true,
|
|
2726
|
+
"text-field": ["get", "label"],
|
|
2727
|
+
"text-font": [MARKER_TEXT_FONT],
|
|
2728
|
+
"text-size": ["get", "textSize"],
|
|
2729
|
+
"text-anchor": "top",
|
|
2730
|
+
"text-offset": TEXT_OFFSET_EXPRESSION,
|
|
2731
|
+
"text-optional": true
|
|
2732
|
+
},
|
|
2733
|
+
paint: {
|
|
2734
|
+
"text-color": "#333333",
|
|
2735
|
+
"text-halo-color": "#ffffff",
|
|
2736
|
+
"text-halo-width": 1.5
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2739
|
+
this.map.addLayer(layer);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Remove the markers' layer, source and registered images from the map.
|
|
2744
|
+
* Images are removed by the keys this layer REGISTERED, not by whatever
|
|
2745
|
+
* `items` happens to hold now — deriving them from the items meant
|
|
2746
|
+
* `setMarkers(undefined)` followed by `clear()`/`destroy()` removed
|
|
2747
|
+
* nothing at all.
|
|
2748
|
+
*/
|
|
2749
|
+
clear() {
|
|
2750
|
+
if (this.map.getLayer(MARKERS_ID)) this.map.removeLayer(MARKERS_ID);
|
|
2751
|
+
if (this.map.getSource(MARKERS_ID)) this.map.removeSource(MARKERS_ID);
|
|
2752
|
+
for (const key of this.registeredImageKeys) {
|
|
2753
|
+
if (this.map.hasImage(key)) this.map.removeImage(key);
|
|
2754
|
+
}
|
|
2755
|
+
this.registeredImageKeys.clear();
|
|
2756
|
+
this.items = [];
|
|
2757
|
+
this.data = void 0;
|
|
2758
|
+
this.loadedImages.clear();
|
|
2759
|
+
this.failedImages.clear();
|
|
2760
|
+
this.pendingImages.clear();
|
|
2761
|
+
this.generation++;
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Remove the markers and detach the style.load listener. Call when
|
|
2765
|
+
* disposing of the layer (the map is left untouched). Any image load
|
|
2766
|
+
* still in flight resolves into a no-op.
|
|
2767
|
+
*/
|
|
2768
|
+
destroy() {
|
|
2769
|
+
this.destroyed = true;
|
|
2770
|
+
this.map.off("style.load", this.handleStyleLoad);
|
|
2771
|
+
this.clear();
|
|
2772
|
+
}
|
|
2773
|
+
};
|
|
2774
|
+
|
|
2156
2775
|
// src/route.ts
|
|
2157
2776
|
var SIGNAL_BLUE = "#3a86ff";
|
|
2158
2777
|
var CASING_COLOR = "#1f438a";
|
|
2159
2778
|
var CORRIDOR_COLOR = "#e05c6c";
|
|
2160
2779
|
var CORRIDOR_OPACITY = 0.55;
|
|
2780
|
+
var ENDPOINT_START_COLOUR = "#3ecf8e";
|
|
2781
|
+
var DEFAULT_START = {
|
|
2782
|
+
icon: "dot",
|
|
2783
|
+
colour: ENDPOINT_START_COLOUR,
|
|
2784
|
+
size: "m"
|
|
2785
|
+
};
|
|
2786
|
+
var DEFAULT_END = {
|
|
2787
|
+
icon: "pin",
|
|
2788
|
+
colour: SIGNAL_BLUE,
|
|
2789
|
+
size: "m"
|
|
2790
|
+
};
|
|
2791
|
+
var DEFAULT_WAYPOINT = {
|
|
2792
|
+
icon: "pin",
|
|
2793
|
+
colour: SIGNAL_BLUE,
|
|
2794
|
+
size: "s"
|
|
2795
|
+
};
|
|
2796
|
+
function resolveEndpoints(option) {
|
|
2797
|
+
if (option === void 0 || option === false) return void 0;
|
|
2798
|
+
const design = option === true ? {} : option;
|
|
2799
|
+
const merge = (value, fallback) => value === false ? false : { ...fallback, ...value };
|
|
2800
|
+
return {
|
|
2801
|
+
start: merge(design.start, DEFAULT_START),
|
|
2802
|
+
end: merge(design.end, DEFAULT_END),
|
|
2803
|
+
waypoint: merge(design.waypoint, DEFAULT_WAYPOINT),
|
|
2804
|
+
numberWaypoints: design.numberWaypoints ?? true
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2161
2807
|
var PROGRESS_COLOR = "#b0b0b0";
|
|
2162
2808
|
var ALT_CASING_COLOR = "#5a6b84";
|
|
2163
2809
|
var ALT_LINE_COLOR = "#aebdd2";
|
|
@@ -2187,14 +2833,64 @@ function arrowImage() {
|
|
|
2187
2833
|
}
|
|
2188
2834
|
return { width: size, height: size, data };
|
|
2189
2835
|
}
|
|
2836
|
+
var ENDPOINT_TEXT_FONT = "MapMap Sans Regular";
|
|
2837
|
+
var ENDPOINT_TEXT_OFFSET = [
|
|
2838
|
+
"match",
|
|
2839
|
+
["get", "offsetKey"],
|
|
2840
|
+
"tip",
|
|
2841
|
+
["literal", [0, 0.4]],
|
|
2842
|
+
"cs",
|
|
2843
|
+
["literal", [0, 1.3]],
|
|
2844
|
+
"cm",
|
|
2845
|
+
["literal", [0, 1.55]],
|
|
2846
|
+
"cl",
|
|
2847
|
+
["literal", [0, 1.75]],
|
|
2848
|
+
["literal", [0, 0.4]]
|
|
2849
|
+
];
|
|
2850
|
+
function endpointRender(marker, coordinates, role, numbered) {
|
|
2851
|
+
const size = marker.size ?? "m";
|
|
2852
|
+
const icon = marker.icon ?? "pin";
|
|
2853
|
+
const colour2 = marker.colour ?? SIGNAL_BLUE;
|
|
2854
|
+
const text = markerText(marker.text) ?? markerText(numbered);
|
|
2855
|
+
const key = text !== void 0 ? markerTextImageKey(text, colour2, size) : markerImageKey(icon, colour2, size);
|
|
2856
|
+
const anchor = icon === "dot" && text === void 0 ? "center" : "bottom";
|
|
2857
|
+
const properties = {
|
|
2858
|
+
role,
|
|
2859
|
+
iconImage: key,
|
|
2860
|
+
iconAnchor: anchor,
|
|
2861
|
+
textSize: MARKER_LABEL_TEXT_SIZES[size],
|
|
2862
|
+
offsetKey: anchor === "bottom" ? "tip" : `c${size}`
|
|
2863
|
+
};
|
|
2864
|
+
if (marker.label !== void 0) properties["label"] = marker.label;
|
|
2865
|
+
return {
|
|
2866
|
+
feature: {
|
|
2867
|
+
type: "Feature",
|
|
2868
|
+
geometry: { type: "Point", coordinates },
|
|
2869
|
+
properties
|
|
2870
|
+
},
|
|
2871
|
+
key,
|
|
2872
|
+
render: text !== void 0 ? { text, colour: colour2, size } : { icon, colour: colour2, size }
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2190
2875
|
var RouteLayer = class {
|
|
2191
2876
|
constructor(map, options = {}) {
|
|
2877
|
+
/** Intermediate stops for the endpoint markers, `[lng, lat]` each. */
|
|
2878
|
+
this.waypoints = [];
|
|
2879
|
+
/** Marker images this layer registered, so teardown can remove them. */
|
|
2880
|
+
this.endpointImageKeys = /* @__PURE__ */ new Set();
|
|
2192
2881
|
/** Unselected alternatives, drawn beneath the selected route. */
|
|
2193
2882
|
this.alternatives = [];
|
|
2194
2883
|
/** Ferry legs of the selected route, drawn as dashes over its line. */
|
|
2195
2884
|
this.ferrySegments = [];
|
|
2196
2885
|
this.progress = 0;
|
|
2197
2886
|
this.corridors = [];
|
|
2887
|
+
/**
|
|
2888
|
+
* Monotonic id of the newest route request. A response only draws when it
|
|
2889
|
+
* still owns this value - see {@link RouteLayer.routePath}.
|
|
2890
|
+
*/
|
|
2891
|
+
this.requestSeq = 0;
|
|
2892
|
+
/** Whether the alternative-line click listener is currently attached. */
|
|
2893
|
+
this.altClickBound = false;
|
|
2198
2894
|
this.handleStyleLoad = () => {
|
|
2199
2895
|
this.installAlternatives();
|
|
2200
2896
|
if (this.lastRoute) this.install(this.lastRoute);
|
|
@@ -2243,12 +2939,33 @@ var RouteLayer = class {
|
|
|
2243
2939
|
this.altLineLayerId = `${id}-alt-line`;
|
|
2244
2940
|
this.ferrySourceId = `${id}-ferry-src`;
|
|
2245
2941
|
this.ferryLayerId = `${id}-ferry`;
|
|
2942
|
+
this.endpointsSourceId = `${id}-endpoints`;
|
|
2943
|
+
this.endpointsLayerId = `${id}-endpoints`;
|
|
2944
|
+
this.endpoints = resolveEndpoints(options.endpoints);
|
|
2246
2945
|
this.progressColor = options.progressColor ?? PROGRESS_COLOR;
|
|
2247
2946
|
this.altCasingColor = options.alternativeCasingColor ?? ALT_CASING_COLOR;
|
|
2248
2947
|
this.altColor = options.alternativeColor ?? ALT_LINE_COLOR;
|
|
2249
2948
|
this.ferryColor = options.ferryColor ?? FERRY_COLOR;
|
|
2250
2949
|
this.map.on("style.load", this.handleStyleLoad);
|
|
2251
2950
|
}
|
|
2951
|
+
/**
|
|
2952
|
+
* The MapLibre source and layer ids this layer owns - public API for
|
|
2953
|
+
* escape-hatch styling and layer ordering (`map.moveLayer`). All are
|
|
2954
|
+
* derived from the `id` option (default `mapmap-route`).
|
|
2955
|
+
*/
|
|
2956
|
+
get ids() {
|
|
2957
|
+
return {
|
|
2958
|
+
source: this.sourceId,
|
|
2959
|
+
casing: this.casingLayerId,
|
|
2960
|
+
line: this.lineLayerId,
|
|
2961
|
+
maneuverSource: this.maneuverSourceId,
|
|
2962
|
+
maneuver: this.maneuverLayerId,
|
|
2963
|
+
corridorSource: this.corridorSourceId,
|
|
2964
|
+
corridor: this.corridorLayerId,
|
|
2965
|
+
endpointsSource: this.endpointsSourceId,
|
|
2966
|
+
endpoints: this.endpointsLayerId
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2252
2969
|
/**
|
|
2253
2970
|
* Route from `from` to `to` (with optional intermediate `via` points passed
|
|
2254
2971
|
* through `options` is not supported here - pass a coordinate list to
|
|
@@ -2257,8 +2974,21 @@ var RouteLayer = class {
|
|
|
2257
2974
|
async route(from, to, options = {}) {
|
|
2258
2975
|
return this.routePath([from, to], options);
|
|
2259
2976
|
}
|
|
2260
|
-
/**
|
|
2977
|
+
/**
|
|
2978
|
+
* Route through an ordered list of coordinates and draw the result.
|
|
2979
|
+
*
|
|
2980
|
+
* Concurrent calls are ordered by ISSUE, not by arrival: only the newest
|
|
2981
|
+
* request draws. A destination retyped while the first request is still in
|
|
2982
|
+
* flight used to leave the map showing whichever response happened to land
|
|
2983
|
+
* last, so a driver could be looking at the line to the address they had
|
|
2984
|
+
* already replaced. Each caller still receives its own parsed route, so a
|
|
2985
|
+
* caller that wants the data of a superseded request keeps getting it -
|
|
2986
|
+
* only the drawing is suppressed. {@link RouteLayer.clear} likewise
|
|
2987
|
+
* supersedes anything in flight.
|
|
2988
|
+
*/
|
|
2261
2989
|
async routePath(points, options = {}) {
|
|
2990
|
+
const seq = ++this.requestSeq;
|
|
2991
|
+
this.waypoints = points.slice(1, -1).map(toLngLat);
|
|
2262
2992
|
const url = buildRouteUrl(
|
|
2263
2993
|
this.baseUrl,
|
|
2264
2994
|
options.profile ?? "driving",
|
|
@@ -2280,7 +3010,7 @@ var RouteLayer = class {
|
|
|
2280
3010
|
}
|
|
2281
3011
|
}
|
|
2282
3012
|
const parsed = parseOsrmRoute(body);
|
|
2283
|
-
this.draw(parsed);
|
|
3013
|
+
if (seq === this.requestSeq) this.draw(parsed);
|
|
2284
3014
|
return parsed;
|
|
2285
3015
|
}
|
|
2286
3016
|
/** The most recently drawn route, if any. */
|
|
@@ -2314,6 +3044,7 @@ var RouteLayer = class {
|
|
|
2314
3044
|
this.applyProgress();
|
|
2315
3045
|
this.applyLineStyle();
|
|
2316
3046
|
this.installManeuver();
|
|
3047
|
+
this.installEndpoints();
|
|
2317
3048
|
return;
|
|
2318
3049
|
}
|
|
2319
3050
|
const casing = {
|
|
@@ -2350,6 +3081,115 @@ var RouteLayer = class {
|
|
|
2350
3081
|
this.applyProgress();
|
|
2351
3082
|
this.applyLineStyle();
|
|
2352
3083
|
this.installManeuver();
|
|
3084
|
+
this.installEndpoints();
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Set the intermediate stops drawn by the endpoint markers, in order.
|
|
3088
|
+
* {@link routePath} fills these in from its own via points, so this is
|
|
3089
|
+
* only needed when drawing a route parsed elsewhere (e.g. a route fetched
|
|
3090
|
+
* by the app and handed to {@link draw}). An empty array clears them.
|
|
3091
|
+
*/
|
|
3092
|
+
setWaypoints(waypoints) {
|
|
3093
|
+
this.waypoints = waypoints.map(toLngLat);
|
|
3094
|
+
if (this.lastRoute && this.map.isStyleLoaded()) this.installEndpoints();
|
|
3095
|
+
}
|
|
3096
|
+
/** The intermediate stops currently drawn, `[lng, lat]` each. */
|
|
3097
|
+
get currentWaypoints() {
|
|
3098
|
+
return this.waypoints;
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Build one Point feature per endpoint marker: start (first route
|
|
3102
|
+
* coordinate), each waypoint in order, then end (last coordinate). The
|
|
3103
|
+
* end is added last so it wins MapLibre's symbol-collision ordering when
|
|
3104
|
+
* markers overlap on a short route.
|
|
3105
|
+
*/
|
|
3106
|
+
endpointFeatures(route) {
|
|
3107
|
+
const design = this.endpoints;
|
|
3108
|
+
if (!design) return [];
|
|
3109
|
+
const line = route.geometry.coordinates;
|
|
3110
|
+
const first = line[0];
|
|
3111
|
+
const last = line[line.length - 1];
|
|
3112
|
+
if (!first || !last) return [];
|
|
3113
|
+
const rendered = [];
|
|
3114
|
+
const push = (marker, coordinates, role, numbered) => {
|
|
3115
|
+
if (marker === false) return;
|
|
3116
|
+
rendered.push(endpointRender(marker, coordinates, role, numbered));
|
|
3117
|
+
};
|
|
3118
|
+
push(design.start, first, "start");
|
|
3119
|
+
this.waypoints.forEach((coordinates, index) => {
|
|
3120
|
+
const numbered = design.numberWaypoints ? String(index + 1) : void 0;
|
|
3121
|
+
push(design.waypoint, coordinates, "waypoint", numbered);
|
|
3122
|
+
});
|
|
3123
|
+
push(design.end, last, "end");
|
|
3124
|
+
return rendered;
|
|
3125
|
+
}
|
|
3126
|
+
/**
|
|
3127
|
+
* Register every endpoint marker image the current features need
|
|
3128
|
+
* (idempotent; `setStyle` wipes runtime images, so this runs on each
|
|
3129
|
+
* install). Keys are remembered so teardown removes exactly what this
|
|
3130
|
+
* layer added.
|
|
3131
|
+
*/
|
|
3132
|
+
registerEndpointImages(rendered) {
|
|
3133
|
+
for (const item of rendered) {
|
|
3134
|
+
this.endpointImageKeys.add(item.key);
|
|
3135
|
+
if (this.map.hasImage(item.key)) continue;
|
|
3136
|
+
const image = renderMarkerImage(item.render);
|
|
3137
|
+
if (image) {
|
|
3138
|
+
this.map.addImage(item.key, image.image, { pixelRatio: image.pixelRatio });
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
/** Add-or-update the endpoint markers' source and symbol layer. */
|
|
3143
|
+
installEndpoints() {
|
|
3144
|
+
if (!this.endpoints || !this.lastRoute) return;
|
|
3145
|
+
const rendered = this.endpointFeatures(this.lastRoute);
|
|
3146
|
+
const data = {
|
|
3147
|
+
type: "FeatureCollection",
|
|
3148
|
+
features: rendered.map((item) => item.feature)
|
|
3149
|
+
};
|
|
3150
|
+
this.registerEndpointImages(rendered);
|
|
3151
|
+
const source = this.map.getSource(this.endpointsSourceId);
|
|
3152
|
+
if (source) {
|
|
3153
|
+
source.setData(data);
|
|
3154
|
+
} else {
|
|
3155
|
+
this.map.addSource(this.endpointsSourceId, { type: "geojson", data });
|
|
3156
|
+
}
|
|
3157
|
+
if (this.map.getLayer(this.endpointsLayerId)) return;
|
|
3158
|
+
const layer = {
|
|
3159
|
+
id: this.endpointsLayerId,
|
|
3160
|
+
type: "symbol",
|
|
3161
|
+
source: this.endpointsSourceId,
|
|
3162
|
+
layout: {
|
|
3163
|
+
"icon-image": ["get", "iconImage"],
|
|
3164
|
+
"icon-anchor": ["get", "iconAnchor"],
|
|
3165
|
+
"icon-allow-overlap": true,
|
|
3166
|
+
"text-field": ["get", "label"],
|
|
3167
|
+
"text-font": [ENDPOINT_TEXT_FONT],
|
|
3168
|
+
"text-size": ["get", "textSize"],
|
|
3169
|
+
"text-anchor": "top",
|
|
3170
|
+
"text-offset": ENDPOINT_TEXT_OFFSET,
|
|
3171
|
+
"text-optional": true
|
|
3172
|
+
},
|
|
3173
|
+
paint: {
|
|
3174
|
+
"text-color": "#333333",
|
|
3175
|
+
"text-halo-color": "#ffffff",
|
|
3176
|
+
"text-halo-width": 1.5
|
|
3177
|
+
}
|
|
3178
|
+
};
|
|
3179
|
+
this.map.addLayer(layer);
|
|
3180
|
+
}
|
|
3181
|
+
/** Remove the endpoint markers, their source and their images. */
|
|
3182
|
+
clearEndpoints() {
|
|
3183
|
+
if (this.map.getLayer(this.endpointsLayerId)) {
|
|
3184
|
+
this.map.removeLayer(this.endpointsLayerId);
|
|
3185
|
+
}
|
|
3186
|
+
if (this.map.getSource(this.endpointsSourceId)) {
|
|
3187
|
+
this.map.removeSource(this.endpointsSourceId);
|
|
3188
|
+
}
|
|
3189
|
+
for (const key of this.endpointImageKeys) {
|
|
3190
|
+
if (this.map.hasImage(key)) this.map.removeImage(key);
|
|
3191
|
+
}
|
|
3192
|
+
this.endpointImageKeys.clear();
|
|
2353
3193
|
}
|
|
2354
3194
|
/**
|
|
2355
3195
|
* Draw a bare line from coordinates, without a parsed OSRM route.
|
|
@@ -2466,12 +3306,18 @@ var RouteLayer = class {
|
|
|
2466
3306
|
"line-opacity": 0.9
|
|
2467
3307
|
}
|
|
2468
3308
|
}, below);
|
|
3309
|
+
}
|
|
3310
|
+
if (!this.altClickBound) {
|
|
2469
3311
|
this.map.on("click", this.altLineLayerId, this.handleAlternativeClick);
|
|
3312
|
+
this.altClickBound = true;
|
|
2470
3313
|
}
|
|
2471
3314
|
}
|
|
2472
3315
|
removeAlternativeLayers() {
|
|
2473
|
-
if (this.
|
|
3316
|
+
if (this.altClickBound) {
|
|
2474
3317
|
this.map.off("click", this.altLineLayerId, this.handleAlternativeClick);
|
|
3318
|
+
this.altClickBound = false;
|
|
3319
|
+
}
|
|
3320
|
+
if (this.map.getLayer(this.altLineLayerId)) {
|
|
2475
3321
|
this.map.removeLayer(this.altLineLayerId);
|
|
2476
3322
|
}
|
|
2477
3323
|
if (this.map.getLayer(this.altCasingLayerId)) this.map.removeLayer(this.altCasingLayerId);
|
|
@@ -2680,14 +3526,21 @@ var RouteLayer = class {
|
|
|
2680
3526
|
this.map.setPaintProperty(this.corridorLayerId, "line-width", width);
|
|
2681
3527
|
}
|
|
2682
3528
|
}
|
|
2683
|
-
/**
|
|
3529
|
+
/**
|
|
3530
|
+
* Remove the route's layers and source from the map. Any route request
|
|
3531
|
+
* still in flight is superseded, so a late response cannot redraw a route
|
|
3532
|
+
* the caller has already cleared.
|
|
3533
|
+
*/
|
|
2684
3534
|
clear() {
|
|
3535
|
+
this.requestSeq += 1;
|
|
2685
3536
|
this.clearManeuver();
|
|
2686
3537
|
this.clearAlertCorridors();
|
|
2687
3538
|
this.clearFerrySegments();
|
|
2688
3539
|
this.alternatives = [];
|
|
2689
3540
|
this.altOriginalIndices = [];
|
|
2690
3541
|
this.removeAlternativeLayers();
|
|
3542
|
+
this.clearEndpoints();
|
|
3543
|
+
this.waypoints = [];
|
|
2691
3544
|
for (const layerId of [this.lineLayerId, this.casingLayerId]) {
|
|
2692
3545
|
if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
|
|
2693
3546
|
}
|
|
@@ -2757,7 +3610,7 @@ function alertContrastIssues(design) {
|
|
|
2757
3610
|
return issues;
|
|
2758
3611
|
}
|
|
2759
3612
|
var MAX_PUCK_IMAGE_BYTES = 64 * 1024;
|
|
2760
|
-
function
|
|
3613
|
+
function dataUriBytes2(uri) {
|
|
2761
3614
|
const comma = uri.indexOf(",");
|
|
2762
3615
|
const payload = comma === -1 ? "" : uri.slice(comma + 1);
|
|
2763
3616
|
const meta = comma === -1 ? uri : uri.slice(0, comma);
|
|
@@ -2766,7 +3619,7 @@ function dataUriBytes(uri) {
|
|
|
2766
3619
|
function effectiveImageUrl(design) {
|
|
2767
3620
|
const url = design.imageUrl?.trim();
|
|
2768
3621
|
if (!url) return void 0;
|
|
2769
|
-
if (/^data:/i.test(url) &&
|
|
3622
|
+
if (/^data:/i.test(url) && dataUriBytes2(url) > MAX_PUCK_IMAGE_BYTES) {
|
|
2770
3623
|
return void 0;
|
|
2771
3624
|
}
|
|
2772
3625
|
return url;
|
|
@@ -3085,7 +3938,7 @@ async function uploadProbeBatch(baseUrl, apiKey, body, options = {}) {
|
|
|
3085
3938
|
} catch {
|
|
3086
3939
|
outcome = "gaveUp";
|
|
3087
3940
|
}
|
|
3088
|
-
if (outcome === "gaveUp" && attempt < maxAttempts) {
|
|
3941
|
+
if (outcome === "gaveUp" && attempt < maxAttempts && options.signal?.aborted !== true) {
|
|
3089
3942
|
await sleep(backoffMs(attempt));
|
|
3090
3943
|
continue;
|
|
3091
3944
|
}
|
|
@@ -3131,6 +3984,8 @@ function haversineDistanceM(a, b) {
|
|
|
3131
3984
|
}
|
|
3132
3985
|
var PlacesLayer = class {
|
|
3133
3986
|
constructor(map, options = {}) {
|
|
3987
|
+
/** Per-place icon keys whose image failed to load (circle fallback). */
|
|
3988
|
+
this.failedIconKeys = /* @__PURE__ */ new Set();
|
|
3134
3989
|
this.places = [];
|
|
3135
3990
|
this.fitted = false;
|
|
3136
3991
|
this.destroyed = false;
|
|
@@ -3183,13 +4038,21 @@ var PlacesLayer = class {
|
|
|
3183
4038
|
this.clustersLayerId = `${id}-clusters`;
|
|
3184
4039
|
this.clusterCountLayerId = `${id}-cluster-count`;
|
|
3185
4040
|
this.pointsLayerId = `${id}-points`;
|
|
4041
|
+
this.pointsFallbackLayerId = `${id}-points-fallback`;
|
|
4042
|
+
this.labelsLayerId = `${id}-labels`;
|
|
3186
4043
|
this.iconImageId = `${id}-icon`;
|
|
3187
4044
|
this.cluster = options.cluster ?? true;
|
|
3188
4045
|
this.clusterRadius = options.clusterRadius;
|
|
3189
4046
|
this.clusterMaxZoom = options.clusterMaxZoom;
|
|
3190
4047
|
this.color = options.color ?? SIGNAL_BLUE;
|
|
3191
4048
|
this.clusterColor = options.clusterColor ?? (typeof this.color === "string" ? this.color : SIGNAL_BLUE);
|
|
3192
|
-
|
|
4049
|
+
if (options.icon && typeof options.icon.url === "string") {
|
|
4050
|
+
this.singleIcon = options.icon;
|
|
4051
|
+
} else if (options.icon) {
|
|
4052
|
+
this.iconRecord = options.icon;
|
|
4053
|
+
}
|
|
4054
|
+
this.iconProperty = options.iconProperty ?? "kind";
|
|
4055
|
+
this.label = options.label === true ? {} : options.label ? options.label : void 0;
|
|
3193
4056
|
this.wantFitBounds = options.fitBounds ?? false;
|
|
3194
4057
|
this.onPlaceClick = options.onPlaceClick;
|
|
3195
4058
|
this.popupFn = options.popup;
|
|
@@ -3197,6 +4060,11 @@ var PlacesLayer = class {
|
|
|
3197
4060
|
this.map.on("click", this.pointsLayerId, this.handlePlaceClick);
|
|
3198
4061
|
this.map.on("mouseenter", this.pointsLayerId, this.handleMouseEnter);
|
|
3199
4062
|
this.map.on("mouseleave", this.pointsLayerId, this.handleMouseLeave);
|
|
4063
|
+
if (this.iconRecord) {
|
|
4064
|
+
this.map.on("click", this.pointsFallbackLayerId, this.handlePlaceClick);
|
|
4065
|
+
this.map.on("mouseenter", this.pointsFallbackLayerId, this.handleMouseEnter);
|
|
4066
|
+
this.map.on("mouseleave", this.pointsFallbackLayerId, this.handleMouseLeave);
|
|
4067
|
+
}
|
|
3200
4068
|
if (this.cluster) {
|
|
3201
4069
|
this.map.on("click", this.clustersLayerId, this.handleClusterClick);
|
|
3202
4070
|
this.map.on("mouseenter", this.clustersLayerId, this.handleMouseEnter);
|
|
@@ -3256,7 +4124,9 @@ var PlacesLayer = class {
|
|
|
3256
4124
|
source: this.sourceId,
|
|
3257
4125
|
points: this.pointsLayerId,
|
|
3258
4126
|
clusters: this.clustersLayerId,
|
|
3259
|
-
clusterCounts: this.clusterCountLayerId
|
|
4127
|
+
clusterCounts: this.clusterCountLayerId,
|
|
4128
|
+
labels: this.labelsLayerId,
|
|
4129
|
+
pointsFallback: this.pointsFallbackLayerId
|
|
3260
4130
|
};
|
|
3261
4131
|
}
|
|
3262
4132
|
/**
|
|
@@ -3300,6 +4170,11 @@ var PlacesLayer = class {
|
|
|
3300
4170
|
* attached; unreachable places (`null` duration) are dropped. Needs the
|
|
3301
4171
|
* gateway `baseUrl`/`apiKey` - picked up from a `MapMapMap` automatically,
|
|
3302
4172
|
* or pass them as layer options.
|
|
4173
|
+
*
|
|
4174
|
+
* Answers always describe the places that were asked about: the list is
|
|
4175
|
+
* snapshotted at call time, so a `setPlaces` landing mid-flight (a
|
|
4176
|
+
* search-as-you-type filter, say) cannot mis-pair the matrix rows with a
|
|
4177
|
+
* different set of places.
|
|
3303
4178
|
*/
|
|
3304
4179
|
async nearestByDriveTime(origin, options = {}) {
|
|
3305
4180
|
if (!this.baseUrl) {
|
|
@@ -3307,7 +4182,8 @@ var PlacesLayer = class {
|
|
|
3307
4182
|
"PlacesLayer needs a baseUrl (or a MapMapMap) for nearestByDriveTime"
|
|
3308
4183
|
);
|
|
3309
4184
|
}
|
|
3310
|
-
|
|
4185
|
+
const places = this.places;
|
|
4186
|
+
if (places.length === 0) return [];
|
|
3311
4187
|
const headers = {
|
|
3312
4188
|
"Content-Type": "application/json",
|
|
3313
4189
|
Accept: "application/json"
|
|
@@ -3315,7 +4191,7 @@ var PlacesLayer = class {
|
|
|
3315
4191
|
if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
3316
4192
|
const body = {
|
|
3317
4193
|
sources: [{ lat: origin.lat, lon: origin.lon }],
|
|
3318
|
-
targets:
|
|
4194
|
+
targets: places.map((p) => ({ lat: p.lat, lon: p.lon })),
|
|
3319
4195
|
costing: options.costing ?? "auto"
|
|
3320
4196
|
};
|
|
3321
4197
|
if (options.costingOptions) body["costing_options"] = options.costingOptions;
|
|
@@ -3334,7 +4210,7 @@ var PlacesLayer = class {
|
|
|
3334
4210
|
}
|
|
3335
4211
|
const { durations, distances } = matrixRows(payload);
|
|
3336
4212
|
const reachable = [];
|
|
3337
|
-
|
|
4213
|
+
places.forEach((place, index) => {
|
|
3338
4214
|
const durationS = durations[index];
|
|
3339
4215
|
if (typeof durationS !== "number") return;
|
|
3340
4216
|
const distanceM = distances?.[index];
|
|
@@ -3363,6 +4239,38 @@ var PlacesLayer = class {
|
|
|
3363
4239
|
}
|
|
3364
4240
|
if (this.cluster) this.installClusterLayers();
|
|
3365
4241
|
this.installPointsLayer();
|
|
4242
|
+
this.installLabelsLayer();
|
|
4243
|
+
}
|
|
4244
|
+
/**
|
|
4245
|
+
* The per-place name labels layer (only with a `label` option): the map
|
|
4246
|
+
* font with a halo, anchored just below each unclustered point, hidden
|
|
4247
|
+
* before colliding (`text-optional`). Re-installed with the rest of the
|
|
4248
|
+
* layers on every `style.load`.
|
|
4249
|
+
*/
|
|
4250
|
+
installLabelsLayer() {
|
|
4251
|
+
if (!this.label || this.map.getLayer(this.labelsLayerId)) return;
|
|
4252
|
+
const labels = {
|
|
4253
|
+
id: this.labelsLayerId,
|
|
4254
|
+
type: "symbol",
|
|
4255
|
+
source: this.sourceId,
|
|
4256
|
+
...this.cluster ? { filter: ["!", ["has", "point_count"]] } : {},
|
|
4257
|
+
layout: {
|
|
4258
|
+
"text-field": ["get", this.label.property ?? "name"],
|
|
4259
|
+
"text-font": [CLUSTER_TEXT_FONT],
|
|
4260
|
+
"text-size": this.label.size ?? 12,
|
|
4261
|
+
"text-anchor": "top",
|
|
4262
|
+
"text-offset": [0, 0.9],
|
|
4263
|
+
"text-optional": true
|
|
4264
|
+
},
|
|
4265
|
+
paint: {
|
|
4266
|
+
// Both spellings are accepted (British wins) — see
|
|
4267
|
+
// PlacesLabelOptions.
|
|
4268
|
+
"text-color": this.label.colour ?? this.label.color ?? "#333333",
|
|
4269
|
+
"text-halo-color": this.label.haloColour ?? this.label.haloColor ?? "#ffffff",
|
|
4270
|
+
"text-halo-width": 1.5
|
|
4271
|
+
}
|
|
4272
|
+
};
|
|
4273
|
+
this.map.addLayer(labels);
|
|
3366
4274
|
}
|
|
3367
4275
|
/** Cluster circles (sized by count) and the count badge on top. */
|
|
3368
4276
|
installClusterLayers() {
|
|
@@ -3404,8 +4312,12 @@ var PlacesLayer = class {
|
|
|
3404
4312
|
* circle look if it fails, like PositionPuck's image fallback).
|
|
3405
4313
|
*/
|
|
3406
4314
|
installPointsLayer() {
|
|
4315
|
+
if (this.iconRecord) {
|
|
4316
|
+
this.installPerPlaceIconLayers(this.iconRecord);
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
3407
4319
|
if (this.map.getLayer(this.pointsLayerId)) return;
|
|
3408
|
-
if (!this.
|
|
4320
|
+
if (!this.singleIcon) {
|
|
3409
4321
|
this.addCirclePointsLayer();
|
|
3410
4322
|
return;
|
|
3411
4323
|
}
|
|
@@ -3413,7 +4325,7 @@ var PlacesLayer = class {
|
|
|
3413
4325
|
this.addIconPointsLayer();
|
|
3414
4326
|
return;
|
|
3415
4327
|
}
|
|
3416
|
-
void this.map.loadImage(this.
|
|
4328
|
+
void this.map.loadImage(this.singleIcon.url).then((image) => {
|
|
3417
4329
|
if (this.destroyed || this.map.getLayer(this.pointsLayerId)) return;
|
|
3418
4330
|
if (!this.map.hasImage(this.iconImageId)) {
|
|
3419
4331
|
this.map.addImage(this.iconImageId, image.data);
|
|
@@ -3424,6 +4336,103 @@ var PlacesLayer = class {
|
|
|
3424
4336
|
this.addCirclePointsLayer();
|
|
3425
4337
|
});
|
|
3426
4338
|
}
|
|
4339
|
+
/** The `map.addImage` id for one key of the per-place icon record. */
|
|
4340
|
+
iconImageIdFor(key) {
|
|
4341
|
+
return `${this.iconImageId}-${key}`;
|
|
4342
|
+
}
|
|
4343
|
+
/**
|
|
4344
|
+
* Per-place icon mode: load every record image (each key independently -
|
|
4345
|
+
* one bad URL never sinks the rest), then install a symbol layer whose
|
|
4346
|
+
* `icon-image` is a match expression on `iconProperty`, plus a circle
|
|
4347
|
+
* fallback layer for places whose property value has no loaded icon.
|
|
4348
|
+
*/
|
|
4349
|
+
installPerPlaceIconLayers(record) {
|
|
4350
|
+
const keys = Object.keys(record);
|
|
4351
|
+
if (keys.length === 0) {
|
|
4352
|
+
if (!this.map.getLayer(this.pointsLayerId)) this.addCirclePointsLayer();
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4355
|
+
const toLoad = keys.filter(
|
|
4356
|
+
(key) => !this.map.hasImage(this.iconImageIdFor(key)) && !this.failedIconKeys.has(key)
|
|
4357
|
+
);
|
|
4358
|
+
if (toLoad.length === 0) {
|
|
4359
|
+
this.addPerPlaceLayers(record);
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
void Promise.all(
|
|
4363
|
+
toLoad.map(async (key) => {
|
|
4364
|
+
try {
|
|
4365
|
+
const image = await this.map.loadImage(record[key].url);
|
|
4366
|
+
if (this.destroyed) return;
|
|
4367
|
+
if (!this.map.hasImage(this.iconImageIdFor(key))) {
|
|
4368
|
+
this.map.addImage(this.iconImageIdFor(key), image.data);
|
|
4369
|
+
}
|
|
4370
|
+
} catch {
|
|
4371
|
+
this.failedIconKeys.add(key);
|
|
4372
|
+
}
|
|
4373
|
+
})
|
|
4374
|
+
).then(() => {
|
|
4375
|
+
if (!this.destroyed) this.addPerPlaceLayers(record);
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
/** Install the per-place icon symbol layer + circle fallback layer. */
|
|
4379
|
+
addPerPlaceLayers(record) {
|
|
4380
|
+
const loaded = Object.keys(record).filter(
|
|
4381
|
+
(key) => this.map.hasImage(this.iconImageIdFor(key))
|
|
4382
|
+
);
|
|
4383
|
+
const unclustered = ["!", ["has", "point_count"]];
|
|
4384
|
+
if (!this.map.getLayer(this.pointsFallbackLayerId)) {
|
|
4385
|
+
const notCovered = [
|
|
4386
|
+
"!",
|
|
4387
|
+
["in", ["get", this.iconProperty], ["literal", loaded]]
|
|
4388
|
+
];
|
|
4389
|
+
const fallback = {
|
|
4390
|
+
id: this.pointsFallbackLayerId,
|
|
4391
|
+
type: "circle",
|
|
4392
|
+
source: this.sourceId,
|
|
4393
|
+
filter: this.cluster ? ["all", unclustered, notCovered] : notCovered,
|
|
4394
|
+
paint: {
|
|
4395
|
+
"circle-color": this.color,
|
|
4396
|
+
"circle-radius": 7,
|
|
4397
|
+
"circle-stroke-color": "#ffffff",
|
|
4398
|
+
"circle-stroke-width": 2
|
|
4399
|
+
}
|
|
4400
|
+
};
|
|
4401
|
+
this.map.addLayer(fallback);
|
|
4402
|
+
}
|
|
4403
|
+
if (loaded.length === 0) return;
|
|
4404
|
+
if (this.map.getLayer(this.pointsLayerId)) return;
|
|
4405
|
+
const iconImage = [
|
|
4406
|
+
"match",
|
|
4407
|
+
["get", this.iconProperty],
|
|
4408
|
+
...loaded.flatMap((key) => [key, this.iconImageIdFor(key)]),
|
|
4409
|
+
this.iconImageIdFor(loaded[0])
|
|
4410
|
+
// unreachable under the filter
|
|
4411
|
+
];
|
|
4412
|
+
const iconSize = [
|
|
4413
|
+
"match",
|
|
4414
|
+
["get", this.iconProperty],
|
|
4415
|
+
...loaded.flatMap((key) => [key, record[key]?.size ?? 1]),
|
|
4416
|
+
1
|
|
4417
|
+
];
|
|
4418
|
+
const covered = [
|
|
4419
|
+
"in",
|
|
4420
|
+
["get", this.iconProperty],
|
|
4421
|
+
["literal", loaded]
|
|
4422
|
+
];
|
|
4423
|
+
const points = {
|
|
4424
|
+
id: this.pointsLayerId,
|
|
4425
|
+
type: "symbol",
|
|
4426
|
+
source: this.sourceId,
|
|
4427
|
+
filter: this.cluster ? ["all", unclustered, covered] : covered,
|
|
4428
|
+
layout: {
|
|
4429
|
+
"icon-image": iconImage,
|
|
4430
|
+
"icon-size": iconSize,
|
|
4431
|
+
"icon-allow-overlap": true
|
|
4432
|
+
}
|
|
4433
|
+
};
|
|
4434
|
+
this.map.addLayer(points);
|
|
4435
|
+
}
|
|
3427
4436
|
addCirclePointsLayer() {
|
|
3428
4437
|
const points = {
|
|
3429
4438
|
id: this.pointsLayerId,
|
|
@@ -3447,7 +4456,7 @@ var PlacesLayer = class {
|
|
|
3447
4456
|
...this.cluster ? { filter: ["!", ["has", "point_count"]] } : {},
|
|
3448
4457
|
layout: {
|
|
3449
4458
|
"icon-image": this.iconImageId,
|
|
3450
|
-
"icon-size": this.
|
|
4459
|
+
"icon-size": this.singleIcon?.size ?? 1,
|
|
3451
4460
|
"icon-allow-overlap": true
|
|
3452
4461
|
}
|
|
3453
4462
|
};
|
|
@@ -3471,7 +4480,9 @@ var PlacesLayer = class {
|
|
|
3471
4480
|
this.popup?.remove();
|
|
3472
4481
|
this.popup = void 0;
|
|
3473
4482
|
for (const layerId of [
|
|
4483
|
+
this.labelsLayerId,
|
|
3474
4484
|
this.pointsLayerId,
|
|
4485
|
+
this.pointsFallbackLayerId,
|
|
3475
4486
|
this.clusterCountLayerId,
|
|
3476
4487
|
this.clustersLayerId
|
|
3477
4488
|
]) {
|
|
@@ -3481,6 +4492,11 @@ var PlacesLayer = class {
|
|
|
3481
4492
|
if (this.map.hasImage(this.iconImageId)) {
|
|
3482
4493
|
this.map.removeImage(this.iconImageId);
|
|
3483
4494
|
}
|
|
4495
|
+
for (const key of Object.keys(this.iconRecord ?? {})) {
|
|
4496
|
+
if (this.map.hasImage(this.iconImageIdFor(key))) {
|
|
4497
|
+
this.map.removeImage(this.iconImageIdFor(key));
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
3484
4500
|
this.places = [];
|
|
3485
4501
|
this.data = void 0;
|
|
3486
4502
|
}
|
|
@@ -3494,6 +4510,11 @@ var PlacesLayer = class {
|
|
|
3494
4510
|
this.map.off("click", this.pointsLayerId, this.handlePlaceClick);
|
|
3495
4511
|
this.map.off("mouseenter", this.pointsLayerId, this.handleMouseEnter);
|
|
3496
4512
|
this.map.off("mouseleave", this.pointsLayerId, this.handleMouseLeave);
|
|
4513
|
+
if (this.iconRecord) {
|
|
4514
|
+
this.map.off("click", this.pointsFallbackLayerId, this.handlePlaceClick);
|
|
4515
|
+
this.map.off("mouseenter", this.pointsFallbackLayerId, this.handleMouseEnter);
|
|
4516
|
+
this.map.off("mouseleave", this.pointsFallbackLayerId, this.handleMouseLeave);
|
|
4517
|
+
}
|
|
3497
4518
|
if (this.cluster) {
|
|
3498
4519
|
this.map.off("click", this.clustersLayerId, this.handleClusterClick);
|
|
3499
4520
|
this.map.off("mouseenter", this.clustersLayerId, this.handleMouseEnter);
|
|
@@ -3503,16 +4524,26 @@ var PlacesLayer = class {
|
|
|
3503
4524
|
}
|
|
3504
4525
|
};
|
|
3505
4526
|
function boundsOf(places) {
|
|
3506
|
-
let minLng = Infinity;
|
|
3507
4527
|
let minLat = Infinity;
|
|
3508
|
-
let maxLng = -Infinity;
|
|
3509
4528
|
let maxLat = -Infinity;
|
|
3510
|
-
for (const {
|
|
3511
|
-
if (lon < minLng) minLng = lon;
|
|
4529
|
+
for (const { lat } of places) {
|
|
3512
4530
|
if (lat < minLat) minLat = lat;
|
|
3513
|
-
if (lon > maxLng) maxLng = lon;
|
|
3514
4531
|
if (lat > maxLat) maxLat = lat;
|
|
3515
4532
|
}
|
|
4533
|
+
const lngs = places.map((p) => p.lon).sort((a, b) => a - b);
|
|
4534
|
+
let widestGap = -Infinity;
|
|
4535
|
+
let cutAfter = lngs.length - 1;
|
|
4536
|
+
for (let i = 0; i < lngs.length; i++) {
|
|
4537
|
+
const here = lngs[i];
|
|
4538
|
+
const next = i === lngs.length - 1 ? lngs[0] + 360 : lngs[i + 1];
|
|
4539
|
+
if (next - here > widestGap) {
|
|
4540
|
+
widestGap = next - here;
|
|
4541
|
+
cutAfter = i;
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
const minLng = lngs[(cutAfter + 1) % lngs.length];
|
|
4545
|
+
let maxLng = lngs[cutAfter];
|
|
4546
|
+
if (maxLng < minLng) maxLng += 360;
|
|
3516
4547
|
return [
|
|
3517
4548
|
[minLng, minLat],
|
|
3518
4549
|
[maxLng, maxLat]
|
|
@@ -3631,7 +4662,7 @@ var NavigationCamera = class {
|
|
|
3631
4662
|
if (!route || route.coordinates.length === 0) return;
|
|
3632
4663
|
this.clearRecentreTimer();
|
|
3633
4664
|
this.currentMode = "overview";
|
|
3634
|
-
this.m.fitBounds(boundsOf2(route.coordinates), {
|
|
4665
|
+
this.m.fitBounds(boundsOf2(unwrapLngs(route.coordinates)), {
|
|
3635
4666
|
padding: OVERVIEW_PADDING_PX,
|
|
3636
4667
|
pitch: 0,
|
|
3637
4668
|
bearing: 0,
|
|
@@ -3994,10 +5025,20 @@ var IsochroneLayer = class {
|
|
|
3994
5025
|
get current() {
|
|
3995
5026
|
return this.lastData;
|
|
3996
5027
|
}
|
|
3997
|
-
/**
|
|
5028
|
+
/**
|
|
5029
|
+
* Draw (or update) a ring collection. Safe before the style has loaded.
|
|
5030
|
+
* Never silently drops an update: once the source exists the data is
|
|
5031
|
+
* applied immediately, even while `isStyleLoaded()` is transiently
|
|
5032
|
+
* `false` mid-render (PlacesLayer has the same rule) - otherwise the map
|
|
5033
|
+
* would keep showing the previous rings until the next style swap.
|
|
5034
|
+
* Calls made before the source has first been installed are stashed in
|
|
5035
|
+
* `lastData` - the latest wins - and installed on the next `style.load`.
|
|
5036
|
+
*/
|
|
3998
5037
|
draw(collection) {
|
|
3999
5038
|
this.lastData = collection;
|
|
4000
|
-
if (!this.map.isStyleLoaded())
|
|
5039
|
+
if (!this.map.getSource(this.sourceId) && !this.map.isStyleLoaded()) {
|
|
5040
|
+
return;
|
|
5041
|
+
}
|
|
4001
5042
|
this.install(collection);
|
|
4002
5043
|
}
|
|
4003
5044
|
/** Add-or-update the source and ring layers on the current style. */
|
|
@@ -4153,10 +5194,13 @@ var AdrCheck = class {
|
|
|
4153
5194
|
throw new Error("ADR check returned a non-object response");
|
|
4154
5195
|
}
|
|
4155
5196
|
const obj = body;
|
|
4156
|
-
const
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
5197
|
+
const status = obj["status"];
|
|
5198
|
+
if (status !== "allowed" && status !== "blocked") {
|
|
5199
|
+
throw new Error(
|
|
5200
|
+
`ADR check returned an undecodable decision (status ${JSON.stringify(status)}) - refusing to report a non-blocking result for a compliance check`
|
|
5201
|
+
);
|
|
5202
|
+
}
|
|
5203
|
+
const result = { status, raw: obj };
|
|
4160
5204
|
if (typeof obj["reason"] === "string") result.reason = obj["reason"];
|
|
4161
5205
|
return result;
|
|
4162
5206
|
}
|
|
@@ -4235,9 +5279,15 @@ function bannerLanes(banner) {
|
|
|
4235
5279
|
const lane = { directions: c.directions ?? [] };
|
|
4236
5280
|
if (c.active !== void 0) lane.active = c.active;
|
|
4237
5281
|
if (c.active === true) lane.valid = true;
|
|
5282
|
+
if (typeof c.active_direction === "string" && c.active_direction.length > 0) {
|
|
5283
|
+
lane.activeDirection = c.active_direction;
|
|
5284
|
+
}
|
|
4238
5285
|
return lane;
|
|
4239
5286
|
});
|
|
4240
5287
|
}
|
|
5288
|
+
function laneArrowDirection(lane) {
|
|
5289
|
+
return lane.activeDirection ?? lane.directions[0] ?? "none";
|
|
5290
|
+
}
|
|
4241
5291
|
function directionArrow(direction) {
|
|
4242
5292
|
switch (direction) {
|
|
4243
5293
|
case "left":
|
|
@@ -4328,13 +5378,13 @@ var GuidanceBanner = class {
|
|
|
4328
5378
|
laneRow.style.cssText = `margin-left:auto;letter-spacing:${spacing}px;white-space:nowrap`;
|
|
4329
5379
|
for (const lane of lanes) {
|
|
4330
5380
|
const glyph = doc.createElement("span");
|
|
4331
|
-
glyph.textContent = directionArrow(lane
|
|
5381
|
+
glyph.textContent = directionArrow(laneArrowDirection(lane));
|
|
4332
5382
|
glyph.style.opacity = lane.active ? "0.95" : "0.35";
|
|
4333
5383
|
laneRow.appendChild(glyph);
|
|
4334
5384
|
}
|
|
4335
5385
|
} else {
|
|
4336
5386
|
laneRow.style.cssText = "margin-left:auto;letter-spacing:4px";
|
|
4337
|
-
laneRow.textContent = lanes.map((lane) => directionArrow(lane
|
|
5387
|
+
laneRow.textContent = lanes.map((lane) => directionArrow(laneArrowDirection(lane))).join("");
|
|
4338
5388
|
laneRow.style.opacity = "0.9";
|
|
4339
5389
|
}
|
|
4340
5390
|
children.push(laneRow);
|
|
@@ -4370,11 +5420,11 @@ function cameraKindsShownOnMap(design, mode = "browse") {
|
|
|
4370
5420
|
}
|
|
4371
5421
|
function cameraSymbolLayer(design, source, mode = "browse", variant = "dark", id = "safety-cameras") {
|
|
4372
5422
|
const kinds = cameraKindsShownOnMap(design, mode);
|
|
4373
|
-
const
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
iconImage.push(
|
|
5423
|
+
const kind = cameraKindExpression();
|
|
5424
|
+
const iconImage = ["match", kind];
|
|
5425
|
+
for (const canonical of NAV_CAMERA_KINDS) {
|
|
5426
|
+
if (canonical === "unknown") continue;
|
|
5427
|
+
iconImage.push(canonical, cameraSpriteName(canonical, variant));
|
|
4378
5428
|
}
|
|
4379
5429
|
iconImage.push(cameraSpriteName("unknown", variant));
|
|
4380
5430
|
return {
|
|
@@ -4382,7 +5432,7 @@ function cameraSymbolLayer(design, source, mode = "browse", variant = "dark", id
|
|
|
4382
5432
|
type: "symbol",
|
|
4383
5433
|
source,
|
|
4384
5434
|
minzoom: design.mapMinZoom,
|
|
4385
|
-
filter: ["in",
|
|
5435
|
+
filter: ["in", kind, ["literal", kinds]],
|
|
4386
5436
|
layout: {
|
|
4387
5437
|
"icon-image": iconImage,
|
|
4388
5438
|
"icon-size": design.mapIconSize,
|
|
@@ -4391,6 +5441,22 @@ function cameraSymbolLayer(design, source, mode = "browse", variant = "dark", id
|
|
|
4391
5441
|
}
|
|
4392
5442
|
};
|
|
4393
5443
|
}
|
|
5444
|
+
var CAMERA_KIND_WIRE_SPELLINGS = {
|
|
5445
|
+
fixed: ["fixed", "permanent"],
|
|
5446
|
+
average: ["average", "average_speed", "section", "section_control"],
|
|
5447
|
+
red_light: ["red_light", "redlight", "traffic_signals"],
|
|
5448
|
+
mobile_site: ["mobile_site", "mobile"],
|
|
5449
|
+
unknown: ["unknown"]
|
|
5450
|
+
};
|
|
5451
|
+
function cameraKindExpression(property = "kind") {
|
|
5452
|
+
const match = ["match", ["to-string", ["get", property]]];
|
|
5453
|
+
for (const canonical of NAV_CAMERA_KINDS) {
|
|
5454
|
+
if (canonical === "unknown") continue;
|
|
5455
|
+
match.push(CAMERA_KIND_WIRE_SPELLINGS[canonical], canonical);
|
|
5456
|
+
}
|
|
5457
|
+
match.push("unknown");
|
|
5458
|
+
return match;
|
|
5459
|
+
}
|
|
4394
5460
|
function cameraSpriteName(kind, variant = "dark") {
|
|
4395
5461
|
const normalised = navCameraKind(kind);
|
|
4396
5462
|
const slug = normalised === "red_light" ? "redlight" : normalised.replace(/_/g, "-");
|
|
@@ -4653,6 +5719,6 @@ function clampVolume(volume) {
|
|
|
4653
5719
|
return Math.min(1, Math.max(0, volume));
|
|
4654
5720
|
}
|
|
4655
5721
|
|
|
4656
|
-
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 };
|
|
5722
|
+
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 };
|
|
4657
5723
|
//# sourceMappingURL=index.js.map
|
|
4658
5724
|
//# sourceMappingURL=index.js.map
|