maplibre-gl-basemap-control 0.9.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.
@@ -1349,6 +1349,7 @@ const PROVIDER_CREDENTIAL_HELP = {
1349
1349
  label: "Get a Google Maps API key"
1350
1350
  }
1351
1351
  };
1352
+ const STYLE_AUTH_GRACE_MS = 2500;
1352
1353
  const MIN_PANEL_WIDTH = 240;
1353
1354
  const MIN_PANEL_HEIGHT = 200;
1354
1355
  const PANEL_VIEWPORT_MARGIN = 12;
@@ -1360,6 +1361,30 @@ class MissingCredentialError extends Error {
1360
1361
  this.provider = provider;
1361
1362
  }
1362
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
+ }
1363
1388
  class BasemapControl {
1364
1389
  constructor(options) {
1365
1390
  __publicField(this, "_map");
@@ -1399,6 +1424,10 @@ class BasemapControl {
1399
1424
  // The basemap whose application last failed for a missing credential, so the
1400
1425
  // inline credential field's Enter key can re-attempt it (#837).
1401
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");
1402
1431
  __publicField(this, "_resizeHandler", null);
1403
1432
  __publicField(this, "_mapResizeHandler", null);
1404
1433
  __publicField(this, "_resizeAnchor", null);
@@ -1477,7 +1506,7 @@ class BasemapControl {
1477
1506
  return this._container;
1478
1507
  }
1479
1508
  onRemove() {
1480
- var _a, _b, _c, _d;
1509
+ var _a, _b, _c, _d, _e;
1481
1510
  if (this._resizeHandler) {
1482
1511
  window.removeEventListener("resize", this._resizeHandler);
1483
1512
  this._resizeHandler = null;
@@ -1489,10 +1518,11 @@ class BasemapControl {
1489
1518
  window.removeEventListener("pointermove", this._onResizeMove);
1490
1519
  window.removeEventListener("pointerup", this._onResizeEnd);
1491
1520
  window.removeEventListener("pointercancel", this._onResizeEnd);
1521
+ (_a = this._cancelStyleWatch) == null ? void 0 : _a.call(this);
1492
1522
  this._resizeAnchor = null;
1493
1523
  this._resizeHandleEl = null;
1494
- (_b = (_a = this._panel) == null ? void 0 : _a.parentNode) == null ? void 0 : _b.removeChild(this._panel);
1495
- (_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);
1496
1526
  this._map = void 0;
1497
1527
  this._mapContainer = void 0;
1498
1528
  this._container = void 0;
@@ -1669,8 +1699,10 @@ class BasemapControl {
1669
1699
  this._state = { ...this._state, loading: true, error: void 0 };
1670
1700
  this._renderContent(true);
1671
1701
  this._emit({ type: "statechange", state: this.getState() });
1702
+ const previousActiveBasemapId = this._state.activeBasemapId;
1672
1703
  try {
1673
1704
  let managedRaster;
1705
+ let resolvedStyleUrl;
1674
1706
  if (isOverlay) {
1675
1707
  await this._waitForStyleReady();
1676
1708
  if (effectiveMode === "replace") {
@@ -1681,6 +1713,7 @@ class BasemapControl {
1681
1713
  managedRaster = await this._addOverlay(basemap);
1682
1714
  } else {
1683
1715
  const styleUrl = this._resolveStyleUrl(basemap);
1716
+ resolvedStyleUrl = styleUrl;
1684
1717
  const styleOptions = this._getStyleOptions(basemap);
1685
1718
  this._removeManagedBasemap();
1686
1719
  if (styleOptions) {
@@ -1688,6 +1721,7 @@ class BasemapControl {
1688
1721
  } else {
1689
1722
  this._map.setStyle(styleUrl);
1690
1723
  }
1724
+ this._watchStyleLoad(styleUrl, basemap, previousActiveBasemapId);
1691
1725
  }
1692
1726
  this._applyBasemapView(basemap);
1693
1727
  const activeBasemapIds = effectiveMode === "add" ? [...this._state.activeBasemapIds.filter((value) => value !== basemap.id), basemap.id] : [basemap.id];
@@ -1704,6 +1738,7 @@ class BasemapControl {
1704
1738
  state: this.getState(),
1705
1739
  basemap,
1706
1740
  managedRaster,
1741
+ resolvedStyleUrl,
1707
1742
  mode: effectiveMode
1708
1743
  });
1709
1744
  this._emit({ type: "statechange", state: this.getState() });
@@ -1720,6 +1755,131 @@ class BasemapControl {
1720
1755
  throw error;
1721
1756
  }
1722
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
+ }
1723
1883
  // The public mutators rethrow after emitting the error event so callers that
1724
1884
  // await them can react. The panel's click handler uses this instead to avoid
1725
1885
  // unhandled promise rejections; the failure is still reported via `error`.
@@ -2892,4 +3052,4 @@ export {
2892
3052
  getBasemapCategories as g,
2893
3053
  resolveBasemapProviders as r
2894
3054
  };
2895
- //# sourceMappingURL=BasemapControl-DjBg1WYY.js.map
3055
+ //# sourceMappingURL=BasemapControl-AGhhvlrX.js.map