maplibre-gl-basemap-control 0.8.0 → 0.10.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.
@@ -5,6 +5,7 @@ const DEFAULT_BASEMAP_PROVIDERS = [
5
5
  { id: "amazon", name: "Amazon Location", category: "General" },
6
6
  { id: "carto", name: "Carto", category: "General" },
7
7
  { id: "cyclosm", name: "CyclOSM", category: "Cycling" },
8
+ { id: "eox", name: "EOX", category: "Imagery" },
8
9
  { id: "esri", name: "ESRI", category: "Imagery" },
9
10
  { id: "google", name: "Google", category: "General" },
10
11
  { id: "here", name: "HERE", category: "Traffic" },
@@ -149,6 +150,8 @@ const OPENFREEMAP_ATTRIBUTION = "OpenFreeMap © OpenMapTiles Data from OpenS
149
150
  const TOMTOM_ATTRIBUTION = "© TomTom";
150
151
  const HERE_ATTRIBUTION = "© HERE";
151
152
  const GOOGLE_ATTRIBUTION = "© Google";
153
+ 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 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)';
152
155
  const MAPBOX_TRAFFIC_CONGESTION_COLORS = [
153
156
  "match",
154
157
  ["get", "congestion"],
@@ -517,7 +520,9 @@ const DEFAULT_BASEMAPS = [
517
520
  category: "Labels",
518
521
  description: "Carto Positron labels layer.",
519
522
  attribution: CARTO_ATTRIBUTION,
520
- tiles: ["https://a.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png"],
523
+ tiles: [
524
+ "https://a.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png"
525
+ ],
521
526
  maxzoom: 20,
522
527
  tags: ["carto", "labels"]
523
528
  }),
@@ -582,7 +587,9 @@ const DEFAULT_BASEMAPS = [
582
587
  category: "Street",
583
588
  description: "Carto Voyager without labels.",
584
589
  attribution: CARTO_ATTRIBUTION,
585
- tiles: ["https://a.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}.png"],
590
+ tiles: [
591
+ "https://a.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}.png"
592
+ ],
586
593
  maxzoom: 20,
587
594
  tags: ["carto", "voyager", "no labels"]
588
595
  }),
@@ -593,7 +600,9 @@ const DEFAULT_BASEMAPS = [
593
600
  category: "Labels",
594
601
  description: "Carto Voyager labels layer.",
595
602
  attribution: CARTO_ATTRIBUTION,
596
- tiles: ["https://a.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}.png"],
603
+ tiles: [
604
+ "https://a.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}.png"
605
+ ],
597
606
  maxzoom: 20,
598
607
  tags: ["carto", "voyager", "labels"]
599
608
  }),
@@ -604,7 +613,9 @@ const DEFAULT_BASEMAPS = [
604
613
  category: "Street",
605
614
  description: "Carto Voyager labels-under variant.",
606
615
  attribution: CARTO_ATTRIBUTION,
607
- tiles: ["https://a.basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}.png"],
616
+ tiles: [
617
+ "https://a.basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}.png"
618
+ ],
608
619
  maxzoom: 20,
609
620
  tags: ["carto", "voyager", "labels"]
610
621
  }),
@@ -726,6 +737,39 @@ const DEFAULT_BASEMAPS = [
726
737
  ],
727
738
  tags: ["esri", "terrain"]
728
739
  }),
