maplibre-gl-basemap-control 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,15 +7,18 @@ 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" },
13
14
  { id: "nlmaps", name: "nlmaps", category: "Regional" },
15
+ { id: "openbasiskaart", name: "Openbasiskaart", category: "Regional" },
14
16
  { id: "openfreemap", name: "OpenFreeMap", category: "Vector Styles" },
15
17
  { id: "openrailwaymap", name: "OpenRailwayMap", category: "Transport" },
16
18
  { id: "openstreetmap", name: "OpenStreetMap", category: "Community" },
17
19
  { id: "opentopomap", name: "OpenTopoMap", category: "Terrain" },
18
20
  { id: "swisstopo", name: "Swiss Federal Geoportal", category: "Regional" },
21
+ { id: "tomtom", name: "TomTom", category: "Traffic" },
19
22
  { id: "topplusopen", name: "TopPlusOpen", category: "Regional" },
20
23
  { id: "usgs", name: "USGS", category: "United States" },
21
24
  { id: "waymarkedtrails", name: "Waymarked Trails", category: "Outdoor" }
@@ -143,6 +146,44 @@ const MAPBOX_ATTRIBUTION = "© Mapbox © OpenStreetMap contributors";
143
146
  const MAPTILER_ATTRIBUTION = "© MapTiler © OpenStreetMap contributors";
144
147
  const OSM_ATTRIBUTION = "© OpenStreetMap contributors";
145
148
  const OPENFREEMAP_ATTRIBUTION = "OpenFreeMap © OpenMapTiles Data from OpenStreetMap";
