ogcopenapis 1.1.0 → 1.2.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/CHANGELOG.md +51 -1
- package/README.md +74 -5
- package/lib/OgcOpenApiFeatureStore.d.ts +10 -0
- package/lib/OgcOpenApiFeatureStore.d.ts.map +1 -1
- package/lib/OgcOpenApiFeatureStore.js +55 -5
- package/lib/OgcOpenApiFeatureStore.js.map +1 -1
- package/lib/OgcOpenApiGetCapabilities.d.ts +45 -1
- package/lib/OgcOpenApiGetCapabilities.d.ts.map +1 -1
- package/lib/OgcOpenApiGetCapabilities.js +175 -132
- package/lib/OgcOpenApiGetCapabilities.js.map +1 -1
- package/lib/OgcOpenApiMapsModel.d.ts.map +1 -1
- package/lib/OgcOpenApiMapsModel.js.map +1 -1
- package/lib/OgcOpenApiTilesModel.d.ts.map +1 -1
- package/lib/OgcOpenApiTilesModel.js.map +1 -1
- package/package.json +1 -1
- package/src/OgcOpenApiFeatureStore.ts +68 -5
- package/src/OgcOpenApiGetCapabilities.ts +197 -157
- package/src/OgcOpenApiMapsModel.ts +2 -0
- package/src/OgcOpenApiTilesModel.ts +2 -1
|
@@ -25,6 +25,22 @@ export interface OgcOpenApiFeatureStoreConstructorOptions {
|
|
|
25
25
|
extent?: { spatial: { bbox: any[] } };
|
|
26
26
|
crs: string;
|
|
27
27
|
proxify?: ProxifyFunction
|
|
28
|
+
// Called once for each query that succeeds, with what the response says about its size
|
|
29
|
+
onQueryResult?: (result: OgcOpenApiFeatureQueryResult) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// What the response to a query says about its size (OGC API Features Part 1)
|
|
33
|
+
export interface OgcOpenApiFeatureQueryResult {
|
|
34
|
+
// The request, and the query it was made from
|
|
35
|
+
url: string;
|
|
36
|
+
query: OgcOpenApiFeatureStoreQueryOptions;
|
|
37
|
+
// The number of features that match the query, left out when the server gives none (it is optional, PDOK)
|
|
38
|
+
numberMatched?: number;
|
|
39
|
+
// The number of features in the response. Servers cap limit: it can be less than limit (pygeoapi gives 10)
|
|
40
|
+
numberReturned?: number;
|
|
41
|
+
// The response links a next page. Left out when the response has no links to tell: a GML response whose Link
|
|
42
|
+
// header the browser may not read (CORS)
|
|
43
|
+
hasNext?: boolean;
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
export interface OgcOpenApiFeatureStoreQueryOptions {
|
|
@@ -86,11 +102,13 @@ export class OgcOpenApiFeatureStore extends Store {
|
|
|
86
102
|
private credentials: boolean;
|
|
87
103
|
private serverOptions: { proxy: (url: string, fetchOptions) => { fetchOptions: any; url: string }; hostname: string; complete: (url: string) => string };
|
|
88
104
|
private swapAxes: string[] | undefined;
|
|
105
|
+
private onQueryResult: ((result: OgcOpenApiFeatureQueryResult) => void) | undefined;
|
|
89
106
|
|
|
90
107
|
constructor(options: OgcOpenApiFeatureStoreConstructorOptions) {
|
|
91
108
|
super();
|
|
92
109
|
// console.log(options);
|
|
93
110
|
this.swapAxes = options.swapAxes;
|
|
111
|
+
this.onQueryResult = options.onQueryResult;
|
|
94
112
|
this.useCrs84Bounds = typeof options.useCrs84Bounds !== "undefined" ? options.useCrs84Bounds : false;
|
|
95
113
|
this.dataUrl = options.dataUrl;
|
|
96
114
|
this.outputFormat = options.outputFormat;
|
|
@@ -152,7 +170,7 @@ export class OgcOpenApiFeatureStore extends Store {
|
|
|
152
170
|
query.f = query.f ? query.f : this.dataFormat;
|
|
153
171
|
query.crs = query.crs ? query.crs : this.customCrs;
|
|
154
172
|
query.limit = query.limit ? query.limit : undefined;
|
|
155
|
-
return this.fetchFeatures(this.withQuery(this.baseUrl, query), options?.abortSignal);
|
|
173
|
+
return this.fetchFeatures(this.withQuery(this.baseUrl, query), options?.abortSignal, query);
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
// The parameters of the items link, then those of the request, which replace a link parameter of the same name.
|
|
@@ -169,7 +187,8 @@ export class OgcOpenApiFeatureStore extends Store {
|
|
|
169
187
|
|
|
170
188
|
// Fetches and decodes features. The promise always settles: it rejects with the reason when the request,
|
|
171
189
|
// the response or the decoding fails, and when LuciadRIA aborts the request through abortSignal.
|
|
172
|
-
|
|
190
|
+
// A query is reported to onQueryResult once its features are decoded, unless it was aborted
|
|
191
|
+
private fetchFeatures(request: string, abortSignal?: AbortSignal, query?: OgcOpenApiFeatureStoreQueryOptions): Promise<Cursor> {
|
|
173
192
|
// Mod starta
|
|
174
193
|
const itemsAccept =
|
|
175
194
|
typeof this.requestHeaders.accept !== 'undefined'
|
|
@@ -203,10 +222,13 @@ export class OgcOpenApiFeatureStore extends Store {
|
|
|
203
222
|
const codecOptions: OgcOpenApiFeaturesCodecDecodeOptions = {
|
|
204
223
|
content, contentType, contentCrs, contentLength
|
|
205
224
|
};
|
|
206
|
-
|
|
207
|
-
|
|
225
|
+
const cursor = this.codec instanceof GeoJsonCodec || this.codec instanceof GMLCodec ?
|
|
226
|
+
this.codec.decode({...codecOptions, reference: getReference(OgcOpenApiCrsTools.getReferenceName(contentCrs))}) :
|
|
227
|
+
this.codec.decode(codecOptions);
|
|
228
|
+
if (query && this.onQueryResult && !abortSignal?.aborted) {
|
|
229
|
+
this.onQueryResult({url: request, query, ...OgcOpenApiFeatureStore.getResultSize(content, contentType, response.headers.get('Link'))});
|
|
208
230
|
}
|
|
209
|
-
return
|
|
231
|
+
return cursor;
|
|
210
232
|
});
|
|
211
233
|
});
|
|
212
234
|
}
|
|
@@ -251,6 +273,47 @@ export class OgcOpenApiFeatureStore extends Store {
|
|
|
251
273
|
return this.query(query, options);
|
|
252
274
|
}
|
|
253
275
|
|
|
276
|
+
// The size of a response: the members of a JSON response, or the attributes of the root element of GML, and the
|
|
277
|
+
// next link of the response or of its Link header
|
|
278
|
+
private static getResultSize(content: string, contentType: string, linkHeader: string | null) {
|
|
279
|
+
const size: { numberMatched?: number, numberReturned?: number, hasNext?: boolean } = {};
|
|
280
|
+
const count = (value: any) => {
|
|
281
|
+
const n = typeof value === "string" ? (/^\d+$/.test(value) ? Number(value) : undefined) : value;
|
|
282
|
+
return typeof n === "number" && isFinite(n) && n >= 0 ? n : undefined;
|
|
283
|
+
};
|
|
284
|
+
if (/json/i.test(contentType)) {
|
|
285
|
+
let data: any;
|
|
286
|
+
try {
|
|
287
|
+
data = JSON.parse(content);
|
|
288
|
+
} catch (e) {
|
|
289
|
+
data = undefined;
|
|
290
|
+
}
|
|
291
|
+
if (data && typeof data === "object") {
|
|
292
|
+
size.numberMatched = count(data.numberMatched);
|
|
293
|
+
size.numberReturned = count(data.numberReturned);
|
|
294
|
+
if (Array.isArray(data.links)) size.hasNext = data.links.some(l => l && l.rel === "next");
|
|
295
|
+
}
|
|
296
|
+
} else if (/xml/i.test(contentType)) {
|
|
297
|
+
// WFS 2.0 allows numberMatched="unknown"
|
|
298
|
+
const root = /<(?:[\w.-]+:)?FeatureCollection\b([^>]*)>/.exec(content);
|
|
299
|
+
const attribute = (name: string) => {
|
|
300
|
+
const match = root ? new RegExp("\\s" + name + "=[\"']([^\"']*)[\"']").exec(root[1]) : null;
|
|
301
|
+
return match ? count(match[1]) : undefined;
|
|
302
|
+
};
|
|
303
|
+
size.numberMatched = attribute("numberMatched");
|
|
304
|
+
size.numberReturned = attribute("numberReturned");
|
|
305
|
+
}
|
|
306
|
+
if (linkHeader) {
|
|
307
|
+
const next = linkHeader.split(/,\s*(?=<)/).some(link => {
|
|
308
|
+
const rel = /;\s*rel\s*=\s*"?([^";]*)"?/i.exec(link);
|
|
309
|
+
return rel !== null && rel[1].split(/\s+/).indexOf("next") > -1;
|
|
310
|
+
});
|
|
311
|
+
size.hasNext = size.hasNext || next;
|
|
312
|
+
}
|
|
313
|
+
for (const key of Object.keys(size)) if (typeof size[key] === "undefined") delete size[key];
|
|
314
|
+
return size;
|
|
315
|
+
}
|
|
316
|
+
|
|
254
317
|
private static getCRS84BoundingBox(shape: Bounds) {
|
|
255
318
|
const WGS84 = getReference("CRS:84");
|
|
256
319
|
const bounds = shape.bounds;
|
|
@@ -22,6 +22,36 @@ export const COLLECTIONS_SELECTION_CONFORMANCE = "http://www.opengis.net/spec/og
|
|
|
22
22
|
// collections parameter as the standard requires. To be removed once ldproxy declares the class of the standard
|
|
23
23
|
export const COLLECTIONS_SELECTION_CONFORMANCE_VARIANTS = ["http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/collection-selection"];
|
|
24
24
|
|
|
25
|
+
// The conformance class of OGC API Features Part 3 for the filter parameter of the items of a collection
|
|
26
|
+
export const FEATURES_FILTER_CONFORMANCE = "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter";
|
|
27
|
+
// The conformance classes of CQL2 1.0 (OGC 21-065) share this prefix, the encodings and the levels of the language
|
|
28
|
+
export const CQL2_CONFORMANCE_PREFIX = "http://www.opengis.net/spec/cql2/1.0/conf/";
|
|
29
|
+
// The CQL2 encodings, as conformance classes name them. They are also the values of the filter-lang parameter
|
|
30
|
+
export const CQL2_ENCODINGS = ["cql2-text", "cql2-json"];
|
|
31
|
+
|
|
32
|
+
export interface OgcOpenApiFilterSupport {
|
|
33
|
+
// The server declares the Features Filter class of OGC API Features Part 3: the items of its collections take a filter
|
|
34
|
+
filter: boolean;
|
|
35
|
+
// The CQL2 encodings the server declares, cql2-text or cql2-json
|
|
36
|
+
encodings: string[];
|
|
37
|
+
// The other CQL2 classes the server declares, such as basic-cql2, advanced-comparison-operators or spatial-functions
|
|
38
|
+
levels: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The queryables of a collection (OGC API Features Part 3): a JSON Schema, as the server sends it. Each property has
|
|
42
|
+
// a JSON Schema of its own, with type, format, title, enum or x-ogc-role among others
|
|
43
|
+
export interface OgcOpenApiQueryables {
|
|
44
|
+
$schema?: string;
|
|
45
|
+
$id?: string;
|
|
46
|
+
type?: string;
|
|
47
|
+
title?: string;
|
|
48
|
+
properties?: { [name: string]: { [keyword: string]: any } };
|
|
49
|
+
[keyword: string]: any;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The media types of a queryables document, in the order they are preferred
|
|
53
|
+
const QUERYABLES_MEDIA_TYPES = ["application/schema+json", "application/json"];
|
|
54
|
+
|
|
25
55
|
export interface TileMatrix {
|
|
26
56
|
id: string,
|
|
27
57
|
scaleDenominator: number;
|
|
@@ -187,155 +217,97 @@ export interface OgcOpenApiGetCapabilitiesFromURL {
|
|
|
187
217
|
|
|
188
218
|
export class OgcOpenApiGetCapabilities {
|
|
189
219
|
|
|
190
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Reads the landing page of a server, its collections, the API document and the conformance classes, asking for
|
|
222
|
+
* JSON. Rejects with an Error when the landing page or the collections cannot be read, and with the error of the
|
|
223
|
+
* request as it is when it fails, an AbortError included. The API document and the conformance classes are
|
|
224
|
+
* optional: without them the capabilities have no version and declare no conformance classes.
|
|
225
|
+
*/
|
|
226
|
+
public static async fromURL(inputRequest: string, fromUrlOptions?: OgcOpenApiGetCapabilitiesFromURL): Promise<OgcOpenApiCapabilitiesObject> {
|
|
191
227
|
const options: OgcOpenApiGetCapabilitiesFromURL = fromUrlOptions ? fromUrlOptions : {};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
: collection.name;
|
|
268
|
-
const collectionCrsObject = {} as any;
|
|
269
|
-
if (typeof collection.crs !== "undefined") collectionCrsObject.crs = collection.crs;
|
|
270
|
-
if (typeof collection.storageCrs !== "undefined") collectionCrsObject.storageCrs = collection.storageCrs;
|
|
271
|
-
const layer: OgcOpenApiCapabilitiesCollection = {
|
|
272
|
-
description: collection.description,
|
|
273
|
-
id: name,
|
|
274
|
-
name,
|
|
275
|
-
title: collection.title,
|
|
276
|
-
keywords: collection.keywords
|
|
277
|
-
? collection.keywords
|
|
278
|
-
: [],
|
|
279
|
-
defaultReference: 'CRS:84',
|
|
280
|
-
links: collection.links,
|
|
281
|
-
outputFormats: options.filterCollectionsByLinkType ?
|
|
282
|
-
this.filterCollectionLinks(collection.links, options.filterCollectionsByLinkType).map(link => this.detectTypeFormat(link))
|
|
283
|
-
:
|
|
284
|
-
collection.links.map(link => this.detectTypeFormat(link))
|
|
285
|
-
,
|
|
286
|
-
extent: collection.extent,
|
|
287
|
-
...collectionCrsObject
|
|
288
|
-
};
|
|
289
|
-
return layer;
|
|
290
|
-
}
|
|
291
|
-
);
|
|
292
|
-
// console.log(featureTypes);
|
|
293
|
-
const o: OgcOpenApiCapabilitiesObject = {
|
|
294
|
-
name: '',
|
|
295
|
-
collections: foundCollections.map(col=>({...col, crs: col.crs ? col.crs : [], links: col.links.map(l => resolveLink(l, collectionsLink.href))})),
|
|
296
|
-
version: responseOpenApi ? responseOpenApi.openapi : '',
|
|
297
|
-
service: '',
|
|
298
|
-
info: responseOpenApi ? responseOpenApi.info : {},
|
|
299
|
-
...crsArray,
|
|
300
|
-
serverOptions: serverOptions,
|
|
301
|
-
links: jsonObject.links.map(l => resolveLink(l, requestUrl)),
|
|
302
|
-
baseUrl,
|
|
303
|
-
tileMatrixSetsUrl: linkToTileMatrixSets ? resolveLink(linkToTileMatrixSets, requestUrl).href : undefined
|
|
304
|
-
};
|
|
305
|
-
conformancePromise.then(conformsTo => resolve({...o, conformsTo}));
|
|
306
|
-
});
|
|
307
|
-
} else {
|
|
308
|
-
reject("Invalid format: (property 'links' contains no links with rel=data");
|
|
309
|
-
}
|
|
310
|
-
}, ()=>{
|
|
311
|
-
reject("Not JSON")
|
|
312
|
-
});
|
|
313
|
-
} else {
|
|
314
|
-
reject(response.status);
|
|
315
|
-
}
|
|
316
|
-
},
|
|
317
|
-
(err) => {
|
|
318
|
-
reject(err);
|
|
319
|
-
}
|
|
320
|
-
);
|
|
321
|
-
});
|
|
228
|
+
// The landing page, with the query it was given, such as an api key. baseUrl is without it
|
|
229
|
+
const baseUrl = OgcOpenApiFetchTools.cleanUrl(inputRequest);
|
|
230
|
+
const requestUrl = OgcOpenApiFetchTools.appendPath(inputRequest, "");
|
|
231
|
+
const serverOptions = OgcOpenApiFetchTools.createFetchOptions({originalUrl: requestUrl, proxify: options.proxify});
|
|
232
|
+
// The requests before the capabilities exist
|
|
233
|
+
const server = {serverOptions} as OgcOpenApiCapabilitiesObject;
|
|
234
|
+
const getJson = (href: string, accept?: string) =>
|
|
235
|
+
OgcOpenApiGetCapabilities.fetchJson(server, href, options, accept).catch(e => {
|
|
236
|
+
throw OgcOpenApiGetCapabilities.toError(e, href);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const landing = await getJson(requestUrl);
|
|
240
|
+
if (!landing || !Array.isArray(landing.links)) throw new Error("Invalid format: (property 'links' is missing) in " + requestUrl);
|
|
241
|
+
const links: OgcOpenApiCapabilitiesCollectionServiceLinkType[] = landing.links;
|
|
242
|
+
const linkToData = links.find(link => link.rel === 'data' && link.type === 'application/json') || links.find(link => link.rel === 'data');
|
|
243
|
+
if (!linkToData) throw new Error("Invalid format: (property 'links' contains no links with rel=data) in " + requestUrl);
|
|
244
|
+
const linkToApi = links.find(link => (link.rel === 'service-desc' || link.rel === 'service') && (link.type || "").indexOf('openapi+json') > -1);
|
|
245
|
+
const tilingSchemes = links.filter(link => isRel(link, 'tiling-schemes'));
|
|
246
|
+
const linkToTileMatrixSets = tilingSchemes.find(link => link.type === 'application/json') || tilingSchemes.find(link => !link.type);
|
|
247
|
+
const conformance = links.find(link => isRel(link, 'conformance'));
|
|
248
|
+
// The links of the landing page are relative to it, the links of the collections to /collections
|
|
249
|
+
const resolveLink = (link: OgcOpenApiCapabilitiesCollectionServiceLinkType, documentUrl: string) =>
|
|
250
|
+
({...link, href: OgcOpenApiFetchTools.resolveUrl(link.href, documentUrl, serverOptions)});
|
|
251
|
+
const collectionsLink = resolveLink(linkToData, requestUrl);
|
|
252
|
+
|
|
253
|
+
// The collections are required, the API document (in its own media type) and the conformance classes are not
|
|
254
|
+
const [collectionsDocument, api, conformsTo] = await Promise.all([
|
|
255
|
+
getJson(collectionsLink.href),
|
|
256
|
+
linkToApi ? getJson(resolveLink(linkToApi, requestUrl).href, linkToApi.type).catch(() => undefined) : Promise.resolve(undefined),
|
|
257
|
+
conformance ?
|
|
258
|
+
getJson(resolveLink(conformance, requestUrl).href).then(data => Array.isArray(data.conformsTo) ? data.conformsTo as string[] : [], () => [] as string[]) :
|
|
259
|
+
Promise.resolve([] as string[]),
|
|
260
|
+
]);
|
|
261
|
+
if (!collectionsDocument || !Array.isArray(collectionsDocument.collections)) {
|
|
262
|
+
throw new Error("Invalid format: (property 'collections' is missing) in " + collectionsLink.href);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const crsArray = typeof collectionsDocument.crs !== "undefined" ? {crs: collectionsDocument.crs} : {};
|
|
266
|
+
const foundCollections = collectionsDocument.collections
|
|
267
|
+
.filter(c => options.filterCollectionsByLinkType ?
|
|
268
|
+
OgcOpenApiGetCapabilities.filterCollectionLinks(c.links, options.filterCollectionsByLinkType).length > 0 : true)
|
|
269
|
+
.map((collection: any) => {
|
|
270
|
+
const name = typeof collection.id !== 'undefined' ? collection.id : collection.name;
|
|
271
|
+
const collectionCrsObject = {} as any;
|
|
272
|
+
if (typeof collection.crs !== "undefined") collectionCrsObject.crs = collection.crs;
|
|
273
|
+
if (typeof collection.storageCrs !== "undefined") collectionCrsObject.storageCrs = collection.storageCrs;
|
|
274
|
+
const layer: OgcOpenApiCapabilitiesCollection = {
|
|
275
|
+
description: collection.description,
|
|
276
|
+
id: name,
|
|
277
|
+
name,
|
|
278
|
+
title: collection.title,
|
|
279
|
+
keywords: collection.keywords ? collection.keywords : [],
|
|
280
|
+
defaultReference: 'CRS:84',
|
|
281
|
+
links: collection.links,
|
|
282
|
+
outputFormats: options.filterCollectionsByLinkType ?
|
|
283
|
+
this.filterCollectionLinks(collection.links, options.filterCollectionsByLinkType).map(link => this.detectTypeFormat(link)) :
|
|
284
|
+
collection.links.map(link => this.detectTypeFormat(link)),
|
|
285
|
+
extent: collection.extent,
|
|
286
|
+
...collectionCrsObject
|
|
287
|
+
};
|
|
288
|
+
return layer;
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
name: '',
|
|
292
|
+
collections: foundCollections.map(col => ({...col, crs: col.crs ? col.crs : [], links: col.links.map(l => resolveLink(l, collectionsLink.href))})),
|
|
293
|
+
version: api ? api.openapi : '',
|
|
294
|
+
service: '',
|
|
295
|
+
info: api ? api.info : {},
|
|
296
|
+
...crsArray,
|
|
297
|
+
serverOptions: serverOptions,
|
|
298
|
+
links: links.map(l => resolveLink(l, requestUrl)),
|
|
299
|
+
baseUrl,
|
|
300
|
+
tileMatrixSetsUrl: linkToTileMatrixSets ? resolveLink(linkToTileMatrixSets, requestUrl).href : undefined,
|
|
301
|
+
conformsTo,
|
|
302
|
+
};
|
|
322
303
|
}
|
|
323
304
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
} else {
|
|
331
|
-
resolve(undefined);
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
() => {
|
|
335
|
-
resolve(undefined);
|
|
336
|
-
}
|
|
337
|
-
);
|
|
338
|
-
});
|
|
305
|
+
// An Error for a failed request of fromURL: the error of fetch as it is, such as an AbortError, otherwise its message
|
|
306
|
+
// and the URL. An error of fetch can come from another realm (a DOMException of jsdom is not instanceof Error), so
|
|
307
|
+
// anything with a name and a message is kept; the {error, message} of fetchJson has no name
|
|
308
|
+
private static toError(e: any, href: string): Error {
|
|
309
|
+
if (e instanceof Error || (e && typeof e.name === "string" && typeof e.message === "string")) return e;
|
|
310
|
+
return new Error((e && e.message ? e.message : String(e)) + " for " + href);
|
|
339
311
|
}
|
|
340
312
|
|
|
341
313
|
static filterCollectionLinks(
|
|
@@ -451,17 +423,15 @@ export class OgcOpenApiGetCapabilities {
|
|
|
451
423
|
}
|
|
452
424
|
|
|
453
425
|
// GETs a JSON document. Servers answer JSON for the Accept header, or for the f parameter that their links carry.
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
throw {error: true, message: err.message};
|
|
464
|
-
}
|
|
426
|
+
// accept is the media type to ask for, application/json by default; requestHeaders can replace it. A failed request
|
|
427
|
+
// rejects with the error of fetch as it is, such as an AbortError
|
|
428
|
+
private static async fetchJson(capabilities: OgcOpenApiCapabilitiesObject, href: string, options?: {credentials?: boolean, requestHeaders?: RequestHeaders},
|
|
429
|
+
accept = "application/json") {
|
|
430
|
+
const result = await OgcOpenApiFetchTools.fetch(href, {
|
|
431
|
+
method: "GET",
|
|
432
|
+
credentials: options?.credentials ? "same-origin" : "omit",
|
|
433
|
+
headers: {Accept: accept, ...(options?.requestHeaders ? options.requestHeaders : {})}
|
|
434
|
+
}, capabilities.serverOptions);
|
|
465
435
|
if (result.status !== 200) throw {error: true, message: "Invalid response: " + result.status};
|
|
466
436
|
try {
|
|
467
437
|
return await result.json();
|
|
@@ -510,6 +480,76 @@ export class OgcOpenApiGetCapabilities {
|
|
|
510
480
|
COLLECTIONS_SELECTION_CONFORMANCE_VARIANTS.some(variant => conformsTo.indexOf(variant) > -1);
|
|
511
481
|
}
|
|
512
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Whether, and how, the server filters features, as its conformance classes declare: the Features Filter class of
|
|
485
|
+
* OGC API Features Part 3, and the CQL2 1.0 classes by their names, in the order the server lists them. Each is
|
|
486
|
+
* reported as declared, a CQL2 encoding without the Features Filter class gives filter false. Pre-standard CQL
|
|
487
|
+
* classes (ogcapi-features-3/1.0/conf/cql-text) are not CQL2 and are left out.
|
|
488
|
+
*/
|
|
489
|
+
public static getFilterSupport(capabilities: OgcOpenApiCapabilitiesObject | null): OgcOpenApiFilterSupport {
|
|
490
|
+
const conformsTo = capabilities && capabilities.conformsTo ? capabilities.conformsTo : [];
|
|
491
|
+
const cql2 = conformsTo
|
|
492
|
+
.filter(uri => uri.indexOf(CQL2_CONFORMANCE_PREFIX) === 0)
|
|
493
|
+
.map(uri => uri.substring(CQL2_CONFORMANCE_PREFIX.length));
|
|
494
|
+
return {
|
|
495
|
+
filter: conformsTo.indexOf(FEATURES_FILTER_CONFORMANCE) > -1,
|
|
496
|
+
encodings: cql2.filter(name => CQL2_ENCODINGS.indexOf(name) > -1),
|
|
497
|
+
levels: cql2.filter(name => CQL2_ENCODINGS.indexOf(name) === -1),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Fetches the queryables of a collection (OGC API Features Part 3), the properties a filter can use: a JSON Schema,
|
|
503
|
+
* as the server sends it. Resolves null for a collection that has none: a collection without features, or a
|
|
504
|
+
* queryables request that answers 404. Rejects with an Error otherwise, and with the error of the request as it is
|
|
505
|
+
* when it fails, an AbortError included.
|
|
506
|
+
*/
|
|
507
|
+
public static async fetchQueryables(
|
|
508
|
+
capabilities: OgcOpenApiCapabilitiesObject | null,
|
|
509
|
+
collection: OgcOpenApiCapabilitiesCollection,
|
|
510
|
+
options?: {credentials?: boolean, requestHeaders?: RequestHeaders}
|
|
511
|
+
): Promise<OgcOpenApiQueryables | null> {
|
|
512
|
+
if (capabilities === null) throw new Error("No capabilities to fetch the queryables of " + collection.id);
|
|
513
|
+
const href = OgcOpenApiGetCapabilities.getQueryablesHref(capabilities, collection);
|
|
514
|
+
if (!href) return null;
|
|
515
|
+
let response: Response;
|
|
516
|
+
try {
|
|
517
|
+
response = await OgcOpenApiFetchTools.fetch(href, {
|
|
518
|
+
method: "GET",
|
|
519
|
+
credentials: options?.credentials ? "same-origin" : "omit",
|
|
520
|
+
headers: {Accept: QUERYABLES_MEDIA_TYPES.join(", "), ...(options?.requestHeaders ? options.requestHeaders : {})}
|
|
521
|
+
}, capabilities.serverOptions);
|
|
522
|
+
} catch (e) {
|
|
523
|
+
throw OgcOpenApiGetCapabilities.toError(e, href);
|
|
524
|
+
}
|
|
525
|
+
if (response.status === 404) return null;
|
|
526
|
+
if (response.status !== 200) throw new Error("Invalid response: " + response.status + " for " + href);
|
|
527
|
+
let data: any;
|
|
528
|
+
try {
|
|
529
|
+
data = await response.json();
|
|
530
|
+
} catch (e) {
|
|
531
|
+
throw new Error("Not JSON data for " + href);
|
|
532
|
+
}
|
|
533
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("Invalid format: (not a JSON Schema) in " + href);
|
|
534
|
+
return data;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// The queryables link of the collection, in JSON. Some servers (ldproxy, PDOK) list it only in the collection
|
|
538
|
+
// document, not in /collections: then the path of the standard, /collections/{collectionId}/queryables, for a
|
|
539
|
+
// collection with features. Other collections have no queryables, '' then
|
|
540
|
+
private static getQueryablesHref(capabilities: OgcOpenApiCapabilitiesObject, collection: OgcOpenApiCapabilitiesCollection) {
|
|
541
|
+
const links = collection.links ? collection.links : [];
|
|
542
|
+
const queryables = links.filter(l => isRel(l, "queryables"));
|
|
543
|
+
const link = QUERYABLES_MEDIA_TYPES.map(type => queryables.find(l => l.type === type)).find(l => l) ||
|
|
544
|
+
queryables.find(l => !l.type);
|
|
545
|
+
if (link) return link.href;
|
|
546
|
+
if (OgcOpenApiGetCapabilities.filterCollectionLinks(links, CollectionLinkType.Items).length === 0) return '';
|
|
547
|
+
// The self link keeps its query, such as f=json (PDOK)
|
|
548
|
+
const self = links.find(l => l.rel === "self" && (!l.type || l.type === "application/json"));
|
|
549
|
+
const collectionHref = self ? self.href : capabilities.baseUrl + "collections/" + encodeURIComponent(collection.id);
|
|
550
|
+
return OgcOpenApiFetchTools.appendPath(collectionHref, "queryables");
|
|
551
|
+
}
|
|
552
|
+
|
|
513
553
|
/**
|
|
514
554
|
* Whether the server tiles the dataset as a whole, all its collections in each tile, for linkType Tiles or VectorTiles
|
|
515
555
|
*/
|
|
@@ -21,6 +21,8 @@ interface OgcOpenApiMapsModelConstructorOptions {
|
|
|
21
21
|
samplingMode?: RasterSamplingMode;
|
|
22
22
|
credentials?: boolean;
|
|
23
23
|
requestHeaders?: { [p: string]: string };
|
|
24
|
+
// The f value of the requests, as the server names its formats (pygeoapi: png), not a media type (image/png), unlike
|
|
25
|
+
// the format of OgcOpenApiTilesModel. Best left out: the f value of the map link, in baseURL, is then used
|
|
24
26
|
format?: string;
|
|
25
27
|
reverseAxis?: boolean;
|
|
26
28
|
datetime?: string;
|
|
@@ -31,7 +31,8 @@ export interface OgcOpenApiTilesModelCreateOptions extends OgcOpenApiTilesModelO
|
|
|
31
31
|
tileMatrixSetLimits?: TileMatrixLimits[];
|
|
32
32
|
// The data extent in CRS:84, [minX, minY, maxX, maxY], such as the extent of the collection
|
|
33
33
|
extent?: number[];
|
|
34
|
-
// The media type of the tiles, image/png by default
|
|
34
|
+
// The media type of the tiles, image/png by default, such as the type of the tile link. Unlike the format of
|
|
35
|
+
// OgcOpenApiMapsModel, which is an f value
|
|
35
36
|
format?: string;
|
|
36
37
|
}
|
|
37
38
|
|