maplibre-gl-basemap-control 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -1
- package/dist/{BasemapControl-BITP2bjU.js → BasemapControl-CnjmO-_2.js} +395 -13
- package/dist/BasemapControl-CnjmO-_2.js.map +1 -0
- package/dist/{BasemapControl-B8WWNorQ.cjs → BasemapControl-DBRMf6n3.cjs} +395 -13
- package/dist/BasemapControl-DBRMf6n3.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/react.cjs +1 -1
- package/dist/react.mjs +1 -1
- package/dist/types/lib/core/BasemapControl.d.ts +19 -0
- package/dist/types/lib/core/BasemapControl.d.ts.map +1 -1
- package/dist/types/lib/core/catalog.d.ts.map +1 -1
- package/dist/types/lib/core/types.d.ts +55 -2
- package/dist/types/lib/core/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/BasemapControl-B8WWNorQ.cjs.map +0 -1
- package/dist/BasemapControl-BITP2bjU.js.map +0 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ A MapLibre GL JS control for searching and switching public basemaps. It keeps t
|
|
|
9
9
|
|
|
10
10
|
- Search-first basemap picker inspired by QuickMapServices
|
|
11
11
|
- Built-in catalog for common public basemaps, MapTiler styles, Amazon Location styles, and Mapbox styles
|
|
12
|
+
- Stackable traffic overlays for TomTom, HERE, Mapbox, and Google
|
|
12
13
|
- Custom basemap and provider definitions
|
|
13
14
|
- MapLibre `IControl` implementation
|
|
14
15
|
- React wrapper and state hook
|
|
@@ -171,6 +172,45 @@ Mapbox style URLs follow this form:
|
|
|
171
172
|
https://api.mapbox.com/styles/v1/mapbox/{styleId}?access_token={api-key}
|
|
172
173
|
```
|
|
173
174
|
|
|
175
|
+
### Traffic Overlays
|
|
176
|
+
|
|
177
|
+
The catalog ships real-time traffic overlays in the `Traffic` category. They are
|
|
178
|
+
stackable overlays rather than full basemaps, so enable `allowMultiple: true` (or
|
|
179
|
+
toggle "Add basemaps" in the panel) to lay them on top of any basemap. Click an
|
|
180
|
+
active traffic layer again to remove it.
|
|
181
|
+
|
|
182
|
+
| Basemap id | Provider | Credential |
|
|
183
|
+
|------------|----------|------------|
|
|
184
|
+
| `tomtom-traffic-flow-relative`, `tomtom-traffic-flow-absolute`, `tomtom-traffic-flow-relative-delay` | TomTom | `tomtomApiKey` |
|
|
185
|
+
| `here-traffic-flow` | HERE | `hereApiKey` |
|
|
186
|
+
| `mapbox-traffic` | Mapbox | `mapboxAccessToken` |
|
|
187
|
+
| `google-traffic` | Google | `googleMapsApiKey` |
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const control = new BasemapControl({
|
|
191
|
+
allowMultiple: true,
|
|
192
|
+
tomtomApiKey: 'YOUR_TOMTOM_API_KEY',
|
|
193
|
+
hereApiKey: 'YOUR_HERE_API_KEY',
|
|
194
|
+
mapboxAccessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
|
|
195
|
+
googleMapsApiKey: 'YOUR_GOOGLE_MAPS_API_KEY',
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The TomTom and HERE overlays are transparent raster flow tiles; Mapbox Traffic is
|
|
200
|
+
a vector overlay colored by congestion level. Google Traffic uses the
|
|
201
|
+
[Map Tiles API](https://developers.google.com/maps/documentation/tile/session_tokens):
|
|
202
|
+
the control creates a tile session (with `layerTypes: ['layerTraffic']`) using your
|
|
203
|
+
key, caches the session token until it expires, then loads the traffic tiles. The
|
|
204
|
+
key must have the Map Tiles API enabled.
|
|
205
|
+
|
|
206
|
+
> **Troubleshooting `API_KEY_SERVICE_BLOCKED`** ("Requests to this API tile method
|
|
207
|
+
> ...Bootstrap are blocked"): this is a key restriction, not project API
|
|
208
|
+
> enablement. Enabling the Map Tiles API on the project is not enough if the key
|
|
209
|
+
> itself has an API allow-list. In Cloud Console go to **APIs & Services →
|
|
210
|
+
> Credentials → your key → API restrictions** and either choose "Don't restrict
|
|
211
|
+
> key" or add **Map Tiles API** to the allowed list, then save (changes can take a
|
|
212
|
+
> few minutes to propagate). Make sure billing is enabled on the project too.
|
|
213
|
+
|
|
174
214
|
## API
|
|
175
215
|
|
|
176
216
|
### BasemapControl Options
|
|
@@ -185,7 +225,10 @@ https://api.mapbox.com/styles/v1/mapbox/{styleId}?access_token={api-key}
|
|
|
185
225
|
| `mapTilerApiKey` | `string` | `undefined` | Initial MapTiler API key for built-in MapTiler styles |
|
|
186
226
|
| `amazonApiKey` | `string` | `undefined` | Initial Amazon Location API key for built-in Amazon styles |
|
|
187
227
|
| `awsRegion` | `string` | `'us-east-1'` | AWS region for built-in Amazon Location styles |
|
|
188
|
-
| `mapboxAccessToken` | `string` | `undefined` | Initial Mapbox access token for built-in Mapbox styles |
|
|
228
|
+
| `mapboxAccessToken` | `string` | `undefined` | Initial Mapbox access token for built-in Mapbox styles and the Mapbox Traffic overlay |
|
|
229
|
+
| `tomtomApiKey` | `string` | `undefined` | Initial TomTom API key for the TomTom Traffic overlays |
|
|
230
|
+
| `hereApiKey` | `string` | `undefined` | Initial HERE API key for the HERE Traffic overlay |
|
|
231
|
+
| `googleMapsApiKey` | `string` | `undefined` | Initial Google Maps API key (Map Tiles API) for the Google Traffic overlay |
|
|
189
232
|
| `basemaps` | `BasemapDefinition[]` | `[]` | Custom basemaps to add or use |
|
|
190
233
|
| `providers` | `BasemapProvider[]` | `[]` | Custom provider labels |
|
|
191
234
|
| `includeDefaultBasemaps` | `boolean` | `true` | Include the built-in public catalog |
|
|
@@ -249,6 +292,9 @@ const control = new BasemapControl({
|
|
|
249
292
|
- `setMapTilerApiKey(apiKey)` - Set or update the MapTiler API key used by MapTiler styles
|
|
250
293
|
- `setAmazonCredentials(apiKey, awsRegion)` - Set or update Amazon Location credentials
|
|
251
294
|
- `setMapboxAccessToken(accessToken)` - Set or update the Mapbox access token
|
|
295
|
+
- `setTomTomApiKey(apiKey)` - Set or update the TomTom API key used by TomTom Traffic overlays
|
|
296
|
+
- `setHereApiKey(apiKey)` - Set or update the HERE API key used by the HERE Traffic overlay
|
|
297
|
+
- `setGoogleMapsApiKey(apiKey)` - Set or update the Google Maps API key used by the Google Traffic overlay
|
|
252
298
|
- `getActiveBasemap()` - Return the most recently selected basemap definition
|
|
253
299
|
- `getActiveBasemaps()` - Return all currently active basemap definitions
|
|
254
300
|
- `getBasemaps()` - Return the catalog
|
|
@@ -7,6 +7,7 @@ const DEFAULT_BASEMAP_PROVIDERS = [
|
|
|
7
7
|
{ id: "cyclosm", name: "CyclOSM", category: "Cycling" },
|
|
8
8
|
{ id: "esri", name: "ESRI", category: "Imagery" },
|
|
9
9
|
{ id: "google", name: "Google", category: "General" },
|
|
10
|
+
{ id: "here", name: "HERE", category: "Traffic" },
|
|
10
11
|
{ id: "mapbox", name: "Mapbox", category: "General" },
|
|
11
12
|
{ id: "maptiler", name: "MapTiler", category: "General" },
|
|
12
13
|
{ id: "nasa-gibs", name: "NASA GIBS", category: "Imagery" },
|
|
@@ -16,6 +17,7 @@ const DEFAULT_BASEMAP_PROVIDERS = [
|
|
|
16
17
|
{ id: "openstreetmap", name: "OpenStreetMap", category: "Community" },
|
|
17
18
|
{ id: "opentopomap", name: "OpenTopoMap", category: "Terrain" },
|
|
18
19
|
{ id: "swisstopo", name: "Swiss Federal Geoportal", category: "Regional" },
|
|
20
|
+
{ id: "tomtom", name: "TomTom", category: "Traffic" },
|
|
19
21
|
{ id: "topplusopen", name: "TopPlusOpen", category: "Regional" },
|
|
20
22
|
{ id: "usgs", name: "USGS", category: "United States" },
|
|
21
23
|
{ id: "waymarkedtrails", name: "Waymarked Trails", category: "Outdoor" }
|
|
@@ -143,6 +145,44 @@ const MAPBOX_ATTRIBUTION = "© Mapbox © OpenStreetMap contributors";
|
|
|
143
145
|
const MAPTILER_ATTRIBUTION = "© MapTiler © OpenStreetMap contributors";
|
|
144
146
|
const OSM_ATTRIBUTION = "© OpenStreetMap contributors";
|
|
145
147
|
const OPENFREEMAP_ATTRIBUTION = "OpenFreeMap © OpenMapTiles Data from OpenStreetMap";
|
|
148
|
+
const TOMTOM_ATTRIBUTION = "© TomTom";
|
|
149
|
+
const HERE_ATTRIBUTION = "© HERE";
|
|
150
|
+
const GOOGLE_ATTRIBUTION = "© Google";
|
|
151
|
+
const MAPBOX_TRAFFIC_CONGESTION_COLORS = [
|
|
152
|
+
"match",
|
|
153
|
+
["get", "congestion"],
|
|
154
|
+
"low",
|
|
155
|
+
"#4caf50",
|
|
156
|
+
"moderate",
|
|
157
|
+
"#ffc107",
|
|
158
|
+
"heavy",
|
|
159
|
+
"#ff5722",
|
|
160
|
+
"severe",
|
|
161
|
+
"#b71c1c",
|
|
162
|
+
"#9e9e9e"
|
|
163
|
+
];
|
|
164
|
+
function rasterTrafficBasemap({
|
|
165
|
+
id,
|
|
166
|
+
name,
|
|
167
|
+
provider,
|
|
168
|
+
description,
|
|
169
|
+
attribution,
|
|
170
|
+
tiles,
|
|
171
|
+
maxzoom = 22,
|
|
172
|
+
tags = []
|
|
173
|
+
}) {
|
|
174
|
+
return rasterBasemap({
|
|
175
|
+
id,
|
|
176
|
+
name,
|
|
177
|
+
provider,
|
|
178
|
+
category: "Traffic",
|
|
179
|
+
description,
|
|
180
|
+
attribution,
|
|
181
|
+
tiles,
|
|
182
|
+
maxzoom,
|
|
183
|
+
tags: ["traffic", "overlay", ...tags]
|
|
184
|
+
});
|
|
185
|
+
}
|
|
146
186
|
const DEFAULT_BASEMAPS = [
|
|
147
187
|
rasterBasemap({
|
|
148
188
|
id: "osm-standard",
|
|
@@ -1048,6 +1088,89 @@ const DEFAULT_BASEMAPS = [
|
|
|
1048
1088
|
pitch: 60
|
|
1049
1089
|
},
|
|
1050
1090
|
tags: ["openfreemap", "vector", "style", "3d", "liberty"]
|
|
1091
|
+
},
|
|
1092
|
+
rasterTrafficBasemap({
|
|
1093
|
+
id: "tomtom-traffic-flow-relative",
|
|
1094
|
+
name: "TomTom Traffic Flow",
|
|
1095
|
+
provider: "tomtom",
|
|
1096
|
+
description: "TomTom traffic speeds relative to free-flow speed. Requires a TomTom API key.",
|
|
1097
|
+
attribution: TOMTOM_ATTRIBUTION,
|
|
1098
|
+
tiles: ["https://api.tomtom.com/traffic/map/4/tile/flow/relative0/{z}/{x}/{y}.png?key={api-key}"],
|
|
1099
|
+
tags: ["tomtom", "flow", "relative"]
|
|
1100
|
+
}),
|
|
1101
|
+
rasterTrafficBasemap({
|
|
1102
|
+
id: "tomtom-traffic-flow-absolute",
|
|
1103
|
+
name: "TomTom Traffic Flow Absolute",
|
|
1104
|
+
provider: "tomtom",
|
|
1105
|
+
description: "TomTom traffic colored by absolute speed. Requires a TomTom API key.",
|
|
1106
|
+
attribution: TOMTOM_ATTRIBUTION,
|
|
1107
|
+
tiles: ["https://api.tomtom.com/traffic/map/4/tile/flow/absolute/{z}/{x}/{y}.png?key={api-key}"],
|
|
1108
|
+
tags: ["tomtom", "flow", "absolute"]
|
|
1109
|
+
}),
|
|
1110
|
+
rasterTrafficBasemap({
|
|
1111
|
+
id: "tomtom-traffic-flow-relative-delay",
|
|
1112
|
+
name: "TomTom Traffic Flow Delay",
|
|
1113
|
+
provider: "tomtom",
|
|
1114
|
+
description: "TomTom traffic highlighting only roads delayed below free-flow speed. Requires a TomTom API key.",
|
|
1115
|
+
attribution: TOMTOM_ATTRIBUTION,
|
|
1116
|
+
tiles: [
|
|
1117
|
+
"https://api.tomtom.com/traffic/map/4/tile/flow/relative-delay/{z}/{x}/{y}.png?key={api-key}"
|
|
1118
|
+
],
|
|
1119
|
+
tags: ["tomtom", "flow", "delay"]
|
|
1120
|
+
}),
|
|
1121
|
+
rasterTrafficBasemap({
|
|
1122
|
+
id: "here-traffic-flow",
|
|
1123
|
+
name: "HERE Traffic Flow",
|
|
1124
|
+
provider: "here",
|
|
1125
|
+
description: "HERE real-time traffic flow overlay. Requires a HERE API key.",
|
|
1126
|
+
attribution: HERE_ATTRIBUTION,
|
|
1127
|
+
tiles: ["https://traffic.maps.hereapi.com/v3/flow/mc/{z}/{x}/{y}/png?apiKey={api-key}"],
|
|
1128
|
+
tags: ["here", "flow"]
|
|
1129
|
+
}),
|
|
1130
|
+
{
|
|
1131
|
+
id: "mapbox-traffic",
|
|
1132
|
+
name: "Mapbox Traffic",
|
|
1133
|
+
provider: "mapbox",
|
|
1134
|
+
type: "vector-overlay",
|
|
1135
|
+
category: "Traffic",
|
|
1136
|
+
description: "Mapbox real-time traffic congestion overlay. Requires a Mapbox access token.",
|
|
1137
|
+
attribution: MAPBOX_ATTRIBUTION,
|
|
1138
|
+
source: {
|
|
1139
|
+
type: "vector-overlay",
|
|
1140
|
+
url: "mapbox://mapbox.mapbox-traffic-v1",
|
|
1141
|
+
sourceLayer: "traffic",
|
|
1142
|
+
layerType: "line",
|
|
1143
|
+
layout: {
|
|
1144
|
+
"line-join": "round",
|
|
1145
|
+
"line-cap": "round"
|
|
1146
|
+
},
|
|
1147
|
+
paint: {
|
|
1148
|
+
"line-width": ["interpolate", ["linear"], ["zoom"], 6, 1.5, 14, 4, 18, 8],
|
|
1149
|
+
"line-color": MAPBOX_TRAFFIC_CONGESTION_COLORS
|
|
1150
|
+
}
|
|
1151
|
+
},
|
|
1152
|
+
tags: ["mapbox", "traffic", "overlay", "congestion"]
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
id: "google-traffic",
|
|
1156
|
+
name: "Google Traffic",
|
|
1157
|
+
provider: "google",
|
|
1158
|
+
type: "raster",
|
|
1159
|
+
category: "Traffic",
|
|
1160
|
+
description: "Google real-time traffic overlay via the Map Tiles API. Requires a Google Maps API key.",
|
|
1161
|
+
attribution: GOOGLE_ATTRIBUTION,
|
|
1162
|
+
source: {
|
|
1163
|
+
type: "raster",
|
|
1164
|
+
tiles: ["https://tile.googleapis.com/v1/2dtiles/{z}/{x}/{y}?session={session}&key={api-key}"],
|
|
1165
|
+
tileSize: 256,
|
|
1166
|
+
maxzoom: 22,
|
|
1167
|
+
googleSession: {
|
|
1168
|
+
mapType: "roadmap",
|
|
1169
|
+
layerTypes: ["layerTraffic"],
|
|
1170
|
+
overlay: true
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
tags: ["google", "traffic", "overlay"]
|
|
1051
1174
|
}
|
|
1052
1175
|
];
|
|
1053
1176
|
function combineProviders(defaults, custom = []) {
|
|
@@ -1125,6 +1248,18 @@ const PROVIDER_CREDENTIAL_HELP = {
|
|
|
1125
1248
|
mapbox: {
|
|
1126
1249
|
url: "https://docs.mapbox.com/help/getting-started/access-tokens/",
|
|
1127
1250
|
label: "Get a Mapbox access token"
|
|
1251
|
+
},
|
|
1252
|
+
tomtom: {
|
|
1253
|
+
url: "https://developer.tomtom.com/how-to-get-tomtom-api-key",
|
|
1254
|
+
label: "Get a TomTom API key"
|
|
1255
|
+
},
|
|
1256
|
+
here: {
|
|
1257
|
+
url: "https://www.here.com/get-started/pricing",
|
|
1258
|
+
label: "Get a HERE API key"
|
|
1259
|
+
},
|
|
1260
|
+
google: {
|
|
1261
|
+
url: "https://developers.google.com/maps/documentation/tile/get-api-key",
|
|
1262
|
+
label: "Get a Google Maps API key"
|
|
1128
1263
|
}
|
|
1129
1264
|
};
|
|
1130
1265
|
const MIN_PANEL_WIDTH = 240;
|
|
@@ -1158,6 +1293,13 @@ class BasemapControl {
|
|
|
1158
1293
|
__publicField(this, "_amazonApiKey", "");
|
|
1159
1294
|
__publicField(this, "_awsRegion", "");
|
|
1160
1295
|
__publicField(this, "_mapboxAccessToken", "");
|
|
1296
|
+
__publicField(this, "_tomtomApiKey", "");
|
|
1297
|
+
__publicField(this, "_hereApiKey", "");
|
|
1298
|
+
__publicField(this, "_googleMapsApiKey", "");
|
|
1299
|
+
// Cached Google Map Tiles API session tokens keyed by their session config, so
|
|
1300
|
+
// a traffic overlay reuses a token until it expires instead of creating a new
|
|
1301
|
+
// session on every add. Keyed by the serialized config + API key.
|
|
1302
|
+
__publicField(this, "_googleSessions", new globalThis.Map());
|
|
1161
1303
|
__publicField(this, "_providerSettingsCollapsed", true);
|
|
1162
1304
|
// Provider of the most recent missing-credential error, used to pick the
|
|
1163
1305
|
// matching help link. Kept in sync with `_state.error` (both are set together
|
|
@@ -1197,6 +1339,9 @@ class BasemapControl {
|
|
|
1197
1339
|
this._amazonApiKey = (options == null ? void 0 : options.amazonApiKey) ?? "";
|
|
1198
1340
|
this._awsRegion = (options == null ? void 0 : options.awsRegion) ?? "us-east-1";
|
|
1199
1341
|
this._mapboxAccessToken = (options == null ? void 0 : options.mapboxAccessToken) ?? "";
|
|
1342
|
+
this._tomtomApiKey = (options == null ? void 0 : options.tomtomApiKey) ?? "";
|
|
1343
|
+
this._hereApiKey = (options == null ? void 0 : options.hereApiKey) ?? "";
|
|
1344
|
+
this._googleMapsApiKey = (options == null ? void 0 : options.googleMapsApiKey) ?? "";
|
|
1200
1345
|
this._basemaps = createBasemapCatalog(
|
|
1201
1346
|
options == null ? void 0 : options.basemaps,
|
|
1202
1347
|
this._options.includeDefaultBasemaps
|
|
@@ -1324,6 +1469,25 @@ class BasemapControl {
|
|
|
1324
1469
|
this._renderContent();
|
|
1325
1470
|
this._emit({ type: "statechange", state: this.getState() });
|
|
1326
1471
|
}
|
|
1472
|
+
setTomTomApiKey(apiKey) {
|
|
1473
|
+
this._tomtomApiKey = apiKey;
|
|
1474
|
+
this._state = { ...this._state, error: void 0 };
|
|
1475
|
+
this._renderContent();
|
|
1476
|
+
this._emit({ type: "statechange", state: this.getState() });
|
|
1477
|
+
}
|
|
1478
|
+
setHereApiKey(apiKey) {
|
|
1479
|
+
this._hereApiKey = apiKey;
|
|
1480
|
+
this._state = { ...this._state, error: void 0 };
|
|
1481
|
+
this._renderContent();
|
|
1482
|
+
this._emit({ type: "statechange", state: this.getState() });
|
|
1483
|
+
}
|
|
1484
|
+
setGoogleMapsApiKey(apiKey) {
|
|
1485
|
+
this._googleMapsApiKey = apiKey;
|
|
1486
|
+
this._googleSessions.clear();
|
|
1487
|
+
this._state = { ...this._state, error: void 0 };
|
|
1488
|
+
this._renderContent();
|
|
1489
|
+
this._emit({ type: "statechange", state: this.getState() });
|
|
1490
|
+
}
|
|
1327
1491
|
getActiveBasemap() {
|
|
1328
1492
|
return this._basemaps.find((basemap) => basemap.id === this._state.activeBasemapId);
|
|
1329
1493
|
}
|
|
@@ -1386,9 +1550,9 @@ class BasemapControl {
|
|
|
1386
1550
|
this._handleError(error, basemap);
|
|
1387
1551
|
throw error;
|
|
1388
1552
|
}
|
|
1389
|
-
const
|
|
1390
|
-
const effectiveMode =
|
|
1391
|
-
if (!
|
|
1553
|
+
const isOverlay = basemap.source.type === "raster" || basemap.source.type === "vector-overlay";
|
|
1554
|
+
const effectiveMode = isOverlay ? mode : "replace";
|
|
1555
|
+
if (!isOverlay && this._state.allowMultiple && this._managedRasters.size > 0 && this._options.confirmStyleReplace) {
|
|
1392
1556
|
const replacedBasemapIds = [...this._managedRasters.keys()];
|
|
1393
1557
|
let confirmed = false;
|
|
1394
1558
|
try {
|
|
@@ -1403,14 +1567,14 @@ class BasemapControl {
|
|
|
1403
1567
|
this._emit({ type: "statechange", state: this.getState() });
|
|
1404
1568
|
try {
|
|
1405
1569
|
let managedRaster;
|
|
1406
|
-
if (
|
|
1570
|
+
if (isOverlay) {
|
|
1407
1571
|
await this._waitForStyleReady();
|
|
1408
1572
|
if (effectiveMode === "replace") {
|
|
1409
1573
|
this._removeManagedBasemap();
|
|
1410
1574
|
} else {
|
|
1411
1575
|
this._removeManagedRaster(basemap.id);
|
|
1412
1576
|
}
|
|
1413
|
-
managedRaster = this.
|
|
1577
|
+
managedRaster = await this._addOverlay(basemap);
|
|
1414
1578
|
} else {
|
|
1415
1579
|
const styleUrl = this._resolveStyleUrl(basemap);
|
|
1416
1580
|
const styleOptions = this._getStyleOptions(basemap);
|
|
@@ -1457,8 +1621,8 @@ class BasemapControl {
|
|
|
1457
1621
|
// In multiple mode a raster basemap click toggles the overlay on or off.
|
|
1458
1622
|
_selectBasemap(id) {
|
|
1459
1623
|
const basemap = this._basemaps.find((candidate) => candidate.id === id);
|
|
1460
|
-
const
|
|
1461
|
-
if (this._state.allowMultiple &&
|
|
1624
|
+
const isOverlay = (basemap == null ? void 0 : basemap.source.type) === "raster" || (basemap == null ? void 0 : basemap.source.type) === "vector-overlay";
|
|
1625
|
+
if (this._state.allowMultiple && isOverlay) {
|
|
1462
1626
|
this.toggleBasemap(id).catch(() => {
|
|
1463
1627
|
});
|
|
1464
1628
|
return;
|
|
@@ -1706,6 +1870,49 @@ class BasemapControl {
|
|
|
1706
1870
|
})
|
|
1707
1871
|
);
|
|
1708
1872
|
}
|
|
1873
|
+
if (this._hasTomTomBasemaps()) {
|
|
1874
|
+
fields.appendChild(
|
|
1875
|
+
this._createProviderSettingsInput({
|
|
1876
|
+
className: "basemap-control-tomtom-key",
|
|
1877
|
+
type: "password",
|
|
1878
|
+
placeholder: "TomTom API key",
|
|
1879
|
+
ariaLabel: "TomTom API key",
|
|
1880
|
+
value: this._tomtomApiKey,
|
|
1881
|
+
onInput: (value) => {
|
|
1882
|
+
this._tomtomApiKey = value;
|
|
1883
|
+
}
|
|
1884
|
+
})
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
if (this._hasHereBasemaps()) {
|
|
1888
|
+
fields.appendChild(
|
|
1889
|
+
this._createProviderSettingsInput({
|
|
1890
|
+
className: "basemap-control-here-key",
|
|
1891
|
+
type: "password",
|
|
1892
|
+
placeholder: "HERE API key",
|
|
1893
|
+
ariaLabel: "HERE API key",
|
|
1894
|
+
value: this._hereApiKey,
|
|
1895
|
+
onInput: (value) => {
|
|
1896
|
+
this._hereApiKey = value;
|
|
1897
|
+
}
|
|
1898
|
+
})
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
if (this._hasGoogleApiKeyBasemaps()) {
|
|
1902
|
+
fields.appendChild(
|
|
1903
|
+
this._createProviderSettingsInput({
|
|
1904
|
+
className: "basemap-control-google-key",
|
|
1905
|
+
type: "password",
|
|
1906
|
+
placeholder: "Google Maps API key",
|
|
1907
|
+
ariaLabel: "Google Maps API key",
|
|
1908
|
+
value: this._googleMapsApiKey,
|
|
1909
|
+
onInput: (value) => {
|
|
1910
|
+
this._googleMapsApiKey = value;
|
|
1911
|
+
this._googleSessions.clear();
|
|
1912
|
+
}
|
|
1913
|
+
})
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1709
1916
|
if (this._hasAmazonBasemaps()) {
|
|
1710
1917
|
fields.appendChild(
|
|
1711
1918
|
this._createProviderSettingsInput({
|
|
@@ -1885,7 +2092,7 @@ class BasemapControl {
|
|
|
1885
2092
|
main.appendChild(meta);
|
|
1886
2093
|
const type = document.createElement("span");
|
|
1887
2094
|
type.className = "basemap-control-result-type";
|
|
1888
|
-
type.textContent = basemap.type === "raster" ? "Raster" : "Style";
|
|
2095
|
+
type.textContent = basemap.type === "raster" ? "Raster" : basemap.type === "vector-overlay" ? "Overlay" : "Style";
|
|
1889
2096
|
row.appendChild(main);
|
|
1890
2097
|
row.appendChild(type);
|
|
1891
2098
|
if (basemap.attribution) {
|
|
@@ -1916,11 +2123,25 @@ class BasemapControl {
|
|
|
1916
2123
|
_hasMapboxBasemaps() {
|
|
1917
2124
|
return this._basemaps.some((basemap) => basemap.provider === "mapbox");
|
|
1918
2125
|
}
|
|
2126
|
+
_hasTomTomBasemaps() {
|
|
2127
|
+
return this._basemaps.some((basemap) => basemap.provider === "tomtom");
|
|
2128
|
+
}
|
|
2129
|
+
_hasHereBasemaps() {
|
|
2130
|
+
return this._basemaps.some((basemap) => basemap.provider === "here");
|
|
2131
|
+
}
|
|
2132
|
+
// True when a Google basemap needs a Map Tiles API key. The existing Google
|
|
2133
|
+
// raster basemaps (Maps/Satellite/etc.) use keyless tiles, so only session or
|
|
2134
|
+
// api-key based Google layers (e.g. Google Traffic) require the key input.
|
|
2135
|
+
_hasGoogleApiKeyBasemaps() {
|
|
2136
|
+
return this._basemaps.some(
|
|
2137
|
+
(basemap) => basemap.provider === "google" && basemap.source.type === "raster" && (Boolean(basemap.source.googleSession) || basemap.source.tiles.some((tile) => tile.includes(API_KEY_PLACEHOLDER)))
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
1919
2140
|
_hasProviderSettings() {
|
|
1920
|
-
return this._hasMapTilerBasemaps() || this._hasAmazonBasemaps() || this._hasMapboxBasemaps();
|
|
2141
|
+
return this._hasMapTilerBasemaps() || this._hasAmazonBasemaps() || this._hasMapboxBasemaps() || this._hasTomTomBasemaps() || this._hasHereBasemaps() || this._hasGoogleApiKeyBasemaps();
|
|
1921
2142
|
}
|
|
1922
2143
|
_resolveStyleUrl(basemap) {
|
|
1923
|
-
if (basemap.source.type
|
|
2144
|
+
if (basemap.source.type !== "style" && basemap.source.type !== "vector-style") {
|
|
1924
2145
|
throw new Error(`Basemap "${basemap.id}" is not a style basemap.`);
|
|
1925
2146
|
}
|
|
1926
2147
|
const url = basemap.source.url;
|
|
@@ -2048,14 +2269,23 @@ class BasemapControl {
|
|
|
2048
2269
|
_isUrlLikeCredential(value) {
|
|
2049
2270
|
return /^https?:\/\//i.test(value);
|
|
2050
2271
|
}
|
|
2051
|
-
|
|
2272
|
+
// Add a stackable overlay (raster tiles or a vector overlay) to the map,
|
|
2273
|
+
// resolving any provider credentials first.
|
|
2274
|
+
async _addOverlay(basemap) {
|
|
2275
|
+
if (basemap.source.type === "vector-overlay") {
|
|
2276
|
+
return this._addVectorOverlay(basemap, basemap.source);
|
|
2277
|
+
}
|
|
2278
|
+
return this._addRasterBasemap(basemap);
|
|
2279
|
+
}
|
|
2280
|
+
async _addRasterBasemap(basemap) {
|
|
2052
2281
|
if (!this._map || basemap.source.type !== "raster") return void 0;
|
|
2282
|
+
const tiles = await this._resolveRasterTiles(basemap);
|
|
2053
2283
|
const sourceId = `${CONTROL_SOURCE_PREFIX}-${basemap.id}`;
|
|
2054
2284
|
const layerId = [CONTROL_LAYER_PREFIX, basemap.id].filter(Boolean).join("-");
|
|
2055
2285
|
const beforeId = this._getBasemapInsertBeforeId();
|
|
2056
2286
|
const source = {
|
|
2057
2287
|
type: "raster",
|
|
2058
|
-
tiles
|
|
2288
|
+
tiles,
|
|
2059
2289
|
tileSize: basemap.source.tileSize ?? 256,
|
|
2060
2290
|
minzoom: basemap.source.minzoom,
|
|
2061
2291
|
maxzoom: basemap.source.maxzoom,
|
|
@@ -2073,6 +2303,158 @@ class BasemapControl {
|
|
|
2073
2303
|
this._managedRasters.set(basemap.id, managed);
|
|
2074
2304
|
return managed;
|
|
2075
2305
|
}
|
|
2306
|
+
_addVectorOverlay(basemap, overlay) {
|
|
2307
|
+
if (!this._map) return void 0;
|
|
2308
|
+
const sourceId = `${CONTROL_SOURCE_PREFIX}-${basemap.id}`;
|
|
2309
|
+
const layerId = [CONTROL_LAYER_PREFIX, basemap.id].filter(Boolean).join("-");
|
|
2310
|
+
const beforeId = this._getBasemapInsertBeforeId();
|
|
2311
|
+
const source = {
|
|
2312
|
+
type: "vector",
|
|
2313
|
+
...this._resolveVectorOverlaySource(basemap, overlay),
|
|
2314
|
+
minzoom: overlay.minzoom,
|
|
2315
|
+
maxzoom: overlay.maxzoom,
|
|
2316
|
+
attribution: basemap.attribution
|
|
2317
|
+
};
|
|
2318
|
+
const layer = {
|
|
2319
|
+
id: layerId,
|
|
2320
|
+
type: overlay.layerType ?? "line",
|
|
2321
|
+
source: sourceId,
|
|
2322
|
+
"source-layer": overlay.sourceLayer,
|
|
2323
|
+
...overlay.layout ? { layout: overlay.layout } : {},
|
|
2324
|
+
...overlay.paint ? { paint: overlay.paint } : {}
|
|
2325
|
+
};
|
|
2326
|
+
this._map.addSource(sourceId, source);
|
|
2327
|
+
this._map.addLayer(layer, beforeId);
|
|
2328
|
+
const managed = { sourceId, layerId, beforeId };
|
|
2329
|
+
this._managedRasters.set(basemap.id, managed);
|
|
2330
|
+
return managed;
|
|
2331
|
+
}
|
|
2332
|
+
// Resolves the `{url}` or `{tiles}` of a vector overlay, substituting provider
|
|
2333
|
+
// credentials. Mapbox vector overlays are pointed at the v4 TileJSON endpoint
|
|
2334
|
+
// with the access token, reusing the same resolver as Mapbox style sources.
|
|
2335
|
+
_resolveVectorOverlaySource(basemap, overlay) {
|
|
2336
|
+
if (basemap.provider === "mapbox") {
|
|
2337
|
+
const accessToken = this._mapboxAccessToken.trim();
|
|
2338
|
+
if (!accessToken) {
|
|
2339
|
+
throw new MissingCredentialError(
|
|
2340
|
+
"Enter a Mapbox access token before applying this overlay.",
|
|
2341
|
+
"mapbox"
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
if (this._isUrlLikeCredential(accessToken)) {
|
|
2345
|
+
throw new MissingCredentialError(
|
|
2346
|
+
"Enter a valid Mapbox access token, not a URL.",
|
|
2347
|
+
"mapbox"
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
const encoded = encodeURIComponent(accessToken);
|
|
2351
|
+
if (overlay.url) {
|
|
2352
|
+
return { url: this._resolveMapboxInternalUrl(overlay.url, encoded) };
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
if (overlay.url) return { url: overlay.url };
|
|
2356
|
+
if (overlay.tiles) return { tiles: overlay.tiles };
|
|
2357
|
+
throw new Error(`Vector overlay "${basemap.id}" has no url or tiles.`);
|
|
2358
|
+
}
|
|
2359
|
+
// Resolves the tile templates of a raster basemap, substituting provider
|
|
2360
|
+
// credentials (and, for Google, a Map Tiles API session token). Throws a
|
|
2361
|
+
// MissingCredentialError when a required key is absent so the panel can
|
|
2362
|
+
// surface a "Get a ..." link and reveal the credential inputs.
|
|
2363
|
+
async _resolveRasterTiles(basemap) {
|
|
2364
|
+
if (basemap.source.type !== "raster") return [];
|
|
2365
|
+
const { tiles, googleSession } = basemap.source;
|
|
2366
|
+
if (googleSession) {
|
|
2367
|
+
return this._resolveGoogleSessionTiles(tiles, googleSession);
|
|
2368
|
+
}
|
|
2369
|
+
if (!tiles.some((tile) => tile.includes(API_KEY_PLACEHOLDER))) {
|
|
2370
|
+
return tiles;
|
|
2371
|
+
}
|
|
2372
|
+
const apiKey = this._rasterApiKeyFor(basemap.provider);
|
|
2373
|
+
if (!apiKey) {
|
|
2374
|
+
throw this._missingRasterKeyError(basemap.provider);
|
|
2375
|
+
}
|
|
2376
|
+
const encoded = encodeURIComponent(apiKey);
|
|
2377
|
+
return tiles.map((tile) => tile.split(API_KEY_PLACEHOLDER).join(encoded));
|
|
2378
|
+
}
|
|
2379
|
+
_rasterApiKeyFor(provider) {
|
|
2380
|
+
if (provider === "tomtom") return this._tomtomApiKey.trim();
|
|
2381
|
+
if (provider === "here") return this._hereApiKey.trim();
|
|
2382
|
+
if (provider === "google") return this._googleMapsApiKey.trim();
|
|
2383
|
+
return "";
|
|
2384
|
+
}
|
|
2385
|
+
_missingRasterKeyError(provider) {
|
|
2386
|
+
const label = provider === "tomtom" ? "TomTom API key" : provider === "here" ? "HERE API key" : provider === "google" ? "Google Maps API key" : "API key";
|
|
2387
|
+
const helpProvider = provider in PROVIDER_CREDENTIAL_HELP ? provider : "maptiler";
|
|
2388
|
+
return new MissingCredentialError(`Enter a ${label} before applying this layer.`, helpProvider);
|
|
2389
|
+
}
|
|
2390
|
+
// Resolves Google tile templates by minting (or reusing) a Map Tiles API
|
|
2391
|
+
// session token, then substituting both `{session}` and `{api-key}`.
|
|
2392
|
+
async _resolveGoogleSessionTiles(tiles, config) {
|
|
2393
|
+
const apiKey = this._googleMapsApiKey.trim();
|
|
2394
|
+
if (!apiKey) {
|
|
2395
|
+
throw new MissingCredentialError(
|
|
2396
|
+
"Enter a Google Maps API key before applying this layer.",
|
|
2397
|
+
"google"
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
const token = await this._getGoogleSessionToken(apiKey, config);
|
|
2401
|
+
const encodedKey = encodeURIComponent(apiKey);
|
|
2402
|
+
const encodedSession = encodeURIComponent(token);
|
|
2403
|
+
return tiles.map(
|
|
2404
|
+
(tile) => tile.split("{session}").join(encodedSession).split(API_KEY_PLACEHOLDER).join(encodedKey)
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
async _getGoogleSessionToken(apiKey, config) {
|
|
2408
|
+
const cacheKey = `${apiKey}:${JSON.stringify(config)}`;
|
|
2409
|
+
const cached = this._googleSessions.get(cacheKey);
|
|
2410
|
+
if (cached && cached.expiry - 6e4 > Date.now()) {
|
|
2411
|
+
return cached.token;
|
|
2412
|
+
}
|
|
2413
|
+
const response = await fetch(
|
|
2414
|
+
`https://tile.googleapis.com/v1/createSession?key=${encodeURIComponent(apiKey)}`,
|
|
2415
|
+
{
|
|
2416
|
+
method: "POST",
|
|
2417
|
+
headers: { "Content-Type": "application/json" },
|
|
2418
|
+
body: JSON.stringify({
|
|
2419
|
+
mapType: config.mapType,
|
|
2420
|
+
language: config.language ?? "en-US",
|
|
2421
|
+
region: config.region ?? "US",
|
|
2422
|
+
layerTypes: config.layerTypes,
|
|
2423
|
+
overlay: config.overlay
|
|
2424
|
+
})
|
|
2425
|
+
}
|
|
2426
|
+
);
|
|
2427
|
+
if (!response.ok) {
|
|
2428
|
+
const detail = await response.text().catch(() => "");
|
|
2429
|
+
throw new MissingCredentialError(
|
|
2430
|
+
`Google could not create a tile session (HTTP ${response.status}). ${this._summarizeGoogleError(
|
|
2431
|
+
detail
|
|
2432
|
+
)}`.trim(),
|
|
2433
|
+
"google"
|
|
2434
|
+
);
|
|
2435
|
+
}
|
|
2436
|
+
const data = await response.json();
|
|
2437
|
+
if (!data.session) {
|
|
2438
|
+
throw new MissingCredentialError(
|
|
2439
|
+
"Google did not return a tile session token. Check that the Map Tiles API is enabled for your key.",
|
|
2440
|
+
"google"
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
const expirySeconds = data.expiry ? Number(data.expiry) : NaN;
|
|
2444
|
+
const expiry = Number.isFinite(expirySeconds) ? expirySeconds * 1e3 : Date.now() + 60 * 60 * 1e3;
|
|
2445
|
+
this._googleSessions.set(cacheKey, { token: data.session, expiry });
|
|
2446
|
+
return data.session;
|
|
2447
|
+
}
|
|
2448
|
+
_summarizeGoogleError(detail) {
|
|
2449
|
+
var _a;
|
|
2450
|
+
if (!detail) return "";
|
|
2451
|
+
try {
|
|
2452
|
+
const parsed = JSON.parse(detail);
|
|
2453
|
+
return ((_a = parsed.error) == null ? void 0 : _a.message) ?? "";
|
|
2454
|
+
} catch {
|
|
2455
|
+
return detail.slice(0, 200);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2076
2458
|
_waitForStyleReady() {
|
|
2077
2459
|
if (!this._map) return Promise.resolve();
|
|
2078
2460
|
if (typeof this._map.isStyleLoaded !== "function" || this._map.isStyleLoaded()) {
|
|
@@ -2237,4 +2619,4 @@ export {
|
|
|
2237
2619
|
getBasemapCategories as g,
|
|
2238
2620
|
resolveBasemapProviders as r
|
|
2239
2621
|
};
|
|
2240
|
-
//# sourceMappingURL=BasemapControl-
|
|
2622
|
+
//# sourceMappingURL=BasemapControl-CnjmO-_2.js.map
|