149
+ const TOMTOM_ATTRIBUTION = "© TomTom";
150
+ const HERE_ATTRIBUTION = "© HERE";
151
+ const GOOGLE_ATTRIBUTION = "© Google";
152
+ const MAPBOX_TRAFFIC_CONGESTION_COLORS = [
153
+ "match",
154
+ ["get", "congestion"],
155
+ "low",
156
+ "#4caf50",
157
+ "moderate",
158
+ "#ffc107",
159
+ "heavy",
160
+ "#ff5722",
161
+ "severe",
162
+ "#b71c1c",
163
+ "#9e9e9e"
164
+ ];
165
+ function rasterTrafficBasemap({
166
+ id,
167
+ name,
168
+ provider,
169
+ description,
170
+ attribution,
171
+ tiles,
172
+ maxzoom = 22,
173
+ tags = []
174
+ }) {
175
+ return rasterBasemap({
176
+ id,
177
+ name,
178
+ provider,
179
+ category: "Traffic",
180
+ description,
181
+ attribution,
182
+ tiles,
183
+ maxzoom,
184
+ tags: ["traffic", "overlay", ...tags]
185
+ });
186
+ }
146
187
  const DEFAULT_BASEMAPS = [
147
188
  rasterBasemap({
148
189
  id: "osm-standard",
@@ -959,6 +1000,21 @@ const DEFAULT_BASEMAPS = [
959
1000
  ],
960
1001
  tags: ["netherlands", "imagery", "aerial"]
961
1002
  }),
1003
+ rasterBasemap({
1004
+ id: "openbasiskaart",
1005
+ name: "Openbasiskaart",
1006
+ provider: "openbasiskaart",
1007
+ category: "Regional",
1008
+ description: "Netherlands topographic background map based on OpenStreetMap data.",
1009
+ attribution: "Map data © OpenStreetMap contributors | Openbasiskaart",
1010
+ // The Google Maps Compatible (osm-g) layer uses the standard Web Mercator
1011
+ // grid, so its WMTS {TileMatrix}/{TileRow}/{TileCol} maps directly to xyz.
1012
+ tiles: [
1013
+ "https://www.openbasiskaart.nl/mapcache/wmts/1.0.0/osm-g/default/g/{z}/{y}/{x}.png"
1014
+ ],
1015
+ maxzoom: 18,
1016
+ tags: ["netherlands", "regional", "topographic", "osm", "openbasiskaart"]
1017
+ }),
962
1018
  {
963
1019
  id: "openfreemap-positron",
964
1020
  name: "OpenFreeMap Positron",
@@ -1048,6 +1104,89 @@ const DEFAULT_BASEMAPS = [
1048
1104
  pitch: 60
1049
1105
  },
1050
1106
  tags: ["openfreemap", "vector", "style", "3d", "liberty"]
1107
+ },
1108
+ rasterTrafficBasemap({
1109
+ id: "tomtom-traffic-flow-relative",
1110
+ name: "TomTom Traffic Flow",
1111
+ provider: "tomtom",
1112
+ description: "TomTom traffic speeds relative to free-flow speed. Requires a TomTom API key.",
1113
+ attribution: TOMTOM_ATTRIBUTION,
1114
+ tiles: ["https://api.tomtom.com/traffic/map/4/tile/flow/relative0/{z}/{x}/{y}.png?key={api-key}"],
1115
+ tags: ["tomtom", "flow", "relative"]
1116
+ }),
1117
+ rasterTrafficBasemap({
1118
+ id: "tomtom-traffic-flow-absolute",
1119
+ name: "TomTom Traffic Flow Absolute",
1120
+ provider: "tomtom",
1121
+ description: "TomTom traffic colored by absolute speed. Requires a TomTom API key.",
1122
+ attribution: TOMTOM_ATTRIBUTION,
1123
+ tiles: ["https://api.tomtom.com/traffic/map/4/tile/flow/absolute/{z}/{x}/{y}.png?key={api-key}"],
1124
+ tags: ["tomtom", "flow", "absolute"]
1125
+ }),
1126
+ rasterTrafficBasemap({
1127
+ id: "tomtom-traffic-flow-relative-delay",
1128
+ name: "TomTom Traffic Flow Delay",
1129
+ provider: "tomtom",
1130
+ description: "TomTom traffic highlighting only roads delayed below free-flow speed. Requires a TomTom API key.",
1131
+ attribution: TOMTOM_ATTRIBUTION,
1132
+ tiles: [
1133
+ "https://api.tomtom.com/traffic/map/4/tile/flow/relative-delay/{z}/{x}/{y}.png?key={api-key}"
1134
+ ],
1135
+ tags: ["tomtom", "flow", "delay"]
1136
+ }),
1137
+ rasterTrafficBasemap({
1138
+ id: "here-traffic-flow",
1139
+ name: "HERE Traffic Flow",
1140
+ provider: "here",
1141
+ description: "HERE real-time traffic flow overlay. Requires a HERE API key.",
1142
+ attribution: HERE_ATTRIBUTION,
1143
+ tiles: ["https://traffic.maps.hereapi.com/v3/flow/mc/{z}/{x}/{y}/png?apiKey={api-key}"],
1144
+ tags: ["here", "flow"]
1145
+ }),
1146
+ {
1147
+ id: "mapbox-traffic",
1148
+ name: "Mapbox Traffic",
1149
+ provider: "mapbox",
1150
+ type: "vector-overlay",
1151
+ category: "Traffic",
1152
+ description: "Mapbox real-time traffic congestion overlay. Requires a Mapbox access token.",
1153
+ attribution: MAPBOX_ATTRIBUTION,
1154
+ source: {
1155
+ type: "vector-overlay",
1156
+ url: "mapbox://mapbox.mapbox-traffic-v1",
1157
+ sourceLayer: "traffic",
1158
+ layerType: "line",
1159
+ layout: {
1160
+ "line-join": "round",
1161
+ "line-cap": "round"
1162
+ },
1163
+ paint: {
1164
+ "line-width": ["interpolate", ["linear"], ["zoom"], 6, 1.5, 14, 4, 18, 8],
1165
+ "line-color": MAPBOX_TRAFFIC_CONGESTION_COLORS
1166
+ }
1167
+ },
1168
+ tags: ["mapbox", "traffic", "overlay", "congestion"]
1169
+ },
1170
+ {
1171
+ id: "google-traffic",
1172
+ name: "Google Traffic",
1173
+ provider: "google",
1174
+ type: "raster",
1175
+ category: "Traffic",
1176
+ description: "Google real-time traffic overlay via the Map Tiles API. Requires a Google Maps API key.",
1177
+ attribution: GOOGLE_ATTRIBUTION,
1178
+ source: {
1179
+ type: "raster",
1180
+ tiles: ["https://tile.googleapis.com/v1/2dtiles/{z}/{x}/{y}?session={session}&key={api-key}"],
1181
+ tileSize: 256,
1182
+ maxzoom: 22,
1183
+ googleSession: {
1184
+ mapType: "roadmap",
1185
+ layerTypes: ["layerTraffic"],
1186
+ overlay: true
1187
+ }
1188
+ },
1189
+ tags: ["google", "traffic", "overlay"]
1051
1190
  }
1052
1191
  ];
