ogcopenapis 0.0.11 → 1.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.
Files changed (60) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +310 -72
  3. package/lib/OgcOpenApiCrsTools.d.ts +10 -9
  4. package/lib/OgcOpenApiCrsTools.d.ts.map +1 -0
  5. package/lib/OgcOpenApiCrsTools.js +40 -39
  6. package/lib/OgcOpenApiCrsTools.js.map +1 -0
  7. package/lib/OgcOpenApiFeatureStore.d.ts +94 -93
  8. package/lib/OgcOpenApiFeatureStore.d.ts.map +1 -0
  9. package/lib/OgcOpenApiFeatureStore.js +200 -240
  10. package/lib/OgcOpenApiFeatureStore.js.map +1 -0
  11. package/lib/OgcOpenApiFetchTools.d.ts +37 -29
  12. package/lib/OgcOpenApiFetchTools.d.ts.map +1 -0
  13. package/lib/OgcOpenApiFetchTools.js +68 -48
  14. package/lib/OgcOpenApiFetchTools.js.map +1 -0
  15. package/lib/OgcOpenApiGetCapabilities.d.ts +209 -112
  16. package/lib/OgcOpenApiGetCapabilities.d.ts.map +1 -0
  17. package/lib/OgcOpenApiGetCapabilities.js +424 -277
  18. package/lib/OgcOpenApiGetCapabilities.js.map +1 -0
  19. package/lib/OgcOpenApiMapsModel.d.ts +41 -36
  20. package/lib/OgcOpenApiMapsModel.d.ts.map +1 -0
  21. package/lib/OgcOpenApiMapsModel.js +88 -75
  22. package/lib/OgcOpenApiMapsModel.js.map +1 -0
  23. package/lib/OgcOpenApiMvtTools.d.ts +12 -0
  24. package/lib/OgcOpenApiMvtTools.d.ts.map +1 -0
  25. package/lib/OgcOpenApiMvtTools.js +100 -0
  26. package/lib/OgcOpenApiMvtTools.js.map +1 -0
  27. package/lib/OgcOpenApiStyleTools.d.ts +51 -0
  28. package/lib/OgcOpenApiStyleTools.d.ts.map +1 -0
  29. package/lib/OgcOpenApiStyleTools.js +155 -0
  30. package/lib/OgcOpenApiStyleTools.js.map +1 -0
  31. package/lib/OgcOpenApiTileMatrixTools.d.ts +41 -0
  32. package/lib/OgcOpenApiTileMatrixTools.d.ts.map +1 -0
  33. package/lib/OgcOpenApiTileMatrixTools.js +88 -0
  34. package/lib/OgcOpenApiTileMatrixTools.js.map +1 -0
  35. package/lib/OgcOpenApiTilesModel.d.ts +56 -31
  36. package/lib/OgcOpenApiTilesModel.d.ts.map +1 -0
  37. package/lib/OgcOpenApiTilesModel.js +167 -128
  38. package/lib/OgcOpenApiTilesModel.js.map +1 -0
  39. package/lib/OgcOpenApiVectorTilesModel.d.ts +39 -0
  40. package/lib/OgcOpenApiVectorTilesModel.d.ts.map +1 -0
  41. package/lib/OgcOpenApiVectorTilesModel.js +122 -0
  42. package/lib/OgcOpenApiVectorTilesModel.js.map +1 -0
  43. package/lib/OgcOpenApiWmtsTools.d.ts +30 -0
  44. package/lib/OgcOpenApiWmtsTools.d.ts.map +1 -0
  45. package/lib/OgcOpenApiWmtsTools.js +89 -0
  46. package/lib/OgcOpenApiWmtsTools.js.map +1 -0
  47. package/package.json +68 -39
  48. package/src/OgcOpenApiCrsTools.ts +40 -0
  49. package/src/OgcOpenApiFeatureStore.ts +296 -0
  50. package/src/OgcOpenApiFetchTools.ts +84 -0
  51. package/src/OgcOpenApiGetCapabilities.ts +641 -0
  52. package/src/OgcOpenApiMapsModel.ts +128 -0
  53. package/src/OgcOpenApiMvtTools.ts +104 -0
  54. package/src/OgcOpenApiStyleTools.ts +187 -0
  55. package/src/OgcOpenApiTileMatrixTools.ts +111 -0
  56. package/src/OgcOpenApiTilesModel.ts +214 -0
  57. package/src/OgcOpenApiVectorTilesModel.ts +141 -0
  58. package/src/OgcOpenApiWmtsTools.ts +114 -0
  59. package/lib/OgcOpenApiGetCollectionTiles.d.ts +0 -7
  60. package/lib/OgcOpenApiGetCollectionTiles.js +0 -7
