esri-gl 1.0.5 → 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
 
@@ -41,83 +45,154 @@ function useReactMapGL() {
41
45
  }
42
46
 
43
47
  /**
44
- * React Map GL component for Esri Dynamic Map Service
48
+ * Copy any defined auth/passthrough props from a layer component's props
49
+ * onto a service-options object. Undefined values are skipped so they
50
+ * do not override service defaults.
45
51
  */
46
- function EsriDynamicLayer(props) {
47
- const {
48
- current: map
49
- } = useReactMapGL();
50
- const sourceId = props.sourceId || `esri-dynamic-${props.id}`;
51
- const [isMapLoaded, setIsMapLoaded] = useState(false);
52
- // Wait for map to be loaded before creating service
52
+ function applyAuthOptions(options, props) {
53
+ const target = options;
54
+ if (props.token !== undefined) target.token = props.token;
55
+ if (props.apiKey !== undefined) target.apiKey = props.apiKey;
56
+ if (props.authentication !== undefined) target.authentication = props.authentication;
57
+ if (props.proxy !== undefined) target.proxy = props.proxy;
58
+ if (props.getAttributionFromService !== undefined) target.getAttributionFromService = props.getAttributionFromService;
59
+ if (props.requestParams !== undefined) target.requestParams = props.requestParams;
60
+ if (props.fetchOptions !== undefined) target.fetchOptions = props.fetchOptions;
61
+ return options;
62
+ }
63
+
64
+ /**
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.
68
+ */
69
+ function useMapLoaded(map) {
70
+ const [isLoaded, setIsLoaded] = useState(false);
53
71
  useEffect(() => {
54
- if (!map) return;
72
+ if (!map) {
73
+ setIsLoaded(false);
74
+ return;
75
+ }
55
76
  const mapInstance = map.getMap?.();
56
- const mi = mapInstance;
57
- if (!mi || typeof mi.isStyleLoaded !== 'function') {
77
+ if (!mapInstance || typeof mapInstance.isStyleLoaded !== 'function') {
78
+ setIsLoaded(false);
58
79
  return;
59
80
  }
60
- if (mi.isStyleLoaded()) {
61
- setIsMapLoaded(true);
81
+ if (mapInstance.isStyleLoaded()) {
82
+ setIsLoaded(true);
62
83
  return;
63
84
  }
64
- const handleLoad = () => setIsMapLoaded(true);
65
- mi?.once?.('load', handleLoad);
85
+ const handleLoad = () => setIsLoaded(true);
86
+ mapInstance.once?.('load', handleLoad);
66
87
  return () => {
67
- mi?.off?.('load', handleLoad);
88
+ mapInstance.off?.('load', handleLoad);
68
89
  };
69
90
  }, [map]);
70
- const service = useMemo(() => {
71
- if (!map || !isMapLoaded) return null;
72
- const mapInstance = map.getMap?.();
73
- if (!mapInstance) return null;
74
- // Only include defined properties to avoid overriding defaults with undefined
75
- const options = {
76
- url: props.url
77
- };
78
- if (props.layers !== undefined) options.layers = props.layers;
79
- if (props.layerDefs !== undefined) options.layerDefs = props.layerDefs;
80
- if (props.format !== undefined) options.format = props.format;
81
- if (props.dpi !== undefined) options.dpi = props.dpi;
82
- if (props.transparent !== undefined) options.transparent = props.transparent;
83
- return new DynamicMapService(sourceId, mapInstance,
84
- // Type assertion for map compatibility
85
- options);
86
- }, [map, isMapLoaded, sourceId, props.url, props.layers, props.layerDefs, props.format, props.dpi, props.transparent]);
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;
87
117
  useEffect(() => {
88
- if (!map || !service) return;
118
+ if (!map || !isMapLoaded) {
119
+ setService(null);
120
+ return;
121
+ }
89
122
  const mapInstance = map.getMap?.();
90
- if (!mapInstance || typeof mapInstance.getLayer !== 'function' || typeof mapInstance.addLayer !== 'function') {
91
- return () => {
92
- service.remove();
93
- };
123
+ if (!mapInstance || typeof mapInstance.addLayer !== 'function') {
124
+ return;
94
125
  }
95
- // Add raster layer
96
- 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) {
97
131
  const layerConfig = {
98
- id: props.id,
132
+ id: layerId,
99
133
  type: 'raster',
100
134
  source: sourceId,
101
135
  layout: {
102
- visibility: props.visible !== false ? 'visible' : 'none'
136
+ visibility: visible !== false ? 'visible' : 'none'
103
137
  }
104
138
  };
105
- if (props.beforeId) {
106
- mapInstance.addLayer(layerConfig, props.beforeId);
107
- } else {
108
- 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
+ }
109
149
  }
110
150
  }
111
- // Cleanup function
112
151
  return () => {
113
- if (mapInstance.getStyle?.() && mapInstance.getLayer?.(props.id)) {
114
- mapInstance.removeLayer(props.id);
115
- }
116
- if (service) {
117
- 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
118
160
  }
161
+ svc.remove();
119
162
  };
120
- }, [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
+ });
121
196
  return null;
122
197
  }
123
198
 
@@ -129,67 +204,21 @@ function EsriTiledLayer(props) {
129
204
  current: map
130
205
  } = useReactMapGL();
131
206
  const sourceId = props.sourceId || `esri-tiled-${props.id}`;
132
- const [isMapLoaded, setIsMapLoaded] = useState(false);
133
- // Wait for map to be loaded before creating service
134
- useEffect(() => {
135
- if (!map) return;
136
- const mapInstance = map.getMap?.();
137
- const mi = mapInstance;
138
- if (!mi || typeof mi.isStyleLoaded !== 'function') {
139
- return;
140
- }
141
- if (mi.isStyleLoaded()) {
142
- setIsMapLoaded(true);
143
- return;
144
- }
145
- const handleLoad = () => setIsMapLoaded(true);
146
- mi?.once?.('load', handleLoad);
147
- return () => {
148
- mi?.off?.('load', handleLoad);
149
- };
150
- }, [map]);
151
- const service = useMemo(() => {
152
- if (!map || !isMapLoaded) return null;
153
- const mapInstance = map.getMap?.();
154
- if (!mapInstance) return null;
155
- return new TiledMapService(sourceId, mapInstance, {
156
- url: props.url
157
- });
158
- }, [map, isMapLoaded, sourceId, props.url]);
159
- useEffect(() => {
160
- if (!map || !service) return;
161
- const mapInstance = map.getMap?.();
162
- if (!mapInstance || typeof mapInstance.getLayer !== 'function' || typeof mapInstance.addLayer !== 'function') {
163
- return () => {
164
- service.remove();
165
- };
166
- }
167
- // Add raster layer
168
- if (!mapInstance.getLayer(props.id)) {
169
- const layerConfig = {
170
- id: props.id,
171
- type: 'raster',
172
- source: sourceId,
173
- layout: {
174
- visibility: props.visible !== false ? 'visible' : 'none'
175
- }
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
176
217
  };
177
- if (props.beforeId) {
178
- mapInstance.addLayer(layerConfig, props.beforeId);
179
- } else {
180
- mapInstance.addLayer(layerConfig);
181
- }
218
+ applyAuthOptions(options, props);
219
+ return new TiledMapService(resolvedSourceId, mapInstance, options);
182
220
  }
183
- // Cleanup function
184
- return () => {
185
- if (mapInstance.getStyle?.() && mapInstance.getLayer?.(props.id)) {
186
- mapInstance.removeLayer(props.id);
187
- }
188
- if (service) {
189
- service.remove();
190
- }
191
- };
192
- }, [map, service, props.id, props.beforeId, props.visible, sourceId]);
221
+ });
193
222
  return null;
