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 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 isRaster = basemap.source.type === "raster";
1390
- const effectiveMode = isRaster ? mode : "replace";
1391
- if (!isRaster && this._state.allowMultiple && this._managedRasters.size > 0 && this._options.confirmStyleReplace) {
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 (isRaster) {
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._addRasterBasemap(basemap);
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 isRaster = (basemap == null ? void 0 : basemap.source.type) === "raster";
1461
- if (this._state.allowMultiple && isRaster) {
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 === "raster") {
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
- _addRasterBasemap(basemap) {
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: basemap.source.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-BITP2bjU.js.map
2622
+ //# sourceMappingURL=BasemapControl-CnjmO-_2.js.map