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.
- package/README.md +375 -35
- package/dist/IdentifyImage-B1oeOjpm.js +4174 -0
- package/dist/IdentifyImage-B1oeOjpm.js.map +1 -0
- package/dist/index-DuO9oU8x.d.ts +1500 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.js +4215 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/package.json +69 -0
- package/dist/react-map-gl.d.ts +101 -0
- package/dist/react-map-gl.js +479 -0
- package/dist/react-map-gl.js.map +1 -0
- package/dist/react.d.ts +114 -0
- package/dist/react.js +406 -0
- package/dist/react.js.map +1 -0
- package/dist/useFeatureService-BSZr2zvH.d.ts +140 -0
- package/dist/useFeatureService-D2kPkS3Y.js +205 -0
- package/dist/useFeatureService-D2kPkS3Y.js.map +1 -0
- package/package.json +103 -47
- package/dist/esri-gl.esm.js +0 -2694
- package/dist/esri-gl.esm.js.map +0 -1
- package/dist/esri-gl.js +0 -2719
- package/dist/esri-gl.js.map +0 -1
- package/dist/esri-gl.min.js +0 -2
- package/dist/esri-gl.min.js.map +0 -1
- package/dist/types/Layers/BasemapLayer.d.ts +0 -30
- package/dist/types/Layers/DynamicMapLayer.d.ts +0 -75
- package/dist/types/Layers/Layer.d.ts +0 -67
- package/dist/types/Layers/RasterLayer.d.ts +0 -71
- package/dist/types/Services/DynamicMapService.d.ts +0 -35
- package/dist/types/Services/FeatureService.d.ts +0 -48
- package/dist/types/Services/ImageService.d.ts +0 -32
- package/dist/types/Services/MapService.d.ts +0 -30
- package/dist/types/Services/MapServiceTypes.d.ts +0 -87
- package/dist/types/Services/Service.d.ts +0 -118
- package/dist/types/Services/SimpleMapService.d.ts +0 -30
- package/dist/types/Services/TiledMapService.d.ts +0 -28
- package/dist/types/Services/VectorBasemapStyle.d.ts +0 -9
- package/dist/types/Services/VectorTileService.d.ts +0 -41
- package/dist/types/Tasks/Find.d.ts +0 -85
- package/dist/types/Tasks/IdentifyFeatures.d.ts +0 -91
- package/dist/types/Tasks/IdentifyImage.d.ts +0 -107
- package/dist/types/Tasks/Query.d.ts +0 -180
- package/dist/types/Tasks/Task.d.ts +0 -50
- package/dist/types/examples/EsriLeafletStyleAPI.d.ts +0 -9
- package/dist/types/main.d.ts +0 -14
- package/dist/types/types.d.ts +0 -88
- 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
|