194
223
  }
195
224
 
@@ -201,72 +230,24 @@ function EsriImageLayer(props) {
201
230
  current: map
202
231
  } = useReactMapGL();
203
232
  const sourceId = props.sourceId || `esri-image-${props.id}`;
204
- const [isMapLoaded, setIsMapLoaded] = useState(false);
205
- // Wait for map to be loaded before creating service
206
- useEffect(() => {
207
- if (!map) return;
208
- const mapInstance = map.getMap?.();
209
- const mi = mapInstance;
210
- if (!mi || typeof mi.isStyleLoaded !== 'function') {
211
- return;
212
- }
213
- if (mi.isStyleLoaded()) {
214
- setIsMapLoaded(true);
215
- return;
216
- }
217
- const handleLoad = () => setIsMapLoaded(true);
218
- mi?.once?.('load', handleLoad);
219
- return () => {
220
- mi?.off?.('load', handleLoad);
221
- };
222
- }, [map]);
223
- const service = useMemo(() => {
224
- if (!map || !isMapLoaded) return null;
225
- const mapInstance = map.getMap?.();
226
- if (!mapInstance) return null;
227
- // Only include defined properties to avoid overriding defaults with undefined
228
- const options = {
229
- url: props.url
230
- };
231
- if (props.renderingRule !== undefined) options.renderingRule = props.renderingRule;
232
- if (props.mosaicRule !== undefined) options.mosaicRule = props.mosaicRule;
233
- if (props.format !== undefined) options.format = props.format;
234
- return new ImageService(sourceId, mapInstance, options);
235
- }, [map, isMapLoaded, sourceId, props.url, props.renderingRule, props.mosaicRule, props.format]);
236
- useEffect(() => {
237
- if (!map || !service) return;
238
- const mapInstance = map.getMap?.();
239
- if (!mapInstance || typeof mapInstance.getLayer !== 'function' || typeof mapInstance.addLayer !== 'function') {
240
- return () => {
241
- service.remove();
242
- };
243
- }
244
- // Add raster layer
245
- if (!mapInstance.getLayer(props.id)) {
246
- const layerConfig = {
247
- id: props.id,
248
- type: 'raster',
249
- source: sourceId,
250
- layout: {
251
- visibility: props.visible !== false ? 'visible' : 'none'
252
- }
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
253
243
  };
254
- if (props.beforeId) {
255
- mapInstance.addLayer(layerConfig, props.beforeId);
256
- } else {
257
- mapInstance.addLayer(layerConfig);
258
- }
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);
259
249
  }
260
- // Cleanup function
261
- return () => {
262
- if (mapInstance.getStyle?.() && mapInstance.getLayer?.(props.id)) {
263
- mapInstance.removeLayer(props.id);
264
- }
265
- if (service) {
266
- service.remove();
267
- }
268
- };
269
- }, [map, service, props.id, props.beforeId, props.visible, sourceId]);
250
+ });
270
251
  return null;
