@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
180 lines (179 loc) • 7.28 kB
JavaScript
import StateManager from '../state/statemanager';
import { OgcApiDefaultEncoding } from '../../models/serverogcapi';
/**
* This class covers the common part of the OGC API standard described in https://docs.ogc.org/is/19-072/19-072.html.
* It contains shared methods between clients of different OGC API standards.
* Any OGC API (Maps, Records, Features, STAC...) shall extend this class.
*/
export default class OgcApiClient {
get state() {
return this.stateManager.state;
}
get url() {
return this.getUrl();
}
constructor(serverConfig) {
Object.defineProperty(this, "serverConfig", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "server", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "stateManager", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "initialized", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.serverConfig = serverConfig;
this.stateManager = StateManager.getInstance();
this.initialized = false;
}
async getServer() {
return this.describeServer();
}
async describeServer() {
if (!this.server) {
this.server = this.loadDescribeServer().then((server) => {
this.initialized = true;
return server;
});
this.server.catch((error) => {
console.error(`OGC API client server with URL ${this.url} could not be initialized. Error:`, error);
return undefined;
});
}
return this.server;
}
/**
* Load the list of conformance classes, which describe what kind of functionality the server provides.
*/
async loadConformance() {
const response = await fetch(`${this.url}/conformance?f=json`, this.getFetchOptions());
if (!response?.ok) {
throw new Error(`Failed to get conformance: ${response?.status} - ${response?.statusText}`);
}
const conformance = await response.json();
return conformance.conformsTo ?? [];
}
/**
* Fetches and retrieves links from the specified path or URL and returns them in a sanitized format.
*/
async getLinks(path) {
const response = await fetch(path, this.getFetchOptions());
if (!response.ok) {
throw new Error(`Failed to get links: ${response.status} - ${response.statusText}`);
}
const jsonResponse = await response.json();
return this.sanitizeLinks(path, jsonResponse.links);
}
/**
* Retrieves a specific link from the OGC API resource based on the given relation and encoding type.
*/
async getLink(relation, path = this.url, encodingType = OgcApiDefaultEncoding) {
const links = await this.getLinks(path);
const link = links.find((link) => link.rel === relation && (!('type' in link) || link.type === encodingType))?.href;
if (!link) {
throw new Error(`OGC API resource at '${path}' does not support '${relation}'}`);
}
return link;
}
/**
* Fetches all paginated resources from the specified URL until the entire dataset is retrieved.
*/
async fetchAll(url, fetchOptions) {
const responses = [];
let featureCount = 0;
while (url) {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`Failed to fetch all: ${response.status} - ${response.statusText}`);
}
const responseJson = await response.json();
responses.push(responseJson);
// Check if a request limit has been reached
const limit = new URL(url).searchParams.get('limit');
featureCount += responseJson.features?.length ?? 0;
if (limit && featureCount >= Number(limit)) {
url = '';
}
else {
// Get next page URL if it exists
const links = this.sanitizeLinks(url, responseJson.links);
url = links?.find((link) => link.rel === 'next')?.href ?? '';
}
}
return responses;
}
getFetchOptions(method = 'GET') {
const fetchOptions = {
method: method
};
fetchOptions.headers = new Headers();
return fetchOptions;
}
/**
* Sanitizes a list of links by ensuring that their URLs are consistent with the original request URL.
* Sanitizes if the original URL does not match the link in the `self` href.
* Limitation: Only works if the original URL and the faulty link share at least one common URL path element, e.g.:
* '/api/' in (original) https://example.com/api/ and (faulty) https://12345-example.com:8080/mapserv_proxy/api/
*
* @param {string} originalPath - The URL of the original request.
* @param {OgcApiLinksResponse[] | undefined} links - The array of links to sanitize.
* @return {OgcApiLinksResponse[]} The sanitized list of links.
*/
sanitizeLinks(originalPath, links) {
if (!links || links.length === 0) {
return [];
}
const selfPath = links.find((link) => link.rel === 'self')?.href;
if (!selfPath) {
console.warn('OGC API client: Cannot sanitize links, list does not contain the `self` relation');
return links;
}
if (!this.areIdenticalUrls(originalPath, selfPath)) {
const origUrlAsStr = originalPath.toString();
links = links.map((link) => {
const commonPath = this.findFirstCommonPath(originalPath, link.href);
if (commonPath) {
const baseUrl = origUrlAsStr.slice(0, origUrlAsStr.indexOf(commonPath));
const pathAndParams = link.href.slice(link.href.indexOf(commonPath));
link.href = baseUrl + pathAndParams;
}
return link;
});
}
return links;
}
/**
* Compares two URLs to determine if they are identical, ignoring URL parameters.
*/
areIdenticalUrls(url1, url2) {
url1 = typeof url1 === 'string' ? new URL(url1) : url1;
url2 = typeof url2 === 'string' ? new URL(url2) : url2;
return url1.host === url2.host && url1.pathname === url2.pathname;
}
/**
* Finds the first common path segment between two given URLs.
*/
findFirstCommonPath(url1, url2) {
url1 = typeof url1 === 'string' ? new URL(url1) : url1;
url2 = typeof url2 === 'string' ? new URL(url2) : url2;
const path1 = url1.pathname.split('/').filter(Boolean);
const path2 = url2.pathname.split('/').filter(Boolean);
const commonPath = path1.find((segment) => path2.includes(segment));
return commonPath || null;
}
}