esri-gl 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.
Files changed (49) hide show
  1. package/README.md +375 -35
  2. package/dist/IdentifyImage-DWBF8K_a.js +4277 -0
  3. package/dist/IdentifyImage-DWBF8K_a.js.map +1 -0
  4. package/dist/index-CfCttkZx.d.ts +1535 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +4 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/index.umd.js +4318 -0
  9. package/dist/index.umd.js.map +1 -0
  10. package/dist/package.json +69 -0
  11. package/dist/react-map-gl.d.ts +102 -0
  12. package/dist/react-map-gl.js +555 -0
  13. package/dist/react-map-gl.js.map +1 -0
  14. package/dist/react.d.ts +116 -0
  15. package/dist/react.js +412 -0
  16. package/dist/react.js.map +1 -0
  17. package/dist/useFeatureService-B0ot0WFz.js +205 -0
  18. package/dist/useFeatureService-B0ot0WFz.js.map +1 -0
  19. package/dist/useFeatureService-BmqltfVc.d.ts +140 -0
  20. package/package.json +103 -47
  21. package/dist/esri-gl.esm.js +0 -2694
  22. package/dist/esri-gl.esm.js.map +0 -1
  23. package/dist/esri-gl.js +0 -2719
  24. package/dist/esri-gl.js.map +0 -1
  25. package/dist/esri-gl.min.js +0 -2
  26. package/dist/esri-gl.min.js.map +0 -1
  27. package/dist/types/Layers/BasemapLayer.d.ts +0 -30
  28. package/dist/types/Layers/DynamicMapLayer.d.ts +0 -75
  29. package/dist/types/Layers/Layer.d.ts +0 -67
  30. package/dist/types/Layers/RasterLayer.d.ts +0 -71
  31. package/dist/types/Services/DynamicMapService.d.ts +0 -35
  32. package/dist/types/Services/FeatureService.d.ts +0 -48
  33. package/dist/types/Services/ImageService.d.ts +0 -32
  34. package/dist/types/Services/MapService.d.ts +0 -30
  35. package/dist/types/Services/MapServiceTypes.d.ts +0 -87
  36. package/dist/types/Services/Service.d.ts +0 -118
  37. package/dist/types/Services/SimpleMapService.d.ts +0 -30
  38. package/dist/types/Services/TiledMapService.d.ts +0 -28
  39. package/dist/types/Services/VectorBasemapStyle.d.ts +0 -9
  40. package/dist/types/Services/VectorTileService.d.ts +0 -41
  41. package/dist/types/Tasks/Find.d.ts +0 -85
  42. package/dist/types/Tasks/IdentifyFeatures.d.ts +0 -91
  43. package/dist/types/Tasks/IdentifyImage.d.ts +0 -107
  44. package/dist/types/Tasks/Query.d.ts +0 -180
  45. package/dist/types/Tasks/Task.d.ts +0 -50
  46. package/dist/types/examples/EsriLeafletStyleAPI.d.ts +0 -9
  47. package/dist/types/main.d.ts +0 -14
  48. package/dist/types/types.d.ts +0 -88
  49. package/dist/types/utils.d.ts +0 -4