271
252
  }
272
253
 
@@ -278,53 +259,38 @@ function EsriVectorTileLayer(props) {
278
259
  current: map
279
260
  } = useReactMapGL();
280
261
  const sourceId = props.sourceId || `esri-vector-tile-${props.id}`;
281
- const [isMapLoaded, setIsMapLoaded] = useState(false);
262
+ const isMapLoaded = useMapLoaded(map);
282
263
  const serviceRef = useRef(null);
283
264
  const layerIdsRef = useRef([]);
284
- // Wait for map to be loaded before creating service
285
- useEffect(() => {
286
- if (!map) return;
287
- const mapInstance = map.getMap?.();
288
- const mi = mapInstance;
289
- if (!mi || typeof mi.isStyleLoaded !== 'function') return;
290
- if (mi.isStyleLoaded()) {
291
- setIsMapLoaded(true);
292
- return;
293
- }
294
- const handleLoad = () => setIsMapLoaded(true);
295
- mi?.once?.('load', handleLoad);
296
- return () => {
297
- mi?.off?.('load', handleLoad);
298
- };
299
- }, [map]);
300
- // Create VectorTileService, fetch style, add layers, and clean up on unmount
301
265
  useEffect(() => {
302
266
  if (!map || !isMapLoaded) return;
303
267
  const mapInstance = map.getMap?.();
304
268
  if (!mapInstance) return;
305
- const mi = mapInstance;
269
+ const layerApi = mapInstance;
306
270
  let cancelled = false;
307
- const service = new VectorTileService(sourceId, mapInstance, {
271
+ const options = {
308
272
  url: props.url
309
- });
273
+ };
274
+ applyAuthOptions(options, props);
275
+ const service = new VectorTileService(sourceId, mapInstance, options);
310
276
  serviceRef.current = service;
311
- // Fetch the full style and add all layers from it
312
277
  service.getStyle().then(() => {
313
- if (cancelled) return;
314
- // Fetch the full style document to get all layers
315
- const styleUrl = `${props.url}/resources/styles/root.json`;
316
- return fetch(styleUrl);
317
- }).then(response => {
318
- if (cancelled || !response) return;
319
- if (!response.ok) throw new Error(`Failed to fetch style: ${response.status}`);
320
- 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
+ });
321
287
  }).then(data => {
322
288
  if (cancelled || !data?.layers) return;
323
289
  const addedIds = [];
324
290
  for (const layer of data.layers) {
325
291
  if (!layer['source-layer']) continue;
326
292
  const layerId = `${props.id}-${layer.id}`;
327
- if (mi.getLayer?.(layerId)) continue;
293
+ if (layerApi.getLayer?.(layerId)) continue;
328
294
  const layerConfig = {
329
295
  id: layerId,
330
296
  type: layer.type,
@@ -336,17 +302,16 @@ function EsriVectorTileLayer(props) {
336
302
  if (layer.filter) layerConfig.filter = layer.filter;
337
303
  if (layer.minzoom !== undefined) layerConfig.minzoom = layer.minzoom;
338
304
  if (layer.maxzoom !== undefined) layerConfig.maxzoom = layer.maxzoom;
339
- // Apply visibility from props
340
305
  if (props.visible === false) {
341
306
  layerConfig.layout = {
342
- ...layerConfig.layout,
307
+ ...(layerConfig.layout || {}),
343
308
  visibility: 'none'
344
309
  };
345
310
  }
346
311
  if (props.beforeId) {
347
- mi.addLayer(layerConfig, props.beforeId);
312
+ layerApi.addLayer?.(layerConfig, props.beforeId);
348
313
  } else {
349
- mi.addLayer(layerConfig);
314
+ layerApi.addLayer?.(layerConfig);
350
315
  }
351
316
  addedIds.push(layerId);
352
317
  }
@@ -356,11 +321,10 @@ function EsriVectorTileLayer(props) {
356
321
  });
357
322
  return () => {
358
323
  cancelled = true;
359
- // Remove layers we added
360
- if (mi.getStyle?.()) {
324
+ if (layerApi.getStyle?.()) {
361
325
  for (const id of layerIdsRef.current) {
362
326
  try {
363
- if (mi.getLayer?.(id)) mi.removeLayer(id);
327
+ if (layerApi.getLayer?.(id)) layerApi.removeLayer?.(id);
364
328
  } catch {
365
329
  // layer may already be gone
366
330
  }
@@ -370,7 +334,7 @@ function EsriVectorTileLayer(props) {
370
334
  service.remove();
371
335
  serviceRef.current = null;
372
336
  };
373
- }, [map, isMapLoaded, sourceId, props.url, props.id, props.beforeId, props.visible]);
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]);
374
338
  return null;
375
339
  }
376
340
 
@@ -382,10 +346,15 @@ function EsriVectorBasemapLayer(props) {
382
346
  current: map
383
347
  } = useReactMapGL();
384
348
  const service = useMemo(() => {
385
- return new VectorBasemapStyle(props.basemapEnum, {
386
- token: props.token
387
- });
388
- }, [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]);
389
358
  useEffect(() => {
390
359
  if (!map || !service) return;
391
360
  // Vector basemap styles replace the entire map style
@@ -409,47 +378,27 @@ function EsriFeatureLayer(props) {
409
378
  current: map
410
379
  } = useReactMapGL();
411
380
  const sourceId = props.sourceId || `esri-feature-${props.id}`;
412
- const [isMapLoaded, setIsMapLoaded] = useState(false);
381
+ const isMapLoaded = useMapLoaded(map);
413
382
  const serviceRef = useRef(null);
414
383
  // Keep stable refs for object props to avoid effect re-runs on every render
415
384
  const paintRef = useRef(props.paint);
416
385
  paintRef.current = props.paint;
417
386
  const layoutRef = useRef(props.layout);
418
387
  layoutRef.current = props.layout;
419
- // Wait for map to be loaded before creating service
420
- useEffect(() => {
421
- if (!map) return;
422
- const mapInstance = map.getMap?.();
423
- const mi = mapInstance;
424
- if (!mi || typeof mi.isStyleLoaded !== 'function') {
425
- return;
426
- }
427
- if (mi.isStyleLoaded()) {
428
- setIsMapLoaded(true);
429
- return;
430
- }
431
- const handleLoad = () => setIsMapLoaded(true);
432
- mi?.once?.('load', handleLoad);
433
- return () => {
434
- mi?.off?.('load', handleLoad);
435
- };
436
- }, [map]);
437
- // Create FeatureService, add layer, and clean up on unmount
438
388
  useEffect(() => {
439
389
  if (!map || !isMapLoaded) return;
440
390
  const mapInstance = map.getMap?.();
441
391
  if (!mapInstance) return;
442
- const mi = mapInstance;
443
- // Only include defined properties to avoid overriding defaults with undefined
392
+ const layerApi = mapInstance;
444
393
  const options = {
445
394
  url: props.url
446
395
  };
447
396
  if (props.where !== undefined) options.where = props.where;
448
397
  if (props.outFields !== undefined) options.outFields = props.outFields;
398
+ applyAuthOptions(options, props);
449
399
  const service = new FeatureService(sourceId, mapInstance, options);
450
400
  serviceRef.current = service;
451
- // Add GeoJSON layer
452
- 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)) {
453
402
  const layerType = props.type || 'fill';
454
403
  const defaultPaint = layerType === 'circle' ? {
455
404
  'circle-radius': 4,
@@ -469,87 +418,161 @@ function EsriFeatureLayer(props) {
469
418
  }
470
419
  };
471
420
  if (props.beforeId) {
472
- mi.addLayer(layerConfig, props.beforeId);
421
+ layerApi.addLayer(layerConfig, props.beforeId);
473
422
  } else {
474
- mi.addLayer(layerConfig);
423
+ layerApi.addLayer(layerConfig);
475
424
  }
476
425
  }
477
426
  return () => {
478
- if (mi.getStyle?.() && mi.getLayer?.(props.id)) {
479
- mi.removeLayer(props.id);
427
+ if (layerApi.getStyle?.() && layerApi.getLayer?.(props.id)) {
428
+ layerApi.removeLayer?.(props.id);
480
429
  }
481
430
  service.remove();
482
431
  serviceRef.current = null;
483
432
  };
484
- }, [map, isMapLoaded, sourceId, props.url, props.where, props.outFields, props.id, props.beforeId, props.visible, props.type]);
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]);
485
434
  return null;
486
435
  }
