esri-gl 1.0.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -1
- package/dist/{index-DczUYC4z.d.ts → index-Doq93o-G.d.ts} +252 -75
- package/dist/{IdentifyImage-DmyKcbAv.js → index-DsY1_0df.js} +825 -678
- package/dist/index-DsY1_0df.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +7181 -1017
- package/dist/index.umd.js.map +1 -1
- package/dist/package.json +5 -1
- package/dist/react-map-gl.d.ts +57 -10
- package/dist/react-map-gl.js +313 -290
- package/dist/react-map-gl.js.map +1 -1
- package/dist/react.d.ts +23 -28
- package/dist/react.js +202 -178
- package/dist/react.js.map +1 -1
- package/dist/{useFeatureService-E9PiUOLP.js → useFeatureService-BRY6PHUs.js} +50 -12
- package/dist/useFeatureService-BRY6PHUs.js.map +1 -0
- package/dist/{useFeatureService-QUqwJcet.d.ts → useFeatureService-CG70gjQy.d.ts} +29 -41
- package/package.json +10 -7
- package/dist/IdentifyImage-DmyKcbAv.js.map +0 -1
- package/dist/useFeatureService-E9PiUOLP.js.map +0 -1
|
@@ -1,9 +1,163 @@
|
|
|
1
|
+
import { BasemapStyleSession } from '@esri/arcgis-rest-basemap-sessions';
|
|
2
|
+
import { SearchQueryBuilder, searchItems, getItem, getItemData } from '@esri/arcgis-rest-portal';
|
|
3
|
+
import { ApiKeyManager, request } from '@esri/arcgis-rest-request';
|
|
4
|
+
import { queryFeatures, getLayer, getAllLayersAndTables, queryRelated, decodeValues, addFeatures, updateFeatures, deleteFeatures, applyEdits, getAttachments, deleteAttachments } from '@esri/arcgis-rest-feature-service';
|
|
1
5
|
import * as tilebelt from '@mapbox/tilebelt';
|
|
2
6
|
import tileDecode from 'arcgis-pbf-parser';
|
|
3
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Shared request + authentication adapter built on `@esri/arcgis-rest-request`.
|
|
10
|
+
*
|
|
11
|
+
* Every network call in esri-gl that talks to an ArcGIS REST endpoint flows
|
|
12
|
+
* through this module so that authentication, error handling and parameter
|
|
13
|
+
* encoding are handled once by the official ArcGIS REST JS client instead of
|
|
14
|
+
* the bespoke `fetch` wrappers this library used to ship.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Normalise the various auth inputs into an `IAuthenticationManager` (or
|
|
18
|
+
* `undefined` for anonymous requests). A bare string is wrapped in an
|
|
19
|
+
* `ApiKeyManager` so it is sent as the `token` parameter, matching ArcGIS REST
|
|
20
|
+
* conventions.
|
|
21
|
+
*/
|
|
22
|
+
function resolveAuthentication(options) {
|
|
23
|
+
if (!options) return undefined;
|
|
24
|
+
const {
|
|
25
|
+
authentication,
|
|
26
|
+
apiKey,
|
|
27
|
+
token
|
|
28
|
+
} = options;
|
|
29
|
+
if (authentication) {
|
|
30
|
+
return typeof authentication === 'string' ? ApiKeyManager.fromKey(authentication) : authentication;
|
|
31
|
+
}
|
|
32
|
+
const key = apiKey ?? token;
|
|
33
|
+
return key ? ApiKeyManager.fromKey(key) : undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Thin wrapper over `@esri/arcgis-rest-request`'s `request()` that applies
|
|
37
|
+
* esri-gl's auth resolution and sensible defaults. Throws `ArcGISRequestError`
|
|
38
|
+
* (and friends) on ArcGIS service-level or HTTP errors.
|
|
39
|
+
*/
|
|
40
|
+
function esriRequest(url, options = {}) {
|
|
41
|
+
const {
|
|
42
|
+
params,
|
|
43
|
+
httpMethod,
|
|
44
|
+
rawResponse,
|
|
45
|
+
signal,
|
|
46
|
+
headers,
|
|
47
|
+
...auth
|
|
48
|
+
} = options;
|
|
49
|
+
const requestOptions = {
|
|
50
|
+
params: {
|
|
51
|
+
f: 'json',
|
|
52
|
+
...params
|
|
53
|
+
},
|
|
54
|
+
authentication: resolveAuthentication(auth)
|
|
55
|
+
};
|
|
56
|
+
if (httpMethod) requestOptions.httpMethod = httpMethod;
|
|
57
|
+
if (rawResponse) requestOptions.rawResponse = true;
|
|
58
|
+
if (signal) requestOptions.signal = signal;
|
|
59
|
+
if (headers) requestOptions.headers = headers;
|
|
60
|
+
return request(url, requestOptions);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Fetch a **binary** ArcGIS response (PBF tiles, exported images) as a raw
|
|
64
|
+
* `Response`, applying esri-gl auth. Used instead of `esriRequest`'s deprecated
|
|
65
|
+
* `rawResponse` option (removed in ArcGIS REST JS v5).
|
|
66
|
+
*/
|
|
67
|
+
async function esriRawRequest(url, options = {}) {
|
|
68
|
+
const {
|
|
69
|
+
params,
|
|
70
|
+
signal,
|
|
71
|
+
headers,
|
|
72
|
+
...auth
|
|
73
|
+
} = options;
|
|
74
|
+
const search = new URLSearchParams();
|
|
75
|
+
for (const [key, value] of Object.entries(params ?? {})) {
|
|
76
|
+
if (value === undefined || value === null) continue;
|
|
77
|
+
search.append(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
78
|
+
}
|
|
79
|
+
const manager = resolveAuthentication(auth);
|
|
80
|
+
if (manager) {
|
|
81
|
+
try {
|
|
82
|
+
const token = await manager.getToken(url);
|
|
83
|
+
if (token) search.append('token', token);
|
|
84
|
+
} catch {
|
|
85
|
+
// fall through and request anonymously
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const response = await fetch(`${url}?${search.toString()}`, {
|
|
89
|
+
signal,
|
|
90
|
+
headers
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error(`Request failed: HTTP ${response.status}`);
|
|
94
|
+
}
|
|
95
|
+
return response;
|
|
96
|
+
}
|
|
97
|
+
|
|
4
98
|
function cleanTrailingSlash(url) {
|
|
5
99
|
return url.replace(/\/$/, '');
|
|
6
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Append a `token` query parameter if a non-empty token is supplied.
|
|
103
|
+
* Centralizes the duplicated token-injection logic used by every service.
|
|
104
|
+
*/
|
|
105
|
+
function appendTokenIfExists(params, token) {
|
|
106
|
+
if (token) {
|
|
107
|
+
params.append('token', token);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Remove every layer that references the given source, then remove the
|
|
112
|
+
* source itself. Safe against disposed maps, missing style methods, and
|
|
113
|
+
* layers that were already removed elsewhere.
|
|
114
|
+
*/
|
|
115
|
+
function removeMapSource(map, sourceId) {
|
|
116
|
+
if (!map || typeof map.removeSource !== 'function') return;
|
|
117
|
+
try {
|
|
118
|
+
if (!map.style) return;
|
|
119
|
+
const mapWithStyle = map;
|
|
120
|
+
if (typeof mapWithStyle.getStyle === 'function') {
|
|
121
|
+
const style = mapWithStyle.getStyle();
|
|
122
|
+
const layers = style?.layers || [];
|
|
123
|
+
layers.forEach(layer => {
|
|
124
|
+
if (layer.source !== sourceId) return;
|
|
125
|
+
if (typeof mapWithStyle.getLayer !== 'function' || typeof mapWithStyle.removeLayer !== 'function') {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
let hasLayer = false;
|
|
129
|
+
try {
|
|
130
|
+
hasLayer = Boolean(mapWithStyle.getLayer(layer.id));
|
|
131
|
+
} catch {
|
|
132
|
+
hasLayer = false;
|
|
133
|
+
}
|
|
134
|
+
if (!hasLayer) return;
|
|
135
|
+
try {
|
|
136
|
+
mapWithStyle.removeLayer(layer.id);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.warn(`Failed to remove layer ${layer.id} for source ${sourceId}:`, error);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (typeof mapWithStyle.getSource === 'function') {
|
|
143
|
+
let hasSource = false;
|
|
144
|
+
try {
|
|
145
|
+
hasSource = Boolean(mapWithStyle.getSource(sourceId));
|
|
146
|
+
} catch {
|
|
147
|
+
hasSource = false;
|
|
148
|
+
}
|
|
149
|
+
if (hasSource) {
|
|
150
|
+
try {
|
|
151
|
+
map.removeSource(sourceId);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.warn(`Failed to remove source ${sourceId}:`, error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.warn(`Failed to remove source ${sourceId}:`, error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
7
161
|
/**
|
|
8
162
|
* Check if an error represents an AbortError (request was cancelled)
|
|
9
163
|
* Handles various error shapes from different browsers and environments
|
|
@@ -27,22 +181,18 @@ function isAbortError(error) {
|
|
|
27
181
|
const stringified = String(error).toLowerCase();
|
|
28
182
|
return stringified.includes('abort');
|
|
29
183
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Fetch the `?f=json` metadata document for an ArcGIS service endpoint.
|
|
186
|
+
*
|
|
187
|
+
* Delegates to `@esri/arcgis-rest-request` via {@link esriRequest}, which
|
|
188
|
+
* handles authentication and ArcGIS error responses. Accepts an
|
|
189
|
+
* {@link EsriRequestOptions} object (`token` / `apiKey` / `authentication`).
|
|
190
|
+
*/
|
|
191
|
+
async function getServiceDetails(url, options = {}) {
|
|
192
|
+
return esriRequest(url, {
|
|
193
|
+
httpMethod: 'GET',
|
|
194
|
+
...options
|
|
33
195
|
});
|
|
34
|
-
if (token) {
|
|
35
|
-
params.append('token', token);
|
|
36
|
-
}
|
|
37
|
-
const response = await fetch(`${url}?${params.toString()}`, fetchOptions);
|
|
38
|
-
if (!response.ok) {
|
|
39
|
-
throw new Error(`Failed to fetch service details: HTTP ${response.status}`);
|
|
40
|
-
}
|
|
41
|
-
const data = await response.json();
|
|
42
|
-
if (data.error) {
|
|
43
|
-
throw new Error(data.error.message || 'Service returned an error');
|
|
44
|
-
}
|
|
45
|
-
return data;
|
|
46
196
|
}
|
|
47
197
|
const POWERED_BY_ESRI_ATTRIBUTION_STRING = 'Powered by <a href="https://www.esri.com">Esri</a>';
|
|
48
198
|
// This requires hooking into some undocumented properties
|
|
@@ -274,12 +424,11 @@ class Service {
|
|
|
274
424
|
const wrappedCallback = this._createServiceCallback(method, path, params, (error, response) => {
|
|
275
425
|
callback(error, response);
|
|
276
426
|
});
|
|
427
|
+
// Token/apiKey/authentication are applied by the request adapter, not as
|
|
428
|
+
// raw params, so they are not injected here.
|
|
277
429
|
let finalParams = {
|
|
278
430
|
...params
|
|
279
431
|
};
|
|
280
|
-
if (this.options.token) {
|
|
281
|
-
finalParams.token = this.options.token;
|
|
282
|
-
}
|
|
283
432
|
if (this.options.requestParams) {
|
|
284
433
|
finalParams = {
|
|
285
434
|
...finalParams,
|
|
@@ -326,7 +475,7 @@ class Service {
|
|
|
326
475
|
});
|
|
327
476
|
}
|
|
328
477
|
async _makeRequest(method, path, params, callback) {
|
|
329
|
-
const url =
|
|
478
|
+
const url = `${this.options.url}${path}`;
|
|
330
479
|
// Set up abort controller for timeout support
|
|
331
480
|
const controller = new AbortController();
|
|
332
481
|
let timeoutId;
|
|
@@ -336,71 +485,22 @@ class Service {
|
|
|
336
485
|
}, this.options.timeout);
|
|
337
486
|
}
|
|
338
487
|
try {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
if (method === 'POST') {
|
|
350
|
-
const formData = new FormData();
|
|
351
|
-
Object.keys(params).forEach(key => {
|
|
352
|
-
const value = params[key];
|
|
353
|
-
if (value !== undefined && value !== null) {
|
|
354
|
-
if (typeof value === 'object') {
|
|
355
|
-
formData.append(key, JSON.stringify(value));
|
|
356
|
-
} else {
|
|
357
|
-
formData.append(key, value.toString());
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
response = await fetch(url, {
|
|
362
|
-
...fetchOptions,
|
|
363
|
-
method: 'POST',
|
|
364
|
-
body: formData
|
|
365
|
-
});
|
|
366
|
-
} else {
|
|
367
|
-
const searchParams = new URLSearchParams();
|
|
368
|
-
Object.keys(params).forEach(key => {
|
|
369
|
-
const value = params[key];
|
|
370
|
-
if (value !== undefined && value !== null) {
|
|
371
|
-
if (Array.isArray(value)) {
|
|
372
|
-
searchParams.append(key, value.join(','));
|
|
373
|
-
} else if (typeof value === 'object') {
|
|
374
|
-
searchParams.append(key, JSON.stringify(value));
|
|
375
|
-
} else {
|
|
376
|
-
searchParams.append(key, value.toString());
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
const fullUrl = `${url}?${searchParams.toString()}`;
|
|
381
|
-
response = await fetch(fullUrl, fetchOptions);
|
|
382
|
-
}
|
|
383
|
-
if (!response.ok) {
|
|
384
|
-
const error = new Error(`HTTP error! status: ${response.status}`);
|
|
385
|
-
error.code = response.status;
|
|
386
|
-
throw error;
|
|
387
|
-
}
|
|
388
|
-
const data = await response.json();
|
|
389
|
-
// Check for AGOL JSON-level errors (HTTP 200 with error body)
|
|
390
|
-
if (data && typeof data === 'object' && data.error) {
|
|
391
|
-
const err = new Error(data.error.message || 'ArcGIS service error');
|
|
392
|
-
err.code = data.error.code;
|
|
393
|
-
err.details = data.error.details;
|
|
394
|
-
callback(err);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
488
|
+
const data = await esriRequest(url, {
|
|
489
|
+
params,
|
|
490
|
+
httpMethod: method === 'POST' ? 'POST' : 'GET',
|
|
491
|
+
signal: controller.signal,
|
|
492
|
+
token: this.options.token,
|
|
493
|
+
apiKey: this.options.apiKey,
|
|
494
|
+
authentication: this.options.authentication
|
|
495
|
+
});
|
|
397
496
|
callback(undefined, data);
|
|
398
497
|
} catch (error) {
|
|
399
498
|
// Provide clearer error message for timeout
|
|
400
499
|
if (error instanceof Error && error.name === 'AbortError' && this.options.timeout > 0) {
|
|
401
|
-
|
|
402
|
-
callback(timeoutError);
|
|
500
|
+
callback(new Error(`Request timed out after ${this.options.timeout}ms`));
|
|
403
501
|
} else {
|
|
502
|
+
// ArcGISRequestError exposes a numeric `code` used downstream for
|
|
503
|
+
// authentication (498/499) detection.
|
|
404
504
|
callback(error);
|
|
405
505
|
}
|
|
406
506
|
} finally {
|
|
@@ -797,10 +897,21 @@ class DynamicMapService {
|
|
|
797
897
|
if (where) this.setLayerDefinition(layerId, where);
|
|
798
898
|
}
|
|
799
899
|
_appendTokenIfExists(params) {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
900
|
+
// apiKey is sent as the token URL parameter, matching the request-based paths.
|
|
901
|
+
const auth = this.esriServiceOptions;
|
|
902
|
+
appendTokenIfExists(params, auth.token ?? auth.apiKey);
|
|
903
|
+
}
|
|
904
|
+
_auth() {
|
|
905
|
+
const o = this.esriServiceOptions;
|
|
906
|
+
return {
|
|
907
|
+
token: o.token,
|
|
908
|
+
apiKey: o.apiKey,
|
|
909
|
+
authentication: o.authentication
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/** Resolved ArcGIS REST JS auth manager for the typed feature-service helpers. */
|
|
913
|
+
_authentication() {
|
|
914
|
+
return resolveAuthentication(this._auth());
|
|
804
915
|
}
|
|
805
916
|
_escapeValue(val) {
|
|
806
917
|
if (val === null) return 'NULL';
|
|
@@ -876,7 +987,7 @@ class DynamicMapService {
|
|
|
876
987
|
getMetadata() {
|
|
877
988
|
if (this._serviceMetadata !== null) return Promise.resolve(this._serviceMetadata);
|
|
878
989
|
return new Promise((resolve, reject) => {
|
|
879
|
-
getServiceDetails(this.esriServiceOptions.url, this.
|
|
990
|
+
getServiceDetails(this.esriServiceOptions.url, this._auth()).then(data => {
|
|
880
991
|
this._serviceMetadata = data;
|
|
881
992
|
resolve(this._serviceMetadata);
|
|
882
993
|
}).catch(err => reject(err));
|
|
@@ -891,7 +1002,7 @@ class DynamicMapService {
|
|
|
891
1002
|
const canvas = this._map.getCanvas();
|
|
892
1003
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
893
1004
|
const bounds = this._map.getBounds().toArray();
|
|
894
|
-
const params =
|
|
1005
|
+
const params = {
|
|
895
1006
|
sr: '4326',
|
|
896
1007
|
geometryType: 'esriGeometryPoint',
|
|
897
1008
|
geometry: JSON.stringify({
|
|
@@ -905,21 +1016,15 @@ class DynamicMapService {
|
|
|
905
1016
|
returnGeometry: returnGeometry.toString(),
|
|
906
1017
|
imageDisplay: `${canvas.width},${canvas.height},${this.options.dpi}`,
|
|
907
1018
|
mapExtent: `${bounds[0][0]},${bounds[0][1]},${bounds[1][0]},${bounds[1][1]}`,
|
|
908
|
-
layers: this._layersStrIdentify || ''
|
|
909
|
-
|
|
1019
|
+
layers: this._layersStrIdentify || ''
|
|
1020
|
+
};
|
|
1021
|
+
if (this._layerDefs) params.layerDefs = this._layerDefs;
|
|
1022
|
+
if (this._dynamicLayers) params.dynamicLayers = this._dynamicLayers;
|
|
1023
|
+
if (this._time) params.time = this._time;
|
|
1024
|
+
const data = await esriRequest(`${this.esriServiceOptions.url}/identify`, {
|
|
1025
|
+
params,
|
|
1026
|
+
...this._auth()
|
|
910
1027
|
});
|
|
911
|
-
if (this._layerDefs) params.append('layerDefs', this._layerDefs);
|
|
912
|
-
if (this._dynamicLayers) params.append('dynamicLayers', this._dynamicLayers);
|
|
913
|
-
if (this._time) params.append('time', this._time);
|
|
914
|
-
this._appendTokenIfExists(params);
|
|
915
|
-
const response = await fetch(`${this.esriServiceOptions.url}/identify?${params.toString()}`, this.esriServiceOptions.fetchOptions);
|
|
916
|
-
if (!response.ok) {
|
|
917
|
-
throw new Error(`Identify request failed: HTTP ${response.status}`);
|
|
918
|
-
}
|
|
919
|
-
const data = await response.json();
|
|
920
|
-
if (data.error) {
|
|
921
|
-
throw new Error(`Identify request failed: ${data.error.message}`);
|
|
922
|
-
}
|
|
923
1028
|
return data;
|
|
924
1029
|
}
|
|
925
1030
|
// ========================================
|
|
@@ -1046,71 +1151,44 @@ class DynamicMapService {
|
|
|
1046
1151
|
}
|
|
1047
1152
|
/** Get statistics for a sublayer */
|
|
1048
1153
|
async getLayerStatistics(layerId, statisticFields, options = {}) {
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
f: 'json',
|
|
1154
|
+
const data = await queryFeatures({
|
|
1155
|
+
url: `${this.esriServiceOptions.url}/${layerId}`,
|
|
1052
1156
|
where: options.where || '1=1',
|
|
1053
|
-
|
|
1054
|
-
|
|
1157
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1158
|
+
outStatistics: statisticFields,
|
|
1159
|
+
groupByFieldsForStatistics: options.groupByFieldsForStatistics,
|
|
1160
|
+
returnGeometry: false,
|
|
1161
|
+
authentication: this._authentication()
|
|
1055
1162
|
});
|
|
1056
|
-
if (options.groupByFieldsForStatistics) {
|
|
1057
|
-
params.append('groupByFieldsForStatistics', options.groupByFieldsForStatistics);
|
|
1058
|
-
}
|
|
1059
|
-
this._appendTokenIfExists(params);
|
|
1060
|
-
const response = await fetch(`${queryUrl}?${params.toString()}`);
|
|
1061
|
-
if (!response.ok) {
|
|
1062
|
-
throw new Error(`Statistics query failed: HTTP ${response.status}`);
|
|
1063
|
-
}
|
|
1064
|
-
const data = await response.json();
|
|
1065
|
-
if (data.error) {
|
|
1066
|
-
throw new Error(`Statistics query failed: ${data.error.message}`);
|
|
1067
|
-
}
|
|
1068
1163
|
return data.features || [];
|
|
1069
1164
|
}
|
|
1070
1165
|
/** Query features from a specific sublayer */
|
|
1071
1166
|
async queryLayerFeatures(layerId, options = {}) {
|
|
1072
|
-
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1167
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1168
|
+
const requestOptions = {
|
|
1169
|
+
url: `${this.esriServiceOptions.url}/${layerId}`,
|
|
1075
1170
|
where: options.where || '1=1',
|
|
1076
|
-
returnGeometry: options.returnGeometry !== false
|
|
1077
|
-
outFields:
|
|
1078
|
-
|
|
1171
|
+
returnGeometry: options.returnGeometry !== false,
|
|
1172
|
+
outFields: options.outFields || '*',
|
|
1173
|
+
authentication: this._authentication()
|
|
1174
|
+
};
|
|
1079
1175
|
if (options.geometry) {
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
}
|
|
1084
|
-
if (options.orderByFields)
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if (options.
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
if (options.resultRecordCount) {
|
|
1091
|
-
params.append('resultRecordCount', options.resultRecordCount.toString());
|
|
1092
|
-
}
|
|
1093
|
-
if (options.returnCountOnly) {
|
|
1094
|
-
params.append('returnCountOnly', 'true');
|
|
1095
|
-
}
|
|
1096
|
-
if (options.returnIdsOnly) {
|
|
1097
|
-
params.append('returnIdsOnly', 'true');
|
|
1098
|
-
}
|
|
1099
|
-
this._appendTokenIfExists(params);
|
|
1100
|
-
const response = await fetch(`${queryUrl}?${params.toString()}`);
|
|
1101
|
-
if (!response.ok) {
|
|
1102
|
-
throw new Error(`Layer query failed: HTTP ${response.status}`);
|
|
1103
|
-
}
|
|
1104
|
-
const data = await response.json();
|
|
1105
|
-
if (data.error) {
|
|
1106
|
-
throw new Error(`Layer query failed: ${data.error.message}`);
|
|
1107
|
-
}
|
|
1176
|
+
requestOptions.geometry = options.geometry;
|
|
1177
|
+
requestOptions.geometryType = options.geometryType || 'esriGeometryEnvelope';
|
|
1178
|
+
requestOptions.spatialRel = options.spatialRel || 'esriSpatialRelIntersects';
|
|
1179
|
+
}
|
|
1180
|
+
if (options.orderByFields) requestOptions.orderByFields = options.orderByFields;
|
|
1181
|
+
if (options.resultOffset) requestOptions.resultOffset = options.resultOffset;
|
|
1182
|
+
if (options.resultRecordCount) requestOptions.resultRecordCount = options.resultRecordCount;
|
|
1183
|
+
if (options.returnCountOnly) requestOptions.returnCountOnly = true;
|
|
1184
|
+
if (options.returnIdsOnly) requestOptions.returnIdsOnly = true;
|
|
1185
|
+
const data = await queryFeatures(requestOptions);
|
|
1108
1186
|
return data;
|
|
1109
1187
|
}
|
|
1110
1188
|
/** Export high-resolution map image */
|
|
1111
1189
|
async exportMapImage(options) {
|
|
1112
1190
|
const exportUrl = `${this.esriServiceOptions.url}/export`;
|
|
1113
|
-
const params =
|
|
1191
|
+
const params = {
|
|
1114
1192
|
f: 'image',
|
|
1115
1193
|
bbox: options.bbox.join(','),
|
|
1116
1194
|
size: options.size.join(','),
|
|
@@ -1119,62 +1197,45 @@ class DynamicMapService {
|
|
|
1119
1197
|
dpi: (options.dpi || 96).toString(),
|
|
1120
1198
|
bboxSR: (options.bboxSR || 3857).toString(),
|
|
1121
1199
|
imageSR: (options.imageSR || 3857).toString()
|
|
1122
|
-
}
|
|
1200
|
+
};
|
|
1123
1201
|
if (options.layerDefs) {
|
|
1124
|
-
params.
|
|
1202
|
+
params.layerDefs = JSON.stringify(options.layerDefs);
|
|
1125
1203
|
}
|
|
1126
1204
|
if (options.dynamicLayers) {
|
|
1127
1205
|
const normalized = this._ensureAllVisibleLayers(options.dynamicLayers);
|
|
1128
|
-
params.
|
|
1206
|
+
params.dynamicLayers = JSON.stringify(normalized);
|
|
1129
1207
|
}
|
|
1130
1208
|
if (options.gdbVersion) {
|
|
1131
|
-
params.
|
|
1209
|
+
params.gdbVersion = options.gdbVersion;
|
|
1132
1210
|
}
|
|
1133
1211
|
if (options.historicMoment) {
|
|
1134
|
-
params.
|
|
1135
|
-
}
|
|
1136
|
-
this._appendTokenIfExists(params);
|
|
1137
|
-
const response = await fetch(`${exportUrl}?${params.toString()}`);
|
|
1138
|
-
if (!response.ok) {
|
|
1139
|
-
throw new Error(`Export failed: ${response.statusText}`);
|
|
1212
|
+
params.historicMoment = options.historicMoment.toString();
|
|
1140
1213
|
}
|
|
1214
|
+
const response = await esriRawRequest(`${exportUrl}`, {
|
|
1215
|
+
params,
|
|
1216
|
+
...this._auth()
|
|
1217
|
+
});
|
|
1141
1218
|
return response.blob();
|
|
1142
1219
|
}
|
|
1143
1220
|
/** Generate legend information for layers */
|
|
1144
1221
|
async generateLegend(layerIds) {
|
|
1145
1222
|
const legendUrl = `${this.esriServiceOptions.url}/legend`;
|
|
1146
|
-
const params =
|
|
1147
|
-
f: 'json'
|
|
1148
|
-
});
|
|
1223
|
+
const params = {};
|
|
1149
1224
|
if (layerIds?.length) {
|
|
1150
|
-
params.
|
|
1151
|
-
}
|
|
1152
|
-
this._appendTokenIfExists(params);
|
|
1153
|
-
const response = await fetch(`${legendUrl}?${params.toString()}`);
|
|
1154
|
-
if (!response.ok) {
|
|
1155
|
-
throw new Error(`Legend generation failed: HTTP ${response.status}`);
|
|
1156
|
-
}
|
|
1157
|
-
const data = await response.json();
|
|
1158
|
-
if (data.error) {
|
|
1159
|
-
throw new Error(`Legend generation failed: ${data.error.message}`);
|
|
1225
|
+
params.layers = layerIds.join(',');
|
|
1160
1226
|
}
|
|
1227
|
+
const data = await esriRequest(legendUrl, {
|
|
1228
|
+
params,
|
|
1229
|
+
...this._auth()
|
|
1230
|
+
});
|
|
1161
1231
|
return data.layers || [];
|
|
1162
1232
|
}
|
|
1163
1233
|
/** Get detailed information about a specific layer */
|
|
1164
1234
|
async getLayerInfo(layerId) {
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1235
|
+
const data = await getLayer({
|
|
1236
|
+
url: `${this.esriServiceOptions.url}/${layerId}`,
|
|
1237
|
+
authentication: this._authentication()
|
|
1168
1238
|
});
|
|
1169
|
-
this._appendTokenIfExists(params);
|
|
1170
|
-
const response = await fetch(`${layerUrl}?${params.toString()}`);
|
|
1171
|
-
if (!response.ok) {
|
|
1172
|
-
throw new Error(`Layer info request failed: HTTP ${response.status}`);
|
|
1173
|
-
}
|
|
1174
|
-
const data = await response.json();
|
|
1175
|
-
if (data.error) {
|
|
1176
|
-
throw new Error(`Layer info request failed: ${data.error.message}`);
|
|
1177
|
-
}
|
|
1178
1239
|
return data;
|
|
1179
1240
|
}
|
|
1180
1241
|
/** Get field information for a layer */
|
|
@@ -1192,19 +1253,10 @@ class DynamicMapService {
|
|
|
1192
1253
|
}
|
|
1193
1254
|
/** Discover all layers in the service */
|
|
1194
1255
|
async discoverLayers() {
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1256
|
+
const data = await getAllLayersAndTables({
|
|
1257
|
+
url: this.esriServiceOptions.url,
|
|
1258
|
+
authentication: this._authentication()
|
|
1198
1259
|
});
|
|
1199
|
-
this._appendTokenIfExists(params);
|
|
1200
|
-
const response = await fetch(`${serviceUrl}?${params.toString()}`);
|
|
1201
|
-
if (!response.ok) {
|
|
1202
|
-
throw new Error(`Service discovery failed: HTTP ${response.status}`);
|
|
1203
|
-
}
|
|
1204
|
-
const data = await response.json();
|
|
1205
|
-
if (data.error) {
|
|
1206
|
-
throw new Error(`Service discovery failed: ${data.error.message}`);
|
|
1207
|
-
}
|
|
1208
1260
|
return data.layers || [];
|
|
1209
1261
|
}
|
|
1210
1262
|
/** Apply multiple layer operations in a single update */
|
|
@@ -1283,56 +1335,7 @@ class DynamicMapService {
|
|
|
1283
1335
|
this._updateSource();
|
|
1284
1336
|
}
|
|
1285
1337
|
remove() {
|
|
1286
|
-
|
|
1287
|
-
if (!map || typeof map.removeSource !== 'function') {
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
try {
|
|
1291
|
-
// Guard against map whose style has already been destroyed
|
|
1292
|
-
if (!map.style) return;
|
|
1293
|
-
const mapWithStyle = map;
|
|
1294
|
-
const mapLayerApi = map;
|
|
1295
|
-
const mapSourceApi = map;
|
|
1296
|
-
if (typeof mapWithStyle.getStyle === 'function') {
|
|
1297
|
-
const style = mapWithStyle.getStyle();
|
|
1298
|
-
const layers = style?.layers || [];
|
|
1299
|
-
layers.forEach(layer => {
|
|
1300
|
-
if (layer.source !== this._sourceId) return;
|
|
1301
|
-
if (typeof mapLayerApi.getLayer !== 'function' || typeof mapLayerApi.removeLayer !== 'function') {
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
let hasLayer = false;
|
|
1305
|
-
try {
|
|
1306
|
-
hasLayer = Boolean(mapLayerApi.getLayer(layer.id));
|
|
1307
|
-
} catch {
|
|
1308
|
-
hasLayer = false;
|
|
1309
|
-
}
|
|
1310
|
-
if (!hasLayer) return;
|
|
1311
|
-
try {
|
|
1312
|
-
mapLayerApi.removeLayer(layer.id);
|
|
1313
|
-
} catch (error) {
|
|
1314
|
-
console.warn(`Failed to remove layer ${layer.id} for source ${this._sourceId}:`, error);
|
|
1315
|
-
}
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
if (typeof mapSourceApi.getSource === 'function') {
|
|
1319
|
-
let hasSource = false;
|
|
1320
|
-
try {
|
|
1321
|
-
hasSource = Boolean(mapSourceApi.getSource(this._sourceId));
|
|
1322
|
-
} catch {
|
|
1323
|
-
hasSource = false;
|
|
1324
|
-
}
|
|
1325
|
-
if (hasSource) {
|
|
1326
|
-
try {
|
|
1327
|
-
map.removeSource(this._sourceId);
|
|
1328
|
-
} catch (error) {
|
|
1329
|
-
console.warn(`Failed to remove source ${this._sourceId}:`, error);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
} catch (error) {
|
|
1334
|
-
console.warn(`Failed to remove source ${this._sourceId}:`, error);
|
|
1335
|
-
}
|
|
1338
|
+
removeMapSource(this._map, this._sourceId);
|
|
1336
1339
|
}
|
|
1337
1340
|
}
|
|
1338
1341
|
|
|
@@ -1417,8 +1420,17 @@ class TiledMapService {
|
|
|
1417
1420
|
}
|
|
1418
1421
|
getMetadata() {
|
|
1419
1422
|
if (this._serviceMetadata !== null) return Promise.resolve(this._serviceMetadata);
|
|
1423
|
+
const {
|
|
1424
|
+
token,
|
|
1425
|
+
apiKey,
|
|
1426
|
+
authentication
|
|
1427
|
+
} = this.esriServiceOptions;
|
|
1420
1428
|
return new Promise((resolve, reject) => {
|
|
1421
|
-
getServiceDetails(this.esriServiceOptions.url,
|
|
1429
|
+
getServiceDetails(this.esriServiceOptions.url, {
|
|
1430
|
+
token,
|
|
1431
|
+
apiKey,
|
|
1432
|
+
authentication
|
|
1433
|
+
}).then(data => {
|
|
1422
1434
|
this._serviceMetadata = data;
|
|
1423
1435
|
resolve(data);
|
|
1424
1436
|
}).catch(err => reject(err));
|
|
@@ -1429,41 +1441,7 @@ class TiledMapService {
|
|
|
1429
1441
|
// This is a no-op to satisfy the common service interface.
|
|
1430
1442
|
}
|
|
1431
1443
|
remove() {
|
|
1432
|
-
|
|
1433
|
-
if (!this._map || typeof this._map.removeSource !== 'function') {
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
try {
|
|
1437
|
-
// Guard against map whose style has already been destroyed
|
|
1438
|
-
if (!this._map.style) return;
|
|
1439
|
-
// First, remove any layers that are using this source
|
|
1440
|
-
const mapWithStyle = this._map;
|
|
1441
|
-
if (mapWithStyle.getStyle && typeof mapWithStyle.getLayer === 'function') {
|
|
1442
|
-
const style = mapWithStyle.getStyle();
|
|
1443
|
-
const layers = style?.layers || [];
|
|
1444
|
-
const getLayer = mapWithStyle.getLayer;
|
|
1445
|
-
layers.forEach(layer => {
|
|
1446
|
-
if (layer.source === this._sourceId) {
|
|
1447
|
-
try {
|
|
1448
|
-
if (getLayer(layer.id)) {
|
|
1449
|
-
this._map.removeLayer(layer.id);
|
|
1450
|
-
}
|
|
1451
|
-
} catch {
|
|
1452
|
-
// Layer may already be removed
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
});
|
|
1456
|
-
}
|
|
1457
|
-
// Then check if source exists before trying to remove it
|
|
1458
|
-
if (typeof this._map.getSource === 'function') {
|
|
1459
|
-
const source = this._map.getSource(this._sourceId);
|
|
1460
|
-
if (source) {
|
|
1461
|
-
this._map.removeSource(this._sourceId);
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
} catch (error) {
|
|
1465
|
-
console.warn(`Failed to remove source ${this._sourceId}:`, error);
|
|
1466
|
-
}
|
|
1444
|
+
removeMapSource(this._map, this._sourceId);
|
|
1467
1445
|
}
|
|
1468
1446
|
}
|
|
1469
1447
|
|
|
@@ -1559,9 +1537,9 @@ class ImageService {
|
|
|
1559
1537
|
if (this._time) params.append('time', this._time);
|
|
1560
1538
|
if (this.options.mosaicRule) params.append('mosaicRule', JSON.stringify(this.options.mosaicRule));
|
|
1561
1539
|
if (this.options.renderingRule) params.append('renderingRule', JSON.stringify(this.options.renderingRule));
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1540
|
+
// apiKey is sent as the token URL parameter, matching the request-based paths.
|
|
1541
|
+
const auth = this.esriServiceOptions;
|
|
1542
|
+
appendTokenIfExists(params, auth.token ?? auth.apiKey);
|
|
1565
1543
|
return {
|
|
1566
1544
|
type: 'raster',
|
|
1567
1545
|
tiles: [`${this.options.url}/exportImage?bbox={bbox-epsg-3857}&${params.toString()}`],
|
|
@@ -1625,8 +1603,17 @@ class ImageService {
|
|
|
1625
1603
|
}
|
|
1626
1604
|
getMetadata() {
|
|
1627
1605
|
if (this._serviceMetadata !== null) return Promise.resolve(this._serviceMetadata);
|
|
1606
|
+
const {
|
|
1607
|
+
token,
|
|
1608
|
+
apiKey,
|
|
1609
|
+
authentication
|
|
1610
|
+
} = this.esriServiceOptions;
|
|
1628
1611
|
return new Promise((resolve, reject) => {
|
|
1629
|
-
getServiceDetails(this.esriServiceOptions.url,
|
|
1612
|
+
getServiceDetails(this.esriServiceOptions.url, {
|
|
1613
|
+
token,
|
|
1614
|
+
apiKey,
|
|
1615
|
+
authentication
|
|
1616
|
+
}).then(data => {
|
|
1630
1617
|
this._serviceMetadata = data;
|
|
1631
1618
|
resolve(this._serviceMetadata);
|
|
1632
1619
|
}).catch(err => reject(err));
|
|
@@ -1637,7 +1624,7 @@ class ImageService {
|
|
|
1637
1624
|
const canvas = this._map.getCanvas();
|
|
1638
1625
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1639
1626
|
const bounds = this._map.getBounds().toArray();
|
|
1640
|
-
const params =
|
|
1627
|
+
const params = {
|
|
1641
1628
|
sr: '4326',
|
|
1642
1629
|
geometryType: 'esriGeometryPoint',
|
|
1643
1630
|
geometry: JSON.stringify({
|
|
@@ -1650,50 +1637,26 @@ class ImageService {
|
|
|
1650
1637
|
tolerance: '3',
|
|
1651
1638
|
returnGeometry: returnGeometry.toString(),
|
|
1652
1639
|
imageDisplay: `${canvas.width},${canvas.height},${this.options.dpi}`,
|
|
1653
|
-
mapExtent: `${bounds[0][0]},${bounds[0][1]},${bounds[1][0]},${bounds[1][1]}
|
|
1654
|
-
|
|
1640
|
+
mapExtent: `${bounds[0][0]},${bounds[0][1]},${bounds[1][0]},${bounds[1][1]}`
|
|
1641
|
+
};
|
|
1642
|
+
if (this._time) params.time = this._time;
|
|
1643
|
+
const {
|
|
1644
|
+
token,
|
|
1645
|
+
apiKey,
|
|
1646
|
+
authentication
|
|
1647
|
+
} = this.esriServiceOptions;
|
|
1648
|
+
return esriRequest(`${this.esriServiceOptions.url}/identify`, {
|
|
1649
|
+
params,
|
|
1650
|
+
token,
|
|
1651
|
+
apiKey,
|
|
1652
|
+
authentication
|
|
1655
1653
|
});
|
|
1656
|
-
if (this._time) params.append('time', this._time);
|
|
1657
|
-
if (this.esriServiceOptions.token) {
|
|
1658
|
-
params.append('token', this.esriServiceOptions.token);
|
|
1659
|
-
}
|
|
1660
|
-
const response = await fetch(`${this.esriServiceOptions.url}/identify?${params.toString()}`, this.esriServiceOptions.fetchOptions);
|
|
1661
|
-
if (!response.ok) {
|
|
1662
|
-
throw new Error(`Identify request failed: HTTP ${response.status}`);
|
|
1663
|
-
}
|
|
1664
|
-
const data = await response.json();
|
|
1665
|
-
if (data.error) {
|
|
1666
|
-
throw new Error(`Identify request failed: ${data.error.message}`);
|
|
1667
|
-
}
|
|
1668
|
-
return data;
|
|
1669
1654
|
}
|
|
1670
1655
|
update() {
|
|
1671
1656
|
this._updateSource();
|
|
1672
1657
|
}
|
|
1673
1658
|
remove() {
|
|
1674
|
-
|
|
1675
|
-
try {
|
|
1676
|
-
// Guard against map whose style has already been destroyed
|
|
1677
|
-
const mapWithStyle = this._map;
|
|
1678
|
-
if (!mapWithStyle.style) return;
|
|
1679
|
-
// First, remove any layers that are using this source
|
|
1680
|
-
if (mapWithStyle.getStyle) {
|
|
1681
|
-
const style = mapWithStyle.getStyle();
|
|
1682
|
-
const layers = style?.layers || [];
|
|
1683
|
-
layers.forEach(layer => {
|
|
1684
|
-
if (layer.source === this._sourceId && this._map.getLayer(layer.id)) {
|
|
1685
|
-
this._map.removeLayer(layer.id);
|
|
1686
|
-
}
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
// Then check if source exists before trying to remove it
|
|
1690
|
-
if (this._map.getSource && this._map.getSource(this._sourceId)) {
|
|
1691
|
-
this._map.removeSource(this._sourceId);
|
|
1692
|
-
}
|
|
1693
|
-
} catch (error) {
|
|
1694
|
-
console.warn(`Failed to remove source ${this._sourceId}:`, error);
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1659
|
+
removeMapSource(this._map, this._sourceId);
|
|
1697
1660
|
}
|
|
1698
1661
|
}
|
|
1699
1662
|
|
|
@@ -1767,6 +1730,24 @@ class VectorBasemapStyle {
|
|
|
1767
1730
|
writable: true,
|
|
1768
1731
|
value: void 0
|
|
1769
1732
|
});
|
|
1733
|
+
Object.defineProperty(this, "_useSession", {
|
|
1734
|
+
enumerable: true,
|
|
1735
|
+
configurable: true,
|
|
1736
|
+
writable: true,
|
|
1737
|
+
value: void 0
|
|
1738
|
+
});
|
|
1739
|
+
Object.defineProperty(this, "_sessionDuration", {
|
|
1740
|
+
enumerable: true,
|
|
1741
|
+
configurable: true,
|
|
1742
|
+
writable: true,
|
|
1743
|
+
value: void 0
|
|
1744
|
+
});
|
|
1745
|
+
Object.defineProperty(this, "_session", {
|
|
1746
|
+
enumerable: true,
|
|
1747
|
+
configurable: true,
|
|
1748
|
+
writable: true,
|
|
1749
|
+
value: void 0
|
|
1750
|
+
});
|
|
1770
1751
|
let opts;
|
|
1771
1752
|
if (typeof auth === 'string') {
|
|
1772
1753
|
opts = {
|
|
@@ -1783,6 +1764,8 @@ class VectorBasemapStyle {
|
|
|
1783
1764
|
this._language = opts.language;
|
|
1784
1765
|
this._worldview = opts.worldview;
|
|
1785
1766
|
this._itemId = opts.itemId;
|
|
1767
|
+
this._useSession = opts.useSession ?? false;
|
|
1768
|
+
this._sessionDuration = opts.sessionDuration;
|
|
1786
1769
|
// Infer version: token => v2, else apiKey => v1
|
|
1787
1770
|
this._version = opts.version || (this._token ? 'v2' : 'v1');
|
|
1788
1771
|
// Determine host based on version if not overridden
|
|
@@ -1847,6 +1830,71 @@ class VectorBasemapStyle {
|
|
|
1847
1830
|
const vectorStyle = new VectorBasemapStyle(styleName, auth);
|
|
1848
1831
|
map.setStyle(vectorStyle.styleUrl);
|
|
1849
1832
|
}
|
|
1833
|
+
/**
|
|
1834
|
+
* The style family for the official basemap sessions API, derived from the
|
|
1835
|
+
* canonical style id. esri-gl styles are all in the `arcgis` family.
|
|
1836
|
+
*/
|
|
1837
|
+
get _styleFamily() {
|
|
1838
|
+
return this._canonical.startsWith('open/') ? 'open' : 'arcgis';
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* The token from the active session, if a session has been started. Useful
|
|
1842
|
+
* for inspection / debugging. Returns `undefined` until `startSession()` runs.
|
|
1843
|
+
*/
|
|
1844
|
+
get sessionToken() {
|
|
1845
|
+
return this._session?.token;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Start (or reuse) an official basemap style session via
|
|
1849
|
+
* `@esri/arcgis-rest-basemap-sessions`. Requires an apiKey or token. The
|
|
1850
|
+
* resulting session is cached so repeated calls return the same instance.
|
|
1851
|
+
*/
|
|
1852
|
+
async startSession() {
|
|
1853
|
+
if (this._session) return this._session;
|
|
1854
|
+
const authentication = resolveAuthentication({
|
|
1855
|
+
apiKey: this._apiKey,
|
|
1856
|
+
token: this._token
|
|
1857
|
+
});
|
|
1858
|
+
if (!authentication) {
|
|
1859
|
+
throw new Error('An Esri API Key or token must be supplied to start a basemap style session');
|
|
1860
|
+
}
|
|
1861
|
+
this._session = await BasemapStyleSession.start({
|
|
1862
|
+
authentication,
|
|
1863
|
+
styleFamily: this._styleFamily,
|
|
1864
|
+
...(this._sessionDuration !== undefined ? {
|
|
1865
|
+
duration: this._sessionDuration
|
|
1866
|
+
} : {})
|
|
1867
|
+
});
|
|
1868
|
+
return this._session;
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Resolve the style URL to apply. When `useSession` is enabled this starts a
|
|
1872
|
+
* session and returns a v2 style URL backed by the session token; otherwise it
|
|
1873
|
+
* returns the synchronous `styleUrl`.
|
|
1874
|
+
*/
|
|
1875
|
+
async getStyleUrl() {
|
|
1876
|
+
if (!this._useSession) return this.styleUrl;
|
|
1877
|
+
const session = await this.startSession();
|
|
1878
|
+
const token = await session.getToken();
|
|
1879
|
+
const params = new URLSearchParams();
|
|
1880
|
+
params.set('f', this._format);
|
|
1881
|
+
params.set('token', token);
|
|
1882
|
+
if (this._language) params.set('language', this._language);
|
|
1883
|
+
if (this._worldview) params.set('worldview', this._worldview);
|
|
1884
|
+
return `https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/${this._canonical}?${params.toString()}`;
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Apply a basemap style to a MapLibre/Mapbox map using a session-backed style
|
|
1888
|
+
* URL. Mirrors {@link applyStyle} but starts a basemap style session first.
|
|
1889
|
+
*/
|
|
1890
|
+
static async applyStyleWithSession(map, styleName, auth) {
|
|
1891
|
+
const vectorStyle = new VectorBasemapStyle(styleName, {
|
|
1892
|
+
...auth,
|
|
1893
|
+
useSession: true
|
|
1894
|
+
});
|
|
1895
|
+
const url = await vectorStyle.getStyleUrl();
|
|
1896
|
+
map.setStyle(url);
|
|
1897
|
+
}
|
|
1850
1898
|
static _toCanonical(name) {
|
|
1851
1899
|
if (!name) return 'arcgis/streets';
|
|
1852
1900
|
// Already slash form
|
|
@@ -1994,10 +2042,17 @@ class VectorTileService {
|
|
|
1994
2042
|
});
|
|
1995
2043
|
}
|
|
1996
2044
|
_retrieveStyle() {
|
|
2045
|
+
const {
|
|
2046
|
+
token,
|
|
2047
|
+
apiKey,
|
|
2048
|
+
authentication
|
|
2049
|
+
} = this.esriServiceOptions;
|
|
1997
2050
|
return new Promise((resolve, reject) => {
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2051
|
+
esriRequest(`${this.options.url}/${this._styleUrl}`, {
|
|
2052
|
+
httpMethod: 'GET',
|
|
2053
|
+
token,
|
|
2054
|
+
apiKey,
|
|
2055
|
+
authentication
|
|
2001
2056
|
}).then(data => {
|
|
2002
2057
|
if (!data || !Array.isArray(data.layers) || data.layers.length === 0) {
|
|
2003
2058
|
throw new Error('VectorTile style document is missing layers.');
|
|
@@ -2010,8 +2065,17 @@ class VectorTileService {
|
|
|
2010
2065
|
}
|
|
2011
2066
|
getMetadata() {
|
|
2012
2067
|
if (this._serviceMetadata !== null) return Promise.resolve(this._serviceMetadata);
|
|
2068
|
+
const {
|
|
2069
|
+
token,
|
|
2070
|
+
apiKey,
|
|
2071
|
+
authentication
|
|
2072
|
+
} = this.esriServiceOptions;
|
|
2013
2073
|
return new Promise((resolve, reject) => {
|
|
2014
|
-
getServiceDetails(this.esriServiceOptions.url,
|
|
2074
|
+
getServiceDetails(this.esriServiceOptions.url, {
|
|
2075
|
+
token,
|
|
2076
|
+
apiKey,
|
|
2077
|
+
authentication
|
|
2078
|
+
}).then(data => {
|
|
2015
2079
|
this._serviceMetadata = data;
|
|
2016
2080
|
resolve(this._serviceMetadata);
|
|
2017
2081
|
}).catch(err => reject(err));
|
|
@@ -2021,56 +2085,7 @@ class VectorTileService {
|
|
|
2021
2085
|
// Vector tile services don't need dynamic updates like dynamic services
|
|
2022
2086
|
}
|
|
2023
2087
|
remove() {
|
|
2024
|
-
|
|
2025
|
-
if (!map || typeof map.removeSource !== 'function') {
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
try {
|
|
2029
|
-
// Guard against map whose style has already been destroyed
|
|
2030
|
-
if (!map.style) return;
|
|
2031
|
-
const mapWithStyle = map;
|
|
2032
|
-
const mapLayerApi = map;
|
|
2033
|
-
const mapSourceApi = map;
|
|
2034
|
-
if (typeof mapWithStyle.getStyle === 'function') {
|
|
2035
|
-
const style = mapWithStyle.getStyle();
|
|
2036
|
-
const layers = style?.layers || [];
|
|
2037
|
-
layers.forEach(layer => {
|
|
2038
|
-
if (layer.source !== this._sourceId) return;
|
|
2039
|
-
if (typeof mapLayerApi.getLayer !== 'function' || typeof mapLayerApi.removeLayer !== 'function') {
|
|
2040
|
-
return;
|
|
2041
|
-
}
|
|
2042
|
-
let hasLayer = false;
|
|
2043
|
-
try {
|
|
2044
|
-
hasLayer = Boolean(mapLayerApi.getLayer(layer.id));
|
|
2045
|
-
} catch {
|
|
2046
|
-
hasLayer = false;
|
|
2047
|
-
}
|
|
2048
|
-
if (!hasLayer) return;
|
|
2049
|
-
try {
|
|
2050
|
-
mapLayerApi.removeLayer(layer.id);
|
|
2051
|
-
} catch (error) {
|
|
2052
|
-
console.warn(`Failed to remove layer ${layer.id} for source ${this._sourceId}:`, error);
|
|
2053
|
-
}
|
|
2054
|
-
});
|
|
2055
|
-
}
|
|
2056
|
-
if (typeof mapSourceApi.getSource === 'function') {
|
|
2057
|
-
let hasSource = false;
|
|
2058
|
-
try {
|
|
2059
|
-
hasSource = Boolean(mapSourceApi.getSource(this._sourceId));
|
|
2060
|
-
} catch {
|
|
2061
|
-
hasSource = false;
|
|
2062
|
-
}
|
|
2063
|
-
if (hasSource) {
|
|
2064
|
-
try {
|
|
2065
|
-
map.removeSource(this._sourceId);
|
|
2066
|
-
} catch (error) {
|
|
2067
|
-
console.warn(`Failed to remove source ${this._sourceId}:`, error);
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
} catch (error) {
|
|
2072
|
-
console.warn(`Failed to remove source ${this._sourceId}:`, error);
|
|
2073
|
-
}
|
|
2088
|
+
removeMapSource(this._map, this._sourceId);
|
|
2074
2089
|
}
|
|
2075
2090
|
}
|
|
2076
2091
|
|
|
@@ -2149,6 +2164,12 @@ class FeatureService {
|
|
|
2149
2164
|
writable: true,
|
|
2150
2165
|
value: null
|
|
2151
2166
|
});
|
|
2167
|
+
Object.defineProperty(this, "_removed", {
|
|
2168
|
+
enumerable: true,
|
|
2169
|
+
configurable: true,
|
|
2170
|
+
writable: true,
|
|
2171
|
+
value: false
|
|
2172
|
+
});
|
|
2152
2173
|
Object.defineProperty(this, "_format", {
|
|
2153
2174
|
enumerable: true,
|
|
2154
2175
|
configurable: true,
|
|
@@ -2292,29 +2313,9 @@ class FeatureService {
|
|
|
2292
2313
|
* Remove the source and clean up event listeners
|
|
2293
2314
|
*/
|
|
2294
2315
|
remove() {
|
|
2316
|
+
this._removed = true;
|
|
2295
2317
|
this.disableRequests();
|
|
2296
|
-
|
|
2297
|
-
try {
|
|
2298
|
-
// Guard against map whose style has already been destroyed
|
|
2299
|
-
const mapWithStyle = this._map;
|
|
2300
|
-
if (!mapWithStyle.style) return;
|
|
2301
|
-
// First, remove any layers that are using this source
|
|
2302
|
-
if (mapWithStyle.getStyle) {
|
|
2303
|
-
const style = mapWithStyle.getStyle();
|
|
2304
|
-
const layers = style?.layers || [];
|
|
2305
|
-
layers.forEach(layer => {
|
|
2306
|
-
if (layer.source === this._sourceId && this._map.getLayer && this._map.getLayer(layer.id)) {
|
|
2307
|
-
this._map.removeLayer(layer.id);
|
|
2308
|
-
}
|
|
2309
|
-
});
|
|
2310
|
-
}
|
|
2311
|
-
if (this._map.getSource && this._map.getSource(this._sourceId)) {
|
|
2312
|
-
this._map.removeSource(this._sourceId);
|
|
2313
|
-
}
|
|
2314
|
-
} catch (error) {
|
|
2315
|
-
console.warn(`Failed to remove source ${this._sourceId}:`, error);
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
+
removeMapSource(this._map, this._sourceId);
|
|
2318
2319
|
}
|
|
2319
2320
|
/** Alias for remove() for API compatibility */
|
|
2320
2321
|
destroySource() {
|
|
@@ -2485,6 +2486,7 @@ class FeatureService {
|
|
|
2485
2486
|
return newFeatureIdIndex;
|
|
2486
2487
|
}
|
|
2487
2488
|
async _findAndMapData() {
|
|
2489
|
+
if (this._removed) return;
|
|
2488
2490
|
const z = this._map.getZoom();
|
|
2489
2491
|
if (z < this._esriServiceOptions.minZoom) {
|
|
2490
2492
|
return;
|
|
@@ -2583,7 +2585,7 @@ class FeatureService {
|
|
|
2583
2585
|
xmax: tileBounds[2],
|
|
2584
2586
|
ymax: tileBounds[3]
|
|
2585
2587
|
};
|
|
2586
|
-
const params =
|
|
2588
|
+
const params = {
|
|
2587
2589
|
f: this._format,
|
|
2588
2590
|
geometry: JSON.stringify(extent),
|
|
2589
2591
|
where: this._esriServiceOptions.where,
|
|
@@ -2601,18 +2603,24 @@ class FeatureService {
|
|
|
2601
2603
|
spatialRel: 'esriSpatialRelIntersects',
|
|
2602
2604
|
geometryType: 'esriGeometryEnvelope',
|
|
2603
2605
|
inSR: '4326'
|
|
2604
|
-
}
|
|
2606
|
+
};
|
|
2605
2607
|
if (this._time) {
|
|
2606
|
-
params.
|
|
2608
|
+
params.time = this._time;
|
|
2607
2609
|
}
|
|
2608
|
-
|
|
2610
|
+
const {
|
|
2611
|
+
token,
|
|
2612
|
+
apiKey,
|
|
2613
|
+
authentication
|
|
2614
|
+
} = this._authOptions();
|
|
2615
|
+
const queryUrl = `${this._esriServiceOptions.url}/query`;
|
|
2609
2616
|
try {
|
|
2610
|
-
const response = await fetch(`${this._esriServiceOptions.url}/query?${params.toString()}`, this._esriServiceOptions.fetchOptions);
|
|
2611
|
-
if (!response.ok) {
|
|
2612
|
-
console.warn(`Tile fetch failed: HTTP ${response.status}`);
|
|
2613
|
-
return null;
|
|
2614
|
-
}
|
|
2615
2617
|
if (this._format === 'pbf') {
|
|
2618
|
+
const response = await esriRawRequest(queryUrl, {
|
|
2619
|
+
params,
|
|
2620
|
+
token,
|
|
2621
|
+
apiKey,
|
|
2622
|
+
authentication
|
|
2623
|
+
});
|
|
2616
2624
|
const buffer = await response.arrayBuffer();
|
|
2617
2625
|
try {
|
|
2618
2626
|
const decoded = tileDecode(new Uint8Array(buffer));
|
|
@@ -2622,8 +2630,12 @@ class FeatureService {
|
|
|
2622
2630
|
return null;
|
|
2623
2631
|
}
|
|
2624
2632
|
} else {
|
|
2625
|
-
const data = await
|
|
2626
|
-
|
|
2633
|
+
const data = await esriRequest(queryUrl, {
|
|
2634
|
+
params,
|
|
2635
|
+
token,
|
|
2636
|
+
apiKey,
|
|
2637
|
+
authentication
|
|
2638
|
+
});
|
|
2627
2639
|
return data;
|
|
2628
2640
|
}
|
|
2629
2641
|
} catch (error) {
|
|
@@ -2633,8 +2645,17 @@ class FeatureService {
|
|
|
2633
2645
|
}
|
|
2634
2646
|
}
|
|
2635
2647
|
_updateFcOnMap(fc) {
|
|
2636
|
-
|
|
2637
|
-
|
|
2648
|
+
// Guard against an in-flight tile request resolving after the service/map
|
|
2649
|
+
// was removed (e.g. a moveend handler firing during unmount).
|
|
2650
|
+
const map = this._map;
|
|
2651
|
+
if (this._removed || !map || !map.style || typeof map.getSource !== 'function') return;
|
|
2652
|
+
let source;
|
|
2653
|
+
try {
|
|
2654
|
+
source = map.getSource(this._sourceId);
|
|
2655
|
+
} catch {
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
if (source && typeof source === 'object' && 'setData' in source) {
|
|
2638
2659
|
source.setData(fc);
|
|
2639
2660
|
}
|
|
2640
2661
|
}
|
|
@@ -2648,18 +2669,13 @@ class FeatureService {
|
|
|
2648
2669
|
}
|
|
2649
2670
|
async _getServiceMetadata() {
|
|
2650
2671
|
if (this._serviceMetadata !== null) return this._serviceMetadata;
|
|
2651
|
-
|
|
2652
|
-
|
|
2672
|
+
// The FeatureService url is a layer endpoint, so the layer definition
|
|
2673
|
+
// (supportedQueryFormats, uniqueIdField, extent, geometryType, …) comes
|
|
2674
|
+
// from the typed `getLayer` helper.
|
|
2675
|
+
const data = await getLayer({
|
|
2676
|
+
url: this._esriServiceOptions.url,
|
|
2677
|
+
authentication: this._authentication()
|
|
2653
2678
|
});
|
|
2654
|
-
this._appendTokenIfExists(params);
|
|
2655
|
-
const response = await fetch(`${this._esriServiceOptions.url}?${params.toString()}`, this._esriServiceOptions.fetchOptions);
|
|
2656
|
-
if (!response.ok) {
|
|
2657
|
-
throw new Error(`Failed to fetch service metadata: HTTP ${response.status}`);
|
|
2658
|
-
}
|
|
2659
|
-
const data = await response.json();
|
|
2660
|
-
if (data.error) {
|
|
2661
|
-
throw new Error(JSON.stringify(data.error));
|
|
2662
|
-
}
|
|
2663
2679
|
this._serviceMetadata = data;
|
|
2664
2680
|
return this._serviceMetadata;
|
|
2665
2681
|
}
|
|
@@ -2667,7 +2683,7 @@ class FeatureService {
|
|
|
2667
2683
|
* Query features by longitude/latitude with optional radius
|
|
2668
2684
|
*/
|
|
2669
2685
|
async getFeaturesByLonLat(lnglat, radius = 20, returnGeometry = false) {
|
|
2670
|
-
const params =
|
|
2686
|
+
const params = {
|
|
2671
2687
|
sr: '4326',
|
|
2672
2688
|
geometryType: 'esriGeometryPoint',
|
|
2673
2689
|
geometry: JSON.stringify({
|
|
@@ -2677,41 +2693,41 @@ class FeatureService {
|
|
|
2677
2693
|
wkid: 4326
|
|
2678
2694
|
}
|
|
2679
2695
|
}),
|
|
2680
|
-
returnGeometry
|
|
2696
|
+
returnGeometry,
|
|
2681
2697
|
outFields: '*',
|
|
2682
2698
|
spatialRel: 'esriSpatialRelIntersects',
|
|
2683
2699
|
units: 'esriSRUnit_Meter',
|
|
2684
|
-
distance: radius
|
|
2700
|
+
distance: radius,
|
|
2685
2701
|
f: 'geojson'
|
|
2686
|
-
}
|
|
2702
|
+
};
|
|
2687
2703
|
if (this._time) {
|
|
2688
|
-
params.
|
|
2704
|
+
params.time = this._time;
|
|
2689
2705
|
}
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
}
|
|
2695
|
-
return
|
|
2706
|
+
const result = await queryFeatures({
|
|
2707
|
+
url: this._esriServiceOptions.url,
|
|
2708
|
+
params,
|
|
2709
|
+
authentication: this._authentication()
|
|
2710
|
+
});
|
|
2711
|
+
return result;
|
|
2696
2712
|
}
|
|
2697
2713
|
/**
|
|
2698
2714
|
* Query features by object IDs
|
|
2699
2715
|
*/
|
|
2700
2716
|
async getFeaturesByObjectIds(objectIds, returnGeometry = false) {
|
|
2701
2717
|
const idsString = Array.isArray(objectIds) ? objectIds.join(',') : objectIds;
|
|
2702
|
-
const params =
|
|
2718
|
+
const params = {
|
|
2703
2719
|
sr: '4326',
|
|
2704
2720
|
objectIds: idsString,
|
|
2705
|
-
returnGeometry
|
|
2721
|
+
returnGeometry,
|
|
2706
2722
|
outFields: '*',
|
|
2707
2723
|
f: 'geojson'
|
|
2724
|
+
};
|
|
2725
|
+
const result = await queryFeatures({
|
|
2726
|
+
url: this._esriServiceOptions.url,
|
|
2727
|
+
params,
|
|
2728
|
+
authentication: this._authentication()
|
|
2708
2729
|
});
|
|
2709
|
-
|
|
2710
|
-
const response = await fetch(`${this._esriServiceOptions.url}/query?${params.toString()}`, this._esriServiceOptions.fetchOptions);
|
|
2711
|
-
if (!response.ok) {
|
|
2712
|
-
throw new Error(`Query failed: HTTP ${response.status}`);
|
|
2713
|
-
}
|
|
2714
|
-
return await response.json();
|
|
2730
|
+
return result;
|
|
2715
2731
|
}
|
|
2716
2732
|
/**
|
|
2717
2733
|
* Query features with custom options
|
|
@@ -2721,31 +2737,31 @@ class FeatureService {
|
|
|
2721
2737
|
...this._esriServiceOptions,
|
|
2722
2738
|
...options
|
|
2723
2739
|
};
|
|
2724
|
-
const params =
|
|
2725
|
-
params.
|
|
2726
|
-
params.
|
|
2727
|
-
params.
|
|
2728
|
-
params.
|
|
2740
|
+
const params = {};
|
|
2741
|
+
params.f = 'geojson';
|
|
2742
|
+
params.where = queryOptions.where || '1=1';
|
|
2743
|
+
params.outFields = typeof queryOptions.outFields === 'string' ? queryOptions.outFields : queryOptions.outFields?.join(',') || '*';
|
|
2744
|
+
params.returnGeometry = (queryOptions.returnGeometry !== false).toString();
|
|
2729
2745
|
if (queryOptions.geometry) {
|
|
2730
|
-
params.
|
|
2731
|
-
if (queryOptions.geometryType) params.
|
|
2732
|
-
if (queryOptions.spatialRel) params.
|
|
2733
|
-
if (queryOptions.inSR) params.
|
|
2734
|
-
}
|
|
2735
|
-
if (queryOptions.outSR) params.
|
|
2736
|
-
if (queryOptions.orderByFields) params.
|
|
2737
|
-
if (queryOptions.groupByFieldsForStatistics) params.
|
|
2738
|
-
if (queryOptions.outStatistics && queryOptions.outStatistics.length > 0) params.
|
|
2739
|
-
if (queryOptions.having) params.
|
|
2740
|
-
if (queryOptions.resultOffset) params.
|
|
2741
|
-
if (queryOptions.resultRecordCount) params.
|
|
2742
|
-
if (queryOptions.token) params.append('token', queryOptions.token);
|
|
2746
|
+
params.geometry = JSON.stringify(queryOptions.geometry);
|
|
2747
|
+
if (queryOptions.geometryType) params.geometryType = queryOptions.geometryType;
|
|
2748
|
+
if (queryOptions.spatialRel) params.spatialRel = queryOptions.spatialRel;
|
|
2749
|
+
if (queryOptions.inSR) params.inSR = queryOptions.inSR;
|
|
2750
|
+
}
|
|
2751
|
+
if (queryOptions.outSR) params.outSR = queryOptions.outSR;
|
|
2752
|
+
if (queryOptions.orderByFields) params.orderByFields = queryOptions.orderByFields;
|
|
2753
|
+
if (queryOptions.groupByFieldsForStatistics) params.groupByFieldsForStatistics = queryOptions.groupByFieldsForStatistics;
|
|
2754
|
+
if (queryOptions.outStatistics && queryOptions.outStatistics.length > 0) params.outStatistics = JSON.stringify(queryOptions.outStatistics);
|
|
2755
|
+
if (queryOptions.having) params.having = queryOptions.having;
|
|
2756
|
+
if (queryOptions.resultOffset) params.resultOffset = queryOptions.resultOffset.toString();
|
|
2757
|
+
if (queryOptions.resultRecordCount) params.resultRecordCount = queryOptions.resultRecordCount.toString();
|
|
2743
2758
|
try {
|
|
2744
|
-
const
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2759
|
+
const result = await queryFeatures({
|
|
2760
|
+
url: this._esriServiceOptions.url,
|
|
2761
|
+
params,
|
|
2762
|
+
authentication: this._authentication()
|
|
2763
|
+
});
|
|
2764
|
+
return result;
|
|
2749
2765
|
} catch (error) {
|
|
2750
2766
|
const isTestEnvironment = typeof process !== 'undefined' && process.env?.NODE_ENV === 'test';
|
|
2751
2767
|
if (!isTestEnvironment) {
|
|
@@ -2756,7 +2772,7 @@ class FeatureService {
|
|
|
2756
2772
|
}
|
|
2757
2773
|
async _projectBounds() {
|
|
2758
2774
|
if (!this._serviceMetadata?.extent) return;
|
|
2759
|
-
const params =
|
|
2775
|
+
const params = {
|
|
2760
2776
|
geometries: JSON.stringify({
|
|
2761
2777
|
geometryType: 'esriGeometryEnvelope',
|
|
2762
2778
|
geometries: [this._serviceMetadata.extent]
|
|
@@ -2764,22 +2780,15 @@ class FeatureService {
|
|
|
2764
2780
|
inSR: (this._serviceMetadata.extent.spatialReference?.wkid || 4326).toString(),
|
|
2765
2781
|
outSR: '4326',
|
|
2766
2782
|
f: 'json'
|
|
2767
|
-
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
} else {
|
|
2772
|
-
fetchOptions = undefined;
|
|
2773
|
-
}
|
|
2783
|
+
};
|
|
2784
|
+
// The public fallback projection endpoint is anonymous; only forward auth
|
|
2785
|
+
// to the service's own projection endpoint.
|
|
2786
|
+
const auth = this._projectionEndpointIsFallback() ? {} : this._authOptions();
|
|
2774
2787
|
try {
|
|
2775
|
-
const
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
}
|
|
2779
|
-
const data = await response.json();
|
|
2780
|
-
if (data.error) {
|
|
2781
|
-
throw new Error(JSON.stringify(data.error));
|
|
2782
|
-
}
|
|
2788
|
+
const data = await esriRequest(this._esriServiceOptions.projectionEndpoint, {
|
|
2789
|
+
params,
|
|
2790
|
+
...auth
|
|
2791
|
+
});
|
|
2783
2792
|
const extent = data.geometries[0];
|
|
2784
2793
|
this._maxExtent = [extent.xmin, extent.ymin, extent.xmax, extent.ymax];
|
|
2785
2794
|
} catch (error) {
|
|
@@ -2805,28 +2814,22 @@ class FeatureService {
|
|
|
2805
2814
|
updateAttribution('', this._sourceId, this._map);
|
|
2806
2815
|
}
|
|
2807
2816
|
}
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
}
|
|
2820
|
-
return undefined;
|
|
2817
|
+
/**
|
|
2818
|
+
* Read the authentication-related fields off the service options. The
|
|
2819
|
+
* `authentication` field is not part of the public options type, so the
|
|
2820
|
+
* options object is cast to surface it for {@link resolveAuthentication}.
|
|
2821
|
+
*/
|
|
2822
|
+
_authOptions() {
|
|
2823
|
+
const opts = this._esriServiceOptions;
|
|
2824
|
+
return {
|
|
2825
|
+
token: opts.token,
|
|
2826
|
+
apiKey: opts.apiKey,
|
|
2827
|
+
authentication: opts.authentication
|
|
2828
|
+
};
|
|
2821
2829
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
const err = new Error(errorData.message || 'ArcGIS service error');
|
|
2826
|
-
err.code = errorData.code;
|
|
2827
|
-
err.details = errorData.details;
|
|
2828
|
-
throw err;
|
|
2829
|
-
}
|
|
2830
|
+
/** Build an ArcGIS REST JS authentication manager from the service options. */
|
|
2831
|
+
_authentication() {
|
|
2832
|
+
return resolveAuthentication(this._authOptions());
|
|
2830
2833
|
}
|
|
2831
2834
|
_handleAuthError(error) {
|
|
2832
2835
|
if (error && typeof error === 'object' && 'code' in error) {
|
|
@@ -2868,104 +2871,101 @@ class FeatureService {
|
|
|
2868
2871
|
return this;
|
|
2869
2872
|
}
|
|
2870
2873
|
// ========================================
|
|
2874
|
+
// Related Records & Domain Decoding
|
|
2875
|
+
// ========================================
|
|
2876
|
+
/**
|
|
2877
|
+
* Query records related to this layer's features through a relationship
|
|
2878
|
+
* class. Wraps `queryRelated` from `@esri/arcgis-rest-feature-service`.
|
|
2879
|
+
*/
|
|
2880
|
+
async queryRelatedRecords(options) {
|
|
2881
|
+
return queryRelated({
|
|
2882
|
+
url: this._esriServiceOptions.url,
|
|
2883
|
+
relationshipId: options.relationshipId,
|
|
2884
|
+
objectIds: options.objectIds,
|
|
2885
|
+
outFields: options.outFields,
|
|
2886
|
+
definitionExpression: options.definitionExpression,
|
|
2887
|
+
// `returnGeometry` is not a top-level queryRelated option; pass via params.
|
|
2888
|
+
params: options.returnGeometry !== undefined ? {
|
|
2889
|
+
returnGeometry: options.returnGeometry
|
|
2890
|
+
} : undefined,
|
|
2891
|
+
authentication: this._authentication()
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Replace coded-value-domain codes with their human-readable names in a
|
|
2896
|
+
* query response. Wraps `decodeValues` from
|
|
2897
|
+
* `@esri/arcgis-rest-feature-service`.
|
|
2898
|
+
*
|
|
2899
|
+
* @param queryResponse A response from a `f=json` feature query.
|
|
2900
|
+
* @param fields Optional subset of field names to decode (defaults to all).
|
|
2901
|
+
*/
|
|
2902
|
+
async decodeValues(queryResponse, fields) {
|
|
2903
|
+
return decodeValues({
|
|
2904
|
+
url: this._esriServiceOptions.url,
|
|
2905
|
+
queryResponse: queryResponse,
|
|
2906
|
+
fields: fields,
|
|
2907
|
+
authentication: this._authentication()
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
// ========================================
|
|
2871
2911
|
// Feature Editing Methods
|
|
2872
2912
|
// ========================================
|
|
2873
2913
|
/**
|
|
2874
2914
|
* Add features to the service
|
|
2875
2915
|
*/
|
|
2876
2916
|
async addFeatures(features, options) {
|
|
2877
|
-
const
|
|
2878
|
-
|
|
2879
|
-
features:
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
this._appendTokenIfExists(params);
|
|
2883
|
-
const response = await fetch(`${this._esriServiceOptions.url}/addFeatures`, {
|
|
2884
|
-
method: 'POST',
|
|
2885
|
-
body: params,
|
|
2886
|
-
headers: this._getFetchHeaders(),
|
|
2887
|
-
...this._esriServiceOptions.fetchOptions
|
|
2917
|
+
const res = await addFeatures({
|
|
2918
|
+
url: this._esriServiceOptions.url,
|
|
2919
|
+
features: features,
|
|
2920
|
+
gdbVersion: options?.gdbVersion,
|
|
2921
|
+
authentication: this._authentication()
|
|
2888
2922
|
});
|
|
2889
|
-
|
|
2890
|
-
throw new Error(`Add features failed: HTTP ${response.status}`);
|
|
2891
|
-
}
|
|
2892
|
-
const data = await response.json();
|
|
2893
|
-
this._checkAgolError(data);
|
|
2894
|
-
return data.addResults;
|
|
2923
|
+
return res.addResults;
|
|
2895
2924
|
}
|
|
2896
2925
|
/**
|
|
2897
2926
|
* Update existing features
|
|
2898
2927
|
*/
|
|
2899
2928
|
async updateFeatures(features, options) {
|
|
2900
|
-
const
|
|
2901
|
-
|
|
2902
|
-
features:
|
|
2929
|
+
const res = await updateFeatures({
|
|
2930
|
+
url: this._esriServiceOptions.url,
|
|
2931
|
+
features: features,
|
|
2932
|
+
gdbVersion: options?.gdbVersion,
|
|
2933
|
+
authentication: this._authentication()
|
|
2903
2934
|
});
|
|
2904
|
-
|
|
2905
|
-
this._appendTokenIfExists(params);
|
|
2906
|
-
const response = await fetch(`${this._esriServiceOptions.url}/updateFeatures`, {
|
|
2907
|
-
method: 'POST',
|
|
2908
|
-
body: params,
|
|
2909
|
-
headers: this._getFetchHeaders(),
|
|
2910
|
-
...this._esriServiceOptions.fetchOptions
|
|
2911
|
-
});
|
|
2912
|
-
if (!response.ok) {
|
|
2913
|
-
throw new Error(`Update features failed: HTTP ${response.status}`);
|
|
2914
|
-
}
|
|
2915
|
-
const data = await response.json();
|
|
2916
|
-
this._checkAgolError(data);
|
|
2917
|
-
return data.updateResults;
|
|
2935
|
+
return res.updateResults;
|
|
2918
2936
|
}
|
|
2919
2937
|
/**
|
|
2920
2938
|
* Delete features by objectIds or where clause
|
|
2921
2939
|
*/
|
|
2922
2940
|
async deleteFeatures(deleteParams) {
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
headers: this._getFetchHeaders(),
|
|
2937
|
-
...this._esriServiceOptions.fetchOptions
|
|
2941
|
+
// Pass objectIds / where as first-order fields so arcgis-rest encodes them.
|
|
2942
|
+
// Only include each when supplied: deletion by `where` alone must not send
|
|
2943
|
+
// an empty objectIds list, and a hardcoded `objectIds: []` would (being
|
|
2944
|
+
// truthy) override a supplied `where`/objectIds via appendCustomParams.
|
|
2945
|
+
const res = await deleteFeatures({
|
|
2946
|
+
url: this._esriServiceOptions.url,
|
|
2947
|
+
authentication: this._authentication(),
|
|
2948
|
+
...(deleteParams.objectIds ? {
|
|
2949
|
+
objectIds: deleteParams.objectIds
|
|
2950
|
+
} : {}),
|
|
2951
|
+
...(deleteParams.where ? {
|
|
2952
|
+
where: deleteParams.where
|
|
2953
|
+
} : {})
|
|
2938
2954
|
});
|
|
2939
|
-
|
|
2940
|
-
throw new Error(`Delete features failed: HTTP ${response.status}`);
|
|
2941
|
-
}
|
|
2942
|
-
const data = await response.json();
|
|
2943
|
-
this._checkAgolError(data);
|
|
2944
|
-
return data.deleteResults;
|
|
2955
|
+
return res.deleteResults;
|
|
2945
2956
|
}
|
|
2946
2957
|
/**
|
|
2947
2958
|
* Apply multiple edits in a single transaction
|
|
2948
2959
|
*/
|
|
2949
2960
|
async applyEdits(edits, options) {
|
|
2950
|
-
const
|
|
2951
|
-
|
|
2961
|
+
const data = await applyEdits({
|
|
2962
|
+
url: this._esriServiceOptions.url,
|
|
2963
|
+
adds: edits.adds,
|
|
2964
|
+
updates: edits.updates,
|
|
2965
|
+
deletes: edits.deletes,
|
|
2966
|
+
gdbVersion: options?.gdbVersion,
|
|
2967
|
+
authentication: this._authentication()
|
|
2952
2968
|
});
|
|
2953
|
-
if (edits.adds) params.append('adds', JSON.stringify(edits.adds));
|
|
2954
|
-
if (edits.updates) params.append('updates', JSON.stringify(edits.updates));
|
|
2955
|
-
if (edits.deletes) params.append('deletes', edits.deletes.join(','));
|
|
2956
|
-
if (options?.gdbVersion) params.append('gdbVersion', options.gdbVersion);
|
|
2957
|
-
this._appendTokenIfExists(params);
|
|
2958
|
-
const response = await fetch(`${this._esriServiceOptions.url}/applyEdits`, {
|
|
2959
|
-
method: 'POST',
|
|
2960
|
-
body: params,
|
|
2961
|
-
headers: this._getFetchHeaders(),
|
|
2962
|
-
...this._esriServiceOptions.fetchOptions
|
|
2963
|
-
});
|
|
2964
|
-
if (!response.ok) {
|
|
2965
|
-
throw new Error(`Apply edits failed: HTTP ${response.status}`);
|
|
2966
|
-
}
|
|
2967
|
-
const data = await response.json();
|
|
2968
|
-
this._checkAgolError(data);
|
|
2969
2969
|
return data;
|
|
2970
2970
|
}
|
|
2971
2971
|
// ========================================
|
|
@@ -2975,71 +2975,45 @@ class FeatureService {
|
|
|
2975
2975
|
* Query attachments for a feature
|
|
2976
2976
|
*/
|
|
2977
2977
|
async queryAttachments(objectId, options) {
|
|
2978
|
-
const
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
const response = await fetch(`${this._esriServiceOptions.url}/${objectId}/attachments?${params.toString()}`, {
|
|
2986
|
-
headers: this._getFetchHeaders(),
|
|
2987
|
-
...this._esriServiceOptions.fetchOptions
|
|
2978
|
+
const res = await getAttachments({
|
|
2979
|
+
url: this._esriServiceOptions.url,
|
|
2980
|
+
featureId: objectId,
|
|
2981
|
+
params: options?.globalIds ? {
|
|
2982
|
+
globalIds: options.globalIds.join(',')
|
|
2983
|
+
} : undefined,
|
|
2984
|
+
authentication: this._authentication()
|
|
2988
2985
|
});
|
|
2989
|
-
|
|
2990
|
-
throw new Error(`Query attachments failed: HTTP ${response.status}`);
|
|
2991
|
-
}
|
|
2992
|
-
const data = await response.json();
|
|
2993
|
-
this._checkAgolError(data);
|
|
2994
|
-
return data.attachmentInfos || [];
|
|
2986
|
+
return res.attachmentInfos || [];
|
|
2995
2987
|
}
|
|
2996
2988
|
/**
|
|
2997
2989
|
* Add an attachment to a feature
|
|
2998
2990
|
*/
|
|
2999
2991
|
async addAttachment(objectId, file, fileName) {
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
method: 'POST',
|
|
3012
|
-
body: formData,
|
|
3013
|
-
headers: Object.keys(headers).length > 0 ? headers : undefined
|
|
2992
|
+
// The package `addAttachment` only accepts a `File`. Preserve the existing
|
|
2993
|
+
// support for bare `Blob`s and an explicit `fileName` by posting the
|
|
2994
|
+
// attachment ourselves; `esriRequest` handles auth and error responses.
|
|
2995
|
+
const name = fileName || (file instanceof File ? file.name : 'attachment');
|
|
2996
|
+
const res = await esriRequest(`${this._esriServiceOptions.url}/${objectId}/addAttachment`, {
|
|
2997
|
+
params: {
|
|
2998
|
+
attachment: file,
|
|
2999
|
+
fileName: name
|
|
3000
|
+
},
|
|
3001
|
+
httpMethod: 'POST',
|
|
3002
|
+
...this._authOptions()
|
|
3014
3003
|
});
|
|
3015
|
-
|
|
3016
|
-
throw new Error(`Add attachment failed: HTTP ${response.status}`);
|
|
3017
|
-
}
|
|
3018
|
-
const data = await response.json();
|
|
3019
|
-
this._checkAgolError(data);
|
|
3020
|
-
return data.addAttachmentResult;
|
|
3004
|
+
return res.addAttachmentResult;
|
|
3021
3005
|
}
|
|
3022
3006
|
/**
|
|
3023
3007
|
* Delete attachments from a feature
|
|
3024
3008
|
*/
|
|
3025
3009
|
async deleteAttachments(objectId, attachmentIds) {
|
|
3026
|
-
const
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
const response = await fetch(`${this._esriServiceOptions.url}/${objectId}/deleteAttachments`, {
|
|
3032
|
-
method: 'POST',
|
|
3033
|
-
body: params,
|
|
3034
|
-
headers: this._getFetchHeaders(),
|
|
3035
|
-
...this._esriServiceOptions.fetchOptions
|
|
3010
|
+
const res = await deleteAttachments({
|
|
3011
|
+
url: this._esriServiceOptions.url,
|
|
3012
|
+
featureId: objectId,
|
|
3013
|
+
attachmentIds,
|
|
3014
|
+
authentication: this._authentication()
|
|
3036
3015
|
});
|
|
3037
|
-
|
|
3038
|
-
throw new Error(`Delete attachments failed: HTTP ${response.status}`);
|
|
3039
|
-
}
|
|
3040
|
-
const data = await response.json();
|
|
3041
|
-
this._checkAgolError(data);
|
|
3042
|
-
return data.deleteAttachmentResults;
|
|
3016
|
+
return res.deleteAttachmentResults;
|
|
3043
3017
|
}
|
|
3044
3018
|
// Legacy method aliases for backwards compatibility
|
|
3045
3019
|
/** @deprecated Use setWhere instead */
|
|
@@ -3212,58 +3186,31 @@ class Task {
|
|
|
3212
3186
|
});
|
|
3213
3187
|
}
|
|
3214
3188
|
/**
|
|
3215
|
-
* Direct HTTP request (when not using a service)
|
|
3189
|
+
* Direct HTTP request (when not using a service), delegated to
|
|
3190
|
+
* `@esri/arcgis-rest-request`. Token/apiKey supplied either as task options
|
|
3191
|
+
* or as `token`/`apiKey` params are routed through the auth layer rather than
|
|
3192
|
+
* being sent as raw query parameters.
|
|
3216
3193
|
*/
|
|
3217
3194
|
_request(method, path, params, callback) {
|
|
3218
3195
|
if (!this.options.url) {
|
|
3219
3196
|
callback(new Error('URL is required for task execution'));
|
|
3220
3197
|
return;
|
|
3221
3198
|
}
|
|
3222
|
-
|
|
3223
|
-
const baseUrl = this.options.url.endsWith('/') ? this.options.url.slice(0, -1) : this.options.url;
|
|
3199
|
+
const baseUrl = cleanTrailingSlash(this.options.url);
|
|
3224
3200
|
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
|
3225
|
-
const
|
|
3226
|
-
const
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
}
|
|
3239
|
-
}
|
|
3240
|
-
});
|
|
3241
|
-
const fullUrl = method === 'GET' ? `${url}?${searchParams.toString()}` : url;
|
|
3242
|
-
const fetchOptions = {
|
|
3243
|
-
method,
|
|
3244
|
-
headers: {
|
|
3245
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
3246
|
-
}
|
|
3247
|
-
};
|
|
3248
|
-
if (method === 'POST') {
|
|
3249
|
-
fetchOptions.body = searchParams.toString();
|
|
3250
|
-
}
|
|
3251
|
-
fetch(fullUrl, fetchOptions).then(response => {
|
|
3252
|
-
if (!response.ok) {
|
|
3253
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
3254
|
-
}
|
|
3255
|
-
return response.json();
|
|
3256
|
-
}).then(data => {
|
|
3257
|
-
// Check for AGOL JSON-level errors (HTTP 200 with error body)
|
|
3258
|
-
if (data && typeof data === 'object' && data.error) {
|
|
3259
|
-
const err = new Error(data.error.message || 'ArcGIS service error');
|
|
3260
|
-
err.code = data.error.code;
|
|
3261
|
-
err.details = data.error.details;
|
|
3262
|
-
callback(err);
|
|
3263
|
-
return;
|
|
3264
|
-
}
|
|
3265
|
-
callback(undefined, data);
|
|
3266
|
-
}).catch(error => callback(error));
|
|
3201
|
+
const url = `${baseUrl}${cleanPath}`;
|
|
3202
|
+
const {
|
|
3203
|
+
token,
|
|
3204
|
+
apiKey,
|
|
3205
|
+
...restParams
|
|
3206
|
+
} = params;
|
|
3207
|
+
esriRequest(url, {
|
|
3208
|
+
params: restParams,
|
|
3209
|
+
httpMethod: method === 'GET' ? 'GET' : 'POST',
|
|
3210
|
+
token: token ?? this.options.token,
|
|
3211
|
+
apiKey: apiKey ?? this.options.apikey,
|
|
3212
|
+
authentication: this.options.authentication
|
|
3213
|
+
}).then(data => callback(undefined, data)).catch(error => callback(error));
|
|
3267
3214
|
}
|
|
3268
3215
|
}
|
|
3269
3216
|
|
|
@@ -4276,5 +4223,205 @@ function identifyImage(options) {
|
|
|
4276
4223
|
return new IdentifyImage(options);
|
|
4277
4224
|
}
|
|
4278
4225
|
|
|
4279
|
-
|
|
4280
|
-
|
|
4226
|
+
/**
|
|
4227
|
+
* Portal item resolution.
|
|
4228
|
+
*
|
|
4229
|
+
* Turns an ArcGIS portal item id into ready-to-render esri-gl services using
|
|
4230
|
+
* `@esri/arcgis-rest-portal`. Two entry points are provided:
|
|
4231
|
+
*
|
|
4232
|
+
* - {@link serviceFromPortalItem} — resolve a single-layer item (Feature / Map /
|
|
4233
|
+
* Image / Vector Tile service) to the matching esri-gl service.
|
|
4234
|
+
* - {@link servicesFromWebMap} — read a Web Map item's `operationalLayers`
|
|
4235
|
+
* (and optionally its basemap) and instantiate a service per layer.
|
|
4236
|
+
*/
|
|
4237
|
+
function requestOptions(options) {
|
|
4238
|
+
const authentication = resolveAuthentication(options);
|
|
4239
|
+
const opts = {};
|
|
4240
|
+
if (authentication) opts.authentication = authentication;
|
|
4241
|
+
if (options?.portal) opts.portal = options.portal;
|
|
4242
|
+
return opts;
|
|
4243
|
+
}
|
|
4244
|
+
/** Shared auth fields forwarded into a constructed service's options. */
|
|
4245
|
+
function authServiceFields(options) {
|
|
4246
|
+
const fields = {};
|
|
4247
|
+
if (options?.authentication !== undefined) fields.authentication = options.authentication;
|
|
4248
|
+
if (options?.apiKey !== undefined) fields.apiKey = options.apiKey;
|
|
4249
|
+
if (options?.token !== undefined) fields.token = options.token;
|
|
4250
|
+
return fields;
|
|
4251
|
+
}
|
|
4252
|
+
/** True when a Feature Service url already points at a specific sublayer. */
|
|
4253
|
+
function urlHasLayerIndex(url) {
|
|
4254
|
+
return /\/\d+$/.test(cleanTrailingSlash(url));
|
|
4255
|
+
}
|
|
4256
|
+
/**
|
|
4257
|
+
* Construct an esri-gl service for a known ArcGIS service `url` and a
|
|
4258
|
+
* normalized service `kind`. Shared by single-item and web map resolution.
|
|
4259
|
+
*/
|
|
4260
|
+
function constructService(kind, sourceId, map, url, options) {
|
|
4261
|
+
const auth = authServiceFields(options);
|
|
4262
|
+
const serviceOptions = options?.serviceOptions ?? {};
|
|
4263
|
+
const baseOptions = {
|
|
4264
|
+
url,
|
|
4265
|
+
...auth,
|
|
4266
|
+
...serviceOptions
|
|
4267
|
+
};
|
|
4268
|
+
switch (kind) {
|
|
4269
|
+
case 'tiled':
|
|
4270
|
+
return new TiledMapService(sourceId, map, baseOptions, options?.rasterSrcOptions);
|
|
4271
|
+
case 'image':
|
|
4272
|
+
return new ImageService(sourceId, map, baseOptions, options?.rasterSrcOptions);
|
|
4273
|
+
case 'vector-tile':
|
|
4274
|
+
return new VectorTileService(sourceId, map, baseOptions, options?.vectorSrcOptions);
|
|
4275
|
+
case 'feature':
|
|
4276
|
+
{
|
|
4277
|
+
let layerUrl = cleanTrailingSlash(url);
|
|
4278
|
+
if (!urlHasLayerIndex(layerUrl)) {
|
|
4279
|
+
layerUrl = `${layerUrl}/${options?.layerId ?? 0}`;
|
|
4280
|
+
}
|
|
4281
|
+
return new FeatureService(sourceId, map, {
|
|
4282
|
+
...baseOptions,
|
|
4283
|
+
url: layerUrl
|
|
4284
|
+
}, options?.geojsonSourceOptions);
|
|
4285
|
+
}
|
|
4286
|
+
case 'dynamic':
|
|
4287
|
+
default:
|
|
4288
|
+
return new DynamicMapService(sourceId, map, baseOptions, options?.rasterSrcOptions);
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
/** Map a portal item `type` (+ typeKeywords) to an esri-gl service kind. */
|
|
4292
|
+
function kindFromItemType(type, typeKeywords = []) {
|
|
4293
|
+
switch (type) {
|
|
4294
|
+
case 'Feature Service':
|
|
4295
|
+
case 'Feature Layer':
|
|
4296
|
+
return 'feature';
|
|
4297
|
+
case 'Image Service':
|
|
4298
|
+
return 'image';
|
|
4299
|
+
case 'Vector Tile Service':
|
|
4300
|
+
return 'vector-tile';
|
|
4301
|
+
case 'Map Service':
|
|
4302
|
+
{
|
|
4303
|
+
const tiled = typeKeywords.some(k => /tiled|cached/i.test(k));
|
|
4304
|
+
return tiled ? 'tiled' : 'dynamic';
|
|
4305
|
+
}
|
|
4306
|
+
default:
|
|
4307
|
+
return null;
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
/**
|
|
4311
|
+
* Resolve a single-layer portal item to the matching esri-gl service and add
|
|
4312
|
+
* its source to the map.
|
|
4313
|
+
*
|
|
4314
|
+
* @example
|
|
4315
|
+
* const { service } = await serviceFromPortalItem('my-source', map, 'a1b2c3', { token });
|
|
4316
|
+
*/
|
|
4317
|
+
async function serviceFromPortalItem(sourceId, map, itemId, options) {
|
|
4318
|
+
const item = await getItem(itemId, requestOptions(options));
|
|
4319
|
+
if (!item.url) {
|
|
4320
|
+
throw new Error(`Portal item ${itemId} ("${item.title}") has no service url.`);
|
|
4321
|
+
}
|
|
4322
|
+
const kind = kindFromItemType(item.type, item.typeKeywords);
|
|
4323
|
+
if (!kind) {
|
|
4324
|
+
throw new Error(`Portal item ${itemId} has unsupported type "${item.type}" for esri-gl.`);
|
|
4325
|
+
}
|
|
4326
|
+
const service = constructService(kind, sourceId, map, item.url, options);
|
|
4327
|
+
return {
|
|
4328
|
+
service,
|
|
4329
|
+
kind,
|
|
4330
|
+
sourceId,
|
|
4331
|
+
url: item.url,
|
|
4332
|
+
item,
|
|
4333
|
+
title: item.title
|
|
4334
|
+
};
|
|
4335
|
+
}
|
|
4336
|
+
/** Map a Web Map operationalLayer `layerType` to an esri-gl service kind. */
|
|
4337
|
+
function kindFromLayerType(layerType) {
|
|
4338
|
+
switch (layerType) {
|
|
4339
|
+
case 'ArcGISFeatureLayer':
|
|
4340
|
+
return 'feature';
|
|
4341
|
+
case 'ArcGISMapServiceLayer':
|
|
4342
|
+
return 'dynamic';
|
|
4343
|
+
case 'ArcGISTiledMapServiceLayer':
|
|
4344
|
+
return 'tiled';
|
|
4345
|
+
case 'ArcGISImageServiceLayer':
|
|
4346
|
+
return 'image';
|
|
4347
|
+
case 'VectorTileLayer':
|
|
4348
|
+
return 'vector-tile';
|
|
4349
|
+
default:
|
|
4350
|
+
return null;
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
/**
|
|
4354
|
+
* Read a Web Map item's data and instantiate an esri-gl service for each
|
|
4355
|
+
* supported operational layer (optionally including basemap layers).
|
|
4356
|
+
* Unsupported layer types are skipped.
|
|
4357
|
+
*
|
|
4358
|
+
* @example
|
|
4359
|
+
* const layers = await servicesFromWebMap(map, 'webmap-item-id', { token });
|
|
4360
|
+
* layers.forEach(({ service, title }) => { ... });
|
|
4361
|
+
*/
|
|
4362
|
+
async function servicesFromWebMap(map, itemId, options) {
|
|
4363
|
+
const data = await getItemData(itemId, requestOptions(options));
|
|
4364
|
+
if (!data) {
|
|
4365
|
+
throw new Error(`Web Map ${itemId} has no item data (is it shared / a Web Map?).`);
|
|
4366
|
+
}
|
|
4367
|
+
const prefix = options?.sourceIdPrefix ?? itemId;
|
|
4368
|
+
const results = [];
|
|
4369
|
+
const layers = [...(data.operationalLayers ?? [])];
|
|
4370
|
+
if (options?.includeBasemap && data.baseMap?.baseMapLayers) {
|
|
4371
|
+
layers.push(...data.baseMap.baseMapLayers);
|
|
4372
|
+
}
|
|
4373
|
+
layers.forEach((layer, index) => {
|
|
4374
|
+
const kind = kindFromLayerType(layer.layerType);
|
|
4375
|
+
const url = layer.url ?? layer.styleUrl;
|
|
4376
|
+
if (!kind || !url) return; // skip unsupported / urlless layers
|
|
4377
|
+
const sourceId = layer.id ? `${prefix}-${layer.id}` : `${prefix}-${index}`;
|
|
4378
|
+
try {
|
|
4379
|
+
const service = constructService(kind, sourceId, map, url, options);
|
|
4380
|
+
results.push({
|
|
4381
|
+
service,
|
|
4382
|
+
kind,
|
|
4383
|
+
sourceId,
|
|
4384
|
+
url,
|
|
4385
|
+
title: layer.title
|
|
4386
|
+
});
|
|
4387
|
+
} catch (error) {
|
|
4388
|
+
console.warn(`Skipping web map layer "${layer.title ?? sourceId}":`, error);
|
|
4389
|
+
}
|
|
4390
|
+
});
|
|
4391
|
+
return results;
|
|
4392
|
+
}
|
|
4393
|
+
// -----------------------------
|
|
4394
|
+
// Portal item search
|
|
4395
|
+
// -----------------------------
|
|
4396
|
+
/**
|
|
4397
|
+
* Search an ArcGIS portal for items, e.g. to discover services to load.
|
|
4398
|
+
* Thin wrapper over `searchItems` from `@esri/arcgis-rest-portal`; accepts a
|
|
4399
|
+
* query string, a {@link SearchQueryBuilder}, or a full `ISearchOptions`.
|
|
4400
|
+
*
|
|
4401
|
+
* @example
|
|
4402
|
+
* const { results } = await searchPortalItems('type:"Feature Service" AND owner:esri');
|
|
4403
|
+
* // or with auth / paging:
|
|
4404
|
+
* await searchPortalItems({ q: 'wildfire', num: 20, authentication }, { token });
|
|
4405
|
+
*/
|
|
4406
|
+
async function searchPortalItems(search, options) {
|
|
4407
|
+
if (typeof search === 'string' || search instanceof SearchQueryBuilder) {
|
|
4408
|
+
const {
|
|
4409
|
+
authentication
|
|
4410
|
+
} = requestOptions(options);
|
|
4411
|
+
return searchItems({
|
|
4412
|
+
q: search,
|
|
4413
|
+
authentication
|
|
4414
|
+
});
|
|
4415
|
+
}
|
|
4416
|
+
// Full ISearchOptions: merge in resolved auth/portal unless already provided.
|
|
4417
|
+
const {
|
|
4418
|
+
authentication
|
|
4419
|
+
} = requestOptions(options);
|
|
4420
|
+
return searchItems({
|
|
4421
|
+
authentication,
|
|
4422
|
+
...search
|
|
4423
|
+
});
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
export { DynamicMapService as D, FeatureService as F, IdentifyFeatures as I, Query as Q, Service as S, Task as T, VectorBasemapStyle as V, Find as a, IdentifyImage as b, ImageService as c, TiledMapService as d, VectorTileService as e, cleanTrailingSlash as f, esriRequest as g, find as h, getServiceDetails as i, identifyImage as j, serviceFromPortalItem as k, servicesFromWebMap as l, query as q, resolveAuthentication as r, searchPortalItems as s, updateAttribution as u };
|
|
4427
|
+
//# sourceMappingURL=index-DsY1_0df.js.map
|