esri-gl 0.9.0-alpha.10
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/LICENSE +227 -0
- package/README.md +455 -0
- package/dist/esri-gl.esm.js +2694 -0
- package/dist/esri-gl.esm.js.map +1 -0
- package/dist/esri-gl.js +2719 -0
- package/dist/esri-gl.js.map +1 -0
- package/dist/esri-gl.min.js +2 -0
- package/dist/esri-gl.min.js.map +1 -0
- package/dist/types/Services/DynamicMapService.d.ts +35 -0
- package/dist/types/Services/FeatureService.d.ts +48 -0
- package/dist/types/Services/ImageService.d.ts +32 -0
- package/dist/types/Services/MapService.d.ts +30 -0
- package/dist/types/Services/MapServiceTypes.d.ts +87 -0
- package/dist/types/Services/Service.d.ts +118 -0
- package/dist/types/Services/SimpleMapService.d.ts +30 -0
- package/dist/types/Services/TiledMapService.d.ts +28 -0
- package/dist/types/Services/VectorBasemapStyle.d.ts +9 -0
- package/dist/types/Services/VectorTileService.d.ts +41 -0
- package/dist/types/Tasks/Find.d.ts +85 -0
- package/dist/types/Tasks/IdentifyFeatures.d.ts +91 -0
- package/dist/types/Tasks/IdentifyImage.d.ts +107 -0
- package/dist/types/Tasks/Query.d.ts +180 -0
- package/dist/types/Tasks/Task.d.ts +50 -0
- package/dist/types/examples/EsriLeafletStyleAPI.d.ts +8 -0
- package/dist/types/main.d.ts +14 -0
- package/dist/types/types.d.ts +88 -0
- package/dist/types/utils.d.ts +4 -0
- package/package.json +116 -0
package/dist/esri-gl.js
ADDED
|
@@ -0,0 +1,2719 @@
|
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.esrigl = {}));
|
|
5
|
+
})(this, (function (exports) { 'use strict';
|
|
6
|
+
|
|
7
|
+
function cleanTrailingSlash(url) {
|
|
8
|
+
return url.replace(/\/$/, '');
|
|
9
|
+
}
|
|
10
|
+
function getServiceDetails(url, fetchOptions = {}) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
fetch(`${url}?f=json`, fetchOptions)
|
|
13
|
+
.then(response => response.json())
|
|
14
|
+
.then(data => resolve(data))
|
|
15
|
+
.catch(error => reject(error));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const POWERED_BY_ESRI_ATTRIBUTION_STRING = 'Powered by <a href="https://www.esri.com">Esri</a>';
|
|
19
|
+
// This requires hooking into some undocumented properties
|
|
20
|
+
function updateAttribution(newAttribution, sourceId, map) {
|
|
21
|
+
// Accessing undocumented MapLibre/Mapbox internal properties
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
const mapWithControls = map;
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
const attributionController = mapWithControls._controls?.find((c) => '_attribHTML' in c);
|
|
26
|
+
if (!attributionController)
|
|
27
|
+
return;
|
|
28
|
+
const customAttribution = attributionController.options?.customAttribution;
|
|
29
|
+
if (typeof customAttribution === 'string') {
|
|
30
|
+
attributionController.options.customAttribution = `${customAttribution} | ${POWERED_BY_ESRI_ATTRIBUTION_STRING}`;
|
|
31
|
+
}
|
|
32
|
+
else if (customAttribution === undefined) {
|
|
33
|
+
if (attributionController.options) {
|
|
34
|
+
attributionController.options.customAttribution = POWERED_BY_ESRI_ATTRIBUTION_STRING;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (Array.isArray(customAttribution)) {
|
|
38
|
+
if (customAttribution.indexOf(POWERED_BY_ESRI_ATTRIBUTION_STRING) === -1) {
|
|
39
|
+
customAttribution.push(POWERED_BY_ESRI_ATTRIBUTION_STRING);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Accessing undocumented map style properties
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
const mapStyle = map.style;
|
|
45
|
+
if (mapStyle?.sourceCaches?.[sourceId]?._source) {
|
|
46
|
+
mapStyle.sourceCaches[sourceId]._source.attribution = newAttribution;
|
|
47
|
+
}
|
|
48
|
+
else if (mapStyle?._otherSourceCaches?.[sourceId]?._source) {
|
|
49
|
+
mapStyle._otherSourceCaches[sourceId]._source.attribution = newAttribution;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.warn(`Source ${sourceId} not found when trying to update attribution`);
|
|
53
|
+
return; // Don't try to update attributions if source doesn't exist
|
|
54
|
+
}
|
|
55
|
+
// Call undocumented method to update attribution display
|
|
56
|
+
if (attributionController._updateAttributions) {
|
|
57
|
+
attributionController._updateAttributions();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Base Service class for ArcGIS REST API services
|
|
63
|
+
* Similar to Esri Leaflet's Service class
|
|
64
|
+
*/
|
|
65
|
+
class Service {
|
|
66
|
+
constructor(options) {
|
|
67
|
+
Object.defineProperty(this, "options", {
|
|
68
|
+
enumerable: true,
|
|
69
|
+
configurable: true,
|
|
70
|
+
writable: true,
|
|
71
|
+
value: void 0
|
|
72
|
+
});
|
|
73
|
+
Object.defineProperty(this, "_requestQueue", {
|
|
74
|
+
enumerable: true,
|
|
75
|
+
configurable: true,
|
|
76
|
+
writable: true,
|
|
77
|
+
value: []
|
|
78
|
+
});
|
|
79
|
+
Object.defineProperty(this, "_authenticating", {
|
|
80
|
+
enumerable: true,
|
|
81
|
+
configurable: true,
|
|
82
|
+
writable: true,
|
|
83
|
+
value: false
|
|
84
|
+
});
|
|
85
|
+
Object.defineProperty(this, "_serviceMetadata", {
|
|
86
|
+
enumerable: true,
|
|
87
|
+
configurable: true,
|
|
88
|
+
writable: true,
|
|
89
|
+
value: null
|
|
90
|
+
});
|
|
91
|
+
Object.defineProperty(this, "_map", {
|
|
92
|
+
enumerable: true,
|
|
93
|
+
configurable: true,
|
|
94
|
+
writable: true,
|
|
95
|
+
value: void 0
|
|
96
|
+
});
|
|
97
|
+
Object.defineProperty(this, "_eventListeners", {
|
|
98
|
+
enumerable: true,
|
|
99
|
+
configurable: true,
|
|
100
|
+
writable: true,
|
|
101
|
+
value: {}
|
|
102
|
+
});
|
|
103
|
+
if (!options.url) {
|
|
104
|
+
throw new Error('A url must be supplied as part of the service options.');
|
|
105
|
+
}
|
|
106
|
+
this.options = {
|
|
107
|
+
proxy: false,
|
|
108
|
+
useCors: true,
|
|
109
|
+
timeout: 0,
|
|
110
|
+
getAttributionFromService: true,
|
|
111
|
+
...options,
|
|
112
|
+
url: cleanTrailingSlash(options.url),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Make a GET request
|
|
117
|
+
*/
|
|
118
|
+
async get(path, params = {}) {
|
|
119
|
+
return this._request('GET', path, params);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Make a POST request
|
|
123
|
+
*/
|
|
124
|
+
async post(path, params = {}) {
|
|
125
|
+
return this._request('POST', path, params);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Make a generic request
|
|
129
|
+
*/
|
|
130
|
+
async request(path, params = {}) {
|
|
131
|
+
return this._request('GET', path, params);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Make a request with callback support (public for Tasks)
|
|
135
|
+
*/
|
|
136
|
+
requestWithCallback(method, path, params, callback) {
|
|
137
|
+
if (callback) {
|
|
138
|
+
this._requestWithCallback(method, path, params, callback);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
return this._request(method, path, params);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get service metadata
|
|
145
|
+
*/
|
|
146
|
+
async metadata() {
|
|
147
|
+
if (this._serviceMetadata) {
|
|
148
|
+
return this._serviceMetadata;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const response = await this._request('GET', '', { f: 'json' });
|
|
152
|
+
this._serviceMetadata = response;
|
|
153
|
+
return this._serviceMetadata;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
|
|
157
|
+
if (!isTestEnvironment) {
|
|
158
|
+
console.error('Error fetching service metadata:', error);
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Set authentication token
|
|
165
|
+
*/
|
|
166
|
+
authenticate(token) {
|
|
167
|
+
this._authenticating = false;
|
|
168
|
+
this.options.token = token;
|
|
169
|
+
this._runQueue();
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get request timeout
|
|
174
|
+
*/
|
|
175
|
+
getTimeout() {
|
|
176
|
+
return this.options.timeout;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Set request timeout
|
|
180
|
+
*/
|
|
181
|
+
setTimeout(timeout) {
|
|
182
|
+
this.options.timeout = timeout;
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Set attribution from service metadata
|
|
187
|
+
*/
|
|
188
|
+
async setAttributionFromService() {
|
|
189
|
+
if (!this._map) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (this._serviceMetadata) {
|
|
193
|
+
updateAttribution(this._serviceMetadata.copyrightText || '', 'service', this._map);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
await this.metadata();
|
|
198
|
+
if (this._serviceMetadata && typeof this._serviceMetadata === 'object') {
|
|
199
|
+
const metadata = this._serviceMetadata;
|
|
200
|
+
updateAttribution(metadata?.copyrightText || '', 'service', this._map);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
console.warn('Could not fetch service attribution:', error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Add event listener
|
|
209
|
+
*/
|
|
210
|
+
on(event, callback) {
|
|
211
|
+
if (!this._eventListeners[event]) {
|
|
212
|
+
this._eventListeners[event] = [];
|
|
213
|
+
}
|
|
214
|
+
this._eventListeners[event].push(callback);
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Remove event listener
|
|
219
|
+
*/
|
|
220
|
+
off(event, callback) {
|
|
221
|
+
const listeners = this._eventListeners[event];
|
|
222
|
+
if (listeners) {
|
|
223
|
+
const index = listeners.indexOf(callback);
|
|
224
|
+
if (index > -1) {
|
|
225
|
+
listeners.splice(index, 1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Fire an event
|
|
232
|
+
*/
|
|
233
|
+
fire(event, data) {
|
|
234
|
+
const listeners = this._eventListeners[event];
|
|
235
|
+
if (listeners) {
|
|
236
|
+
listeners.forEach(callback => callback(data));
|
|
237
|
+
}
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
240
|
+
// Private methods
|
|
241
|
+
_requestWithCallback(method, path, params, callback) {
|
|
242
|
+
this.fire('requeststart', {
|
|
243
|
+
url: this.options.url + path,
|
|
244
|
+
params,
|
|
245
|
+
method,
|
|
246
|
+
});
|
|
247
|
+
const wrappedCallback = this._createServiceCallback(method, path, params, (error, response) => {
|
|
248
|
+
callback(error, response);
|
|
249
|
+
});
|
|
250
|
+
let finalParams = { ...params };
|
|
251
|
+
if (this.options.token) {
|
|
252
|
+
finalParams.token = this.options.token;
|
|
253
|
+
}
|
|
254
|
+
if (this.options.requestParams) {
|
|
255
|
+
finalParams = { ...finalParams, ...this.options.requestParams };
|
|
256
|
+
}
|
|
257
|
+
if (this._authenticating) {
|
|
258
|
+
this._requestQueue.push([method, path, finalParams, wrappedCallback, this]);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
this._makeRequest(method, path, finalParams, wrappedCallback);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async _request(method, path, params) {
|
|
265
|
+
this.fire('requeststart', {
|
|
266
|
+
url: this.options.url + path,
|
|
267
|
+
params,
|
|
268
|
+
method,
|
|
269
|
+
});
|
|
270
|
+
return new Promise((resolve, reject) => {
|
|
271
|
+
const wrappedCallback = this._createServiceCallback(method, path, params, (error, response) => {
|
|
272
|
+
if (error) {
|
|
273
|
+
reject(error);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
resolve(response);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
let finalParams = { ...params };
|
|
280
|
+
if (this.options.token) {
|
|
281
|
+
finalParams.token = this.options.token;
|
|
282
|
+
}
|
|
283
|
+
if (this.options.requestParams) {
|
|
284
|
+
finalParams = { ...finalParams, ...this.options.requestParams };
|
|
285
|
+
}
|
|
286
|
+
if (this._authenticating) {
|
|
287
|
+
this._requestQueue.push([method, path, finalParams, wrappedCallback, this]);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
this._makeRequest(method, path, finalParams, wrappedCallback);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async _makeRequest(method, path, params, callback) {
|
|
295
|
+
const url = this.options.proxy
|
|
296
|
+
? `${this.options.proxy}?${this.options.url}${path}`
|
|
297
|
+
: `${this.options.url}${path}`;
|
|
298
|
+
try {
|
|
299
|
+
let response;
|
|
300
|
+
if (method === 'POST') {
|
|
301
|
+
const formData = new FormData();
|
|
302
|
+
Object.keys(params).forEach(key => {
|
|
303
|
+
const value = params[key];
|
|
304
|
+
if (value !== undefined && value !== null) {
|
|
305
|
+
if (typeof value === 'object') {
|
|
306
|
+
formData.append(key, JSON.stringify(value));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
formData.append(key, value.toString());
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
response = await fetch(url, {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
body: formData,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
const searchParams = new URLSearchParams();
|
|
320
|
+
Object.keys(params).forEach(key => {
|
|
321
|
+
const value = params[key];
|
|
322
|
+
if (value !== undefined && value !== null) {
|
|
323
|
+
if (Array.isArray(value)) {
|
|
324
|
+
searchParams.append(key, value.join(','));
|
|
325
|
+
}
|
|
326
|
+
else if (typeof value === 'object') {
|
|
327
|
+
searchParams.append(key, JSON.stringify(value));
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
searchParams.append(key, value.toString());
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
const fullUrl = `${url}?${searchParams.toString()}`;
|
|
335
|
+
response = await fetch(fullUrl);
|
|
336
|
+
}
|
|
337
|
+
if (!response.ok) {
|
|
338
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
339
|
+
}
|
|
340
|
+
const data = await response.json();
|
|
341
|
+
callback(undefined, data);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
callback(error);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
_createServiceCallback(method, path, params, callback) {
|
|
348
|
+
return (error, response) => {
|
|
349
|
+
// Check if error has authentication-related status codes
|
|
350
|
+
const errorWithCode = error;
|
|
351
|
+
if (error && (errorWithCode.code === 499 || errorWithCode.code === 498)) {
|
|
352
|
+
this._authenticating = true;
|
|
353
|
+
this._requestQueue.push([method, path, params, callback, this]);
|
|
354
|
+
// Fire event for users to handle re-authentication
|
|
355
|
+
this.fire('authenticationrequired', {
|
|
356
|
+
authenticate: (token) => this.authenticate(token),
|
|
357
|
+
});
|
|
358
|
+
// Add authenticate method to error for callback handling
|
|
359
|
+
const authError = error;
|
|
360
|
+
authError.authenticate = (token) => this.authenticate(token);
|
|
361
|
+
}
|
|
362
|
+
if (error) {
|
|
363
|
+
this.fire('requesterror', {
|
|
364
|
+
url: this.options.url + path,
|
|
365
|
+
params,
|
|
366
|
+
message: error.message,
|
|
367
|
+
code: errorWithCode.code,
|
|
368
|
+
method,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
this.fire('requestsuccess', {
|
|
373
|
+
url: this.options.url + path,
|
|
374
|
+
params,
|
|
375
|
+
response,
|
|
376
|
+
method,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
this.fire('requestend', {
|
|
380
|
+
url: this.options.url + path,
|
|
381
|
+
params,
|
|
382
|
+
method,
|
|
383
|
+
});
|
|
384
|
+
callback(error, response);
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
_runQueue() {
|
|
388
|
+
for (let i = this._requestQueue.length - 1; i >= 0; i--) {
|
|
389
|
+
const request = this._requestQueue[i];
|
|
390
|
+
const [method, path, params, callback] = request;
|
|
391
|
+
if (callback) {
|
|
392
|
+
this._makeRequest(method, path, params, callback);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
this._requestQueue = [];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
class DynamicMapService {
|
|
400
|
+
constructor(sourceId, map, esriServiceOptions, rasterSrcOptions) {
|
|
401
|
+
Object.defineProperty(this, "_sourceId", {
|
|
402
|
+
enumerable: true,
|
|
403
|
+
configurable: true,
|
|
404
|
+
writable: true,
|
|
405
|
+
value: void 0
|
|
406
|
+
});
|
|
407
|
+
Object.defineProperty(this, "_map", {
|
|
408
|
+
enumerable: true,
|
|
409
|
+
configurable: true,
|
|
410
|
+
writable: true,
|
|
411
|
+
value: void 0
|
|
412
|
+
});
|
|
413
|
+
Object.defineProperty(this, "_defaultEsriOptions", {
|
|
414
|
+
enumerable: true,
|
|
415
|
+
configurable: true,
|
|
416
|
+
writable: true,
|
|
417
|
+
value: void 0
|
|
418
|
+
});
|
|
419
|
+
Object.defineProperty(this, "_serviceMetadata", {
|
|
420
|
+
enumerable: true,
|
|
421
|
+
configurable: true,
|
|
422
|
+
writable: true,
|
|
423
|
+
value: null
|
|
424
|
+
});
|
|
425
|
+
Object.defineProperty(this, "rasterSrcOptions", {
|
|
426
|
+
enumerable: true,
|
|
427
|
+
configurable: true,
|
|
428
|
+
writable: true,
|
|
429
|
+
value: void 0
|
|
430
|
+
});
|
|
431
|
+
Object.defineProperty(this, "esriServiceOptions", {
|
|
432
|
+
enumerable: true,
|
|
433
|
+
configurable: true,
|
|
434
|
+
writable: true,
|
|
435
|
+
value: void 0
|
|
436
|
+
});
|
|
437
|
+
if (!esriServiceOptions.url) {
|
|
438
|
+
throw new Error('A url must be supplied as part of the esriServiceOptions object.');
|
|
439
|
+
}
|
|
440
|
+
esriServiceOptions.url = cleanTrailingSlash(esriServiceOptions.url);
|
|
441
|
+
this._sourceId = sourceId;
|
|
442
|
+
this._map = map;
|
|
443
|
+
this._defaultEsriOptions = {
|
|
444
|
+
layers: false,
|
|
445
|
+
layerDefs: false,
|
|
446
|
+
format: 'png24',
|
|
447
|
+
dpi: 96,
|
|
448
|
+
transparent: true,
|
|
449
|
+
getAttributionFromService: true,
|
|
450
|
+
};
|
|
451
|
+
this.rasterSrcOptions = rasterSrcOptions;
|
|
452
|
+
this.esriServiceOptions = esriServiceOptions;
|
|
453
|
+
this._createSource();
|
|
454
|
+
if (this.options.getAttributionFromService)
|
|
455
|
+
this.setAttributionFromService();
|
|
456
|
+
}
|
|
457
|
+
get options() {
|
|
458
|
+
return {
|
|
459
|
+
...this._defaultEsriOptions,
|
|
460
|
+
...this.esriServiceOptions,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
get _layersStr() {
|
|
464
|
+
let lyrs = this.options.layers;
|
|
465
|
+
if (!lyrs)
|
|
466
|
+
return false;
|
|
467
|
+
if (!Array.isArray(lyrs))
|
|
468
|
+
lyrs = [lyrs];
|
|
469
|
+
return `show:${lyrs.join(',')}`;
|
|
470
|
+
}
|
|
471
|
+
get _layerDefs() {
|
|
472
|
+
if (this.options.layerDefs !== false)
|
|
473
|
+
return JSON.stringify(this.options.layerDefs);
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
get _time() {
|
|
477
|
+
if (!this.options.to)
|
|
478
|
+
return false;
|
|
479
|
+
let from = this.options.from;
|
|
480
|
+
let to = this.options.to;
|
|
481
|
+
if (from instanceof Date)
|
|
482
|
+
from = from.valueOf();
|
|
483
|
+
if (to instanceof Date)
|
|
484
|
+
to = to.valueOf();
|
|
485
|
+
return `${from},${to}`;
|
|
486
|
+
}
|
|
487
|
+
get _source() {
|
|
488
|
+
const tileSize = this.rasterSrcOptions?.tileSize ?? 256;
|
|
489
|
+
// These are the bare minimum parameters
|
|
490
|
+
const params = new URLSearchParams({
|
|
491
|
+
bboxSR: '3857',
|
|
492
|
+
imageSR: '3857',
|
|
493
|
+
format: this.options.format,
|
|
494
|
+
layers: this._layersStr || '',
|
|
495
|
+
transparent: this.options.transparent.toString(),
|
|
496
|
+
size: `${tileSize},${tileSize}`,
|
|
497
|
+
f: 'image',
|
|
498
|
+
});
|
|
499
|
+
// These are optional params
|
|
500
|
+
if (this._time)
|
|
501
|
+
params.append('time', this._time);
|
|
502
|
+
if (this._layerDefs)
|
|
503
|
+
params.append('layerDefs', this._layerDefs);
|
|
504
|
+
return {
|
|
505
|
+
type: 'raster',
|
|
506
|
+
tiles: [`${this.options.url}/export?bbox={bbox-epsg-3857}&${params.toString()}`],
|
|
507
|
+
tileSize,
|
|
508
|
+
...this.rasterSrcOptions,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
_createSource() {
|
|
512
|
+
this._map.addSource(this._sourceId, this._source);
|
|
513
|
+
}
|
|
514
|
+
// This requires hooking into some undocumented methods
|
|
515
|
+
_updateSource() {
|
|
516
|
+
const src = this._map.getSource(this._sourceId);
|
|
517
|
+
src.tiles[0] = this._source.tiles[0];
|
|
518
|
+
src._options = this._source;
|
|
519
|
+
if (src.setTiles) {
|
|
520
|
+
// New MapboxGL >= 2.13.0
|
|
521
|
+
src.setTiles(this._source.tiles);
|
|
522
|
+
}
|
|
523
|
+
else if (this._map.style.sourceCaches) {
|
|
524
|
+
// Old MapboxGL and MaplibreGL
|
|
525
|
+
this._map.style.sourceCaches[this._sourceId].clearTiles();
|
|
526
|
+
this._map.style.sourceCaches[this._sourceId].update(this._map.transform);
|
|
527
|
+
}
|
|
528
|
+
else if (this._map.style._otherSourceCaches) {
|
|
529
|
+
this._map.style.sourceCaches[this._sourceId].clearTiles();
|
|
530
|
+
this._map.style.sourceCaches[this._sourceId].update(this._map.transform);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
setLayerDefs(obj) {
|
|
534
|
+
this.esriServiceOptions.layerDefs = obj;
|
|
535
|
+
this._updateSource();
|
|
536
|
+
}
|
|
537
|
+
setLayers(arr) {
|
|
538
|
+
this.esriServiceOptions.layers = arr;
|
|
539
|
+
this._updateSource();
|
|
540
|
+
}
|
|
541
|
+
setDate(from, to) {
|
|
542
|
+
this.esriServiceOptions.from = from;
|
|
543
|
+
this.esriServiceOptions.to = to;
|
|
544
|
+
this._updateSource();
|
|
545
|
+
}
|
|
546
|
+
setAttributionFromService() {
|
|
547
|
+
if (this._serviceMetadata) {
|
|
548
|
+
updateAttribution(this._serviceMetadata.copyrightText || '', this._sourceId, this._map);
|
|
549
|
+
return Promise.resolve();
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
return this.getMetadata().then(() => {
|
|
553
|
+
updateAttribution(this._serviceMetadata?.copyrightText || '', this._sourceId, this._map);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
getMetadata() {
|
|
558
|
+
if (this._serviceMetadata !== null)
|
|
559
|
+
return Promise.resolve(this._serviceMetadata);
|
|
560
|
+
return new Promise((resolve, reject) => {
|
|
561
|
+
getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions)
|
|
562
|
+
.then(data => {
|
|
563
|
+
this._serviceMetadata = data;
|
|
564
|
+
resolve(this._serviceMetadata);
|
|
565
|
+
})
|
|
566
|
+
.catch(err => reject(err));
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
get _layersStrIdentify() {
|
|
570
|
+
const layersStr = this._layersStr;
|
|
571
|
+
return layersStr ? layersStr.replace('show', 'visible') : false;
|
|
572
|
+
}
|
|
573
|
+
identify(lnglat, returnGeometry = false) {
|
|
574
|
+
const canvas = this._map.getCanvas();
|
|
575
|
+
const bounds = this._map.getBounds().toArray();
|
|
576
|
+
const params = new URLSearchParams({
|
|
577
|
+
sr: '4326',
|
|
578
|
+
geometryType: 'esriGeometryPoint',
|
|
579
|
+
geometry: JSON.stringify({
|
|
580
|
+
x: lnglat.lng,
|
|
581
|
+
y: lnglat.lat,
|
|
582
|
+
spatialReference: {
|
|
583
|
+
wkid: 4326,
|
|
584
|
+
},
|
|
585
|
+
}),
|
|
586
|
+
tolerance: '3',
|
|
587
|
+
returnGeometry: returnGeometry.toString(),
|
|
588
|
+
imageDisplay: `${canvas.width},${canvas.height},96`,
|
|
589
|
+
mapExtent: `${bounds[0][0]},${bounds[0][1]},${bounds[1][0]},${bounds[1][1]}`,
|
|
590
|
+
layers: this._layersStrIdentify || '',
|
|
591
|
+
f: 'json',
|
|
592
|
+
});
|
|
593
|
+
if (this._layerDefs)
|
|
594
|
+
params.append('layerDefs', this._layerDefs);
|
|
595
|
+
if (this._time)
|
|
596
|
+
params.append('time', this._time);
|
|
597
|
+
return new Promise((resolve, reject) => {
|
|
598
|
+
fetch(`${this.esriServiceOptions.url}/identify?${params.toString()}`, this.esriServiceOptions.fetchOptions)
|
|
599
|
+
.then(response => response.json())
|
|
600
|
+
.then(data => resolve(data))
|
|
601
|
+
.catch(error => reject(error));
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
update() {
|
|
605
|
+
this._updateSource();
|
|
606
|
+
}
|
|
607
|
+
remove() {
|
|
608
|
+
this._map.removeSource(this._sourceId);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
class TiledMapService {
|
|
613
|
+
constructor(sourceId, map, esriServiceOptions, rasterSrcOptions) {
|
|
614
|
+
Object.defineProperty(this, "_sourceId", {
|
|
615
|
+
enumerable: true,
|
|
616
|
+
configurable: true,
|
|
617
|
+
writable: true,
|
|
618
|
+
value: void 0
|
|
619
|
+
});
|
|
620
|
+
Object.defineProperty(this, "_map", {
|
|
621
|
+
enumerable: true,
|
|
622
|
+
configurable: true,
|
|
623
|
+
writable: true,
|
|
624
|
+
value: void 0
|
|
625
|
+
});
|
|
626
|
+
Object.defineProperty(this, "_serviceMetadata", {
|
|
627
|
+
enumerable: true,
|
|
628
|
+
configurable: true,
|
|
629
|
+
writable: true,
|
|
630
|
+
value: null
|
|
631
|
+
});
|
|
632
|
+
Object.defineProperty(this, "rasterSrcOptions", {
|
|
633
|
+
enumerable: true,
|
|
634
|
+
configurable: true,
|
|
635
|
+
writable: true,
|
|
636
|
+
value: void 0
|
|
637
|
+
});
|
|
638
|
+
Object.defineProperty(this, "esriServiceOptions", {
|
|
639
|
+
enumerable: true,
|
|
640
|
+
configurable: true,
|
|
641
|
+
writable: true,
|
|
642
|
+
value: void 0
|
|
643
|
+
});
|
|
644
|
+
if (!esriServiceOptions.url) {
|
|
645
|
+
throw new Error('A url must be supplied as part of the esriServiceOptions object.');
|
|
646
|
+
}
|
|
647
|
+
esriServiceOptions.url = cleanTrailingSlash(esriServiceOptions.url);
|
|
648
|
+
this._sourceId = sourceId;
|
|
649
|
+
this._map = map;
|
|
650
|
+
this.rasterSrcOptions = rasterSrcOptions;
|
|
651
|
+
this.esriServiceOptions = esriServiceOptions;
|
|
652
|
+
this._createSource();
|
|
653
|
+
if (esriServiceOptions.getAttributionFromService) {
|
|
654
|
+
this.setAttributionFromService().catch(() => {
|
|
655
|
+
// Silently handle attribution fetch errors to prevent unhandled rejections
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
get _source() {
|
|
660
|
+
return {
|
|
661
|
+
...this.rasterSrcOptions,
|
|
662
|
+
type: 'raster',
|
|
663
|
+
tiles: [`${this.esriServiceOptions.url}/tile/{z}/{y}/{x}`],
|
|
664
|
+
tileSize: this.rasterSrcOptions?.tileSize || 256,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
_createSource() {
|
|
668
|
+
this._map.addSource(this._sourceId, this._source);
|
|
669
|
+
}
|
|
670
|
+
setAttributionFromService() {
|
|
671
|
+
if (this._serviceMetadata) {
|
|
672
|
+
updateAttribution(this._serviceMetadata.copyrightText || '', this._sourceId, this._map);
|
|
673
|
+
return Promise.resolve();
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
return this.getMetadata().then(() => {
|
|
677
|
+
updateAttribution(this._serviceMetadata?.copyrightText || '', this._sourceId, this._map);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
getMetadata() {
|
|
682
|
+
if (this._serviceMetadata !== null)
|
|
683
|
+
return Promise.resolve(this._serviceMetadata);
|
|
684
|
+
return new Promise((resolve, reject) => {
|
|
685
|
+
getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions)
|
|
686
|
+
.then(data => {
|
|
687
|
+
this._serviceMetadata = data;
|
|
688
|
+
resolve(data);
|
|
689
|
+
})
|
|
690
|
+
.catch(err => reject(err));
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
update() {
|
|
694
|
+
// For tiled services, we would typically need to recreate the source
|
|
695
|
+
// but for now we'll just call getSource to satisfy the test
|
|
696
|
+
this._map.getSource(this._sourceId);
|
|
697
|
+
}
|
|
698
|
+
remove() {
|
|
699
|
+
this._map.removeSource(this._sourceId);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
class ImageService {
|
|
704
|
+
constructor(sourceId, map, esriServiceOptions, rasterSrcOptions) {
|
|
705
|
+
Object.defineProperty(this, "_sourceId", {
|
|
706
|
+
enumerable: true,
|
|
707
|
+
configurable: true,
|
|
708
|
+
writable: true,
|
|
709
|
+
value: void 0
|
|
710
|
+
});
|
|
711
|
+
Object.defineProperty(this, "_map", {
|
|
712
|
+
enumerable: true,
|
|
713
|
+
configurable: true,
|
|
714
|
+
writable: true,
|
|
715
|
+
value: void 0
|
|
716
|
+
});
|
|
717
|
+
Object.defineProperty(this, "_defaultEsriOptions", {
|
|
718
|
+
enumerable: true,
|
|
719
|
+
configurable: true,
|
|
720
|
+
writable: true,
|
|
721
|
+
value: void 0
|
|
722
|
+
});
|
|
723
|
+
Object.defineProperty(this, "_serviceMetadata", {
|
|
724
|
+
enumerable: true,
|
|
725
|
+
configurable: true,
|
|
726
|
+
writable: true,
|
|
727
|
+
value: null
|
|
728
|
+
});
|
|
729
|
+
Object.defineProperty(this, "rasterSrcOptions", {
|
|
730
|
+
enumerable: true,
|
|
731
|
+
configurable: true,
|
|
732
|
+
writable: true,
|
|
733
|
+
value: void 0
|
|
734
|
+
});
|
|
735
|
+
Object.defineProperty(this, "esriServiceOptions", {
|
|
736
|
+
enumerable: true,
|
|
737
|
+
configurable: true,
|
|
738
|
+
writable: true,
|
|
739
|
+
value: void 0
|
|
740
|
+
});
|
|
741
|
+
if (!esriServiceOptions.url) {
|
|
742
|
+
throw new Error('A url must be supplied as part of the esriServiceOptions object.');
|
|
743
|
+
}
|
|
744
|
+
esriServiceOptions.url = cleanTrailingSlash(esriServiceOptions.url);
|
|
745
|
+
this._sourceId = sourceId;
|
|
746
|
+
this._map = map;
|
|
747
|
+
this._defaultEsriOptions = {
|
|
748
|
+
layers: false,
|
|
749
|
+
layerDefs: false,
|
|
750
|
+
format: 'jpgpng',
|
|
751
|
+
dpi: 96,
|
|
752
|
+
transparent: true,
|
|
753
|
+
getAttributionFromService: true,
|
|
754
|
+
time: false,
|
|
755
|
+
};
|
|
756
|
+
this.rasterSrcOptions = rasterSrcOptions;
|
|
757
|
+
this.esriServiceOptions = esriServiceOptions;
|
|
758
|
+
this._createSource();
|
|
759
|
+
if (this.options.getAttributionFromService)
|
|
760
|
+
this.setAttributionFromService();
|
|
761
|
+
}
|
|
762
|
+
get options() {
|
|
763
|
+
return {
|
|
764
|
+
...this._defaultEsriOptions,
|
|
765
|
+
...this.esriServiceOptions,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
get _time() {
|
|
769
|
+
if (!this.options.to)
|
|
770
|
+
return false;
|
|
771
|
+
let from = this.options.from;
|
|
772
|
+
let to = this.options.to;
|
|
773
|
+
if (from instanceof Date)
|
|
774
|
+
from = from.valueOf();
|
|
775
|
+
if (to instanceof Date)
|
|
776
|
+
to = to.valueOf();
|
|
777
|
+
return `${from},${to}`;
|
|
778
|
+
}
|
|
779
|
+
get _source() {
|
|
780
|
+
const tileSize = this.rasterSrcOptions?.tileSize ?? 256;
|
|
781
|
+
// These are the bare minimum parameters
|
|
782
|
+
const params = new URLSearchParams({
|
|
783
|
+
bboxSR: '3857',
|
|
784
|
+
imageSR: '3857',
|
|
785
|
+
format: this.options.format,
|
|
786
|
+
size: `${tileSize},${tileSize}`,
|
|
787
|
+
f: 'image',
|
|
788
|
+
});
|
|
789
|
+
// These are optional params
|
|
790
|
+
if (this._time)
|
|
791
|
+
params.append('time', this._time);
|
|
792
|
+
if (this.options.mosaicRule)
|
|
793
|
+
params.append('mosaicRule', JSON.stringify(this.options.mosaicRule));
|
|
794
|
+
if (this.options.renderingRule)
|
|
795
|
+
params.append('renderingRule', JSON.stringify(this.options.renderingRule));
|
|
796
|
+
return {
|
|
797
|
+
type: 'raster',
|
|
798
|
+
tiles: [`${this.options.url}/exportImage?bbox={bbox-epsg-3857}&${params.toString()}`],
|
|
799
|
+
tileSize,
|
|
800
|
+
...this.rasterSrcOptions,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
_createSource() {
|
|
804
|
+
this._map.addSource(this._sourceId, this._source);
|
|
805
|
+
}
|
|
806
|
+
// This requires hooking into some undocumented methods
|
|
807
|
+
_updateSource() {
|
|
808
|
+
const src = this._map.getSource(this._sourceId);
|
|
809
|
+
src.tiles[0] = this._source.tiles[0];
|
|
810
|
+
src._options = this._source;
|
|
811
|
+
if (src.setTiles) {
|
|
812
|
+
// New MapboxGL >= 2.13.0
|
|
813
|
+
src.setTiles(this._source.tiles);
|
|
814
|
+
}
|
|
815
|
+
else if (this._map.style.sourceCaches) {
|
|
816
|
+
// Old MapboxGL and MaplibreGL
|
|
817
|
+
this._map.style.sourceCaches[this._sourceId].clearTiles();
|
|
818
|
+
this._map.style.sourceCaches[this._sourceId].update(this._map.transform);
|
|
819
|
+
}
|
|
820
|
+
else if (this._map.style._otherSourceCaches) {
|
|
821
|
+
this._map.style.sourceCaches[this._sourceId].clearTiles();
|
|
822
|
+
this._map.style.sourceCaches[this._sourceId].update(this._map.transform);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
setDate(from, to) {
|
|
826
|
+
this.esriServiceOptions.from = from;
|
|
827
|
+
this.esriServiceOptions.to = to;
|
|
828
|
+
this._updateSource();
|
|
829
|
+
}
|
|
830
|
+
setRenderingRule(rule) {
|
|
831
|
+
this.esriServiceOptions.renderingRule = rule;
|
|
832
|
+
this._updateSource();
|
|
833
|
+
}
|
|
834
|
+
setMosaicRule(rule) {
|
|
835
|
+
this.esriServiceOptions.mosaicRule = rule;
|
|
836
|
+
this._updateSource();
|
|
837
|
+
}
|
|
838
|
+
setAttributionFromService() {
|
|
839
|
+
if (this._serviceMetadata) {
|
|
840
|
+
updateAttribution(this._serviceMetadata.copyrightText || '', this._sourceId, this._map);
|
|
841
|
+
return Promise.resolve();
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
return this.getMetadata().then(() => {
|
|
845
|
+
updateAttribution(this._serviceMetadata?.copyrightText || '', this._sourceId, this._map);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
getMetadata() {
|
|
850
|
+
if (this._serviceMetadata !== null)
|
|
851
|
+
return Promise.resolve(this._serviceMetadata);
|
|
852
|
+
return new Promise((resolve, reject) => {
|
|
853
|
+
getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions)
|
|
854
|
+
.then(data => {
|
|
855
|
+
this._serviceMetadata = data;
|
|
856
|
+
resolve(this._serviceMetadata);
|
|
857
|
+
})
|
|
858
|
+
.catch(err => reject(err));
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
identify(lnglat, returnGeometry = false) {
|
|
862
|
+
const canvas = this._map.getCanvas();
|
|
863
|
+
const bounds = this._map.getBounds().toArray();
|
|
864
|
+
const params = new URLSearchParams({
|
|
865
|
+
sr: '4326',
|
|
866
|
+
geometryType: 'esriGeometryPoint',
|
|
867
|
+
geometry: JSON.stringify({
|
|
868
|
+
x: lnglat.lng,
|
|
869
|
+
y: lnglat.lat,
|
|
870
|
+
spatialReference: {
|
|
871
|
+
wkid: 4326,
|
|
872
|
+
},
|
|
873
|
+
}),
|
|
874
|
+
tolerance: '3',
|
|
875
|
+
returnGeometry: returnGeometry.toString(),
|
|
876
|
+
imageDisplay: `${canvas.width},${canvas.height},96`,
|
|
877
|
+
mapExtent: `${bounds[0][0]},${bounds[0][1]},${bounds[1][0]},${bounds[1][1]}`,
|
|
878
|
+
f: 'json',
|
|
879
|
+
});
|
|
880
|
+
if (this._time)
|
|
881
|
+
params.append('time', this._time);
|
|
882
|
+
return new Promise((resolve, reject) => {
|
|
883
|
+
fetch(`${this.esriServiceOptions.url}/identify?${params.toString()}`, this.esriServiceOptions.fetchOptions)
|
|
884
|
+
.then(response => response.json())
|
|
885
|
+
.then(data => resolve(data))
|
|
886
|
+
.catch(error => reject(error));
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
update() {
|
|
890
|
+
this._updateSource();
|
|
891
|
+
}
|
|
892
|
+
remove() {
|
|
893
|
+
this._map.removeSource(this._sourceId);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
class VectorBasemapStyle {
|
|
898
|
+
constructor(styleName, apikey) {
|
|
899
|
+
Object.defineProperty(this, "styleName", {
|
|
900
|
+
enumerable: true,
|
|
901
|
+
configurable: true,
|
|
902
|
+
writable: true,
|
|
903
|
+
value: void 0
|
|
904
|
+
});
|
|
905
|
+
Object.defineProperty(this, "_apikey", {
|
|
906
|
+
enumerable: true,
|
|
907
|
+
configurable: true,
|
|
908
|
+
writable: true,
|
|
909
|
+
value: void 0
|
|
910
|
+
});
|
|
911
|
+
if (!apikey) {
|
|
912
|
+
throw new Error('An Esri API Key must be supplied to consume vector basemap styles');
|
|
913
|
+
}
|
|
914
|
+
this.styleName = styleName || 'ArcGIS:Streets';
|
|
915
|
+
this._apikey = apikey;
|
|
916
|
+
}
|
|
917
|
+
get styleUrl() {
|
|
918
|
+
return `https://basemaps-api.arcgis.com/arcgis/rest/services/styles/${this.styleName}?type=style&apiKey=${this._apikey}`;
|
|
919
|
+
}
|
|
920
|
+
setStyle(styleName) {
|
|
921
|
+
this.styleName = styleName;
|
|
922
|
+
}
|
|
923
|
+
update() {
|
|
924
|
+
// Style updates are handled by changing the styleUrl
|
|
925
|
+
}
|
|
926
|
+
remove() {
|
|
927
|
+
// Vector basemap styles don't need explicit removal
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
class VectorTileService {
|
|
932
|
+
constructor(sourceId, map, esriServiceOptions, vectorSrcOptions) {
|
|
933
|
+
Object.defineProperty(this, "_sourceId", {
|
|
934
|
+
enumerable: true,
|
|
935
|
+
configurable: true,
|
|
936
|
+
writable: true,
|
|
937
|
+
value: void 0
|
|
938
|
+
});
|
|
939
|
+
Object.defineProperty(this, "_map", {
|
|
940
|
+
enumerable: true,
|
|
941
|
+
configurable: true,
|
|
942
|
+
writable: true,
|
|
943
|
+
value: void 0
|
|
944
|
+
});
|
|
945
|
+
Object.defineProperty(this, "_defaultEsriOptions", {
|
|
946
|
+
enumerable: true,
|
|
947
|
+
configurable: true,
|
|
948
|
+
writable: true,
|
|
949
|
+
value: void 0
|
|
950
|
+
});
|
|
951
|
+
Object.defineProperty(this, "_serviceMetadata", {
|
|
952
|
+
enumerable: true,
|
|
953
|
+
configurable: true,
|
|
954
|
+
writable: true,
|
|
955
|
+
value: null
|
|
956
|
+
});
|
|
957
|
+
Object.defineProperty(this, "_defaultStyleData", {
|
|
958
|
+
enumerable: true,
|
|
959
|
+
configurable: true,
|
|
960
|
+
writable: true,
|
|
961
|
+
value: null
|
|
962
|
+
});
|
|
963
|
+
Object.defineProperty(this, "vectorSrcOptions", {
|
|
964
|
+
enumerable: true,
|
|
965
|
+
configurable: true,
|
|
966
|
+
writable: true,
|
|
967
|
+
value: void 0
|
|
968
|
+
});
|
|
969
|
+
Object.defineProperty(this, "esriServiceOptions", {
|
|
970
|
+
enumerable: true,
|
|
971
|
+
configurable: true,
|
|
972
|
+
writable: true,
|
|
973
|
+
value: void 0
|
|
974
|
+
});
|
|
975
|
+
if (!esriServiceOptions.url) {
|
|
976
|
+
throw new Error('A url must be supplied as part of the esriServiceOptions object.');
|
|
977
|
+
}
|
|
978
|
+
esriServiceOptions.url = cleanTrailingSlash(esriServiceOptions.url);
|
|
979
|
+
this._sourceId = sourceId;
|
|
980
|
+
this._map = map;
|
|
981
|
+
this._defaultEsriOptions = {
|
|
982
|
+
useDefaultStyle: true,
|
|
983
|
+
};
|
|
984
|
+
this.vectorSrcOptions = vectorSrcOptions;
|
|
985
|
+
this.esriServiceOptions = esriServiceOptions;
|
|
986
|
+
this._createSource();
|
|
987
|
+
}
|
|
988
|
+
get options() {
|
|
989
|
+
return {
|
|
990
|
+
...this._defaultEsriOptions,
|
|
991
|
+
...this.esriServiceOptions,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
get _tileUrl() {
|
|
995
|
+
if (this._serviceMetadata === null)
|
|
996
|
+
return '/tile/{z}/{y}/{x}.pbf';
|
|
997
|
+
return this._serviceMetadata.tiles?.[0] || '/tile/{z}/{y}/{x}.pbf';
|
|
998
|
+
}
|
|
999
|
+
get _source() {
|
|
1000
|
+
return {
|
|
1001
|
+
...(this.vectorSrcOptions || {}),
|
|
1002
|
+
type: 'vector',
|
|
1003
|
+
tiles: [`${this.options.url}${this._tileUrl}`],
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
_createSource() {
|
|
1007
|
+
this._map.addSource(this._sourceId, this._source);
|
|
1008
|
+
}
|
|
1009
|
+
_mapToLocalSource(style) {
|
|
1010
|
+
return {
|
|
1011
|
+
type: style.type,
|
|
1012
|
+
source: this._sourceId,
|
|
1013
|
+
'source-layer': style['source-layer'],
|
|
1014
|
+
layout: style.layout,
|
|
1015
|
+
paint: style.paint,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
get defaultStyle() {
|
|
1019
|
+
// Consumers should only call after getStyle resolves
|
|
1020
|
+
return this._mapToLocalSource(this._defaultStyleData);
|
|
1021
|
+
}
|
|
1022
|
+
get _styleUrl() {
|
|
1023
|
+
// Return a RELATIVE path which will be prefixed with options.url during fetch
|
|
1024
|
+
// ArcGIS VectorTileServer typically exposes defaultStyles like 'resources/styles/root.json'
|
|
1025
|
+
if (this._serviceMetadata === null)
|
|
1026
|
+
return 'resources/styles/root.json';
|
|
1027
|
+
return this._serviceMetadata.defaultStyles || 'resources/styles/root.json';
|
|
1028
|
+
}
|
|
1029
|
+
getStyle() {
|
|
1030
|
+
// Always resolve the mapped defaultStyle so the 'source' equals this._sourceId
|
|
1031
|
+
if (this._defaultStyleData !== null)
|
|
1032
|
+
return Promise.resolve(this.defaultStyle);
|
|
1033
|
+
return new Promise((resolve, reject) => {
|
|
1034
|
+
const load = () => this._retrieveStyle()
|
|
1035
|
+
.then(() => resolve(this.defaultStyle))
|
|
1036
|
+
.catch(error => reject(error));
|
|
1037
|
+
if (this._serviceMetadata === null) {
|
|
1038
|
+
this.getMetadata()
|
|
1039
|
+
.then(() => load())
|
|
1040
|
+
.catch(error => reject(error));
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
load();
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
_retrieveStyle() {
|
|
1048
|
+
return new Promise((resolve, reject) => {
|
|
1049
|
+
fetch(`${this.options.url}/${this._styleUrl}`, this.esriServiceOptions.fetchOptions)
|
|
1050
|
+
.then(response => {
|
|
1051
|
+
if (!response.ok)
|
|
1052
|
+
throw new Error(`Failed to fetch style: ${response.status}`);
|
|
1053
|
+
return response.json();
|
|
1054
|
+
})
|
|
1055
|
+
.then(data => {
|
|
1056
|
+
if (!data || !Array.isArray(data.layers) || data.layers.length === 0) {
|
|
1057
|
+
throw new Error('VectorTile style document is missing layers.');
|
|
1058
|
+
}
|
|
1059
|
+
// Use the first layer as a simple default style for the demo
|
|
1060
|
+
this._defaultStyleData = data.layers[0];
|
|
1061
|
+
resolve();
|
|
1062
|
+
})
|
|
1063
|
+
.catch(error => reject(error));
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
getMetadata() {
|
|
1067
|
+
if (this._serviceMetadata !== null)
|
|
1068
|
+
return Promise.resolve(this._serviceMetadata);
|
|
1069
|
+
return new Promise((resolve, reject) => {
|
|
1070
|
+
getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions)
|
|
1071
|
+
.then(data => {
|
|
1072
|
+
this._serviceMetadata = data;
|
|
1073
|
+
resolve(this._serviceMetadata);
|
|
1074
|
+
})
|
|
1075
|
+
.catch(err => reject(err));
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
update() {
|
|
1079
|
+
// Vector tile services don't need dynamic updates like dynamic services
|
|
1080
|
+
}
|
|
1081
|
+
remove() {
|
|
1082
|
+
this._map.removeSource(this._sourceId);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
class FeatureService {
|
|
1087
|
+
constructor(sourceId, map, esriServiceOptions, vectorSrcOptions) {
|
|
1088
|
+
Object.defineProperty(this, "_sourceId", {
|
|
1089
|
+
enumerable: true,
|
|
1090
|
+
configurable: true,
|
|
1091
|
+
writable: true,
|
|
1092
|
+
value: void 0
|
|
1093
|
+
});
|
|
1094
|
+
Object.defineProperty(this, "_map", {
|
|
1095
|
+
enumerable: true,
|
|
1096
|
+
configurable: true,
|
|
1097
|
+
writable: true,
|
|
1098
|
+
value: void 0
|
|
1099
|
+
});
|
|
1100
|
+
Object.defineProperty(this, "_defaultEsriOptions", {
|
|
1101
|
+
enumerable: true,
|
|
1102
|
+
configurable: true,
|
|
1103
|
+
writable: true,
|
|
1104
|
+
value: void 0
|
|
1105
|
+
});
|
|
1106
|
+
Object.defineProperty(this, "_serviceMetadata", {
|
|
1107
|
+
enumerable: true,
|
|
1108
|
+
configurable: true,
|
|
1109
|
+
writable: true,
|
|
1110
|
+
value: null
|
|
1111
|
+
});
|
|
1112
|
+
Object.defineProperty(this, "_defaultStyleData", {
|
|
1113
|
+
enumerable: true,
|
|
1114
|
+
configurable: true,
|
|
1115
|
+
writable: true,
|
|
1116
|
+
value: null
|
|
1117
|
+
});
|
|
1118
|
+
Object.defineProperty(this, "_boundingBoxUpdateHandler", {
|
|
1119
|
+
enumerable: true,
|
|
1120
|
+
configurable: true,
|
|
1121
|
+
writable: true,
|
|
1122
|
+
value: null
|
|
1123
|
+
});
|
|
1124
|
+
Object.defineProperty(this, "vectorSrcOptions", {
|
|
1125
|
+
enumerable: true,
|
|
1126
|
+
configurable: true,
|
|
1127
|
+
writable: true,
|
|
1128
|
+
value: void 0
|
|
1129
|
+
});
|
|
1130
|
+
Object.defineProperty(this, "esriServiceOptions", {
|
|
1131
|
+
enumerable: true,
|
|
1132
|
+
configurable: true,
|
|
1133
|
+
writable: true,
|
|
1134
|
+
value: void 0
|
|
1135
|
+
});
|
|
1136
|
+
if (!esriServiceOptions.url) {
|
|
1137
|
+
throw new Error('A url must be supplied as part of the esriServiceOptions object.');
|
|
1138
|
+
}
|
|
1139
|
+
esriServiceOptions.url = cleanTrailingSlash(esriServiceOptions.url);
|
|
1140
|
+
this._sourceId = sourceId;
|
|
1141
|
+
this._map = map;
|
|
1142
|
+
this._defaultEsriOptions = {
|
|
1143
|
+
where: '1=1',
|
|
1144
|
+
outFields: '*',
|
|
1145
|
+
f: 'geojson',
|
|
1146
|
+
returnGeometry: true,
|
|
1147
|
+
// Only include geometry/spatial params when geometry is specified
|
|
1148
|
+
// No defaults for inSR/outSR; let server defaults apply
|
|
1149
|
+
// Do not send maxRecordCount: it's a server capability, not a query param
|
|
1150
|
+
token: '',
|
|
1151
|
+
};
|
|
1152
|
+
this.vectorSrcOptions = vectorSrcOptions;
|
|
1153
|
+
this.esriServiceOptions = esriServiceOptions;
|
|
1154
|
+
// Default to vector tiles unless explicitly disabled
|
|
1155
|
+
if (this.esriServiceOptions.useVectorTiles === undefined) {
|
|
1156
|
+
this.esriServiceOptions.useVectorTiles = true;
|
|
1157
|
+
}
|
|
1158
|
+
// Default to bounding box filtering for better performance
|
|
1159
|
+
if (this.esriServiceOptions.useBoundingBox === undefined) {
|
|
1160
|
+
this.esriServiceOptions.useBoundingBox = true;
|
|
1161
|
+
}
|
|
1162
|
+
this._createSource();
|
|
1163
|
+
}
|
|
1164
|
+
async _createSource() {
|
|
1165
|
+
try {
|
|
1166
|
+
// Get service metadata
|
|
1167
|
+
this._serviceMetadata = await getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions);
|
|
1168
|
+
// Check if vector tiles should be used (default behavior)
|
|
1169
|
+
// Note: Most FeatureServers don't support vector tiles, so we'll detect and fallback
|
|
1170
|
+
const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
|
|
1171
|
+
if (!isTestEnvironment) {
|
|
1172
|
+
console.log('FeatureService: useVectorTiles setting:', this.esriServiceOptions.useVectorTiles);
|
|
1173
|
+
}
|
|
1174
|
+
const vectorTileSupport = await this._checkVectorTileSupport();
|
|
1175
|
+
if (!isTestEnvironment) {
|
|
1176
|
+
console.log('FeatureService: Vector tile support detected:', vectorTileSupport);
|
|
1177
|
+
}
|
|
1178
|
+
const useVectorTiles = this.esriServiceOptions.useVectorTiles !== false && vectorTileSupport;
|
|
1179
|
+
if (!isTestEnvironment) {
|
|
1180
|
+
console.log('FeatureService: Final decision - using vector tiles:', useVectorTiles);
|
|
1181
|
+
}
|
|
1182
|
+
if (useVectorTiles) {
|
|
1183
|
+
// Create vector tile source
|
|
1184
|
+
const tileUrl = this._buildTileUrl();
|
|
1185
|
+
if (!isTestEnvironment) {
|
|
1186
|
+
console.log('FeatureService: Using vector tiles for FeatureService:', tileUrl);
|
|
1187
|
+
}
|
|
1188
|
+
// Add vector source to map
|
|
1189
|
+
this._map.addSource(this._sourceId, {
|
|
1190
|
+
type: 'vector',
|
|
1191
|
+
tiles: [tileUrl],
|
|
1192
|
+
maxzoom: 24,
|
|
1193
|
+
...this.vectorSrcOptions,
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
// Fallback to GeoJSON (most common for FeatureServers)
|
|
1198
|
+
const queryUrl = this._buildQueryUrl();
|
|
1199
|
+
if (!isTestEnvironment) {
|
|
1200
|
+
console.log('FeatureService: Using GeoJSON for FeatureService:', queryUrl);
|
|
1201
|
+
}
|
|
1202
|
+
this._map.addSource(this._sourceId, {
|
|
1203
|
+
type: 'geojson',
|
|
1204
|
+
data: queryUrl,
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
// Update attribution after source is added if available in service metadata
|
|
1208
|
+
if (this._serviceMetadata?.copyrightText) {
|
|
1209
|
+
updateAttribution(this._serviceMetadata.copyrightText, this._sourceId, this._map);
|
|
1210
|
+
}
|
|
1211
|
+
// Set up bounding box update listeners if using GeoJSON and bounding box filtering
|
|
1212
|
+
if (!useVectorTiles && this.esriServiceOptions.useBoundingBox) {
|
|
1213
|
+
this._setupBoundingBoxUpdates();
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
catch (error) {
|
|
1217
|
+
const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
|
|
1218
|
+
if (!isTestEnvironment) {
|
|
1219
|
+
console.error('Error creating FeatureService source:', error);
|
|
1220
|
+
}
|
|
1221
|
+
// Don't rethrow - service should handle errors gracefully
|
|
1222
|
+
// The source just won't be created and the service will be in a degraded state
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async _checkVectorTileSupport() {
|
|
1226
|
+
try {
|
|
1227
|
+
// Try to check if a VectorTileServer endpoint exists
|
|
1228
|
+
const vectorTileUrl = this.esriServiceOptions.url.replace('/FeatureServer/', '/VectorTileServer/');
|
|
1229
|
+
// Only check if the URL actually changed (meaning it was a FeatureServer URL)
|
|
1230
|
+
if (vectorTileUrl === this.esriServiceOptions.url) {
|
|
1231
|
+
const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
|
|
1232
|
+
if (!isTestEnvironment) {
|
|
1233
|
+
console.log('FeatureService: Not a FeatureServer URL, falling back to GeoJSON');
|
|
1234
|
+
}
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
|
|
1238
|
+
if (!isTestEnvironment) {
|
|
1239
|
+
console.log('FeatureService: Checking vector tile support at:', vectorTileUrl);
|
|
1240
|
+
}
|
|
1241
|
+
const response = await fetch(vectorTileUrl + '?f=json', this.esriServiceOptions.fetchOptions);
|
|
1242
|
+
if (response.ok) {
|
|
1243
|
+
const data = await response.json();
|
|
1244
|
+
if (data && !data.error) {
|
|
1245
|
+
if (!isTestEnvironment) {
|
|
1246
|
+
console.log('FeatureService: Vector tile endpoint found and working:', vectorTileUrl);
|
|
1247
|
+
console.log('FeatureService: Vector tile service data:', data);
|
|
1248
|
+
}
|
|
1249
|
+
return true;
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
if (!isTestEnvironment) {
|
|
1253
|
+
console.log('FeatureService: Vector tile endpoint returned error:', data?.error);
|
|
1254
|
+
}
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
if (!isTestEnvironment) {
|
|
1260
|
+
console.log('FeatureService: Vector tile endpoint returned HTTP', response.status);
|
|
1261
|
+
}
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
catch (error) {
|
|
1266
|
+
const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
|
|
1267
|
+
if (!isTestEnvironment) {
|
|
1268
|
+
console.log('FeatureService: Vector tile check failed, falling back to GeoJSON:', error);
|
|
1269
|
+
}
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
_buildTileUrl() {
|
|
1274
|
+
const baseUrl = this.esriServiceOptions.url;
|
|
1275
|
+
// Check if this is a FeatureServer that supports vector tiles
|
|
1276
|
+
// Most FeatureServers don't have VectorTileServer endpoints
|
|
1277
|
+
// We'll use a different approach for FeatureServers with vector tile capability
|
|
1278
|
+
// Try to construct vector tile URL from FeatureServer URL
|
|
1279
|
+
// Some services have both FeatureServer and VectorTileServer endpoints
|
|
1280
|
+
const vectorTileUrl = baseUrl.replace('/FeatureServer/', '/VectorTileServer/');
|
|
1281
|
+
return `${vectorTileUrl}/tile/{z}/{y}/{x}.pbf`;
|
|
1282
|
+
}
|
|
1283
|
+
_buildQueryUrl() {
|
|
1284
|
+
const options = { ...this._defaultEsriOptions, ...this.esriServiceOptions };
|
|
1285
|
+
const baseUrl = `${options.url}/query`;
|
|
1286
|
+
const params = new URLSearchParams();
|
|
1287
|
+
// Add query parameters (FeatureServer /layerId/query does not accept 'layers')
|
|
1288
|
+
params.append('where', options.where || '1=1');
|
|
1289
|
+
params.append('outFields', Array.isArray(options.outFields) ? options.outFields.join(',') : options.outFields || '*');
|
|
1290
|
+
params.append('f', options.f || 'geojson');
|
|
1291
|
+
params.append('returnGeometry', (options.returnGeometry !== false).toString());
|
|
1292
|
+
// Add bounding box geometry if enabled and map is available
|
|
1293
|
+
if (options.useBoundingBox !== false && this._map) {
|
|
1294
|
+
const bounds = this._map.getBounds();
|
|
1295
|
+
if (bounds) {
|
|
1296
|
+
const geometry = {
|
|
1297
|
+
xmin: bounds.getWest(),
|
|
1298
|
+
ymin: bounds.getSouth(),
|
|
1299
|
+
xmax: bounds.getEast(),
|
|
1300
|
+
ymax: bounds.getNorth(),
|
|
1301
|
+
spatialReference: { wkid: 4326 },
|
|
1302
|
+
};
|
|
1303
|
+
params.append('geometry', JSON.stringify(geometry));
|
|
1304
|
+
params.append('geometryType', 'esriGeometryEnvelope');
|
|
1305
|
+
params.append('spatialRel', 'esriSpatialRelIntersects');
|
|
1306
|
+
params.append('inSR', '4326');
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
// Only include geometry-related params when geometry is present (and not bounding box)
|
|
1310
|
+
if (options.geometry && options.useBoundingBox === false) {
|
|
1311
|
+
params.append('geometry', JSON.stringify(options.geometry));
|
|
1312
|
+
if (options.geometryType)
|
|
1313
|
+
params.append('geometryType', options.geometryType);
|
|
1314
|
+
if (options.spatialRel)
|
|
1315
|
+
params.append('spatialRel', options.spatialRel);
|
|
1316
|
+
if (options.inSR)
|
|
1317
|
+
params.append('inSR', options.inSR);
|
|
1318
|
+
}
|
|
1319
|
+
if (options.outSR)
|
|
1320
|
+
params.append('outSR', options.outSR);
|
|
1321
|
+
if (options.orderByFields)
|
|
1322
|
+
params.append('orderByFields', options.orderByFields);
|
|
1323
|
+
if (options.groupByFieldsForStatistics)
|
|
1324
|
+
params.append('groupByFieldsForStatistics', options.groupByFieldsForStatistics);
|
|
1325
|
+
if (options.outStatistics && options.outStatistics.length > 0)
|
|
1326
|
+
params.append('outStatistics', JSON.stringify(options.outStatistics));
|
|
1327
|
+
if (options.having)
|
|
1328
|
+
params.append('having', options.having);
|
|
1329
|
+
if (options.resultOffset)
|
|
1330
|
+
params.append('resultOffset', options.resultOffset.toString());
|
|
1331
|
+
if (options.resultRecordCount)
|
|
1332
|
+
params.append('resultRecordCount', options.resultRecordCount.toString());
|
|
1333
|
+
if (options.token)
|
|
1334
|
+
params.append('token', options.token);
|
|
1335
|
+
return `${baseUrl}?${params.toString()}`;
|
|
1336
|
+
}
|
|
1337
|
+
get _source() {
|
|
1338
|
+
return this._map.getSource(this._sourceId);
|
|
1339
|
+
}
|
|
1340
|
+
get _url() {
|
|
1341
|
+
return this._buildQueryUrl();
|
|
1342
|
+
}
|
|
1343
|
+
get serviceMetadata() {
|
|
1344
|
+
return this._serviceMetadata;
|
|
1345
|
+
}
|
|
1346
|
+
get defaultStyle() {
|
|
1347
|
+
if (this._defaultStyleData)
|
|
1348
|
+
return this._defaultStyleData;
|
|
1349
|
+
// Generate default style based on geometry type from service metadata
|
|
1350
|
+
const geometryType = String(this._serviceMetadata?.geometryType || 'esriGeometryPoint');
|
|
1351
|
+
const isVectorTiles = this.esriServiceOptions.useVectorTiles !== false;
|
|
1352
|
+
// For vector tiles, we need to include source-layer
|
|
1353
|
+
const baseStyle = {
|
|
1354
|
+
source: this._sourceId,
|
|
1355
|
+
};
|
|
1356
|
+
if (isVectorTiles && this._serviceMetadata?.name) {
|
|
1357
|
+
baseStyle['source-layer'] = String(this._serviceMetadata.name);
|
|
1358
|
+
}
|
|
1359
|
+
if (geometryType.includes('Point')) {
|
|
1360
|
+
return {
|
|
1361
|
+
type: 'circle',
|
|
1362
|
+
...baseStyle,
|
|
1363
|
+
paint: {
|
|
1364
|
+
'circle-radius': 4,
|
|
1365
|
+
'circle-color': '#3b82f6',
|
|
1366
|
+
'circle-stroke-color': '#1e40af',
|
|
1367
|
+
'circle-stroke-width': 1,
|
|
1368
|
+
},
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
else if (geometryType.includes('Polyline')) {
|
|
1372
|
+
return {
|
|
1373
|
+
type: 'line',
|
|
1374
|
+
...baseStyle,
|
|
1375
|
+
paint: {
|
|
1376
|
+
'line-color': '#3b82f6',
|
|
1377
|
+
'line-width': 2,
|
|
1378
|
+
},
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
else if (geometryType.includes('Polygon')) {
|
|
1382
|
+
return {
|
|
1383
|
+
type: 'fill',
|
|
1384
|
+
...baseStyle,
|
|
1385
|
+
paint: {
|
|
1386
|
+
'fill-color': 'rgba(59, 130, 246, 0.4)',
|
|
1387
|
+
'fill-outline-color': '#1e40af',
|
|
1388
|
+
},
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
// Default to circle for unknown geometry types
|
|
1392
|
+
return {
|
|
1393
|
+
type: 'circle',
|
|
1394
|
+
...baseStyle,
|
|
1395
|
+
paint: {
|
|
1396
|
+
'circle-radius': 4,
|
|
1397
|
+
'circle-color': '#3b82f6',
|
|
1398
|
+
'circle-stroke-color': '#1e40af',
|
|
1399
|
+
'circle-stroke-width': 1,
|
|
1400
|
+
},
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
getStyle() {
|
|
1404
|
+
return new Promise((resolve, reject) => {
|
|
1405
|
+
if (this._serviceMetadata) {
|
|
1406
|
+
resolve(this.defaultStyle);
|
|
1407
|
+
}
|
|
1408
|
+
else {
|
|
1409
|
+
// Wait for service metadata to be loaded
|
|
1410
|
+
this._getServiceMetadata()
|
|
1411
|
+
.then(() => resolve(this.defaultStyle))
|
|
1412
|
+
.catch(error => reject(error));
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
async _getServiceMetadata() {
|
|
1417
|
+
if (this._serviceMetadata)
|
|
1418
|
+
return;
|
|
1419
|
+
this._serviceMetadata = await getServiceDetails(this.esriServiceOptions.url, this.esriServiceOptions.fetchOptions);
|
|
1420
|
+
}
|
|
1421
|
+
updateData() {
|
|
1422
|
+
// For GeoJSON sources with bounding box filtering, update the data URL
|
|
1423
|
+
if (this.esriServiceOptions.useVectorTiles === false &&
|
|
1424
|
+
this.esriServiceOptions.useBoundingBox) {
|
|
1425
|
+
const source = this._map.getSource(this._sourceId);
|
|
1426
|
+
if (source && 'setData' in source && typeof source.setData === 'function') {
|
|
1427
|
+
const newQueryUrl = this._buildQueryUrl();
|
|
1428
|
+
const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
|
|
1429
|
+
if (!isTestEnvironment) {
|
|
1430
|
+
console.log('Updating FeatureService data with new bounding box:', newQueryUrl);
|
|
1431
|
+
}
|
|
1432
|
+
// @ts-ignore - GeoJSON source setData method not in generic Source type
|
|
1433
|
+
source.setData(newQueryUrl);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
// Vector tile sources don't need dynamic data updates like GeoJSON
|
|
1438
|
+
// The tiles are fetched automatically based on the tile URL
|
|
1439
|
+
// For filtering, we would need to recreate the source or use layer filtering
|
|
1440
|
+
console.warn('updateData() is only applicable for GeoJSON sources with bounding box filtering.');
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
_setupBoundingBoxUpdates() {
|
|
1444
|
+
if (this._boundingBoxUpdateHandler) {
|
|
1445
|
+
// Remove existing handler
|
|
1446
|
+
this._map.off('moveend', this._boundingBoxUpdateHandler);
|
|
1447
|
+
this._map.off('zoomend', this._boundingBoxUpdateHandler);
|
|
1448
|
+
}
|
|
1449
|
+
// Debounced update function to prevent excessive API calls
|
|
1450
|
+
let updateTimeout = null;
|
|
1451
|
+
this._boundingBoxUpdateHandler = () => {
|
|
1452
|
+
if (updateTimeout) {
|
|
1453
|
+
clearTimeout(updateTimeout);
|
|
1454
|
+
}
|
|
1455
|
+
updateTimeout = setTimeout(() => {
|
|
1456
|
+
this.updateData();
|
|
1457
|
+
}, 300); // 300ms debounce
|
|
1458
|
+
};
|
|
1459
|
+
// Listen for map movement and zoom changes
|
|
1460
|
+
this._map.on('moveend', this._boundingBoxUpdateHandler);
|
|
1461
|
+
this._map.on('zoomend', this._boundingBoxUpdateHandler);
|
|
1462
|
+
}
|
|
1463
|
+
_removeBoundingBoxUpdates() {
|
|
1464
|
+
if (this._boundingBoxUpdateHandler) {
|
|
1465
|
+
this._map.off('moveend', this._boundingBoxUpdateHandler);
|
|
1466
|
+
this._map.off('zoomend', this._boundingBoxUpdateHandler);
|
|
1467
|
+
this._boundingBoxUpdateHandler = null;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
updateSource() {
|
|
1471
|
+
// Remove existing source and recreate with new parameters
|
|
1472
|
+
if (this._map.getSource(this._sourceId)) {
|
|
1473
|
+
this._map.removeSource(this._sourceId);
|
|
1474
|
+
}
|
|
1475
|
+
this._createSource();
|
|
1476
|
+
}
|
|
1477
|
+
setWhere(whereClause) {
|
|
1478
|
+
this.esriServiceOptions.where = whereClause;
|
|
1479
|
+
this.updateSource();
|
|
1480
|
+
}
|
|
1481
|
+
setOutFields(fields) {
|
|
1482
|
+
this.esriServiceOptions.outFields = fields;
|
|
1483
|
+
this.updateSource();
|
|
1484
|
+
}
|
|
1485
|
+
setLayers(layers) {
|
|
1486
|
+
this.esriServiceOptions.layers = layers;
|
|
1487
|
+
this.updateSource();
|
|
1488
|
+
}
|
|
1489
|
+
setGeometry(geometry, geometryType) {
|
|
1490
|
+
this.esriServiceOptions.geometry = geometry;
|
|
1491
|
+
if (geometryType) {
|
|
1492
|
+
this.esriServiceOptions.geometryType = geometryType;
|
|
1493
|
+
}
|
|
1494
|
+
this.updateSource();
|
|
1495
|
+
}
|
|
1496
|
+
clearGeometry() {
|
|
1497
|
+
delete this.esriServiceOptions.geometry;
|
|
1498
|
+
delete this.esriServiceOptions.geometryType;
|
|
1499
|
+
this.updateSource();
|
|
1500
|
+
}
|
|
1501
|
+
setBoundingBoxFilter(enabled) {
|
|
1502
|
+
this.esriServiceOptions.useBoundingBox = enabled;
|
|
1503
|
+
if (enabled && this.esriServiceOptions.useVectorTiles === false) {
|
|
1504
|
+
this._setupBoundingBoxUpdates();
|
|
1505
|
+
}
|
|
1506
|
+
else {
|
|
1507
|
+
this._removeBoundingBoxUpdates();
|
|
1508
|
+
}
|
|
1509
|
+
this.updateSource();
|
|
1510
|
+
}
|
|
1511
|
+
// Note: maxRecordCount is a server capability; not settable via query params
|
|
1512
|
+
remove() {
|
|
1513
|
+
this._removeBoundingBoxUpdates();
|
|
1514
|
+
if (this._map.getSource(this._sourceId)) {
|
|
1515
|
+
this._map.removeSource(this._sourceId);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
async queryFeatures(options) {
|
|
1519
|
+
const queryOptions = { ...this.esriServiceOptions, ...options };
|
|
1520
|
+
const queryUrl = this._buildQueryUrlWithOptions(queryOptions);
|
|
1521
|
+
try {
|
|
1522
|
+
const response = await fetch(queryUrl, this.esriServiceOptions.fetchOptions);
|
|
1523
|
+
if (!response.ok) {
|
|
1524
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
1525
|
+
}
|
|
1526
|
+
return await response.json();
|
|
1527
|
+
}
|
|
1528
|
+
catch (error) {
|
|
1529
|
+
const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
|
|
1530
|
+
if (!isTestEnvironment) {
|
|
1531
|
+
console.error('Error querying features:', error);
|
|
1532
|
+
}
|
|
1533
|
+
throw error;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
_buildQueryUrlWithOptions(options) {
|
|
1537
|
+
const mergedOptions = { ...this._defaultEsriOptions, ...options };
|
|
1538
|
+
const baseUrl = `${mergedOptions.url}/query`;
|
|
1539
|
+
const params = new URLSearchParams();
|
|
1540
|
+
// Add query parameters using the same logic as _buildQueryUrl (omit 'layers')
|
|
1541
|
+
params.append('where', mergedOptions.where || '1=1');
|
|
1542
|
+
params.append('outFields', Array.isArray(mergedOptions.outFields)
|
|
1543
|
+
? mergedOptions.outFields.join(',')
|
|
1544
|
+
: mergedOptions.outFields || '*');
|
|
1545
|
+
params.append('f', mergedOptions.f || 'geojson');
|
|
1546
|
+
params.append('returnGeometry', (mergedOptions.returnGeometry !== false).toString());
|
|
1547
|
+
if (mergedOptions.geometry) {
|
|
1548
|
+
params.append('geometry', JSON.stringify(mergedOptions.geometry));
|
|
1549
|
+
if (mergedOptions.geometryType)
|
|
1550
|
+
params.append('geometryType', mergedOptions.geometryType);
|
|
1551
|
+
if (mergedOptions.spatialRel)
|
|
1552
|
+
params.append('spatialRel', mergedOptions.spatialRel);
|
|
1553
|
+
if (mergedOptions.inSR)
|
|
1554
|
+
params.append('inSR', mergedOptions.inSR);
|
|
1555
|
+
}
|
|
1556
|
+
if (mergedOptions.outSR)
|
|
1557
|
+
params.append('outSR', mergedOptions.outSR);
|
|
1558
|
+
if (mergedOptions.orderByFields)
|
|
1559
|
+
params.append('orderByFields', mergedOptions.orderByFields);
|
|
1560
|
+
if (mergedOptions.groupByFieldsForStatistics)
|
|
1561
|
+
params.append('groupByFieldsForStatistics', mergedOptions.groupByFieldsForStatistics);
|
|
1562
|
+
if (mergedOptions.outStatistics && mergedOptions.outStatistics.length > 0)
|
|
1563
|
+
params.append('outStatistics', JSON.stringify(mergedOptions.outStatistics));
|
|
1564
|
+
if (mergedOptions.having)
|
|
1565
|
+
params.append('having', mergedOptions.having);
|
|
1566
|
+
if (mergedOptions.resultOffset)
|
|
1567
|
+
params.append('resultOffset', mergedOptions.resultOffset.toString());
|
|
1568
|
+
if (mergedOptions.resultRecordCount)
|
|
1569
|
+
params.append('resultRecordCount', mergedOptions.resultRecordCount.toString());
|
|
1570
|
+
if (mergedOptions.token)
|
|
1571
|
+
params.append('token', mergedOptions.token);
|
|
1572
|
+
return `${baseUrl}?${params.toString()}`;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Base Task class for ArcGIS REST API operations
|
|
1578
|
+
* Similar to Esri Leaflet's Task class
|
|
1579
|
+
*/
|
|
1580
|
+
class Task {
|
|
1581
|
+
constructor(endpoint) {
|
|
1582
|
+
Object.defineProperty(this, "_service", {
|
|
1583
|
+
enumerable: true,
|
|
1584
|
+
configurable: true,
|
|
1585
|
+
writable: true,
|
|
1586
|
+
value: void 0
|
|
1587
|
+
});
|
|
1588
|
+
Object.defineProperty(this, "options", {
|
|
1589
|
+
enumerable: true,
|
|
1590
|
+
configurable: true,
|
|
1591
|
+
writable: true,
|
|
1592
|
+
value: void 0
|
|
1593
|
+
});
|
|
1594
|
+
Object.defineProperty(this, "params", {
|
|
1595
|
+
enumerable: true,
|
|
1596
|
+
configurable: true,
|
|
1597
|
+
writable: true,
|
|
1598
|
+
value: {}
|
|
1599
|
+
});
|
|
1600
|
+
Object.defineProperty(this, "path", {
|
|
1601
|
+
enumerable: true,
|
|
1602
|
+
configurable: true,
|
|
1603
|
+
writable: true,
|
|
1604
|
+
value: ''
|
|
1605
|
+
});
|
|
1606
|
+
Object.defineProperty(this, "setters", {
|
|
1607
|
+
enumerable: true,
|
|
1608
|
+
configurable: true,
|
|
1609
|
+
writable: true,
|
|
1610
|
+
value: {}
|
|
1611
|
+
});
|
|
1612
|
+
// endpoint can be either a url (and options) for an ArcGIS Rest Service or an instance of Service
|
|
1613
|
+
if (endpoint && typeof endpoint === 'object' && 'request' in endpoint) {
|
|
1614
|
+
this._service = endpoint;
|
|
1615
|
+
this.options = {}; // Service will handle its own options
|
|
1616
|
+
}
|
|
1617
|
+
else if (typeof endpoint === 'string') {
|
|
1618
|
+
this.options = { url: cleanTrailingSlash(endpoint) };
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
this.options = { ...endpoint };
|
|
1622
|
+
if (this.options.url) {
|
|
1623
|
+
this.options.url = cleanTrailingSlash(this.options.url);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
// Initialize params if not already set by subclass
|
|
1627
|
+
if (!this.params) {
|
|
1628
|
+
this.params = {};
|
|
1629
|
+
}
|
|
1630
|
+
// Generate setter methods based on the setters object
|
|
1631
|
+
if (this.setters) {
|
|
1632
|
+
for (const setter in this.setters) {
|
|
1633
|
+
const param = this.setters[setter];
|
|
1634
|
+
this[setter] = this.generateSetter(param, this);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Generate a method for each methodName:paramName in the setters for this task
|
|
1640
|
+
*/
|
|
1641
|
+
generateSetter(param, context) {
|
|
1642
|
+
return function (value) {
|
|
1643
|
+
context.params[param] = value;
|
|
1644
|
+
return context;
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Set authentication token
|
|
1649
|
+
*/
|
|
1650
|
+
token(token) {
|
|
1651
|
+
if (this._service) {
|
|
1652
|
+
this._service.authenticate(token);
|
|
1653
|
+
}
|
|
1654
|
+
else {
|
|
1655
|
+
this.params.token = token;
|
|
1656
|
+
}
|
|
1657
|
+
return this;
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Set API key (alias for token)
|
|
1661
|
+
*/
|
|
1662
|
+
apikey(apikey) {
|
|
1663
|
+
return this.token(apikey);
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Set whether to return formatted or unformatted values (ArcGIS Server 10.5+)
|
|
1667
|
+
*/
|
|
1668
|
+
format(formatted) {
|
|
1669
|
+
this.params.returnUnformattedValues = !formatted;
|
|
1670
|
+
return this;
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Execute the task request with callback (Esri Leaflet style)
|
|
1674
|
+
*/
|
|
1675
|
+
run(callback) {
|
|
1676
|
+
if (this.options.requestParams) {
|
|
1677
|
+
Object.assign(this.params, this.options.requestParams);
|
|
1678
|
+
}
|
|
1679
|
+
if (this._service) {
|
|
1680
|
+
this._service.requestWithCallback('POST', this.path, this.params, callback);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
// Direct request fallback
|
|
1684
|
+
this._request('POST', this.path, this.params, callback);
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Execute the task request with Promise
|
|
1688
|
+
*/
|
|
1689
|
+
async request() {
|
|
1690
|
+
if (this.options.requestParams) {
|
|
1691
|
+
Object.assign(this.params, this.options.requestParams);
|
|
1692
|
+
}
|
|
1693
|
+
if (this._service) {
|
|
1694
|
+
return this._service.requestWithCallback('POST', this.path, this.params);
|
|
1695
|
+
}
|
|
1696
|
+
return new Promise((resolve, reject) => {
|
|
1697
|
+
this._request('POST', this.path, this.params, (error, response) => {
|
|
1698
|
+
if (error) {
|
|
1699
|
+
reject(error);
|
|
1700
|
+
}
|
|
1701
|
+
else {
|
|
1702
|
+
resolve(response);
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Direct HTTP request (when not using a service)
|
|
1709
|
+
*/
|
|
1710
|
+
_request(method, path, params, callback) {
|
|
1711
|
+
if (!this.options.url) {
|
|
1712
|
+
callback(new Error('URL is required for task execution'));
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
// Ensure proper URL construction with path separator
|
|
1716
|
+
const baseUrl = this.options.url.endsWith('/')
|
|
1717
|
+
? this.options.url.slice(0, -1)
|
|
1718
|
+
: this.options.url;
|
|
1719
|
+
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
|
1720
|
+
const fullServiceUrl = `${baseUrl}${cleanPath}`;
|
|
1721
|
+
const url = this.options.proxy ? `${this.options.proxy}?${fullServiceUrl}` : fullServiceUrl;
|
|
1722
|
+
// Convert params to URLSearchParams
|
|
1723
|
+
const searchParams = new URLSearchParams();
|
|
1724
|
+
Object.keys(params).forEach(key => {
|
|
1725
|
+
const value = params[key];
|
|
1726
|
+
if (value !== undefined && value !== null) {
|
|
1727
|
+
if (Array.isArray(value)) {
|
|
1728
|
+
searchParams.append(key, value.join(','));
|
|
1729
|
+
}
|
|
1730
|
+
else if (typeof value === 'object') {
|
|
1731
|
+
searchParams.append(key, JSON.stringify(value));
|
|
1732
|
+
}
|
|
1733
|
+
else {
|
|
1734
|
+
searchParams.append(key, value.toString());
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
const fullUrl = method === 'GET' ? `${url}?${searchParams.toString()}` : url;
|
|
1739
|
+
const fetchOptions = {
|
|
1740
|
+
method,
|
|
1741
|
+
headers: {
|
|
1742
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1743
|
+
},
|
|
1744
|
+
};
|
|
1745
|
+
if (method === 'POST') {
|
|
1746
|
+
fetchOptions.body = searchParams.toString();
|
|
1747
|
+
}
|
|
1748
|
+
fetch(fullUrl, fetchOptions)
|
|
1749
|
+
.then(response => {
|
|
1750
|
+
if (!response.ok) {
|
|
1751
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
1752
|
+
}
|
|
1753
|
+
return response.json();
|
|
1754
|
+
})
|
|
1755
|
+
.then(data => callback(undefined, data))
|
|
1756
|
+
.catch(error => callback(error));
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* Query task for ArcGIS Feature Services
|
|
1762
|
+
* Based on Esri Leaflet's Query functionality
|
|
1763
|
+
*/
|
|
1764
|
+
class Query extends Task {
|
|
1765
|
+
constructor(options) {
|
|
1766
|
+
super(options);
|
|
1767
|
+
Object.defineProperty(this, "setters", {
|
|
1768
|
+
enumerable: true,
|
|
1769
|
+
configurable: true,
|
|
1770
|
+
writable: true,
|
|
1771
|
+
value: {
|
|
1772
|
+
offset: 'resultOffset',
|
|
1773
|
+
limit: 'resultRecordCount',
|
|
1774
|
+
fields: 'outFields',
|
|
1775
|
+
precision: 'geometryPrecision',
|
|
1776
|
+
featureIds: 'objectIds',
|
|
1777
|
+
returnGeometry: 'returnGeometry',
|
|
1778
|
+
returnM: 'returnM',
|
|
1779
|
+
transform: 'datumTransformation',
|
|
1780
|
+
token: 'token',
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
Object.defineProperty(this, "path", {
|
|
1784
|
+
enumerable: true,
|
|
1785
|
+
configurable: true,
|
|
1786
|
+
writable: true,
|
|
1787
|
+
value: 'query'
|
|
1788
|
+
});
|
|
1789
|
+
Object.defineProperty(this, "params", {
|
|
1790
|
+
enumerable: true,
|
|
1791
|
+
configurable: true,
|
|
1792
|
+
writable: true,
|
|
1793
|
+
value: {
|
|
1794
|
+
returnGeometry: true,
|
|
1795
|
+
where: '1=1',
|
|
1796
|
+
outSR: 4326,
|
|
1797
|
+
outFields: '*',
|
|
1798
|
+
f: 'json',
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
this.path = 'query';
|
|
1802
|
+
// If options is a QueryOptions object, merge relevant properties into params
|
|
1803
|
+
if (options &&
|
|
1804
|
+
typeof options === 'object' &&
|
|
1805
|
+
!('request' in options) &&
|
|
1806
|
+
typeof options !== 'string') {
|
|
1807
|
+
const queryOptions = options;
|
|
1808
|
+
// Merge query-specific options into params
|
|
1809
|
+
if (queryOptions.where !== undefined)
|
|
1810
|
+
this.params.where = queryOptions.where;
|
|
1811
|
+
if (queryOptions.outFields !== undefined)
|
|
1812
|
+
this.params.outFields = queryOptions.outFields;
|
|
1813
|
+
if (queryOptions.returnGeometry !== undefined)
|
|
1814
|
+
this.params.returnGeometry = queryOptions.returnGeometry;
|
|
1815
|
+
if (queryOptions.spatialRel !== undefined)
|
|
1816
|
+
this.params.spatialRel = queryOptions.spatialRel;
|
|
1817
|
+
if (queryOptions.geometry !== undefined)
|
|
1818
|
+
this.params.geometry = queryOptions.geometry;
|
|
1819
|
+
if (queryOptions.geometryType !== undefined)
|
|
1820
|
+
this.params.geometryType = queryOptions.geometryType;
|
|
1821
|
+
if (queryOptions.inSR !== undefined)
|
|
1822
|
+
this.params.inSR = queryOptions.inSR;
|
|
1823
|
+
if (queryOptions.outSR !== undefined)
|
|
1824
|
+
this.params.outSR = queryOptions.outSR;
|
|
1825
|
+
if (queryOptions.returnDistinctValues !== undefined)
|
|
1826
|
+
this.params.returnDistinctValues = queryOptions.returnDistinctValues;
|
|
1827
|
+
if (queryOptions.returnIdsOnly !== undefined)
|
|
1828
|
+
this.params.returnIdsOnly = queryOptions.returnIdsOnly;
|
|
1829
|
+
if (queryOptions.returnCountOnly !== undefined)
|
|
1830
|
+
this.params.returnCountOnly = queryOptions.returnCountOnly;
|
|
1831
|
+
if (queryOptions.returnExtentOnly !== undefined)
|
|
1832
|
+
this.params.returnExtentOnly = queryOptions.returnExtentOnly;
|
|
1833
|
+
if (queryOptions.orderByFields !== undefined)
|
|
1834
|
+
this.params.orderByFields = queryOptions.orderByFields;
|
|
1835
|
+
if (queryOptions.groupByFieldsForStatistics !== undefined)
|
|
1836
|
+
this.params.groupByFieldsForStatistics = queryOptions.groupByFieldsForStatistics;
|
|
1837
|
+
if (queryOptions.outStatistics !== undefined)
|
|
1838
|
+
this.params.outStatistics = queryOptions.outStatistics;
|
|
1839
|
+
if (queryOptions.resultOffset !== undefined)
|
|
1840
|
+
this.params.resultOffset = queryOptions.resultOffset;
|
|
1841
|
+
if (queryOptions.resultRecordCount !== undefined)
|
|
1842
|
+
this.params.resultRecordCount = queryOptions.resultRecordCount;
|
|
1843
|
+
if (queryOptions.maxAllowableOffset !== undefined)
|
|
1844
|
+
this.params.maxAllowableOffset = queryOptions.maxAllowableOffset;
|
|
1845
|
+
if (queryOptions.geometryPrecision !== undefined)
|
|
1846
|
+
this.params.geometryPrecision = queryOptions.geometryPrecision;
|
|
1847
|
+
if (queryOptions.time !== undefined)
|
|
1848
|
+
this.params.time = queryOptions.time;
|
|
1849
|
+
if (queryOptions.gdbVersion !== undefined)
|
|
1850
|
+
this.params.gdbVersion = queryOptions.gdbVersion;
|
|
1851
|
+
if (queryOptions.historicMoment !== undefined)
|
|
1852
|
+
this.params.historicMoment = queryOptions.historicMoment;
|
|
1853
|
+
if (queryOptions.returnTrueCurves !== undefined)
|
|
1854
|
+
this.params.returnTrueCurves = queryOptions.returnTrueCurves;
|
|
1855
|
+
if (queryOptions.returnZ !== undefined)
|
|
1856
|
+
this.params.returnZ = queryOptions.returnZ;
|
|
1857
|
+
if (queryOptions.returnM !== undefined)
|
|
1858
|
+
this.params.returnM = queryOptions.returnM;
|
|
1859
|
+
if (queryOptions.token !== undefined)
|
|
1860
|
+
this.params.token = queryOptions.token;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
// Spatial relationship methods
|
|
1864
|
+
/**
|
|
1865
|
+
* Returns a feature if its shape is wholly contained within the search geometry
|
|
1866
|
+
*/
|
|
1867
|
+
within(geometry) {
|
|
1868
|
+
this._setGeometryParams(geometry);
|
|
1869
|
+
this.params.spatialRel = 'esriSpatialRelContains';
|
|
1870
|
+
return this;
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Returns a feature if any spatial relationship is found
|
|
1874
|
+
*/
|
|
1875
|
+
intersects(geometry) {
|
|
1876
|
+
this._setGeometryParams(geometry);
|
|
1877
|
+
this.params.spatialRel = 'esriSpatialRelIntersects';
|
|
1878
|
+
return this;
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Returns a feature if its shape wholly contains the search geometry
|
|
1882
|
+
*/
|
|
1883
|
+
contains(geometry) {
|
|
1884
|
+
this._setGeometryParams(geometry);
|
|
1885
|
+
this.params.spatialRel = 'esriSpatialRelWithin';
|
|
1886
|
+
return this;
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Returns a feature if the intersection of the interiors is not empty and has lower dimension
|
|
1890
|
+
*/
|
|
1891
|
+
crosses(geometry) {
|
|
1892
|
+
this._setGeometryParams(geometry);
|
|
1893
|
+
this.params.spatialRel = 'esriSpatialRelCrosses';
|
|
1894
|
+
return this;
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Returns a feature if the two shapes share a common boundary
|
|
1898
|
+
*/
|
|
1899
|
+
touches(geometry) {
|
|
1900
|
+
this._setGeometryParams(geometry);
|
|
1901
|
+
this.params.spatialRel = 'esriSpatialRelTouches';
|
|
1902
|
+
return this;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Returns a feature if the intersection results in same dimension but different from both shapes
|
|
1906
|
+
*/
|
|
1907
|
+
overlaps(geometry) {
|
|
1908
|
+
this._setGeometryParams(geometry);
|
|
1909
|
+
this.params.spatialRel = 'esriSpatialRelOverlaps';
|
|
1910
|
+
return this;
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Returns a feature if the envelope of the two shapes intersects
|
|
1914
|
+
*/
|
|
1915
|
+
bboxIntersects(geometry) {
|
|
1916
|
+
this._setGeometryParams(geometry);
|
|
1917
|
+
this.params.spatialRel = 'esriSpatialRelEnvelopeIntersects';
|
|
1918
|
+
return this;
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Nearby search - only valid for ArcGIS Server 10.3+ or ArcGIS Online
|
|
1922
|
+
*/
|
|
1923
|
+
nearby(latlng, radius) {
|
|
1924
|
+
this.params.geometry = [latlng.lng, latlng.lat];
|
|
1925
|
+
this.params.geometryType = 'esriGeometryPoint';
|
|
1926
|
+
this.params.spatialRel = 'esriSpatialRelIntersects';
|
|
1927
|
+
this.params.units = 'esriSRUnit_Meter';
|
|
1928
|
+
this.params.distance = radius;
|
|
1929
|
+
this.params.inSR = 4326;
|
|
1930
|
+
return this;
|
|
1931
|
+
}
|
|
1932
|
+
// Query methods
|
|
1933
|
+
/**
|
|
1934
|
+
* Set WHERE clause for the query
|
|
1935
|
+
*/
|
|
1936
|
+
where(whereClause) {
|
|
1937
|
+
this.params.where = whereClause;
|
|
1938
|
+
return this;
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Set time range for temporal queries
|
|
1942
|
+
*/
|
|
1943
|
+
between(start, end) {
|
|
1944
|
+
const startTime = start instanceof Date ? start.valueOf() : start;
|
|
1945
|
+
const endTime = end instanceof Date ? end.valueOf() : end;
|
|
1946
|
+
this.params.time = [startTime, endTime];
|
|
1947
|
+
return this;
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Simplify geometries based on map resolution
|
|
1951
|
+
*/
|
|
1952
|
+
simplify(map, factor) {
|
|
1953
|
+
const bounds = map.getBounds();
|
|
1954
|
+
const mapWidth = Math.abs(bounds._northEast.lng - bounds._southWest.lng);
|
|
1955
|
+
this.params.maxAllowableOffset = (mapWidth / map.getSize().x) * factor;
|
|
1956
|
+
return this;
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Set order by fields
|
|
1960
|
+
*/
|
|
1961
|
+
orderBy(fieldName, order = 'ASC') {
|
|
1962
|
+
const currentOrder = this.params.orderByFields || '';
|
|
1963
|
+
this.params.orderByFields = currentOrder
|
|
1964
|
+
? `${currentOrder},${fieldName} ${order}`
|
|
1965
|
+
: `${fieldName} ${order}`;
|
|
1966
|
+
return this;
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Set specific layer to query (for Map Services)
|
|
1970
|
+
*/
|
|
1971
|
+
layer(layerId) {
|
|
1972
|
+
this.path = `${layerId}/query`;
|
|
1973
|
+
return this;
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Return only distinct values
|
|
1977
|
+
*/
|
|
1978
|
+
distinct() {
|
|
1979
|
+
this.params.returnGeometry = false;
|
|
1980
|
+
this.params.returnDistinctValues = true;
|
|
1981
|
+
return this;
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Set pixel size for image services
|
|
1985
|
+
*/
|
|
1986
|
+
pixelSize(point) {
|
|
1987
|
+
this.params.pixelSize = [point.x, point.y];
|
|
1988
|
+
return this;
|
|
1989
|
+
}
|
|
1990
|
+
// Execution methods
|
|
1991
|
+
/**
|
|
1992
|
+
* Execute the query and return features
|
|
1993
|
+
*/
|
|
1994
|
+
async run() {
|
|
1995
|
+
this._cleanParams();
|
|
1996
|
+
// Use GeoJSON format if supported
|
|
1997
|
+
this.params.f = 'geojson';
|
|
1998
|
+
try {
|
|
1999
|
+
const response = await this.request();
|
|
2000
|
+
return response;
|
|
2001
|
+
}
|
|
2002
|
+
catch {
|
|
2003
|
+
// Fallback to JSON format and convert
|
|
2004
|
+
this.params.f = 'json';
|
|
2005
|
+
const response = await this.request();
|
|
2006
|
+
return this._convertToGeoJSON(response);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Execute the query and return only the count
|
|
2011
|
+
*/
|
|
2012
|
+
async count() {
|
|
2013
|
+
this._cleanParams();
|
|
2014
|
+
this.params.returnCountOnly = true;
|
|
2015
|
+
const response = await this.request();
|
|
2016
|
+
return response.count;
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Execute the query and return only feature IDs
|
|
2020
|
+
*/
|
|
2021
|
+
async ids() {
|
|
2022
|
+
this._cleanParams();
|
|
2023
|
+
this.params.returnIdsOnly = true;
|
|
2024
|
+
const response = await this.request();
|
|
2025
|
+
return response.objectIds;
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Execute the query and return extent bounds (ArcGIS Server 10.3+)
|
|
2029
|
+
*/
|
|
2030
|
+
async bounds() {
|
|
2031
|
+
this._cleanParams();
|
|
2032
|
+
this.params.returnExtentOnly = true;
|
|
2033
|
+
const response = await this.request();
|
|
2034
|
+
if (!response.extent) {
|
|
2035
|
+
throw new Error('Invalid bounds returned');
|
|
2036
|
+
}
|
|
2037
|
+
return {
|
|
2038
|
+
_southWest: { lat: response.extent.ymin, lng: response.extent.xmin },
|
|
2039
|
+
_northEast: { lat: response.extent.ymax, lng: response.extent.xmax },
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
// Private methods
|
|
2043
|
+
_setGeometryParams(geometry) {
|
|
2044
|
+
this.params.inSR = 4326;
|
|
2045
|
+
const converted = this._setGeometry(geometry);
|
|
2046
|
+
this.params.geometry = converted.geometry;
|
|
2047
|
+
this.params.geometryType = converted.geometryType;
|
|
2048
|
+
}
|
|
2049
|
+
_setGeometry(geometry) {
|
|
2050
|
+
if (!geometry) {
|
|
2051
|
+
return { geometry: null, geometryType: 'esriGeometryPoint' };
|
|
2052
|
+
}
|
|
2053
|
+
// Handle different geometry types
|
|
2054
|
+
if (typeof geometry === 'object' && geometry !== null) {
|
|
2055
|
+
const geom = geometry;
|
|
2056
|
+
if ('lat' in geom && 'lng' in geom) {
|
|
2057
|
+
// Leaflet LatLng-like object
|
|
2058
|
+
return {
|
|
2059
|
+
geometry: { x: geom.lng, y: geom.lat, spatialReference: { wkid: 4326 } },
|
|
2060
|
+
geometryType: 'esriGeometryPoint',
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
if ('_southWest' in geom && '_northEast' in geom) {
|
|
2064
|
+
// Leaflet Bounds-like object
|
|
2065
|
+
const bounds = geom;
|
|
2066
|
+
return {
|
|
2067
|
+
geometry: {
|
|
2068
|
+
xmin: bounds._southWest.lng,
|
|
2069
|
+
ymin: bounds._southWest.lat,
|
|
2070
|
+
xmax: bounds._northEast.lng,
|
|
2071
|
+
ymax: bounds._northEast.lat,
|
|
2072
|
+
spatialReference: { wkid: 4326 },
|
|
2073
|
+
},
|
|
2074
|
+
geometryType: 'esriGeometryEnvelope',
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
if ('xmin' in geom && 'ymin' in geom && 'xmax' in geom && 'ymax' in geom) {
|
|
2078
|
+
// Esri envelope geometry
|
|
2079
|
+
return {
|
|
2080
|
+
geometry,
|
|
2081
|
+
geometryType: 'esriGeometryEnvelope',
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
// Default: assume it's already in Esri geometry format
|
|
2086
|
+
return {
|
|
2087
|
+
geometry,
|
|
2088
|
+
geometryType: 'esriGeometryPoint',
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
_cleanParams() {
|
|
2092
|
+
delete this.params.returnIdsOnly;
|
|
2093
|
+
delete this.params.returnExtentOnly;
|
|
2094
|
+
delete this.params.returnCountOnly;
|
|
2095
|
+
delete this.params.returnDistinctValues;
|
|
2096
|
+
}
|
|
2097
|
+
_convertToGeoJSON(response) {
|
|
2098
|
+
// Handle cases where features might be undefined or empty
|
|
2099
|
+
const features = (response.features || []).map(feature => ({
|
|
2100
|
+
type: 'Feature',
|
|
2101
|
+
properties: feature.attributes,
|
|
2102
|
+
geometry: feature.geometry || null,
|
|
2103
|
+
}));
|
|
2104
|
+
return {
|
|
2105
|
+
type: 'FeatureCollection',
|
|
2106
|
+
features,
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
function query(options) {
|
|
2111
|
+
return new Query(options);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
/**
|
|
2115
|
+
* Find task for searching text in ArcGIS Map Services
|
|
2116
|
+
* Based on Esri Leaflet's Find functionality
|
|
2117
|
+
*/
|
|
2118
|
+
class Find extends Task {
|
|
2119
|
+
constructor(options) {
|
|
2120
|
+
super(options);
|
|
2121
|
+
Object.defineProperty(this, "setters", {
|
|
2122
|
+
enumerable: true,
|
|
2123
|
+
configurable: true,
|
|
2124
|
+
writable: true,
|
|
2125
|
+
value: {
|
|
2126
|
+
contains: 'contains',
|
|
2127
|
+
text: 'searchText',
|
|
2128
|
+
fields: 'searchFields',
|
|
2129
|
+
spatialReference: 'sr',
|
|
2130
|
+
sr: 'sr',
|
|
2131
|
+
layers: 'layers',
|
|
2132
|
+
returnGeometry: 'returnGeometry',
|
|
2133
|
+
maxAllowableOffset: 'maxAllowableOffset',
|
|
2134
|
+
precision: 'geometryPrecision',
|
|
2135
|
+
dynamicLayers: 'dynamicLayers',
|
|
2136
|
+
returnZ: 'returnZ',
|
|
2137
|
+
returnM: 'returnM',
|
|
2138
|
+
gdbVersion: 'gdbVersion',
|
|
2139
|
+
token: 'token',
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
Object.defineProperty(this, "path", {
|
|
2143
|
+
enumerable: true,
|
|
2144
|
+
configurable: true,
|
|
2145
|
+
writable: true,
|
|
2146
|
+
value: 'find'
|
|
2147
|
+
});
|
|
2148
|
+
Object.defineProperty(this, "params", {
|
|
2149
|
+
enumerable: true,
|
|
2150
|
+
configurable: true,
|
|
2151
|
+
writable: true,
|
|
2152
|
+
value: {
|
|
2153
|
+
searchText: '', // Required parameter
|
|
2154
|
+
layers: 'all', // Can be 'all' or comma-separated layer IDs
|
|
2155
|
+
contains: true,
|
|
2156
|
+
returnGeometry: true,
|
|
2157
|
+
f: 'json',
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
this.path = 'find';
|
|
2161
|
+
// If options is a FindOptions object, merge relevant properties into params
|
|
2162
|
+
if (options &&
|
|
2163
|
+
typeof options === 'object' &&
|
|
2164
|
+
!('request' in options) &&
|
|
2165
|
+
typeof options !== 'string') {
|
|
2166
|
+
const findOptions = options;
|
|
2167
|
+
// Merge find-specific options into params
|
|
2168
|
+
if (findOptions.searchText !== undefined)
|
|
2169
|
+
this.params.searchText = findOptions.searchText;
|
|
2170
|
+
if (findOptions.contains !== undefined)
|
|
2171
|
+
this.params.contains = findOptions.contains;
|
|
2172
|
+
if (findOptions.searchFields !== undefined) {
|
|
2173
|
+
this.params.searchFields = Array.isArray(findOptions.searchFields)
|
|
2174
|
+
? findOptions.searchFields.join(',')
|
|
2175
|
+
: findOptions.searchFields;
|
|
2176
|
+
}
|
|
2177
|
+
if (findOptions.sr !== undefined)
|
|
2178
|
+
this.params.sr = findOptions.sr;
|
|
2179
|
+
if (findOptions.layers !== undefined) {
|
|
2180
|
+
// Convert array to comma-separated string or use as-is if already string
|
|
2181
|
+
if (Array.isArray(findOptions.layers)) {
|
|
2182
|
+
this.params.layers = findOptions.layers.join(',');
|
|
2183
|
+
}
|
|
2184
|
+
else if (typeof findOptions.layers === 'string') {
|
|
2185
|
+
this.params.layers = findOptions.layers;
|
|
2186
|
+
}
|
|
2187
|
+
else {
|
|
2188
|
+
this.params.layers = findOptions.layers.toString();
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
if (findOptions.returnGeometry !== undefined)
|
|
2192
|
+
this.params.returnGeometry = findOptions.returnGeometry;
|
|
2193
|
+
if (findOptions.maxAllowableOffset !== undefined)
|
|
2194
|
+
this.params.maxAllowableOffset = findOptions.maxAllowableOffset;
|
|
2195
|
+
if (findOptions.geometryPrecision !== undefined)
|
|
2196
|
+
this.params.geometryPrecision = findOptions.geometryPrecision;
|
|
2197
|
+
if (findOptions.dynamicLayers !== undefined)
|
|
2198
|
+
this.params.dynamicLayers = findOptions.dynamicLayers;
|
|
2199
|
+
if (findOptions.returnZ !== undefined)
|
|
2200
|
+
this.params.returnZ = findOptions.returnZ;
|
|
2201
|
+
if (findOptions.returnM !== undefined)
|
|
2202
|
+
this.params.returnM = findOptions.returnM;
|
|
2203
|
+
if (findOptions.gdbVersion !== undefined)
|
|
2204
|
+
this.params.gdbVersion = findOptions.gdbVersion;
|
|
2205
|
+
if (findOptions.layerDefs !== undefined)
|
|
2206
|
+
this.params.layerDefs = findOptions.layerDefs;
|
|
2207
|
+
if (findOptions.token !== undefined)
|
|
2208
|
+
this.params.token = findOptions.token;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Set the text to search for
|
|
2213
|
+
*/
|
|
2214
|
+
text(searchText) {
|
|
2215
|
+
this.params.searchText = searchText;
|
|
2216
|
+
return this;
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* Set the fields to search in
|
|
2220
|
+
*/
|
|
2221
|
+
fields(fields) {
|
|
2222
|
+
this.params.searchFields = Array.isArray(fields) ? fields.join(',') : fields;
|
|
2223
|
+
return this;
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Set whether the search should contain the text (partial match) or exact match
|
|
2227
|
+
*/
|
|
2228
|
+
contains(contains) {
|
|
2229
|
+
this.params.contains = contains;
|
|
2230
|
+
return this;
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Set which layers to search in
|
|
2234
|
+
*/
|
|
2235
|
+
layers(layers) {
|
|
2236
|
+
if (Array.isArray(layers)) {
|
|
2237
|
+
this.params.layers = layers.join(',');
|
|
2238
|
+
}
|
|
2239
|
+
else {
|
|
2240
|
+
this.params.layers = layers;
|
|
2241
|
+
}
|
|
2242
|
+
return this;
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Set layer definitions for filtering specific layers
|
|
2246
|
+
*/
|
|
2247
|
+
layerDefs(layerId, whereClause) {
|
|
2248
|
+
const currentLayerDefs = this.params.layerDefs || '';
|
|
2249
|
+
this.params.layerDefs = currentLayerDefs
|
|
2250
|
+
? `${currentLayerDefs};${layerId}:${whereClause}`
|
|
2251
|
+
: `${layerId}:${whereClause}`;
|
|
2252
|
+
return this;
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Simplify geometries based on map resolution
|
|
2256
|
+
*/
|
|
2257
|
+
simplify(map, factor) {
|
|
2258
|
+
const bounds = map.getBounds();
|
|
2259
|
+
const mapWidth = Math.abs(bounds.getWest() - bounds.getEast());
|
|
2260
|
+
this.params.maxAllowableOffset = (mapWidth / map.getSize().x) * factor;
|
|
2261
|
+
return this;
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Execute the find operation
|
|
2265
|
+
*/
|
|
2266
|
+
async run() {
|
|
2267
|
+
// Always use JSON format for Find API (GeoJSON might not be supported)
|
|
2268
|
+
this.params.f = 'json';
|
|
2269
|
+
try {
|
|
2270
|
+
const response = await this.request();
|
|
2271
|
+
return this._convertToGeoJSON(response);
|
|
2272
|
+
}
|
|
2273
|
+
catch (error) {
|
|
2274
|
+
const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
|
|
2275
|
+
if (!isTestEnvironment) {
|
|
2276
|
+
console.error('Find task error:', error);
|
|
2277
|
+
}
|
|
2278
|
+
throw error;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
_convertToGeoJSON(response) {
|
|
2282
|
+
// Handle cases where response is null or results might be undefined, null, or empty
|
|
2283
|
+
const results = response?.results || [];
|
|
2284
|
+
const features = results.map(result => ({
|
|
2285
|
+
type: 'Feature',
|
|
2286
|
+
properties: {
|
|
2287
|
+
...result.attributes,
|
|
2288
|
+
layerId: result.layerId,
|
|
2289
|
+
layerName: result.layerName,
|
|
2290
|
+
foundFieldName: result.foundFieldName,
|
|
2291
|
+
value: result.value,
|
|
2292
|
+
},
|
|
2293
|
+
geometry: this._convertEsriGeometry(result.geometry),
|
|
2294
|
+
}));
|
|
2295
|
+
return {
|
|
2296
|
+
type: 'FeatureCollection',
|
|
2297
|
+
features,
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
_convertEsriGeometry(esriGeom) {
|
|
2301
|
+
if (!esriGeom || typeof esriGeom !== 'object')
|
|
2302
|
+
return null;
|
|
2303
|
+
const geom = esriGeom;
|
|
2304
|
+
// Point geometry
|
|
2305
|
+
if ('x' in geom && 'y' in geom) {
|
|
2306
|
+
return {
|
|
2307
|
+
type: 'Point',
|
|
2308
|
+
coordinates: [geom.x, geom.y],
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
// Polygon geometry
|
|
2312
|
+
if ('rings' in geom && Array.isArray(geom.rings)) {
|
|
2313
|
+
return {
|
|
2314
|
+
type: 'Polygon',
|
|
2315
|
+
coordinates: geom.rings,
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
// Polyline geometry
|
|
2319
|
+
if ('paths' in geom && Array.isArray(geom.paths)) {
|
|
2320
|
+
const paths = geom.paths;
|
|
2321
|
+
if (paths.length === 1) {
|
|
2322
|
+
// Single path = LineString
|
|
2323
|
+
return {
|
|
2324
|
+
type: 'LineString',
|
|
2325
|
+
coordinates: paths[0],
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
else {
|
|
2329
|
+
// Multiple paths = MultiLineString
|
|
2330
|
+
return {
|
|
2331
|
+
type: 'MultiLineString',
|
|
2332
|
+
coordinates: paths,
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
// Default: return null for unknown geometry types
|
|
2337
|
+
return null;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
function find(options) {
|
|
2341
|
+
return new Find(options);
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
/**
|
|
2345
|
+
* IdentifyFeatures task for performing identify operations against ArcGIS Map Services
|
|
2346
|
+
* Similar to Esri Leaflet's identifyFeatures functionality
|
|
2347
|
+
*/
|
|
2348
|
+
class IdentifyFeatures extends Task {
|
|
2349
|
+
constructor(options) {
|
|
2350
|
+
// Handle different input types and convert to TaskOptions format
|
|
2351
|
+
let taskOptions;
|
|
2352
|
+
if (typeof options === 'string') {
|
|
2353
|
+
taskOptions = options;
|
|
2354
|
+
}
|
|
2355
|
+
else if (options instanceof Service) {
|
|
2356
|
+
// Extract URL from Service instance
|
|
2357
|
+
taskOptions = { url: options.options.url };
|
|
2358
|
+
}
|
|
2359
|
+
else {
|
|
2360
|
+
// It's IdentifyFeaturesOptions, use as TaskOptions
|
|
2361
|
+
taskOptions = options;
|
|
2362
|
+
}
|
|
2363
|
+
super(taskOptions);
|
|
2364
|
+
Object.defineProperty(this, "setters", {
|
|
2365
|
+
enumerable: true,
|
|
2366
|
+
configurable: true,
|
|
2367
|
+
writable: true,
|
|
2368
|
+
value: {
|
|
2369
|
+
layers: 'layers',
|
|
2370
|
+
precision: 'geometryPrecision',
|
|
2371
|
+
tolerance: 'tolerance',
|
|
2372
|
+
returnGeometry: 'returnGeometry',
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
Object.defineProperty(this, "path", {
|
|
2376
|
+
enumerable: true,
|
|
2377
|
+
configurable: true,
|
|
2378
|
+
writable: true,
|
|
2379
|
+
value: '/identify'
|
|
2380
|
+
});
|
|
2381
|
+
Object.defineProperty(this, "params", {
|
|
2382
|
+
enumerable: true,
|
|
2383
|
+
configurable: true,
|
|
2384
|
+
writable: true,
|
|
2385
|
+
value: {
|
|
2386
|
+
sr: 4326,
|
|
2387
|
+
layers: 'all',
|
|
2388
|
+
tolerance: 3,
|
|
2389
|
+
returnGeometry: true,
|
|
2390
|
+
f: 'json',
|
|
2391
|
+
}
|
|
2392
|
+
});
|
|
2393
|
+
this.path = '/identify';
|
|
2394
|
+
// Ensure dynamic setters are available even though subclass fields
|
|
2395
|
+
// are initialized after super() runs. This rebinds setter methods.
|
|
2396
|
+
if (this.setters) {
|
|
2397
|
+
for (const [method, param] of Object.entries(this.setters)) {
|
|
2398
|
+
this[method] = this.generateSetter(param, this);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Perform identify operation at a point location
|
|
2404
|
+
*/
|
|
2405
|
+
at(point) {
|
|
2406
|
+
let geometry;
|
|
2407
|
+
if (Array.isArray(point)) {
|
|
2408
|
+
geometry = {
|
|
2409
|
+
x: point[0],
|
|
2410
|
+
y: point[1],
|
|
2411
|
+
spatialReference: { wkid: 4326 },
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
else {
|
|
2415
|
+
geometry = {
|
|
2416
|
+
x: point.lng,
|
|
2417
|
+
y: point.lat,
|
|
2418
|
+
spatialReference: { wkid: 4326 },
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
this.params.geometry = JSON.stringify(geometry);
|
|
2422
|
+
this.params.geometryType = 'esriGeometryPoint';
|
|
2423
|
+
return this;
|
|
2424
|
+
}
|
|
2425
|
+
// Strongly-typed chainable setters for common Identify params
|
|
2426
|
+
layers(value) {
|
|
2427
|
+
this.params.layers = value;
|
|
2428
|
+
return this;
|
|
2429
|
+
}
|
|
2430
|
+
tolerance(value) {
|
|
2431
|
+
this.params.tolerance = value;
|
|
2432
|
+
return this;
|
|
2433
|
+
}
|
|
2434
|
+
returnGeometry(value) {
|
|
2435
|
+
this.params.returnGeometry = value;
|
|
2436
|
+
return this;
|
|
2437
|
+
}
|
|
2438
|
+
precision(value) {
|
|
2439
|
+
this.params.geometryPrecision = value;
|
|
2440
|
+
return this;
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Set the map extent and image display for the identify operation
|
|
2444
|
+
*/
|
|
2445
|
+
on(map) {
|
|
2446
|
+
try {
|
|
2447
|
+
const bounds = map.getBounds().toArray();
|
|
2448
|
+
this.params.mapExtent = [bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1]].join(',');
|
|
2449
|
+
const canvas = map.getCanvas();
|
|
2450
|
+
this.params.imageDisplay = [canvas.width, canvas.height, 96].join(',');
|
|
2451
|
+
}
|
|
2452
|
+
catch (error) {
|
|
2453
|
+
console.warn('Could not extract map extent and display info:', error);
|
|
2454
|
+
}
|
|
2455
|
+
return this;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Set layer definitions for filtering specific layers
|
|
2459
|
+
*/
|
|
2460
|
+
layerDef(layerId, whereClause) {
|
|
2461
|
+
const currentLayerDefs = this.params.layerDefs || '';
|
|
2462
|
+
this.params.layerDefs = currentLayerDefs
|
|
2463
|
+
? `${currentLayerDefs};${layerId}:${whereClause}`
|
|
2464
|
+
: `${layerId}:${whereClause}`;
|
|
2465
|
+
return this;
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Simplify geometries based on map resolution
|
|
2469
|
+
*/
|
|
2470
|
+
simplify(map, factor) {
|
|
2471
|
+
const bounds = map.getBounds();
|
|
2472
|
+
const mapWidth = Math.abs(bounds.getWest() - bounds.getEast());
|
|
2473
|
+
this.params.maxAllowableOffset = (mapWidth / map.getSize().x) * factor;
|
|
2474
|
+
return this;
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Execute the identify operation
|
|
2478
|
+
*/
|
|
2479
|
+
async run() {
|
|
2480
|
+
try {
|
|
2481
|
+
const response = await this.request();
|
|
2482
|
+
return this._convertToGeoJSON(response);
|
|
2483
|
+
}
|
|
2484
|
+
catch (error) {
|
|
2485
|
+
const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
|
|
2486
|
+
if (!isTestEnvironment) {
|
|
2487
|
+
console.error('IdentifyFeatures error:', error);
|
|
2488
|
+
}
|
|
2489
|
+
throw error;
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
_convertToGeoJSON(response) {
|
|
2493
|
+
const features = (response.results || []).map(result => {
|
|
2494
|
+
const feature = {
|
|
2495
|
+
type: 'Feature',
|
|
2496
|
+
properties: {
|
|
2497
|
+
...result.attributes,
|
|
2498
|
+
layerId: result.layerId,
|
|
2499
|
+
layerName: result.layerName,
|
|
2500
|
+
displayFieldName: result.displayFieldName,
|
|
2501
|
+
value: result.value,
|
|
2502
|
+
},
|
|
2503
|
+
geometry: result.geometry || null,
|
|
2504
|
+
};
|
|
2505
|
+
// Add layerId as a custom property for easier identification
|
|
2506
|
+
const featureWithLayerId = feature;
|
|
2507
|
+
featureWithLayerId.layerId = result.layerId;
|
|
2508
|
+
return feature;
|
|
2509
|
+
});
|
|
2510
|
+
return {
|
|
2511
|
+
type: 'FeatureCollection',
|
|
2512
|
+
features,
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
/**
|
|
2518
|
+
* IdentifyImage task for identifying pixel values in ArcGIS Image Services
|
|
2519
|
+
* Based on Esri Leaflet's IdentifyImage functionality
|
|
2520
|
+
*/
|
|
2521
|
+
class IdentifyImage extends Task {
|
|
2522
|
+
constructor(options) {
|
|
2523
|
+
super(options);
|
|
2524
|
+
Object.defineProperty(this, "setters", {
|
|
2525
|
+
enumerable: true,
|
|
2526
|
+
configurable: true,
|
|
2527
|
+
writable: true,
|
|
2528
|
+
value: {
|
|
2529
|
+
returnCatalogItems: 'returnCatalogItems',
|
|
2530
|
+
returnGeometry: 'returnGeometry',
|
|
2531
|
+
pixelSize: 'pixelSize',
|
|
2532
|
+
token: 'token',
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
Object.defineProperty(this, "path", {
|
|
2536
|
+
enumerable: true,
|
|
2537
|
+
configurable: true,
|
|
2538
|
+
writable: true,
|
|
2539
|
+
value: 'identify'
|
|
2540
|
+
});
|
|
2541
|
+
Object.defineProperty(this, "params", {
|
|
2542
|
+
enumerable: true,
|
|
2543
|
+
configurable: true,
|
|
2544
|
+
writable: true,
|
|
2545
|
+
value: {
|
|
2546
|
+
sr: 4326,
|
|
2547
|
+
returnGeometry: false,
|
|
2548
|
+
returnCatalogItems: false,
|
|
2549
|
+
f: 'json',
|
|
2550
|
+
}
|
|
2551
|
+
});
|
|
2552
|
+
// If options is a IdentifyImageOptions object, merge relevant properties into params
|
|
2553
|
+
if (options && typeof options === 'object' && typeof options !== 'string') {
|
|
2554
|
+
const imageOptions = options;
|
|
2555
|
+
// Merge identify-specific options into params
|
|
2556
|
+
if (imageOptions.geometry !== undefined)
|
|
2557
|
+
this.params.geometry = imageOptions.geometry;
|
|
2558
|
+
if (imageOptions.geometryType !== undefined)
|
|
2559
|
+
this.params.geometryType = imageOptions.geometryType;
|
|
2560
|
+
if (imageOptions.sr !== undefined)
|
|
2561
|
+
this.params.sr = imageOptions.sr;
|
|
2562
|
+
if (imageOptions.mosaic !== undefined)
|
|
2563
|
+
this.params.mosaic = imageOptions.mosaic;
|
|
2564
|
+
if (imageOptions.renderingRules !== undefined)
|
|
2565
|
+
this.params.renderingRules = imageOptions.renderingRules;
|
|
2566
|
+
if (imageOptions.pixelSize !== undefined)
|
|
2567
|
+
this.params.pixelSize = imageOptions.pixelSize;
|
|
2568
|
+
if (imageOptions.returnGeometry !== undefined)
|
|
2569
|
+
this.params.returnGeometry = imageOptions.returnGeometry;
|
|
2570
|
+
if (imageOptions.returnCatalogItems !== undefined)
|
|
2571
|
+
this.params.returnCatalogItems = imageOptions.returnCatalogItems;
|
|
2572
|
+
if (imageOptions.token !== undefined)
|
|
2573
|
+
this.params.token = imageOptions.token;
|
|
2574
|
+
if (imageOptions.f !== undefined)
|
|
2575
|
+
this.params.f = imageOptions.f;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
/**
|
|
2579
|
+
* Identify pixel values at a specific point
|
|
2580
|
+
*/
|
|
2581
|
+
at(point) {
|
|
2582
|
+
let geometry;
|
|
2583
|
+
if (Array.isArray(point)) {
|
|
2584
|
+
geometry = {
|
|
2585
|
+
x: point[0],
|
|
2586
|
+
y: point[1],
|
|
2587
|
+
spatialReference: { wkid: 4326 },
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
else {
|
|
2591
|
+
geometry = {
|
|
2592
|
+
x: point.lng,
|
|
2593
|
+
y: point.lat,
|
|
2594
|
+
spatialReference: { wkid: 4326 },
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
this.params.geometry = JSON.stringify(geometry);
|
|
2598
|
+
this.params.geometryType = 'esriGeometryPoint';
|
|
2599
|
+
this.params.sr = 4326;
|
|
2600
|
+
return this;
|
|
2601
|
+
}
|
|
2602
|
+
/**
|
|
2603
|
+
* Set custom geometry for identification
|
|
2604
|
+
*/
|
|
2605
|
+
geometry(geometry, geometryType) {
|
|
2606
|
+
this.params.geometry = JSON.stringify(geometry);
|
|
2607
|
+
this.params.geometryType = geometryType || 'esriGeometryPoint';
|
|
2608
|
+
return this;
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Set pixel size for the identify operation
|
|
2612
|
+
*/
|
|
2613
|
+
pixelSize(size) {
|
|
2614
|
+
if (Array.isArray(size)) {
|
|
2615
|
+
this.params.pixelSize = `${size[0]},${size[1]}`;
|
|
2616
|
+
}
|
|
2617
|
+
else {
|
|
2618
|
+
this.params.pixelSize = `${size.x},${size.y}`;
|
|
2619
|
+
}
|
|
2620
|
+
return this;
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Set whether to return geometry with results
|
|
2624
|
+
*/
|
|
2625
|
+
returnGeometry(returnGeom) {
|
|
2626
|
+
this.params.returnGeometry = returnGeom;
|
|
2627
|
+
return this;
|
|
2628
|
+
}
|
|
2629
|
+
/**
|
|
2630
|
+
* Set whether to return catalog items
|
|
2631
|
+
*/
|
|
2632
|
+
returnCatalogItems(returnItems) {
|
|
2633
|
+
this.params.returnCatalogItems = returnItems;
|
|
2634
|
+
return this;
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2637
|
+
* Set mosaic rule for the identify operation
|
|
2638
|
+
*/
|
|
2639
|
+
mosaicRule(rule) {
|
|
2640
|
+
this.params.mosaicRule = JSON.stringify(rule);
|
|
2641
|
+
return this;
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Set rendering rules for the identify operation
|
|
2645
|
+
*/
|
|
2646
|
+
renderingRule(rule) {
|
|
2647
|
+
this.params.renderingRule = JSON.stringify(rule);
|
|
2648
|
+
return this;
|
|
2649
|
+
}
|
|
2650
|
+
/**
|
|
2651
|
+
* Execute the identify operation
|
|
2652
|
+
*/
|
|
2653
|
+
async run() {
|
|
2654
|
+
const response = await this.request();
|
|
2655
|
+
// Normalize different response formats
|
|
2656
|
+
let results = [];
|
|
2657
|
+
if (response.results) {
|
|
2658
|
+
results = response.results;
|
|
2659
|
+
}
|
|
2660
|
+
else if (response.value !== undefined || response.values) {
|
|
2661
|
+
// Handle simple value responses
|
|
2662
|
+
results = [
|
|
2663
|
+
{
|
|
2664
|
+
value: response.value || (response.values && response.values[0]) || '',
|
|
2665
|
+
attributes: response.properties || {},
|
|
2666
|
+
},
|
|
2667
|
+
];
|
|
2668
|
+
}
|
|
2669
|
+
return {
|
|
2670
|
+
results,
|
|
2671
|
+
location: response.location,
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
/**
|
|
2675
|
+
* Execute and return pixel values as a simple array
|
|
2676
|
+
*/
|
|
2677
|
+
async getPixelValues() {
|
|
2678
|
+
const response = await this.run();
|
|
2679
|
+
return response.results.map(result => {
|
|
2680
|
+
if (result.value !== undefined) {
|
|
2681
|
+
const numValue = parseFloat(result.value);
|
|
2682
|
+
return isNaN(numValue) ? result.value : numValue;
|
|
2683
|
+
}
|
|
2684
|
+
return null;
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* Execute and return detailed pixel information
|
|
2689
|
+
*/
|
|
2690
|
+
async getPixelData() {
|
|
2691
|
+
const response = await this.run();
|
|
2692
|
+
return response.results;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
function identifyImage(options) {
|
|
2696
|
+
return new IdentifyImage(options);
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
exports.DynamicMapService = DynamicMapService;
|
|
2700
|
+
exports.FeatureService = FeatureService;
|
|
2701
|
+
exports.Find = Find;
|
|
2702
|
+
exports.IdentifyFeatures = IdentifyFeatures;
|
|
2703
|
+
exports.IdentifyImage = IdentifyImage;
|
|
2704
|
+
exports.ImageService = ImageService;
|
|
2705
|
+
exports.Query = Query;
|
|
2706
|
+
exports.Service = Service;
|
|
2707
|
+
exports.Task = Task;
|
|
2708
|
+
exports.TiledMapService = TiledMapService;
|
|
2709
|
+
exports.VectorBasemapStyle = VectorBasemapStyle;
|
|
2710
|
+
exports.VectorTileService = VectorTileService;
|
|
2711
|
+
exports.cleanTrailingSlash = cleanTrailingSlash;
|
|
2712
|
+
exports.find = find;
|
|
2713
|
+
exports.getServiceDetails = getServiceDetails;
|
|
2714
|
+
exports.identifyImage = identifyImage;
|
|
2715
|
+
exports.query = query;
|
|
2716
|
+
exports.updateAttribution = updateAttribution;
|
|
2717
|
+
|
|
2718
|
+
}));
|
|
2719
|
+
//# sourceMappingURL=esri-gl.js.map
|