1053
1192
  function combineProviders(defaults, custom = []) {
@@ -1125,6 +1264,18 @@ const PROVIDER_CREDENTIAL_HELP = {
1125
1264
  mapbox: {
1126
1265
  url: "https://docs.mapbox.com/help/getting-started/access-tokens/",
1127
1266
  label: "Get a Mapbox access token"
1267
+ },
1268
+ tomtom: {
1269
+ url: "https://developer.tomtom.com/how-to-get-tomtom-api-key",
1270
+ label: "Get a TomTom API key"
1271
+ },
1272
+ here: {
1273
+ url: "https://www.here.com/get-started/pricing",
1274
+ label: "Get a HERE API key"
1275
+ },
1276
+ google: {
1277
+ url: "https://developers.google.com/maps/documentation/tile/get-api-key",
1278
+ label: "Get a Google Maps API key"
1128
1279
  }
1129
1280
  };
1130
1281
  const MIN_PANEL_WIDTH = 240;
@@ -1158,6 +1309,13 @@ class BasemapControl {
1158
1309
  __publicField(this, "_amazonApiKey", "");
1159
1310
  __publicField(this, "_awsRegion", "");
1160
1311
  __publicField(this, "_mapboxAccessToken", "");
1312
+ __publicField(this, "_tomtomApiKey", "");
1313
+ __publicField(this, "_hereApiKey", "");
1314
+ __publicField(this, "_googleMapsApiKey", "");
1315
+ // Cached Google Map Tiles API session tokens keyed by their session config, so
1316
+ // a traffic overlay reuses a token until it expires instead of creating a new
1317
+ // session on every add. Keyed by the serialized config + API key.
1318
+ __publicField(this, "_googleSessions", new globalThis.Map());
1161
1319
  __publicField(this, "_providerSettingsCollapsed", true);
1162
1320
  // Provider of the most recent missing-credential error, used to pick the
1163
1321
  // matching help link. Kept in sync with `_state.error` (both are set together
@@ -1197,6 +1355,9 @@ class BasemapControl {
1197
1355
  this._amazonApiKey = (options == null ? void 0 : options.amazonApiKey) ?? "";
1198
1356
  this._awsRegion = (options == null ? void 0 : options.awsRegion) ?? "us-east-1";
1199
1357
  this._mapboxAccessToken = (options == null ? void 0 : options.mapboxAccessToken) ?? "";
1358
+ this._tomtomApiKey = (options == null ? void 0 : options.tomtomApiKey) ?? "";
1359
+ this._hereApiKey = (options == null ? void 0 : options.hereApiKey) ?? "";
1360
+ this._googleMapsApiKey = (options == null ? void 0 : options.googleMapsApiKey) ?? "";
1200
1361
  this._basemaps = createBasemapCatalog(
1201
1362
  options == null ? void 0 : options.basemaps,
1202
1363
  this._options.includeDefaultBasemaps
@@ -1324,6 +1485,25 @@ class BasemapControl {
1324
1485
  this._renderContent();
1325
1486
  this._emit({ type: "statechange", state: this.getState() });
1326
1487
  }
1488
+ setTomTomApiKey(apiKey) {
1489
+ this._tomtomApiKey = apiKey;
1490
+ this._state = { ...this._state, error: void 0 };
1491
+ this._renderContent();
1492
+ this._emit({ type: "statechange", state: this.getState() });
1493
+ }
1494
+ setHereApiKey(apiKey) {
1495
+ this._hereApiKey = apiKey;
1496
+ this._state = { ...this._state, error: void 0 };
1497
+ this._renderContent();
1498
+ this._emit({ type: "statechange", state: this.getState() });
1499
+ }
1500
+ setGoogleMapsApiKey(apiKey) {
1501
+ this._googleMapsApiKey = apiKey;
1502
+ this._googleSessions.clear();
1503
+ this._state = { ...this._state, error: void 0 };
1504
+ this._renderContent();
1505
+ this._emit({ type: "statechange", state: this.getState() });
1506
+ }
1327
1507
  getActiveBasemap() {
1328
1508
  return this._basemaps.find((basemap) => basemap.id === this._state.activeBasemapId);
1329
1509
  }
@@ -1386,9 +1566,9 @@ class BasemapControl {
1386
1566
  this._handleError(error, basemap);
1387
1567
  throw error;
1388
1568
  }
1389
- const isRaster = basemap.source.type === "raster";
1390
- const effectiveMode = isRaster ? mode : "replace";
1391
- if (!isRaster && this._state.allowMultiple && this._managedRasters.size > 0 && this._options.confirmStyleReplace) {
1569
+ const isOverlay = basemap.source.type === "raster" || basemap.source.type === "vector-overlay";
1570
+ const effectiveMode = isOverlay ? mode : "replace";
1571
+ if (!isOverlay && this._state.allowMultiple && this._managedRasters.size > 0 && this._options.confirmStyleReplace) {
1392
1572
  const replacedBasemapIds = [...this._managedRasters.keys()];
1393
1573
  let confirmed = false;
1394
1574
  try {
@@ -1403,14 +1583,14 @@ class BasemapControl {
1403
1583
  this._emit({ type: "statechange", state: this.getState() });
1404
1584
  try {
1405
1585
  let managedRaster;
1406
- if (isRaster) {
1586
+ if (isOverlay) {
1407
1587
  await this._waitForStyleReady();
1408
1588
  if (effectiveMode === "replace") {
1409
1589
  this._removeManagedBasemap();
1410
1590
  } else {
1411
1591
  this._removeManagedRaster(basemap.id);
1412
1592
  }
1413
- managedRaster = this._addRasterBasemap(basemap);
1593
+ managedRaster = await this._addOverlay(basemap);
1414
1594
  } else {
1415
1595
  const styleUrl = this._resolveStyleUrl(basemap);
1416
1596
  const styleOptions = this._getStyleOptions(basemap);
@@ -1457,8 +1637,8 @@ class BasemapControl {
1457
1637
  // In multiple mode a raster basemap click toggles the overlay on or off.
1458
1638
  _selectBasemap(id) {
1459
1639
  const basemap = this._basemaps.find((candidate) => candidate.id === id);
1460
- const isRaster = (basemap == null ? void 0 : basemap.source.type) === "raster";
1461
- if (this._state.allowMultiple && isRaster) {
1640
+ const isOverlay = (basemap == null ? void 0 : basemap.source.type) === "raster" || (basemap == null ? void 0 : basemap.source.type) === "vector-overlay";
1641
+ if (this._state.allowMultiple && isOverlay) {
1462
1642
  this.toggleBasemap(id).catch(() => {
1463
1643
  });
1464
1644
  return;
@@ -1706,6 +1886,49 @@ class BasemapControl {
1706
1886
  })
1707
1887
  );
1708
1888
  }
1889
+ if (this._hasTomTomBasemaps()) {
1890
+ fields.appendChild(
1891
+ this._createProviderSettingsInput({
1892
+ className: "basemap-control-tomtom-key",
1893
+ type: "password",
1894
+ placeholder: "TomTom API key",
1895
+ ariaLabel: "TomTom API key",
1896
+ value: this._tomtomApiKey,
1897
+ onInput: (value) => {
1898
+ this._tomtomApiKey = value;
1899
+ }
1900
+ })
1901
+ );
1902
+ }
1903
+ if (this._hasHereBasemaps()) {
1904
+ fields.appendChild(
1905
+ this._createProviderSettingsInput({
1906
+ className: "basemap-control-here-key",
1907
+ type: "password",
1908
+ placeholder: "HERE API key",
1909
+ ariaLabel: "HERE API key",
1910
+ value: this._hereApiKey,
1911
+ onInput: (value) => {
1912
+ this._hereApiKey = value;
1913
+ }
1914
+ })
1915
+ );
1916
+ }
1917
+ if (this._hasGoogleApiKeyBasemaps()) {
1918
+ fields.appendChild(
1919
+ this._createProviderSettingsInput({
1920
+ className: "basemap-control-google-key",
1921
+ type: "password",
1922
+ placeholder: "Google Maps API key",
1923
+ ariaLabel: "Google Maps API key",
1924
+ value: this._googleMapsApiKey,
1925
+ onInput: (value) => {
1926
+ this._googleMapsApiKey = value;
1927
+ this._googleSessions.clear();
1928
+ }
1929
+ })
1930
+ );
1931
+ }
1709
1932
  if (this._hasAmazonBasemaps()) {
1710
1933
  fields.appendChild(
1711
1934
  this._createProviderSettingsInput({
@@ -1885,7 +2108,7 @@ class BasemapControl {
1885
2108
  main.appendChild(meta);
1886
2109
  const type = document.createElement("span");
1887
2110
  type.className = "basemap-control-result-type";
1888
- type.textContent = basemap.type === "raster" ? "Raster" : "Style";
2111
+ type.textContent = basemap.type === "raster" ? "Raster" : basemap.type === "vector-overlay" ? "Overlay" : "Style";
1889
2112
  row.appendChild(main);
1890
2113
  row.appendChild(type);
1891
2114
  if (basemap.attribution) {
@@ -1916,11 +2139,25 @@ class BasemapControl {
1916
2139
  _hasMapboxBasemaps() {
1917
2140
  return this._basemaps.some((basemap) => basemap.provider === "mapbox");
1918
2141
  }
2142
+ _hasTomTomBasemaps() {
2143
+ return this._basemaps.some((basemap) => basemap.provider === "tomtom");
2144
+ }
2145
+ _hasHereBasemaps() {
2146
+ return this._basemaps.some((basemap) => basemap.provider === "here");
2147
+ }
2148
+ // True when a Google basemap needs a Map Tiles API key. The existing Google
2149
+ // raster basemaps (Maps/Satellite/etc.) use keyless tiles, so only session or
2150
+ // api-key based Google layers (e.g. Google Traffic) require the key input.
2151
+ _hasGoogleApiKeyBasemaps() {
2152
+ return this._basemaps.some(
2153
+ (basemap) => basemap.provider === "google" && basemap.source.type === "raster" && (Boolean(basemap.source.googleSession) || basemap.source.tiles.some((tile) => tile.includes(API_KEY_PLACEHOLDER)))
2154
+ );
2155
+ }
1919
2156
  _hasProviderSettings() {
1920
- return this._hasMapTilerBasemaps() || this._hasAmazonBasemaps() || this._hasMapboxBasemaps();
2157
+ return this._hasMapTilerBasemaps() || this._hasAmazonBasemaps() || this._hasMapboxBasemaps() || this._hasTomTomBasemaps() || this._hasHereBasemaps() || this._hasGoogleApiKeyBasemaps();
1921
2158
  }
1922
2159
  _resolveStyleUrl(basemap) {
1923
- if (basemap.source.type === "raster") {
2160
+ if (basemap.source.type !== "style" && basemap.source.type !== "vector-style") {
1924
2161
  throw new Error(`Basemap "${basemap.id}" is not a style basemap.`);
1925
2162
  }
1926
2163
  const url = basemap.source.url;
@@ -2048,14 +2285,23 @@ class BasemapControl {
2048
2285
  _isUrlLikeCredential(value) {
2049
2286
  return /^https?:\/\//i.test(value);
2050
2287
  }
2051
- _addRasterBasemap(basemap) {
2288
+ // Add a stackable overlay (raster tiles or a vector overlay) to the map,
2289
+ // resolving any provider credentials first.
2290
+ async _addOverlay(basemap) {
2291
+ if (basemap.source.type === "vector-overlay") {
2292
+ return this._addVectorOverlay(basemap, basemap.source);
2293
+ }
2294
+ return this._addRasterBasemap(basemap);
2295
+ }
2296
+ async _addRasterBasemap(basemap) {
2052
2297
  if (!this._map || basemap.source.type !== "raster") return void 0;
2298
+ const tiles = await this._resolveRasterTiles(basemap);
2053
2299
  const sourceId = `${CONTROL_SOURCE_PREFIX}-${basemap.id}`;
2054
2300
  const layerId = [CONTROL_LAYER_PREFIX, basemap.id].filter(Boolean).join("-");
2055
2301
  const beforeId = this._getBasemapInsertBeforeId();
2056
2302
  const source = {
2057
2303
  type: "raster",
2058
- tiles: basemap.source.tiles,
2304
+ tiles,
2059
2305
  tileSize: basemap.source.tileSize ?? 256,
2060
2306
  minzoom: basemap.source.minzoom,
2061
2307
  maxzoom: basemap.source.maxzoom,
@@ -2073,6 +2319,158 @@ class BasemapControl {
2073
2319
  this._managedRasters.set(basemap.id, managed);
2074
2320
  return managed;
2075
2321
  }
2322
+ _addVectorOverlay(basemap, overlay) {
2323
+ if (!this._map) return void 0;
2324
+ const sourceId = `${CONTROL_SOURCE_PREFIX}-${basemap.id}`;
2325
+ const layerId = [CONTROL_LAYER_PREFIX, basemap.id].filter(Boolean).join("-");
2326
+ const beforeId = this._getBasemapInsertBeforeId();
2327
+ const source = {
2328
+ type: "vector",
2329
+ ...this._resolveVectorOverlaySource(basemap, overlay),
2330
+ minzoom: overlay.minzoom,
2331
+ maxzoom: overlay.maxzoom,
2332
+ attribution: basemap.attribution
2333
+ };
2334
+ const layer = {
2335
+ id: layerId,
2336
+ type: overlay.layerType ?? "line",
2337
+ source: sourceId,
2338
+ "source-layer": overlay.sourceLayer,
2339
+ ...overlay.layout ? { layout: overlay.layout } : {},
2340
+ ...overlay.paint ? { paint: overlay.paint } : {}
2341
+ };
2342
+ this._map.addSource(sourceId, source);
2343
+ this._map.addLayer(layer, beforeId);
2344
+ const managed = { sourceId, layerId, beforeId };
2345
+ this._managedRasters.set(basemap.id, managed);
2346
+ return managed;
2347
+ }
2348
+ // Resolves the `{url}` or `{tiles}` of a vector overlay, substituting provider
2349
+ // credentials. Mapbox vector overlays are pointed at the v4 TileJSON endpoint
2350
+ // with the access token, reusing the same resolver as Mapbox style sources.
2351
+ _resolveVectorOverlaySource(basemap, overlay) {
2352
+ if (basemap.provider === "mapbox") {
2353
+ const accessToken = this._mapboxAccessToken.trim();
2354
+ if (!accessToken) {
2355
+ throw new MissingCredentialError(
2356
+ "Enter a Mapbox access token before applying this overlay.",
2357
+ "mapbox"
2358
+ );
2359
+ }
2360
+ if (this._isUrlLikeCredential(accessToken)) {
2361
+ throw new MissingCredentialError(
2362
+ "Enter a valid Mapbox access token, not a URL.",
2363
+ "mapbox"
2364
+ );
2365
+ }
2366
+ const encoded = encodeURIComponent(accessToken);
2367
+ if (overlay.url) {
2368
+ return { url: this._resolveMapboxInternalUrl(overlay.url, encoded) };
2369
+ }
2370
+ }
2371
+ if (overlay.url) return { url: overlay.url };
2372
+ if (overlay.tiles) return { tiles: overlay.tiles };
2373
+ throw new Error(`Vector overlay "${basemap.id}" has no url or tiles.`);
2374
+ }
2375
+ // Resolves the tile templates of a raster basemap, substituting provider
2376
+ // credentials (and, for Google, a Map Tiles API session token). Throws a
2377
+ // MissingCredentialError when a required key is absent so the panel can
2378
+ // surface a "Get a ..." link and reveal the credential inputs.
2379
+ async _resolveRasterTiles(basemap) {
2380
+ if (basemap.source.type !== "raster") return [];
2381
+ const { tiles, googleSession } = basemap.source;
2382
+ if (googleSession) {
2383
+ return this._resolveGoogleSessionTiles(tiles, googleSession);
2384
+ }
2385
+ if (!tiles.some((tile) => tile.includes(API_KEY_PLACEHOLDER))) {
2386
+ return tiles;
2387
+ }
2388
+ const apiKey = this._rasterApiKeyFor(basemap.provider);
2389
+ if (!apiKey) {
2390
+ throw this._missingRasterKeyError(basemap.provider);
2391
+ }
2392
+ const encoded = encodeURIComponent(apiKey);
2393
+ return tiles.map((tile) => tile.split(API_KEY_PLACEHOLDER).join(encoded));
2394
+ }
2395
+ _rasterApiKeyFor(provider) {
2396
+ if (provider === "tomtom") return this._tomtomApiKey.trim();
2397
+ if (provider === "here") return this._hereApiKey.trim();
2398
+ if (provider === "google") return this._googleMapsApiKey.trim();
2399
+ return "";
2400
+ }
2401
+ _missingRasterKeyError(provider) {
2402
+ const label = provider === "tomtom" ? "TomTom API key" : provider === "here" ? "HERE API key" : provider === "google" ? "Google Maps API key" : "API key";
2403
+ const helpProvider = provider in PROVIDER_CREDENTIAL_HELP ? provider : "maptiler";
2404
+ return new MissingCredentialError(`Enter a ${label} before applying this layer.`, helpProvider);
2405
+ }
2406
+ // Resolves Google tile templates by minting (or reusing) a Map Tiles API
2407
+ // session token, then substituting both `{session}` and `{api-key}`.
2408
+ async _resolveGoogleSessionTiles(tiles, config) {
2409
+ const apiKey = this._googleMapsApiKey.trim();
2410
+ if (!apiKey) {
2411
+ throw new MissingCredentialError(
2412
+ "Enter a Google Maps API key before applying this layer.",
2413
+ "google"
2414
+ );
2415
+ }
2416
+ const token = await this._getGoogleSessionToken(apiKey, config);
2417
+ const encodedKey = encodeURIComponent(apiKey);
2418
+ const encodedSession = encodeURIComponent(token);
2419
+ return tiles.map(
2420
+ (tile) => tile.split("{session}").join(encodedSession).split(API_KEY_PLACEHOLDER).join(encodedKey)
2421
+ );
2422
+ }
2423
+ async _getGoogleSessionToken(apiKey, config) {
2424
+ const cacheKey = `${apiKey}:${JSON.stringify(config)}`;
2425
+ const cached = this._googleSessions.get(cacheKey);
2426
+ if (cached && cached.expiry - 6e4 > Date.now()) {
2427
+ return cached.token;
2428
+ }
2429
+ const response = await fetch(
2430
+ `https://tile.googleapis.com/v1/createSession?key=${encodeURIComponent(apiKey)}`,
2431
+ {
2432
+ method: "POST",
2433
+ headers: { "Content-Type": "application/json" },
2434
+ body: JSON.stringify({
2435
+ mapType: config.mapType,
2436
+ language: config.language ?? "en-US",
2437
+ region: config.region ?? "US",
2438
+ layerTypes: config.layerTypes,
2439
+ overlay: config.overlay
2440
+ })
2441
+ }
2442
+ );
2443
+ if (!response.ok) {
2444
+ const detail = await response.text().catch(() => "");
2445
+ throw new MissingCredentialError(
2446
+ `Google could not create a tile session (HTTP ${response.status}). ${this._summarizeGoogleError(
2447
+ detail
2448
+ )}`.trim(),
2449
+ "google"
2450
+ );
2451
+ }
2452
+ const data = await response.json();
2453
+ if (!data.session) {
2454
+ throw new MissingCredentialError(
2455
+ "Google did not return a tile session token. Check that the Map Tiles API is enabled for your key.",
2456
+ "google"
2457
+ );
2458
+ }
2459
+ const expirySeconds = data.expiry ? Number(data.expiry) : NaN;
2460
+ const expiry = Number.isFinite(expirySeconds) ? expirySeconds * 1e3 : Date.now() + 60 * 60 * 1e3;
2461
+ this._googleSessions.set(cacheKey, { token: data.session, expiry });
2462
+ return data.session;
2463
+ }
2464
+ _summarizeGoogleError(detail) {
2465
+ var _a;
2466
+ if (!detail) return "";
2467
+ try {
2468
+ const parsed = JSON.parse(detail);
2469
+ return ((_a = parsed.error) == null ? void 0 : _a.message) ?? "";
2470
+ } catch {
2471
+ return detail.slice(0, 200);
2472
+ }
2473
+ }
2076
2474
  _waitForStyleReady() {
2077
2475
  if (!this._map) return Promise.resolve();
2078
2476
  if (typeof this._map.isStyleLoaded !== "function" || this._map.isStyleLoaded()) {
@@ -2237,4 +2635,4 @@ export {
2237
2635
  getBasemapCategories as g,
2238
2636
  resolveBasemapProviders as r
2239
2637
  };
2240
- //# sourceMappingURL=BasemapControl-BITP2bjU.js.map
2638
+ //# sourceMappingURL=BasemapControl-CfFepxWc.js.map