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