@@ -0,0 +1,641 @@
1
+ import {type FetchServerOptions, OgcOpenApiFetchTools} from "./OgcOpenApiFetchTools.js";
2
+ const SCALE_DENOMINATOR_TOLERANCE = 1e-8;
3
+
4
+ type RequestHeaders = {[headerName: string]: string;} | null
5
+ export interface LinkType {
6
+ rel: string;
7
+ type: string;
8
+ title: string;
9
+ href: string;
10
+ }
11
+
12
+ export interface TileMatrix {
13
+ id: string,
14
+ scaleDenominator: number;
15
+ cellSize: number;
16
+ cornerOfOrigin: string;
17
+ pointOfOrigin: number[];
18
+ matrixWidth: number;
19
+ matrixHeight: number;
20
+ tileWidth: number;
21
+ tileHeight: number;
22
+ variableMatrixWidths: {
23
+ coalesce: number;
24
+ minTileRow: number;
25
+ maxTileRow: number;
26
+ }[]
27
+ }
28
+ export interface TileSetData {
29
+ id: string;
30
+ title: string;
31
+ uri: string;
32
+ crs: string;
33
+ orderedAxes: string[];
34
+ tileMatrices: TileMatrix[];
35
+ boundingBox: {
36
+ lowerLeft: number[],
37
+ upperRight: number[],
38
+ }
39
+ }
40
+
41
+ export interface TileSetLayer {
42
+ id: string;
43
+ title?: string;
44
+ dataType?: string;
45
+ geometryDimension?: number;
46
+ minTileMatrix?: string;
47
+ maxTileMatrix?: string;
48
+ }
49
+
50
+ // A tileset of a collection, as listed in its tilesets (/collections/{collectionId}/tiles)
51
+ export interface CollectionTileSetInfo {
52
+ // The URI of the tile matrix set, which identifies it whatever id the server gives it
53
+ tileMatrixSetURI?: string;
54
+ tileMatrixSetId?: string;
55
+ dataType?: string;
56
+ crs?: string;
57
+ // The tileset itself, its self link
58
+ href: string;
59
+ // The definition of the tile matrix set, when the server links it
60
+ tilingSchemeHref?: string;
61
+ // The definition of the tile matrix set, when the tileset includes it
62
+ tileMatrixSet?: TileSetData;
63
+ }
64
+
65
+ export interface CollectionTileSet {
66
+ dataType?: string;
67
+ links: LinkType[];
68
+ layers: TileSetLayer[];
69
+ tileMatrixSetLimits?: TileMatrixLimits[];
70
+ // Where the data is, when the server tells: in the CRS of boundingBox.crs, or of the tile matrix set
71
+ boundingBox?: {lowerLeft: number[], upperRight: number[], crs?: string};
72
+ }
73
+
74
+ export interface TileMatrixLimits {
75
+ tileMatrix: string;
76
+ minTileRow: number;
77
+ maxTileRow: number;
78
+ minTileCol: number;
79
+ maxTileCol: number;
80
+ }
81
+
82
+ export interface TileSetMeta {
83
+ id: string;
84
+ title: string;
85
+ links: LinkType[];
86
+ }
87
+
88
+ export interface OgcOpenApiCapabilitiesObject {
89
+ name: string;
90
+ collections: OgcOpenApiCapabilitiesCollection[];
91
+ version: string;
92
+ service: string;
93
+ info: any;
94
+ crs?: any[];
95
+ baseUrl: string;
96
+ // The landing page link to the tile matrix sets (rel tiling-schemes)
97
+ tileMatrixSetsUrl?: string;
98
+ // The links of the landing page, to the tilesets and styles of the dataset among others
99
+ links?: LinkType[];
100
+ serverOptions: FetchServerOptions;
101
+ }
102
+
103
+ export interface OgcOpenApiCapabilitiesCollection {
104
+ id: string;
105
+ name: string;
106
+ title: string;
107
+ defaultReference: string;
108
+ outputFormats: string[];
109
+ description: string;
110
+ keywords: string[];
111
+ links: OgcOpenApiCapabilitiesCollectionServiceLinkType[];
112
+ extent: any;
113
+ crs?: any;
114
+ storageCrs?: any;
115
+ }
116
+
117
+ export interface OgcOpenApiCapabilitiesCollectionServiceLinkType {
118
+ rel: string;
119
+ type: string;
120
+ title: string;
121
+ href: string;
122
+ }
123
+
124
+ export enum CollectionLinkType {
125
+ Items = 'items',
126
+ Styles = 'styles',
127
+ Map = 'map',
128
+ Tiles = 'tiles',
129
+ VectorTiles = 'vectorTiles',
130
+ }
131
+
132
+ const CollectionLinkTypeAlternatives = {
133
+ [CollectionLinkType.Items]: ["items", "item"],
134
+ [CollectionLinkType.Styles]: ["http://www.opengis.net/def/rel/ogc/1.0/styles", "styles"],
135
+ [CollectionLinkType.Map]: ["http://www.opengis.net/def/rel/ogc/1.0/map", "map"],
136
+ [CollectionLinkType.Tiles]: ["http://www.opengis.net/def/rel/ogc/1.0/tilesets-map", "tilesets-map"],
137
+ [CollectionLinkType.VectorTiles]: ["http://www.opengis.net/def/rel/ogc/1.0/tilesets-vector", "tilesets-vector"],
138
+ }
139
+
140
+ // OGC API link relations are given in full or, by older servers, short: tiling-scheme or http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme
141
+ const OGC_REL = "http://www.opengis.net/def/rel/ogc/1.0/";
142
+ const isRel = (link: LinkType, rel: string) => link.rel === OGC_REL + rel || link.rel === rel;
143
+
144
+ const CollectionLinkTypeDefaultFormat = {
145
+ [CollectionLinkType.Map]: "image/png",
146
+ [CollectionLinkType.Tiles]: "image/png",
147
+ [CollectionLinkType.VectorTiles]: "application/vnd.mapbox-vector-tile",
148
+ }
149
+
150
+ interface FetchLinkContentOptions {
151
+ hostUrl: string;
152
+ credentials?: boolean;
153
+ requestHeaders?: {
154
+ [headerName: string]: string;
155
+ } | null;
156
+ }
157
+
158
+ export type ProxifyFunction = (url: string, fetchOptions: RequestInit) => {
159
+ url: string;
160
+ fetchOptions: RequestInit;
161
+ }
162
+
163
+ export interface OgcOpenApiGetCapabilitiesFromURL {
164
+ filterCollectionsByLinkType?: CollectionLinkType
165
+ credentials?: boolean;
166
+ requestHeaders?: {
167
+ [headerName: string]: string;
168
+ } | null;
169
+ proxify?: ProxifyFunction
170
+ }
171
+
172
+ export class OgcOpenApiGetCapabilities {
173
+
174
+ public static fromURL(inputRequest: string, fromUrlOptions?: OgcOpenApiGetCapabilitiesFromURL) {
175
+ const options: OgcOpenApiGetCapabilitiesFromURL = fromUrlOptions ? fromUrlOptions : {};
176
+ return new Promise<OgcOpenApiCapabilitiesObject>((resolve, reject) => {
177
+ const baseUrl = OgcOpenApiFetchTools.cleanUrl(inputRequest);
178
+ const requestUrl = OgcOpenApiFetchTools.cleanUrl(inputRequest);
179
+ const serverOptions = OgcOpenApiFetchTools.createFetchOptions({originalUrl: requestUrl, proxify: options?.proxify});
180
+
181
+ OgcOpenApiFetchTools.fetch(requestUrl, {
182
+ method: 'GET',
183
+ credentials: options?.credentials ? "same-origin" : "omit",
184
+ headers: options? options.requestHeaders: {},
185
+ }, serverOptions).then(
186
+ (response) => {
187
+ if (response.status === 200) {
188
+ response.json().then((jsonObject) => {
189
+ if (!jsonObject.links) {
190
+ reject("Invalid format: (property 'links' is missing)")
191
+ return;
192
+ }
193
+ let linkToData = jsonObject.links.find(
194
+ (link: OgcOpenApiCapabilitiesCollectionServiceLinkType) =>
195
+ link.rel === 'data' && link.type === 'application/json'
196
+ );
197
+ if (!linkToData) {
198
+ linkToData = jsonObject.links.find((link) => link.rel === 'data' );
199
+ }
200
+ const linkToApi = jsonObject.links.find(
201
+ (link: OgcOpenApiCapabilitiesCollectionServiceLinkType) => {
202
+ return (
203
+ (link.rel === 'service-desc' || link.rel === 'service') &&
204
+ link.type.indexOf('openapi+json') > -1
205
+ );
206
+ }
207
+ );
208
+ const tilingSchemes = jsonObject.links.filter((link) => isRel(link, 'tiling-schemes'));
209
+ const linkToTileMatrixSets = tilingSchemes.find((link) => link.type === 'application/json') || tilingSchemes.find((link) => !link.type);
210
+ // The links of the landing page are relative to it, the links of the collections to /collections
211
+ const resolveLink = (link: OgcOpenApiCapabilitiesCollectionServiceLinkType, documentUrl: string) =>
212
+ ({...link, href: OgcOpenApiFetchTools.resolveUrl(link.href, documentUrl, serverOptions)});
213
+ const promiseArray = [];
214
+ if (linkToData) {
215
+ const collectionsLink = resolveLink(linkToData, requestUrl);
216
+ const collectionsPromise = OgcOpenApiGetCapabilities.fetchLinkContentAsJSON(
217
+ collectionsLink,
218
+ {credentials: options.credentials, requestHeaders: options.requestHeaders},
219
+ serverOptions
220
+ );
221
+ promiseArray.push(collectionsPromise);
222
+ if (linkToApi) {
223
+ const apiPromise = OgcOpenApiGetCapabilities.fetchLinkContentAsJSON(
224
+ resolveLink(linkToApi, requestUrl),
225
+ {credentials: options.credentials, requestHeaders: options.requestHeaders},
226
+ serverOptions
227
+ );
228
+ promiseArray.push(apiPromise);
229
+ }
230
+ Promise.all(promiseArray).then((responses: any) => {
231
+ const responseDataLink = responses[0];
232
+ const responseOpenApi =
233
+ responses.length >= 1 ? responses[1] : undefined;
234
+ const crsArray = typeof responseDataLink.crs !== "undefined" ? { crs: responseDataLink.crs } : {};
235
+ const foundCollections = responseDataLink.collections.filter( c =>
236
+ options && options.filterCollectionsByLinkType ?
237
+ OgcOpenApiGetCapabilities.filterCollectionLinks(c.links,options.filterCollectionsByLinkType).length > 0 : true
238
+ )
239
+ .map(
240
+ (collection: any) => {
241
+ const name =
242
+ typeof collection.id !== 'undefined'
243
+ ? collection.id
244
+ : collection.name;
245
+ const collectionCrsObject = {} as any;
246
+ if (typeof collection.crs !== "undefined") collectionCrsObject.crs = collection.crs;
247
+ if (typeof collection.storageCrs !== "undefined") collectionCrsObject.storageCrs = collection.storageCrs;
248
+ const layer: OgcOpenApiCapabilitiesCollection = {
249
+ description: collection.description,
250
+ id: name,
251
+ name,
252
+ title: collection.title,
253
+ keywords: collection.keywords
254
+ ? collection.keywords
255
+ : [],
256
+ defaultReference: 'CRS:84',
257
+ links: collection.links,
258
+ outputFormats: options.filterCollectionsByLinkType ?
259
+ this.filterCollectionLinks(collection.links, options.filterCollectionsByLinkType).map(link => this.detectTypeFormat(link))
260
+ :
261
+ collection.links.map(link => this.detectTypeFormat(link))
262
+ ,
263
+ extent: collection.extent,
264
+ ...collectionCrsObject
265
+ };
266
+ return layer;
267
+ }
268
+ );
269
+ // console.log(featureTypes);
270
+ const o: OgcOpenApiCapabilitiesObject = {
271
+ name: '',
272
+ collections: foundCollections.map(col=>({...col, crs: col.crs ? col.crs : [], links: col.links.map(l => resolveLink(l, collectionsLink.href))})),
273
+ version: responseOpenApi ? responseOpenApi.openapi : '',
274
+ service: '',
275
+ info: responseOpenApi ? responseOpenApi.info : {},
276
+ ...crsArray,
277
+ serverOptions: serverOptions,
278
+ links: jsonObject.links.map(l => resolveLink(l, requestUrl)),
279
+ baseUrl,
280
+ tileMatrixSetsUrl: linkToTileMatrixSets ? resolveLink(linkToTileMatrixSets, requestUrl).href : undefined
281
+ };
282
+ resolve(o);
283
+ });
284
+ } else {
285
+ reject("Invalid format: (property 'links' contains no links with rel=data");
286
+ }
287
+ }, ()=>{
288
+ reject("Not JSON")
289
+ });
290
+ } else {
291
+ reject(response.status);
292
+ }
293
+ },
294
+ (err) => {
295
+ reject(err);
296
+ }
297
+ );
298
+ });
299
+ }
300
+
301
+ private static fetchLinkContentAsJSON(link: OgcOpenApiCapabilitiesCollectionServiceLinkType, options: {credentials: boolean, requestHeaders: RequestHeaders}, serverOptions?: FetchServerOptions) {
302
+ return new Promise((resolve) => {
303
+ OgcOpenApiFetchTools.fetch(link.href, {method: "GET", credentials: options.credentials ? "same-origin":"omit", headers: options.requestHeaders}, serverOptions).then(
304
+ (result) => {
305
+ if (result.status === 200) {
306
+ result.json().then((json) => resolve(json));
307
+ } else {
308
+ resolve(undefined);
309
+ }
310
+ },
311
+ () => {
312
+ resolve(undefined);
313
+ }
314
+ );
315
+ });
316
+ }
317
+
318
+ static filterCollectionLinks(
319
+ links: OgcOpenApiCapabilitiesCollectionServiceLinkType[],
320
+ linkType: CollectionLinkType
321
+ ) {
322
+ switch (linkType) {
323
+ case CollectionLinkType.Items:
324
+ case CollectionLinkType.Map:
325
+ case CollectionLinkType.Styles:
326
+ case CollectionLinkType.Tiles:
327
+ case CollectionLinkType.VectorTiles:
328
+ return links.filter((link: any) => CollectionLinkTypeAlternatives[linkType].some(e=>e===link.rel));
329
+ default:
330
+ return [];
331
+ }
332
+ }
333
+
334
+ public static getDataLink(
335
+ currentLayer: OgcOpenApiCapabilitiesCollection,
336
+ preferedFormat: string
337
+ ) {
338
+ const dataLink = OgcOpenApiGetCapabilities.filterCollectionLinks(
339
+ currentLayer.links,
340
+ CollectionLinkType.Items
341
+ ).find((link) => link.type === preferedFormat);
342
+ return dataLink ? dataLink.href : '';
343
+ }
344
+
345
+ private static detectTypeFormat(link: OgcOpenApiCapabilitiesCollectionServiceLinkType) {
346
+ if (link.type) return link.type;
347
+ for (const key in CollectionLinkTypeAlternatives) {
348
+ if (CollectionLinkTypeAlternatives.hasOwnProperty(key) && CollectionLinkTypeAlternatives[key].some(e=>e===link.rel)) {
349
+ if (CollectionLinkTypeDefaultFormat[key]) return CollectionLinkTypeDefaultFormat[key];
350
+ break;
351
+ }
352
+ }
353
+ return "application/json"
354
+ }
355
+
356
+ /**
357
+ * The tile matrix sets of the server, with their definitions. A definition that cannot be read is left out, so one
358
+ * broken definition does not hide the others. Rejects only when none can be read.
359
+ */
360
+ public static async fetchTileSetsInFull(capabilities: OgcOpenApiCapabilitiesObject | null, options?:{credentials?:boolean, requestHeaders?: RequestHeaders}) {
361
+ const tileSetMetaArray = await this.fetchTileSets(capabilities, options);
362
+ const settled = await Promise.all(tileSetMetaArray.map(tml => this.fetchTileset(capabilities, tml, options).then(
363
+ tileSet => ({tileSet, error: undefined}),
364
+ error => ({tileSet: undefined as TileSetData, error})
365
+ )));
366
+ const tileSets = settled.filter(s => s.tileSet).map(s => s.tileSet);
367
+ const failed = settled.find(s => !s.tileSet);
368
+ if (tileSets.length === 0 && failed) throw failed.error;
369
+ return tileSets;
370
+ }
371
+
372
+ public static getQuadTreeCompatibleLevelOffset(tileMatrixSet: TileSetData) {
373
+ const levels = tileMatrixSet.tileMatrices;
374
+ const reference = levels[levels.length - 1];
375
+
376
+ for (let level = levels.length - 2; level >= 0; level--) {
377
+ const current = levels[level];
378
+
379
+ // tslint:disable-next-line:no-bitwise
380
+ const multiplier = 1 << (levels.length - level - 1);
381
+
382
+ const errorMargin = current.scaleDenominator * SCALE_DENOMINATOR_TOLERANCE;
383
+ if (Math.abs(current.scaleDenominator - reference.scaleDenominator * multiplier) > errorMargin) {
384
+ return level + 1;
385
+ }
386
+
387
+ if (current.pointOfOrigin[0] !== reference.pointOfOrigin[0]) {
388
+ return level + 1;
389
+ }
390
+
391
+ if (current.tileWidth !== reference.tileWidth || current.tileHeight !== reference.tileHeight) {
392
+ return level + 1;
393
+ }
394
+
395
+ if (typeof current.variableMatrixWidths!=="undefined") {
396
+ return level + 1;
397
+ }
398
+
399
+ if (current.matrixWidth !== (reference.matrixWidth / multiplier) || current.matrixHeight !== (reference.matrixHeight / multiplier)) {
400
+ return level + 1;
401
+ }
402
+ }
403
+ return 0;
404
+ }
405
+
406
+ private static async fetchTileSets(capabilities: OgcOpenApiCapabilitiesObject | null, options?:{credentials?:boolean, requestHeaders?: RequestHeaders}): Promise<TileSetMeta[]> {
407
+ if (capabilities === null) throw {error: true, message: "No capabilities"};
408
+ const url = capabilities.tileMatrixSetsUrl ? capabilities.tileMatrixSetsUrl : capabilities.baseUrl + "tileMatrixSets";
409
+ const data = await OgcOpenApiGetCapabilities.fetchJson(capabilities, url, options);
410
+ if (!data.tileMatrixSets) throw {error: true, message: "tileMatrixSets not defined"};
411
+ const resolve = (href: string) => OgcOpenApiFetchTools.resolveUrl(href, url, capabilities.serverOptions);
412
+ return data.tileMatrixSets.map(tml => ({
413
+ ...tml,
414
+ href: resolve(tml.href),
415
+ links: (tml.links ? tml.links : []).map(l => ({...l, href: resolve(l.href)}))
416
+ }));
417
+ }
418
+
419
+ // GETs a JSON document. Servers answer JSON for the Accept header, or for the f parameter that their links carry.
420
+ private static async fetchJson(capabilities: OgcOpenApiCapabilitiesObject, href: string, options?: {credentials?: boolean, requestHeaders?: RequestHeaders}) {
421
+ let result: Response;
422
+ try {
423
+ result = await OgcOpenApiFetchTools.fetch(href, {
424
+ method: "GET",
425
+ credentials: options?.credentials ? "same-origin" : "omit",
426
+ headers: {Accept: "application/json", ...(options?.requestHeaders ? options.requestHeaders : {})}
427
+ }, capabilities.serverOptions);
428
+ } catch (err) {
429
+ throw {error: true, message: err.message};
430
+ }
431
+ if (result.status !== 200) throw {error: true, message: "Invalid response: " + result.status};
432
+ try {
433
+ return await result.json();
434
+ } catch (err) {
435
+ throw {error: true, message: "Not JSON data"};
436
+ }
437
+ }
438
+
439
+ // The link that identifies the tile matrix set of a tileset, relative to the document that has it
440
+ private static getTilingSchemeHref(capabilities: OgcOpenApiCapabilitiesObject, links: LinkType[], documentUrl: string) {
441
+ const tilingScheme = links.find(l => isRel(l, "tiling-scheme"));
442
+ return tilingScheme ? OgcOpenApiFetchTools.resolveUrl(tilingScheme.href, documentUrl, capabilities.serverOptions) : undefined;
443
+ }
444
+
445
+ public static getTilesLink (
446
+ capabilities: OgcOpenApiCapabilitiesObject | null,
447
+ currentCollection: OgcOpenApiCapabilitiesCollection,
448
+ tileMatrixId: string,
449
+ options?: {credentials?: boolean, requestHeaders?: RequestHeaders},
450
+ linkType: CollectionLinkType = CollectionLinkType.Tiles
451
+ ) {
452
+ return this.fetchCollectionTileSet(capabilities, currentCollection, tileMatrixId, options, linkType).then(tileSet => tileSet.links);
453
+ }
454
+
455
+ // The tilesets link of a collection, the JSON one when the server types its links
456
+ private static getTileSetsHref(collection: OgcOpenApiCapabilitiesCollection, linkType: CollectionLinkType) {
457
+ const dataLinks = OgcOpenApiGetCapabilities.filterCollectionLinks(collection.links, linkType);
458
+ const link = dataLinks.find((link) => link.type === "application/json");
459
+ return link ? link.href : dataLinks.length > 0 ? dataLinks[0].href : '';
460
+ }
461
+
462
+ // The tilesets link of the landing page: the tilesets of the dataset, with all its collections in each tile
463
+ private static getDatasetTileSetsHref(capabilities: OgcOpenApiCapabilitiesObject, linkType: CollectionLinkType) {
464
+ const dataLinks = OgcOpenApiGetCapabilities.filterCollectionLinks(capabilities.links ? capabilities.links : [], linkType);
465
+ const link = dataLinks.find((link) => link.type === "application/json") || dataLinks.find((link) => !link.type) || dataLinks[0];
466
+ return link ? link.href : '';
467
+ }
468
+
469
+ /**
470
+ * Whether the server tiles the dataset as a whole, all its collections in each tile, for linkType Tiles or VectorTiles
471
+ */
472
+ public static hasDatasetTileSets(capabilities: OgcOpenApiCapabilitiesObject | null, linkType: CollectionLinkType) {
473
+ return capabilities !== null && OgcOpenApiGetCapabilities.getDatasetTileSetsHref(capabilities, linkType) !== '';
474
+ }
475
+
476
+ /**
477
+ * Lists the tilesets of a collection: the tile matrix sets the collection is tiled in, which can be fewer than the
478
+ * tile matrix sets of the server.
479
+ */
480
+ public static async fetchCollectionTileSets(
481
+ capabilities: OgcOpenApiCapabilitiesObject | null,
482
+ collection: OgcOpenApiCapabilitiesCollection,
483
+ linkType: CollectionLinkType = CollectionLinkType.Tiles,
484
+ options?: {credentials?: boolean, requestHeaders?: RequestHeaders}
485
+ ): Promise<CollectionTileSetInfo[]> {
486
+ if (capabilities === null) throw {error: true, message: "No capabilities"};
487
+ const href = OgcOpenApiGetCapabilities.getTileSetsHref(collection, linkType);
488
+ if (href === '') throw {error: true, message: "Collection " + collection.id + " has no " + linkType + " link"};
489
+ return OgcOpenApiGetCapabilities.fetchTileSetList(capabilities, href, options);
490
+ }
491
+
492
+ /**
493
+ * Lists the tilesets of the dataset (/tiles of the landing page): each tile has the data of all its collections.
494
+ * Some servers tile only the dataset (PDOK).
495
+ */
496
+ public static async fetchDatasetTileSets(
497
+ capabilities: OgcOpenApiCapabilitiesObject | null,
498
+ linkType: CollectionLinkType = CollectionLinkType.Tiles,
499
+ options?: {credentials?: boolean, requestHeaders?: RequestHeaders}
500
+ ): Promise<CollectionTileSetInfo[]> {
501
+ if (capabilities === null) throw {error: true, message: "No capabilities"};
502
+ const href = OgcOpenApiGetCapabilities.getDatasetTileSetsHref(capabilities, linkType);
503
+ if (href === '') throw {error: true, message: "The dataset has no " + linkType + " link"};
504
+ return OgcOpenApiGetCapabilities.fetchTileSetList(capabilities, href, options);
505
+ }
506
+
507
+ private static async fetchTileSetList(capabilities: OgcOpenApiCapabilitiesObject, href: string, options?: {credentials?: boolean, requestHeaders?: RequestHeaders}) {
508
+ const data = await OgcOpenApiGetCapabilities.fetchJson(capabilities, href, options);
509
+ if (!Array.isArray(data.tilesets)) throw {error: true, message: "Response has no tilesets"};
510
+ const tileSets: CollectionTileSetInfo[] = [];
511
+ for (const tileSet of data.tilesets) {
512
+ const links: LinkType[] = tileSet.links ? tileSet.links : [];
513
+ const selfLinks = links.filter(l => l.rel === "self");
514
+ const self = selfLinks.find(l => l.type === "application/json") || selfLinks[0];
515
+ if (!self) continue;
516
+ tileSets.push({
517
+ tileMatrixSetURI: tileSet.tileMatrixSetURI,
518
+ tileMatrixSetId: tileSet.tileMatrixSetId,
519
+ dataType: tileSet.dataType,
520
+ crs: typeof tileSet.crs === "string" ? tileSet.crs : undefined,
521
+ href: OgcOpenApiFetchTools.resolveUrl(self.href, href, capabilities.serverOptions),
522
+ tilingSchemeHref: OgcOpenApiGetCapabilities.getTilingSchemeHref(capabilities, links, href),
523
+ tileMatrixSet: OgcOpenApiGetCapabilities.getInlineTileMatrixSet(tileSet),
524
+ });
525
+ }
526
+ return tileSets;
527
+ }
528
+
529
+ /**
530
+ * The tile matrix set of a tileset among the tile matrix sets of the server (fetchTileSetsInFull): by URI, which
531
+ * servers give with their own ids (CubeWerx: WebMercatorQuad is smerc), then by id.
532
+ */
533
+ public static matchTileMatrixSet(tileSet: CollectionTileSetInfo, tileMatrixSets: TileSetData[]): TileSetData | undefined {
534
+ const lastSegment = (url: string) => url.split("?")[0].replace(/\/$/, "").split("/").pop();
535
+ const byUri = tileSet.tileMatrixSetURI ? tileMatrixSets.find(t => OgcOpenApiGetCapabilities.sameUri(t.uri, tileSet.tileMatrixSetURI)) : undefined;
536
+ if (byUri) return byUri;
537
+ const ids = [tileSet.tileMatrixSetId,
538
+ tileSet.tileMatrixSetURI ? lastSegment(tileSet.tileMatrixSetURI) : undefined,
539
+ tileSet.tilingSchemeHref ? lastSegment(tileSet.tilingSchemeHref) : undefined].filter(id => !!id);
540
+ return tileMatrixSets.find(t => ids.indexOf(t.id) > -1);
541
+ }
542
+
543
+ // Both forms name the same resource: http or https, the case of the scheme and host, a trailing slash
544
+ private static sameUri(a: string | undefined, b: string | undefined) {
545
+ const normalize = (uri: string) => uri.trim().replace(/\/$/, "")
546
+ .replace(/^https?:\/\/[^/]*/i, prefix => prefix.toLowerCase().replace(/^https:/, "http:"));
547
+ return !!a && !!b && normalize(a) === normalize(b);
548
+ }
549
+
550
+ /**
551
+ * The tile matrix set of a tileset: matched among the tile matrix sets of the server, or otherwise fetched from the
552
+ * tiling-scheme link of the tileset. A tileset in a list of tilesets is a summary that need not identify its tile
553
+ * matrix set, then the tileset itself is read (CubeWerx).
554
+ */
555
+ public static async fetchTileMatrixSetOfTileSet(
556
+ capabilities: OgcOpenApiCapabilitiesObject | null,
557
+ tileSet: CollectionTileSetInfo,
558
+ tileMatrixSets: TileSetData[],
559
+ options?: {credentials?: boolean, requestHeaders?: RequestHeaders}
560
+ ): Promise<TileSetData> {
561
+ if (capabilities === null) throw {error: true, message: "No capabilities"};
562
+ if (tileSet.tileMatrixSet) return tileSet.tileMatrixSet;
563
+ let identified = tileSet;
564
+ if (!tileSet.tileMatrixSetURI && !tileSet.tileMatrixSetId && !tileSet.tilingSchemeHref) {
565
+ identified = {...tileSet, ...await OgcOpenApiGetCapabilities.fetchTileSetIdentity(capabilities, tileSet.href, options)};
566
+ if (identified.tileMatrixSet) return identified.tileMatrixSet;
567
+ }
568
+ const match = OgcOpenApiGetCapabilities.matchTileMatrixSet(identified, tileMatrixSets);
569
+ if (match) return match;
570
+ if (!identified.tilingSchemeHref) {
571
+ throw {error: true, message: "No tile matrix set for " + (identified.tileMatrixSetURI || identified.href)};
572
+ }
573
+ return OgcOpenApiGetCapabilities.fetchTileset(capabilities, {id: identified.tileMatrixSetId, title: "", links: [{rel: "self", type: "application/json", title: "", href: identified.tilingSchemeHref}]}, options);
574
+ }
575
+
576
+ // What identifies the tile matrix set in a tileset: its URI, its id or its tiling-scheme link
577
+ private static async fetchTileSetIdentity(capabilities: OgcOpenApiCapabilitiesObject, href: string, options?: {credentials?: boolean, requestHeaders?: RequestHeaders}) {
578
+ const data = await OgcOpenApiGetCapabilities.fetchJson(capabilities, href, options);
579
+ return {
580
+ tileMatrixSetURI: data.tileMatrixSetURI as string | undefined,
581
+ tileMatrixSetId: data.tileMatrixSetId as string | undefined,
582
+ tilingSchemeHref: OgcOpenApiGetCapabilities.getTilingSchemeHref(capabilities, data.links ? data.links : [], href),
583
+ tileMatrixSet: OgcOpenApiGetCapabilities.getInlineTileMatrixSet(data),
584
+ };
585
+ }
586
+
587
+ // A tileset can include the definition of its tile matrix set instead of linking it
588
+ private static getInlineTileMatrixSet(tileSet: any): TileSetData | undefined {
589
+ const tileMatrixSet = tileSet ? tileSet.tileMatrixSet : undefined;
590
+ return tileMatrixSet && typeof tileMatrixSet.id !== "undefined" && Array.isArray(tileMatrixSet.tileMatrices) && tileMatrixSet.crs ?
591
+ tileMatrixSet : undefined;
592
+ }
593
+
594
+ /**
595
+ * Fetches the tileset of a collection for one tile matrix set: the tile URL templates (rel item) and, for vector
596
+ * tiles, the layers contained in each tile.
597
+ */
598
+ public static async fetchCollectionTileSet (
599
+ capabilities: OgcOpenApiCapabilitiesObject | null,
600
+ // null for a tileset of the dataset
601
+ currentCollection: OgcOpenApiCapabilitiesCollection | null,
602
+ // The id of the tile matrix set, or a tileset from fetchCollectionTileSets or fetchDatasetTileSets
603
+ tileMatrix: string | CollectionTileSetInfo,
604
+ options?: {credentials?: boolean, requestHeaders?: RequestHeaders},
605
+ linkType: CollectionLinkType = CollectionLinkType.Tiles
606
+ ): Promise<CollectionTileSet> {
607
+ if (capabilities === null) throw {error: true, message: "No capabilities"};
608
+ const tileSetsHref = currentCollection ?
609
+ OgcOpenApiGetCapabilities.getTileSetsHref(currentCollection, linkType) :
610
+ OgcOpenApiGetCapabilities.getDatasetTileSetsHref(capabilities, linkType);
611
+ const tileInfoUrl = typeof tileMatrix === "string" ? OgcOpenApiFetchTools.cleanUrl(tileSetsHref) + tileMatrix : tileMatrix.href;
612
+ // pygeoapi answers 500 without f=json or an Accept header
613
+ const data = await OgcOpenApiGetCapabilities.fetchJson(capabilities, tileInfoUrl, options);
614
+ const links = data.links ? data.links.filter(l => l.rel === "item" || l.rel === "items").map(a => ({...a, href: OgcOpenApiFetchTools.resolveUrl(a.href, tileInfoUrl, capabilities.serverOptions)})) : [];
615
+ if (links.length === 0) throw {error: true, message: "Invalid"};
616
+ return {
617
+ dataType: data.dataType,
618
+ links,
619
+ layers: data.layers ? data.layers : [],
620
+ tileMatrixSetLimits: data.tileMatrixSetLimits,
621
+ boundingBox: data.boundingBox && Array.isArray(data.boundingBox.lowerLeft) && Array.isArray(data.boundingBox.upperRight) ? data.boundingBox : undefined,
622
+ };
623
+ }
624
+
625
+ /**
626
+ * Fetches the definition of a tile matrix set, by its JSON link as the server gives it: with the query that tells
627
+ * the server to answer JSON (f=json), if it has one.
628
+ */
629
+ public static async fetchTileset(
630
+ capabilities: OgcOpenApiCapabilitiesObject | null,
631
+ tileset: TileSetMeta,
632
+ options?: {credentials?: boolean, requestHeaders?: RequestHeaders}
633
+ ): Promise<TileSetData> {
634
+ if (capabilities === null) throw {error: true, message: "No capabilities"};
635
+ const jsonLink = tileset.links.find(l => l.type === "application/json") || tileset.links.find(l => !l.type) || tileset.links[0];
636
+ if (!jsonLink) throw {error: true, message: "Tile matrix set " + tileset.id + " has no link"};
637
+ const data: TileSetData = await OgcOpenApiGetCapabilities.fetchJson(capabilities, jsonLink.href, options);
638
+ if (typeof data.id === "undefined" || !data.tileMatrices || !data.crs) throw {error: true, message: "TileSet has invalid data"};
639
+ return data;
640
+ }
641
+ }