maplibre-gl-basemap-control 0.10.0 → 0.12.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
@@ -213,6 +213,21 @@ key must have the Map Tiles API enabled.
213
213
  > key" or add **Map Tiles API** to the allowed list, then save (changes can take a
214
214
  > few minutes to propagate). Make sure billing is enabled on the project too.
215
215
 
216
+ The same `googleMapsApiKey` also upgrades the base Google basemaps
217
+ (`google-maps`, `google-satellite`, `google-terrain`, `google-hybrid`). When a
218
+ key is set they load from the authorized Map Tiles API (via a tile session, like
219
+ Google Traffic); without a key they fall back to keyless `mt1.google.com` xyz
220
+ tiles so they keep working out of the box.
221
+
222
+ > **Licensing caveat.** The keyless `mt1.google.com` tiles are Google Maps'
223
+ > internal endpoints. They are **not** covered by any public or open license, and
224
+ > accessing Google map content outside an official Google Maps Platform API
225
+ > violates the [Google Maps Platform Terms of Service](https://cloud.google.com/maps-platform/terms).
226
+ > They are convenient for local development and demos, but Google may rate-limit,
227
+ > change, or block them at any time. For production use, set a `googleMapsApiKey`
228
+ > so these basemaps use the authorized [Map Tiles API](https://developers.google.com/maps/documentation/tile)
229
+ > (which requires the Map Tiles API enabled and billing on your project).
230
+
216
231
  ## API
217
232
 
218
233
  ### BasemapControl Options
@@ -230,7 +245,7 @@ key must have the Map Tiles API enabled.
230
245
  | `mapboxAccessToken` | `string` | `undefined` | Initial Mapbox access token for built-in Mapbox styles and the Mapbox Traffic overlay |
231
246
  | `tomtomApiKey` | `string` | `undefined` | Initial TomTom API key for the TomTom Traffic overlays |
232
247
  | `hereApiKey` | `string` | `undefined` | Initial HERE API key for the HERE Traffic overlay |
233
- | `googleMapsApiKey` | `string` | `undefined` | Initial Google Maps API key (Map Tiles API) for the Google Traffic overlay |
248
+ | `googleMapsApiKey` | `string` | `undefined` | Initial Google Maps API key (Map Tiles API) for the Google Traffic overlay and the base Google Maps/Satellite/Terrain/Hybrid basemaps (which fall back to keyless tiles without a key) |
234
249
  | `basemaps` | `BasemapDefinition[]` | `[]` | Custom basemaps to add or use |
235
250
  | `providers` | `BasemapProvider[]` | `[]` | Custom provider labels |
236
251
  | `includeDefaultBasemaps` | `boolean` | `true` | Include the built-in public catalog |
@@ -296,7 +311,7 @@ const control = new BasemapControl({
296
311
  - `setMapboxAccessToken(accessToken)` - Set or update the Mapbox access token
297
312
  - `setTomTomApiKey(apiKey)` - Set or update the TomTom API key used by TomTom Traffic overlays
298
313
  - `setHereApiKey(apiKey)` - Set or update the HERE API key used by the HERE Traffic overlay
299
- - `setGoogleMapsApiKey(apiKey)` - Set or update the Google Maps API key used by the Google Traffic overlay
314
+ - `setGoogleMapsApiKey(apiKey)` - Set or update the Google Maps API key used by the Google Traffic overlay and the base Google basemaps
300
315
  - `getActiveBasemap()` - Return the most recently selected basemap definition
301
316
  - `getActiveBasemaps()` - Return all currently active basemap definitions
302
317
  - `getBasemaps()` - Return the catalog
@@ -151,8 +151,74 @@ const OPENFREEMAP_ATTRIBUTION = "OpenFreeMap © OpenMapTiles Data from OpenS
151
151
  const TOMTOM_ATTRIBUTION = "© TomTom";
152
152
  const HERE_ATTRIBUTION = "© HERE";
153
153
  const GOOGLE_ATTRIBUTION = "© Google";
154
- const EOX_S2CLOUDLESS_ATTRIBUTION = 'Sentinel-2 cloudless <a href="https://cloudless.eox.at" target="_blank" rel="noopener">EOxCloudless</a> by EOX IT Services GmbH (Contains modified Copernicus Sentinel data 2025)';
154
+ const GOOGLE_2DTILES_URL = "https://tile.googleapis.com/v1/2dtiles/{z}/{x}/{y}?session={session}&key={api-key}";
155
+ function googleRasterBasemap({
156
+ id,
157
+ name,
158
+ category,
159
+ description,
160
+ fallbackLyrs,
161
+ session,
162
+ tags
163
+ }) {
164
+ return {
165
+ id,
166
+ name,
167
+ provider: "google",
168
+ type: "raster",
169
+ category,
170
+ description,
171
+ attribution: GOOGLE_ATTRIBUTION,
172
+ source: {
173
+ type: "raster",
174
+ tiles: [GOOGLE_2DTILES_URL],
175
+ fallbackTiles: [
176
+ `https://mt1.google.com/vt/lyrs=${fallbackLyrs}&x={x}&y={y}&z={z}`
177
+ ],
178
+ tileSize: 256,
179
+ maxzoom: 20,
180
+ googleSession: session
181
+ },
182
+ tags
183
+ };
184
+ }
185
+ const eoxS2CloudlessAttribution = (year) => `Sentinel-2 cloudless <a href="https://cloudless.eox.at" target="_blank" rel="noopener">EOxCloudless</a> by EOX IT Services GmbH (Contains modified Copernicus Sentinel data ${year})`;
186
+ const EOX_S2CLOUDLESS_YEARS = [
187
+ 2018,
188
+ 2019,
189
+ 2020,
190
+ 2021,
191
+ 2022,
192
+ 2023,
193
+ 2024,
194
+ 2025
195
+ ];
196
+ function eoxS2CloudlessBasemap(year) {
197
+ return rasterBasemap({
198
+ id: `eox-s2cloudless-${year}`,
199
+ name: `EOX Sentinel-2 cloudless ${year}`,
200
+ provider: "eox",
201
+ category: "Imagery",
202
+ description: `Cloudless Sentinel-2 satellite mosaic for ${year} (non-commercial use).`,
203
+ attribution: eoxS2CloudlessAttribution(year),
204
+ tiles: [
205
+ `https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-${year}_3857/default/GoogleMapsCompatible/{z}/{y}/{x}.jpg`
206
+ ],
207
+ maxzoom: 16,
208
+ tags: [
209
+ "eox",
210
+ "sentinel",
211
+ "satellite",
212
+ "imagery",
213
+ "cloudless",
214
+ "copernicus",
215
+ String(year)
216
+ ]
217
+ });
218
+ }
155
219
  const EOX_TERRAIN_LIGHT_ATTRIBUTION = 'Terrain Light <a href="https://maps.eox.at" target="_blank" rel="noopener">EOX</a> (Data &copy; OpenStreetMap contributors and others, Rendering &copy; EOX)';
220
+ const EOX_TERRAIN_ATTRIBUTION = 'Terrain <a href="https://maps.eox.at" target="_blank" rel="noopener">EOX</a> (Data &copy; OpenStreetMap contributors and others, Rendering &copy; EOX)';
221
+ const EOX_OVERLAY_ATTRIBUTION = 'Overlay <a href="https://maps.eox.at" target="_blank" rel="noopener">EOX</a> (Data &copy; OpenStreetMap contributors and others, Rendering &copy; EOX)';
156
222
  const MAPBOX_TRAFFIC_CONGESTION_COLORS = [
157
223
  "match",
158
224
  ["get", "congestion"],
@@ -233,48 +299,44 @@ const DEFAULT_BASEMAPS = [
233
299
  tiles: ["https://tile.osm.ch/switzerland/{z}/{x}/{y}.png"],
234
300
  tags: ["osm", "switzerland", "regional"]
235
301
  }),
236
- rasterBasemap({
302
+ googleRasterBasemap({
237
303
  id: "google-maps",
238
304
  name: "Google Maps",
239
- provider: "google",
240
305
  category: "Street",
241
- description: "Google road map tiles from the QGIS basemaps source.",
242
- attribution: "&copy; Google",
243
- tiles: ["https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}"],
244
- maxzoom: 20,
306
+ description: "Google road map tiles. Uses the Map Tiles API when a Google Maps API key is set, otherwise keyless xyz tiles.",
307
+ fallbackLyrs: "m",
308
+ session: { mapType: "roadmap" },
245
309
  tags: ["google", "street"]
246
310
  }),
247
- rasterBasemap({
311
+ googleRasterBasemap({
248
312
  id: "google-satellite",
249
313
  name: "Google Satellite",
250
- provider: "google",
251
314
  category: "Imagery",
252
- description: "Google satellite tiles from the QGIS basemaps source.",
253
- attribution: "&copy; Google",
254
- tiles: ["https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}"],
255
- maxzoom: 20,
315
+ description: "Google satellite tiles. Uses the Map Tiles API when a Google Maps API key is set, otherwise keyless xyz tiles.",
316
+ fallbackLyrs: "s",
317
+ session: { mapType: "satellite" },
256
318
  tags: ["google", "satellite", "imagery"]
257
319
  }),
258
- rasterBasemap({
320
+ googleRasterBasemap({
259
321
  id: "google-terrain",
260
322
  name: "Google Terrain",
261
- provider: "google",
262
323
  category: "Terrain",
263
- description: "Google terrain tiles from the QGIS basemaps source.",
264
- attribution: "&copy; Google",
265
- tiles: ["https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}"],
266
- maxzoom: 20,
324
+ description: "Google terrain tiles. Uses the Map Tiles API when a Google Maps API key is set, otherwise keyless xyz tiles.",
325
+ fallbackLyrs: "p",
326
+ // The Map Tiles API requires `layerRoadmap` alongside the terrain map type
327
+ // for the terrain tiles to render.
328
+ session: { mapType: "terrain", layerTypes: ["layerRoadmap"] },
267
329
  tags: ["google", "terrain"]
268
330
  }),
269
- rasterBasemap({
331
+ googleRasterBasemap({
270
332
  id: "google-hybrid",
271
333
  name: "Google Hybrid",
272
- provider: "google",
273
334
  category: "Imagery",
274
- description: "Google hybrid imagery tiles from the QGIS basemaps source.",
275
- attribution: "&copy; Google",
276
- tiles: ["https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}"],
277
- maxzoom: 20,
335
+ description: "Google hybrid imagery tiles. Uses the Map Tiles API when a Google Maps API key is set, otherwise keyless xyz tiles.",
336
+ fallbackLyrs: "y",
337
+ // Hybrid is satellite imagery with the roadmap layer (roads and labels)
338
+ // painted on top, matching the classic Google "hybrid" map type.
339
+ session: { mapType: "satellite", layerTypes: ["layerRoadmap"] },
278
340
  tags: ["google", "hybrid", "imagery"]
279
341
  }),
280
342
  amazonStyleBasemap({
@@ -738,26 +800,7 @@ const DEFAULT_BASEMAPS = [
738
800
  ],
739
801
  tags: ["esri", "terrain"]
740
802
  }),
741
- rasterBasemap({
742
- id: "eox-s2cloudless-2025",
743
- name: "EOX Sentinel-2 cloudless 2025",
744
- provider: "eox",
745
- category: "Imagery",
746
- description: "Cloudless Sentinel-2 satellite mosaic for 2025 (non-commercial use).",
747
- attribution: EOX_S2CLOUDLESS_ATTRIBUTION,
748
- tiles: [
749
- "https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2025_3857/default/GoogleMapsCompatible/{z}/{y}/{x}.jpg"
750
- ],
751
- maxzoom: 16,
752
- tags: [
753
- "eox",
754
- "sentinel",
755
- "satellite",
756
- "imagery",
757
- "cloudless",
758
- "copernicus"
759
- ]
760
- }),
803
+ ...EOX_S2CLOUDLESS_YEARS.map(eoxS2CloudlessBasemap),
761
804
  rasterBasemap({
762
805
  id: "eox-terrain-light",
763
806
  name: "EOX Terrain Light",
@@ -771,6 +814,47 @@ const DEFAULT_BASEMAPS = [
771
814
  maxzoom: 14,
772
815
  tags: ["eox", "terrain", "light", "hillshade"]
773
816
  }),
817
+ rasterBasemap({
818
+ id: "eox-terrain",
819
+ name: "EOX Terrain",
820
+ provider: "eox",
821
+ category: "Terrain",
822
+ description: "Natural-like terrain basemap from EOX Maps (non-commercial use).",
823
+ attribution: EOX_TERRAIN_ATTRIBUTION,
824
+ // terrain_3857 is only published on EOX's "g" tile matrix set, which is the
825
+ // Web Mercator (GoogleMapsCompatible) grid under a different identifier.
826
+ tiles: [
827
+ "https://tiles.maps.eox.at/wmts/1.0.0/terrain_3857/default/g/{z}/{y}/{x}.jpg"
828
+ ],
829
+ maxzoom: 14,
830
+ tags: ["eox", "terrain", "hillshade"]
831
+ }),
832
+ rasterBasemap({
833
+ id: "eox-overlay",
834
+ name: "EOX Overlay",
835
+ provider: "eox",
836
+ category: "Labels",
837
+ description: "Transparent overlay with borders and points of interest from EOX Maps (non-commercial use).",
838
+ attribution: EOX_OVERLAY_ATTRIBUTION,
839
+ tiles: [
840
+ "https://tiles.maps.eox.at/wmts/1.0.0/overlay_3857/default/GoogleMapsCompatible/{z}/{y}/{x}.png"
841
+ ],
842
+ maxzoom: 14,
843
+ tags: ["eox", "overlay", "labels", "borders", "reference"]
844
+ }),
845
+ rasterBasemap({
846
+ id: "eox-overlay-bright",
847
+ name: "EOX Overlay Bright",
848
+ provider: "eox",
849
+ category: "Labels",
850
+ description: "Bright transparent overlay with borders and points of interest from EOX Maps (non-commercial use).",
851
+ attribution: EOX_OVERLAY_ATTRIBUTION,
852
+ tiles: [
853
+ "https://tiles.maps.eox.at/wmts/1.0.0/overlay_bright_3857/default/GoogleMapsCompatible/{z}/{y}/{x}.png"
854
+ ],
855
+ maxzoom: 14,
856
+ tags: ["eox", "overlay", "labels", "borders", "reference", "bright"]
857
+ }),
774
858
  rasterBasemap({
775
859
  id: "nasa-gibs-blue-marble",
776
860
  name: "Blue Marble",
@@ -1425,6 +1509,11 @@ class BasemapControl {
1425
1509
  // The basemap whose application last failed for a missing credential, so the
1426
1510
  // inline credential field's Enter key can re-attempt it (#837).
1427
1511
  __publicField(this, "_lastFailedBasemapId");
1512
+ // The id of a just-applied basemap that is showing its keyless public tiles
1513
+ // because no API key is set (currently the Google Maps/Satellite/Terrain/
1514
+ // Hybrid basemaps). The panel offers an optional inline key input that
1515
+ // upgrades it to the authorized tiles; leaving it blank keeps the fallback.
1516
+ __publicField(this, "_optionalKeyBasemapId");
1428
1517
  // Detaches the listeners watching the in-flight style load (see
1429
1518
  // _watchStyleLoad). Set while a style swap is settling, cleared once it
1430
1519
  // succeeds, fails, or is superseded by a newer swap.
@@ -1733,6 +1822,7 @@ class BasemapControl {
1733
1822
  loading: false,
1734
1823
  error: void 0
1735
1824
  };
1825
+ this._optionalKeyBasemapId = this._basemapUsesKeylessFallback(basemap) ? basemap.id : void 0;
1736
1826
  this._renderContent(true);
1737
1827
  this._emit({
1738
1828
  type: "basemapchange",
@@ -2361,6 +2451,55 @@ class BasemapControl {
2361
2451
  _retryLastFailedBasemap() {
2362
2452
  if (this._lastFailedBasemapId) this._selectBasemap(this._lastFailedBasemapId);
2363
2453
  }
2454
+ // Renders the optional API key input shown after a basemap falls back to its
2455
+ // keyless public tiles, so the user can enter a key to upgrade to the
2456
+ // authorized provider tiles (or leave it blank to keep the public ones).
2457
+ // Returns null unless such a basemap is the active selection and still keyless.
2458
+ _createOptionalKeyPrompt() {
2459
+ const id = this._optionalKeyBasemapId;
2460
+ if (!id) return null;
2461
+ const basemap = this._basemaps.find((candidate) => candidate.id === id);
2462
+ if (!basemap || !this._state.activeBasemapIds.includes(id) || !this._basemapUsesKeylessFallback(basemap)) {
2463
+ return null;
2464
+ }
2465
+ const def = this._providerFieldDefs().find(
2466
+ (candidate) => candidate.provider === basemap.provider && candidate.has
2467
+ );
2468
+ if (!def) return null;
2469
+ const wrapper = document.createElement("div");
2470
+ wrapper.className = "basemap-control-optional-key";
2471
+ const message = document.createElement("span");
2472
+ message.className = "basemap-control-optional-key-message";
2473
+ message.textContent = `Enter a ${def.fields[0].placeholder} to load the official ${def.label} tiles.`;
2474
+ const help = PROVIDER_CREDENTIAL_HELP[def.provider];
2475
+ if (help) {
2476
+ message.appendChild(document.createTextNode(" "));
2477
+ const link = document.createElement("a");
2478
+ link.className = "basemap-control-status-link";
2479
+ link.href = help.url;
2480
+ link.target = "_blank";
2481
+ link.rel = "noopener noreferrer";
2482
+ link.textContent = help.label;
2483
+ message.appendChild(link);
2484
+ }
2485
+ wrapper.appendChild(message);
2486
+ const fields = document.createElement("div");
2487
+ fields.className = "basemap-control-status-fields";
2488
+ for (const field of def.fields) {
2489
+ fields.appendChild(
2490
+ this._createProviderSettingsInput({
2491
+ ...field,
2492
+ // Keep the field on screen while typing; Enter re-applies the basemap
2493
+ // so the freshly entered key takes effect immediately.
2494
+ rerenderOnInput: false,
2495
+ clearErrorOnInput: false,
2496
+ onEnter: () => this._reapplyBasemap(id)
2497
+ })
2498
+ );
2499
+ }
2500
+ wrapper.appendChild(fields);
2501
+ return wrapper;
2502
+ }
2364
2503
  _createProviderSettingsInput({
2365
2504
  className,
2366
2505
  type,
@@ -2482,7 +2621,15 @@ class BasemapControl {
2482
2621
  const inlineFields = this._createInlineCredentialFields();
2483
2622
  if (inlineFields) status.appendChild(inlineFields);
2484
2623
  } else {
2485
- status.textContent = `${resultCount} basemap${resultCount === 1 ? "" : "s"}`;
2624
+ const count = document.createElement("span");
2625
+ count.className = "basemap-control-status-message";
2626
+ count.textContent = `${resultCount} basemap${resultCount === 1 ? "" : "s"}`;
2627
+ status.appendChild(count);
2628
+ const optionalKey = this._createOptionalKeyPrompt();
2629
+ if (optionalKey) {
2630
+ status.classList.add("has-optional-key");
2631
+ status.appendChild(optionalKey);
2632
+ }
2486
2633
  }
2487
2634
  return status;
2488
2635
  }
@@ -2563,9 +2710,10 @@ class BasemapControl {
2563
2710
  _hasHereBasemaps() {
2564
2711
  return this._basemaps.some((basemap) => basemap.provider === "here");
2565
2712
  }
2566
- // True when a Google basemap needs a Map Tiles API key. The existing Google
2567
- // raster basemaps (Maps/Satellite/etc.) use keyless tiles, so only session or
2568
- // api-key based Google layers (e.g. Google Traffic) require the key input.
2713
+ // True when a Google basemap can use a Map Tiles API key. Session-based Google
2714
+ // layers (Google Traffic, plus the base Maps/Satellite/Terrain/Hybrid layers
2715
+ // that upgrade to the authorized Map Tiles API when a key is present) and any
2716
+ // api-key based Google tiles reveal the key input.
2569
2717
  _hasGoogleApiKeyBasemaps() {
2570
2718
  return this._basemaps.some(
2571
2719
  (basemap) => basemap.provider === "google" && basemap.source.type === "raster" && (Boolean(basemap.source.googleSession) || basemap.source.tiles.some((tile) => tile.includes(API_KEY_PLACEHOLDER)))
@@ -2796,8 +2944,11 @@ class BasemapControl {
2796
2944
  // surface a "Get a ..." link and reveal the credential inputs.
2797
2945
  async _resolveRasterTiles(basemap) {
2798
2946
  if (basemap.source.type !== "raster") return [];
2799
- const { tiles, googleSession } = basemap.source;
2947
+ const { tiles, googleSession, fallbackTiles } = basemap.source;
2800
2948
  if (googleSession) {
2949
+ if (!this._googleMapsApiKey.trim() && (fallbackTiles == null ? void 0 : fallbackTiles.length)) {
2950
+ return fallbackTiles;
2951
+ }
2801
2952
  return this._resolveGoogleSessionTiles(tiles, googleSession);
2802
2953
  }
2803
2954
  if (!tiles.some((tile) => tile.includes(API_KEY_PLACEHOLDER))) {
@@ -2816,6 +2967,29 @@ class BasemapControl {
2816
2967
  if (provider === "google") return this._googleMapsApiKey.trim();
2817
2968
  return "";
2818
2969
  }
2970
+ // True when a basemap is (or would be) served from its keyless public
2971
+ // fallback tiles because its provider API key is not set. Drives the optional
2972
+ // key prompt: these basemaps work without a key but can upgrade to the
2973
+ // authorized provider tiles once one is entered.
2974
+ _basemapUsesKeylessFallback(basemap) {
2975
+ var _a;
2976
+ return basemap.source.type === "raster" && Boolean(basemap.source.googleSession) && (((_a = basemap.source.fallbackTiles) == null ? void 0 : _a.length) ?? 0) > 0 && !this._rasterApiKeyFor(basemap.provider);
2977
+ }
2978
+ // Re-applies an already-active basemap so a newly entered key takes effect.
2979
+ // Unlike `_selectBasemap`, this never toggles the basemap off in multiple
2980
+ // mode: a stacked raster is re-added on top (replacing its prior instance),
2981
+ // and a single-mode basemap is simply re-set.
2982
+ _reapplyBasemap(id) {
2983
+ const basemap = this._basemaps.find((candidate) => candidate.id === id);
2984
+ const isOverlay = (basemap == null ? void 0 : basemap.source.type) === "raster" || (basemap == null ? void 0 : basemap.source.type) === "vector-overlay";
2985
+ if (this._state.allowMultiple && isOverlay) {
2986
+ this.addBasemap(id).catch(() => {
2987
+ });
2988
+ } else {
2989
+ this.setBasemap(id).catch(() => {
2990
+ });
2991
+ }
2992
+ }
2819
2993
  _missingRasterKeyError(provider) {
2820
2994
  const label = provider === "tomtom" ? "TomTom API key" : provider === "here" ? "HERE API key" : provider === "google" ? "Google Maps API key" : "API key";
2821
2995
  const helpProvider = provider in PROVIDER_CREDENTIAL_HELP ? provider : "maptiler";
@@ -3051,4 +3225,4 @@ exports.createBasemapCatalog = createBasemapCatalog;
3051
3225
  exports.filterBasemaps = filterBasemaps;
3052
3226
  exports.getBasemapCategories = getBasemapCategories;
3053
3227
  exports.resolveBasemapProviders = resolveBasemapProviders;
3054
- //# sourceMappingURL=BasemapControl-DXJHjc6Z.cjs.map
3228
+ //# sourceMappingURL=BasemapControl-BbsxylnF.cjs.map