esri-gl 0.9.0 → 0.9.1

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