@loaders.gl/services 5.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +52 -0
  2. package/README.md +51 -0
  3. package/dist/arcgis/arcgis-capability-graph.d.ts +48 -0
  4. package/dist/arcgis/arcgis-capability-graph.d.ts.map +1 -0
  5. package/dist/arcgis/arcgis-capability-graph.js +220 -0
  6. package/dist/arcgis/arcgis-capability-graph.js.map +1 -0
  7. package/dist/arcgis/arcgis-feature-server-source-loader.d.ts +94 -0
  8. package/dist/arcgis/arcgis-feature-server-source-loader.d.ts.map +1 -0
  9. package/dist/arcgis/arcgis-feature-server-source-loader.js +228 -0
  10. package/dist/arcgis/arcgis-feature-server-source-loader.js.map +1 -0
  11. package/dist/arcgis/arcgis-image-server-source-loader.d.ts +96 -0
  12. package/dist/arcgis/arcgis-image-server-source-loader.d.ts.map +1 -0
  13. package/dist/arcgis/arcgis-image-server-source-loader.js +143 -0
  14. package/dist/arcgis/arcgis-image-server-source-loader.js.map +1 -0
  15. package/dist/arcgis/arcgis-image-tile-source-loader.d.ts +71 -0
  16. package/dist/arcgis/arcgis-image-tile-source-loader.d.ts.map +1 -0
  17. package/dist/arcgis/arcgis-image-tile-source-loader.js +153 -0
  18. package/dist/arcgis/arcgis-image-tile-source-loader.js.map +1 -0
  19. package/dist/arcgis/arcgis-map-tile-source-loader.d.ts +106 -0
  20. package/dist/arcgis/arcgis-map-tile-source-loader.d.ts.map +1 -0
  21. package/dist/arcgis/arcgis-map-tile-source-loader.js +202 -0
  22. package/dist/arcgis/arcgis-map-tile-source-loader.js.map +1 -0
  23. package/dist/arcgis/arcgis-scene-aggregation.d.ts +34 -0
  24. package/dist/arcgis/arcgis-scene-aggregation.d.ts.map +1 -0
  25. package/dist/arcgis/arcgis-scene-aggregation.js +57 -0
  26. package/dist/arcgis/arcgis-scene-aggregation.js.map +1 -0
  27. package/dist/arcgis/arcgis-scene-server-source-loader.d.ts +112 -0
  28. package/dist/arcgis/arcgis-scene-server-source-loader.d.ts.map +1 -0
  29. package/dist/arcgis/arcgis-scene-server-source-loader.js +157 -0
  30. package/dist/arcgis/arcgis-scene-server-source-loader.js.map +1 -0
  31. package/dist/arcgis/arcgis-server.d.ts +15 -0
  32. package/dist/arcgis/arcgis-server.d.ts.map +1 -0
  33. package/dist/arcgis/arcgis-server.js +41 -0
  34. package/dist/arcgis/arcgis-server.js.map +1 -0
  35. package/dist/arcgis/arcgis-url-utils.d.ts +3 -0
  36. package/dist/arcgis/arcgis-url-utils.d.ts.map +1 -0
  37. package/dist/arcgis/arcgis-url-utils.js +23 -0
  38. package/dist/arcgis/arcgis-url-utils.js.map +1 -0
  39. package/dist/arcgis/arcgis-vector-tile-server-source-loader.d.ts +115 -0
  40. package/dist/arcgis/arcgis-vector-tile-server-source-loader.d.ts.map +1 -0
  41. package/dist/arcgis/arcgis-vector-tile-server-source-loader.js +156 -0
  42. package/dist/arcgis/arcgis-vector-tile-server-source-loader.js.map +1 -0
  43. package/dist/authentication.d.ts +37 -0
  44. package/dist/authentication.d.ts.map +1 -0
  45. package/dist/authentication.js +41 -0
  46. package/dist/authentication.js.map +1 -0
  47. package/dist/bundled.d.ts +2 -0
  48. package/dist/bundled.d.ts.map +1 -0
  49. package/dist/bundled.js +6 -0
  50. package/dist/bundled.js.map +1 -0
  51. package/dist/index.cjs +21614 -0
  52. package/dist/index.cjs.map +6 -0
  53. package/dist/index.d.ts +32 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +24 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/service-registry.d.ts +18 -0
  58. package/dist/service-registry.d.ts.map +1 -0
  59. package/dist/service-registry.js +29 -0
  60. package/dist/service-registry.js.map +1 -0
  61. package/dist/unbundled.d.ts +2 -0
  62. package/dist/unbundled.d.ts.map +1 -0
  63. package/dist/unbundled.js +6 -0
  64. package/dist/unbundled.js.map +1 -0
  65. package/package.json +60 -0
  66. package/src/arcgis/arcgis-capability-graph.ts +295 -0
  67. package/src/arcgis/arcgis-feature-server-source-loader.ts +342 -0
  68. package/src/arcgis/arcgis-image-server-source-loader.ts +241 -0
  69. package/src/arcgis/arcgis-image-tile-source-loader.ts +206 -0
  70. package/src/arcgis/arcgis-map-tile-source-loader.ts +286 -0
  71. package/src/arcgis/arcgis-scene-aggregation.ts +90 -0
  72. package/src/arcgis/arcgis-scene-server-source-loader.ts +258 -0
  73. package/src/arcgis/arcgis-server.ts +61 -0
  74. package/src/arcgis/arcgis-url-utils.ts +26 -0
  75. package/src/arcgis/arcgis-vector-tile-server-source-loader.ts +233 -0
  76. package/src/authentication.ts +83 -0
  77. package/src/bundled.ts +6 -0
  78. package/src/index.ts +99 -0
  79. package/src/service-registry.ts +43 -0
  80. package/src/unbundled.ts +6 -0