@@ -0,0 +1,4277 @@
1
+ import * as tilebelt from '@mapbox/tilebelt';
2
+ import tileDecode from 'arcgis-pbf-parser';
3
+
4
+ function cleanTrailingSlash(url) {
5
+ return url.replace(/\/$/, '');
6
+ }
7
+ /**
8
+ * Check if an error represents an AbortError (request was cancelled)
9
+ * Handles various error shapes from different browsers and environments
10
+ */
11
+ function isAbortError(error) {
12
+ if (!error) return false;
13
+ // Standard DOMException check
14
+ if (typeof DOMException !== 'undefined' && error instanceof DOMException) {
15
+ return error.name === 'AbortError';
16
+ }
17
+ // Error instance checks
18
+ if (error instanceof Error) {
19
+ if (error.name === 'AbortError') return true;
20
+ if (error.message?.toLowerCase().includes('abort')) return true;
21
+ }
22
+ // Duck typing for error-like objects
23
+ const errorObj = error;
24
+ if (errorObj.name === 'AbortError') return true;
25
+ if (errorObj.constructor?.name === 'AbortError') return true;
26
+ // String check
27
+ const stringified = String(error).toLowerCase();
28
+ return stringified.includes('abort');
29
+ }
30
+ async function getServiceDetails(url, fetchOptions = {}, token) {
31
+ const params = new URLSearchParams({
32
+ f: 'json'
33
+ });
34
+ if (token) {
35
+ params.append('token', token);
36
+ }
37
+ const response = await fetch(`${url}?${params.toString()}`, fetchOptions);
38
+ if (!response.ok) {
39
+ throw new Error(`Failed to fetch service details: HTTP ${response.status}`);
40
+ }
41
+ const data = await response.json();
42
+ if (data.error) {
43
+ throw new Error(data.error.message || 'Service returned an error');
44
+ }
45
+ return data;
46
+ }
47
+ const POWERED_BY_ESRI_ATTRIBUTION_STRING = 'Powered by <a href="https://www.esri.com">Esri</a>';
48
+ // This requires hooking into some undocumented properties
49
+ function updateAttribution(newAttribution, sourceId, map) {
50
+ // Accessing undocumented MapLibre/Mapbox internal properties
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ const mapWithControls = map;
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ const attributionController = mapWithControls._controls?.find(c => '_attribHTML' in c);
55
+ if (!attributionController) return;
56
+ const customAttribution = attributionController.options?.customAttribution;
57
+ if (typeof customAttribution === 'string') {
58
+ if (!customAttribution.includes(POWERED_BY_ESRI_ATTRIBUTION_STRING)) {
59
+ attributionController.options.customAttribution = `${customAttribution} | ${POWERED_BY_ESRI_ATTRIBUTION_STRING}`;
60
+ }
61
+ } else if (customAttribution === undefined) {
62
+ if (attributionController.options) {
63
+ attributionController.options.customAttribution = POWERED_BY_ESRI_ATTRIBUTION_STRING;
64
+ }
65
+ } else if (Array.isArray(customAttribution)) {
66
+ if (customAttribution.indexOf(POWERED_BY_ESRI_ATTRIBUTION_STRING) === -1) {
67
+ customAttribution.push(POWERED_BY_ESRI_ATTRIBUTION_STRING);
68
+ }
69
+ }
70
+ // Accessing undocumented map style properties
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ const mapStyle = map.style;
73
+ if (mapStyle?.sourceCaches?.[sourceId]?._source) {
74
+ mapStyle.sourceCaches[sourceId]._source.attribution = newAttribution;
75
+ } else if (mapStyle?._otherSourceCaches?.[sourceId]?._source) {
76
+ mapStyle._otherSourceCaches[sourceId]._source.attribution = newAttribution;
77
+ } else {
78
+ // Source may not be registered in style caches yet (async attribution fetch race).
79
+ // Silently skip — attribution will be set when the source is ready.
80
+ return;
81
+ }
82
+ // Call undocumented method to update attribution display
83
+ if (attributionController._updateAttributions) {
84
+ attributionController._updateAttributions();
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Base Service class for ArcGIS REST API services
90
+ * Similar to Esri Leaflet's Service class
91
+ */
92
+ class Service {
93
+ constructor(options) {
94
+ Object.defineProperty(this, "options", {
95
+ enumerable: true,
96
+ configurable: true,
97
+ writable: true,
98
+ value: void 0
99
+ });
100
+ Object.defineProperty(this, "_requestQueue", {
101
+ enumerable: true,
102
+ configurable: true,
103
+ writable: true,
104
+ value: []
105
+ });
106
+ Object.defineProperty(this, "_authenticating", {
107
+ enumerable: true,
108
+ configurable: true,
109
+ writable: true,
110
+ value: false
111
+ });
112
+ Object.defineProperty(this, "_serviceMetadata", {
113
+ enumerable: true,
114
+ configurable: true,
115
+ writable: true,
116
+ value: null
117
+ });
118
+ Object.defineProperty(this, "_map", {
119
+ enumerable: true,
120
+ configurable: true,
121
+ writable: true,
122
+ value: void 0
123
+ });
124
+ Object.defineProperty(this, "_eventListeners", {
125
+ enumerable: true,
126
+ configurable: true,
127
+ writable: true,
128
+ value: {}
129
+ });
130
+ if (!options.url) {
131
+ throw new Error('A url must be supplied as part of the service options.');
132
+ }
133
+ this.options = {
134
+ proxy: false,
135
+ useCors: true,
136
+ timeout: 0,
137
+ getAttributionFromService: true,
138
+ ...options,
139
+ url: cleanTrailingSlash(options.url)
140
+ };
141
+ }
142
+ /**
143
+ * Make a GET request
144
+ */
145
+ async get(path, params = {}) {
146
+ return this._request('GET', path, params);
147
+ }
148
+ /**
149
+ * Make a POST request
150
+ */
151
+ async post(path, params = {}) {
152
+ return this._request('POST', path, params);
153
+ }
154
+ /**
155
+ * Make a generic request
156
+ */
157
+ async request(path, params = {}) {
158
+ return this._request('GET', path, params);
159
+ }
160
+ /**
161
+ * Make a request with callback support (public for Tasks)
162
+ */
163
+ requestWithCallback(method, path, params, callback) {
164
+ if (callback) {
165
+ this._requestWithCallback(method, path, params, callback);
166
+ return;
167
+ }
168
+ return this._request(method, path, params);
169
+ }
170
+ /**
171
+ * Get service metadata
172
+ */
173
+ async metadata() {
174
+ if (this._serviceMetadata) {
175
+ return this._serviceMetadata;
176
+ }
177
+ try {
178
+ const response = await this._request('GET', '', {
179
+ f: 'json'
180
+ });
181
+ this._serviceMetadata = response;
182
+ return this._serviceMetadata;
183
+ } catch (error) {
184
+ const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
185
+ if (!isTestEnvironment) {
186
+ console.error('Error fetching service metadata:', error);
187
+ }
188
+ throw error;
189
+ }
190
+ }
191
+ /**
192
+ * Set authentication token
193
+ */
194
+ authenticate(token) {
195
+ this._authenticating = false;
196
+ this.options.token = token;
197
+ this._runQueue();
198
+ return this;
199
+ }
200
+ /**
201
+ * Get request timeout
202
+ */
203
+ getTimeout() {
204
+ return this.options.timeout;
205
+ }
206
+ /**
207
+ * Set request timeout
208
+ */
209
+ setTimeout(timeout) {
210
+ this.options.timeout = timeout;
211
+ return this;
212
+ }
213
+ /**
214
+ * Set attribution from service metadata
215
+ */
216
+ async setAttributionFromService() {
217
+ if (!this._map) {
218
+ return;
219
+ }
220
+ if (this._serviceMetadata) {
221
+ updateAttribution(this._serviceMetadata.copyrightText || '', 'service', this._map);
222
+ return;
223
+ }
224
+ try {
225
+ await this.metadata();
226
+ if (this._serviceMetadata && typeof this._serviceMetadata === 'object') {
227
+ const metadata = this._serviceMetadata;
228
+ updateAttribution(metadata?.copyrightText || '', 'service', this._map);
229
+ }
230
+ } catch (error) {
231
+ console.warn('Could not fetch service attribution:', error);
232
+ }
233
+ }
234
+ /**
235
+ * Add event listener
236
+ */
237
+ on(event, callback) {
238
+ if (!this._eventListeners[event]) {
239
+ this._eventListeners[event] = [];
240
+ }
241
+ this._eventListeners[event].push(callback);
242
+ return this;
243
+ }
244
+ /**
245
+ * Remove event listener
246
+ */
247
+ off(event, callback) {
248
+ const listeners = this._eventListeners[event];
249
+ if (listeners) {
250
+ const index = listeners.indexOf(callback);
251
+ if (index > -1) {
252
+ listeners.splice(index, 1);
253
+ }
254
+ }
255
+ return this;
256
+ }
257
+ /**
258
+ * Fire an event
259
+ */
260
+ fire(event, data) {
261
+ const listeners = this._eventListeners[event];
262
+ if (listeners) {
263
+ listeners.forEach(callback => callback(data));
264
+ }
265
+ return this;
266
+ }
267
+ // Private methods
268
+ _requestWithCallback(method, path, params, callback) {
269
+ this.fire('requeststart', {
270
+ url: this.options.url + path,
271
+ params,
272
+ method
273
+ });
274
+ const wrappedCallback = this._createServiceCallback(method, path, params, (error, response) => {
275
+ callback(error, response);
276
+ });
277
+ let finalParams = {
278
+ ...params
279
+ };
280
+ if (this.options.token) {
281
+ finalParams.token = this.options.token;
282
+ }
283
+ if (this.options.requestParams) {
284
+ finalParams = {
285
+ ...finalParams,
286
+ ...this.options.requestParams
287
+ };
288
+ }
289
+ if (this._authenticating) {
290
+ this._requestQueue.push([method, path, finalParams, wrappedCallback, this]);
291
+ } else {
292
+ this._makeRequest(method, path, finalParams, wrappedCallback);
293
+ }
294
+ }
295
+ async _request(method, path, params) {
296
+ this.fire('requeststart', {
297
+ url: this.options.url + path,
298
+ params,
299
+ method
300
+ });
301
+ return new Promise((resolve, reject) => {
302
+ const wrappedCallback = this._createServiceCallback(method, path, params, (error, response) => {
303
+ if (error) {
304
+ reject(error);
305
+ } else {
306
+ resolve(response);
307
+ }
308
+ });
309
+ let finalParams = {
310
+ ...params
311
+ };
312
+ if (this.options.token) {
313
+ finalParams.token = this.options.token;
314
+ }
315
+ if (this.options.requestParams) {
316
+ finalParams = {
317
+ ...finalParams,
318
+ ...this.options.requestParams
319
+ };
320
+ }
321
+ if (this._authenticating) {
322
+ this._requestQueue.push([method, path, finalParams, wrappedCallback, this]);
323
+ } else {
324
+ this._makeRequest(method, path, finalParams, wrappedCallback);
325
+ }
326
+ });
327
+ }
328
+ async _makeRequest(method, path, params, callback) {
329
+ const url = this.options.proxy ? `${this.options.proxy}?${this.options.url}${path}` : `${this.options.url}${path}`;
330
+ // Set up abort controller for timeout support
331
+ const controller = new AbortController();
332
+ let timeoutId;
333
+ if (this.options.timeout > 0) {
334
+ timeoutId = setTimeout(() => {
335
+ controller.abort();
336
+ }, this.options.timeout);
337
+ }
338
+ try {
339
+ let response;
340
+ const fetchOptions = {
341
+ signal: controller.signal
342
+ };
343
+ // Add API key header if present
344
+ if (this.options.apiKey) {
345
+ fetchOptions.headers = {
346
+ 'X-Esri-Authorization': `Bearer ${this.options.apiKey}`
347
+ };
348
+ }
349
+ if (method === 'POST') {
350
+ const formData = new FormData();
351
+ Object.keys(params).forEach(key => {
352
+ const value = params[key];
353
+ if (value !== undefined && value !== null) {
354
+ if (typeof value === 'object') {
355
+ formData.append(key, JSON.stringify(value));
356
+ } else {
357
+ formData.append(key, value.toString());
358
+ }
359
+ }
360
+ });
361
+ response = await fetch(url, {
362
+ ...fetchOptions,
363
+ method: 'POST',
364
+ body: formData
365
+ });
366
+ } else {
367
+ const searchParams = new URLSearchParams();
368
+ Object.keys(params).forEach(key => {
369
+ const value = params[key];
370
+ if (value !== undefined && value !== null) {
371
+ if (Array.isArray(value)) {
372
+ searchParams.append(key, value.join(','));
373
+ } else if (typeof value === 'object') {
374
+ searchParams.append(key, JSON.stringify(value));
375
+ } else {
376
+ searchParams.append(key, value.toString());
377
+ }
378
+ }
379
+ });
380
+ const fullUrl = `${url}?${searchParams.toString()}`;
381
+ response = await fetch(fullUrl, fetchOptions);
382
+ }
383
+ if (!response.ok) {
384
+ const error = new Error(`HTTP error! status: ${response.status}`);
385
+ error.code = response.status;
386
+ throw error;
387
+ }
388
+ const data = await response.json();
389
+ // Check for AGOL JSON-level errors (HTTP 200 with error body)
390
+ if (data && typeof data === 'object' && data.error) {
391
+ const err = new Error(data.error.message || 'ArcGIS service error');
392
+ err.code = data.error.code;
393
+ err.details = data.error.details;
394
+ callback(err);
395
+ return;
396
+ }
397
+ callback(undefined, data);
398
+ } catch (error) {
399
+ // Provide clearer error message for timeout
400
+ if (error instanceof Error && error.name === 'AbortError' && this.options.timeout > 0) {
401
+ const timeoutError = new Error(`Request timed out after ${this.options.timeout}ms`);
402
+ callback(timeoutError);
403
+ } else {
404
+ callback(error);
405
+ }
406
+ } finally {
407
+ if (timeoutId !== undefined) {
408
+ clearTimeout(timeoutId);
409
+ }
410
+ }
411
+ }
412
+ _createServiceCallback(method, path, params, callback) {
413
+ return (error, response) => {
414
+ // Check if error has authentication-related status codes
415
+ const errorWithCode = error;
416
+ if (error && (errorWithCode.code === 499 || errorWithCode.code === 498)) {
417
+ this._authenticating = true;
418
+ this._requestQueue.push([method, path, params, callback, this]);
419
+ // Fire event for users to handle re-authentication
420
+ this.fire('authenticationrequired', {
421
+ authenticate: token => this.authenticate(token)
422
+ });
423
+ // Add authenticate method to error for callback handling
424
+ const authError = error;
425
+ authError.authenticate = token => this.authenticate(token);
426
+ return;
427
+ }
428
+ if (error) {
429
+ this.fire('requesterror', {
430
+ url: this.options.url + path,
431
+ params,
432
+ message: error.message,
433
+ code: errorWithCode.code,
434
+ method
435
+ });
436
+ } else {
437
+ this.fire('requestsuccess', {
438
+ url: this.options.url + path,
439
+ params,
440
+ response,
441
+ method
442
+ });
443
+ }
444
+ this.fire('requestend', {
445
+ url: this.options.url + path,
446
+ params,
447
+ method
448
+ });
449
+ callback(error, response);
450
+ };
451
+ }
452
+ _runQueue() {
453
+ for (let i = this._requestQueue.length - 1; i >= 0; i--) {
454
+ const request = this._requestQueue[i];
455
+ const [method, path, params, callback] = request;
456
+ if (callback) {
457
+ this._makeRequest(method, path, params, callback);
458
+ }
459
+ }
460
+ this._requestQueue = [];
461
+ }
462
+ }
463
+
464
+ class DynamicMapService {
465
+ constructor(sourceId, map, esriServiceOptions, rasterSrcOptions) {
466
+ Object.defineProperty(this, "_sourceId", {
467
+ enumerable: true,
468
+ configurable: true,
469
+ writable: true,
470
+ value: void 0
471
+ });
472
+ Object.defineProperty(this, "_map", {
473
+ enumerable: true,
474
+ configurable: true,
475
+ writable: true,
476
+ value: void 0
477
+ });
478
+ Object.defineProperty(this, "_defaultEsriOptions", {
479
+ enumerable: true,
480
+ configurable: true,
481
+ writable: true,
482
+ value: void 0
483
+ });
484
+ Object.defineProperty(this, "_serviceMetadata", {
485
+ enumerable: true,
486
+ configurable: true,
487
+ writable: true,
488
+ value: null
489
+ });
490
+ Object.defineProperty(this, "_pendingUpdate", {
491
+ enumerable: true,
492
+ configurable: true,
493
+ writable: true,
494
+ value: null
495
+ });
496
+ Object.defineProperty(this, "_lastUpdateTime", {
497
+ enumerable: true,
498
+ configurable: true,
499
+ writable: true,
500
+ value: 0
501
+ });
502
+ Object.defineProperty(this, "_updateDelay", {
503
+ enumerable: true,
504
+ configurable: true,
505
+ writable: true,
506
+ value: 50
507
+ }); // ms debounce to avoid rapid successive aborts
508
+ Object.defineProperty(this, "rasterSrcOptions", {
509
+ enumerable: true,
510
+ configurable: true,
511
+ writable: true,
512
+ value: void 0
513
+ });
514
+ Object.defineProperty(this, "esriServiceOptions", {
515
+ enumerable: true,
516
+ configurable: true,
517
+ writable: true,
518
+ value: void 0
519
+ });
520
+ // Transaction-like updates
521
+ Object.defineProperty(this, "_pendingUpdates", {
522
+ enumerable: true,
523
+ configurable: true,
524
+ writable: true,
525
+ value: null
526
+ });
527
+ if (!esriServiceOptions.url) {
528
+ throw new Error('A url must be supplied as part of the esriServiceOptions object.');
529
+ }
530
+ esriServiceOptions.url = cleanTrailingSlash(esriServiceOptions.url);
531
+ this._sourceId = sourceId;
532
+ this._map = map;
533
+ this._defaultEsriOptions = {
534
+ layers: false,
535
+ layerDefs: false,
536
+ dynamicLayers: false,
537
+ format: 'png24',
538
+ dpi: 96,
539
+ transparent: true,
540
+ getAttributionFromService: true
541
+ };
542
+ this.rasterSrcOptions = rasterSrcOptions;
543
+ this.esriServiceOptions = esriServiceOptions;
544
+ this._createSource();
545
+ if (this.options.getAttributionFromService) {
546
+ this.setAttributionFromService().catch(() => {
547
+ // Silently handle attribution fetch errors to prevent unhandled rejections
548
+ });
549
+ }
550
+ }
551
+ get options() {
552
+ return {
553
+ ...this._defaultEsriOptions,
554
+ ...this.esriServiceOptions
555
+ };
556
+ }
557
+ get _layersStr() {
558
+ let lyrs = this.options.layers;
559
+ if (!lyrs) return false;
560
+ if (!Array.isArray(lyrs)) lyrs = [lyrs];
561
+ return `show:${lyrs.join(',')}`;
562
+ }
563
+ get _layerDefs() {
564
+ if (this.options.layerDefs !== false) return JSON.stringify(this.options.layerDefs);
565
+ return false;
566
+ }
567
+ get _time() {
568
+ if (!this.options.to) return false;
569
+ let from = this.options.from;
570
+ let to = this.options.to;
571
+ if (from instanceof Date) from = from.valueOf();
572
+ if (to instanceof Date) to = to.valueOf();
573
+ return `${from},${to}`;
574
+ }
575
+ // ArcGIS Dynamic Layer styling parameter (JSON string)
576
+ get _dynamicLayers() {
577
+ const dl = this.options.dynamicLayers;
578
+ if (!dl) return false;
579
+ try {
580
+ const normalized = dl.map(l => {
581
+ const {
582
+ visible,
583
+ ...rest
584
+ } = l;
585
+ const withSource = {
586
+ ...rest,
587
+ // ensure required source exists
588
+ source: l.source ?? {
589
+ type: 'mapLayer',
590
+ mapLayerId: l.id
591
+ }
592
+ };
593
+ // Convert client-friendly 'visible' to ArcGIS 'visibility'
594
+ if (typeof visible === 'boolean') {
595
+ withSource.visibility = visible;
596
+ }
597
+ return withSource;
598
+ });
599
+ const result = JSON.stringify(normalized);
600
+ return result;
601
+ } catch {
602
+ return false;
603
+ }
604
+ }
605
+ get _source() {
606
+ const tileSize = this.rasterSrcOptions?.tileSize ?? 256;
607
+ // These are the bare minimum parameters
608
+ const params = new URLSearchParams({
609
+ bboxSR: '3857',
610
+ imageSR: '3857',
611
+ format: this.options.format,
612
+ layers: this._layersStr || '',
613
+ transparent: this.options.transparent.toString(),
614
+ size: `${tileSize},${tileSize}`,
615
+ f: 'image'
616
+ });
617
+ // These are optional params
618
+ if (this._time) params.append('time', this._time);
619
+ if (this._layerDefs) params.append('layerDefs', this._layerDefs);
620
+ if (this._dynamicLayers) params.append('dynamicLayers', this._dynamicLayers);
621
+ this._appendTokenIfExists(params);
622
+ const tileUrl = `${this.options.url}/export?bbox={bbox-epsg-3857}&${params.toString()}`;
623
+ return {
624
+ type: 'raster',
625
+ tiles: [tileUrl],
626
+ tileSize,
627
+ ...this.rasterSrcOptions
628
+ };
629
+ }
630
+ _createSource() {
631
+ // Check if source already exists before adding
632
+ if (!this._map.getSource(this._sourceId)) {
633
+ this._map.addSource(this._sourceId, this._source);
634
+ }
635
+ }
636
+ // This requires hooking into some undocumented methods
637
+ _updateSource() {
638
+ // Simple debounce: collapse multiple rapid calls (e.g., visibility + labels in same tick)
639
+ const now = performance.now();
640
+ if (now - this._lastUpdateTime < this._updateDelay) {
641
+ if (this._pendingUpdate) cancelAnimationFrame(this._pendingUpdate);
642
+ this._pendingUpdate = requestAnimationFrame(() => this._updateSourceInternal());
643
+ return;
644
+ }
645
+ this._lastUpdateTime = now;
646
+ this._updateSourceInternal();
647
+ }
648
+ _updateSourceInternal() {
649
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
650
+ const src = this._map.getSource(this._sourceId);
651
+ if (!src) return;
652
+ try {
653
+ // Ensure src.tiles exists before accessing it
654
+ if (src.tiles && Array.isArray(src.tiles) && this._source.tiles && this._source.tiles.length > 0) {
655
+ src.tiles[0] = this._source.tiles[0];
656
+ }
657
+ src._options = this._source;
658
+ if (src.setTiles) {
659
+ // New MapboxGL >= 2.13.0
660
+ // setTiles may return a promise - handle rejections to prevent console errors
661
+ const result = src.setTiles(this._source.tiles);
662
+ if (result && typeof result.catch === 'function') {
663
+ result.catch(() => {
664
+ // Silently ignore - setTiles rejections are often abort errors during rapid updates
665
+ // The outer try/catch will handle any synchronous errors
666
+ });
667
+ }
668
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
669
+ } else if (this._map.style.sourceCaches) {
670
+ // Old MapboxGL and MaplibreGL
671
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
672
+ this._map.style.sourceCaches[this._sourceId].clearTiles();
673
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
674
+ this._map.style.sourceCaches[this._sourceId].update(this._map.transform);
675
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
676
+ } else if (this._map.style._otherSourceCaches) {
677
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
678
+ this._map.style._otherSourceCaches[this._sourceId].clearTiles();
679
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
680
+ this._map.style._otherSourceCaches[this._sourceId].update(this._map.transform);
681
+ }
682
+ } catch (error) {
683
+ // Silently ignore aborted tile operations - MapLibre will retry
684
+ if (isAbortError(error)) {
685
+ return;
686
+ }
687
+ // Swallow occasional transient errors that can happen during style reloads
688
+ if (error && error.message?.includes('Source') && error.message?.includes('not found')) {
689
+ return;
690
+ }
691
+ throw error;
692
+ }
693
+ }
694
+ setLayerDefs(obj) {
695
+ this.esriServiceOptions.layerDefs = obj;
696
+ this._updateSource();
697
+ }
698
+ /**
699
+ * Replace the entire dynamicLayers array. Server applies these drawing rules.
700
+ * Note: dynamicLayers overrides default drawing for listed sublayers.
701
+ */
702
+ setDynamicLayers(layers) {
703
+ this.esriServiceOptions.dynamicLayers = layers || false;
704
+ this._updateSource();
705
+ }
706
+ /** Helper to ensure all visible layers are included when using dynamicLayers */
707
+ _ensureAllVisibleLayers(dynamicLayers) {
708
+ const visibleLayerIds = this._getVisibleLayerIds();
709
+ const existingIds = new Set(dynamicLayers.map(dl => dl.id));
710
+ // Add entries for visible layers that aren't already in dynamicLayers
711
+ const additional = visibleLayerIds.filter(id => !existingIds.has(id)).map(id => ({
712
+ id,
713
+ visible: true
714
+ }));
715
+ return [...dynamicLayers, ...additional];
716
+ }
717
+ /** Get the list of currently visible layer IDs */
718
+ _getVisibleLayerIds() {
719
+ const lyrs = this.options.layers;
720
+ if (!lyrs) return [];
721
+ if (!Array.isArray(lyrs)) return [lyrs];
722
+ return lyrs;
723
+ }
724
+ /** Merge/update a sublayer's drawingInfo with provided fields. */
725
+ setLayerDrawingInfo(layerId, drawingInfo) {
726
+ const current = this.esriServiceOptions.dynamicLayers || [];
727
+ const next = Array.isArray(current) ? [...current] : [];
728
+ const idx = next.findIndex(l => l.id === layerId);
729
+ if (idx >= 0) {
730
+ next[idx] = {
731
+ ...next[idx],
732
+ drawingInfo: {
733
+ ...next[idx].drawingInfo,
734
+ ...drawingInfo
735
+ }
736
+ };
737
+ } else {
738
+ next.push({
739
+ id: layerId,
740
+ drawingInfo
741
+ });
742
+ }
743
+ // Ensure all visible layers are included
744
+ this.esriServiceOptions.dynamicLayers = this._ensureAllVisibleLayers(next);
745
+ this._updateSource();
746
+ }
747
+ /** Convenience to set a renderer on a sublayer */
748
+ setLayerRenderer(layerId, renderer) {
749
+ this.setLayerDrawingInfo(layerId, {
750
+ renderer
751
+ });
752
+ }
753
+ /** Show/hide a sublayer via dynamicLayers */
754
+ setLayerVisibility(layerId, visible) {
755
+ const current = this.esriServiceOptions.dynamicLayers || [];
756
+ const next = Array.isArray(current) ? [...current] : [];
757
+ const idx = next.findIndex(l => l.id === layerId);
758
+ if (idx >= 0) {
759
+ next[idx] = {
760
+ ...next[idx],
761
+ visible
762
+ };
763
+ } else {
764
+ next.push({
765
+ id: layerId,
766
+ visible
767
+ });
768
+ }
769
+ // Ensure all visible layers are included
770
+ this.esriServiceOptions.dynamicLayers = this._ensureAllVisibleLayers(next);
771
+ this._updateSource();
772
+ }
773
+ /** Set a definitionExpression for a sublayer, applied server-side */
774
+ setLayerDefinition(layerId, definitionExpression) {
775
+ const current = this.esriServiceOptions.dynamicLayers || [];
776
+ const next = Array.isArray(current) ? [...current] : [];
777
+ const idx = next.findIndex(l => l.id === layerId);
778
+ if (idx >= 0) {
779
+ next[idx] = {
780
+ ...next[idx],
781
+ definitionExpression
782
+ };
783
+ } else {
784
+ next.push({
785
+ id: layerId,
786
+ definitionExpression
787
+ });
788
+ }
789
+ // Ensure all visible layers are included
790
+ this.esriServiceOptions.dynamicLayers = this._ensureAllVisibleLayers(next);
791
+ this._updateSource();
792
+ }
793
+ // Build a SQL where clause and apply as definitionExpression
794
+ setLayerFilter(layerId, filter) {
795
+ const where = this._buildWhere(filter);
796
+ if (where) this.setLayerDefinition(layerId, where);
797
+ }
798
+ _appendTokenIfExists(params) {
799
+ const token = this.esriServiceOptions.token;
800
+ if (token) {
801
+ params.append('token', token);
802
+ }
803
+ }
804
+ _escapeValue(val) {
805
+ if (val === null) return 'NULL';
806
+ if (val instanceof Date) return `${val.valueOf()}`; // epoch ms for time-enabled services
807
+ if (typeof val === 'number') return `${val}`;
808
+ if (typeof val === 'boolean') return val ? '1' : '0';
809
+ const s = String(val).replace(/'/g, "''");
810
+ return `'${s}'`;
811
+ }
812
+ // Very small, pragmatic builder that covers common cases in types.ts
813
+ _isGroupFilter(f) {
814
+ return typeof f !== 'string' && 'op' in f && (f.op === 'AND' || f.op === 'OR') && 'filters' in f && Array.isArray(f.filters);
815
+ }
816
+ _isBetweenFilter(f) {
817
+ return typeof f !== 'string' && 'op' in f && f.op === 'BETWEEN' && 'field' in f && typeof f.field === 'string' && 'from' in f && f.from !== undefined && 'to' in f && f.to !== undefined;
818
+ }
819
+ _isInFilter(f) {
820
+ return typeof f !== 'string' && 'op' in f && f.op === 'IN' && 'field' in f && typeof f.field === 'string' && 'values' in f && Array.isArray(f.values);
821
+ }
822
+ _isNullFilter(f) {
823
+ return typeof f !== 'string' && 'op' in f && (f.op === 'IS NULL' || f.op === 'IS NOT NULL') && 'field' in f && typeof f.field === 'string';
824
+ }
825
+ _isComparisonFilter(f) {
826
+ return typeof f !== 'string' && 'field' in f && typeof f.field === 'string' && 'op' in f && typeof f.op === 'string' && 'value' in f && f.value !== undefined;
827
+ }
828
+ _buildWhere(filter) {
829
+ if (!filter) return undefined;
830
+ if (typeof filter === 'string') return filter.trim();
831
+ if (this._isBetweenFilter(filter)) {
832
+ return `${filter.field} BETWEEN ${this._escapeValue(filter.from)} AND ${this._escapeValue(filter.to)}`;
833
+ }
834
+ if (this._isInFilter(filter)) {
835
+ const vals = filter.values.map(v => this._escapeValue(v)).join(', ');
836
+ return `${filter.field} IN (${vals})`;
837
+ }
838
+ if (this._isNullFilter(filter)) {
839
+ return `${filter.field} ${filter.op}`;
840
+ }
841
+ if (this._isGroupFilter(filter)) {
842
+ const built = filter.filters.map(f => this._buildWhere(f)).filter(s => Boolean(s));
843
+ if (!built.length) return undefined;
844
+ if (built.length === 1) return built[0];
845
+ return `(${built.join(` ${filter.op} `)})`;
846
+ }
847
+ if (this._isComparisonFilter(filter)) {
848
+ return `${filter.field} ${filter.op} ${this._escapeValue(filter.value)}`;
849
+ }
850
+ return undefined;
851
+ }
852
+ setLayers(arr) {
853
+ this.esriServiceOptions.layers = arr;
854
+ this._updateSource();
855
+ }
856
+ setDate(from, to) {
857
+ this.esriServiceOptions.from = from;
858
+ this.esriServiceOptions.to = to;
859
+ this._updateSource();
860
+ }
861
+ setToken(token) {
862
+ this.esriServiceOptions.token = token ?? undefined;
863
+ this._updateSource();
864
+ }
865
+ setAttributionFromService() {
866
+ if (this._serviceMetadata) {
867
+ updateAttribution(this._serviceMetadata.copyrightText || '', this._sourceId, this._map);
868
+ return Promise.resolve();
869
+ } else {
870
+ return this.getMetadata().then(() => {
871
+ updateAttribution(this._serviceMetadata?.copyrightText || '', this._sourceId, this._map);
872
+ });
873
+ }
874
+ }
875
+ getMetadata() {
876
+ if (this._serviceMetadata !== null) return Promise.resolve(this._serviceMetadata);
877
+ return new Promise((resolve, reject) => {
878
+ getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions, this.esriServiceOptions.token).then(data => {
879
+ this._serviceMetadata = data;
880
+ resolve(this._serviceMetadata);
881
+ }).catch(err => reject(err));
882
+ });
883
+ }
884
+ get _layersStrIdentify() {
885
+ const layersStr = this._layersStr;
886
+ return layersStr ? layersStr.replace('show', 'visible') : false;
887
+ }
888
+ async identify(lnglat, returnGeometry = false) {
889
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
890
+ const canvas = this._map.getCanvas();
891
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
892
+ const bounds = this._map.getBounds().toArray();
893
+ const params = new URLSearchParams({
894
+ sr: '4326',
895
+ geometryType: 'esriGeometryPoint',
896
+ geometry: JSON.stringify({
897
+ x: lnglat.lng,
898
+ y: lnglat.lat,
899
+ spatialReference: {
900
+ wkid: 4326
901
+ }
902
+ }),
903
+ tolerance: '3',
904
+ returnGeometry: returnGeometry.toString(),
905
+ imageDisplay: `${canvas.width},${canvas.height},96`,
906
+ mapExtent: `${bounds[0][0]},${bounds[0][1]},${bounds[1][0]},${bounds[1][1]}`,
907
+ layers: this._layersStrIdentify || '',
908
+ f: 'json'
909
+ });
910
+ if (this._layerDefs) params.append('layerDefs', this._layerDefs);
911
+ if (this._dynamicLayers) params.append('dynamicLayers', this._dynamicLayers);
912
+ if (this._time) params.append('time', this._time);
913
+ this._appendTokenIfExists(params);
914
+ const response = await fetch(`${this.esriServiceOptions.url}/identify?${params.toString()}`, this.esriServiceOptions.fetchOptions);
915
+ if (!response.ok) {
916
+ throw new Error(`Identify request failed: HTTP ${response.status}`);
917
+ }
918
+ const data = await response.json();
919
+ if (data.error) {
920
+ throw new Error(`Identify request failed: ${data.error.message}`);
921
+ }
922
+ return data;
923
+ }
924
+ // ========================================
925
+ // Advanced Features
926
+ // ========================================
927
+ /** Set labeling configuration for a sublayer */
928
+ setLayerLabels(layerId, labelingInfo) {
929
+ const current = this.esriServiceOptions.dynamicLayers || [];
930
+ const next = Array.isArray(current) ? [...current] : [];
931
+ const idx = next.findIndex(l => l.id === layerId);
932
+ if (idx >= 0) {
933
+ next[idx] = {
934
+ ...next[idx],
935
+ drawingInfo: {
936
+ ...next[idx].drawingInfo,
937
+ labelingInfo: [labelingInfo]
938
+ }
939
+ };
940
+ } else {
941
+ next.push({
942
+ id: layerId,
943
+ drawingInfo: {
944
+ labelingInfo: [labelingInfo]
945
+ }
946
+ });
947
+ }
948
+ // Ensure all visible layers are included
949
+ this.esriServiceOptions.dynamicLayers = this._ensureAllVisibleLayers(next);
950
+ this._updateSource();
951
+ }
952
+ /** Toggle label visibility for a sublayer */
953
+ setLayerLabelsVisible(layerId, visible) {
954
+ if (visible) {
955
+ // If enabling labels but no labeling info exists, set a default
956
+ const current = this.esriServiceOptions.dynamicLayers || [];
957
+ const layer = Array.isArray(current) ? current.find(l => l.id === layerId) : null;
958
+ if (!layer?.drawingInfo?.labelingInfo) {
959
+ // Set a basic label configuration
960
+ this.setLayerLabels(layerId, {
961
+ labelExpression: '[OBJECTID]',
962
+ // Default to object ID
963
+ symbol: {
964
+ type: 'esriTS',
965
+ color: [0, 0, 0, 255],
966
+ font: {
967
+ family: 'Arial',
968
+ size: 8
969
+ }
970
+ }
971
+ });
972
+ }
973
+ } else {
974
+ // Remove labeling info to disable labels
975
+ const current = this.esriServiceOptions.dynamicLayers || [];
976
+ const next = Array.isArray(current) ? [...current] : [];
977
+ const idx = next.findIndex(l => l.id === layerId);
978
+ if (idx >= 0) {
979
+ const drawingInfo = {
980
+ ...next[idx].drawingInfo
981
+ };
982
+ delete drawingInfo.labelingInfo;
983
+ next[idx] = {
984
+ ...next[idx],
985
+ drawingInfo
986
+ };
987
+ // Ensure all visible layers are included
988
+ this.esriServiceOptions.dynamicLayers = this._ensureAllVisibleLayers(next);
989
+ this._updateSource();
990
+ }
991
+ }
992
+ }
993
+ /** Set time options for a time-enabled sublayer */
994
+ setLayerTimeOptions(layerId, timeOptions) {
995
+ const current = this.esriServiceOptions.dynamicLayers || [];
996
+ const next = Array.isArray(current) ? [...current] : [];
997
+ const idx = next.findIndex(l => l.id === layerId);
998
+ if (idx >= 0) {
999
+ next[idx] = {
1000
+ ...next[idx],
1001
+ layerTimeOptions: timeOptions
1002
+ };
1003
+ } else {
1004
+ next.push({
1005
+ id: layerId,
1006
+ layerTimeOptions: timeOptions
1007
+ });
1008
+ }
1009
+ // Ensure all visible layers are included
1010
+ this.esriServiceOptions.dynamicLayers = this._ensureAllVisibleLayers(next);
1011
+ this._updateSource();
1012
+ }
1013
+ /** Animate through time periods for time-enabled layers */
1014
+ async animateTime(options) {
1015
+ const {
1016
+ from,
1017
+ to,
1018
+ intervalMs,
1019
+ loop = false,
1020
+ onFrame,
1021
+ onComplete
1022
+ } = options;
1023
+ const totalDuration = to.getTime() - from.getTime();
1024
+ const steps = Math.ceil(totalDuration / intervalMs);
1025
+ return new Promise(resolve => {
1026
+ let currentStep = 0;
1027
+ const animate = () => {
1028
+ if (currentStep >= steps && !loop) {
1029
+ onComplete?.();
1030
+ resolve();
1031
+ return;
1032
+ }
1033
+ const progress = currentStep % steps / steps;
1034
+ const currentTime = new Date(from.getTime() + progress * totalDuration);
1035
+ // Update service time extent
1036
+ this.esriServiceOptions.from = currentTime;
1037
+ this.esriServiceOptions.to = currentTime;
1038
+ this._updateSource();
1039
+ onFrame?.(currentTime, progress);
1040
+ currentStep++;
1041
+ setTimeout(animate, intervalMs);
1042
+ };
1043
+ animate();
1044
+ });
1045
+ }
1046
+ /** Get statistics for a sublayer */
1047
+ async getLayerStatistics(layerId, statisticFields, options = {}) {
1048
+ const queryUrl = `${this.esriServiceOptions.url}/${layerId}/query`;
1049
+ const params = new URLSearchParams({
1050
+ f: 'json',
1051
+ where: options.where || '1=1',
1052
+ outStatistics: JSON.stringify(statisticFields),
1053
+ returnGeometry: 'false'
1054
+ });
1055
+ if (options.groupByFieldsForStatistics) {
1056
+ params.append('groupByFieldsForStatistics', options.groupByFieldsForStatistics);
1057
+ }
1058
+ this._appendTokenIfExists(params);
1059
+ const response = await fetch(`${queryUrl}?${params.toString()}`);
1060
+ if (!response.ok) {
1061
+ throw new Error(`Statistics query failed: HTTP ${response.status}`);
1062
+ }
1063
+ const data = await response.json();
1064
+ if (data.error) {
1065
+ throw new Error(`Statistics query failed: ${data.error.message}`);
1066
+ }
1067
+ return data.features || [];
1068
+ }
1069
+ /** Query features from a specific sublayer */
1070
+ async queryLayerFeatures(layerId, options = {}) {
1071
+ const queryUrl = `${this.esriServiceOptions.url}/${layerId}/query`;
1072
+ const params = new URLSearchParams({
1073
+ f: 'json',
1074
+ where: options.where || '1=1',
1075
+ returnGeometry: options.returnGeometry !== false ? 'true' : 'false',
1076
+ outFields: Array.isArray(options.outFields) ? options.outFields.join(',') : options.outFields || '*'
1077
+ });
1078
+ if (options.geometry) {
1079
+ params.append('geometry', JSON.stringify(options.geometry));
1080
+ params.append('geometryType', options.geometryType || 'esriGeometryEnvelope');
1081
+ params.append('spatialRel', options.spatialRel || 'esriSpatialRelIntersects');
1082
+ }
1083
+ if (options.orderByFields) {
1084
+ params.append('orderByFields', options.orderByFields);
1085
+ }
1086
+ if (options.resultOffset) {
1087
+ params.append('resultOffset', options.resultOffset.toString());
1088
+ }
1089
+ if (options.resultRecordCount) {
1090
+ params.append('resultRecordCount', options.resultRecordCount.toString());
1091
+ }
1092
+ if (options.returnCountOnly) {
1093
+ params.append('returnCountOnly', 'true');
1094
+ }
1095
+ if (options.returnIdsOnly) {
1096
+ params.append('returnIdsOnly', 'true');
1097
+ }
1098
+ this._appendTokenIfExists(params);
1099
+ const response = await fetch(`${queryUrl}?${params.toString()}`);
1100
+ if (!response.ok) {
1101
+ throw new Error(`Layer query failed: HTTP ${response.status}`);
1102
+ }
1103
+ const data = await response.json();
1104
+ if (data.error) {
1105
+ throw new Error(`Layer query failed: ${data.error.message}`);
1106
+ }
1107
+ return data;
1108
+ }
1109
+ /** Export high-resolution map image */
1110
+ async exportMapImage(options) {
1111
+ const exportUrl = `${this.esriServiceOptions.url}/export`;
1112
+ const params = new URLSearchParams({
1113
+ f: 'image',
1114
+ bbox: options.bbox.join(','),
1115
+ size: options.size.join(','),
1116
+ format: options.format || 'png24',
1117
+ transparent: options.transparent !== false ? 'true' : 'false',
1118
+ dpi: (options.dpi || 96).toString(),
1119
+ bboxSR: (options.bboxSR || 3857).toString(),
1120
+ imageSR: (options.imageSR || 3857).toString()
1121
+ });
1122
+ if (options.layerDefs) {
1123
+ params.append('layerDefs', JSON.stringify(options.layerDefs));
1124
+ }
1125
+ if (options.dynamicLayers) {
1126
+ const normalized = this._ensureAllVisibleLayers(options.dynamicLayers);
1127
+ params.append('dynamicLayers', JSON.stringify(normalized));
1128
+ }
1129
+ if (options.gdbVersion) {
1130
+ params.append('gdbVersion', options.gdbVersion);
1131
+ }
1132
+ if (options.historicMoment) {
1133
+ params.append('historicMoment', options.historicMoment.toString());
1134
+ }
1135
+ this._appendTokenIfExists(params);
1136
+ const response = await fetch(`${exportUrl}?${params.toString()}`);
1137
+ if (!response.ok) {
1138
+ throw new Error(`Export failed: ${response.statusText}`);
1139
+ }
1140
+ return response.blob();
1141
+ }
1142
+ /** Generate legend information for layers */
1143
+ async generateLegend(layerIds) {
1144
+ const legendUrl = `${this.esriServiceOptions.url}/legend`;
1145
+ const params = new URLSearchParams({
1146
+ f: 'json'
1147
+ });
1148
+ if (layerIds?.length) {
1149
+ params.append('layers', layerIds.join(','));
1150
+ }
1151
+ this._appendTokenIfExists(params);
1152
+ const response = await fetch(`${legendUrl}?${params.toString()}`);
1153
+ if (!response.ok) {
1154
+ throw new Error(`Legend generation failed: HTTP ${response.status}`);
1155
+ }
1156
+ const data = await response.json();
1157
+ if (data.error) {
1158
+ throw new Error(`Legend generation failed: ${data.error.message}`);
1159
+ }
1160
+ return data.layers || [];
1161
+ }
1162
+ /** Get detailed information about a specific layer */
1163
+ async getLayerInfo(layerId) {
1164
+ const layerUrl = `${this.esriServiceOptions.url}/${layerId}`;
1165
+ const params = new URLSearchParams({
1166
+ f: 'json'
1167
+ });
1168
+ this._appendTokenIfExists(params);
1169
+ const response = await fetch(`${layerUrl}?${params.toString()}`);
1170
+ if (!response.ok) {
1171
+ throw new Error(`Layer info request failed: HTTP ${response.status}`);
1172
+ }
1173
+ const data = await response.json();
1174
+ if (data.error) {
1175
+ throw new Error(`Layer info request failed: ${data.error.message}`);
1176
+ }
1177
+ return data;
1178
+ }
1179
+ /** Get field information for a layer */
1180
+ async getLayerFields(layerId) {
1181
+ const layerInfo = await this.getLayerInfo(layerId);
1182
+ return layerInfo.fields || [];
1183
+ }
1184
+ /** Get spatial extent of a layer */
1185
+ async getLayerExtent(layerId) {
1186
+ const layerInfo = await this.getLayerInfo(layerId);
1187
+ if (!layerInfo.extent) {
1188
+ throw new Error(`No extent available for layer ${layerId}`);
1189
+ }
1190
+ return layerInfo.extent;
1191
+ }
1192
+ /** Discover all layers in the service */
1193
+ async discoverLayers() {
1194
+ const serviceUrl = this.esriServiceOptions.url;
1195
+ const params = new URLSearchParams({
1196
+ f: 'json'
1197
+ });
1198
+ this._appendTokenIfExists(params);
1199
+ const response = await fetch(`${serviceUrl}?${params.toString()}`);
1200
+ if (!response.ok) {
1201
+ throw new Error(`Service discovery failed: HTTP ${response.status}`);
1202
+ }
1203
+ const data = await response.json();
1204
+ if (data.error) {
1205
+ throw new Error(`Service discovery failed: ${data.error.message}`);
1206
+ }
1207
+ return data.layers || [];
1208
+ }
1209
+ /** Apply multiple layer operations in a single update */
1210
+ setBulkLayerProperties(operations) {
1211
+ const current = this.esriServiceOptions.dynamicLayers || [];
1212
+ const next = Array.isArray(current) ? [...current] : [];
1213
+ // Process all operations
1214
+ for (const op of operations) {
1215
+ const idx = next.findIndex(l => l.id === op.layerId);
1216
+ const layer = idx >= 0 ? {
1217
+ ...next[idx]
1218
+ } : {
1219
+ id: op.layerId
1220
+ };
1221
+ switch (op.operation) {
1222
+ case 'visibility':
1223
+ layer.visible = op.value;
1224
+ break;
1225
+ case 'renderer':
1226
+ layer.drawingInfo = {
1227
+ ...layer.drawingInfo,
1228
+ renderer: op.value
1229
+ };
1230
+ break;
1231
+ case 'definition':
1232
+ layer.definitionExpression = op.value;
1233
+ break;
1234
+ case 'filter':
1235
+ {
1236
+ const where = this._buildWhere(op.value);
1237
+ if (where) layer.definitionExpression = where;
1238
+ break;
1239
+ }
1240
+ case 'labels':
1241
+ layer.drawingInfo = {
1242
+ ...layer.drawingInfo,
1243
+ labelingInfo: op.value
1244
+ };
1245
+ break;
1246
+ case 'time':
1247
+ layer.layerTimeOptions = op.value;
1248
+ break;
1249
+ }
1250
+ if (idx >= 0) {
1251
+ next[idx] = layer;
1252
+ } else {
1253
+ next.push(layer);
1254
+ }
1255
+ }
1256
+ // Ensure all visible layers are included
1257
+ this.esriServiceOptions.dynamicLayers = this._ensureAllVisibleLayers(next);
1258
+ this._updateSource();
1259
+ }
1260
+ /** Begin a batch update transaction */
1261
+ beginUpdate() {
1262
+ const current = this.esriServiceOptions.dynamicLayers || [];
1263
+ this._pendingUpdates = Array.isArray(current) ? [...current] : [];
1264
+ }
1265
+ /** Commit all pending updates */
1266
+ commitUpdate() {
1267
+ if (this._pendingUpdates) {
1268
+ this.esriServiceOptions.dynamicLayers = this._ensureAllVisibleLayers(this._pendingUpdates);
1269
+ this._pendingUpdates = null;
1270
+ this._updateSource();
1271
+ }
1272
+ }
1273
+ /** Rollback pending updates */
1274
+ rollbackUpdate() {
1275
+ this._pendingUpdates = null;
1276
+ }
1277
+ /** Check if currently in a transaction */
1278
+ get isInTransaction() {
1279
+ return this._pendingUpdates !== null;
1280
+ }
1281
+ update() {
1282
+ this._updateSource();
1283
+ }
1284
+ remove() {
1285
+ const map = this._map;
1286
+ if (!map || typeof map.removeSource !== 'function') {
1287
+ return;
1288
+ }
1289
+ try {
1290
+ // Guard against map whose style has already been destroyed
1291
+ if (!map.style) return;
1292
+ const mapWithStyle = map;
1293
+ const mapLayerApi = map;
1294
+ const mapSourceApi = map;
1295
+ if (typeof mapWithStyle.getStyle === 'function') {
1296
+ const style = mapWithStyle.getStyle();
1297
+ const layers = style?.layers || [];
1298
+ layers.forEach(layer => {
1299
+ if (layer.source !== this._sourceId) return;
1300
+ if (typeof mapLayerApi.getLayer !== 'function' || typeof mapLayerApi.removeLayer !== 'function') {
1301
+ return;
1302
+ }
1303
+ let hasLayer = false;
1304
+ try {
1305
+ hasLayer = Boolean(mapLayerApi.getLayer(layer.id));
1306
+ } catch {
1307
+ hasLayer = false;
1308
+ }
1309
+ if (!hasLayer) return;
1310
+ try {
1311
+ mapLayerApi.removeLayer(layer.id);
1312
+ } catch (error) {
1313
+ console.warn(`Failed to remove layer ${layer.id} for source ${this._sourceId}:`, error);
1314
+ }
1315
+ });
1316
+ }
1317
+ if (typeof mapSourceApi.getSource === 'function') {
1318
+ let hasSource = false;
1319
+ try {
1320
+ hasSource = Boolean(mapSourceApi.getSource(this._sourceId));
1321
+ } catch {
1322
+ hasSource = false;
1323
+ }
1324
+ if (hasSource) {
1325
+ try {
1326
+ map.removeSource(this._sourceId);
1327
+ } catch (error) {
1328
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1329
+ }
1330
+ }
1331
+ }
1332
+ } catch (error) {
1333
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1334
+ }
1335
+ }
1336
+ }
1337
+
1338
+ class TiledMapService {
1339
+ constructor(sourceId, map, esriServiceOptions, rasterSrcOptions) {
1340
+ Object.defineProperty(this, "_sourceId", {
1341
+ enumerable: true,
1342
+ configurable: true,
1343
+ writable: true,
1344
+ value: void 0
1345
+ });
1346
+ Object.defineProperty(this, "_map", {
1347
+ enumerable: true,
1348
+ configurable: true,
1349
+ writable: true,
1350
+ value: void 0
1351
+ });
1352
+ Object.defineProperty(this, "_serviceMetadata", {
1353
+ enumerable: true,
1354
+ configurable: true,
1355
+ writable: true,
1356
+ value: null
1357
+ });
1358
+ Object.defineProperty(this, "rasterSrcOptions", {
1359
+ enumerable: true,
1360
+ configurable: true,
1361
+ writable: true,
1362
+ value: void 0
1363
+ });
1364
+ Object.defineProperty(this, "esriServiceOptions", {
1365
+ enumerable: true,
1366
+ configurable: true,
1367
+ writable: true,
1368
+ value: void 0
1369
+ });
1370
+ if (!esriServiceOptions.url) {
1371
+ throw new Error('A url must be supplied as part of the esriServiceOptions object.');
1372
+ }
1373
+ esriServiceOptions.url = cleanTrailingSlash(esriServiceOptions.url);
1374
+ this._sourceId = sourceId;
1375
+ this._map = map;
1376
+ this.rasterSrcOptions = rasterSrcOptions;
1377
+ this.esriServiceOptions = esriServiceOptions;
1378
+ this._createSource();
1379
+ if (esriServiceOptions.getAttributionFromService) {
1380
+ this.setAttributionFromService().catch(() => {
1381
+ // Silently handle attribution fetch errors to prevent unhandled rejections
1382
+ });
1383
+ }
1384
+ }
1385
+ get _source() {
1386
+ return {
1387
+ ...this.rasterSrcOptions,
1388
+ type: 'raster',
1389
+ tiles: [this.esriServiceOptions.token ? `${this.esriServiceOptions.url}/tile/{z}/{y}/{x}?token=${this.esriServiceOptions.token}` : `${this.esriServiceOptions.url}/tile/{z}/{y}/{x}`],
1390
+ tileSize: this.rasterSrcOptions?.tileSize || 256
1391
+ };
1392
+ }
1393
+ _createSource() {
1394
+ // Check if source already exists before adding
1395
+ if (!this._map.getSource(this._sourceId)) {
1396
+ this._map.addSource(this._sourceId, this._source);
1397
+ }
1398
+ }
1399
+ setToken(token) {
1400
+ this.esriServiceOptions.token = token ?? undefined;
1401
+ // Tiled sources need recreation to pick up new URL
1402
+ const src = this._map.getSource(this._sourceId);
1403
+ if (src && src.setTiles) {
1404
+ src.setTiles(this._source.tiles);
1405
+ }
1406
+ }
1407
+ setAttributionFromService() {
1408
+ if (this._serviceMetadata) {
1409
+ updateAttribution(this._serviceMetadata.copyrightText || '', this._sourceId, this._map);
1410
+ return Promise.resolve();
1411
+ } else {
1412
+ return this.getMetadata().then(() => {
1413
+ updateAttribution(this._serviceMetadata?.copyrightText || '', this._sourceId, this._map);
1414
+ });
1415
+ }
1416
+ }
1417
+ getMetadata() {
1418
+ if (this._serviceMetadata !== null) return Promise.resolve(this._serviceMetadata);
1419
+ return new Promise((resolve, reject) => {
1420
+ getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions, this.esriServiceOptions.token).then(data => {
1421
+ this._serviceMetadata = data;
1422
+ resolve(data);
1423
+ }).catch(err => reject(err));
1424
+ });
1425
+ }
1426
+ update() {
1427
+ // Tiled sources use static tile URLs and don't support hot-updates.
1428
+ // This is a no-op to satisfy the common service interface.
1429
+ }
1430
+ remove() {
1431
+ // Guard against disposed or invalid map
1432
+ if (!this._map || typeof this._map.removeSource !== 'function') {
1433
+ return;
1434
+ }
1435
+ try {
1436
+ // Guard against map whose style has already been destroyed
1437
+ if (!this._map.style) return;
1438
+ // First, remove any layers that are using this source
1439
+ const mapWithStyle = this._map;
1440
+ if (mapWithStyle.getStyle && typeof mapWithStyle.getLayer === 'function') {
1441
+ const style = mapWithStyle.getStyle();
1442
+ const layers = style?.layers || [];
1443
+ const getLayer = mapWithStyle.getLayer;
1444
+ layers.forEach(layer => {
1445
+ if (layer.source === this._sourceId) {
1446
+ try {
1447
+ if (getLayer(layer.id)) {
1448
+ this._map.removeLayer(layer.id);
1449
+ }
1450
+ } catch {
1451
+ // Layer may already be removed
1452
+ }
1453
+ }
1454
+ });
1455
+ }
1456
+ // Then check if source exists before trying to remove it
1457
+ if (typeof this._map.getSource === 'function') {
1458
+ const source = this._map.getSource(this._sourceId);
1459
+ if (source) {
1460
+ this._map.removeSource(this._sourceId);
1461
+ }
1462
+ }
1463
+ } catch (error) {
1464
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1465
+ }
1466
+ }
1467
+ }
1468
+
1469
+ class ImageService {
1470
+ constructor(sourceId, map, esriServiceOptions, rasterSrcOptions) {
1471
+ Object.defineProperty(this, "_sourceId", {
1472
+ enumerable: true,
1473
+ configurable: true,
1474
+ writable: true,
1475
+ value: void 0
1476
+ });
1477
+ Object.defineProperty(this, "_map", {
1478
+ enumerable: true,
1479
+ configurable: true,
1480
+ writable: true,
1481
+ value: void 0
1482
+ });
1483
+ Object.defineProperty(this, "_defaultEsriOptions", {
1484
+ enumerable: true,
1485
+ configurable: true,
1486
+ writable: true,
1487
+ value: void 0
1488
+ });
1489
+ Object.defineProperty(this, "_serviceMetadata", {
1490
+ enumerable: true,
1491
+ configurable: true,
1492
+ writable: true,
1493
+ value: null
1494
+ });
1495
+ Object.defineProperty(this, "rasterSrcOptions", {
1496
+ enumerable: true,
1497
+ configurable: true,
1498
+ writable: true,
1499
+ value: void 0
1500
+ });
1501
+ Object.defineProperty(this, "esriServiceOptions", {
1502
+ enumerable: true,
1503
+ configurable: true,
1504
+ writable: true,
1505
+ value: void 0
1506
+ });
1507
+ if (!esriServiceOptions.url) {
1508
+ throw new Error('A url must be supplied as part of the esriServiceOptions object.');
1509
+ }
1510
+ esriServiceOptions.url = cleanTrailingSlash(esriServiceOptions.url);
1511
+ this._sourceId = sourceId;
1512
+ this._map = map;
1513
+ this._defaultEsriOptions = {
1514
+ layers: false,
1515
+ layerDefs: false,
1516
+ dynamicLayers: false,
1517
+ format: 'jpgpng',
1518
+ dpi: 96,
1519
+ transparent: true,
1520
+ getAttributionFromService: true,
1521
+ time: false
1522
+ };
1523
+ this.rasterSrcOptions = rasterSrcOptions;
1524
+ this.esriServiceOptions = esriServiceOptions;
1525
+ this._createSource();
1526
+ if (this.options.getAttributionFromService) {
1527
+ this.setAttributionFromService().catch(() => {
1528
+ // Silently handle attribution fetch errors to prevent unhandled rejections
1529
+ });
1530
+ }
1531
+ }
1532
+ get options() {
1533
+ return {
1534
+ ...this._defaultEsriOptions,
1535
+ ...this.esriServiceOptions
1536
+ };
1537
+ }
1538
+ get _time() {
1539
+ if (!this.options.to) return false;
1540
+ let from = this.options.from;
1541
+ let to = this.options.to;
1542
+ if (from instanceof Date) from = from.valueOf();
1543
+ if (to instanceof Date) to = to.valueOf();
1544
+ return `${from},${to}`;
1545
+ }
1546
+ get _source() {
1547
+ const tileSize = this.rasterSrcOptions?.tileSize ?? 256;
1548
+ // These are the bare minimum parameters
1549
+ const params = new URLSearchParams({
1550
+ bboxSR: '3857',
1551
+ imageSR: '3857',
1552
+ format: this.options.format,
1553
+ size: `${tileSize},${tileSize}`,
1554
+ f: 'image'
1555
+ });
1556
+ // These are optional params
1557
+ if (this._time) params.append('time', this._time);
1558
+ if (this.options.mosaicRule) params.append('mosaicRule', JSON.stringify(this.options.mosaicRule));
1559
+ if (this.options.renderingRule) params.append('renderingRule', JSON.stringify(this.options.renderingRule));
1560
+ if (this.esriServiceOptions.token) {
1561
+ params.append('token', this.esriServiceOptions.token);
1562
+ }
1563
+ return {
1564
+ type: 'raster',
1565
+ tiles: [`${this.options.url}/exportImage?bbox={bbox-epsg-3857}&${params.toString()}`],
1566
+ tileSize,
1567
+ ...this.rasterSrcOptions
1568
+ };
1569
+ }
1570
+ _createSource() {
1571
+ // Check if source already exists before adding
1572
+ if (!this._map.getSource(this._sourceId)) {
1573
+ this._map.addSource(this._sourceId, this._source);
1574
+ }
1575
+ }
1576
+ // This requires hooking into some undocumented methods
1577
+ _updateSource() {
1578
+ const src = this._map.getSource(this._sourceId);
1579
+ if (!src) {
1580
+ // Source not yet added to map, nothing to update
1581
+ return;
1582
+ }
1583
+ src.tiles[0] = this._source.tiles[0];
1584
+ src._options = this._source;
1585
+ if (src.setTiles) {
1586
+ // New MapboxGL >= 2.13.0
1587
+ src.setTiles(this._source.tiles);
1588
+ } else if (this._map.style.sourceCaches) {
1589
+ // Old MapboxGL and MaplibreGL
1590
+ this._map.style.sourceCaches[this._sourceId].clearTiles();
1591
+ this._map.style.sourceCaches[this._sourceId].update(this._map.transform);
1592
+ } else if (this._map.style._otherSourceCaches) {
1593
+ this._map.style._otherSourceCaches[this._sourceId].clearTiles();
1594
+ this._map.style._otherSourceCaches[this._sourceId].update(this._map.transform);
1595
+ }
1596
+ }
1597
+ setDate(from, to) {
1598
+ this.esriServiceOptions.from = from;
1599
+ this.esriServiceOptions.to = to;
1600
+ this._updateSource();
1601
+ }
1602
+ setRenderingRule(rule) {
1603
+ this.esriServiceOptions.renderingRule = rule;
1604
+ this._updateSource();
1605
+ }
1606
+ setMosaicRule(rule) {
1607
+ this.esriServiceOptions.mosaicRule = rule;
1608
+ this._updateSource();
1609
+ }
1610
+ setToken(token) {
1611
+ this.esriServiceOptions.token = token ?? undefined;
1612
+ this._updateSource();
1613
+ }
1614
+ setAttributionFromService() {
1615
+ if (this._serviceMetadata) {
1616
+ updateAttribution(this._serviceMetadata.copyrightText || '', this._sourceId, this._map);
1617
+ return Promise.resolve();
1618
+ } else {
1619
+ return this.getMetadata().then(() => {
1620
+ updateAttribution(this._serviceMetadata?.copyrightText || '', this._sourceId, this._map);
1621
+ });
1622
+ }
1623
+ }
1624
+ getMetadata() {
1625
+ if (this._serviceMetadata !== null) return Promise.resolve(this._serviceMetadata);
1626
+ return new Promise((resolve, reject) => {
1627
+ getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions, this.esriServiceOptions.token).then(data => {
1628
+ this._serviceMetadata = data;
1629
+ resolve(this._serviceMetadata);
1630
+ }).catch(err => reject(err));
1631
+ });
1632
+ }
1633
+ async identify(lnglat, returnGeometry = false) {
1634
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1635
+ const canvas = this._map.getCanvas();
1636
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1637
+ const bounds = this._map.getBounds().toArray();
1638
+ const params = new URLSearchParams({
1639
+ sr: '4326',
1640
+ geometryType: 'esriGeometryPoint',
1641
+ geometry: JSON.stringify({
1642
+ x: lnglat.lng,
1643
+ y: lnglat.lat,
1644
+ spatialReference: {
1645
+ wkid: 4326
1646
+ }
1647
+ }),
1648
+ tolerance: '3',
1649
+ returnGeometry: returnGeometry.toString(),
1650
+ imageDisplay: `${canvas.width},${canvas.height},96`,
1651
+ mapExtent: `${bounds[0][0]},${bounds[0][1]},${bounds[1][0]},${bounds[1][1]}`,
1652
+ f: 'json'
1653
+ });
1654
+ if (this._time) params.append('time', this._time);
1655
+ if (this.esriServiceOptions.token) {
1656
+ params.append('token', this.esriServiceOptions.token);
1657
+ }
1658
+ const response = await fetch(`${this.esriServiceOptions.url}/identify?${params.toString()}`, this.esriServiceOptions.fetchOptions);
1659
+ if (!response.ok) {
1660
+ throw new Error(`Identify request failed: HTTP ${response.status}`);
1661
+ }
1662
+ const data = await response.json();
1663
+ if (data.error) {
1664
+ throw new Error(`Identify request failed: ${data.error.message}`);
1665
+ }
1666
+ return data;
1667
+ }
1668
+ update() {
1669
+ this._updateSource();
1670
+ }
1671
+ remove() {
1672
+ if (this._map && typeof this._map.removeSource === 'function') {
1673
+ try {
1674
+ // Guard against map whose style has already been destroyed
1675
+ const mapWithStyle = this._map;
1676
+ if (!mapWithStyle.style) return;
1677
+ // First, remove any layers that are using this source
1678
+ if (mapWithStyle.getStyle) {
1679
+ const style = mapWithStyle.getStyle();
1680
+ const layers = style?.layers || [];
1681
+ layers.forEach(layer => {
1682
+ if (layer.source === this._sourceId && this._map.getLayer(layer.id)) {
1683
+ this._map.removeLayer(layer.id);
1684
+ }
1685
+ });
1686
+ }
1687
+ // Then check if source exists before trying to remove it
1688
+ if (this._map.getSource && this._map.getSource(this._sourceId)) {
1689
+ this._map.removeSource(this._sourceId);
1690
+ }
1691
+ } catch (error) {
1692
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
1693
+ }
1694
+ }
1695
+ }
1696
+ }
1697
+
1698
+ /**
1699
+ * VectorBasemapStyle supports both modern slash form (arcgis/streets) and legacy colon form (ArcGIS:Streets).
1700
+ * The URL always uses the slash form expected by the Basemap Styles Service.
1701
+ *
1702
+ * Note: MapLibre GL v5+ may show vector tile parsing warnings with some Esri tiles.
1703
+ * These warnings are expected and do not affect map rendering functionality.
1704
+ */
1705
+ class VectorBasemapStyle {
1706
+ // Overload signature: (styleName, apiKeyString) or (styleName, options)
1707
+ constructor(styleName, auth) {
1708
+ Object.defineProperty(this, "styleName", {
1709
+ enumerable: true,
1710
+ configurable: true,
1711
+ writable: true,
1712
+ value: void 0
1713
+ }); // original user-supplied value (for display / debugging)
1714
+ Object.defineProperty(this, "_canonical", {
1715
+ enumerable: true,
1716
+ configurable: true,
1717
+ writable: true,
1718
+ value: void 0
1719
+ }); // normalized slash form used in URL
1720
+ Object.defineProperty(this, "_apiKey", {
1721
+ enumerable: true,
1722
+ configurable: true,
1723
+ writable: true,
1724
+ value: void 0
1725
+ });
1726
+ Object.defineProperty(this, "_token", {
1727
+ enumerable: true,
1728
+ configurable: true,
1729
+ writable: true,
1730
+ value: void 0
1731
+ });
1732
+ Object.defineProperty(this, "_version", {
1733
+ enumerable: true,
1734
+ configurable: true,
1735
+ writable: true,
1736
+ value: void 0
1737
+ });
1738
+ Object.defineProperty(this, "_host", {
1739
+ enumerable: true,
1740
+ configurable: true,
1741
+ writable: true,
1742
+ value: void 0
1743
+ });
1744
+ Object.defineProperty(this, "_format", {
1745
+ enumerable: true,
1746
+ configurable: true,
1747
+ writable: true,
1748
+ value: void 0
1749
+ });
1750
+ Object.defineProperty(this, "_language", {
1751
+ enumerable: true,
1752
+ configurable: true,
1753
+ writable: true,
1754
+ value: void 0
1755
+ });
1756
+ Object.defineProperty(this, "_worldview", {
1757
+ enumerable: true,
1758
+ configurable: true,
1759
+ writable: true,
1760
+ value: void 0
1761
+ });
1762
+ Object.defineProperty(this, "_itemId", {
1763
+ enumerable: true,
1764
+ configurable: true,
1765
+ writable: true,
1766
+ value: void 0
1767
+ });
1768
+ let opts;
1769
+ if (typeof auth === 'string') {
1770
+ opts = {
1771
+ apiKey: auth
1772
+ };
1773
+ } else {
1774
+ opts = auth || {};
1775
+ }
1776
+ const provided = styleName && styleName.trim() ? styleName : opts.itemId ? '' : 'arcgis/streets';
1777
+ this.styleName = provided;
1778
+ this._canonical = VectorBasemapStyle._toCanonical(provided);
1779
+ this._apiKey = opts.apiKey;
1780
+ this._token = opts.token;
1781
+ this._language = opts.language;
1782
+ this._worldview = opts.worldview;
1783
+ this._itemId = opts.itemId;
1784
+ // Infer version: token => v2, else apiKey => v1
1785
+ this._version = opts.version || (this._token ? 'v2' : 'v1');
1786
+ // Determine host based on version if not overridden
1787
+ this._host = opts.host || (this._version === 'v2' ? 'https://basemapstyles-api.arcgis.com' : 'https://basemaps-api.arcgis.com');
1788
+ // For v2 we default to 'style' so the response is directly consumable by map.setStyle
1789
+ this._format = opts.format || 'style';
1790
+ if (!this._apiKey && !this._token) {
1791
+ // Keep legacy error string for backwards compatibility with existing tests / consumers
1792
+ throw new Error('An Esri API Key must be supplied to consume vector basemap styles');
1793
+ }
1794
+ }
1795
+ get styleUrl() {
1796
+ // Custom style via portal item ID
1797
+ if (this._itemId) {
1798
+ const portalHost = this._host.includes('basemap') ? 'https://www.arcgis.com' : this._host;
1799
+ const params = new URLSearchParams();
1800
+ if (this._token) params.set('token', this._token);
1801
+ if (this._apiKey) params.set('token', this._apiKey);
1802
+ const qs = params.toString();
1803
+ return `${portalHost}/sharing/rest/content/items/${this._itemId}/resources/styles/root.json${qs ? '?' + qs : ''}`;
1804
+ }
1805
+ // v1 pattern: https://basemaps-api.arcgis.com/arcgis/rest/services/styles/{id}?type=style&apiKey=KEY
1806
+ // v2 pattern: https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/{id}?f=json&token=TOKEN&echoToken=false
1807
+ if (this._version === 'v2') {
1808
+ const params = new URLSearchParams();
1809
+ params.set('f', this._format); // usually 'style'
1810
+ if (this._token) params.set('token', this._token);
1811
+ if (this._language) params.set('language', this._language);
1812
+ if (this._worldview) params.set('worldview', this._worldview);
1813
+ return `${this._host}/arcgis/rest/services/styles/v2/styles/${this._canonical}?${params.toString()}`;
1814
+ }
1815
+ // v1 (apiKey)
1816
+ const params = new URLSearchParams();
1817
+ params.set('type', 'style');
1818
+ if (this._apiKey) params.set('apiKey', this._apiKey);
1819
+ if (this._language) params.set('language', this._language);
1820
+ if (this._worldview) params.set('worldview', this._worldview);
1821
+ return `${this._host}/arcgis/rest/services/styles/v1/styles/${this._canonical}?${params.toString()}`;
1822
+ }
1823
+ setStyle(styleName) {
1824
+ if (!styleName) return;
1825
+ this.styleName = styleName;
1826
+ this._canonical = VectorBasemapStyle._toCanonical(styleName);
1827
+ }
1828
+ update() {}
1829
+ remove() {}
1830
+ /**
1831
+ * Simple wrapper to apply a basemap style to a MapLibre/Mapbox map.
1832
+ *
1833
+ * @param map - MapLibre GL or Mapbox GL map instance with setStyle method
1834
+ * @param styleName - Esri style name (e.g., 'arcgis/streets', 'arcgis/topographic')
1835
+ * @param auth - Authentication options (API key or token)
1836
+ *
1837
+ * @example
1838
+ * // Using API key
1839
+ * VectorBasemapStyle.applyStyle(map, 'arcgis/streets', { apiKey: 'your-api-key' });
1840
+ *
1841
+ * // Using token
1842
+ * VectorBasemapStyle.applyStyle(map, 'arcgis/topographic', { token: 'your-token' });
1843
+ */
1844
+ static applyStyle(map, styleName, auth) {
1845
+ const vectorStyle = new VectorBasemapStyle(styleName, auth);
1846
+ map.setStyle(vectorStyle.styleUrl);
1847
+ }
1848
+ static _toCanonical(name) {
1849
+ if (!name) return 'arcgis/streets';
1850
+ // Already slash form
1851
+ if (name.includes('/')) return name;
1852
+ const lower = name.toLowerCase();
1853
+ if (this.COLON_TO_SLASH[lower]) return this.COLON_TO_SLASH[lower];
1854
+ // If it looks like ArcGIS:Token (legacy) attempt smart conversion
1855
+ if (/^arcgis:/i.test(name)) {
1856
+ const token = name.split(':')[1];
1857
+ if (token) {
1858
+ const slug = token.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/_/g, '-').toLowerCase();
1859
+ return `arcgis/${slug}`;
1860
+ }
1861
+ }
1862
+ return name; // custom enterprise style: leave untouched (caller controls format)
1863
+ }
1864
+ }
1865
+ // Mapping from legacy colon form to canonical slash form
1866
+ Object.defineProperty(VectorBasemapStyle, "COLON_TO_SLASH", {
1867
+ enumerable: true,
1868
+ configurable: true,
1869
+ writable: true,
1870
+ value: {
1871
+ 'arcgis:streets': 'arcgis/streets',
1872
+ 'arcgis:topographic': 'arcgis/topographic',
1873
+ 'arcgis:navigation': 'arcgis/navigation',
1874
+ 'arcgis:streetsrelief': 'arcgis/streets-relief',
1875
+ 'arcgis:darkgray': 'arcgis/dark-gray',
1876
+ 'arcgis:lightgray': 'arcgis/light-gray',
1877
+ 'arcgis:oceans': 'arcgis/oceans',
1878
+ 'arcgis:imagery': 'arcgis/imagery',
1879
+ 'arcgis:streetsnight': 'arcgis/streets-night'
1880
+ }
1881
+ });
1882
+
1883
+ class VectorTileService {
1884
+ constructor(sourceId, map, esriServiceOptions, vectorSrcOptions) {
1885
+ Object.defineProperty(this, "_sourceId", {
1886
+ enumerable: true,
1887
+ configurable: true,
1888
+ writable: true,
1889
+ value: void 0
1890
+ });
1891
+ Object.defineProperty(this, "_map", {
1892
+ enumerable: true,
1893
+ configurable: true,
1894
+ writable: true,
1895
+ value: void 0
1896
+ });
1897
+ Object.defineProperty(this, "_defaultEsriOptions", {
1898
+ enumerable: true,
1899
+ configurable: true,
1900
+ writable: true,
1901
+ value: void 0
1902
+ });
1903
+ Object.defineProperty(this, "_serviceMetadata", {
1904
+ enumerable: true,
1905
+ configurable: true,
1906
+ writable: true,
1907
+ value: null
1908
+ });
1909
+ Object.defineProperty(this, "_defaultStyleData", {
1910
+ enumerable: true,
1911
+ configurable: true,
1912
+ writable: true,
1913
+ value: null
1914
+ });
1915
+ Object.defineProperty(this, "vectorSrcOptions", {
1916
+ enumerable: true,
1917
+ configurable: true,
1918
+ writable: true,
1919
+ value: void 0
1920
+ });
1921
+ Object.defineProperty(this, "esriServiceOptions", {
1922
+ enumerable: true,
1923
+ configurable: true,
1924
+ writable: true,
1925
+ value: void 0
1926
+ });
1927
+ if (!esriServiceOptions.url) {
1928
+ throw new Error('A url must be supplied as part of the esriServiceOptions object.');
1929
+ }
1930
+ esriServiceOptions.url = cleanTrailingSlash(esriServiceOptions.url);
1931
+ this._sourceId = sourceId;
1932
+ this._map = map;
1933
+ this._defaultEsriOptions = {
1934
+ useDefaultStyle: true
1935
+ };
1936
+ this.vectorSrcOptions = vectorSrcOptions;
1937
+ this.esriServiceOptions = esriServiceOptions;
1938
+ this._createSource();
1939
+ }
1940
+ get options() {
1941
+ return {
1942
+ ...this._defaultEsriOptions,
1943
+ ...this.esriServiceOptions
1944
+ };
1945
+ }
1946
+ get _tileUrl() {
1947
+ if (this._serviceMetadata === null) return '/tile/{z}/{y}/{x}.pbf';
1948
+ return this._serviceMetadata.tiles?.[0] || '/tile/{z}/{y}/{x}.pbf';
1949
+ }
1950
+ get _source() {
1951
+ return {
1952
+ ...(this.vectorSrcOptions || {}),
1953
+ type: 'vector',
1954
+ tiles: [`${this.options.url}${this._tileUrl}`]
1955
+ };
1956
+ }
1957
+ _createSource() {
1958
+ // Check if source already exists before adding
1959
+ if (!this._map.getSource(this._sourceId)) {
1960
+ this._map.addSource(this._sourceId, this._source);
1961
+ }
1962
+ }
1963
+ _mapToLocalSource(style) {
1964
+ return {
1965
+ type: style.type,
1966
+ source: this._sourceId,
1967
+ 'source-layer': style['source-layer'],
1968
+ layout: style.layout,
1969
+ paint: style.paint
1970
+ };
1971
+ }
1972
+ get defaultStyle() {
1973
+ // Consumers should only call after getStyle resolves
1974
+ return this._mapToLocalSource(this._defaultStyleData);
1975
+ }
1976
+ get _styleUrl() {
1977
+ // Return a RELATIVE path which will be prefixed with options.url during fetch
1978
+ // ArcGIS VectorTileServer typically exposes defaultStyles like 'resources/styles/root.json'
1979
+ if (this._serviceMetadata === null) return 'resources/styles/root.json';
1980
+ return this._serviceMetadata.defaultStyles || 'resources/styles/root.json';
1981
+ }
1982
+ getStyle() {
1983
+ // Always resolve the mapped defaultStyle so the 'source' equals this._sourceId
1984
+ if (this._defaultStyleData !== null) return Promise.resolve(this.defaultStyle);
1985
+ return new Promise((resolve, reject) => {
1986
+ const load = () => this._retrieveStyle().then(() => resolve(this.defaultStyle)).catch(error => reject(error));
1987
+ if (this._serviceMetadata === null) {
1988
+ this.getMetadata().then(() => load()).catch(error => reject(error));
1989
+ } else {
1990
+ load();
1991
+ }
1992
+ });
1993
+ }
1994
+ _retrieveStyle() {
1995
+ return new Promise((resolve, reject) => {
1996
+ fetch(`${this.options.url}/${this._styleUrl}`, this.esriServiceOptions.fetchOptions).then(response => {
1997
+ if (!response.ok) throw new Error(`Failed to fetch style: ${response.status}`);
1998
+ return response.json();
1999
+ }).then(data => {
2000
+ if (!data || !Array.isArray(data.layers) || data.layers.length === 0) {
2001
+ throw new Error('VectorTile style document is missing layers.');
2002
+ }
2003
+ // Use the first layer as a simple default style for the demo
2004
+ this._defaultStyleData = data.layers[0];
2005
+ resolve();
2006
+ }).catch(error => reject(error));
2007
+ });
2008
+ }
2009
+ getMetadata() {
2010
+ if (this._serviceMetadata !== null) return Promise.resolve(this._serviceMetadata);
2011
+ return new Promise((resolve, reject) => {
2012
+ getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions).then(data => {
2013
+ this._serviceMetadata = data;
2014
+ resolve(this._serviceMetadata);
2015
+ }).catch(err => reject(err));
2016
+ });
2017
+ }
2018
+ update() {
2019
+ // Vector tile services don't need dynamic updates like dynamic services
2020
+ }
2021
+ remove() {
2022
+ const map = this._map;
2023
+ if (!map || typeof map.removeSource !== 'function') {
2024
+ return;
2025
+ }
2026
+ try {
2027
+ // Guard against map whose style has already been destroyed
2028
+ if (!map.style) return;
2029
+ const mapWithStyle = map;
2030
+ const mapLayerApi = map;
2031
+ const mapSourceApi = map;
2032
+ if (typeof mapWithStyle.getStyle === 'function') {
2033
+ const style = mapWithStyle.getStyle();
2034
+ const layers = style?.layers || [];
2035
+ layers.forEach(layer => {
2036
+ if (layer.source !== this._sourceId) return;
2037
+ if (typeof mapLayerApi.getLayer !== 'function' || typeof mapLayerApi.removeLayer !== 'function') {
2038
+ return;
2039
+ }
2040
+ let hasLayer = false;
2041
+ try {
2042
+ hasLayer = Boolean(mapLayerApi.getLayer(layer.id));
2043
+ } catch {
2044
+ hasLayer = false;
2045
+ }
2046
+ if (!hasLayer) return;
2047
+ try {
2048
+ mapLayerApi.removeLayer(layer.id);
2049
+ } catch (error) {
2050
+ console.warn(`Failed to remove layer ${layer.id} for source ${this._sourceId}:`, error);
2051
+ }
2052
+ });
2053
+ }
2054
+ if (typeof mapSourceApi.getSource === 'function') {
2055
+ let hasSource = false;
2056
+ try {
2057
+ hasSource = Boolean(mapSourceApi.getSource(this._sourceId));
2058
+ } catch {
2059
+ hasSource = false;
2060
+ }
2061
+ if (hasSource) {
2062
+ try {
2063
+ map.removeSource(this._sourceId);
2064
+ } catch (error) {
2065
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
2066
+ }
2067
+ }
2068
+ }
2069
+ } catch (error) {
2070
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
2071
+ }
2072
+ }
2073
+ }
2074
+
2075
+ /**
2076
+ * FeatureService - Tile-based feature loading from ArcGIS FeatureServers
2077
+ *
2078
+ * This implementation is based on mapbox-gl-arcgis-featureserver by Rowan Winsemius
2079
+ * @see https://github.com/rowanwins/mapbox-gl-arcgis-featureserver
2080
+ *
2081
+ * Key features:
2082
+ * - Prioritizes PBF format for minimal payload size (requires ArcGIS Server 10.7+)
2083
+ * - Falls back to GeoJSON if PBF is not supported
2084
+ * - Uses tiled requests for efficient data loading
2085
+ * - Client-side feature deduplication
2086
+ * - Service bounds detection and coordinate projection
2087
+ */
2088
+ class FeatureService {
2089
+ constructor(sourceId, map, arcgisOptions, geojsonSourceOptions) {
2090
+ Object.defineProperty(this, "_sourceId", {
2091
+ enumerable: true,
2092
+ configurable: true,
2093
+ writable: true,
2094
+ value: void 0
2095
+ });
2096
+ Object.defineProperty(this, "_map", {
2097
+ enumerable: true,
2098
+ configurable: true,
2099
+ writable: true,
2100
+ value: void 0
2101
+ });
2102
+ Object.defineProperty(this, "_tileIndices", {
2103
+ enumerable: true,
2104
+ configurable: true,
2105
+ writable: true,
2106
+ value: void 0
2107
+ });
2108
+ Object.defineProperty(this, "_featureIndices", {
2109
+ enumerable: true,
2110
+ configurable: true,
2111
+ writable: true,
2112
+ value: void 0
2113
+ });
2114
+ Object.defineProperty(this, "_featureCollections", {
2115
+ enumerable: true,
2116
+ configurable: true,
2117
+ writable: true,
2118
+ value: void 0
2119
+ });
2120
+ Object.defineProperty(this, "_esriServiceOptions", {
2121
+ enumerable: true,
2122
+ configurable: true,
2123
+ writable: true,
2124
+ value: void 0
2125
+ });
2126
+ Object.defineProperty(this, "_fallbackProjectionEndpoint", {
2127
+ enumerable: true,
2128
+ configurable: true,
2129
+ writable: true,
2130
+ value: void 0
2131
+ });
2132
+ Object.defineProperty(this, "_serviceMetadata", {
2133
+ enumerable: true,
2134
+ configurable: true,
2135
+ writable: true,
2136
+ value: null
2137
+ });
2138
+ Object.defineProperty(this, "_maxExtent", {
2139
+ enumerable: true,
2140
+ configurable: true,
2141
+ writable: true,
2142
+ value: void 0
2143
+ });
2144
+ Object.defineProperty(this, "_boundEvent", {
2145
+ enumerable: true,
2146
+ configurable: true,
2147
+ writable: true,
2148
+ value: null
2149
+ });
2150
+ Object.defineProperty(this, "_format", {
2151
+ enumerable: true,
2152
+ configurable: true,
2153
+ writable: true,
2154
+ value: 'pbf'
2155
+ });
2156
+ Object.defineProperty(this, "_sourceReadyResolve", {
2157
+ enumerable: true,
2158
+ configurable: true,
2159
+ writable: true,
2160
+ value: null
2161
+ });
2162
+ Object.defineProperty(this, "_sourceReadyReject", {
2163
+ enumerable: true,
2164
+ configurable: true,
2165
+ writable: true,
2166
+ value: null
2167
+ });
2168
+ Object.defineProperty(this, "_eventListeners", {
2169
+ enumerable: true,
2170
+ configurable: true,
2171
+ writable: true,
2172
+ value: {}
2173
+ });
2174
+ /**
2175
+ * Promise that resolves when the source has been successfully added to the map
2176
+ * and is ready to receive data.
2177
+ */
2178
+ Object.defineProperty(this, "sourceReady", {
2179
+ enumerable: true,
2180
+ configurable: true,
2181
+ writable: true,
2182
+ value: void 0
2183
+ });
2184
+ /**
2185
+ * The GeoJSON source options passed to the constructor
2186
+ */
2187
+ Object.defineProperty(this, "geojsonSourceOptions", {
2188
+ enumerable: true,
2189
+ configurable: true,
2190
+ writable: true,
2191
+ value: void 0
2192
+ });
2193
+ if (!sourceId || !map || !arcgisOptions) {
2194
+ throw new Error('Source id, map and arcgisOptions must be supplied as the first three arguments.');
2195
+ }
2196
+ if (!arcgisOptions.url) {
2197
+ throw new Error('A url must be supplied as part of the esriServiceOptions object.');
2198
+ }
2199
+ this._sourceId = sourceId;
2200
+ this._map = map;
2201
+ // Initialize tile/feature tracking maps
2202
+ this._tileIndices = new globalThis.Map();
2203
+ this._featureIndices = new globalThis.Map();
2204
+ this._featureCollections = new globalThis.Map();
2205
+ // Clean URL and set up options
2206
+ const cleanedUrl = cleanTrailingSlash(arcgisOptions.url);
2207
+ this._esriServiceOptions = {
2208
+ useStaticZoomLevel: false,
2209
+ minZoom: arcgisOptions.useStaticZoomLevel ? 7 : 2,
2210
+ simplifyFactor: 0.3,
2211
+ precision: 8,
2212
+ where: '1=1',
2213
+ to: null,
2214
+ from: null,
2215
+ outFields: '*',
2216
+ setAttributionFromService: true,
2217
+ useServiceBounds: true,
2218
+ projectionEndpoint: `${cleanedUrl.split('rest/services')[0]}rest/services/Geometry/GeometryServer/project`,
2219
+ token: '',
2220
+ fetchOptions: undefined,
2221
+ ...arcgisOptions,
2222
+ url: cleanedUrl
2223
+ };
2224
+ this._fallbackProjectionEndpoint = 'https://tasks.arcgisonline.com/arcgis/rest/services/Geometry/GeometryServer/project';
2225
+ this._maxExtent = [-Infinity, -Infinity, Infinity, Infinity];
2226
+ this.geojsonSourceOptions = geojsonSourceOptions || {};
2227
+ // Create the sourceReady promise
2228
+ this.sourceReady = new Promise((resolve, reject) => {
2229
+ this._sourceReadyResolve = resolve;
2230
+ this._sourceReadyReject = reject;
2231
+ });
2232
+ // Add GeoJSON source to map
2233
+ this._map.addSource(sourceId, {
2234
+ type: 'geojson',
2235
+ data: this._getBlankFc(),
2236
+ ...this.geojsonSourceOptions
2237
+ });
2238
+ // Initialize the service
2239
+ this._initialize();
2240
+ }
2241
+ async _initialize() {
2242
+ try {
2243
+ await this._getServiceMetadata();
2244
+ if (!this.supportsPbf) {
2245
+ if (!this.supportsGeojson) {
2246
+ this._map.removeSource(this._sourceId);
2247
+ const error = new Error('Server does not support PBF or GeoJSON query formats.');
2248
+ if (this._sourceReadyReject) this._sourceReadyReject(error);
2249
+ throw error;
2250
+ }
2251
+ this._format = 'geojson';
2252
+ }
2253
+ if (this._esriServiceOptions.useServiceBounds && this._serviceMetadata?.extent) {
2254
+ const serviceExtent = this._serviceMetadata.extent;
2255
+ if (serviceExtent.spatialReference?.wkid === 4326) {
2256
+ this._setBounds([serviceExtent.xmin, serviceExtent.ymin, serviceExtent.xmax, serviceExtent.ymax]);
2257
+ } else {
2258
+ await this._projectBounds();
2259
+ }
2260
+ }
2261
+ // Ensure unique ID field is included in outFields
2262
+ if (this._esriServiceOptions.outFields !== '*' && this._serviceMetadata?.uniqueIdField?.name) {
2263
+ const currentFields = this._esriServiceOptions.outFields;
2264
+ const uniqueIdField = this._serviceMetadata.uniqueIdField.name;
2265
+ if (typeof currentFields === 'string') {
2266
+ if (!currentFields.includes(uniqueIdField)) {
2267
+ this._esriServiceOptions.outFields = `${currentFields},${uniqueIdField}`;
2268
+ }
2269
+ } else if (Array.isArray(currentFields)) {
2270
+ if (!currentFields.includes(uniqueIdField)) {
2271
+ this._esriServiceOptions.outFields = [...currentFields, uniqueIdField].join(',');
2272
+ }
2273
+ }
2274
+ }
2275
+ this._setAttribution();
2276
+ this.enableRequests();
2277
+ this._clearAndRefreshTiles();
2278
+ if (this._sourceReadyResolve) this._sourceReadyResolve();
2279
+ } catch (error) {
2280
+ const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
2281
+ if (!isTestEnvironment) {
2282
+ console.error('Error initializing FeatureService:', error);
2283
+ }
2284
+ if (this._sourceReadyReject && error instanceof Error) {
2285
+ this._sourceReadyReject(error);
2286
+ }
2287
+ }
2288
+ }
2289
+ /**
2290
+ * Remove the source and clean up event listeners
2291
+ */
2292
+ remove() {
2293
+ this.disableRequests();
2294
+ if (this._map && typeof this._map.removeSource === 'function') {
2295
+ try {
2296
+ // Guard against map whose style has already been destroyed
2297
+ const mapWithStyle = this._map;
2298
+ if (!mapWithStyle.style) return;
2299
+ // First, remove any layers that are using this source
2300
+ if (mapWithStyle.getStyle) {
2301
+ const style = mapWithStyle.getStyle();
2302
+ const layers = style?.layers || [];
2303
+ layers.forEach(layer => {
2304
+ if (layer.source === this._sourceId && this._map.getLayer && this._map.getLayer(layer.id)) {
2305
+ this._map.removeLayer(layer.id);
2306
+ }
2307
+ });
2308
+ }
2309
+ if (this._map.getSource && this._map.getSource(this._sourceId)) {
2310
+ this._map.removeSource(this._sourceId);
2311
+ }
2312
+ } catch (error) {
2313
+ console.warn(`Failed to remove source ${this._sourceId}:`, error);
2314
+ }
2315
+ }
2316
+ }
2317
+ /** Alias for remove() for API compatibility */
2318
+ destroySource() {
2319
+ this.remove();
2320
+ }
2321
+ _getBlankFc() {
2322
+ return {
2323
+ type: 'FeatureCollection',
2324
+ features: []
2325
+ };
2326
+ }
2327
+ _setBounds(bounds) {
2328
+ this._maxExtent = bounds;
2329
+ }
2330
+ /** Check if service supports GeoJSON format */
2331
+ get supportsGeojson() {
2332
+ return (this._serviceMetadata?.supportedQueryFormats?.indexOf('geoJSON') ?? -1) > -1;
2333
+ }
2334
+ /** Check if service supports PBF format */
2335
+ get supportsPbf() {
2336
+ return (this._serviceMetadata?.supportedQueryFormats?.indexOf('PBF') ?? -1) > -1;
2337
+ }
2338
+ /** Get the service metadata */
2339
+ get serviceMetadata() {
2340
+ return this._serviceMetadata;
2341
+ }
2342
+ /** Get the source object from the map */
2343
+ get _source() {
2344
+ return this._map.getSource(this._sourceId);
2345
+ }
2346
+ /** Get the current query URL */
2347
+ get _url() {
2348
+ return this._esriServiceOptions.url;
2349
+ }
2350
+ /** Get the esri service options */
2351
+ get esriServiceOptions() {
2352
+ return this._esriServiceOptions;
2353
+ }
2354
+ /** Get default style based on geometry type */
2355
+ get defaultStyle() {
2356
+ const geometryType = String(this._serviceMetadata?.geometryType || 'esriGeometryPoint');
2357
+ const baseStyle = {
2358
+ source: this._sourceId
2359
+ };
2360
+ if (geometryType.includes('Point')) {
2361
+ return {
2362
+ type: 'circle',
2363
+ ...baseStyle,
2364
+ paint: {
2365
+ 'circle-radius': 4,
2366
+ 'circle-color': '#3b82f6',
2367
+ 'circle-stroke-color': '#1e40af',
2368
+ 'circle-stroke-width': 1
2369
+ }
2370
+ };
2371
+ } else if (geometryType.includes('Polyline')) {
2372
+ return {
2373
+ type: 'line',
2374
+ ...baseStyle,
2375
+ paint: {
2376
+ 'line-color': '#3b82f6',
2377
+ 'line-width': 2
2378
+ }
2379
+ };
2380
+ } else if (geometryType.includes('Polygon')) {
2381
+ return {
2382
+ type: 'fill',
2383
+ ...baseStyle,
2384
+ paint: {
2385
+ 'fill-color': 'rgba(59, 130, 246, 0.4)',
2386
+ 'fill-outline-color': '#1e40af'
2387
+ }
2388
+ };
2389
+ }
2390
+ // Default to circle for unknown geometry types
2391
+ return {
2392
+ type: 'circle',
2393
+ ...baseStyle,
2394
+ paint: {
2395
+ 'circle-radius': 4,
2396
+ 'circle-color': '#3b82f6',
2397
+ 'circle-stroke-color': '#1e40af',
2398
+ 'circle-stroke-width': 1
2399
+ }
2400
+ };
2401
+ }
2402
+ /** Get style, fetching metadata if needed */
2403
+ async getStyle() {
2404
+ if (this._serviceMetadata) {
2405
+ return this.defaultStyle;
2406
+ }
2407
+ await this._getServiceMetadata();
2408
+ return this.defaultStyle;
2409
+ }
2410
+ /** Disable map event listeners */
2411
+ disableRequests() {
2412
+ if (this._boundEvent) {
2413
+ this._map.off('moveend', this._boundEvent);
2414
+ this._boundEvent = null;
2415
+ }
2416
+ }
2417
+ /** Enable map event listeners */
2418
+ enableRequests() {
2419
+ this._boundEvent = this._findAndMapData.bind(this);
2420
+ this._map.on('moveend', this._boundEvent);
2421
+ }
2422
+ _clearAndRefreshTiles() {
2423
+ this._tileIndices = new globalThis.Map();
2424
+ this._featureIndices = new globalThis.Map();
2425
+ this._featureCollections = new globalThis.Map();
2426
+ this._findAndMapData();
2427
+ }
2428
+ /**
2429
+ * Set the WHERE clause filter
2430
+ */
2431
+ setWhere(newWhere) {
2432
+ this._esriServiceOptions.where = newWhere;
2433
+ this._clearAndRefreshTiles();
2434
+ }
2435
+ /**
2436
+ * Clear the WHERE clause filter
2437
+ */
2438
+ clearWhere() {
2439
+ this._esriServiceOptions.where = '1=1';
2440
+ this._clearAndRefreshTiles();
2441
+ }
2442
+ /**
2443
+ * Set a date/time filter
2444
+ */
2445
+ setDate(to, from) {
2446
+ this._esriServiceOptions.to = to;
2447
+ this._esriServiceOptions.from = from ?? null;
2448
+ this._clearAndRefreshTiles();
2449
+ }
2450
+ /**
2451
+ * Set the authentication token
2452
+ */
2453
+ setToken(token) {
2454
+ this._esriServiceOptions.token = token ?? '';
2455
+ this._clearAndRefreshTiles();
2456
+ }
2457
+ /**
2458
+ * Set output fields
2459
+ */
2460
+ setOutFields(fields) {
2461
+ this._esriServiceOptions.outFields = Array.isArray(fields) ? fields.join(',') : fields;
2462
+ this._clearAndRefreshTiles();
2463
+ }
2464
+ _createOrGetTileIndex(zoomLevel) {
2465
+ const existingZoomIndex = this._tileIndices.get(zoomLevel);
2466
+ if (existingZoomIndex) return existingZoomIndex;
2467
+ const newIndex = new globalThis.Map();
2468
+ this._tileIndices.set(zoomLevel, newIndex);
2469
+ return newIndex;
2470
+ }
2471
+ _createOrGetFeatureCollection(zoomLevel) {
2472
+ const existingZoomIndex = this._featureCollections.get(zoomLevel);
2473
+ if (existingZoomIndex) return existingZoomIndex;
2474
+ const fc = this._getBlankFc();
2475
+ this._featureCollections.set(zoomLevel, fc);
2476
+ return fc;
2477
+ }
2478
+ _createOrGetFeatureIdIndex(zoomLevel) {
2479
+ const existingFeatureIdIndex = this._featureIndices.get(zoomLevel);
2480
+ if (existingFeatureIdIndex) return existingFeatureIdIndex;
2481
+ const newFeatureIdIndex = new globalThis.Map();
2482
+ this._featureIndices.set(zoomLevel, newFeatureIdIndex);
2483
+ return newFeatureIdIndex;
2484
+ }
2485
+ async _findAndMapData() {
2486
+ const z = this._map.getZoom();
2487
+ if (z < this._esriServiceOptions.minZoom) {
2488
+ return;
2489
+ }
2490
+ const bounds = this._map.getBounds();
2491
+ if (!bounds) return;
2492
+ const boundsArray = bounds.toArray();
2493
+ const primaryTile = tilebelt.bboxToTile([boundsArray[0][0], boundsArray[0][1], boundsArray[1][0], boundsArray[1][1]]);
2494
+ if (this._esriServiceOptions.useServiceBounds) {
2495
+ if (this._maxExtent[0] !== -Infinity && !this._doesTileOverlapBbox(this._maxExtent, boundsArray)) {
2496
+ return;
2497
+ }
2498
+ }
2499
+ // Round to nearest even zoom level to reuse data across zooms
2500
+ const zoomLevel = this._esriServiceOptions.useStaticZoomLevel ? this._esriServiceOptions.minZoom : 2 * Math.floor(z / 2);
2501
+ const zoomLevelIndex = this._createOrGetTileIndex(zoomLevel);
2502
+ const featureIdIndex = this._createOrGetFeatureIdIndex(zoomLevel);
2503
+ const fc = this._createOrGetFeatureCollection(zoomLevel);
2504
+ let tilesToRequest = [];
2505
+ if (primaryTile[2] < zoomLevel) {
2506
+ let candidateTiles = tilebelt.getChildren(primaryTile);
2507
+ let minZoomOfCandidates = candidateTiles[0][2];
2508
+ while (minZoomOfCandidates < zoomLevel) {
2509
+ const newCandidateTiles = [];
2510
+ candidateTiles.forEach(t => {
2511
+ newCandidateTiles.push(...tilebelt.getChildren(t));
2512
+ });
2513
+ candidateTiles = newCandidateTiles;
2514
+ minZoomOfCandidates = candidateTiles[0][2];
2515
+ }
2516
+ for (let index = 0; index < candidateTiles.length; index++) {
2517
+ if (this._doesTileOverlapBbox(candidateTiles[index], boundsArray)) {
2518
+ tilesToRequest.push(candidateTiles[index]);
2519
+ }
2520
+ }
2521
+ } else {
2522
+ tilesToRequest.push(primaryTile);
2523
+ }
2524
+ // Filter out already fetched tiles
2525
+ tilesToRequest = tilesToRequest.filter(tile => {
2526
+ const quadKey = tilebelt.tileToQuadkey(tile);
2527
+ if (zoomLevelIndex.has(quadKey)) {
2528
+ return false;
2529
+ }
2530
+ zoomLevelIndex.set(quadKey, true);
2531
+ return true;
2532
+ });
2533
+ if (tilesToRequest.length === 0) {
2534
+ this._updateFcOnMap(fc);
2535
+ return;
2536
+ }
2537
+ // Calculate tolerance for simplification
2538
+ const mapWidth = Math.abs(boundsArray[1][0] - boundsArray[0][0]);
2539
+ const canvas = this._map.getCanvas();
2540
+ const tolerance = mapWidth / canvas.width * this._esriServiceOptions.simplifyFactor;
2541
+ await this._loadTiles(tilesToRequest, tolerance, featureIdIndex, fc);
2542
+ this._updateFcOnMap(fc);
2543
+ }
2544
+ async _loadTiles(tilesToRequest, tolerance, featureIdIndex, fc) {
2545
+ const promises = tilesToRequest.map(t => this._getTile(t, tolerance));
2546
+ const featureCollections = await Promise.all(promises);
2547
+ featureCollections.forEach(tileFc => {
2548
+ if (tileFc) this._iterateItems(tileFc, featureIdIndex, fc);
2549
+ });
2550
+ }
2551
+ _iterateItems(tileFc, featureIdIndex, fc) {
2552
+ if (!tileFc || !tileFc.features) return;
2553
+ tileFc.features.forEach(feature => {
2554
+ const featureId = feature.id;
2555
+ if (featureId !== undefined && !featureIdIndex.has(featureId)) {
2556
+ fc.features.push(feature);
2557
+ featureIdIndex.set(featureId, true);
2558
+ } else if (featureId === undefined) {
2559
+ // If no ID, just add it (can't deduplicate)
2560
+ fc.features.push(feature);
2561
+ }
2562
+ });
2563
+ }
2564
+ get _time() {
2565
+ if (!this._esriServiceOptions.to) return false;
2566
+ let from = this._esriServiceOptions.from;
2567
+ let to = this._esriServiceOptions.to;
2568
+ if (from instanceof Date) from = from.valueOf();
2569
+ if (to instanceof Date) to = to.valueOf();
2570
+ return `${from ?? ''},${to}`;
2571
+ }
2572
+ async _getTile(tile, tolerance) {
2573
+ const tileBounds = tilebelt.tileToBBOX(tile);
2574
+ const extent = {
2575
+ spatialReference: {
2576
+ latestWkid: 4326,
2577
+ wkid: 4326
2578
+ },
2579
+ xmin: tileBounds[0],
2580
+ ymin: tileBounds[1],
2581
+ xmax: tileBounds[2],
2582
+ ymax: tileBounds[3]
2583
+ };
2584
+ const params = new URLSearchParams({
2585
+ f: this._format,
2586
+ geometry: JSON.stringify(extent),
2587
+ where: this._esriServiceOptions.where,
2588
+ outFields: typeof this._esriServiceOptions.outFields === 'string' ? this._esriServiceOptions.outFields : this._esriServiceOptions.outFields?.join(',') || '*',
2589
+ outSR: '4326',
2590
+ returnZ: 'false',
2591
+ returnM: 'false',
2592
+ precision: this._esriServiceOptions.precision.toString(),
2593
+ quantizationParameters: JSON.stringify({
2594
+ extent,
2595
+ tolerance,
2596
+ mode: 'view'
2597
+ }),
2598
+ resultType: 'tile',
2599
+ spatialRel: 'esriSpatialRelIntersects',
2600
+ geometryType: 'esriGeometryEnvelope',
2601
+ inSR: '4326'
2602
+ });
2603
+ if (this._time) {
2604
+ params.append('time', this._time);
2605
+ }
2606
+ this._appendTokenIfExists(params);
2607
+ try {
2608
+ const response = await fetch(`${this._esriServiceOptions.url}/query?${params.toString()}`, this._esriServiceOptions.fetchOptions);
2609
+ if (!response.ok) {
2610
+ console.warn(`Tile fetch failed: HTTP ${response.status}`);
2611
+ return null;
2612
+ }
2613
+ if (this._format === 'pbf') {
2614
+ const buffer = await response.arrayBuffer();
2615
+ try {
2616
+ const decoded = tileDecode(new Uint8Array(buffer));
2617
+ return decoded.featureCollection;
2618
+ } catch {
2619
+ console.error('Could not parse arcgis buffer. Please check the url you requested.');
2620
+ return null;
2621
+ }
2622
+ } else {
2623
+ const data = await response.json();
2624
+ this._checkAgolError(data);
2625
+ return data;
2626
+ }
2627
+ } catch (error) {
2628
+ this._handleAuthError(error);
2629
+ console.warn('Error fetching tile:', error);
2630
+ return null;
2631
+ }
2632
+ }
2633
+ _updateFcOnMap(fc) {
2634
+ const source = this._map.getSource(this._sourceId);
2635
+ if (source && 'setData' in source) {
2636
+ source.setData(fc);
2637
+ }
2638
+ }
2639
+ _doesTileOverlapBbox(tile, bbox) {
2640
+ const tileBounds = tile.length === 4 ? tile : tilebelt.tileToBBOX(tile);
2641
+ if (tileBounds[2] < bbox[0][0]) return false;
2642
+ if (tileBounds[0] > bbox[1][0]) return false;
2643
+ if (tileBounds[3] < bbox[0][1]) return false;
2644
+ if (tileBounds[1] > bbox[1][1]) return false;
2645
+ return true;
2646
+ }
2647
+ async _getServiceMetadata() {
2648
+ if (this._serviceMetadata !== null) return this._serviceMetadata;
2649
+ const params = new URLSearchParams({
2650
+ f: 'json'
2651
+ });
2652
+ this._appendTokenIfExists(params);
2653
+ const response = await fetch(`${this._esriServiceOptions.url}?${params.toString()}`, this._esriServiceOptions.fetchOptions);
2654
+ if (!response.ok) {
2655
+ throw new Error(`Failed to fetch service metadata: HTTP ${response.status}`);
2656
+ }
2657
+ const data = await response.json();
2658
+ if (data.error) {
2659
+ throw new Error(JSON.stringify(data.error));
2660
+ }
2661
+ this._serviceMetadata = data;
2662
+ return this._serviceMetadata;
2663
+ }
2664
+ /**
2665
+ * Query features by longitude/latitude with optional radius
2666
+ */
2667
+ async getFeaturesByLonLat(lnglat, radius = 20, returnGeometry = false) {
2668
+ const params = new URLSearchParams({
2669
+ sr: '4326',
2670
+ geometryType: 'esriGeometryPoint',
2671
+ geometry: JSON.stringify({
2672
+ x: lnglat.lng,
2673
+ y: lnglat.lat,
2674
+ spatialReference: {
2675
+ wkid: 4326
2676
+ }
2677
+ }),
2678
+ returnGeometry: returnGeometry.toString(),
2679
+ outFields: '*',
2680
+ spatialRel: 'esriSpatialRelIntersects',
2681
+ units: 'esriSRUnit_Meter',
2682
+ distance: radius.toString(),
2683
+ f: 'geojson'
2684
+ });
2685
+ if (this._time) {
2686
+ params.append('time', this._time);
2687
+ }
2688
+ this._appendTokenIfExists(params);
2689
+ const response = await fetch(`${this._esriServiceOptions.url}/query?${params.toString()}`, this._esriServiceOptions.fetchOptions);
2690
+ if (!response.ok) {
2691
+ throw new Error(`Query failed: HTTP ${response.status}`);
2692
+ }
2693
+ return await response.json();
2694
+ }
2695
+ /**
2696
+ * Query features by object IDs
2697
+ */
2698
+ async getFeaturesByObjectIds(objectIds, returnGeometry = false) {
2699
+ const idsString = Array.isArray(objectIds) ? objectIds.join(',') : objectIds;
2700
+ const params = new URLSearchParams({
2701
+ sr: '4326',
2702
+ objectIds: idsString,
2703
+ returnGeometry: returnGeometry.toString(),
2704
+ outFields: '*',
2705
+ f: 'geojson'
2706
+ });
2707
+ this._appendTokenIfExists(params);
2708
+ const response = await fetch(`${this._esriServiceOptions.url}/query?${params.toString()}`, this._esriServiceOptions.fetchOptions);
2709
+ if (!response.ok) {
2710
+ throw new Error(`Query failed: HTTP ${response.status}`);
2711
+ }
2712
+ return await response.json();
2713
+ }
2714
+ /**
2715
+ * Query features with custom options
2716
+ */
2717
+ async queryFeatures(options) {
2718
+ const queryOptions = {
2719
+ ...this._esriServiceOptions,
2720
+ ...options
2721
+ };
2722
+ const params = new URLSearchParams();
2723
+ params.append('f', 'geojson');
2724
+ params.append('where', queryOptions.where || '1=1');
2725
+ params.append('outFields', typeof queryOptions.outFields === 'string' ? queryOptions.outFields : queryOptions.outFields?.join(',') || '*');
2726
+ params.append('returnGeometry', (queryOptions.returnGeometry !== false).toString());
2727
+ if (queryOptions.geometry) {
2728
+ params.append('geometry', JSON.stringify(queryOptions.geometry));
2729
+ if (queryOptions.geometryType) params.append('geometryType', queryOptions.geometryType);
2730
+ if (queryOptions.spatialRel) params.append('spatialRel', queryOptions.spatialRel);
2731
+ if (queryOptions.inSR) params.append('inSR', queryOptions.inSR);
2732
+ }
2733
+ if (queryOptions.outSR) params.append('outSR', queryOptions.outSR);
2734
+ if (queryOptions.orderByFields) params.append('orderByFields', queryOptions.orderByFields);
2735
+ if (queryOptions.groupByFieldsForStatistics) params.append('groupByFieldsForStatistics', queryOptions.groupByFieldsForStatistics);
2736
+ if (queryOptions.outStatistics && queryOptions.outStatistics.length > 0) params.append('outStatistics', JSON.stringify(queryOptions.outStatistics));
2737
+ if (queryOptions.having) params.append('having', queryOptions.having);
2738
+ if (queryOptions.resultOffset) params.append('resultOffset', queryOptions.resultOffset.toString());
2739
+ if (queryOptions.resultRecordCount) params.append('resultRecordCount', queryOptions.resultRecordCount.toString());
2740
+ if (queryOptions.token) params.append('token', queryOptions.token);
2741
+ try {
2742
+ const response = await fetch(`${this._esriServiceOptions.url}/query?${params.toString()}`, this._esriServiceOptions.fetchOptions);
2743
+ if (!response.ok) {
2744
+ throw new Error(`HTTP error! status: ${response.status}`);
2745
+ }
2746
+ return await response.json();
2747
+ } catch (error) {
2748
+ const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
2749
+ if (!isTestEnvironment) {
2750
+ console.error('Error querying features:', error);
2751
+ }
2752
+ throw error;
2753
+ }
2754
+ }
2755
+ async _projectBounds() {
2756
+ if (!this._serviceMetadata?.extent) return;
2757
+ const params = new URLSearchParams({
2758
+ geometries: JSON.stringify({
2759
+ geometryType: 'esriGeometryEnvelope',
2760
+ geometries: [this._serviceMetadata.extent]
2761
+ }),
2762
+ inSR: (this._serviceMetadata.extent.spatialReference?.wkid || 4326).toString(),
2763
+ outSR: '4326',
2764
+ f: 'json'
2765
+ });
2766
+ let fetchOptions = this._esriServiceOptions.fetchOptions;
2767
+ if (!this._projectionEndpointIsFallback()) {
2768
+ this._appendTokenIfExists(params);
2769
+ } else {
2770
+ fetchOptions = undefined;
2771
+ }
2772
+ try {
2773
+ const response = await fetch(`${this._esriServiceOptions.projectionEndpoint}?${params.toString()}`, fetchOptions);
2774
+ if (!response.ok) {
2775
+ throw new Error(`Projection failed: HTTP ${response.status}`);
2776
+ }
2777
+ const data = await response.json();
2778
+ if (data.error) {
2779
+ throw new Error(JSON.stringify(data.error));
2780
+ }
2781
+ const extent = data.geometries[0];
2782
+ this._maxExtent = [extent.xmin, extent.ymin, extent.xmax, extent.ymax];
2783
+ } catch (error) {
2784
+ // If projection endpoint fails, try fallback
2785
+ if (!this._projectionEndpointIsFallback()) {
2786
+ this._esriServiceOptions.projectionEndpoint = this._fallbackProjectionEndpoint;
2787
+ await this._projectBounds();
2788
+ } else {
2789
+ console.warn('Could not project service bounds:', error);
2790
+ }
2791
+ }
2792
+ }
2793
+ _projectionEndpointIsFallback() {
2794
+ return this._esriServiceOptions.projectionEndpoint === this._fallbackProjectionEndpoint;
2795
+ }
2796
+ _setAttribution() {
2797
+ if (!this._esriServiceOptions.setAttributionFromService) return;
2798
+ const copyrightText = this._serviceMetadata?.copyrightText;
2799
+ if (copyrightText) {
2800
+ updateAttribution(copyrightText, this._sourceId, this._map);
2801
+ } else {
2802
+ // Add default Esri attribution
2803
+ updateAttribution('', this._sourceId, this._map);
2804
+ }
2805
+ }
2806
+ _appendTokenIfExists(params) {
2807
+ const token = this._esriServiceOptions.token;
2808
+ if (token) {
2809
+ params.append('token', token);
2810
+ }
2811
+ }
2812
+ _getFetchHeaders() {
2813
+ if (this._esriServiceOptions.apiKey) {
2814
+ return {
2815
+ 'X-Esri-Authorization': `Bearer ${this._esriServiceOptions.apiKey}`
2816
+ };
2817
+ }
2818
+ return undefined;
2819
+ }
2820
+ _checkAgolError(data) {
2821
+ if (data && typeof data === 'object' && 'error' in data) {
2822
+ const errorData = data.error;
2823
+ const err = new Error(errorData.message || 'ArcGIS service error');
2824
+ err.code = errorData.code;
2825
+ err.details = errorData.details;
2826
+ throw err;
2827
+ }
2828
+ }
2829
+ _handleAuthError(error) {
2830
+ if (error && typeof error === 'object' && 'code' in error) {
2831
+ const code = error.code;
2832
+ if (code === 498 || code === 499) {
2833
+ this._fireEvent('authenticationrequired', {
2834
+ authenticate: token => this.setToken(token)
2835
+ });
2836
+ }
2837
+ }
2838
+ }
2839
+ _fireEvent(event, data) {
2840
+ const listeners = this._eventListeners[event];
2841
+ if (listeners) {
2842
+ listeners.forEach(cb => cb(data));
2843
+ }
2844
+ }
2845
+ /**
2846
+ * Add event listener
2847
+ */
2848
+ on(event, callback) {
2849
+ if (!this._eventListeners[event]) {
2850
+ this._eventListeners[event] = [];
2851
+ }
2852
+ this._eventListeners[event].push(callback);
2853
+ return this;
2854
+ }
2855
+ /**
2856
+ * Remove event listener
2857
+ */
2858
+ off(event, callback) {
2859
+ const listeners = this._eventListeners[event];
2860
+ if (listeners) {
2861
+ const index = listeners.indexOf(callback);
2862
+ if (index > -1) {
2863
+ listeners.splice(index, 1);
2864
+ }
2865
+ }
2866
+ return this;
2867
+ }
2868
+ // ========================================
2869
+ // Feature Editing Methods
2870
+ // ========================================
2871
+ /**
2872
+ * Add features to the service
2873
+ */
2874
+ async addFeatures(features, options) {
2875
+ const params = new URLSearchParams({
2876
+ f: 'json',
2877
+ features: JSON.stringify(features)
2878
+ });
2879
+ if (options?.gdbVersion) params.append('gdbVersion', options.gdbVersion);
2880
+ this._appendTokenIfExists(params);
2881
+ const response = await fetch(`${this._esriServiceOptions.url}/addFeatures`, {
2882
+ method: 'POST',
2883
+ body: params,
2884
+ headers: this._getFetchHeaders(),
2885
+ ...this._esriServiceOptions.fetchOptions
2886
+ });
2887
+ if (!response.ok) {
2888
+ throw new Error(`Add features failed: HTTP ${response.status}`);
2889
+ }
2890
+ const data = await response.json();
2891
+ this._checkAgolError(data);
2892
+ return data.addResults;
2893
+ }
2894
+ /**
2895
+ * Update existing features
2896
+ */
2897
+ async updateFeatures(features, options) {
2898
+ const params = new URLSearchParams({
2899
+ f: 'json',
2900
+ features: JSON.stringify(features)
2901
+ });
2902
+ if (options?.gdbVersion) params.append('gdbVersion', options.gdbVersion);
2903
+ this._appendTokenIfExists(params);
2904
+ const response = await fetch(`${this._esriServiceOptions.url}/updateFeatures`, {
2905
+ method: 'POST',
2906
+ body: params,
2907
+ headers: this._getFetchHeaders(),
2908
+ ...this._esriServiceOptions.fetchOptions
2909
+ });
2910
+ if (!response.ok) {
2911
+ throw new Error(`Update features failed: HTTP ${response.status}`);
2912
+ }
2913
+ const data = await response.json();
2914
+ this._checkAgolError(data);
2915
+ return data.updateResults;
2916
+ }
2917
+ /**
2918
+ * Delete features by objectIds or where clause
2919
+ */
2920
+ async deleteFeatures(deleteParams) {
2921
+ const params = new URLSearchParams({
2922
+ f: 'json'
2923
+ });
2924
+ if (deleteParams.objectIds) {
2925
+ params.append('objectIds', deleteParams.objectIds.join(','));
2926
+ }
2927
+ if (deleteParams.where) {
2928
+ params.append('where', deleteParams.where);
2929
+ }
2930
+ this._appendTokenIfExists(params);
2931
+ const response = await fetch(`${this._esriServiceOptions.url}/deleteFeatures`, {
2932
+ method: 'POST',
2933
+ body: params,
2934
+ headers: this._getFetchHeaders(),
2935
+ ...this._esriServiceOptions.fetchOptions
2936
+ });
2937
+ if (!response.ok) {
2938
+ throw new Error(`Delete features failed: HTTP ${response.status}`);
2939
+ }
2940
+ const data = await response.json();
2941
+ this._checkAgolError(data);
2942
+ return data.deleteResults;
2943
+ }
2944
+ /**
2945
+ * Apply multiple edits in a single transaction
2946
+ */
2947
+ async applyEdits(edits, options) {
2948
+ const params = new URLSearchParams({
2949
+ f: 'json'
2950
+ });
2951
+ if (edits.adds) params.append('adds', JSON.stringify(edits.adds));
2952
+ if (edits.updates) params.append('updates', JSON.stringify(edits.updates));
2953
+ if (edits.deletes) params.append('deletes', edits.deletes.join(','));
2954
+ if (options?.gdbVersion) params.append('gdbVersion', options.gdbVersion);
2955
+ this._appendTokenIfExists(params);
2956
+ const response = await fetch(`${this._esriServiceOptions.url}/applyEdits`, {
2957
+ method: 'POST',
2958
+ body: params,
2959
+ headers: this._getFetchHeaders(),
2960
+ ...this._esriServiceOptions.fetchOptions
2961
+ });
2962
+ if (!response.ok) {
2963
+ throw new Error(`Apply edits failed: HTTP ${response.status}`);
2964
+ }
2965
+ const data = await response.json();
2966
+ this._checkAgolError(data);
2967
+ return data;
2968
+ }
2969
+ // ========================================
2970
+ // Attachment Methods
2971
+ // ========================================
2972
+ /**
2973
+ * Query attachments for a feature
2974
+ */
2975
+ async queryAttachments(objectId, options) {
2976
+ const params = new URLSearchParams({
2977
+ f: 'json'
2978
+ });
2979
+ if (options?.globalIds) {
2980
+ params.append('globalIds', options.globalIds.join(','));
2981
+ }
2982
+ this._appendTokenIfExists(params);
2983
+ const response = await fetch(`${this._esriServiceOptions.url}/${objectId}/attachments?${params.toString()}`, {
2984
+ headers: this._getFetchHeaders(),
2985
+ ...this._esriServiceOptions.fetchOptions
2986
+ });
2987
+ if (!response.ok) {
2988
+ throw new Error(`Query attachments failed: HTTP ${response.status}`);
2989
+ }
2990
+ const data = await response.json();
2991
+ this._checkAgolError(data);
2992
+ return data.attachmentInfos || [];
2993
+ }
2994
+ /**
2995
+ * Add an attachment to a feature
2996
+ */
2997
+ async addAttachment(objectId, file, fileName) {
2998
+ const formData = new FormData();
2999
+ formData.append('f', 'json');
3000
+ formData.append('attachment', file, fileName || (file instanceof File ? file.name : 'attachment'));
3001
+ if (this._esriServiceOptions.token) {
3002
+ formData.append('token', this._esriServiceOptions.token);
3003
+ }
3004
+ const headers = {};
3005
+ if (this._esriServiceOptions.apiKey) {
3006
+ headers['X-Esri-Authorization'] = `Bearer ${this._esriServiceOptions.apiKey}`;
3007
+ }
3008
+ const response = await fetch(`${this._esriServiceOptions.url}/${objectId}/addAttachment`, {
3009
+ method: 'POST',
3010
+ body: formData,
3011
+ headers: Object.keys(headers).length > 0 ? headers : undefined
3012
+ });
3013
+ if (!response.ok) {
3014
+ throw new Error(`Add attachment failed: HTTP ${response.status}`);
3015
+ }
3016
+ const data = await response.json();
3017
+ this._checkAgolError(data);
3018
+ return data.addAttachmentResult;
3019
+ }
3020
+ /**
3021
+ * Delete attachments from a feature
3022
+ */
3023
+ async deleteAttachments(objectId, attachmentIds) {
3024
+ const params = new URLSearchParams({
3025
+ f: 'json',
3026
+ attachmentIds: attachmentIds.join(',')
3027
+ });
3028
+ this._appendTokenIfExists(params);
3029
+ const response = await fetch(`${this._esriServiceOptions.url}/${objectId}/deleteAttachments`, {
3030
+ method: 'POST',
3031
+ body: params,
3032
+ headers: this._getFetchHeaders(),
3033
+ ...this._esriServiceOptions.fetchOptions
3034
+ });
3035
+ if (!response.ok) {
3036
+ throw new Error(`Delete attachments failed: HTTP ${response.status}`);
3037
+ }
3038
+ const data = await response.json();
3039
+ this._checkAgolError(data);
3040
+ return data.deleteAttachmentResults;
3041
+ }
3042
+ // Legacy method aliases for backwards compatibility
3043
+ /** @deprecated Use setWhere instead */
3044
+ updateSource() {
3045
+ this._clearAndRefreshTiles();
3046
+ }
3047
+ /** @deprecated Use setWhere instead */
3048
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3049
+ setLayers(_layers) {
3050
+ // FeatureService doesn't support layers parameter (single layer only)
3051
+ console.warn('setLayers is not applicable to FeatureService. Use the layer URL directly.');
3052
+ }
3053
+ /** @deprecated Use clearWhere instead */
3054
+ setGeometry(geometry, geometryType) {
3055
+ // For compatibility, but recommend using queryFeatures with geometry parameter
3056
+ console.warn('setGeometry is not recommended. Use queryFeatures({ geometry: ... }) for spatial queries.');
3057
+ this._esriServiceOptions.geometry = geometry;
3058
+ if (geometryType) {
3059
+ this._esriServiceOptions.geometryType = geometryType;
3060
+ }
3061
+ }
3062
+ /** @deprecated */
3063
+ clearGeometry() {
3064
+ delete this._esriServiceOptions.geometry;
3065
+ delete this._esriServiceOptions.geometryType;
3066
+ }
3067
+ /** @deprecated Use enableRequests/disableRequests instead */
3068
+ setBoundingBoxFilter(enabled) {
3069
+ if (enabled) {
3070
+ this.enableRequests();
3071
+ } else {
3072
+ this.disableRequests();
3073
+ }
3074
+ }
3075
+ /** @deprecated Use _clearAndRefreshTiles instead */
3076
+ updateData() {
3077
+ this._clearAndRefreshTiles();
3078
+ }
3079
+ }
3080
+
3081
+ /**
3082
+ * Base Task class for ArcGIS REST API operations
3083
+ * Similar to Esri Leaflet's Task class
3084
+ */
3085
+ class Task {
3086
+ constructor(endpoint) {
3087
+ Object.defineProperty(this, "_service", {
3088
+ enumerable: true,
3089
+ configurable: true,
3090
+ writable: true,
3091
+ value: void 0
3092
+ });
3093
+ Object.defineProperty(this, "options", {
3094
+ enumerable: true,
3095
+ configurable: true,
3096
+ writable: true,
3097
+ value: void 0
3098
+ });
3099
+ Object.defineProperty(this, "params", {
3100
+ enumerable: true,
3101
+ configurable: true,
3102
+ writable: true,
3103
+ value: {}
3104
+ });
3105
+ Object.defineProperty(this, "path", {
3106
+ enumerable: true,
3107
+ configurable: true,
3108
+ writable: true,
3109
+ value: ''
3110
+ });
3111
+ Object.defineProperty(this, "setters", {
3112
+ enumerable: true,
3113
+ configurable: true,
3114
+ writable: true,
3115
+ value: {}
3116
+ });
3117
+ // endpoint can be either a url (and options) for an ArcGIS Rest Service or an instance of Service
3118
+ if (endpoint && typeof endpoint === 'object' && 'request' in endpoint) {
3119
+ this._service = endpoint;
3120
+ this.options = {}; // Service will handle its own options
3121
+ } else if (typeof endpoint === 'string') {
3122
+ this.options = {
3123
+ url: cleanTrailingSlash(endpoint)
3124
+ };
3125
+ } else {
3126
+ this.options = {
3127
+ ...endpoint
3128
+ };
3129
+ if (this.options.url) {
3130
+ this.options.url = cleanTrailingSlash(this.options.url);
3131
+ }
3132
+ }
3133
+ // Initialize params if not already set by subclass
3134
+ if (!this.params) {
3135
+ this.params = {};
3136
+ }
3137
+ // Generate setter methods based on the setters object
3138
+ if (this.setters) {
3139
+ for (const setter in this.setters) {
3140
+ const param = this.setters[setter];
3141
+ this[setter] = this.generateSetter(param, this);
3142
+ }
3143
+ }
3144
+ }
3145
+ /**
3146
+ * Generate a method for each methodName:paramName in the setters for this task
3147
+ */
3148
+ generateSetter(param, context) {
3149
+ return function (value) {
3150
+ context.params[param] = value;
3151
+ return context;
3152
+ };
3153
+ }
3154
+ /**
3155
+ * Set authentication token
3156
+ */
3157
+ token(token) {
3158
+ if (this._service) {
3159
+ this._service.authenticate(token);
3160
+ } else {
3161
+ this.params.token = token;
3162
+ }
3163
+ return this;
3164
+ }
3165
+ /**
3166
+ * Set API key (alias for token)
3167
+ */
3168
+ apikey(apikey) {
3169
+ return this.token(apikey);
3170
+ }
3171
+ /**
3172
+ * Set whether to return formatted or unformatted values (ArcGIS Server 10.5+)
3173
+ */
3174
+ format(formatted) {
3175
+ this.params.returnUnformattedValues = !formatted;
3176
+ return this;
3177
+ }
3178
+ /**
3179
+ * Execute the task request with callback (Esri Leaflet style)
3180
+ */
3181
+ run(callback) {
3182
+ if (this.options.requestParams) {
3183
+ Object.assign(this.params, this.options.requestParams);
3184
+ }
3185
+ if (this._service) {
3186
+ this._service.requestWithCallback('POST', this.path, this.params, callback);
3187
+ return;
3188
+ }
3189
+ // Direct request fallback
3190
+ this._request('POST', this.path, this.params, callback);
3191
+ }
3192
+ /**
3193
+ * Execute the task request with Promise
3194
+ */
3195
+ async request() {
3196
+ if (this.options.requestParams) {
3197
+ Object.assign(this.params, this.options.requestParams);
3198
+ }
3199
+ if (this._service) {
3200
+ return this._service.requestWithCallback('POST', this.path, this.params);
3201
+ }
3202
+ return new Promise((resolve, reject) => {
3203
+ this._request('POST', this.path, this.params, (error, response) => {
3204
+ if (error) {
3205
+ reject(error);
3206
+ } else {
3207
+ resolve(response);
3208
+ }
3209
+ });
3210
+ });
3211
+ }
3212
+ /**
3213
+ * Direct HTTP request (when not using a service)
3214
+ */
3215
+ _request(method, path, params, callback) {
3216
+ if (!this.options.url) {
3217
+ callback(new Error('URL is required for task execution'));
3218
+ return;
3219
+ }
3220
+ // Ensure proper URL construction with path separator
3221
+ const baseUrl = this.options.url.endsWith('/') ? this.options.url.slice(0, -1) : this.options.url;
3222
+ const cleanPath = path.startsWith('/') ? path : `/${path}`;
3223
+ const fullServiceUrl = `${baseUrl}${cleanPath}`;
3224
+ const url = this.options.proxy ? `${this.options.proxy}?${fullServiceUrl}` : fullServiceUrl;
3225
+ // Convert params to URLSearchParams
3226
+ const searchParams = new URLSearchParams();
3227
+ Object.keys(params).forEach(key => {
3228
+ const value = params[key];
3229
+ if (value !== undefined && value !== null) {
3230
+ if (Array.isArray(value)) {
3231
+ searchParams.append(key, value.join(','));
3232
+ } else if (typeof value === 'object') {
3233
+ searchParams.append(key, JSON.stringify(value));
3234
+ } else {
3235
+ searchParams.append(key, value.toString());
3236
+ }
3237
+ }
3238
+ });
3239
+ const fullUrl = method === 'GET' ? `${url}?${searchParams.toString()}` : url;
3240
+ const fetchOptions = {
3241
+ method,
3242
+ headers: {
3243
+ 'Content-Type': 'application/x-www-form-urlencoded'
3244
+ }
3245
+ };
3246
+ if (method === 'POST') {
3247
+ fetchOptions.body = searchParams.toString();
3248
+ }
3249
+ fetch(fullUrl, fetchOptions).then(response => {
3250
+ if (!response.ok) {
3251
+ throw new Error(`HTTP error! status: ${response.status}`);
3252
+ }
3253
+ return response.json();
3254
+ }).then(data => {
3255
+ // Check for AGOL JSON-level errors (HTTP 200 with error body)
3256
+ if (data && typeof data === 'object' && data.error) {
3257
+ const err = new Error(data.error.message || 'ArcGIS service error');
3258
+ err.code = data.error.code;
3259
+ err.details = data.error.details;
3260
+ callback(err);
3261
+ return;
3262
+ }
3263
+ callback(undefined, data);
3264
+ }).catch(error => callback(error));
3265
+ }
3266
+ }
3267
+
3268
+ /**
3269
+ * Query task for ArcGIS Feature Services
3270
+ * Based on Esri Leaflet's Query functionality
3271
+ */
3272
+ class Query extends Task {
3273
+ constructor(options) {
3274
+ super(options);
3275
+ Object.defineProperty(this, "setters", {
3276
+ enumerable: true,
3277
+ configurable: true,
3278
+ writable: true,
3279
+ value: {
3280
+ offset: 'resultOffset',
3281
+ limit: 'resultRecordCount',
3282
+ fields: 'outFields',
3283
+ precision: 'geometryPrecision',
3284
+ featureIds: 'objectIds',
3285
+ returnGeometry: 'returnGeometry',
3286
+ returnM: 'returnM',
3287
+ transform: 'datumTransformation',
3288
+ token: 'token'
3289
+ }
3290
+ });
3291
+ Object.defineProperty(this, "path", {
3292
+ enumerable: true,
3293
+ configurable: true,
3294
+ writable: true,
3295
+ value: 'query'
3296
+ });
3297
+ Object.defineProperty(this, "params", {
3298
+ enumerable: true,
3299
+ configurable: true,
3300
+ writable: true,
3301
+ value: {
3302
+ returnGeometry: true,
3303
+ where: '1=1',
3304
+ outSR: 4326,
3305
+ outFields: '*',
3306
+ f: 'json'
3307
+ }
3308
+ });
3309
+ this.path = 'query';
3310
+ // If options is a QueryOptions object, merge relevant properties into params
3311
+ if (options && typeof options === 'object' && !('request' in options) && typeof options !== 'string') {
3312
+ const queryOptions = options;
3313
+ // Merge query-specific options into params
3314
+ if (queryOptions.where !== undefined) this.params.where = queryOptions.where;
3315
+ if (queryOptions.outFields !== undefined) this.params.outFields = queryOptions.outFields;
3316
+ if (queryOptions.returnGeometry !== undefined) this.params.returnGeometry = queryOptions.returnGeometry;
3317
+ if (queryOptions.spatialRel !== undefined) this.params.spatialRel = queryOptions.spatialRel;
3318
+ if (queryOptions.geometry !== undefined) this.params.geometry = queryOptions.geometry;
3319
+ if (queryOptions.geometryType !== undefined) this.params.geometryType = queryOptions.geometryType;
3320
+ if (queryOptions.inSR !== undefined) this.params.inSR = queryOptions.inSR;
3321
+ if (queryOptions.outSR !== undefined) this.params.outSR = queryOptions.outSR;
3322
+ if (queryOptions.returnDistinctValues !== undefined) this.params.returnDistinctValues = queryOptions.returnDistinctValues;
3323
+ if (queryOptions.returnIdsOnly !== undefined) this.params.returnIdsOnly = queryOptions.returnIdsOnly;
3324
+ if (queryOptions.returnCountOnly !== undefined) this.params.returnCountOnly = queryOptions.returnCountOnly;
3325
+ if (queryOptions.returnExtentOnly !== undefined) this.params.returnExtentOnly = queryOptions.returnExtentOnly;
3326
+ if (queryOptions.orderByFields !== undefined) this.params.orderByFields = queryOptions.orderByFields;
3327
+ if (queryOptions.groupByFieldsForStatistics !== undefined) this.params.groupByFieldsForStatistics = queryOptions.groupByFieldsForStatistics;
3328
+ if (queryOptions.outStatistics !== undefined) this.params.outStatistics = queryOptions.outStatistics;
3329
+ if (queryOptions.resultOffset !== undefined) this.params.resultOffset = queryOptions.resultOffset;
3330
+ if (queryOptions.resultRecordCount !== undefined) this.params.resultRecordCount = queryOptions.resultRecordCount;
3331
+ if (queryOptions.maxAllowableOffset !== undefined) this.params.maxAllowableOffset = queryOptions.maxAllowableOffset;
3332
+ if (queryOptions.geometryPrecision !== undefined) this.params.geometryPrecision = queryOptions.geometryPrecision;
3333
+ if (queryOptions.time !== undefined) this.params.time = queryOptions.time;
3334
+ if (queryOptions.gdbVersion !== undefined) this.params.gdbVersion = queryOptions.gdbVersion;
3335
+ if (queryOptions.historicMoment !== undefined) this.params.historicMoment = queryOptions.historicMoment;
3336
+ if (queryOptions.returnTrueCurves !== undefined) this.params.returnTrueCurves = queryOptions.returnTrueCurves;
3337
+ if (queryOptions.returnZ !== undefined) this.params.returnZ = queryOptions.returnZ;
3338
+ if (queryOptions.returnM !== undefined) this.params.returnM = queryOptions.returnM;
3339
+ if (queryOptions.token !== undefined) this.params.token = queryOptions.token;
3340
+ }
3341
+ }
3342
+ // Spatial relationship methods
3343
+ /**
3344
+ * Returns a feature if its shape is wholly contained within the search geometry
3345
+ */
3346
+ within(geometry) {
3347
+ this._setGeometryParams(geometry);
3348
+ this.params.spatialRel = 'esriSpatialRelContains';
3349
+ return this;
3350
+ }
3351
+ /**
3352
+ * Returns a feature if any spatial relationship is found
3353
+ */
3354
+ intersects(geometry) {
3355
+ this._setGeometryParams(geometry);
3356
+ this.params.spatialRel = 'esriSpatialRelIntersects';
3357
+ return this;
3358
+ }
3359
+ /**
3360
+ * Returns a feature if its shape wholly contains the search geometry
3361
+ */
3362
+ contains(geometry) {
3363
+ this._setGeometryParams(geometry);
3364
+ this.params.spatialRel = 'esriSpatialRelWithin';
3365
+ return this;
3366
+ }
3367
+ /**
3368
+ * Returns a feature if the intersection of the interiors is not empty and has lower dimension
3369
+ */
3370
+ crosses(geometry) {
3371
+ this._setGeometryParams(geometry);
3372
+ this.params.spatialRel = 'esriSpatialRelCrosses';
3373
+ return this;
3374
+ }
3375
+ /**
3376
+ * Returns a feature if the two shapes share a common boundary
3377
+ */
3378
+ touches(geometry) {
3379
+ this._setGeometryParams(geometry);
3380
+ this.params.spatialRel = 'esriSpatialRelTouches';
3381
+ return this;
3382
+ }
3383
+ /**
3384
+ * Returns a feature if the intersection results in same dimension but different from both shapes
3385
+ */
3386
+ overlaps(geometry) {
3387
+ this._setGeometryParams(geometry);
3388
+ this.params.spatialRel = 'esriSpatialRelOverlaps';
3389
+ return this;
3390
+ }
3391
+ /**
3392
+ * Returns a feature if the envelope of the two shapes intersects
3393
+ */
3394
+ bboxIntersects(geometry) {
3395
+ this._setGeometryParams(geometry);
3396
+ this.params.spatialRel = 'esriSpatialRelEnvelopeIntersects';
3397
+ return this;
3398
+ }
3399
+ /**
3400
+ * Nearby search - only valid for ArcGIS Server 10.3+ or ArcGIS Online
3401
+ */
3402
+ nearby(latlng, radius) {
3403
+ this.params.geometry = [latlng.lng, latlng.lat];
3404
+ this.params.geometryType = 'esriGeometryPoint';
3405
+ this.params.spatialRel = 'esriSpatialRelIntersects';
3406
+ this.params.units = 'esriSRUnit_Meter';
3407
+ this.params.distance = radius;
3408
+ this.params.inSR = 4326;
3409
+ return this;
3410
+ }
3411
+ // Query methods
3412
+ /**
3413
+ * Set WHERE clause for the query
3414
+ */
3415
+ where(whereClause) {
3416
+ this.params.where = whereClause;
3417
+ return this;
3418
+ }
3419
+ /**
3420
+ * Set time range for temporal queries
3421
+ */
3422
+ between(start, end) {
3423
+ const startTime = start instanceof Date ? start.valueOf() : start;
3424
+ const endTime = end instanceof Date ? end.valueOf() : end;
3425
+ this.params.time = [startTime, endTime];
3426
+ return this;
3427
+ }
3428
+ /**
3429
+ * Simplify geometries based on map resolution
3430
+ */
3431
+ simplify(map, factor) {
3432
+ const bounds = map.getBounds();
3433
+ const mapWidth = Math.abs(bounds._northEast.lng - bounds._southWest.lng);
3434
+ this.params.maxAllowableOffset = mapWidth / map.getSize().x * factor;
3435
+ return this;
3436
+ }
3437
+ /**
3438
+ * Set order by fields
3439
+ */
3440
+ orderBy(fieldName, order = 'ASC') {
3441
+ const currentOrder = this.params.orderByFields || '';
3442
+ this.params.orderByFields = currentOrder ? `${currentOrder},${fieldName} ${order}` : `${fieldName} ${order}`;
3443
+ return this;
3444
+ }
3445
+ /**
3446
+ * Set specific layer to query (for Map Services)
3447
+ */
3448
+ layer(layerId) {
3449
+ this.path = `${layerId}/query`;
3450
+ return this;
3451
+ }
3452
+ /**
3453
+ * Return only distinct values
3454
+ */
3455
+ distinct() {
3456
+ this.params.returnGeometry = false;
3457
+ this.params.returnDistinctValues = true;
3458
+ return this;
3459
+ }
3460
+ /**
3461
+ * Set pixel size for image services
3462
+ */
3463
+ pixelSize(point) {
3464
+ this.params.pixelSize = [point.x, point.y];
3465
+ return this;
3466
+ }
3467
+ // Execution methods
3468
+ /**
3469
+ * Execute the query and return features
3470
+ */
3471
+ async run() {
3472
+ this._cleanParams();
3473
+ // Use GeoJSON format if supported
3474
+ this.params.f = 'geojson';
3475
+ try {
3476
+ const response = await this.request();
3477
+ return response;
3478
+ } catch {
3479
+ // Fallback to JSON format and convert
3480
+ this.params.f = 'json';
3481
+ const response = await this.request();
3482
+ return this._convertToGeoJSON(response);
3483
+ }
3484
+ }
3485
+ /**
3486
+ * Execute the query with automatic pagination, collecting all pages of results.
3487
+ * Loops while exceededTransferLimit is true, incrementing resultOffset.
3488
+ */
3489
+ async runAll(options) {
3490
+ const maxPages = options?.maxPages ?? 100;
3491
+ const allFeatures = [];
3492
+ let page = 0;
3493
+ let offset = this.params.resultOffset || 0;
3494
+ while (page < maxPages) {
3495
+ this.params.resultOffset = offset;
3496
+ this._cleanParams();
3497
+ this.params.f = 'geojson';
3498
+ const response = await this.request();
3499
+ const features = response.features || [];
3500
+ allFeatures.push(...features);
3501
+ if (!response.exceededTransferLimit || features.length === 0) {
3502
+ break;
3503
+ }
3504
+ offset += features.length;
3505
+ page++;
3506
+ }
3507
+ // Reset offset
3508
+ delete this.params.resultOffset;
3509
+ return {
3510
+ type: 'FeatureCollection',
3511
+ features: allFeatures
3512
+ };
3513
+ }
3514
+ /**
3515
+ * Execute the query and return only the count
3516
+ */
3517
+ async count() {
3518
+ this._cleanParams();
3519
+ this.params.returnCountOnly = true;
3520
+ const response = await this.request();
3521
+ return response.count;
3522
+ }
3523
+ /**
3524
+ * Execute the query and return only feature IDs
3525
+ */
3526
+ async ids() {
3527
+ this._cleanParams();
3528
+ this.params.returnIdsOnly = true;
3529
+ const response = await this.request();
3530
+ return response.objectIds || response.globalIds || [];
3531
+ }
3532
+ /**
3533
+ * Execute the query and return extent bounds (ArcGIS Server 10.3+)
3534
+ */
3535
+ async bounds() {
3536
+ this._cleanParams();
3537
+ this.params.returnExtentOnly = true;
3538
+ const response = await this.request();
3539
+ if (!response.extent) {
3540
+ throw new Error('Invalid bounds returned');
3541
+ }
3542
+ return {
3543
+ _southWest: {
3544
+ lat: response.extent.ymin,
3545
+ lng: response.extent.xmin
3546
+ },
3547
+ _northEast: {
3548
+ lat: response.extent.ymax,
3549
+ lng: response.extent.xmax
3550
+ }
3551
+ };
3552
+ }
3553
+ // Private methods
3554
+ _setGeometryParams(geometry) {
3555
+ this.params.inSR = 4326;
3556
+ const converted = this._setGeometry(geometry);
3557
+ this.params.geometry = converted.geometry;
3558
+ this.params.geometryType = converted.geometryType;
3559
+ }
3560
+ _setGeometry(geometry) {
3561
+ if (!geometry) {
3562
+ return {
3563
+ geometry: null,
3564
+ geometryType: 'esriGeometryPoint'
3565
+ };
3566
+ }
3567
+ // Handle different geometry types
3568
+ if (typeof geometry === 'object' && geometry !== null) {
3569
+ const geom = geometry;
3570
+ if ('lat' in geom && 'lng' in geom) {
3571
+ // Leaflet LatLng-like object
3572
+ return {
3573
+ geometry: {
3574
+ x: geom.lng,
3575
+ y: geom.lat,
3576
+ spatialReference: {
3577
+ wkid: 4326
3578
+ }
3579
+ },
3580
+ geometryType: 'esriGeometryPoint'
3581
+ };
3582
+ }
3583
+ if ('_southWest' in geom && '_northEast' in geom) {
3584
+ // Leaflet Bounds-like object
3585
+ const bounds = geom;
3586
+ return {
3587
+ geometry: {
3588
+ xmin: bounds._southWest.lng,
3589
+ ymin: bounds._southWest.lat,
3590
+ xmax: bounds._northEast.lng,
3591
+ ymax: bounds._northEast.lat,
3592
+ spatialReference: {
3593
+ wkid: 4326
3594
+ }
3595
+ },
3596
+ geometryType: 'esriGeometryEnvelope'
3597
+ };
3598
+ }
3599
+ if ('xmin' in geom && 'ymin' in geom && 'xmax' in geom && 'ymax' in geom) {
3600
+ // Esri envelope geometry
3601
+ return {
3602
+ geometry,
3603
+ geometryType: 'esriGeometryEnvelope'
3604
+ };
3605
+ }
3606
+ }
3607
+ // Default: assume it's already in Esri geometry format
3608
+ return {
3609
+ geometry,
3610
+ geometryType: 'esriGeometryPoint'
3611
+ };
3612
+ }
3613
+ _cleanParams() {
3614
+ delete this.params.returnIdsOnly;
3615
+ delete this.params.returnExtentOnly;
3616
+ delete this.params.returnCountOnly;
3617
+ delete this.params.returnDistinctValues;
3618
+ }
3619
+ _convertToGeoJSON(response) {
3620
+ // Handle cases where features might be undefined or empty
3621
+ const features = (response.features || []).map(feature => ({
3622
+ type: 'Feature',
3623
+ properties: feature.attributes,
3624
+ geometry: feature.geometry || null
3625
+ }));
3626
+ return {
3627
+ type: 'FeatureCollection',
3628
+ features
3629
+ };
3630
+ }
3631
+ }
3632
+ function query(options) {
3633
+ return new Query(options);
3634
+ }
3635
+
3636
+ /**
3637
+ * Find task for searching text in ArcGIS Map Services
3638
+ * Based on Esri Leaflet's Find functionality
3639
+ */
3640
+ class Find extends Task {
3641
+ constructor(options) {
3642
+ super(options);
3643
+ Object.defineProperty(this, "setters", {
3644
+ enumerable: true,
3645
+ configurable: true,
3646
+ writable: true,
3647
+ value: {
3648
+ contains: 'contains',
3649
+ text: 'searchText',
3650
+ fields: 'searchFields',
3651
+ spatialReference: 'sr',
3652
+ sr: 'sr',
3653
+ layers: 'layers',
3654
+ returnGeometry: 'returnGeometry',
3655
+ maxAllowableOffset: 'maxAllowableOffset',
3656
+ precision: 'geometryPrecision',
3657
+ dynamicLayers: 'dynamicLayers',
3658
+ returnZ: 'returnZ',
3659
+ returnM: 'returnM',
3660
+ gdbVersion: 'gdbVersion',
3661
+ token: 'token'
3662
+ }
3663
+ });
3664
+ Object.defineProperty(this, "path", {
3665
+ enumerable: true,
3666
+ configurable: true,
3667
+ writable: true,
3668
+ value: 'find'
3669
+ });
3670
+ Object.defineProperty(this, "params", {
3671
+ enumerable: true,
3672
+ configurable: true,
3673
+ writable: true,
3674
+ value: {
3675
+ searchText: '',
3676
+ // Required parameter
3677
+ layers: 'all',
3678
+ // Can be 'all' or comma-separated layer IDs
3679
+ contains: true,
3680
+ returnGeometry: true,
3681
+ f: 'json'
3682
+ }
3683
+ });
3684
+ this.path = 'find';
3685
+ // If options is a FindOptions object, merge relevant properties into params
3686
+ if (options && typeof options === 'object' && !('request' in options) && typeof options !== 'string') {
3687
+ const findOptions = options;
3688
+ // Merge find-specific options into params
3689
+ if (findOptions.searchText !== undefined) this.params.searchText = findOptions.searchText;
3690
+ if (findOptions.contains !== undefined) this.params.contains = findOptions.contains;
3691
+ if (findOptions.searchFields !== undefined) {
3692
+ this.params.searchFields = Array.isArray(findOptions.searchFields) ? findOptions.searchFields.join(',') : findOptions.searchFields;
3693
+ }
3694
+ if (findOptions.sr !== undefined) this.params.sr = findOptions.sr;
3695
+ if (findOptions.layers !== undefined) {
3696
+ // Convert array to comma-separated string or use as-is if already string
3697
+ if (Array.isArray(findOptions.layers)) {
3698
+ this.params.layers = findOptions.layers.join(',');
3699
+ } else if (typeof findOptions.layers === 'string') {
3700
+ this.params.layers = findOptions.layers;
3701
+ } else {
3702
+ this.params.layers = findOptions.layers.toString();
3703
+ }
3704
+ }
3705
+ if (findOptions.returnGeometry !== undefined) this.params.returnGeometry = findOptions.returnGeometry;
3706
+ if (findOptions.maxAllowableOffset !== undefined) this.params.maxAllowableOffset = findOptions.maxAllowableOffset;
3707
+ if (findOptions.geometryPrecision !== undefined) this.params.geometryPrecision = findOptions.geometryPrecision;
3708
+ if (findOptions.dynamicLayers !== undefined) this.params.dynamicLayers = findOptions.dynamicLayers;
3709
+ if (findOptions.returnZ !== undefined) this.params.returnZ = findOptions.returnZ;
3710
+ if (findOptions.returnM !== undefined) this.params.returnM = findOptions.returnM;
3711
+ if (findOptions.gdbVersion !== undefined) this.params.gdbVersion = findOptions.gdbVersion;
3712
+ if (findOptions.layerDefs !== undefined) this.params.layerDefs = findOptions.layerDefs;
3713
+ if (findOptions.token !== undefined) this.params.token = findOptions.token;
3714
+ }
3715
+ }
3716
+ /**
3717
+ * Set the text to search for
3718
+ */
3719
+ text(searchText) {
3720
+ this.params.searchText = searchText;
3721
+ return this;
3722
+ }
3723
+ /**
3724
+ * Set the fields to search in
3725
+ */
3726
+ fields(fields) {
3727
+ this.params.searchFields = Array.isArray(fields) ? fields.join(',') : fields;
3728
+ return this;
3729
+ }
3730
+ /**
3731
+ * Set whether the search should contain the text (partial match) or exact match
3732
+ */
3733
+ contains(contains) {
3734
+ this.params.contains = contains;
3735
+ return this;
3736
+ }
3737
+ /**
3738
+ * Set which layers to search in
3739
+ */
3740
+ layers(layers) {
3741
+ if (Array.isArray(layers)) {
3742
+ this.params.layers = layers.join(',');
3743
+ } else {
3744
+ this.params.layers = layers;
3745
+ }
3746
+ return this;
3747
+ }
3748
+ /**
3749
+ * Set layer definitions for filtering specific layers
3750
+ */
3751
+ layerDefs(layerId, whereClause) {
3752
+ const currentLayerDefs = this.params.layerDefs || '';
3753
+ this.params.layerDefs = currentLayerDefs ? `${currentLayerDefs};${layerId}:${whereClause}` : `${layerId}:${whereClause}`;
3754
+ return this;
3755
+ }
3756
+ /**
3757
+ * Simplify geometries based on map resolution
3758
+ */
3759
+ simplify(map, factor) {
3760
+ const bounds = map.getBounds();
3761
+ const mapWidth = Math.abs(bounds.getWest() - bounds.getEast());
3762
+ this.params.maxAllowableOffset = mapWidth / map.getSize().x * factor;
3763
+ return this;
3764
+ }
3765
+ /**
3766
+ * Execute the find operation
3767
+ */
3768
+ async run() {
3769
+ // Always use JSON format for Find API (GeoJSON might not be supported)
3770
+ this.params.f = 'json';
3771
+ try {
3772
+ const response = await this.request();
3773
+ return this._convertToGeoJSON(response);
3774
+ } catch (error) {
3775
+ const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
3776
+ if (!isTestEnvironment) {
3777
+ console.error('Find task error:', error);
3778
+ }
3779
+ throw error;
3780
+ }
3781
+ }
3782
+ _convertToGeoJSON(response) {
3783
+ // Handle cases where response is null or results might be undefined, null, or empty
3784
+ const results = response?.results || [];
3785
+ const features = results.map(result => ({
3786
+ type: 'Feature',
3787
+ properties: {
3788
+ ...result.attributes,
3789
+ layerId: result.layerId,
3790
+ layerName: result.layerName,
3791
+ foundFieldName: result.foundFieldName,
3792
+ value: result.value
3793
+ },
3794
+ geometry: this._convertEsriGeometry(result.geometry)
3795
+ }));
3796
+ return {
3797
+ type: 'FeatureCollection',
3798
+ features
3799
+ };
3800
+ }
3801
+ _convertEsriGeometry(esriGeom) {
3802
+ if (!esriGeom || typeof esriGeom !== 'object') return null;
3803
+ const geom = esriGeom;
3804
+ // Point geometry
3805
+ if ('x' in geom && 'y' in geom) {
3806
+ return {
3807
+ type: 'Point',
3808
+ coordinates: [geom.x, geom.y]
3809
+ };
3810
+ }
3811
+ // Polygon geometry
3812
+ if ('rings' in geom && Array.isArray(geom.rings)) {
3813
+ return {
3814
+ type: 'Polygon',
3815
+ coordinates: geom.rings
3816
+ };
3817
+ }
3818
+ // Polyline geometry
3819
+ if ('paths' in geom && Array.isArray(geom.paths)) {
3820
+ const paths = geom.paths;
3821
+ if (paths.length === 1) {
3822
+ // Single path = LineString
3823
+ return {
3824
+ type: 'LineString',
3825
+ coordinates: paths[0]
3826
+ };
3827
+ } else {
3828
+ // Multiple paths = MultiLineString
3829
+ return {
3830
+ type: 'MultiLineString',
3831
+ coordinates: paths
3832
+ };
3833
+ }
3834
+ }
3835
+ // Default: return null for unknown geometry types
3836
+ return null;
3837
+ }
3838
+ }
3839
+ function find(options) {
3840
+ return new Find(options);
3841
+ }
3842
+
3843
+ /**
3844
+ * IdentifyFeatures task for performing identify operations against ArcGIS Map Services
3845
+ * Similar to Esri Leaflet's identifyFeatures functionality
3846
+ */
3847
+ // Scale constant: at zoom 0 and 96 DPI, the Web Mercator scale denominator
3848
+ const SCALE_AT_ZOOM_0 = 559082264.028;
3849
+ class IdentifyFeatures extends Task {
3850
+ constructor(options) {
3851
+ // Handle different input types and convert to TaskOptions format
3852
+ let taskOptions;
3853
+ if (typeof options === 'string') {
3854
+ taskOptions = options;
3855
+ } else if (options instanceof Service) {
3856
+ // Extract URL from Service instance
3857
+ taskOptions = {
3858
+ url: options.options.url
3859
+ };
3860
+ } else {
3861
+ // It's IdentifyFeaturesOptions, use as TaskOptions
3862
+ taskOptions = options;
3863
+ }
3864
+ super(taskOptions);
3865
+ Object.defineProperty(this, "setters", {
3866
+ enumerable: true,
3867
+ configurable: true,
3868
+ writable: true,
3869
+ value: {
3870
+ layers: 'layers',
3871
+ precision: 'geometryPrecision',
3872
+ tolerance: 'tolerance',
3873
+ returnGeometry: 'returnGeometry'
3874
+ }
3875
+ });
3876
+ Object.defineProperty(this, "path", {
3877
+ enumerable: true,
3878
+ configurable: true,
3879
+ writable: true,
3880
+ value: '/identify'
3881
+ });
3882
+ Object.defineProperty(this, "params", {
3883
+ enumerable: true,
3884
+ configurable: true,
3885
+ writable: true,
3886
+ value: {
3887
+ sr: 4326,
3888
+ layers: 'all',
3889
+ tolerance: 3,
3890
+ returnGeometry: true,
3891
+ f: 'json'
3892
+ }
3893
+ });
3894
+ Object.defineProperty(this, "_map", {
3895
+ enumerable: true,
3896
+ configurable: true,
3897
+ writable: true,
3898
+ value: null
3899
+ });
3900
+ Object.defineProperty(this, "_layerScaleRanges", {
3901
+ enumerable: true,
3902
+ configurable: true,
3903
+ writable: true,
3904
+ value: {}
3905
+ });
3906
+ this.path = '/identify';
3907
+ // Ensure dynamic setters are available even though subclass fields
3908
+ // are initialized after super() runs. This rebinds setter methods.
3909
+ if (this.setters) {
3910
+ for (const [method, param] of Object.entries(this.setters)) {
3911
+ this[method] = this.generateSetter(param, this);
3912
+ }
3913
+ }
3914
+ }
3915
+ /**
3916
+ * Perform identify operation at a point location
3917
+ */
3918
+ at(point) {
3919
+ let geometry;
3920
+ if (Array.isArray(point)) {
3921
+ geometry = {
3922
+ x: point[0],
3923
+ y: point[1],
3924
+ spatialReference: {
3925
+ wkid: 4326
3926
+ }
3927
+ };
3928
+ } else {
3929
+ geometry = {
3930
+ x: point.lng,
3931
+ y: point.lat,
3932
+ spatialReference: {
3933
+ wkid: 4326
3934
+ }
3935
+ };
3936
+ }
3937
+ this.params.geometry = JSON.stringify(geometry);
3938
+ this.params.geometryType = 'esriGeometryPoint';
3939
+ return this;
3940
+ }
3941
+ // Strongly-typed chainable setters for common Identify params
3942
+ layers(value) {
3943
+ this.params.layers = value;
3944
+ return this;
3945
+ }
3946
+ tolerance(value) {
3947
+ this.params.tolerance = value;
3948
+ return this;
3949
+ }
3950
+ returnGeometry(value) {
3951
+ this.params.returnGeometry = value;
3952
+ return this;
3953
+ }
3954
+ precision(value) {
3955
+ this.params.geometryPrecision = value;
3956
+ return this;
3957
+ }
3958
+ /**
3959
+ * Set the map extent and image display for the identify operation.
3960
+ * Also stores the map reference for zoom-based scale filtering.
3961
+ */
3962
+ on(map) {
3963
+ this._map = map;
3964
+ try {
3965
+ const bounds = map.getBounds().toArray();
3966
+ this.params.mapExtent = [bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1]].join(',');
3967
+ const canvas = map.getCanvas();
3968
+ this.params.imageDisplay = [canvas.width, canvas.height, 96].join(',');
3969
+ } catch (error) {
3970
+ console.warn('Could not extract map extent and display info:', error);
3971
+ }
3972
+ return this;
3973
+ }
3974
+ /**
3975
+ * Set scale ranges for layers so the identify can skip layers not visible at the current zoom.
3976
+ * ArcGIS uses minScale (zoomed out limit) and maxScale (zoomed in limit).
3977
+ * A value of 0 means no limit.
3978
+ *
3979
+ * @example
3980
+ * identify.layerScales({
3981
+ * 0: { minScale: 1000000, maxScale: 0 }, // Cities: visible below 1:1M
3982
+ * 1: { minScale: 5000000, maxScale: 500000 }, // Highways: visible between 1:5M and 1:500K
3983
+ * 2: { minScale: 0, maxScale: 0 }, // States: always visible
3984
+ * });
3985
+ */
3986
+ layerScales(scales) {
3987
+ this._layerScaleRanges = scales;
3988
+ return this;
3989
+ }
3990
+ /**
3991
+ * Set layer definitions for filtering specific layers
3992
+ */
3993
+ layerDef(layerId, whereClause) {
3994
+ const currentLayerDefs = this.params.layerDefs || '';
3995
+ this.params.layerDefs = currentLayerDefs ? `${currentLayerDefs};${layerId}:${whereClause}` : `${layerId}:${whereClause}`;
3996
+ return this;
3997
+ }
3998
+ /**
3999
+ * Simplify geometries based on map resolution
4000
+ */
4001
+ simplify(map, factor) {
4002
+ const bounds = map.getBounds();
4003
+ const mapWidth = Math.abs(bounds.getWest() - bounds.getEast());
4004
+ this.params.maxAllowableOffset = mapWidth / map.getSize().x * factor;
4005
+ return this;
4006
+ }
4007
+ /**
4008
+ * Get the current map scale from zoom level.
4009
+ * Uses the standard Web Mercator scale formula at 96 DPI.
4010
+ */
4011
+ _getMapScale() {
4012
+ if (!this._map || typeof this._map.getZoom !== 'function') return null;
4013
+ const zoom = this._map.getZoom();
4014
+ return SCALE_AT_ZOOM_0 / Math.pow(2, zoom);
4015
+ }
4016
+ /**
4017
+ * Check if a layer is visible at the given scale.
4018
+ * ArcGIS convention: minScale = most zoomed out (largest number), maxScale = most zoomed in (smallest number).
4019
+ * A value of 0 means no limit in that direction.
4020
+ */
4021
+ _isLayerVisibleAtScale(layerId, scale) {
4022
+ const range = this._layerScaleRanges[layerId];
4023
+ if (!range) return true; // No scale info means always visible
4024
+ if (range.minScale && scale > range.minScale) return false; // Too zoomed out
4025
+ if (range.maxScale && scale < range.maxScale) return false; // Too zoomed in
4026
+ return true;
4027
+ }
4028
+ /**
4029
+ * Filter the layers parameter to only include layers visible at the current scale.
4030
+ * Returns false if no layers are visible (request should be skipped).
4031
+ */
4032
+ _filterLayersByScale() {
4033
+ const scale = this._getMapScale();
4034
+ if (scale === null || Object.keys(this._layerScaleRanges).length === 0) return true;
4035
+ const layersParam = this.params.layers;
4036
+ if (typeof layersParam !== 'string') return true;
4037
+ // Parse layers string: "all", "visible:0,1,2", "top", "visible", "0,1,2"
4038
+ const match = layersParam.match(/^(all|top|visible)(?::(.+))?$/);
4039
+ if (!match) return true;
4040
+ const prefix = match[1];
4041
+ const idsPart = match[2];
4042
+ // If no specific IDs, filter all known layer IDs from scale ranges
4043
+ let layerIds;
4044
+ if (idsPart) {
4045
+ layerIds = idsPart.split(',').map(Number).filter(n => !isNaN(n));
4046
+ } else {
4047
+ layerIds = Object.keys(this._layerScaleRanges).map(Number);
4048
+ }
4049
+ const visibleIds = layerIds.filter(id => this._isLayerVisibleAtScale(id, scale));
4050
+ if (visibleIds.length === 0) return false;
4051
+ // Update layers param with only visible IDs
4052
+ this.params.layers = `${prefix}:${visibleIds.join(',')}`;
4053
+ return true;
4054
+ }
4055
+ /**
4056
+ * Execute the identify operation.
4057
+ * If layer scale ranges are set via `layerScales()` and a map is provided via `on()`,
4058
+ * layers not visible at the current zoom level will be excluded. If no layers are visible,
4059
+ * an empty FeatureCollection is returned without making a network request.
4060
+ */
4061
+ async run() {
4062
+ // Skip request entirely if no layers are visible at the current scale
4063
+ if (!this._filterLayersByScale()) {
4064
+ return {
4065
+ type: 'FeatureCollection',
4066
+ features: []
4067
+ };
4068
+ }
4069
+ try {
4070
+ const response = await this.request();
4071
+ return this._convertToGeoJSON(response);
4072
+ } catch (error) {
4073
+ const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
4074
+ if (!isTestEnvironment) {
4075
+ console.error('IdentifyFeatures error:', error);
4076
+ }
4077
+ throw error;
4078
+ }
4079
+ }
4080
+ _convertToGeoJSON(response) {
4081
+ const features = (response.results || []).map(result => {
4082
+ const feature = {
4083
+ type: 'Feature',
4084
+ properties: {
4085
+ ...result.attributes,
4086
+ layerId: result.layerId,
4087
+ layerName: result.layerName,
4088
+ displayFieldName: result.displayFieldName,
4089
+ value: result.value
4090
+ },
4091
+ geometry: result.geometry || null
4092
+ };
4093
+ // Add layerId as a custom property for easier identification
4094
+ const featureWithLayerId = feature;
4095
+ featureWithLayerId.layerId = result.layerId;
4096
+ return feature;
4097
+ });
4098
+ return {
4099
+ type: 'FeatureCollection',
4100
+ features
4101
+ };
4102
+ }
4103
+ }
4104
+
4105
+ /**
4106
+ * IdentifyImage task for identifying pixel values in ArcGIS Image Services
4107
+ * Based on Esri Leaflet's IdentifyImage functionality
4108
+ */
4109
+ class IdentifyImage extends Task {
4110
+ constructor(options) {
4111
+ super(options);
4112
+ Object.defineProperty(this, "setters", {
4113
+ enumerable: true,
4114
+ configurable: true,
4115
+ writable: true,
4116
+ value: {
4117
+ returnCatalogItems: 'returnCatalogItems',
4118
+ returnGeometry: 'returnGeometry',
4119
+ pixelSize: 'pixelSize',
4120
+ token: 'token'
4121
+ }
4122
+ });
4123
+ Object.defineProperty(this, "path", {
4124
+ enumerable: true,
4125
+ configurable: true,
4126
+ writable: true,
4127
+ value: 'identify'
4128
+ });
4129
+ Object.defineProperty(this, "params", {
4130
+ enumerable: true,
4131
+ configurable: true,
4132
+ writable: true,
4133
+ value: {
4134
+ sr: 4326,
4135
+ returnGeometry: false,
4136
+ returnCatalogItems: false,
4137
+ f: 'json'
4138
+ }
4139
+ });
4140
+ // If options is a IdentifyImageOptions object, merge relevant properties into params
4141
+ if (options && typeof options === 'object' && typeof options !== 'string') {
4142
+ const imageOptions = options;
4143
+ // Merge identify-specific options into params
4144
+ if (imageOptions.geometry !== undefined) this.params.geometry = imageOptions.geometry;
4145
+ if (imageOptions.geometryType !== undefined) this.params.geometryType = imageOptions.geometryType;
4146
+ if (imageOptions.sr !== undefined) this.params.sr = imageOptions.sr;
4147
+ if (imageOptions.mosaic !== undefined) this.params.mosaic = imageOptions.mosaic;
4148
+ if (imageOptions.renderingRules !== undefined) this.params.renderingRules = imageOptions.renderingRules;
4149
+ if (imageOptions.pixelSize !== undefined) this.params.pixelSize = imageOptions.pixelSize;
4150
+ if (imageOptions.returnGeometry !== undefined) this.params.returnGeometry = imageOptions.returnGeometry;
4151
+ if (imageOptions.returnCatalogItems !== undefined) this.params.returnCatalogItems = imageOptions.returnCatalogItems;
4152
+ if (imageOptions.token !== undefined) this.params.token = imageOptions.token;
4153
+ if (imageOptions.f !== undefined) this.params.f = imageOptions.f;
4154
+ }
4155
+ }
4156
+ /**
4157
+ * Identify pixel values at a specific point
4158
+ */
4159
+ at(point) {
4160
+ let geometry;
4161
+ if (Array.isArray(point)) {
4162
+ geometry = {
4163
+ x: point[0],
4164
+ y: point[1],
4165
+ spatialReference: {
4166
+ wkid: 4326
4167
+ }
4168
+ };
4169
+ } else {
4170
+ geometry = {
4171
+ x: point.lng,
4172
+ y: point.lat,
4173
+ spatialReference: {
4174
+ wkid: 4326
4175
+ }
4176
+ };
4177
+ }
4178
+ this.params.geometry = JSON.stringify(geometry);
4179
+ this.params.geometryType = 'esriGeometryPoint';
4180
+ this.params.sr = 4326;
4181
+ return this;
4182
+ }
4183
+ /**
4184
+ * Set custom geometry for identification
4185
+ */
4186
+ geometry(geometry, geometryType) {
4187
+ this.params.geometry = JSON.stringify(geometry);
4188
+ this.params.geometryType = geometryType || 'esriGeometryPoint';
4189
+ return this;
4190
+ }
4191
+ /**
4192
+ * Set pixel size for the identify operation
4193
+ */
4194
+ pixelSize(size) {
4195
+ if (Array.isArray(size)) {
4196
+ this.params.pixelSize = `${size[0]},${size[1]}`;
4197
+ } else {
4198
+ this.params.pixelSize = `${size.x},${size.y}`;
4199
+ }
4200
+ return this;
4201
+ }
4202
+ /**
4203
+ * Set whether to return geometry with results
4204
+ */
4205
+ returnGeometry(returnGeom) {
4206
+ this.params.returnGeometry = returnGeom;
4207
+ return this;
4208
+ }
4209
+ /**
4210
+ * Set whether to return catalog items
4211
+ */
4212
+ returnCatalogItems(returnItems) {
4213
+ this.params.returnCatalogItems = returnItems;
4214
+ return this;
4215
+ }
4216
+ /**
4217
+ * Set mosaic rule for the identify operation
4218
+ */
4219
+ mosaicRule(rule) {
4220
+ this.params.mosaicRule = JSON.stringify(rule);
4221
+ return this;
4222
+ }
4223
+ /**
4224
+ * Set rendering rules for the identify operation
4225
+ */
4226
+ renderingRule(rule) {
4227
+ this.params.renderingRule = JSON.stringify(rule);
4228
+ return this;
4229
+ }
4230
+ /**
4231
+ * Execute the identify operation
4232
+ */
4233
+ async run() {
4234
+ const response = await this.request();
4235
+ // Normalize different response formats
4236
+ let results = [];
4237
+ if (response.results) {
4238
+ results = response.results;
4239
+ } else if (response.value !== undefined || response.values) {
4240
+ // Handle simple value responses
4241
+ results = [{
4242
+ value: response.value || response.values && response.values[0] || '',
4243
+ attributes: response.properties || {}
4244
+ }];
4245
+ }
4246
+ return {
4247
+ results,
4248
+ location: response.location
4249
+ };
4250
+ }
4251
+ /**
4252
+ * Execute and return pixel values as a simple array
4253
+ */
4254
+ async getPixelValues() {
4255
+ const response = await this.run();
4256
+ return response.results.map(result => {
4257
+ if (result.value !== undefined) {
4258
+ const numValue = parseFloat(result.value);
4259
+ return isNaN(numValue) ? result.value : numValue;
4260
+ }
4261
+ return null;
4262
+ });
4263
+ }
4264
+ /**
4265
+ * Execute and return detailed pixel information
4266
+ */
4267
+ async getPixelData() {
4268
+ const response = await this.run();
4269
+ return response.results;
4270
+ }
4271
+ }
4272
+ function identifyImage(options) {
4273
+ return new IdentifyImage(options);
4274
+ }
4275
+
4276
+ export { DynamicMapService as D, FeatureService as F, IdentifyFeatures as I, Query as Q, Service as S, Task as T, VectorBasemapStyle as V, Find as a, IdentifyImage as b, ImageService as c, TiledMapService as d, VectorTileService as e, cleanTrailingSlash as f, find as g, getServiceDetails as h, identifyImage as i, query as q, updateAttribution as u };
4277
+ //# sourceMappingURL=IdentifyImage-DWBF8K_a.js.map