esri-gl 1.0.6 → 2.0.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.
@@ -1,9 +1,13 @@
1
- import { useState, useEffect, useMemo, useRef } from 'react';
2
- import { D as DynamicMapService, d as TiledMapService, c as ImageService, e as VectorTileService, V as VectorBasemapStyle, F as FeatureService } from './IdentifyImage-DmyKcbAv.js';
3
- export { a as Find, I as IdentifyFeatures, b as IdentifyImage, Q as Query, S as Service, T as Task, f as cleanTrailingSlash, g as find, h as getServiceDetails, i as identifyImage, q as query, u as updateAttribution } from './IdentifyImage-DmyKcbAv.js';
1
+ import { D as DynamicMapService, d as TiledMapService, c as ImageService, e as VectorTileService, g as esriRequest, V as VectorBasemapStyle, F as FeatureService, k as serviceFromPortalItem } from './index-DsY1_0df.js';
2
+ export { a as Find, I as IdentifyFeatures, b as IdentifyImage, Q as Query, S as Service, T as Task, f as cleanTrailingSlash, h as find, i as getServiceDetails, j as identifyImage, q as query, r as resolveAuthentication, s as searchPortalItems, l as servicesFromWebMap, u as updateAttribution } from './index-DsY1_0df.js';
4
3
  import { useMap } from 'react-map-gl/mapbox';
5
4
  import { useMap as useMap$1 } from 'react-map-gl/maplibre';
6
- import { b as useFeatureService, e as useVectorTileService, c as useImageService, d as useTiledMapService, u as useDynamicMapService } from './useFeatureService-E9PiUOLP.js';
5
+ import { useState, useEffect, useRef, useMemo } from 'react';
6
+ import { b as useFeatureService, e as useVectorTileService, c as useImageService, d as useTiledMapService, u as useDynamicMapService } from './useFeatureService-BRY6PHUs.js';
7
+ export { BasemapStyleSession } from '@esri/arcgis-rest-basemap-sessions';
8
+ export { ApiKeyManager, ApplicationCredentialsManager, ArcGISAuthError, ArcGISIdentityManager, ArcGISRequestError } from '@esri/arcgis-rest-request';
9
+ export { SearchQueryBuilder } from '@esri/arcgis-rest-portal';
10
+ import '@esri/arcgis-rest-feature-service';
7
11
  import '@mapbox/tilebelt';
8
12
  import 'arcgis-pbf-parser';
9
13
 
