UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

292 lines (291 loc) 13.3 kB
import { GeoJSON } from 'ol/format'; import OgcApiClient from './ogcapiclient'; import ServerOgcApiFeatures from '../../models/serverogcapifeatures'; import { transformExtent } from 'ol/proj'; import { OgcApiDefaultEncoding } from '../../models/serverogcapi'; import { serverConformsTo } from './ogcapiconformance'; import { reprojectGeometry } from '../utils/olutils'; // Default coordinate reference system used by OAPIF and GeoJson: WGS84 in lon / lat const default_crs = 'EPSG:4326'; /** * Class representing an OGC API Features (OAPIF) client that interacts with a server implementing * the OGC API - Features standard. This class provides functionality to fetch, manage, and * manipulate collections and their spatial data from the server. * * Extends `OgcApiClient<ServerOgcApiFeatures>`: Provides base client functionalities. */ export default class OgcApiFeaturesClient extends OgcApiClient { constructor(serverConfig) { super(serverConfig); Object.defineProperty(this, "collections", { enumerable: true, configurable: true, writable: true, value: {} }); } getUrl() { return this.serverConfig.urlOapif ?? ''; } async loadDescribeServer() { const server = new ServerOgcApiFeatures(this.url); const conformance = await this.loadConformance(); server.conformsTo = serverConformsTo(conformance); return server; } async getCollections() { throw new Error('Not implemented yet'); } /** * Retrieves a collection by its ID. If the collection is not already cached, it fetches it from the remote resource, * sanitizes its links, and stores it locally. */ async getCollection(collectionId) { if (collectionId in this.collections) { return this.collections[collectionId]; } const url = `${this.url}/collections/${collectionId}?f=json`; const response = await fetch(url, this.getFetchOptions()); if (!response?.ok) { throw new Error(`Failed to get collection: ${response?.status} - ${response?.statusText}`); } const collection = await response.json(); // Sanitize and save to store collection.links = this.sanitizeLinks(url, collection.links); this.collections[collectionId] = collection; return collection; } /** * Retrieves the schema associated with a specified collection. */ async getSchema(collectionId) { const server = await this.getServer(); if (!server.conformsTo?.ogcapi_features_simple_write) { throw new Error('OGC API Features client does not support schemas'); } const relation = 'http://www.opengis.net/def/rel/ogc/1.0/schema'; // NOSONAR return this.getCollectionRelation(collectionId, relation, 'application/schema+json'); } async getQueryables(collectionId) { const server = await this.getServer(); if (!server.conformsTo?.ogcapi_features_query) { throw new Error('OGC API Features client does not support querying'); } const relation = 'http://www.opengis.net/def/rel/ogc/1.0/queryables'; // NOSONAR return this.getCollectionRelation(collectionId, relation, 'application/schema+json'); } /** * Retrieves the related resource for a specified collection and relation type (e.g. `schema`, `self`, `parent`, etc.) */ async getCollectionRelation(collectionId, relation, encodingType = OgcApiDefaultEncoding) { const collection = await this.getCollection(collectionId); const link = collection.links.find((link) => link.rel === relation && (!('type' in link) || link.type === encodingType))?.href; if (!link) { throw new Error(`OGC API collection '${collectionId}' does not support '${relation}'}`); } const response = await fetch(link, this.getFetchOptions()); if (!response?.ok) { throw new Error(`Failed to get ${relation}: ${response?.status} - ${response?.statusText}`); } if (encodingType.includes('json')) { return await response.json(); } else { return response; } } /** * Retrieves items from a specified collection, optionally applying filters like CRS (Coordinate Reference System), * spatial extent, and limit. */ async getItems(collectionId, crsCode, extent, limit) { const url = new URL(`${this.url}/collections/${collectionId}/items?f=json`); let crsIdentifier; if (crsCode) { crsIdentifier = await this.getCrsIdentifier(collectionId, crsCode); if (crsIdentifier) { url.searchParams.append('crs', crsIdentifier); } } if (extent) { extent = await this.reprojectExtentToSupportedCrs(collectionId, extent, crsCode); url.searchParams.append('bbox', extent.join(',')); if (crsIdentifier) { url.searchParams.append('bbox-crs', crsIdentifier); } } // If no limit is set, set a very high limit. This is to overwrite very low default limits that lead to many // requests and long waiting times. If the server has a lower limit, it will enforce it independent of this limit. limit = limit ?? 9999; url.searchParams.append('limit', limit.toString()); const responses = await this.fetchAll(url, this.getFetchOptions()); const items = []; const geoJsonReader = new GeoJSON(); responses.forEach((response) => { items.push(...geoJsonReader.readFeatures(response, {})); }); return items; } /** * Retrieves a single item from the specified collection by its ID. */ async getItem(collectionId, itemId, crsCode) { const url = new URL(`${this.url}/collections/${collectionId}/items/${itemId}?f=json`); // Specify the request CRS if (crsCode) { const crsIdentifier = await this.getCrsIdentifier(collectionId, crsCode); if (crsIdentifier) { url.searchParams.append('crs', crsIdentifier); } } const response = await fetch(url, this.getFetchOptions()); if (!response.ok) { throw new Error(`Failed to get item: ${response.status} - ${response.statusText}`); } const responseJson = await response.json(); let createdItem = new GeoJSON().readFeature(responseJson); if (Array.isArray(createdItem)) { createdItem = createdItem[0]; } return createdItem; } /** * Creates an item in the specified collection with the specified geometry and CRS code. * Geometries are reprojected if the server does not support the items current CRS. */ async createItem(collectionId, item, crsCode) { const url = `${this.url}/collections/${collectionId}/items`; // Reproject geometry to the collection storage CRS if necessary item = await this.reprojectItemToStorageCrs(collectionId, item, crsCode); // Add CRS header information const crsIdentifier = await this.getStorageCrs(collectionId); const fetchOptions = this.getFetchOptions('POST', crsIdentifier); const geoJson = new GeoJSON().writeFeatureObject(item); fetchOptions.body = JSON.stringify(geoJson); const response = await fetch(url, fetchOptions); if (!response.ok) { throw new Error(`Failed to create feature: ${response.status} - ${response.statusText}`); } return true; } /** * Updates an existing item in the specified collection. */ async updateItem(collectionId, itemId, item, crsCode) { const url = `${this.url}/collections/${collectionId}/items/${itemId}`; // Reproject geometry to the collection storage CRS if necessary item = await this.reprojectItemToStorageCrs(collectionId, item, crsCode); // Add CRS header information const crsIdentifier = await this.getStorageCrs(collectionId); const fetchOptions = this.getFetchOptions('PUT', crsIdentifier); const geoJson = new GeoJSON().writeFeatureObject(item); fetchOptions.body = JSON.stringify(geoJson); const response = await fetch(url, fetchOptions); if (!response.ok) { throw new Error(`Failed to update feature: ${response.status} - ${response.statusText}`); } return true; } /** * Deletes an item from a specific collection. */ async deleteItem(collectionId, itemId) { const url = `${this.url}/collections/${collectionId}/items/${itemId}`; const fetchOptions = this.getFetchOptions('DELETE'); const response = await fetch(url, fetchOptions); if (!response.ok) { throw new Error(`Failed to delete feature: ${response.status} - ${response.statusText}`); } return true; } /** * Retrieves the CRS (Coordinate Reference System) identifier for a specific collection based on the provided CRS code. * This is necessary because CRS identifiers can vary slightly (e.g. http vs https) between servers, * and it's best to use the identifiers provided by the server. */ async getCrsIdentifier(collectionId, crsCode) { crsCode = crsCode.replace('EPSG:', ''); // Check the supported CRS for the current collection const collection = await this.getCollection(collectionId); // If crs code is WGS84, search for default crs with identifier `CRS84` if (crsCode === '4326') { return collection.crs?.find((crs) => crs.split('/').includes('CRS84')); } else { return collection.crs?.find((crs) => crs.split('/').includes(crsCode)); } } getEpsgCodeFromCrsIdentifier(crsIdentifier) { if (crsIdentifier.includes('CRS84')) { return 'EPSG:4326'; } else { return `EPSG:${crsIdentifier.trim().split('/').reverse()[0] ?? 4326}`; } } /** * Checks if the provided extent's coordinate reference system (CRS) is supported by a given collection * and reprojects the extent if necessary. */ async reprojectExtentToSupportedCrs(collectionId, extent, crsCode = default_crs) { const crsIdentifier = await this.getCrsIdentifier(collectionId, crsCode); if (!crsIdentifier) { // If the current extent's CRS isn't supported, we reproject to the default CRS WGS84 extent = transformExtent(extent, this.state.projection, default_crs); } return extent; } /** * Retrieves the storage coordinate reference system (CRS) for a specified collection. * The storageCrs information is only available if the server supports part 2 of the OGC API Features specification. */ async getStorageCrs(collectionId) { // Check the supported CRS for the current collection const collection = await this.getCollection(collectionId); let storageCrs; if (collection.storageCrs) { storageCrs = collection.storageCrs; } else { storageCrs = await this.getCrsIdentifier(collectionId, default_crs); } if (!storageCrs) { throw new Error('Not able to save feature: The OAPIF server does not specify a storage coordinate reference system (crs) ' + 'and also does not support the default WGS84.'); } return storageCrs; } /** * When performing a write operation, reproject the feature geometry to match the storage coordinate reference system * (CRS) of the server. If no storage CRS is specified, geometries are reprojected to WGS84 lon / lat, * which is the default CRS for OAPIF and GeoJson. */ async reprojectItemToStorageCrs(collectionId, item, itemCrsCode) { // if no storage crs is specified, defaults to default_crs const storageCrs = await this.getStorageCrs(collectionId); const storageEpsgCode = this.getEpsgCodeFromCrsIdentifier(storageCrs); // If the current crs doesn't fit the storage coordinate system, reproject the item geometry const geometry = item.getGeometry(); if (itemCrsCode !== storageEpsgCode && geometry) { const reprojectedGeometry = reprojectGeometry(geometry, itemCrsCode, storageEpsgCode); item.setGeometry(reprojectedGeometry); } return item; } /** * Expands the method from OgcApiClient by customizing headers to support GeoJson content and optional CRS identifier. */ getFetchOptions(method = 'GET', crsIdentifier) { const fetchOptions = super.getFetchOptions(method); // Add header info about the request body if (['PUT', 'POST'].includes(method)) { const headers = new Headers(fetchOptions?.headers); headers.set('Content-Type', 'application/geo+json'); if (crsIdentifier) { headers.set('Content-Crs', crsIdentifier); } fetchOptions.headers = headers; } return fetchOptions; } }