@loaders.gl/arcgis 5.0.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 +52 -0
- package/README.md +28 -0
- package/dist/arcgis/arcgis-capability-graph.d.ts +50 -0
- package/dist/arcgis/arcgis-capability-graph.d.ts.map +1 -0
- package/dist/arcgis/arcgis-capability-graph.js +221 -0
- package/dist/arcgis/arcgis-capability-graph.js.map +1 -0
- package/dist/arcgis/arcgis-feature-query.d.ts +53 -0
- package/dist/arcgis/arcgis-feature-query.d.ts.map +1 -0
- package/dist/arcgis/arcgis-feature-query.js +381 -0
- package/dist/arcgis/arcgis-feature-query.js.map +1 -0
- package/dist/arcgis/arcgis-feature-server-source-loader-types.d.ts +33 -0
- package/dist/arcgis/arcgis-feature-server-source-loader-types.d.ts.map +1 -0
- package/dist/arcgis/arcgis-feature-server-source-loader-types.js +21 -0
- package/dist/arcgis/arcgis-feature-server-source-loader-types.js.map +1 -0
- package/dist/arcgis/arcgis-feature-server-source-loader.d.ts +84 -0
- package/dist/arcgis/arcgis-feature-server-source-loader.d.ts.map +1 -0
- package/dist/arcgis/arcgis-feature-server-source-loader.js +280 -0
- package/dist/arcgis/arcgis-feature-server-source-loader.js.map +1 -0
- package/dist/arcgis/arcgis-image-server-source-loader-types.d.ts +31 -0
- package/dist/arcgis/arcgis-image-server-source-loader-types.d.ts.map +1 -0
- package/dist/arcgis/arcgis-image-server-source-loader-types.js +21 -0
- package/dist/arcgis/arcgis-image-server-source-loader-types.js.map +1 -0
- package/dist/arcgis/arcgis-image-server-source-loader.d.ts +60 -0
- package/dist/arcgis/arcgis-image-server-source-loader.d.ts.map +1 -0
- package/dist/arcgis/arcgis-image-server-source-loader.js +130 -0
- package/dist/arcgis/arcgis-image-server-source-loader.js.map +1 -0
- package/dist/arcgis/arcgis-image-tile-source-loader-types.d.ts +31 -0
- package/dist/arcgis/arcgis-image-tile-source-loader-types.d.ts.map +1 -0
- package/dist/arcgis/arcgis-image-tile-source-loader-types.js +21 -0
- package/dist/arcgis/arcgis-image-tile-source-loader-types.js.map +1 -0
- package/dist/arcgis/arcgis-image-tile-source-loader.d.ts +63 -0
- package/dist/arcgis/arcgis-image-tile-source-loader.d.ts.map +1 -0
- package/dist/arcgis/arcgis-image-tile-source-loader.js +147 -0
- package/dist/arcgis/arcgis-image-tile-source-loader.js.map +1 -0
- package/dist/arcgis/arcgis-map-tile-source-loader-types.d.ts +31 -0
- package/dist/arcgis/arcgis-map-tile-source-loader-types.d.ts.map +1 -0
- package/dist/arcgis/arcgis-map-tile-source-loader-types.js +21 -0
- package/dist/arcgis/arcgis-map-tile-source-loader-types.js.map +1 -0
- package/dist/arcgis/arcgis-map-tile-source-loader.d.ts +62 -0
- package/dist/arcgis/arcgis-map-tile-source-loader.d.ts.map +1 -0
- package/dist/arcgis/arcgis-map-tile-source-loader.js +196 -0
- package/dist/arcgis/arcgis-map-tile-source-loader.js.map +1 -0
- package/dist/arcgis/arcgis-scene-aggregation.d.ts +34 -0
- package/dist/arcgis/arcgis-scene-aggregation.d.ts.map +1 -0
- package/dist/arcgis/arcgis-scene-aggregation.js +57 -0
- package/dist/arcgis/arcgis-scene-aggregation.js.map +1 -0
- package/dist/arcgis/arcgis-scene-server-source-loader-types.d.ts +31 -0
- package/dist/arcgis/arcgis-scene-server-source-loader-types.d.ts.map +1 -0
- package/dist/arcgis/arcgis-scene-server-source-loader-types.js +21 -0
- package/dist/arcgis/arcgis-scene-server-source-loader-types.js.map +1 -0
- package/dist/arcgis/arcgis-scene-server-source-loader.d.ts +64 -0
- package/dist/arcgis/arcgis-scene-server-source-loader.d.ts.map +1 -0
- package/dist/arcgis/arcgis-scene-server-source-loader.js +159 -0
- package/dist/arcgis/arcgis-scene-server-source-loader.js.map +1 -0
- package/dist/arcgis/arcgis-server.d.ts +15 -0
- package/dist/arcgis/arcgis-server.d.ts.map +1 -0
- package/dist/arcgis/arcgis-server.js +41 -0
- package/dist/arcgis/arcgis-server.js.map +1 -0
- package/dist/arcgis/arcgis-url-utils.d.ts +3 -0
- package/dist/arcgis/arcgis-url-utils.d.ts.map +1 -0
- package/dist/arcgis/arcgis-url-utils.js +23 -0
- package/dist/arcgis/arcgis-url-utils.js.map +1 -0
- package/dist/arcgis/arcgis-vector-tile-server-source-loader-types.d.ts +31 -0
- package/dist/arcgis/arcgis-vector-tile-server-source-loader-types.d.ts.map +1 -0
- package/dist/arcgis/arcgis-vector-tile-server-source-loader-types.js +21 -0
- package/dist/arcgis/arcgis-vector-tile-server-source-loader-types.js.map +1 -0
- package/dist/arcgis/arcgis-vector-tile-server-source-loader.d.ts +66 -0
- package/dist/arcgis/arcgis-vector-tile-server-source-loader.d.ts.map +1 -0
- package/dist/arcgis/arcgis-vector-tile-server-source-loader.js +150 -0
- package/dist/arcgis/arcgis-vector-tile-server-source-loader.js.map +1 -0
- package/dist/arcgis-feature-query-types.d.ts +77 -0
- package/dist/arcgis-feature-query-types.d.ts.map +1 -0
- package/dist/arcgis-feature-query-types.js +5 -0
- package/dist/arcgis-feature-query-types.js.map +1 -0
- package/dist/arcgis-items.d.ts +64 -0
- package/dist/arcgis-items.d.ts.map +1 -0
- package/dist/arcgis-items.js +137 -0
- package/dist/arcgis-items.js.map +1 -0
- package/dist/arcgis-source-types.d.ts +372 -0
- package/dist/arcgis-source-types.d.ts.map +1 -0
- package/dist/arcgis-source-types.js +108 -0
- package/dist/arcgis-source-types.js.map +1 -0
- package/dist/authentication.d.ts +25 -0
- package/dist/authentication.d.ts.map +1 -0
- package/dist/authentication.js +94 -0
- package/dist/authentication.js.map +1 -0
- package/dist/bundled.d.ts +152 -0
- package/dist/bundled.d.ts.map +1 -0
- package/dist/bundled.js +34 -0
- package/dist/bundled.js.map +1 -0
- package/dist/index.cjs +25620 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/service-registry.d.ts +18 -0
- package/dist/service-registry.d.ts.map +1 -0
- package/dist/service-registry.js +29 -0
- package/dist/service-registry.js.map +1 -0
- package/package.json +96 -0
- package/src/arcgis/arcgis-capability-graph.ts +297 -0
- package/src/arcgis/arcgis-feature-query.ts +442 -0
- package/src/arcgis/arcgis-feature-server-source-loader-types.ts +34 -0
- package/src/arcgis/arcgis-feature-server-source-loader.ts +373 -0
- package/src/arcgis/arcgis-image-server-source-loader-types.ts +34 -0
- package/src/arcgis/arcgis-image-server-source-loader.ts +186 -0
- package/src/arcgis/arcgis-image-tile-source-loader-types.ts +34 -0
- package/src/arcgis/arcgis-image-tile-source-loader.ts +192 -0
- package/src/arcgis/arcgis-map-tile-source-loader-types.ts +34 -0
- package/src/arcgis/arcgis-map-tile-source-loader.ts +248 -0
- package/src/arcgis/arcgis-scene-aggregation.ts +90 -0
- package/src/arcgis/arcgis-scene-server-source-loader-types.ts +36 -0
- package/src/arcgis/arcgis-scene-server-source-loader.ts +216 -0
- package/src/arcgis/arcgis-server.ts +61 -0
- package/src/arcgis/arcgis-url-utils.ts +26 -0
- package/src/arcgis/arcgis-vector-tile-server-source-loader-types.ts +36 -0
- package/src/arcgis/arcgis-vector-tile-server-source-loader.ts +196 -0
- package/src/arcgis-feature-query-types.ts +89 -0
- package/src/arcgis-items.ts +233 -0
- package/src/arcgis-source-types.ts +372 -0
- package/src/authentication.ts +118 -0
- package/src/bundled.ts +68 -0
- package/src/index.ts +70 -0
- package/src/service-registry.ts +43 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {Feature, GeoJSONTable} from '@loaders.gl/schema';
|
|
6
|
+
import type {
|
|
7
|
+
ArcGISFeatureExtent,
|
|
8
|
+
ArcGISFeatureObjectIds,
|
|
9
|
+
ArcGISFeatureQueryOptions,
|
|
10
|
+
ArcGISFeatureQueryProgress,
|
|
11
|
+
ArcGISFeatureQueryResult
|
|
12
|
+
} from '../arcgis-feature-query-types';
|
|
13
|
+
import {buildArcGISResourceURL} from './arcgis-url-utils';
|
|
14
|
+
|
|
15
|
+
/** An HTTP or ArcGIS JSON error, without retaining a credential-bearing request URL. */
|
|
16
|
+
export class ArcGISFeatureQueryError extends Error {
|
|
17
|
+
/** ArcGIS error code, or HTTP status when the response has no ArcGIS error. */
|
|
18
|
+
readonly code: number;
|
|
19
|
+
/** Additional service diagnostics. */
|
|
20
|
+
readonly details: string[];
|
|
21
|
+
|
|
22
|
+
/** Creates a structured service error. */
|
|
23
|
+
constructor(message: string, code: number, details: string[] = []) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'ArcGISFeatureQueryError';
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.details = details;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Fetch signature shared by source transport and the query client. */
|
|
32
|
+
type QueryFetch = (url: string, options?: RequestInit) => Promise<Response>;
|
|
33
|
+
|
|
34
|
+
/** Internal query client shared by FeatureServer and MapServer layer endpoints. */
|
|
35
|
+
export class ArcGISFeatureQueryClient {
|
|
36
|
+
/** Creates a query client with one selected layer and normalized filters. */
|
|
37
|
+
constructor(
|
|
38
|
+
/** Layer URL, including caller-supplied authentication parameters. */
|
|
39
|
+
readonly url: string,
|
|
40
|
+
/** Authenticated source transport. */
|
|
41
|
+
readonly fetch: QueryFetch,
|
|
42
|
+
/** Filter parameters shared by counts, IDs and data requests. */
|
|
43
|
+
readonly parameters: Record<string, unknown>,
|
|
44
|
+
/** Signal applied to metadata and every subsequent request. */
|
|
45
|
+
readonly signal?: AbortSignal
|
|
46
|
+
) {}
|
|
47
|
+
|
|
48
|
+
/** Reads JSON while recognizing ArcGIS errors even on HTTP 200 responses. */
|
|
49
|
+
async request(path: string, parameters: Record<string, unknown> = {}): Promise<any> {
|
|
50
|
+
this.signal?.throwIfAborted();
|
|
51
|
+
const requestUrl = new URL(buildArcGISResourceURL(this.url, path, parameters));
|
|
52
|
+
for (const [name, value] of Object.entries(parameters)) {
|
|
53
|
+
if (value === undefined) requestUrl.searchParams.delete(name);
|
|
54
|
+
}
|
|
55
|
+
const requestOptions: RequestInit = {signal: this.signal};
|
|
56
|
+
// Query is read-only and supports POST. Keep endpoint credentials in the URL so the shared
|
|
57
|
+
// credential transport can still honor explicit tokens and apply its origin restrictions.
|
|
58
|
+
if (path === 'query' && requestUrl.toString().length > 2000) {
|
|
59
|
+
const body = new URLSearchParams();
|
|
60
|
+
for (const name of Object.keys(parameters)) {
|
|
61
|
+
const value = requestUrl.searchParams.get(name);
|
|
62
|
+
if (value !== null) {
|
|
63
|
+
body.set(name, value);
|
|
64
|
+
requestUrl.searchParams.delete(name);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
requestOptions.method = 'POST';
|
|
68
|
+
requestOptions.headers = {'Content-Type': 'application/x-www-form-urlencoded'};
|
|
69
|
+
requestOptions.body = body;
|
|
70
|
+
}
|
|
71
|
+
const response = await this.fetch(requestUrl.toString(), requestOptions);
|
|
72
|
+
let json: any;
|
|
73
|
+
try {
|
|
74
|
+
json = await response.json();
|
|
75
|
+
} catch {
|
|
76
|
+
this.signal?.throwIfAborted();
|
|
77
|
+
throw new ArcGISFeatureQueryError('ArcGIS returned a non-JSON response', response.status);
|
|
78
|
+
}
|
|
79
|
+
this.signal?.throwIfAborted();
|
|
80
|
+
if (!response.ok || json?.error) {
|
|
81
|
+
throw new ArcGISFeatureQueryError(
|
|
82
|
+
json?.error?.message || response.statusText || 'ArcGIS query failed',
|
|
83
|
+
json?.error?.code || response.status,
|
|
84
|
+
json?.error?.details || []
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return json;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Returns the count for the same filters used by data retrieval. */
|
|
91
|
+
async getCount(): Promise<number> {
|
|
92
|
+
const json = await this.request('query', this.getSummaryParameters({returnCountOnly: true}));
|
|
93
|
+
if (!Number.isSafeInteger(json.count) || json.count < 0) {
|
|
94
|
+
throw new Error('ArcGIS query did not return a safe nonnegative count');
|
|
95
|
+
}
|
|
96
|
+
return json.count;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Returns unique object IDs and preserves explicit truncation information. */
|
|
100
|
+
async getObjectIds(): Promise<ArcGISFeatureObjectIds> {
|
|
101
|
+
const json = await this.request('query', this.getSummaryParameters({returnIdsOnly: true}));
|
|
102
|
+
if (!Array.isArray(json.objectIds) || typeof json.objectIdFieldName !== 'string') {
|
|
103
|
+
throw new Error('ArcGIS query did not return object IDs and their field name');
|
|
104
|
+
}
|
|
105
|
+
const objectIds = json.objectIds.map(validateObjectId);
|
|
106
|
+
return {
|
|
107
|
+
objectIdFieldName: json.objectIdFieldName,
|
|
108
|
+
objectIds: [...new Set<number | string>(objectIds)],
|
|
109
|
+
exceededTransferLimit: json.exceededTransferLimit === true
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Requests an extent while retaining its spatial reference. */
|
|
114
|
+
async getExtent(): Promise<ArcGISFeatureExtent> {
|
|
115
|
+
const json = await this.request('query', this.getSummaryParameters({returnExtentOnly: true}));
|
|
116
|
+
const extent = json.extent;
|
|
117
|
+
if (
|
|
118
|
+
extent !== null &&
|
|
119
|
+
(!extent?.spatialReference ||
|
|
120
|
+
![extent.xmin, extent.ymin, extent.xmax, extent.ymax].every(Number.isFinite))
|
|
121
|
+
) {
|
|
122
|
+
throw new Error('ArcGIS query did not return an extent with a spatial reference');
|
|
123
|
+
}
|
|
124
|
+
return {extent, ...(json.count === undefined ? {} : {count: json.count})};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Reads layer metadata and rejects response formats this client cannot safely interpret. */
|
|
128
|
+
async getMetadata(): Promise<any> {
|
|
129
|
+
const metadata = await this.request('', {f: 'json'});
|
|
130
|
+
if (
|
|
131
|
+
metadata.capabilities &&
|
|
132
|
+
!metadata.capabilities
|
|
133
|
+
.split(',')
|
|
134
|
+
.some((capability: string) => capability.trim().toLowerCase() === 'query')
|
|
135
|
+
)
|
|
136
|
+
throw new Error('This ArcGIS layer does not advertise Query capability');
|
|
137
|
+
if (
|
|
138
|
+
metadata.type !== 'Table' &&
|
|
139
|
+
metadata.supportedQueryFormats &&
|
|
140
|
+
!metadata.supportedQueryFormats
|
|
141
|
+
.toLowerCase()
|
|
142
|
+
.split(',')
|
|
143
|
+
.map((format: string) => format.trim())
|
|
144
|
+
.includes('geojson')
|
|
145
|
+
) {
|
|
146
|
+
throw new Error('This spatial layer does not support GeoJSON queries');
|
|
147
|
+
}
|
|
148
|
+
return metadata;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Reads one page without implying that it represents a complete query. */
|
|
152
|
+
async getPage(options: ArcGISFeatureQueryOptions): Promise<ArcGISFeatureQueryResult> {
|
|
153
|
+
const metadata = await this.getMetadata();
|
|
154
|
+
const pageSize = getPageSize(options, metadata);
|
|
155
|
+
const offset = this.parameters.resultOffset ?? 0;
|
|
156
|
+
if (!Number.isSafeInteger(offset) || Number(offset) < 0)
|
|
157
|
+
throw new Error('resultOffset must be a nonnegative safe integer');
|
|
158
|
+
const page = await this.readPage(metadata, {
|
|
159
|
+
resultRecordCount: Math.min(
|
|
160
|
+
pageSize,
|
|
161
|
+
positiveInteger(Number(this.parameters.resultRecordCount ?? pageSize), 'resultRecordCount')
|
|
162
|
+
)
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
data: page.data,
|
|
166
|
+
loaded: page.data.features.length,
|
|
167
|
+
pages: 1,
|
|
168
|
+
duplicates: 0,
|
|
169
|
+
complete: false,
|
|
170
|
+
scope: options.boundingBox ? 'viewport' : 'query',
|
|
171
|
+
strategy: 'page',
|
|
172
|
+
reason: 'single-page'
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Streams bounded pages and assesses completeness using the initial count and stable IDs. */
|
|
177
|
+
async *getPages(options: ArcGISFeatureQueryOptions): AsyncGenerator<ArcGISFeatureQueryResult> {
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const abort = () => controller.abort(this.signal?.reason);
|
|
180
|
+
this.signal?.throwIfAborted();
|
|
181
|
+
this.signal?.addEventListener('abort', abort, {once: true});
|
|
182
|
+
const client = new ArcGISFeatureQueryClient(
|
|
183
|
+
this.url,
|
|
184
|
+
this.fetch,
|
|
185
|
+
this.parameters,
|
|
186
|
+
controller.signal
|
|
187
|
+
);
|
|
188
|
+
try {
|
|
189
|
+
const metadata = await client.getMetadata();
|
|
190
|
+
const pageSize = getPageSize(options, metadata);
|
|
191
|
+
const maximumFeatures = positiveInteger(options.maxFeatures ?? 100000, 'maxFeatures');
|
|
192
|
+
const concurrency = positiveInteger(options.concurrency ?? 4, 'concurrency');
|
|
193
|
+
if (concurrency > 8) throw new Error('concurrency must not exceed 8');
|
|
194
|
+
if (
|
|
195
|
+
this.parameters.resultOffset ||
|
|
196
|
+
this.parameters.resultRecordCount ||
|
|
197
|
+
this.parameters.orderByFields
|
|
198
|
+
) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
'Complete queries manage paging and ordering; use queryFeaturePage() for explicit pages'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const objectIdField =
|
|
204
|
+
metadata.objectIdField ||
|
|
205
|
+
metadata.objectIdFieldName ||
|
|
206
|
+
metadata.fields?.find((field: any) => field.type === 'esriFieldTypeOID')?.name;
|
|
207
|
+
if (!objectIdField) throw new Error('Complete queries require a stable object ID field');
|
|
208
|
+
const supportsOffset =
|
|
209
|
+
metadata.advancedQueryCapabilities?.supportsPagination === true &&
|
|
210
|
+
metadata.advancedQueryCapabilities?.supportsOrderBy === true;
|
|
211
|
+
const strategy =
|
|
212
|
+
options.strategy === 'auto' || !options.strategy
|
|
213
|
+
? supportsOffset
|
|
214
|
+
? 'offset'
|
|
215
|
+
: 'object-ids'
|
|
216
|
+
: options.strategy;
|
|
217
|
+
if (strategy === 'offset' && !supportsOffset) {
|
|
218
|
+
throw new Error('This ArcGIS layer does not advertise ordered pagination; use object-ids');
|
|
219
|
+
}
|
|
220
|
+
const expectedCount = await client.getCount();
|
|
221
|
+
const progress: ArcGISFeatureQueryProgress = {
|
|
222
|
+
loaded: 0,
|
|
223
|
+
expectedCount,
|
|
224
|
+
pages: 0,
|
|
225
|
+
duplicates: 0,
|
|
226
|
+
complete: false,
|
|
227
|
+
scope: options.boundingBox ? 'viewport' : 'query',
|
|
228
|
+
strategy,
|
|
229
|
+
reason: 'loading'
|
|
230
|
+
};
|
|
231
|
+
const seen = new Set<number | string>();
|
|
232
|
+
/** Snapshots progress so previously yielded pages never mutate. */
|
|
233
|
+
const result = (features: Feature[]): ArcGISFeatureQueryResult => {
|
|
234
|
+
progress.loaded = seen.size;
|
|
235
|
+
options.onProgress?.({...progress});
|
|
236
|
+
controller.signal.throwIfAborted();
|
|
237
|
+
return {...progress, data: makeTable(features)};
|
|
238
|
+
};
|
|
239
|
+
/** Retains each safe identity once and applies the application memory bound. */
|
|
240
|
+
const retain = (features: Feature[]): Feature[] => {
|
|
241
|
+
const retained: Feature[] = [];
|
|
242
|
+
for (const feature of features) {
|
|
243
|
+
const identifier = validateObjectId(feature.properties?.[objectIdField] ?? feature.id);
|
|
244
|
+
if (seen.has(identifier)) {
|
|
245
|
+
progress.duplicates++;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (seen.size >= maximumFeatures) break;
|
|
249
|
+
seen.add(identifier);
|
|
250
|
+
retained.push({...feature, id: identifier});
|
|
251
|
+
}
|
|
252
|
+
return retained;
|
|
253
|
+
};
|
|
254
|
+
const fields = this.parameters.outFields;
|
|
255
|
+
const outFields =
|
|
256
|
+
fields === '*' || !fields
|
|
257
|
+
? '*'
|
|
258
|
+
: [
|
|
259
|
+
...new Set([
|
|
260
|
+
...(Array.isArray(fields) ? fields : String(fields).split(',')),
|
|
261
|
+
objectIdField
|
|
262
|
+
])
|
|
263
|
+
];
|
|
264
|
+
if (strategy === 'offset') {
|
|
265
|
+
let offset = 0;
|
|
266
|
+
// Bound requests even when a service repeats pages or emits empty transfer-limited pages.
|
|
267
|
+
while (true) {
|
|
268
|
+
const requestCount = Math.min(pageSize, maximumFeatures - seen.size);
|
|
269
|
+
const page = await client.readPage(metadata, {
|
|
270
|
+
outFields,
|
|
271
|
+
orderByFields: `${objectIdField} ASC`,
|
|
272
|
+
resultOffset: offset,
|
|
273
|
+
resultRecordCount: requestCount
|
|
274
|
+
});
|
|
275
|
+
progress.pages++;
|
|
276
|
+
const retained = retain(page.data.features);
|
|
277
|
+
offset += page.data.features.length;
|
|
278
|
+
const ended =
|
|
279
|
+
page.exceededTransferLimit === false ||
|
|
280
|
+
(page.exceededTransferLimit !== true &&
|
|
281
|
+
(page.data.features.length < requestCount || seen.size === expectedCount));
|
|
282
|
+
if (ended) {
|
|
283
|
+
progress.complete = seen.size === expectedCount;
|
|
284
|
+
progress.reason = progress.complete ? 'complete' : 'count-mismatch';
|
|
285
|
+
} else if (seen.size >= maximumFeatures) progress.reason = 'feature-limit';
|
|
286
|
+
else if (!retained.length) progress.reason = 'no-progress';
|
|
287
|
+
yield result(retained);
|
|
288
|
+
if (progress.reason !== 'loading') return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const identifiers = await client.getObjectIds();
|
|
292
|
+
if (identifiers.objectIdFieldName !== objectIdField) {
|
|
293
|
+
throw new Error('ArcGIS changed the object ID field during the query');
|
|
294
|
+
}
|
|
295
|
+
const identifiersComplete =
|
|
296
|
+
!identifiers.exceededTransferLimit && identifiers.objectIds.length === expectedCount;
|
|
297
|
+
const selectedIds = identifiers.objectIds.slice(0, maximumFeatures);
|
|
298
|
+
let missingRecords = false;
|
|
299
|
+
for (let offset = 0; offset < selectedIds.length; offset += pageSize * concurrency) {
|
|
300
|
+
const batches: (number | string)[][] = [];
|
|
301
|
+
for (
|
|
302
|
+
let index = offset;
|
|
303
|
+
index < Math.min(offset + pageSize * concurrency, selectedIds.length);
|
|
304
|
+
index += pageSize
|
|
305
|
+
) {
|
|
306
|
+
batches.push(selectedIds.slice(index, index + pageSize));
|
|
307
|
+
}
|
|
308
|
+
const pages = await Promise.all(
|
|
309
|
+
batches.map(async objectIds => {
|
|
310
|
+
const page = await client.readPage(metadata, {outFields, objectIds});
|
|
311
|
+
const returnedIds = new Set(
|
|
312
|
+
page.data.features.map(feature =>
|
|
313
|
+
validateObjectId(feature.properties?.[objectIdField] ?? feature.id)
|
|
314
|
+
)
|
|
315
|
+
);
|
|
316
|
+
if (
|
|
317
|
+
page.exceededTransferLimit === true ||
|
|
318
|
+
objectIds.some(identifier => !returnedIds.has(identifier)) ||
|
|
319
|
+
[...returnedIds].some(identifier => !objectIds.includes(identifier))
|
|
320
|
+
)
|
|
321
|
+
missingRecords = true;
|
|
322
|
+
return page;
|
|
323
|
+
})
|
|
324
|
+
);
|
|
325
|
+
for (let index = 0; index < pages.length; index++) {
|
|
326
|
+
const retained = retain(pages[index].data.features);
|
|
327
|
+
progress.pages++;
|
|
328
|
+
if (offset + (index + 1) * pageSize >= selectedIds.length) {
|
|
329
|
+
progress.complete =
|
|
330
|
+
identifiersComplete && !missingRecords && seen.size === expectedCount;
|
|
331
|
+
progress.reason = progress.complete
|
|
332
|
+
? 'complete'
|
|
333
|
+
: !identifiersComplete
|
|
334
|
+
? 'id-limit'
|
|
335
|
+
: selectedIds.length < expectedCount
|
|
336
|
+
? 'feature-limit'
|
|
337
|
+
: 'count-mismatch';
|
|
338
|
+
}
|
|
339
|
+
yield result(retained);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!selectedIds.length) {
|
|
343
|
+
progress.complete = identifiersComplete;
|
|
344
|
+
progress.reason = identifiersComplete ? 'complete' : 'id-limit';
|
|
345
|
+
yield result([]);
|
|
346
|
+
}
|
|
347
|
+
} finally {
|
|
348
|
+
controller.abort();
|
|
349
|
+
this.signal?.removeEventListener('abort', abort);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Removes data-only controls from a count, IDs or extent request. */
|
|
354
|
+
private getSummaryParameters(operation: Record<string, unknown>): Record<string, unknown> {
|
|
355
|
+
return {
|
|
356
|
+
...this.parameters,
|
|
357
|
+
outFields: undefined,
|
|
358
|
+
orderByFields: undefined,
|
|
359
|
+
resultOffset: undefined,
|
|
360
|
+
resultRecordCount: undefined,
|
|
361
|
+
f: 'json',
|
|
362
|
+
returnGeometry: false,
|
|
363
|
+
returnCountOnly: false,
|
|
364
|
+
returnIdsOnly: false,
|
|
365
|
+
returnExtentOnly: false,
|
|
366
|
+
...operation
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Normalizes GeoJSON or nonspatial ArcGIS records without discarding null attributes. */
|
|
371
|
+
private async readPage(
|
|
372
|
+
metadata: any,
|
|
373
|
+
parameters: Record<string, unknown>
|
|
374
|
+
): Promise<{
|
|
375
|
+
/** Normalized records. */
|
|
376
|
+
data: GeoJSONTable;
|
|
377
|
+
/** Explicit server indication of remaining records, when present. */
|
|
378
|
+
exceededTransferLimit?: boolean;
|
|
379
|
+
}> {
|
|
380
|
+
const isTable = metadata.type === 'Table';
|
|
381
|
+
if (!isTable && String(this.parameters.outSR ?? 4326) !== '4326') {
|
|
382
|
+
throw new Error(
|
|
383
|
+
'ArcGIS GeoJSON output requires EPSG:4326; use requestCrs for projected query bounds'
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
const json = await this.request('query', {
|
|
387
|
+
...this.parameters,
|
|
388
|
+
...parameters,
|
|
389
|
+
returnCountOnly: false,
|
|
390
|
+
returnIdsOnly: false,
|
|
391
|
+
returnExtentOnly: false,
|
|
392
|
+
f: isTable ? 'json' : 'geojson'
|
|
393
|
+
});
|
|
394
|
+
let features: Feature[];
|
|
395
|
+
if (isTable && Array.isArray(json.features)) {
|
|
396
|
+
features = json.features.map((record: any) => {
|
|
397
|
+
if (!record.attributes || record.geometry)
|
|
398
|
+
throw new Error('Invalid ArcGIS nonspatial table record');
|
|
399
|
+
return {type: 'Feature', geometry: null, properties: record.attributes};
|
|
400
|
+
});
|
|
401
|
+
} else if (json.type === 'FeatureCollection' && Array.isArray(json.features)) {
|
|
402
|
+
features = json.features;
|
|
403
|
+
if (features.some(feature => feature.type !== 'Feature' || feature.geometry === undefined)) {
|
|
404
|
+
throw new Error('Invalid ArcGIS GeoJSON feature');
|
|
405
|
+
}
|
|
406
|
+
} else throw new Error('ArcGIS query did not return a GeoJSON FeatureCollection');
|
|
407
|
+
return {
|
|
408
|
+
data: makeTable(features),
|
|
409
|
+
exceededTransferLimit: json.exceededTransferLimit ?? json.properties?.exceededTransferLimit
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Makes the normalized table shared by spatial and nonspatial query results. */
|
|
415
|
+
function makeTable(features: Feature[]): GeoJSONTable {
|
|
416
|
+
return {shape: 'geojson-table', type: 'FeatureCollection', features};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Rejects missing or lossy numeric identities before deduplication. */
|
|
420
|
+
function validateObjectId(identifier: unknown): number | string {
|
|
421
|
+
if (
|
|
422
|
+
(typeof identifier === 'number' && Number.isSafeInteger(identifier)) ||
|
|
423
|
+
(typeof identifier === 'string' && identifier.length > 0)
|
|
424
|
+
)
|
|
425
|
+
return identifier;
|
|
426
|
+
throw new Error('ArcGIS query requires nonempty string or safe-integer object IDs');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Validates finite positive retrieval bounds. */
|
|
430
|
+
function positiveInteger(value: number, name: string): number {
|
|
431
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
432
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
433
|
+
return value;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Caps requests by the service limit without trusting invalid metadata. */
|
|
437
|
+
function getPageSize(options: ArcGISFeatureQueryOptions, metadata: any): number {
|
|
438
|
+
return Math.min(
|
|
439
|
+
positiveInteger(options.pageSize ?? 1000, 'pageSize'),
|
|
440
|
+
positiveInteger(metadata.maxRecordCount ?? 1000, 'maxRecordCount')
|
|
441
|
+
);
|
|
442
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {ARCGIS_FEATURE_SERVER_SOURCE_LOADER_METADATA} from '../arcgis-source-types';
|
|
6
|
+
import type {CoreAPI, SourceLoader} from '@loaders.gl/loader-utils';
|
|
7
|
+
import type {ArcGISVectorSource} from './arcgis-feature-server-source-loader';
|
|
8
|
+
import type {ArcGISFeatureServerSourceLoaderOptions} from '../arcgis-source-types';
|
|
9
|
+
|
|
10
|
+
/** Loads the ArcGISFeatureServerSourceLoader implementation on first asynchronous use. */
|
|
11
|
+
async function preloadArcGISFeatureServerSourceLoader(): Promise<SourceLoader<ArcGISVectorSource>> {
|
|
12
|
+
const {ArcGISFeatureServerSourceLoaderWithParser} = await import(
|
|
13
|
+
'@loaders.gl/arcgis/arcgis-feature-server-source-loader'
|
|
14
|
+
);
|
|
15
|
+
return ArcGISFeatureServerSourceLoaderWithParser;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Lightweight service descriptor; use load() or the synchronous /bundled entrypoint. */
|
|
19
|
+
export const ArcGISFeatureServerSourceLoader = {
|
|
20
|
+
...ARCGIS_FEATURE_SERVER_SOURCE_LOADER_METADATA,
|
|
21
|
+
dataType: null as unknown as ArcGISVectorSource,
|
|
22
|
+
batchType: null as never,
|
|
23
|
+
preload: preloadArcGISFeatureServerSourceLoader,
|
|
24
|
+
/** Requires the runtime loader when constructing a source synchronously. */
|
|
25
|
+
createDataSource(
|
|
26
|
+
_url: string,
|
|
27
|
+
_options: ArcGISFeatureServerSourceLoaderOptions,
|
|
28
|
+
_coreApi?: CoreAPI
|
|
29
|
+
): ArcGISVectorSource {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'ArcGISFeatureServerSourceLoader requires async load() or an @loaders.gl/arcgis/bundled import'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
} as const satisfies SourceLoader<ArcGISVectorSource>;
|