740
+ rasterBasemap({
741
+ id: "eox-s2cloudless-2025",
742
+ name: "EOX Sentinel-2 cloudless 2025",
743
+ provider: "eox",
744
+ category: "Imagery",
745
+ description: "Cloudless Sentinel-2 satellite mosaic for 2025 (non-commercial use).",
746
+ attribution: EOX_S2CLOUDLESS_ATTRIBUTION,
747
+ tiles: [
748
+ "https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2025_3857/default/GoogleMapsCompatible/{z}/{y}/{x}.jpg"
749
+ ],
750
+ maxzoom: 16,
751
+ tags: [
752
+ "eox",
753
+ "sentinel",
754
+ "satellite",
755
+ "imagery",
756
+ "cloudless",
757
+ "copernicus"
758
+ ]
759
+ }),
760
+ rasterBasemap({
761
+ id: "eox-terrain-light",
762
+ name: "EOX Terrain Light",
763
+ provider: "eox",
764
+ category: "Terrain",
765
+ description: "Light terrain basemap from EOX Maps (non-commercial use).",
766
+ attribution: EOX_TERRAIN_LIGHT_ATTRIBUTION,
767
+ tiles: [
768
+ "https://tiles.maps.eox.at/wmts/1.0.0/terrain-light_3857/default/GoogleMapsCompatible/{z}/{y}/{x}.jpg"
769
+ ],
770
+ maxzoom: 14,
771
+ tags: ["eox", "terrain", "light", "hillshade"]
772
+ }),
729
773
  rasterBasemap({
730
774
  id: "nasa-gibs-blue-marble",
731
775
  name: "Blue Marble",
@@ -1111,7 +1155,9 @@ const DEFAULT_BASEMAPS = [
1111
1155
  provider: "tomtom",
1112
1156
  description: "TomTom traffic speeds relative to free-flow speed. Requires a TomTom API key.",
1113
1157
  attribution: TOMTOM_ATTRIBUTION,
1114
- tiles: ["https://api.tomtom.com/traffic/map/4/tile/flow/relative0/{z}/{x}/{y}.png?key={api-key}"],
1158
+ tiles: [
1159
+ "https://api.tomtom.com/traffic/map/4/tile/flow/relative0/{z}/{x}/{y}.png?key={api-key}"
1160
+ ],
1115
1161
  tags: ["tomtom", "flow", "relative"]
1116
1162
  }),
1117
1163
  rasterTrafficBasemap({
@@ -1120,7 +1166,9 @@ const DEFAULT_BASEMAPS = [
1120
1166
  provider: "tomtom",
1121
1167
  description: "TomTom traffic colored by absolute speed. Requires a TomTom API key.",
1122
1168
  attribution: TOMTOM_ATTRIBUTION,
1123
- tiles: ["https://api.tomtom.com/traffic/map/4/tile/flow/absolute/{z}/{x}/{y}.png?key={api-key}"],
1169
+ tiles: [
1170
+ "https://api.tomtom.com/traffic/map/4/tile/flow/absolute/{z}/{x}/{y}.png?key={api-key}"
1171
+ ],
1124
1172
  tags: ["tomtom", "flow", "absolute"]
1125
1173
  }),
1126
1174
  rasterTrafficBasemap({
@@ -1140,7 +1188,9 @@ const DEFAULT_BASEMAPS = [
1140
1188
  provider: "here",
1141
1189
  description: "HERE real-time traffic flow overlay. Requires a HERE API key.",
1142
1190
  attribution: HERE_ATTRIBUTION,
1143
- tiles: ["https://traffic.maps.hereapi.com/v3/flow/mc/{z}/{x}/{y}/png?apiKey={api-key}"],
1191
+ tiles: [
1192
+ "https://traffic.maps.hereapi.com/v3/flow/mc/{z}/{x}/{y}/png?apiKey={api-key}"
1193
+ ],
1144
1194
  tags: ["here", "flow"]
1145
1195
  }),
1146
1196
  {
@@ -1161,7 +1211,17 @@ const DEFAULT_BASEMAPS = [
1161
1211
  "line-cap": "round"
1162
1212
  },
1163
1213
  paint: {
1164
- "line-width": ["interpolate", ["linear"], ["zoom"], 6, 1.5, 14, 4, 18, 8],
1214
+ "line-width": [
1215
+ "interpolate",
1216
+ ["linear"],
1217
+ ["zoom"],
1218
+ 6,
1219
+ 1.5,
1220
+ 14,
1221
+ 4,
1222
+ 18,
1223
+ 8
1224
+ ],
1165
1225
  "line-color": MAPBOX_TRAFFIC_CONGESTION_COLORS
1166
1226
  }
1167
1227
  },
@@ -1177,7 +1237,9 @@ const DEFAULT_BASEMAPS = [
1177
1237
  attribution: GOOGLE_ATTRIBUTION,
1178
1238
  source: {
1179
1239
  type: "raster",
1180
- tiles: ["https://tile.googleapis.com/v1/2dtiles/{z}/{x}/{y}?session={session}&key={api-key}"],
1240
+ tiles: [
1241
+ "https://tile.googleapis.com/v1/2dtiles/{z}/{x}/{y}?session={session}&key={api-key}"
1242
+ ],
1181
1243
  tileSize: 256,
1182
1244
  maxzoom: 22,
1183
1245
  googleSession: {
@@ -1198,12 +1260,17 @@ function combineProviders(defaults, custom = []) {
1198
1260
  function resolveBasemapProviders(basemaps, customProviders = [], includeDefaultProviders = true) {
1199
1261
  const byId = /* @__PURE__ */ new Map();
1200
1262
  if (includeDefaultProviders) {
1201
- DEFAULT_BASEMAP_PROVIDERS.forEach((provider) => byId.set(provider.id, provider));
1263
+ DEFAULT_BASEMAP_PROVIDERS.forEach(
1264
+ (provider) => byId.set(provider.id, provider)
1265
+ );
1202
1266
  }
1203
1267
  customProviders.forEach((provider) => byId.set(provider.id, provider));
1204
1268
  basemaps.forEach((basemap) => {
1205
1269
  if (basemap.provider && !byId.has(basemap.provider)) {
1206
- byId.set(basemap.provider, { id: basemap.provider, name: basemap.provider });
1270
+ byId.set(basemap.provider, {
1271
+ id: basemap.provider,
1272
+ name: basemap.provider
1273
+ });
1207
1274
  }
1208
1275
  });
1209
1276
  return sortProviders([...byId.values()]);
@@ -1232,7 +1299,11 @@ function filterBasemaps(basemaps, filter) {
1232
1299
  });
1233
1300
  }
1234
1301
  function getBasemapCategories(basemaps) {
1235
- return [...new Set(basemaps.map((basemap) => basemap.category).filter(Boolean))].sort();
1302
+ return [
1303
+ ...new Set(
1304
+ basemaps.map((basemap) => basemap.category).filter(Boolean)
1305
+ )
1306
+ ].sort();
1236
1307
  }
1237
1308
  const DEFAULT_OPTIONS = {
1238
1309
  collapsed: true,
@@ -1278,6 +1349,7 @@ const PROVIDER_CREDENTIAL_HELP = {
1278
1349
  label: "Get a Google Maps API key"
1279
1350
  }
1280
1351
  };
1352
+ const STYLE_AUTH_GRACE_MS = 2500;
1281
1353
  const MIN_PANEL_WIDTH = 240;
1282
1354
  const MIN_PANEL_HEIGHT = 200;
1283
1355
  const PANEL_VIEWPORT_MARGIN = 12;
@@ -1289,6 +1361,30 @@ class MissingCredentialError extends Error {
1289
1361
  this.provider = provider;
1290
1362
  }
1291
1363
  }
1364
+ class BasemapLoadError extends Error {
1365
+ constructor(message, provider, status) {
1366
+ super(message);
1367
+ __publicField(this, "provider");
1368
+ __publicField(this, "status");
1369
+ this.name = "BasemapLoadError";
1370
+ this.provider = provider;
1371
+ this.status = status;
1372
+ }
1373
+ }
1374
+ function extractErrorUrl(error) {
1375
+ if (error && typeof error === "object" && "url" in error) {
1376
+ const url = error.url;
1377
+ if (typeof url === "string") return url;
1378
+ }
1379
+ return void 0;
1380
+ }
1381
+ function extractErrorStatus(error) {
1382
+ if (error && typeof error === "object" && "status" in error) {
1383
+ const status = error.status;
1384
+ if (typeof status === "number" && status > 0) return status;
1385
+ }
1386
+ return void 0;
1387
+ }
1292
1388
  class BasemapControl {
1293
1389
  constructor(options) {
1294
1390
  __publicField(this, "_map");
@@ -1328,6 +1424,10 @@ class BasemapControl {
1328
1424
  // The basemap whose application last failed for a missing credential, so the
1329
1425
  // inline credential field's Enter key can re-attempt it (#837).
1330
1426
  __publicField(this, "_lastFailedBasemapId");
1427
+ // Detaches the listeners watching the in-flight style load (see
1428
+ // _watchStyleLoad). Set while a style swap is settling, cleared once it
1429
+ // succeeds, fails, or is superseded by a newer swap.
1430
+ __publicField(this, "_cancelStyleWatch");
1331
1431
  __publicField(this, "_resizeHandler", null);
1332
1432
  __publicField(this, "_mapResizeHandler", null);
1333
1433
  __publicField(this, "_resizeAnchor", null);
@@ -1406,7 +1506,7 @@ class BasemapControl {
1406
1506
  return this._container;
1407
1507
  }
1408
1508
  onRemove() {
1409
- var _a, _b, _c, _d;
1509
+ var _a, _b, _c, _d, _e;
1410
1510
  if (this._resizeHandler) {
1411
1511
  window.removeEventListener("resize", this._resizeHandler);
1412
1512
  this._resizeHandler = null;
@@ -1418,10 +1518,11 @@ class BasemapControl {
1418
1518
  window.removeEventListener("pointermove", this._onResizeMove);
1419
1519
  window.removeEventListener("pointerup", this._onResizeEnd);
1420
1520
  window.removeEventListener("pointercancel", this._onResizeEnd);
1521
+ (_a = this._cancelStyleWatch) == null ? void 0 : _a.call(this);
1421
1522
  this._resizeAnchor = null;
1422
1523
  this._resizeHandleEl = null;
1423
- (_b = (_a = this._panel) == null ? void 0 : _a.parentNode) == null ? void 0 : _b.removeChild(this._panel);
1424
- (_d = (_c = this._container) == null ? void 0 : _c.parentNode) == null ? void 0 : _d.removeChild(this._container);
1524
+ (_c = (_b = this._panel) == null ? void 0 : _b.parentNode) == null ? void 0 : _c.removeChild(this._panel);
1525
+ (_e = (_d = this._container) == null ? void 0 : _d.parentNode) == null ? void 0 : _e.removeChild(this._container);
1425
1526
  this._map = void 0;
1426
1527
  this._mapContainer = void 0;
1427
1528
  this._container = void 0;
@@ -1598,8 +1699,10 @@ class BasemapControl {
1598
1699
  this._state = { ...this._state, loading: true, error: void 0 };
1599
1700
  this._renderContent(true);
1600
1701
  this._emit({ type: "statechange", state: this.getState() });
1702
+ const previousActiveBasemapId = this._state.activeBasemapId;
1601
1703
  try {
1602
1704
  let managedRaster;
1705
+ let resolvedStyleUrl;
1603
1706
  if (isOverlay) {
1604
1707
  await this._waitForStyleReady();
1605
1708
  if (effectiveMode === "replace") {
@@ -1610,6 +1713,7 @@ class BasemapControl {
1610
1713
  managedRaster = await this._addOverlay(basemap);
1611
1714
  } else {
1612
1715
  const styleUrl = this._resolveStyleUrl(basemap);
1716
+ resolvedStyleUrl = styleUrl;
1613
1717
  const styleOptions = this._getStyleOptions(basemap);
1614
1718
  this._removeManagedBasemap();
1615
1719
  if (styleOptions) {
@@ -1617,6 +1721,7 @@ class BasemapControl {
1617
1721
  } else {
1618
1722
  this._map.setStyle(styleUrl);
1619
1723
  }
1724
+ this._watchStyleLoad(styleUrl, basemap, previousActiveBasemapId);
1620
1725
  }
1621
1726
  this._applyBasemapView(basemap);
1622
1727
  const activeBasemapIds = effectiveMode === "add" ? [...this._state.activeBasemapIds.filter((value) => value !== basemap.id), basemap.id] : [basemap.id];
@@ -1633,6 +1738,7 @@ class BasemapControl {
1633
1738
  state: this.getState(),
1634
1739
  basemap,
1635
1740
  managedRaster,
1741
+ resolvedStyleUrl,
1636
1742
  mode: effectiveMode
1637
1743
  });
1638
1744
  this._emit({ type: "statechange", state: this.getState() });
@@ -1649,6 +1755,131 @@ class BasemapControl {
1649
1755
  throw error;
1650
1756
  }
1651
1757
  }
1758
+ // Watches the asynchronous style load that `setStyle` kicked off for a style
1759
+ // basemap, and rolls back to the previous basemap (reporting the failure
1760
+ // through the `error` event) when the new style cannot actually render. Two
1761
+ // failure modes are caught so a broken provider basemap never leaves a blank
1762
+ // map:
1763
+ //
1764
+ // 1. The style *document* request fails outright (matched by URL), e.g. an
1765
+ // unreachable host or a descriptor that 404s. Detected whether or not
1766
+ // `style.load` ever fires.
1767
+ // 2. The descriptor loads but the provider rejects the credentials when the
1768
+ // first tiles are fetched (401/403). Amazon Location, for instance,
1769
+ // serves the style JSON publicly but 403s every tile for a bad API key,
1770
+ // so the descriptor "loads" while the map stays empty. This is only
1771
+ // treated as fatal for credentialed providers, and only briefly after
1772
+ // the descriptor loads, so an isolated transient tile error on a healthy
1773
+ // basemap is ignored.
1774
+ _watchStyleLoad(styleUrl, basemap, previousActiveBasemapId) {
1775
+ var _a;
1776
+ const map = this._map;
1777
+ if (!map) return;
1778
+ (_a = this._cancelStyleWatch) == null ? void 0 : _a.call(this);
1779
+ const isCredentialed = basemap.provider in PROVIDER_CREDENTIAL_HELP;
1780
+ let settled = false;
1781
+ let graceTimer;
1782
+ const cleanup = () => {
1783
+ if (settled) return;
1784
+ settled = true;
1785
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
1786
+ map.off("style.load", onLoad);
1787
+ map.off("error", onError);
1788
+ if (this._cancelStyleWatch === cleanup) this._cancelStyleWatch = void 0;
1789
+ };
1790
+ const fail = (cause) => {
1791
+ cleanup();
1792
+ this._handleStyleLoadFailure(basemap, previousActiveBasemapId, cause);
1793
+ };
1794
+ const onLoad = () => {
1795
+ if (!isCredentialed) {
1796
+ cleanup();
1797
+ } else if (graceTimer === void 0) {
1798
+ graceTimer = setTimeout(cleanup, STYLE_AUTH_GRACE_MS);
1799
+ }
1800
+ };
1801
+ const onError = (event) => {
1802
+ const error = event == null ? void 0 : event.error;
1803
+ if (extractErrorUrl(error) === styleUrl) {
1804
+ fail(error);
1805
+ return;
1806
+ }
1807
+ if (isCredentialed) {
1808
+ const status = extractErrorStatus(error);
1809
+ if (status === 401 || status === 403) fail(error);
1810
+ }
1811
+ };
1812
+ this._cancelStyleWatch = cleanup;
1813
+ map.once("style.load", onLoad);
1814
+ map.on("error", onError);
1815
+ }
1816
+ // Rolls back a failed style swap: restores the previously active basemap (when
1817
+ // it is a style basemap the control owns), then reports the failure inline and
1818
+ // through the `error` event. When the failing provider needs an API key, the
1819
+ // inline credential field is revealed so the user can correct the key and
1820
+ // retry without losing their map.
1821
+ _handleStyleLoadFailure(failedBasemap, previousActiveBasemapId, cause) {
1822
+ this._restorePreviousBasemap(previousActiveBasemapId, failedBasemap.id);
1823
+ const status = extractErrorStatus(cause);
1824
+ const credentialProvider = failedBasemap.provider in PROVIDER_CREDENTIAL_HELP ? failedBasemap.provider : void 0;
1825
+ const hint = credentialProvider ? " Check the API key and try again." : " Check your connection and try again.";
1826
+ const error = new BasemapLoadError(
1827
+ `Could not load the "${failedBasemap.name}" basemap${status ? ` (HTTP ${status})` : ""}.${hint}`,
1828
+ failedBasemap.provider,
1829
+ status
1830
+ );
1831
+ this._state = { ...this._state, loading: false, error: error.message };
1832
+ this._missingCredentialProvider = credentialProvider;
1833
+ this._lastFailedBasemapId = credentialProvider ? failedBasemap.id : void 0;
1834
+ this._activeView = "basemaps";
1835
+ this._renderContent(true);
1836
+ this._handleError(error, failedBasemap);
1837
+ }
1838
+ // Reapplies the previously active style basemap after a failed swap. Only a
1839
+ // control-owned style/vector-style basemap can be reapplied; when the previous
1840
+ // basemap is unknown, a raster overlay, or a style the host owns, the panel
1841
+ // state is pointed back at it (so the failed basemap is not highlighted) and
1842
+ // the host is left to restore its own style from the emitted `error` event.
1843
+ _restorePreviousBasemap(previousActiveBasemapId, failedBasemapId) {
1844
+ const map = this._map;
1845
+ const previous = previousActiveBasemapId && previousActiveBasemapId !== failedBasemapId ? this._basemaps.find((candidate) => candidate.id === previousActiveBasemapId) : void 0;
1846
+ const pointStateAtPrevious = () => {
1847
+ this._state = {
1848
+ ...this._state,
1849
+ activeBasemapId: previousActiveBasemapId,
1850
+ activeBasemapIds: previousActiveBasemapId ? [previousActiveBasemapId] : []
1851
+ };
1852
+ };
1853
+ if (!map || !previous || previous.source.type !== "style" && previous.source.type !== "vector-style") {
1854
+ pointStateAtPrevious();
1855
+ return;
1856
+ }
1857
+ try {
1858
+ const styleUrl = this._resolveStyleUrl(previous);
1859
+ const styleOptions = this._getStyleOptions(previous);
1860
+ this._removeManagedBasemap();
1861
+ if (styleOptions) {
1862
+ map.setStyle(styleUrl, styleOptions);
1863
+ } else {
1864
+ map.setStyle(styleUrl);
1865
+ }
1866
+ this._state = {
1867
+ ...this._state,
1868
+ activeBasemapId: previous.id,
1869
+ activeBasemapIds: [previous.id]
1870
+ };
1871
+ this._emit({
1872
+ type: "basemapchange",
1873
+ state: this.getState(),
1874
+ basemap: previous,
1875
+ resolvedStyleUrl: styleUrl,
1876
+ restored: true,
1877
+ mode: "replace"
1878
+ });
1879
+ } catch {
1880
+ pointStateAtPrevious();
1881
+ }
1882
+ }
1652
1883
  // The public mutators rethrow after emitting the error event so callers that
1653
1884
  // await them can react. The panel's click handler uses this instead to avoid
1654
1885
  // unhandled promise rejections; the failure is still reported via `error`.
@@ -2821,4 +3052,4 @@ export {
2821
3052
  getBasemapCategories as g,
2822
3053
  resolveBasemapProviders as r
2823
3054
  };
2824
- //# sourceMappingURL=BasemapControl-C9T87uJx.js.map
3055
+ //# sourceMappingURL=BasemapControl-AGhhvlrX.js.map