487
436
 
488
437
  /**
489
- * 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.
490
440
  */
491
- function useEsriMapboxLayer() {
441
+ function EsriPortalLayer(props) {
492
442
  const {
493
- current: mapRef
494
- } = useMap();
495
- 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;
496
536
  return {
497
537
  map,
498
538
  useDynamicMapService: options => useDynamicMapService({
499
539
  ...options,
500
- map
540
+ map: injectedMap
501
541
  }),
502
542
  useTiledMapService: options => useTiledMapService({
503
543
  ...options,
504
- map
544
+ map: injectedMap
505
545
  }),
506
546
  useImageService: options => useImageService({
507
547
  ...options,
508
- map
548
+ map: injectedMap
509
549
  }),
510
550
  useVectorTileService: options => useVectorTileService({
511
551
  ...options,
512
- map
552
+ map: injectedMap
513
553
  }),
514
554
  useFeatureService: options => useFeatureService({
515
555
  ...options,
516
- map
556
+ map: injectedMap
517
557
  })
518
558
  };
519
559
  }
520
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
+
521
569
  /**
522
570
  * Hook for using Esri services with MapLibre GL JS (via react-map-gl/maplibre)
523
571
  */
524
572
  function useEsriMaplibreLayer() {
525
- const {
526
- current: mapRef
527
- } = useMap$1();
528
- const map = mapRef?.getMap();
529
- return {
530
- map,
531
- useDynamicMapService: options => useDynamicMapService({
532
- ...options,
533
- map
534
- }),
535
- useTiledMapService: options => useTiledMapService({
536
- ...options,
537
- map
538
- }),
539
- useImageService: options => useImageService({
540
- ...options,
541
- map
542
- }),
543
- useVectorTileService: options => useVectorTileService({
544
- ...options,
545
- map
546
- }),
547
- useFeatureService: options => useFeatureService({
548
- ...options,
549
- map
550
- })
551
- };
573
+ const collection = useMap$1();
574
+ return createEsriLayerHooks(collection);
552
575
  }
553
576
 
554
- 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 };
555
578
  //# sourceMappingURL=react-map-gl.js.map