esri-gl 0.9.0-alpha.13 → 0.9.0-alpha.16

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
@@ -368,7 +368,7 @@ For the smoothest React experience with declarative layer management:
368
368
 
369
369
  ```typescript
370
370
  import React, { useState } from 'react';
371
- import { Map } from 'react-map-gl';
371
+ import { Map } from 'react-map-gl/mapbox';
372
372
  import {
373
373
  EsriDynamicLayer,
374
374
  EsriFeatureLayer,
@@ -485,7 +485,7 @@ import type {
485
485
  IdentifyResult,
486
486
  EsriLayerProps
487
487
  } from 'esri-gl';
488
- import type { MapRef } from 'react-map-gl';
488
+ import type { MapRef } from 'react-map-gl/mapbox';
489
489
 
490
490
  // Fully typed component props
491
491
  interface MapComponentProps {
@@ -653,6 +653,8 @@ All type declarations are available in the `dist/` directory after building.
653
653
 
654
654
  ## Contributing
655
655
 
656
+ We welcome contributions! Please follow these steps:
657
+
656
658
  1. Fork the repository
657
659
  2. Create a feature branch: `git checkout -b feature/my-feature`
658
660
  3. Make changes and add tests
@@ -661,6 +663,17 @@ All type declarations are available in the `dist/` directory after building.
661
663
  6. Push to branch: `git push origin feature/my-feature`
662
664
  7. Submit a pull request
663
665
 
666
+ ### Pre-commit Hooks
667
+
668
+ This project uses [Husky](https://typicode.github.io/husky/) to automatically run quality checks before each commit:
669
+
670
+ - **Formatting & Linting**: Automatically formats and lints staged files using Prettier and ESLint
671
+ - **Testing**: Runs the full test suite to ensure no regressions
672
+
673
+ The hooks are automatically installed when you run `npm install`. If you need to skip them (not recommended), you can use `git commit --no-verify`.
674
+
675
+ For more details, see [.husky/README.md](.husky/README.md).
676
+
664
677
  ## License
665
678
 
666
679
  MIT License - see [LICENSE](LICENSE) file for details.
@@ -462,7 +462,11 @@ class DynamicMapService {
462
462
  this.rasterSrcOptions = rasterSrcOptions;
463
463
  this.esriServiceOptions = esriServiceOptions;
464
464
  this._createSource();
465
- if (this.options.getAttributionFromService) this.setAttributionFromService();
465
+ if (this.options.getAttributionFromService) {
466
+ this.setAttributionFromService().catch(() => {
467
+ // Silently handle attribution fetch errors to prevent unhandled rejections
468
+ });
469
+ }
466
470
  }
467
471
  get options() {
468
472
  return {
@@ -543,7 +547,10 @@ class DynamicMapService {
543
547
  };
544
548
  }
545
549
  _createSource() {
546
- this._map.addSource(this._sourceId, this._source);
550
+ // Check if source already exists before adding
551
+ if (!this._map.getSource(this._sourceId)) {
552
+ this._map.addSource(this._sourceId, this._source);
553
+ }
547
554
  }
548
555
  // This requires hooking into some undocumented methods
549
556
  _updateSource() {
@@ -562,11 +569,21 @@ class DynamicMapService {
562
569
  const src = this._map.getSource(this._sourceId);
563
570
  if (!src) return;
564
571
  try {
565
- src.tiles[0] = this._source.tiles[0];
572
+ // Ensure src.tiles exists before accessing it
573
+ if (src.tiles && Array.isArray(src.tiles) && this._source.tiles && this._source.tiles.length > 0) {
574
+ src.tiles[0] = this._source.tiles[0];
575
+ }
566
576
  src._options = this._source;
567
577
  if (src.setTiles) {
568
578
  // New MapboxGL >= 2.13.0
569
- src.setTiles(this._source.tiles);
579
+ // setTiles may return a promise - handle rejections to prevent console errors
580
+ const result = src.setTiles(this._source.tiles);
581
+ if (result && typeof result.catch === 'function') {
582
+ result.catch(() => {
583
+ // Silently ignore - setTiles rejections are often abort errors during rapid updates
584
+ // The outer try/catch will handle any synchronous errors
585
+ });
586
+ }
570
587
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
571
588
  } else if (this._map.style.sourceCaches) {
572
589
  // Old MapboxGL and MaplibreGL
@@ -582,8 +599,21 @@ class DynamicMapService {
582
599
  this._map.style.sourceCaches[this._sourceId].update(this._map.transform);
583
600
  }
584
601
  } catch (error) {
585
- if (error instanceof DOMException && error.name === 'AbortError') {
586
- // Ignore aborted tile refresh; map will request new tiles on next frame
602
+ // Comprehensive abort detection - check all possible error shapes
603
+ // MapLibre can throw various forms of AbortError
604
+ const errorName = error?.name;
605
+ const errorMessage = error?.message;
606
+ const errorConstructor = error?.constructor?.name;
607
+ let stringified = '';
608
+ try {
609
+ stringified = String(error);
610
+ } catch {
611
+ // ignore
612
+ }
613
+ // Check every possible way an error could represent AbortError
614
+ const isAbortError = error instanceof DOMException && error.name === 'AbortError' || error instanceof Error && error.name === 'AbortError' || errorName === 'AbortError' || errorConstructor === 'AbortError' || errorMessage?.toLowerCase().includes('abort') || stringified.toLowerCase().includes('abort') || errorMessage?.includes('AbortError') || stringified.includes('AbortError') || stringified === 'Error: AbortError' || error === 'AbortError';
615
+ if (isAbortError) {
616
+ // Silently ignore aborted tile operations - MapLibre will retry
587
617
  return;
588
618
  }
589
619
  // Swallow occasional transient errors that can happen during style reloads
@@ -1146,7 +1176,54 @@ class DynamicMapService {
1146
1176
  this._updateSource();
1147
1177
  }
1148
1178
  remove() {
1149
- this._map.removeSource(this._sourceId);
1179
+ const map = this._map;
1180
+ if (!map || typeof map.removeSource !== 'function') {
1181
+ return;
1182
+ }
1183
+ try {
1184
+ const mapWithStyle = map;
1185
+ const mapLayerApi = map;
1186
+ const mapSourceApi = map;
1187
+ if (typeof mapWithStyle.getStyle === 'function') {
1188
+ const style = mapWithStyle.getStyle();
1189
+ const layers = style?.layers || [];
1190
+ layers.forEach(layer => {
1191
+ if (layer.source !== this._sourceId) return;
1192
+ if (typeof mapLayerApi.getLayer !== 'function' || typeof mapLayerApi.removeLayer !== 'function') {
1193
+ return;
1194
+ }
1195
+ let hasLayer = false;
1196
+ try {
1197
+ hasLayer = Boolean(mapLayerApi.getLayer(layer.id));
1198
+ } catch {
1199
+ hasLayer = false;
1200
+ }
1201
+ if (!hasLayer) return;
1202
+ try {
1203
+ mapLayerApi.removeLayer(layer.id);
1204
+ } catch (error) {
1205
+ console.warn(`Failed to remove layer ${layer.id} for source ${this._sourceId}:`, error);
1206
+ }
1207
+ });
1208
+ }
1209
+ if (typeof mapSourceApi.getSource === 'function') {
1210
+ let hasSource = false;
1211
+ try {
1212
+ hasSource = Boolean(mapSourceApi.getSource(this._sourceId));
1213
+ } catch {
1214
+ hasSource = false;
1215
+ }
1216
+ if (hasSource) {
1217
+ try {
1218
+ map.removeSource(this._sourceId);
1219
+ } catch (error) {
1220
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1221
+ }
1222
+ }
1223
+ }
1224
+ } catch (error) {
1225
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1226
+ }
1150
1227
  }
1151
1228
  }
1152
1229
 
@@ -1206,7 +1283,10 @@ class TiledMapService {
1206
1283
  };
1207
1284
  }
1208
1285
  _createSource() {
1209
- this._map.addSource(this._sourceId, this._source);
1286
+ // Check if source already exists before adding
1287
+ if (!this._map.getSource(this._sourceId)) {
1288
+ this._map.addSource(this._sourceId, this._source);
1289
+ }
1210
1290
  }
1211
1291
  setAttributionFromService() {
1212
1292
  if (this._serviceMetadata) {
@@ -1233,7 +1313,39 @@ class TiledMapService {
1233
1313
  this._map.getSource(this._sourceId);
1234
1314
  }
1235
1315
  remove() {
1236
- this._map.removeSource(this._sourceId);
1316
+ // Guard against disposed or invalid map
1317
+ if (!this._map || typeof this._map.removeSource !== 'function') {
1318
+ return;
1319
+ }
1320
+ try {
1321
+ // First, remove any layers that are using this source
1322
+ const mapWithStyle = this._map;
1323
+ if (mapWithStyle.getStyle && typeof mapWithStyle.getLayer === 'function') {
1324
+ const style = mapWithStyle.getStyle();
1325
+ const layers = style?.layers || [];
1326
+ const getLayer = mapWithStyle.getLayer;
1327
+ layers.forEach(layer => {
1328
+ if (layer.source === this._sourceId) {
1329
+ try {
1330
+ if (getLayer(layer.id)) {
1331
+ this._map.removeLayer(layer.id);
1332
+ }
1333
+ } catch {
1334
+ // Layer may already be removed
1335
+ }
1336
+ }
1337
+ });
1338
+ }
1339
+ // Then check if source exists before trying to remove it
1340
+ if (typeof this._map.getSource === 'function') {
1341
+ const source = this._map.getSource(this._sourceId);
1342
+ if (source) {
1343
+ this._map.removeSource(this._sourceId);
1344
+ }
1345
+ }
1346
+ } catch (error) {
1347
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1348
+ }
1237
1349
  }
1238
1350
  }
1239
1351
 
@@ -1294,7 +1406,11 @@ class ImageService {
1294
1406
  this.rasterSrcOptions = rasterSrcOptions;
1295
1407
  this.esriServiceOptions = esriServiceOptions;
1296
1408
  this._createSource();
1297
- if (this.options.getAttributionFromService) this.setAttributionFromService();
1409
+ if (this.options.getAttributionFromService) {
1410
+ this.setAttributionFromService().catch(() => {
1411
+ // Silently handle attribution fetch errors to prevent unhandled rejections
1412
+ });
1413
+ }
1298
1414
  }
1299
1415
  get options() {
1300
1416
  return {
@@ -1332,11 +1448,18 @@ class ImageService {
1332
1448
  };
1333
1449
  }
1334
1450
  _createSource() {
1335
- this._map.addSource(this._sourceId, this._source);
1451
+ // Check if source already exists before adding
1452
+ if (!this._map.getSource(this._sourceId)) {
1453
+ this._map.addSource(this._sourceId, this._source);
1454
+ }
1336
1455
  }
1337
1456
  // This requires hooking into some undocumented methods
1338
1457
  _updateSource() {
1339
1458
  const src = this._map.getSource(this._sourceId);
1459
+ if (!src) {
1460
+ // Source not yet added to map, nothing to update
1461
+ return;
1462
+ }
1340
1463
  src.tiles[0] = this._source.tiles[0];
1341
1464
  src._options = this._source;
1342
1465
  if (src.setTiles) {
@@ -1411,7 +1534,27 @@ class ImageService {
1411
1534
  this._updateSource();
1412
1535
  }
1413
1536
  remove() {
1414
- this._map.removeSource(this._sourceId);
1537
+ if (this._map && typeof this._map.removeSource === 'function') {
1538
+ try {
1539
+ // First, remove any layers that are using this source
1540
+ const mapWithStyle = this._map;
1541
+ if (mapWithStyle.getStyle) {
1542
+ const style = mapWithStyle.getStyle();
1543
+ const layers = style?.layers || [];
1544
+ layers.forEach(layer => {
1545
+ if (layer.source === this._sourceId && this._map.getLayer(layer.id)) {
1546
+ this._map.removeLayer(layer.id);
1547
+ }
1548
+ });
1549
+ }
1550
+ // Then check if source exists before trying to remove it
1551
+ if (this._map.getSource && this._map.getSource(this._sourceId)) {
1552
+ this._map.removeSource(this._sourceId);
1553
+ }
1554
+ } catch (error) {
1555
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1556
+ }
1557
+ }
1415
1558
  }
1416
1559
  }
1417
1560
 
@@ -1659,7 +1802,10 @@ class VectorTileService {
1659
1802
  };
1660
1803
  }
1661
1804
  _createSource() {
1662
- this._map.addSource(this._sourceId, this._source);
1805
+ // Check if source already exists before adding
1806
+ if (!this._map.getSource(this._sourceId)) {
1807
+ this._map.addSource(this._sourceId, this._source);
1808
+ }
1663
1809
  }
1664
1810
  _mapToLocalSource(style) {
1665
1811
  return {
@@ -1720,7 +1866,54 @@ class VectorTileService {
1720
1866
  // Vector tile services don't need dynamic updates like dynamic services
1721
1867
  }
1722
1868
  remove() {
1723
- this._map.removeSource(this._sourceId);
1869
+ const map = this._map;
1870
+ if (!map || typeof map.removeSource !== 'function') {
1871
+ return;
1872
+ }
1873
+ try {
1874
+ const mapWithStyle = map;
1875
+ const mapLayerApi = map;
1876
+ const mapSourceApi = map;
1877
+ if (typeof mapWithStyle.getStyle === 'function') {
1878
+ const style = mapWithStyle.getStyle();
1879
+ const layers = style?.layers || [];
1880
+ layers.forEach(layer => {
1881
+ if (layer.source !== this._sourceId) return;
1882
+ if (typeof mapLayerApi.getLayer !== 'function' || typeof mapLayerApi.removeLayer !== 'function') {
1883
+ return;
1884
+ }
1885
+ let hasLayer = false;
1886
+ try {
1887
+ hasLayer = Boolean(mapLayerApi.getLayer(layer.id));
1888
+ } catch {
1889
+ hasLayer = false;
1890
+ }
1891
+ if (!hasLayer) return;
1892
+ try {
1893
+ mapLayerApi.removeLayer(layer.id);
1894
+ } catch (error) {
1895
+ console.warn(`Failed to remove layer ${layer.id} for source ${this._sourceId}:`, error);
1896
+ }
1897
+ });
1898
+ }
1899
+ if (typeof mapSourceApi.getSource === 'function') {
1900
+ let hasSource = false;
1901
+ try {
1902
+ hasSource = Boolean(mapSourceApi.getSource(this._sourceId));
1903
+ } catch {
1904
+ hasSource = false;
1905
+ }
1906
+ if (hasSource) {
1907
+ try {
1908
+ map.removeSource(this._sourceId);
1909
+ } catch (error) {
1910
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1911
+ }
1912
+ }
1913
+ }
1914
+ } catch (error) {
1915
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1916
+ }
1724
1917
  }
1725
1918
  }
1726
1919
 
@@ -1762,6 +1955,18 @@ class FeatureService {
1762
1955
  writable: true,
1763
1956
  value: null
1764
1957
  });
1958
+ Object.defineProperty(this, "_sourceReadyResolve", {
1959
+ enumerable: true,
1960
+ configurable: true,
1961
+ writable: true,
1962
+ value: null
1963
+ });
1964
+ Object.defineProperty(this, "_sourceReadyReject", {
1965
+ enumerable: true,
1966
+ configurable: true,
1967
+ writable: true,
1968
+ value: null
1969
+ });
1765
1970
  Object.defineProperty(this, "vectorSrcOptions", {
1766
1971
  enumerable: true,
1767
1972
  configurable: true,
@@ -1774,6 +1979,32 @@ class FeatureService {
1774
1979
  writable: true,
1775
1980
  value: void 0
1776
1981
  });
1982
+ /**
1983
+ * Promise that resolves when the source has been successfully added to the map.
1984
+ * This can be used to ensure the source exists before adding layers that reference it.
1985
+ *
1986
+ * @example
1987
+ * ```typescript
1988
+ * const service = new FeatureService('my-source', map, { url: '...' });
1989
+ *
1990
+ * // Wait for source to be ready before adding a layer
1991
+ * await service.sourceReady;
1992
+ * map.addLayer({
1993
+ * id: 'my-layer',
1994
+ * type: 'circle',
1995
+ * source: 'my-source',
1996
+ * paint: { 'circle-radius': 5, 'circle-color': '#007cbf' }
1997
+ * });
1998
+ * ```
1999
+ *
2000
+ * If source creation fails, the promise will reject with an error.
2001
+ */
2002
+ Object.defineProperty(this, "sourceReady", {
2003
+ enumerable: true,
2004
+ configurable: true,
2005
+ writable: true,
2006
+ value: void 0
2007
+ });
1777
2008
  if (!esriServiceOptions.url) {
1778
2009
  throw new Error('A url must be supplied as part of the esriServiceOptions object.');
1779
2010
  }
@@ -1800,49 +2031,48 @@ class FeatureService {
1800
2031
  if (this.esriServiceOptions.useBoundingBox === undefined) {
1801
2032
  this.esriServiceOptions.useBoundingBox = true;
1802
2033
  }
2034
+ // Create the sourceReady promise that will resolve when the source is added to the map
2035
+ this.sourceReady = new Promise((resolve, reject) => {
2036
+ this._sourceReadyResolve = resolve;
2037
+ this._sourceReadyReject = reject;
2038
+ });
1803
2039
  this._createSource();
1804
2040
  }
1805
2041
  async _createSource() {
2042
+ if (!this._map) {
2043
+ if (this._sourceReadyReject) {
2044
+ this._sourceReadyReject(new Error('Map not available'));
2045
+ }
2046
+ return;
2047
+ }
1806
2048
  try {
1807
2049
  // Get service metadata
1808
2050
  this._serviceMetadata = await getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions);
1809
2051
  // Check if vector tiles should be used (default behavior)
1810
2052
  // Note: Most FeatureServers don't support vector tiles, so we'll detect and fallback
1811
- const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
1812
- if (!isTestEnvironment) {
1813
- console.log('FeatureService: useVectorTiles setting:', this.esriServiceOptions.useVectorTiles);
1814
- }
1815
2053
  const vectorTileSupport = await this._checkVectorTileSupport();
1816
- if (!isTestEnvironment) {
1817
- console.log('FeatureService: Vector tile support detected:', vectorTileSupport);
1818
- }
1819
2054
  const useVectorTiles = this.esriServiceOptions.useVectorTiles !== false && vectorTileSupport;
1820
- if (!isTestEnvironment) {
1821
- console.log('FeatureService: Final decision - using vector tiles:', useVectorTiles);
1822
- }
1823
2055
  if (useVectorTiles) {
1824
2056
  // Create vector tile source
1825
2057
  const tileUrl = this._buildTileUrl();
1826
- if (!isTestEnvironment) {
1827
- console.log('FeatureService: Using vector tiles for FeatureService:', tileUrl);
2058
+ // Add vector source to map if it doesn't already exist
2059
+ if (!this._map.getSource(this._sourceId)) {
2060
+ this._map.addSource(this._sourceId, {
2061
+ type: 'vector',
2062
+ tiles: [tileUrl],
2063
+ maxzoom: 24,
2064
+ ...this.vectorSrcOptions
2065
+ });
1828
2066
  }
1829
- // Add vector source to map
1830
- this._map.addSource(this._sourceId, {
1831
- type: 'vector',
1832
- tiles: [tileUrl],
1833
- maxzoom: 24,
1834
- ...this.vectorSrcOptions
1835
- });
1836
2067
  } else {
1837
2068
  // Fallback to GeoJSON (most common for FeatureServers)
1838
2069
  const queryUrl = this._buildQueryUrl();
1839
- if (!isTestEnvironment) {
1840
- console.log('FeatureService: Using GeoJSON for FeatureService:', queryUrl);
2070
+ if (!this._map.getSource(this._sourceId)) {
2071
+ this._map.addSource(this._sourceId, {
2072
+ type: 'geojson',
2073
+ data: queryUrl
2074
+ });
1841
2075
  }
1842
- this._map.addSource(this._sourceId, {
1843
- type: 'geojson',
1844
- data: queryUrl
1845
- });
1846
2076
  }
1847
2077
  // Update attribution after source is added if available in service metadata
1848
2078
  if (this._serviceMetadata?.copyrightText) {
@@ -1852,11 +2082,19 @@ class FeatureService {
1852
2082
  if (!useVectorTiles && this.esriServiceOptions.useBoundingBox) {
1853
2083
  this._setupBoundingBoxUpdates();
1854
2084
  }
2085
+ // Resolve the sourceReady promise now that the source is successfully added
2086
+ if (this._sourceReadyResolve) {
2087
+ this._sourceReadyResolve();
2088
+ }
1855
2089
  } catch (error) {
1856
2090
  const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
1857
2091
  if (!isTestEnvironment) {
1858
2092
  console.error('Error creating FeatureService source:', error);
1859
2093
  }
2094
+ // Reject the sourceReady promise so callers can catch the error
2095
+ if (this._sourceReadyReject && error instanceof Error) {
2096
+ this._sourceReadyReject(error);
2097
+ }
1860
2098
  // Don't rethrow - service should handle errors gracefully
1861
2099
  // The source just won't be created and the service will be in a degraded state
1862
2100
  }
@@ -1867,42 +2105,20 @@ class FeatureService {
1867
2105
  const vectorTileUrl = this.esriServiceOptions.url.replace('/FeatureServer/', '/VectorTileServer/');
1868
2106
  // Only check if the URL actually changed (meaning it was a FeatureServer URL)
1869
2107
  if (vectorTileUrl === this.esriServiceOptions.url) {
1870
- const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
1871
- if (!isTestEnvironment) {
1872
- console.log('FeatureService: Not a FeatureServer URL, falling back to GeoJSON');
1873
- }
1874
2108
  return false;
1875
2109
  }
1876
- const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
1877
- if (!isTestEnvironment) {
1878
- console.log('FeatureService: Checking vector tile support at:', vectorTileUrl);
1879
- }
1880
2110
  const response = await fetch(vectorTileUrl + '?f=json', this.esriServiceOptions.fetchOptions);
1881
2111
  if (response.ok) {
1882
2112
  const data = await response.json();
1883
2113
  if (data && !data.error) {
1884
- if (!isTestEnvironment) {
1885
- console.log('FeatureService: Vector tile endpoint found and working:', vectorTileUrl);
1886
- console.log('FeatureService: Vector tile service data:', data);
1887
- }
1888
2114
  return true;
1889
2115
  } else {
1890
- if (!isTestEnvironment) {
1891
- console.log('FeatureService: Vector tile endpoint returned error:', data?.error);
1892
- }
1893
2116
  return false;
1894
2117
  }
1895
2118
  } else {
1896
- if (!isTestEnvironment) {
1897
- console.log('FeatureService: Vector tile endpoint returned HTTP', response.status);
1898
- }
1899
2119
  return false;
1900
2120
  }
1901
- } catch (error) {
1902
- const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
1903
- if (!isTestEnvironment) {
1904
- console.log('FeatureService: Vector tile check failed, falling back to GeoJSON:', error);
1905
- }
2121
+ } catch {
1906
2122
  return false;
1907
2123
  }
1908
2124
  }
@@ -2047,10 +2263,6 @@ class FeatureService {
2047
2263
  const source = this._map.getSource(this._sourceId);
2048
2264
  if (source && 'setData' in source && typeof source.setData === 'function') {
2049
2265
  const newQueryUrl = this._buildQueryUrl();
2050
- const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
2051
- if (!isTestEnvironment) {
2052
- console.log('Updating FeatureService data with new bounding box:', newQueryUrl);
2053
- }
2054
2266
  // @ts-ignore - GeoJSON source setData method not in generic Source type
2055
2267
  source.setData(newQueryUrl);
2056
2268
  }
@@ -2131,8 +2343,26 @@ class FeatureService {
2131
2343
  // Note: maxRecordCount is a server capability; not settable via query params
2132
2344
  remove() {
2133
2345
  this._removeBoundingBoxUpdates();
2134
- if (this._map.getSource(this._sourceId)) {
2135
- this._map.removeSource(this._sourceId);
2346
+ if (this._map && typeof this._map.removeSource === 'function') {
2347
+ try {
2348
+ // First, remove any layers that are using this source
2349
+ const mapWithStyle = this._map;
2350
+ if (mapWithStyle.getStyle) {
2351
+ const style = mapWithStyle.getStyle();
2352
+ const layers = style?.layers || [];
2353
+ layers.forEach(layer => {
2354
+ if (layer.source === this._sourceId && this._map.getLayer && this._map.getLayer(layer.id)) {
2355
+ this._map.removeLayer(layer.id);
2356
+ }
2357
+ });
2358
+ }
2359
+ // Then check if source exists before trying to remove it
2360
+ if (this._map.getSource && this._map.getSource(this._sourceId)) {
2361
+ this._map.removeSource(this._sourceId);
2362
+ }
2363
+ } catch (error) {
2364
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
2365
+ }
2136
2366
  }
2137
2367
  }
2138
2368
  async queryFeatures(options) {