@@ -0,0 +1,342 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {DataType, Schema, GeoJSONTable} from '@loaders.gl/schema';
6
+ import {
7
+ convertFeaturesToWKBArrowTable,
8
+ convertGeojsonToBinaryFeatureCollection
9
+ } from '@loaders.gl/gis';
10
+ import type {
11
+ CoreAPI,
12
+ DataSourceOptions,
13
+ VectorSourceMetadata,
14
+ GetFeaturesParameters,
15
+ VectorSource,
16
+ VectorSourceData
17
+ } from '@loaders.gl/loader-utils';
18
+ import type {SourceLoader} from '@loaders.gl/loader-utils';
19
+ import {DataSource} from '@loaders.gl/loader-utils';
20
+ import {buildArcGISResourceURL} from './arcgis-url-utils';
21
+
22
+ /** Parameters for ArcGIS FeatureServer query requests. */
23
+ export type ArcGISFeatureServiceQueryOptions = {
24
+ /** Include feature geometries in the response. */
25
+ returnGeometry?: boolean;
26
+ /** SQL where clause. */
27
+ where?: string;
28
+ /** Output spatial reference. */
29
+ outSR?: string | number;
30
+ /** Output fields. */
31
+ outFields?: string | string[];
32
+ /** Input spatial reference for supplied geometry. */
33
+ inSR?: string | number;
34
+ /** Filter geometry as an ArcGIS REST geometry string. */
35
+ geometry?: string;
36
+ /** Filter geometry type. */
37
+ geometryType?:
38
+ | 'esriGeometryEnvelope'
39
+ | 'esriGeometryPoint'
40
+ | 'esriGeometryPolyline'
41
+ | 'esriGeometryPolygon';
42
+ /** Spatial relationship for geometry filters. */
43
+ spatialRel?:
44
+ | 'esriSpatialRelIntersects'
45
+ | 'esriSpatialRelContains'
46
+ | 'esriSpatialRelCrosses'
47
+ | 'esriSpatialRelEnvelopeIntersects'
48
+ | 'esriSpatialRelIndexIntersects'
49
+ | 'esriSpatialRelOverlaps'
50
+ | 'esriSpatialRelTouches'
51
+ | 'esriSpatialRelWithin';
52
+ /** Geometry precision. */
53
+ geometryPrecision?: number;
54
+ /** Query result type. */
55
+ resultType?: 'none' | 'standard' | 'tile';
56
+ /** ArcGIS response format. */
57
+ f?: 'geojson' | 'json' | 'pjson';
58
+ };
59
+
60
+ /** Options for the ArcGIS FeatureServer source. */
61
+ export type ArcGISFeatureServerSourceLoaderOptions = DataSourceOptions & {
62
+ 'arcgis-feature-server'?: {
63
+ /** Default ArcGIS query request parameters. */
64
+ queryParameters?: Partial<ArcGISFeatureServiceQueryOptions>;
65
+ };
66
+ };
67
+
68
+ /**
69
+ * @see https://developers.arcgis.com/rest/services-reference/enterprise/feature-service.htm
70
+ */
71
+ export const ArcGISFeatureServerSourceLoader = {
72
+ dataType: null as unknown as ArcGISVectorSource,
73
+ batchType: null as never,
74
+ name: 'ArcGISFeatureServer',
75
+ id: 'arcgis-feature-server',
76
+ module: 'services',
77
+ version: '0.0.0',
78
+ extensions: [],
79
+ mimeTypes: [],
80
+ type: 'arcgis-feature-server',
81
+ fromUrl: true,
82
+ fromBlob: false,
83
+
84
+ options: {
85
+ url: undefined!,
86
+ 'arcgis-feature-server': {}
87
+ },
88
+
89
+ defaultOptions: {
90
+ url: undefined!,
91
+ 'arcgis-feature-server': {}
92
+ },
93
+
94
+ testURL: (url: string): boolean => url.toLowerCase().includes('featureserver'),
95
+ createDataSource: (
96
+ url: string,
97
+ options: ArcGISFeatureServerSourceLoaderOptions,
98
+ coreApi?: CoreAPI
99
+ ): ArcGISVectorSource => new ArcGISVectorSource(url, options, coreApi)
100
+ } as const satisfies SourceLoader<ArcGISVectorSource>;
101
+
102
+ /**
103
+ * ArcGIS FeatureServer
104
+ * Note - exports a big API, that could be exposed here if there is a use case
105
+ * @see https://developers.arcgis.com/rest/services-reference/enterprise/feature-service.htm
106
+ */
107
+ export class ArcGISVectorSource
108
+ extends DataSource<string, ArcGISFeatureServerSourceLoaderOptions>
109
+ implements VectorSource
110
+ {
111
+ /** Cached ArcGIS FeatureServer metadata request. */
112
+ protected formatSpecificMetadata: Promise<any> | null = null;
113
+
114
+ constructor(url: string, options: ArcGISFeatureServerSourceLoaderOptions, coreApi?: CoreAPI) {
115
+ super(url, options, ArcGISFeatureServerSourceLoader.defaultOptions, coreApi);
116
+ }
117
+
118
+ /** Returns a schema inferred from ArcGIS FeatureServer metadata fields. */
119
+ async getSchema(): Promise<Schema> {
120
+ const metadata = await this.getFormatSpecificMetadata();
121
+ return parseArcGISFeatureServerSchema(metadata);
122
+ }
123
+
124
+ /** Returns normalized VectorSource metadata. */
125
+ async getMetadata(
126
+ options: {formatSpecificMetadata?: boolean} = {}
127
+ ): Promise<VectorSourceMetadata> {
128
+ // Wait for raw metadata to load
129
+ const formatSpecificMetadata = await this.getFormatSpecificMetadata();
130
+
131
+ const metadata = parseArcGISFeatureServerMetadata(formatSpecificMetadata);
132
+
133
+ // Only add the big blob of source metadata if explicitly requested
134
+ if (options.formatSpecificMetadata) {
135
+ metadata.formatSpecificMetadata = formatSpecificMetadata;
136
+ }
137
+ return metadata;
138
+ }
139
+
140
+ /** Requests features from the ArcGIS FeatureServer query endpoint. */
141
+ async getFeatures(parameters: GetFeaturesParameters): Promise<VectorSourceData> {
142
+ const url = this.getFeaturesURL(parameters);
143
+ const response = await this.fetch(
144
+ url,
145
+ parameters.signal ? {signal: parameters.signal} : undefined
146
+ );
147
+ await this.checkResponse(response);
148
+ const geoJsonTable = parseGeoJSONTable(await response.json());
149
+ const format = parameters.format || 'arrow';
150
+
151
+ switch (format) {
152
+ case 'binary':
153
+ return convertGeojsonToBinaryFeatureCollection(geoJsonTable.features);
154
+ case 'geojson':
155
+ return geoJsonTable;
156
+ case 'arrow':
157
+ default:
158
+ return convertFeaturesToWKBArrowTable(geoJsonTable.features);
159
+ }
160
+ }
161
+
162
+ /** Requests the raw ArcGIS FeatureServer metadata document. */
163
+ protected async _getFormatSpecificMetadata() {
164
+ // PJSON is formatted by a bit slower than JSON
165
+ const url = this.metadataURL();
166
+ const response = await this.fetch(url);
167
+ await this.checkResponse(response);
168
+ return await response.json();
169
+ }
170
+
171
+ /** Loads and caches the raw ArcGIS FeatureServer metadata. */
172
+ protected async getFormatSpecificMetadata(): Promise<any> {
173
+ this.formatSpecificMetadata ||= this._getFormatSpecificMetadata();
174
+ return await this.formatSpecificMetadata;
175
+ }
176
+
177
+ /** Builds a metadata URL for the ArcGIS FeatureServer endpoint. */
178
+ metadataURL(options?: {parameters?: Record<string, unknown>}): string {
179
+ return this.getUrl('', {f: 'pjson', ...options?.parameters});
180
+ }
181
+
182
+ /** Builds a query URL from generic vector source parameters. */
183
+ getFeaturesURL(parameters: GetFeaturesParameters): string {
184
+ const defaultParameters = this.options['arcgis-feature-server']?.queryParameters || {};
185
+ const spatialReference = normalizeArcGISSpatialReference(parameters.crs) || 4326;
186
+ const queryParameters: ArcGISFeatureServiceQueryOptions = {
187
+ returnGeometry: true,
188
+ where: '1=1',
189
+ outFields: '*',
190
+ outSR: spatialReference,
191
+ inSR: spatialReference,
192
+ f: 'geojson',
193
+ ...defaultParameters
194
+ };
195
+
196
+ if (parameters.boundingBox) {
197
+ queryParameters.geometry = [
198
+ parameters.boundingBox[0][0],
199
+ parameters.boundingBox[0][1],
200
+ parameters.boundingBox[1][0],
201
+ parameters.boundingBox[1][1]
202
+ ].join(',');
203
+ queryParameters.geometryType = 'esriGeometryEnvelope';
204
+ queryParameters.spatialRel ||= 'esriSpatialRelIntersects';
205
+ }
206
+
207
+ const layer = Array.isArray(parameters.layers) ? parameters.layers[0] : parameters.layers;
208
+ const isLayerEndpoint = /\/FeatureServer\/\d+\/?(?:\?|$)/i.test(this.url);
209
+ const resourcePath = isLayerEndpoint || !layer ? 'query' : `${layer}/query`;
210
+ return this.getUrl(resourcePath, queryParameters);
211
+ }
212
+
213
+ /** Builds an ArcGIS FeatureServer URL. */
214
+ protected getUrl(
215
+ path: string,
216
+ options: Record<string, unknown>,
217
+ extra?: Record<string, unknown>
218
+ ): string {
219
+ return buildArcGISResourceURL(this.url, path, {...options, ...extra});
220
+ }
221
+
222
+ /** Checks an ArcGIS FeatureServer response. */
223
+ protected async checkResponse(response: Response): Promise<void> {
224
+ if (!response.ok) {
225
+ throw new Error(
226
+ response.statusText || `ArcGIS FeatureServer request failed: ${response.status}`
227
+ );
228
+ }
229
+ }
230
+ }
231
+
232
+ function parseArcGISFeatureServerMetadata(json: any): VectorSourceMetadata {
233
+ const layers: VectorSourceMetadata['layers'] = [];
234
+ for (const layer of json.layers || []) {
235
+ const extent = layer.extent;
236
+ const spatialReference = layer.spatialReference || extent?.spatialReference;
237
+ layers.push({
238
+ name: layer.id === undefined ? layer.name : String(layer.id),
239
+ title: layer.name,
240
+ crs: getArcGISCoordinateReferenceSystems(spatialReference),
241
+ boundingBox: normalizeArcGISExtent(extent)
242
+ });
243
+ }
244
+
245
+ if (!layers.length && (json.id !== undefined || json.name)) {
246
+ const extent = json.extent;
247
+ layers.push({
248
+ name: json.id === undefined ? json.name : String(json.id),
249
+ title: json.name,
250
+ crs: getArcGISCoordinateReferenceSystems(json.spatialReference || extent?.spatialReference),
251
+ boundingBox: normalizeArcGISExtent(extent)
252
+ });
253
+ }
254
+
255
+ return {
256
+ // version: json.currentVersion || '',
257
+ title: json.serviceDescription || '',
258
+ name: json.serviceDescription || '',
259
+ abstract: json.description || '',
260
+ keywords: [],
261
+ // attrribution: json.copyrightText || ''.
262
+ // crs: 'EPSG:4326',
263
+ layers
264
+ };
265
+ }
266
+
267
+ /** Normalizes an ArcGIS spatial reference to a one-element CRS list. */
268
+ function getArcGISCoordinateReferenceSystems(
269
+ spatialReference: {wkid?: number; latestWkid?: number} | undefined
270
+ ): string[] | undefined {
271
+ const wellKnownIdentifier = spatialReference?.latestWkid || spatialReference?.wkid;
272
+ return wellKnownIdentifier ? [`EPSG:${wellKnownIdentifier}`] : undefined;
273
+ }
274
+
275
+ /** Normalizes an ArcGIS extent to the loaders.gl two-corner shape. */
276
+ function normalizeArcGISExtent(
277
+ extent: {xmin?: number; ymin?: number; xmax?: number; ymax?: number} | undefined
278
+ ): [[number, number], [number, number]] | undefined {
279
+ return extent && [extent.xmin, extent.ymin, extent.xmax, extent.ymax].every(Number.isFinite)
280
+ ? [
281
+ [extent.xmin!, extent.ymin!],
282
+ [extent.xmax!, extent.ymax!]
283
+ ]
284
+ : undefined;
285
+ }
286
+
287
+ /** Normalizes EPSG-prefixed CRS strings to ArcGIS WKID values. */
288
+ function normalizeArcGISSpatialReference(
289
+ spatialReference: string | number | undefined
290
+ ): string | number | undefined {
291
+ if (typeof spatialReference === 'string') {
292
+ const match = /^EPSG:(\d+)$/i.exec(spatialReference);
293
+ if (match) {
294
+ return match[1];
295
+ }
296
+ }
297
+ return spatialReference;
298
+ }
299
+
300
+ /** Builds a schema from ArcGIS FeatureServer metadata fields. */
301
+ function parseArcGISFeatureServerSchema(json: any): Schema {
302
+ const fields = Array.isArray(json.fields)
303
+ ? json.fields.map((field: any) => ({
304
+ name: field.name,
305
+ type: getSchemaTypeFromArcGISFieldType(field.type),
306
+ nullable: field.nullable
307
+ }))
308
+ : [];
309
+
310
+ return {metadata: {}, fields};
311
+ }
312
+
313
+ /** Maps common ArcGIS field types to loaders.gl schema type strings. */
314
+ function getSchemaTypeFromArcGISFieldType(type: string): DataType {
315
+ switch (type) {
316
+ case 'esriFieldTypeDouble':
317
+ return 'float64';
318
+ case 'esriFieldTypeSingle':
319
+ return 'float32';
320
+ case 'esriFieldTypeInteger':
321
+ case 'esriFieldTypeSmallInteger':
322
+ case 'esriFieldTypeOID':
323
+ return 'int32';
324
+ case 'esriFieldTypeDate':
325
+ return 'timestamp-millisecond';
326
+ default:
327
+ return 'utf8';
328
+ }
329
+ }
330
+
331
+ /** Parses a GeoJSON FeatureCollection into the loaders.gl GeoJSON table shape. */
332
+ function parseGeoJSONTable(json: any): GeoJSONTable {
333
+ if (json?.type === 'FeatureCollection' && Array.isArray(json.features)) {
334
+ return {
335
+ shape: 'geojson-table',
336
+ type: 'FeatureCollection',
337
+ features: json.features
338
+ };
339
+ }
340
+
341
+ throw new Error('ArcGIS FeatureServer query did not return a GeoJSON FeatureCollection');
342
+ }
@@ -0,0 +1,241 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {ImageType} from '@loaders.gl/images';
6
+ import {ImageLoader} from '@loaders.gl/images';
7
+ import type {
8
+ CoreAPI,
9
+ SourceLoader,
10
+ DataSourceOptions,
11
+ ImageSourceMetadata,
12
+ GetImageParameters
13
+ } from '@loaders.gl/loader-utils';
14
+ import {DataSource, ImageSource} from '@loaders.gl/loader-utils';
15
+ import type {LERCData} from '@loaders.gl/lerc';
16
+ import {LERCLoader} from '@loaders.gl/lerc';
17
+ import {buildArcGISResourceURL} from './arcgis-url-utils';
18
+
19
+ /** Options for the ArcGIS ImageServer source. */
20
+ export type ArcGISImageSourceLoaderProps = DataSourceOptions & {
21
+ 'arcgis-image-server'?: {
22
+ /** Default ArcGIS exportImage request parameters. */
23
+ exportImageParameters?: Partial<ArcGISExportImageParameters>;
24
+ };
25
+ };
26
+
27
+ /** Parameters for ArcGIS ImageServer exportImage requests. */
28
+ export type ArcGISExportImageParameters = {
29
+ /** Bounding box of the requested image. */
30
+ bbox: [number, number, number, number];
31
+ /** Spatial reference of the supplied bbox. */
32
+ bboxSR?: string | number;
33
+ /** Pixel width of returned image. */
34
+ width: number;
35
+ /** Pixel height of returned image. */
36
+ height: number;
37
+ /** Spatial reference of the returned image. */
38
+ imageSR?: string | number;
39
+ /** Requested image format. */
40
+ format?: 'jpgpng' | 'png' | 'png8' | 'png24' | 'jpg' | 'bmp' | 'gif' | 'tiff' | 'png32' | 'lerc';
41
+ /** Requested pixel type. */
42
+ pixelType?: 'U1' | 'U2' | 'U4' | 'U8' | 'S8' | 'U16' | 'S16' | 'U32' | 'S32' | 'F32' | 'F64';
43
+ /** NoData pixel value. */
44
+ noData?: string | number;
45
+ /** NoData interpretation mode. */
46
+ noDataInterpretation?: 'esriNoDataMatchAny' | 'esriNoDataMatchAll';
47
+ /** Resampling interpolation. */
48
+ interpolation?: string;
49
+ /** Compression type. */
50
+ compression?: string;
51
+ /** Compression quality. */
52
+ compressionQuality?: number;
53
+ /** Band ids to export. */
54
+ bandIds?: string | number[];
55
+ /** Mosaic rule JSON string or object. */
56
+ mosaicRule?: string | Record<string, unknown>;
57
+ /** Rendering rule JSON string or object. */
58
+ renderingRule?: string | Record<string, unknown>;
59
+ /** ArcGIS response format. */
60
+ f?: 'image' | 'json' | 'pjson';
61
+ };
62
+
63
+ export const ArcGISImageServerSourceLoader = {
64
+ dataType: null as unknown as ArcGISImageSource,
65
+ batchType: null as never,
66
+ name: 'ArcGISImageServer',
67
+ id: 'arcgis-image-server',
68
+ module: 'services',
69
+ version: '0.0.0',
70
+ extensions: [],
71
+ mimeTypes: [],
72
+ type: 'arcgis-image-server',
73
+ fromUrl: true,
74
+ fromBlob: false,
75
+
76
+ options: {
77
+ 'arcgis-image-server': {
78
+ // TODO - add options here
79
+ }
80
+ },
81
+
82
+ defaultOptions: {
83
+ 'arcgis-image-server': {
84
+ // TODO - add options here
85
+ }
86
+ },
87
+
88
+ testURL: (url: string): boolean => url.toLowerCase().includes('imageserver'),
89
+ createDataSource: (
90
+ url: string,
91
+ props: ArcGISImageSourceLoaderProps,
92
+ coreApi?: CoreAPI
93
+ ): ArcGISImageSource => new ArcGISImageSource(url, props, coreApi)
94
+ } as const satisfies SourceLoader<ArcGISImageSource>;
95
+
96
+ /**
97
+ * ArcGIS ImageServer
98
+ * Note - exports a big API, that could be exposed here if there is a use case
99
+ * @see https://developers.arcgis.com/rest/services-reference/enterprise/image-service.htm
100
+ */
101
+ export class ArcGISImageSource
102
+ extends DataSource<string, ArcGISImageSourceLoaderProps>
103
+ implements ImageSource
104
+ {
105
+ constructor(url: string, props: ArcGISImageSourceLoaderProps, coreApi?: CoreAPI) {
106
+ super(url, props, ArcGISImageServerSourceLoader.defaultOptions, coreApi);
107
+ }
108
+
109
+ /** Returns normalized ImageSource metadata. */
110
+ async getMetadata(): Promise<ImageSourceMetadata> {
111
+ return normalizeArcGISImageServerMetadata(await this.metadata());
112
+ }
113
+
114
+ /** Requests an image from generic ImageSource parameters. */
115
+ async getImage(parameters: GetImageParameters): Promise<ImageType> {
116
+ const {boundingBox, bbox, width, height, crs, format, signal} = parameters;
117
+ const spatialReference = normalizeArcGISSpatialReference(crs) || '4326';
118
+ const imageParameters: ArcGISExportImageParameters = {
119
+ bbox: boundingBox ? [...boundingBox[0], ...boundingBox[1]] : bbox!,
120
+ bboxSR: spatialReference,
121
+ imageSR: spatialReference,
122
+ width,
123
+ height,
124
+ format: format === 'image/png' ? 'png' : undefined
125
+ };
126
+ return await this.exportImage(imageParameters, signal);
127
+ }
128
+
129
+ /** Requests the ArcGIS ImageServer metadata document. */
130
+ async metadata(): Promise<unknown> {
131
+ const response = await this.fetch(this.metadataURL());
132
+ await this.checkResponse(response);
133
+ return await response.json();
134
+ }
135
+
136
+ /** Requests an exported image from the ArcGIS ImageServer endpoint. */
137
+ async exportImage(
138
+ options: ArcGISExportImageParameters,
139
+ signal?: AbortSignal
140
+ ): Promise<ImageType> {
141
+ const response = await this.fetch(this.exportImageURL(options), signal ? {signal} : undefined);
142
+ await this.checkResponse(response);
143
+ const arrayBuffer = await response.arrayBuffer();
144
+ return (await this.coreApi.parse(arrayBuffer, ImageLoader, this.loadOptions)) as ImageType;
145
+ }
146
+
147
+ /** Requests an analytical LERC raster from the ArcGIS ImageServer endpoint. */
148
+ async exportRaster(
149
+ options: ArcGISExportImageParameters,
150
+ signal?: AbortSignal
151
+ ): Promise<LERCData> {
152
+ const response = await this.fetch(
153
+ this.exportImageURL({...options, format: 'lerc'}),
154
+ signal ? {signal} : undefined
155
+ );
156
+ await this.checkResponse(response);
157
+ return (await this.coreApi.parse(
158
+ await response.arrayBuffer(),
159
+ LERCLoader,
160
+ this.loadOptions
161
+ )) as LERCData;
162
+ }
163
+
164
+ /** Builds a metadata URL for the ArcGIS ImageServer endpoint. */
165
+ metadataURL(options?: {parameters?: Record<string, unknown>}): string {
166
+ return this.getUrl('', {f: 'pjson', ...options?.parameters});
167
+ }
168
+
169
+ /** Builds an exportImage URL for the ArcGIS ImageServer endpoint. */
170
+ exportImageURL(options: ArcGISExportImageParameters): string {
171
+ const defaultParameters = this.options['arcgis-image-server']?.exportImageParameters || {};
172
+ const {width, height, ...parameters} = {...defaultParameters, ...options};
173
+ return this.getUrl('exportImage', {
174
+ ...parameters,
175
+ bbox: parameters.bbox,
176
+ size: [width, height],
177
+ f: parameters.f || 'image'
178
+ });
179
+ }
180
+
181
+ /** Builds an ArcGIS ImageServer URL. */
182
+ protected getUrl(
183
+ path: string,
184
+ options: Record<string, unknown>,
185
+ extra?: Record<string, unknown>
186
+ ): string {
187
+ return buildArcGISResourceURL(this.url, path, {...options, ...extra});
188
+ }
189
+
190
+ /** Checks an ArcGIS ImageServer response. */
191
+ protected async checkResponse(response: Response): Promise<void> {
192
+ if (!response.ok) {
193
+ throw new Error(
194
+ response.statusText || `ArcGIS ImageServer request failed: ${response.status}`
195
+ );
196
+ }
197
+ }
198
+ }
199
+
200
+ /** Normalizes EPSG-prefixed CRS strings to ArcGIS WKID values. */
201
+ function normalizeArcGISSpatialReference(
202
+ spatialReference: string | number | undefined
203
+ ): string | number | undefined {
204
+ if (typeof spatialReference === 'string') {
205
+ const match = /^EPSG:(\d+)$/i.exec(spatialReference);
206
+ if (match) {
207
+ return match[1];
208
+ }
209
+ }
210
+ return spatialReference;
211
+ }
212
+
213
+ /** Normalizes ArcGIS ImageServer metadata to the generic ImageSource metadata shape. */
214
+ function normalizeArcGISImageServerMetadata(metadata: unknown): ImageSourceMetadata {
215
+ const arcgisMetadata = metadata as any;
216
+ const extent = arcgisMetadata.fullExtent || arcgisMetadata.extent;
217
+ const spatialReference = arcgisMetadata.spatialReference || extent?.spatialReference;
218
+ const wellKnownIdentifier = spatialReference?.latestWkid || spatialReference?.wkid;
219
+ const boundingBox =
220
+ extent && [extent.xmin, extent.ymin, extent.xmax, extent.ymax].every(Number.isFinite)
221
+ ? ([
222
+ [extent.xmin, extent.ymin],
223
+ [extent.xmax, extent.ymax]
224
+ ] as [[number, number], [number, number]])
225
+ : undefined;
226
+ const name = arcgisMetadata.name || arcgisMetadata.serviceDescription || '';
227
+ return {
228
+ name,
229
+ title: name,
230
+ abstract: arcgisMetadata.description || arcgisMetadata.serviceDescription || '',
231
+ keywords: Array.isArray(arcgisMetadata.keywords) ? arcgisMetadata.keywords : [],
232
+ layers: [
233
+ {
234
+ name,
235
+ title: name,
236
+ crs: wellKnownIdentifier ? [`EPSG:${wellKnownIdentifier}`] : undefined,
237
+ boundingBox
238
+ }
239
+ ]
240
+ };
241
+ }