@@ -49,6 +53,7 @@ function applyAuthOptions(options, props) {
49
53
  const target = options;
50
54
  if (props.token !== undefined) target.token = props.token;
51
55
  if (props.apiKey !== undefined) target.apiKey = props.apiKey;
56
+ if (props.authentication !== undefined) target.authentication = props.authentication;
52
57
  if (props.proxy !== undefined) target.proxy = props.proxy;
53
58
  if (props.getAttributionFromService !== undefined) target.getAttributionFromService = props.getAttributionFromService;
54
59
  if (props.requestParams !== undefined) target.requestParams = props.requestParams;
@@ -57,84 +62,137 @@ function applyAuthOptions(options, props) {
57
62
  }
58
63
 
59
64
  /**
60
- * React Map GL component for Esri Dynamic Map Service
65
+ * Returns true once the underlying map's style has finished loading.
66
+ * Returns false when there is no map yet, the map does not expose the
67
+ * expected lifecycle API, or the style has not finished loading.
61
68
  */
62
- function EsriDynamicLayer(props) {
63
- const {
64
- current: map
65
- } = useReactMapGL();
66
- const sourceId = props.sourceId || `esri-dynamic-${props.id}`;
67
- const [isMapLoaded, setIsMapLoaded] = useState(false);
68
- // Wait for map to be loaded before creating service
69
+ function useMapLoaded(map) {
70
+ const [isLoaded, setIsLoaded] = useState(false);
69
71
  useEffect(() => {
70
- if (!map) return;
72
+ if (!map) {
73
+ setIsLoaded(false);
74
+ return;
75
+ }
71
76
  const mapInstance = map.getMap?.();
72
- const mi = mapInstance;
73
- if (!mi || typeof mi.isStyleLoaded !== 'function') {
77
+ if (!mapInstance || typeof mapInstance.isStyleLoaded !== 'function') {
78
+ setIsLoaded(false);
74
79
  return;
75
80
  }
76
- if (mi.isStyleLoaded()) {
77
- setIsMapLoaded(true);
81
+ if (mapInstance.isStyleLoaded()) {
82
+ setIsLoaded(true);
78
83
  return;
79
84
  }
80
- const handleLoad = () => setIsMapLoaded(true);
81
- mi?.once?.('load', handleLoad);
85
+ const handleLoad = () => setIsLoaded(true);
86
+ mapInstance.once?.('load', handleLoad);
82
87
  return () => {
83
- mi?.off?.('load', handleLoad);
88
+ mapInstance.off?.('load', handleLoad);
84
89
  };
85
90
  }, [map]);
86
- const service = useMemo(() => {
87
- if (!map || !isMapLoaded) return null;
88
- const mapInstance = map.getMap?.();
89
- if (!mapInstance) return null;
90
- // Only include defined properties to avoid overriding defaults with undefined
91
- const options = {
92
- url: props.url
93
- };
94
- if (props.layers !== undefined) options.layers = props.layers;
95
- if (props.layerDefs !== undefined) options.layerDefs = props.layerDefs;
96
- if (props.format !== undefined) options.format = props.format;
97
- if (props.dpi !== undefined) options.dpi = props.dpi;
98
- if (props.transparent !== undefined) options.transparent = props.transparent;
99
- applyAuthOptions(options, props);
100
- return new DynamicMapService(sourceId, mapInstance,
101
- // Type assertion for map compatibility
102
- options);
103
- }, [map, isMapLoaded, sourceId, props.url, props.layers, props.layerDefs, props.format, props.dpi, props.transparent, props.token, props.apiKey, props.proxy, props.getAttributionFromService, props.requestParams, props.fetchOptions]);
91
+ return isLoaded;
92
+ }
93
+
94
+ /**
95
+ * Shared lifecycle for react-map-gl components that wrap an Esri raster
96
+ * service. Waits for the map style to load, then (re)builds the service and its
97
+ * raster layer in a single effect so that when `serviceDeps` change the old
98
+ * source is torn down *before* the new service recreates it — otherwise the
99
+ * service constructor would see the stale source and skip re-adding it.
100
+ */
101
+ function useRasterLayer(options) {
102
+ const {
103
+ map,
104
+ layerId,
105
+ sourceId,
106
+ beforeId,
107
+ visible,
108
+ serviceDeps,
109
+ createService
110
+ } = options;
111
+ const isMapLoaded = useMapLoaded(map);
112
+ const [service, setService] = useState(null);
113
+ // Keep the latest createService without making it a dependency (callers pass
114
+ // a fresh closure each render; serviceDeps captures what actually matters).
115
+ const createServiceRef = useRef(createService);
116
+ createServiceRef.current = createService;
104
117
  useEffect(() => {
105
- if (!map || !service) return;
118
+ if (!map || !isMapLoaded) {
119
+ setService(null);
120
+ return;
121
+ }
106
122
  const mapInstance = map.getMap?.();
107
- if (!mapInstance || typeof mapInstance.getLayer !== 'function' || typeof mapInstance.addLayer !== 'function') {
108
- return () => {
109
- service.remove();
110
- };
123
+ if (!mapInstance || typeof mapInstance.addLayer !== 'function') {
124
+ return;
111
125
  }
112
- // Add raster layer
113
- if (!mapInstance.getLayer(props.id)) {
126
+ const svc = createServiceRef.current(mapInstance, sourceId);
127
+ setService(svc);
128
+ // Add the raster layer once its source exists.
129
+ const sourceReady = typeof mapInstance.getSource !== 'function' || Boolean(mapInstance.getSource(sourceId));
130
+ if (!mapInstance.getLayer?.(layerId) && sourceReady) {
114
131
  const layerConfig = {
115
- id: props.id,
132
+ id: layerId,
116
133
  type: 'raster',
117
134
  source: sourceId,
118
135
  layout: {
119
- visibility: props.visible !== false ? 'visible' : 'none'
136
+ visibility: visible !== false ? 'visible' : 'none'
120
137
  }
121
138
  };
122
- if (props.beforeId) {
123
- mapInstance.addLayer(layerConfig, props.beforeId);
124
- } else {
125
- mapInstance.addLayer(layerConfig);
139
+ try {
140
+ if (beforeId) {
141
+ mapInstance.addLayer?.(layerConfig, beforeId);
142
+ } else {
143
+ mapInstance.addLayer?.(layerConfig);
144
+ }
145
+ } catch (err) {
146
+ if (process.env?.NODE_ENV !== 'test') {
147
+ console.warn(`useRasterLayer: skipped adding layer "${layerId}"`, err);
148
+ }
126
149
  }
127
150
  }
128
- // Cleanup function
129
151
  return () => {
130
- if (mapInstance.getStyle?.() && mapInstance.getLayer?.(props.id)) {
131
- mapInstance.removeLayer(props.id);
132
- }
133
- if (service) {
134
- service.remove();
152
+ // Remove the layer first, then the service (which removes its source), so
153
+ // a subsequent rebuild gets a clean slate for the same sourceId.
154
+ try {
155
+ if (mapInstance.getStyle?.() && mapInstance.getLayer?.(layerId)) {
156
+ mapInstance.removeLayer?.(layerId);
157
+ }
158
+ } catch {
159
+ // layer may already be gone
135
160
  }
161
+ svc.remove();
136
162
  };
137
- }, [map, service, props.id, props.beforeId, props.visible, sourceId]);
163
+ // serviceDeps is spread so any change rebuilds the layer + source.
164
+ }, [map, isMapLoaded, sourceId, layerId, beforeId, visible, ...serviceDeps]);
165
+ return service;
166
+ }
167
+
168
+ /**
169
+ * React Map GL component for Esri Dynamic Map Service
170
+ */
171
+ function EsriDynamicLayer(props) {
172
+ const {
173
+ current: map
174
+ } = useReactMapGL();
175
+ const sourceId = props.sourceId || `esri-dynamic-${props.id}`;
176
+ useRasterLayer({
177
+ map,
178
+ layerId: props.id,
179
+ sourceId,
180
+ beforeId: props.beforeId,
181
+ visible: props.visible,
182
+ serviceDeps: [props.url, props.layers, props.layerDefs, props.format, props.dpi, props.transparent, props.token, props.apiKey, props.authentication, props.proxy, props.getAttributionFromService, props.requestParams, props.fetchOptions],
183
+ createService: (mapInstance, resolvedSourceId) => {
184
+ const options = {
185
+ url: props.url
186
+ };
187
+ if (props.layers !== undefined) options.layers = props.layers;
188
+ if (props.layerDefs !== undefined) options.layerDefs = props.layerDefs;
189
+ if (props.format !== undefined) options.format = props.format;
190
+ if (props.dpi !== undefined) options.dpi = props.dpi;
191
+ if (props.transparent !== undefined) options.transparent = props.transparent;
192
+ applyAuthOptions(options, props);
193
+ return new DynamicMapService(resolvedSourceId, mapInstance, options);
194
+ }
195
+ });
138
196
  return null;
139
197
  }
140
198
 
@@ -146,69 +204,21 @@ function EsriTiledLayer(props) {
146
204
  current: map
147
205
  } = useReactMapGL();
148
206
  const sourceId = props.sourceId || `esri-tiled-${props.id}`;
149
- const [isMapLoaded, setIsMapLoaded] = useState(false);
150
- // Wait for map to be loaded before creating service
151
- useEffect(() => {
152
- if (!map) return;
153
- const mapInstance = map.getMap?.();
154
- const mi = mapInstance;
155
- if (!mi || typeof mi.isStyleLoaded !== 'function') {
156
- return;
157
- }
158
- if (mi.isStyleLoaded()) {
159
- setIsMapLoaded(true);
160
- return;
161
- }
162
- const handleLoad = () => setIsMapLoaded(true);
163
- mi?.once?.('load', handleLoad);
164
- return () => {
165
- mi?.off?.('load', handleLoad);
166
- };
167
- }, [map]);
168
- const service = useMemo(() => {
169
- if (!map || !isMapLoaded) return null;
170
- const mapInstance = map.getMap?.();
171
- if (!mapInstance) return null;
172
- const options = {
173
- url: props.url
174
- };
175
- applyAuthOptions(options, props);
176
- return new TiledMapService(sourceId, mapInstance, options);
177
- }, [map, isMapLoaded, sourceId, props.url, props.token, props.apiKey, props.proxy, props.getAttributionFromService, props.requestParams, props.fetchOptions]);
178
- useEffect(() => {
179
- if (!map || !service) return;
180
- const mapInstance = map.getMap?.();
181
- if (!mapInstance || typeof mapInstance.getLayer !== 'function' || typeof mapInstance.addLayer !== 'function') {
182
- return () => {
183
- service.remove();
184
- };
185
- }
186
- // Add raster layer
187
- if (!mapInstance.getLayer(props.id)) {
188
- const layerConfig = {
189
- id: props.id,
190
- type: 'raster',
191
- source: sourceId,
192
- layout: {
193
- visibility: props.visible !== false ? 'visible' : 'none'
194
- }
207
+ useRasterLayer({
208
+ map,
209
+ layerId: props.id,
210
+ sourceId,
211
+ beforeId: props.beforeId,
212
+ visible: props.visible,
213
+ serviceDeps: [props.url, props.token, props.apiKey, props.authentication, props.proxy, props.getAttributionFromService, props.requestParams, props.fetchOptions],
214
+ createService: (mapInstance, resolvedSourceId) => {
215
+ const options = {
216
+ url: props.url
195
217
  };
196
- if (props.beforeId) {
197
- mapInstance.addLayer(layerConfig, props.beforeId);
198
- } else {
199
- mapInstance.addLayer(layerConfig);
200
- }
218
+ applyAuthOptions(options, props);
219
+ return new TiledMapService(resolvedSourceId, mapInstance, options);
201
220
  }
202
- // Cleanup function
203
- return () => {
204
- if (mapInstance.getStyle?.() && mapInstance.getLayer?.(props.id)) {
205
- mapInstance.removeLayer(props.id);
206
- }
207
- if (service) {
208
- service.remove();
209
- }
210
- };
211
- }, [map, service, props.id, props.beforeId, props.visible, sourceId]);
221
+ });
212
222
  return null;
213
223
  }
214
224
 
@@ -220,73 +230,24 @@ function EsriImageLayer(props) {
220
230
  current: map
221
231
  } = useReactMapGL();
222
232
  const sourceId = props.sourceId || `esri-image-${props.id}`;
223
- const [isMapLoaded, setIsMapLoaded] = useState(false);
224
- // Wait for map to be loaded before creating service
225
- useEffect(() => {
226
- if (!map) return;
227
- const mapInstance = map.getMap?.();
228
- const mi = mapInstance;
229
- if (!mi || typeof mi.isStyleLoaded !== 'function') {
230
- return;
231
- }
232
- if (mi.isStyleLoaded()) {
233
- setIsMapLoaded(true);
234
- return;
235
- }
236
- const handleLoad = () => setIsMapLoaded(true);
237
- mi?.once?.('load', handleLoad);
238
- return () => {
239
- mi?.off?.('load', handleLoad);
240
- };
241
- }, [map]);
242
- const service = useMemo(() => {
243
- if (!map || !isMapLoaded) return null;
244
- const mapInstance = map.getMap?.();
245
- if (!mapInstance) return null;
246
- // Only include defined properties to avoid overriding defaults with undefined
247
- const options = {
248
- url: props.url
249
- };
250
- if (props.renderingRule !== undefined) options.renderingRule = props.renderingRule;
251
- if (props.mosaicRule !== undefined) options.mosaicRule = props.mosaicRule;
252
- if (props.format !== undefined) options.format = props.format;
253
- applyAuthOptions(options, props);
254
- return new ImageService(sourceId, mapInstance, options);
255
- }, [map, isMapLoaded, sourceId, props.url, props.renderingRule, props.mosaicRule, props.format, props.token, props.apiKey, props.proxy, props.getAttributionFromService, props.requestParams, props.fetchOptions]);
256
- useEffect(() => {
257
- if (!map || !service) return;
258
- const mapInstance = map.getMap?.();
259
- if (!mapInstance || typeof mapInstance.getLayer !== 'function' || typeof mapInstance.addLayer !== 'function') {
260
- return () => {
261
- service.remove();
262
- };
263
- }
264
- // Add raster layer
265
- if (!mapInstance.getLayer(props.id)) {
266
- const layerConfig = {
267
- id: props.id,
268
- type: 'raster',
269
- source: sourceId,
270
- layout: {
271
- visibility: props.visible !== false ? 'visible' : 'none'
272
- }
233
+ useRasterLayer({
234
+ map,
235
+ layerId: props.id,
236
+ sourceId,
237
+ beforeId: props.beforeId,
238
+ visible: props.visible,
239
+ serviceDeps: [props.url, props.renderingRule, props.mosaicRule, props.format, props.token, props.apiKey, props.authentication, props.proxy, props.getAttributionFromService, props.requestParams, props.fetchOptions],
240
+ createService: (mapInstance, resolvedSourceId) => {
241
+ const options = {
242
+ url: props.url
273
243
  };
274
- if (props.beforeId) {
275
- mapInstance.addLayer(layerConfig, props.beforeId);
276
- } else {
277
- mapInstance.addLayer(layerConfig);
278
- }
244
+ if (props.renderingRule !== undefined) options.renderingRule = props.renderingRule;
245
+ if (props.mosaicRule !== undefined) options.mosaicRule = props.mosaicRule;
246
+ if (props.format !== undefined) options.format = props.format;
247
+ applyAuthOptions(options, props);
248
+ return new ImageService(resolvedSourceId, mapInstance, options);
279
249
  }
280
- // Cleanup function
281
- return () => {
282
- if (mapInstance.getStyle?.() && mapInstance.getLayer?.(props.id)) {
283
- mapInstance.removeLayer(props.id);
284
- }
285
- if (service) {
286
- service.remove();
287
- }
288
- };
289
- }, [map, service, props.id, props.beforeId, props.visible, sourceId]);
250
+ });
290
251
  return null;
291
252
  }
292
253
 
@@ -298,31 +259,14 @@ function EsriVectorTileLayer(props) {
298
259
  current: map
299
260
  } = useReactMapGL();
300
261
  const sourceId = props.sourceId || `esri-vector-tile-${props.id}`;
301
- const [isMapLoaded, setIsMapLoaded] = useState(false);
262
+ const isMapLoaded = useMapLoaded(map);
302
263
  const serviceRef = useRef(null);
303
264
  const layerIdsRef = useRef([]);
304
- // Wait for map to be loaded before creating service
305
- useEffect(() => {
306
- if (!map) return;
307
- const mapInstance = map.getMap?.();
308
- const mi = mapInstance;
309
- if (!mi || typeof mi.isStyleLoaded !== 'function') return;
310
- if (mi.isStyleLoaded()) {
311
- setIsMapLoaded(true);
312
- return;
313
- }
314
- const handleLoad = () => setIsMapLoaded(true);
315
- mi?.once?.('load', handleLoad);
316
- return () => {
317
- mi?.off?.('load', handleLoad);
318
- };
319
- }, [map]);
320
- // Create VectorTileService, fetch style, add layers, and clean up on unmount
321
265
  useEffect(() => {
322
266
  if (!map || !isMapLoaded) return;
323
267
  const mapInstance = map.getMap?.();
324
268
  if (!mapInstance) return;
325
- const mi = mapInstance;
269
+ const layerApi = mapInstance;
326
270
  let cancelled = false;
327
271
  const options = {
328
272
  url: props.url
@@ -330,35 +274,23 @@ function EsriVectorTileLayer(props) {
330
274
  applyAuthOptions(options, props);
331
275
  const service = new VectorTileService(sourceId, mapInstance, options);
332
276
  serviceRef.current = service;
333
- // Fetch the full style and add all layers from it
334
277
  service.getStyle().then(() => {
335
- if (cancelled) return;
336
- // Fetch the full style document to get all layers
337
- let styleUrl = `${props.url}/resources/styles/root.json`;
338
- if (props.token) {
339
- styleUrl += `?token=${encodeURIComponent(props.token)}`;
340
- }
341
- const fetchInit = {
342
- ...(props.fetchOptions || {})
343
- };
344
- if (props.apiKey) {
345
- fetchInit.headers = {
346
- ...(fetchInit.headers || {}),
347
- 'X-Esri-Authorization': `Bearer ${props.apiKey}`
348
- };
349
- }
350
- return fetch(styleUrl, fetchInit);
351
- }).then(response => {
352
- if (cancelled || !response) return;
353
- if (!response.ok) throw new Error(`Failed to fetch style: ${response.status}`);
354
- return response.json();
278
+ if (cancelled) return undefined;
279
+ // Fetch the full style document through the shared request layer so
280
+ // auth (token / apiKey / authentication) is handled consistently.
281
+ return esriRequest(`${props.url}/resources/styles/root.json`, {
282
+ httpMethod: 'GET',
283
+ token: props.token,
284
+ apiKey: props.apiKey,
285
+ authentication: props.authentication
286
+ });
355
287
  }).then(data => {
356
288
  if (cancelled || !data?.layers) return;
357
289
  const addedIds = [];
358
290
  for (const layer of data.layers) {
359
291
  if (!layer['source-layer']) continue;
360
292
  const layerId = `${props.id}-${layer.id}`;
361
- if (mi.getLayer?.(layerId)) continue;
293
+ if (layerApi.getLayer?.(layerId)) continue;
362
294
  const layerConfig = {
363
295
  id: layerId,
364
296
  type: layer.type,
@@ -370,17 +302,16 @@ function EsriVectorTileLayer(props) {
370
302
  if (layer.filter) layerConfig.filter = layer.filter;
371
303
  if (layer.minzoom !== undefined) layerConfig.minzoom = layer.minzoom;
372
304
  if (layer.maxzoom !== undefined) layerConfig.maxzoom = layer.maxzoom;
373
- // Apply visibility from props
374
305
  if (props.visible === false) {
375
306
  layerConfig.layout = {
376
- ...layerConfig.layout,
307
+ ...(layerConfig.layout || {}),
377
308
  visibility: 'none'
378
309
  };
379
310
  }
380
311
  if (props.beforeId) {
381
- mi.addLayer(layerConfig, props.beforeId);
312
+ layerApi.addLayer?.(layerConfig, props.beforeId);
382
313
  } else {
383
- mi.addLayer(layerConfig);
314
+ layerApi.addLayer?.(layerConfig);
384
315
  }
385
316
  addedIds.push(layerId);
386
317
  }
@@ -390,11 +321,10 @@ function EsriVectorTileLayer(props) {
390
321
  });
391
322
  return () => {
392
323
  cancelled = true;
393
- // Remove layers we added
394
- if (mi.getStyle?.()) {
324
+ if (layerApi.getStyle?.()) {
395
325
  for (const id of layerIdsRef.current) {
396
326
  try {
397
- if (mi.getLayer?.(id)) mi.removeLayer(id);
327
+ if (layerApi.getLayer?.(id)) layerApi.removeLayer?.(id);
398
328
  } catch {
399
329
  // layer may already be gone
400
330
  }
@@ -404,7 +334,7 @@ function EsriVectorTileLayer(props) {
404
334
  service.remove();
405
335
  serviceRef.current = null;
406
336
  };
407
- }, [map, isMapLoaded, sourceId, props.url, props.id, props.beforeId, props.visible, props.token, props.apiKey, props.proxy, props.getAttributionFromService, props.requestParams, props.fetchOptions]);
337
+ }, [map, isMapLoaded, sourceId, props.url, props.id, props.beforeId, props.visible, props.token, props.apiKey, props.authentication, props.proxy, props.getAttributionFromService, props.requestParams, props.fetchOptions]);
408
338
  return null;
409
339
  }
410
340
 
@@ -416,10 +346,15 @@ function EsriVectorBasemapLayer(props) {
416
346
  current: map
417
347
  } = useReactMapGL();
418
348
  const service = useMemo(() => {
419
- return new VectorBasemapStyle(props.basemapEnum, {
420
- token: props.token
421
- });
422
- }, [props.basemapEnum, props.token]);
349
+ // Forward all the auth/locale options VectorBasemapStyle supports (token or
350
+ // apiKey, plus language/worldview), only including the ones provided.
351
+ const auth = {};
352
+ if (props.token !== undefined) auth.token = props.token;
353
+ if (props.apiKey !== undefined) auth.apiKey = props.apiKey;
354
+ if (props.language !== undefined) auth.language = props.language;
355
+ if (props.worldview !== undefined) auth.worldview = props.worldview;
356
+ return new VectorBasemapStyle(props.basemapEnum, auth);
357
+ }, [props.basemapEnum, props.token, props.apiKey, props.language, props.worldview]);
423
358
  useEffect(() => {
424
359
  if (!map || !service) return;
425
360
  // Vector basemap styles replace the entire map style
@@ -443,38 +378,18 @@ function EsriFeatureLayer(props) {
443
378
  current: map
444
379
  } = useReactMapGL();
445
380
  const sourceId = props.sourceId || `esri-feature-${props.id}`;
446
- const [isMapLoaded, setIsMapLoaded] = useState(false);
381
+ const isMapLoaded = useMapLoaded(map);
447
382
  const serviceRef = useRef(null);
448
383
  // Keep stable refs for object props to avoid effect re-runs on every render
449
384
  const paintRef = useRef(props.paint);
450
385
  paintRef.current = props.paint;
451
386
  const layoutRef = useRef(props.layout);
452
387
  layoutRef.current = props.layout;
453
- // Wait for map to be loaded before creating service
454
- useEffect(() => {
455
- if (!map) return;
456
- const mapInstance = map.getMap?.();
457
- const mi = mapInstance;
458
- if (!mi || typeof mi.isStyleLoaded !== 'function') {
459
- return;
460
- }
461
- if (mi.isStyleLoaded()) {
462
- setIsMapLoaded(true);
463
- return;
464
- }
465
- const handleLoad = () => setIsMapLoaded(true);
466
- mi?.once?.('load', handleLoad);
467
- return () => {
468
- mi?.off?.('load', handleLoad);
469
- };
470
- }, [map]);
471
- // Create FeatureService, add layer, and clean up on unmount
472
388
  useEffect(() => {
473
389
  if (!map || !isMapLoaded) return;
474
390
  const mapInstance = map.getMap?.();
475
391
  if (!mapInstance) return;
476
- const mi = mapInstance;
477
- // Only include defined properties to avoid overriding defaults with undefined
392
+ const layerApi = mapInstance;
478
393
  const options = {
479
394
  url: props.url
480
395
  };
@@ -483,8 +398,7 @@ function EsriFeatureLayer(props) {
483
398
  applyAuthOptions(options, props);
484
399
  const service = new FeatureService(sourceId, mapInstance, options);
485
400
  serviceRef.current = service;
486
- // Add GeoJSON layer
487
- if (typeof mi.getLayer === 'function' && typeof mi.addLayer === 'function' && !mi.getLayer(props.id)) {
401
+ if (typeof layerApi.getLayer === 'function' && typeof layerApi.addLayer === 'function' && !layerApi.getLayer(props.id)) {
488
402
  const layerType = props.type || 'fill';
489
403
  const defaultPaint = layerType === 'circle' ? {
490
404
  'circle-radius': 4,
@@ -504,87 +418,161 @@ function EsriFeatureLayer(props) {
504
418
  }
505
419
  };
506
420
  if (props.beforeId) {
507
- mi.addLayer(layerConfig, props.beforeId);
421
+ layerApi.addLayer(layerConfig, props.beforeId);
508
422
  } else {
509
- mi.addLayer(layerConfig);
423
+ layerApi.addLayer(layerConfig);
510
424
  }
511
425
  }
512
426
  return () => {
513
- if (mi.getStyle?.() && mi.getLayer?.(props.id)) {
514
- mi.removeLayer(props.id);
427
+ if (layerApi.getStyle?.() && layerApi.getLayer?.(props.id)) {
428
+ layerApi.removeLayer?.(props.id);
515
429
  }
516
430
  service.remove();
517
431
  serviceRef.current = null;
518
432
  };
519
- }, [map, isMapLoaded, sourceId, props.url, props.where, props.outFields, props.id, props.beforeId, props.visible, props.type, props.token, props.apiKey, props.proxy, props.getAttributionFromService, props.requestParams, props.fetchOptions]);
433
+ }, [map, isMapLoaded, sourceId, props.url, props.where, props.outFields, props.id, props.beforeId, props.visible, props.type, props.token, props.apiKey, props.authentication, props.proxy, props.getAttributionFromService, props.requestParams, props.fetchOptions]);
520
434
  return null;
521
435
  }
522
436
 
523
437
  /**
524
- * Hook for using Esri services with Mapbox GL JS (via react-map-gl)
438
+ * React Map GL component that resolves an ArcGIS portal item id to the matching
439
+ * esri-gl service and adds a renderer-appropriate layer for it.
525
440
  */
526
- function useEsriMapboxLayer() {
441
+ function EsriPortalLayer(props) {
527
442
  const {
528
- current: mapRef
529
- } = useMap();
530
- const map = mapRef?.getMap();
443
+ current: map
444
+ } = useReactMapGL();
445
+ const sourceId = props.sourceId || `esri-portal-${props.id}`;
446
+ const isMapLoaded = useMapLoaded(map);
447
+ const serviceRef = useRef(null);
448
+ useEffect(() => {
449
+ if (!map || !isMapLoaded || !props.itemId) return;
450
+ const mapInstance = map.getMap?.();
451
+ if (!mapInstance) return;
452
+ const layerApi = mapInstance;
453
+ const layerId = props.id;
454
+ let cancelled = false;
455
+ const options = {};
456
+ applyAuthOptions(options, props);
457
+ if (props.layerId !== undefined) options.layerId = props.layerId;
458
+ if (props.portal !== undefined) options.portal = props.portal;
459
+ serviceFromPortalItem(sourceId, mapInstance, props.itemId, options).then(async result => {
460
+ if (cancelled) {
461
+ try {
462
+ result.service.remove();
463
+ } catch {
464
+ /* ignore */
465
+ }
466
+ return;
467
+ }
468
+ serviceRef.current = result.service;
469
+ if (layerApi.getLayer?.(layerId)) {
470
+ props.onResolve?.(result);
471
+ return;
472
+ }
473
+ let layerConfig;
474
+ if (result.kind === 'feature' || result.kind === 'vector-tile') {
475
+ // These services expose a default style (source already set to sourceId).
476
+ const styled = result.service;
477
+ const style = await styled.getStyle();
478
+ if (cancelled || layerApi.getLayer?.(layerId)) return;
479
+ layerConfig = {
480
+ ...style,
481
+ id: layerId,
482
+ source: sourceId
483
+ };
484
+ } else {
485
+ // dynamic / tiled / image → raster
486
+ layerConfig = {
487
+ id: layerId,
488
+ type: 'raster',
489
+ source: sourceId
490
+ };
491
+ }
492
+ if (props.visible === false) {
493
+ layerConfig.layout = {
494
+ ...(layerConfig.layout || {}),
495
+ visibility: 'none'
496
+ };
497
+ }
498
+ layerApi.addLayer?.(layerConfig, props.beforeId);
499
+ props.onResolve?.(result);
500
+ }).catch(err => {
501
+ if (!cancelled) console.warn('EsriPortalLayer: failed to resolve portal item', err);
502
+ });
503
+ return () => {
504
+ cancelled = true;
505
+ try {
506
+ if (layerApi.getLayer?.(layerId)) layerApi.removeLayer?.(layerId);
507
+ } catch {
508
+ /* layer may already be gone */
509
+ }
510
+ if (serviceRef.current) {
511
+ try {
512
+ serviceRef.current.remove();
513
+ } catch {
514
+ /* ignore */
515
+ }
516
+ serviceRef.current = null;
517
+ }
518
+ };
519
+ // Note: onResolve is intentionally excluded from deps to avoid re-resolving
520
+ // when an inline callback identity changes.
521
+ }, [map, isMapLoaded, sourceId, props.id, props.itemId, props.layerId, props.portal, props.beforeId, props.visible, props.token, props.apiKey, props.authentication]);
522
+ return null;
523
+ }
524
+
525
+ /**
526
+ * Build the `useEsri{Mapbox,Maplibre}Layer` hook result from a map collection
527
+ * returned by the corresponding react-map-gl `useMap` hook. Keeps the two
528
+ * flavors of the hook identical except for the map source.
529
+ */
530
+ function createEsriLayerHooks(collection) {
531
+ // Preserve legacy return shape: `map` is undefined when no ref, the
532
+ // underlying map otherwise. Child hooks type `map: Map | null`, so we
533
+ // coerce for the injection below.
534
+ const map = collection?.current?.getMap?.();
535
+ const injectedMap = map ?? null;
531
536
  return {
532
537
  map,
533
538
  useDynamicMapService: options => useDynamicMapService({
534
539
  ...options,
535
- map
540
+ map: injectedMap
536
541
  }),
537
542
  useTiledMapService: options => useTiledMapService({
538
543
  ...options,
539
- map
544
+ map: injectedMap
540
545
  }),
541
546
  useImageService: options => useImageService({
542
547
  ...options,
543
- map
548
+ map: injectedMap
544
549
  }),
545
550
  useVectorTileService: options => useVectorTileService({
546
551
  ...options,
547
- map
552
+ map: injectedMap
548
553
  }),
549
554
  useFeatureService: options => useFeatureService({
550
555
  ...options,
551
- map
556
+ map: injectedMap
552
557
  })
553
558
  };
554
559
  }
555
560
 
561
+ /**
562
+ * Hook for using Esri services with Mapbox GL JS (via react-map-gl)
563
+ */
564
+ function useEsriMapboxLayer() {
565
+ const collection = useMap();
566
+ return createEsriLayerHooks(collection);
567
+ }
568
+
556
569
  /**
557
570
  * Hook for using Esri services with MapLibre GL JS (via react-map-gl/maplibre)
558
571
  */
559
572
  function useEsriMaplibreLayer() {
560
- const {
561
- current: mapRef
562
- } = useMap$1();
563
- const map = mapRef?.getMap();
564
- return {
565
- map,
566
- useDynamicMapService: options => useDynamicMapService({
567
- ...options,
568
- map
569
- }),
570
- useTiledMapService: options => useTiledMapService({
571
- ...options,
572
- map
573
- }),
574
- useImageService: options => useImageService({
575
- ...options,
576
- map
577
- }),
578
- useVectorTileService: options => useVectorTileService({
579
- ...options,
580
- map
581
- }),
582
- useFeatureService: options => useFeatureService({
583
- ...options,
584
- map
585
- })
586
- };
573
+ const collection = useMap$1();
574
+ return createEsriLayerHooks(collection);
587
575
  }
588
576
 
589
- export { DynamicMapService, EsriDynamicLayer, EsriFeatureLayer, EsriImageLayer, EsriTiledLayer, EsriVectorBasemapLayer, EsriVectorTileLayer, FeatureService, ImageService, TiledMapService, VectorBasemapStyle, VectorTileService, useEsriMapboxLayer, useEsriMaplibreLayer };
577
+ export { DynamicMapService, EsriDynamicLayer, EsriFeatureLayer, EsriImageLayer, EsriPortalLayer, EsriTiledLayer, EsriVectorBasemapLayer, EsriVectorTileLayer, FeatureService, ImageService, TiledMapService, VectorBasemapStyle, VectorTileService, esriRequest, serviceFromPortalItem, useEsriMapboxLayer, useEsriMaplibreLayer };
590
578
  //# sourceMappingURL=react-map-gl.js.map