UNPKG

@dlr-eoc/services-ogc

Version:

This module bundles our clients for OGC standards. E.g. parse OWS Context JSON, WMS, WMTS or WPS.

1,213 lines (1,204 loc) 79.3 kB
import * as i0 from '@angular/core'; import { Injectable, Inject } from '@angular/core'; import { Filtertypes, WmsLayertype, WmtsLayertype, WfsLayertype, KmlLayertype, GeojsonLayertype, XyzLayertype, TmsLayertype, StackedLayer, LayerGroup, Layer, VectorLayer, RasterLayer, WmtsLayer, WmsLayer } from '@dlr-eoc/services-layers'; import { forkJoin, of, concat } from 'rxjs'; import { map, filter } from 'rxjs/operators'; import { DateTime, Interval } from 'luxon'; import { get } from 'ol/proj'; import * as i1 from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http'; import { Jsonix } from '@michaellangbein/jsonix'; import * as XLink_1_0_Factory from 'w3c-schemas/lib/XLink_1_0'; import * as OWS_1_1_0_Factory from 'ogc-schemas/lib/OWS_1_1_0'; import * as SMIL_2_0_Factory from 'ogc-schemas/lib/SMIL_2_0'; import * as SMIL_2_0_Language_Factory from 'ogc-schemas/lib/SMIL_2_0_Language'; import * as GML_3_1_1_Factory from 'ogc-schemas/lib/GML_3_1_1'; import * as WMTS_1_0_Factory from 'ogc-schemas/lib/WMTS_1_0'; import { WpsClient as WpsClient$1, WmsClient } from '@dlr-eoc/utils-ogc'; export { FakeCache } from '@dlr-eoc/utils-ogc'; const wmsOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/wms'; const wfsOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/wfs'; const wcsOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/wcs'; const wpsOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/wps'; const cswOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/csw'; const wmtsOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/wmts'; const gmlOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/gml'; const kmlOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/kml'; const GeoTIFFOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/geotiff'; const GMLJP2Offering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/gmljp2'; const GMLCOVOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/gmlcov'; /** * http://www.owscontext.org/owc_user_guide/C0_userGuide.html#trueextension-offerings */ const GeoJsonOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/geojson'; const xyzOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/xyz'; const tmsOffering = 'http://www.opengis.net/spec/owc-geojson/1.0/req/tms'; /** This file contains functions (Type Guards) to test for types in owc-json.ts */ /** * export types to create layers from Offerings */ const GetMapOperationCode = 'GetMap'; const GetFeatureOperationCode = 'GetFeature'; const GetTileOperationCode = 'GetTile'; const RESTOperationCode = 'REST'; const GetCapabilitiesOperationCode = 'GetCapabilities'; const DescribeFeatureTypeOperationCode = 'DescribeFeatureType'; const GetFeatureInfoOperationCode = 'GetFeatureInfo'; function trueForAll(list, predicate) { for (const entry of list) { if (!predicate(entry)) { return false; } } return true; } function isIOwsContext(object) { let ISCONTEXT_1_0; if (object?.properties?.links) { ISCONTEXT_1_0 = object.properties.links.profiles.find(item => item.href === 'http://www.opengis.net/spec/owc-geojson/1.0/req/core'); } if (!ISCONTEXT_1_0) { console.error('this is not a valid OWS Context v1.0!'); return false; } else { return true; } } function isIOwsResource(object) { return 'id' in object && 'type' in object && 'properties' in object && isIOwsResourceProperties(object.properties); } function isIOwsResourceProperties(object) { return 'title' in object && 'updated' in object && (object.authors ? trueForAll(object.authors, isIOwsAuthor) : true) && (object.offerings ? trueForAll(object.offerings, isIOwsOffering) : true) && (object.categories ? trueForAll(object.categories, isIOwsCategory) : true); } function isIOwsOffering(object) { return 'code' in object && (object.operations ? trueForAll(object.operations, isIOwsOperation) : true) && (object.contents ? trueForAll(object.contents, isIOwsContent) : true) && (object.styles ? trueForAll(object.styles, isIOwsStyleSet) : true); } function isIOwsGenerator(object) { return 'title' in object || 'uri' in object || 'version' in object; } function isIOwsAuthor(object) { return 'name' in object || 'email' in object || 'uri' in object; } function isIOwsCategory(object) { return 'scheme' in object || 'term' in object || 'label' in object; } function isIOwsLinks(object) { return 'rel' in object; } function isIOwsCreatorDisplay(object) { return 'pixelWidth' in object || 'pixelHeight' in object || 'mmPerPixel' in object; } function isIOwsOperation(object) { return 'code' in object && 'method' in object && (object.request ? isIOwsContent(object.request) : true) && (object.result ? isIOwsContent(object.result) : true); } function isIOwsRasterOperation(object) { if (isIOwsOperation(object)) { return [GetMapOperationCode, GetTileOperationCode, RESTOperationCode].includes(object.code); } else { return false; } } function isIOwsVectorOperation(object) { if (isIOwsOperation(object)) { return [GetFeatureOperationCode].includes(object.code); } else { return false; } } function isIOwsContent(object) { return 'type' in object; } function isIOwsStyleSet(object) { return 'name' in object && 'title' in object; } function isWmsOffering(str) { return str === wmsOffering; } function isWfsOffering(str) { return str === wfsOffering; } function isWpsOffering(str) { return str === wcsOffering; } function isCswOffering(str) { return str === cswOffering; } function isWmtsOffering(str) { return str === wmtsOffering; } function isGmlOffering(str) { return str === gmlOffering; } function isKmlOffering(str) { return str === kmlOffering; } function isGeoTIFFOffering(str) { return str === GeoTIFFOffering; } function isGMLJP2Offering(str) { return str === GMLJP2Offering; } function isGMLCOVOffering(str) { return str === GMLCOVOffering; } function isXyzOffering(str) { return str === xyzOffering; } function isGeoJsonOffering(str) { return str === GeoJsonOffering; } function isTMSOffering(str) { return str === tmsOffering; } const XLink_1_0 = XLink_1_0_Factory.XLink_1_0; const OWS_1_1_0 = OWS_1_1_0_Factory.OWS_1_1_0; const SMIL_2_0 = SMIL_2_0_Factory.SMIL_2_0; const SMIL_2_0_Language = SMIL_2_0_Language_Factory.SMIL_2_0_Language; const GML_3_1_1 = GML_3_1_1_Factory.GML_3_1_1; const WMTS_1_0 = WMTS_1_0_Factory.WMTS_1_0; class WmtsClientService { constructor(http) { this.http = http; const context = new Jsonix.Context([SMIL_2_0, SMIL_2_0_Language, GML_3_1_1, XLink_1_0, OWS_1_1_0, WMTS_1_0]); this.xmlunmarshaller = context.createUnmarshaller(); this.xmlmarshaller = context.createMarshaller(); } getCapabilities(url, version = '1.1.0') { // example: https://tiles.geoservice.dlr.de/service/wmts?SERVICE=WMTS&REQUEST=GetCapabilities&VERSION=1.1.0 const getCapabilitiesUrl = `${url}?SERVICE=WMTS&REQUEST=GetCapabilities&VERSION=${version}`; const headers = new HttpHeaders({ 'Content-Type': 'text/xml', Accept: 'text/xml, application/xml' }); return this.http.get(getCapabilitiesUrl, { headers, responseType: 'text' }).pipe(map(response => { return this.xmlunmarshaller.unmarshalString(response); })); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: WmtsClientService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: WmtsClientService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: WmtsClientService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.HttpClient }] }); function shardsExpand(v) { if (!v) { return; } const o = []; const shardsSplit = v.split(','); for (const i in shardsSplit) { if (shardsSplit[i]) { const j = shardsSplit[i].split('-'); if (j.length === 1) { o.push(shardsSplit[i]); } else if (j.length === 2) { const start = j[0].charCodeAt(0); const end = j[1].charCodeAt(0); if (start <= end) { for (let k = start; k <= end; k++) { o.push(String.fromCharCode(k).toLowerCase()); } } else { for (let k = start; k >= end; k--) { o.push(String.fromCharCode(k).toLowerCase()); } } } } } return o; } /** * OWS Context Service * OGC OWS Context Geo Encoding Standard Version: 1.0 * http://docs.opengeospatial.org/is/14-055r2/14-055r2.html * http://www.owscontext.org/owc_user_guide/C0_userGuide.html * * This service allows you to read and write OWC-data. * We have added some custom fields to the OWC standard. * - accepts the OWC-standard-data-types as function inputs (so as to be as general as possible) * - returns our extended OWC-data-types as function outputs (so as to be as information-rich as possible) * * As a policy, this services does *not* make any HTTP requests to GetCapabilities (or similar) to gather * additional information (with very few exceptions) - we want to save on network traffic. * However there are scripts that auto-generate OWC files from Capabilities, those, of course, * *do* scrape as much information online as possible; But they are not intended to be used in * a live-application. Run them batch-wise and server-side instead. */ class OwcJsonService { constructor(wmtsClient, http) { this.wmtsClient = wmtsClient; this.http = http; // http://www.owscontext.org/owc_user_guide/C0_userGuide.html#truegeojson-encoding-2 } checkContext(context) { return isIOwsContext(context); } getContextTitle(context) { return context.properties.title; } getContextPublisher(context) { return (context.properties.publisher) ? context.properties.publisher : null; } getContextExtent(context) { return (context.bbox) ? context.bbox : null; // or [-180, -90, 180, 90]; } getResources(context) { return context.features; } /** * Get Resources whith Folder property but not including Layer-Filtertypes */ getGroupResources(context) { const resources = context.features; return resources.filter(r => { const groupName = this.getLayerGroupFromFolder(r); return groupName && !Object.keys(Filtertypes).includes(groupName); }); } /** * Get Resources without Folder property or Folder is only Layer-Filtertypes */ getSingleResources(context) { const resources = context.features; return resources.filter(r => { const groupName = this.getLayerGroupFromFolder(r); return !groupName || Object.keys(Filtertypes).includes(groupName); }); } /** Resource --------------------------------------------------- */ getResourceTitle(resource) { return resource.properties.title; } /** * The Folder property of IOwsResource * @returns string | `${TFiltertypes}/string` */ getResourceFolder(resource) { return resource.properties.folder; } /** * returns name from Resource Folder if it is not only a Filtertype `TFiltertypes` */ getLayerGroupFromFolder(resource) { const folderName = this.getResourceFolder(resource); if (folderName) { const folderParts = folderName.split('/'); if (folderParts.length === 1) { if (!Filtertypes[folderName]) { return folderName; } } else if (folderParts.length === 2) { const filtertype = folderParts[0]; if (!Filtertypes[filtertype]) { console.warn(`Folder (${folderName}) should be named like: ${Object.keys(Filtertypes).map(k => `${k}/<FolderName>`).join(' | ')}`); } return folderParts[1]; } else { console.log(`only one Folder hierarchy is implemented`, folderParts); } } } /** * FilterType in IOwsResource Folder property */ getFilterType(resource) { if (resource.properties.folder) { const pathParts = resource.properties.folder.split('/'); const first = pathParts[0]; if (Filtertypes[first]) { return first; } } } getResourceUpdated(resource) { return resource.properties.updated; } getResourceDate(resource) { return (resource.properties.date) ? resource.properties.date : null; } getResourceOfferings(resource) { return (resource.properties.offerings) ? resource.properties.offerings : null; } /** * retrieve layer status active / inactive based on IOwsResource * @param resource: IOwsResource */ isActive(resource) { let active = true; if (resource.properties.active === false || resource.properties?.active) { active = resource.properties.active; } return active; } getResourceDescription(resource) { let description = ''; if (resource.properties.abstract) { description = resource.properties.abstract; } return description; } /** OWS Extenson IEocOwsResource */ getResourceOpacity(resource) { let opacity = 1; if (resource.properties?.opacity) { opacity = resource.properties.opacity; } return opacity; } /** OWS Extenson IEocOwsResource */ getResourceAttribution(resource) { let attribution = ''; if (resource.properties?.attribution) { attribution = resource.properties.attribution; } else if (resource.properties.rights) { attribution = resource.properties.rights; } return attribution; } /** OWS Extenson IEocOwsResource */ getResourceShards(resource) { if (resource.properties?.shards) { return resource.properties.shards; } } /** OWS Extenson IEocOwsResource */ getResourceMinMaxZoom(resource, targetProjection = 'EPSG:4326') { const zooms = { minZoom: null, maxZoom: null }; if (resource.properties.minZoom) { zooms.minZoom = resource.properties.minZoom; } else if (resource.properties.maxscaledenominator) { // *Max*ScaleDenom ~ *Min*Zoom zooms.minZoom = this.scaleDenominatorToZoom(resource.properties.maxscaledenominator, targetProjection) || null; } if (resource.properties.maxZoom) { zooms.maxZoom = resource.properties.maxZoom; } else if (resource.properties.minscaledenominator) { // *Min*ScaleDenom ~ *Max*Zoom zooms.maxZoom = this.scaleDenominatorToZoom(resource.properties.minscaledenominator, targetProjection) || null; } return zooms; } /** * e.g. * (array) value: '1984-01-01T00:00:00.000Z/1989-12-31T23:59:59.000Z/PT1S,1990-01-01T00:00:00.000Z/1994-12-31T23:59:59.000Z/PT1S,...' * (array) value: '1984-01-01T00:00:00.000Z/P1D,P1D/2000-01-01T00:00:00.000Z,...' * (array) value: '2000-01-01T00:00:00.000Z,2001-01-01T00:00:00.000Z,2002-01-01T00:00:00.000Z,...' * (single) value: '2016-01-01T00:00:00.000Z/2018-01-01T00:00:00.000Z/P1Y' */ getTimeValueFromDimensions(values, period) { if (values === null) { return; } else { const isList = /,/g.test(values); if (isList) { // values: `${string},${string}` const splitValues = values.split(','); if (splitValues.length > 0) { const parsed = []; // for (const value of splitValues) { const parsedSingle = this.parseSingleTimeOrPeriod(value); if (typeof parsedSingle === 'object' && parsedSingle.interval) { if (!parsedSingle.periodicity && period) { parsedSingle.periodicity = period; } else if (!parsedSingle.periodicity && !period) { console.warn(`Interval without a period`, values, period); } } parsed.push(parsedSingle); } return parsed; } } else { // `${string}/${string}` | `${string}/${string}/P${string}` const parsedSingle = this.parseSingleTimeOrPeriod(values); if (typeof parsedSingle === 'object' && parsedSingle.interval) { if (!parsedSingle.periodicity && period) { parsedSingle.periodicity = period; } else if (!parsedSingle.periodicity && !period) { console.warn(`Interval without a period`, values, period); } return parsedSingle; } else if (typeof parsedSingle === 'string') { return [parsedSingle]; } } } } /** * time could be: * * - date * - start/end/duration //Geoserver specific * - start/end * - start/duration, and duration/end */ parseSingleTimeOrPeriod(time) { const dateTime = DateTime.fromISO(time); if (dateTime.isValid) { return dateTime.toUTC().toISO(); } else { // is Interval ---------------------------- const interval = Interval.fromISO(time); if (interval.isValid) { const period = this.parseISO8601Period(time); const intervalObject = { periodicity: period, interval: `${interval.start.toUTC().toISO()}/${interval.end.toUTC().toISO()}` }; return intervalObject; } else { console.warn(`no Interval or not valid`, time); return null; } } } parseISO8601Period(value) { const periodMatches = value.match(/P\d*[YMWD](T\d\d[HMS])*/); if (periodMatches) { return periodMatches[0]; } } getResourceDimensions(resource) { if (!resource.properties.dimensions) { return undefined; } const dims = {}; for (const d of resource.properties.dimensions) { const name = d.name; if (name === 'time') { dims.time = this.getTimeDimensions(resource.properties.dimensions); /** if dimensions are defined but the values are null */ if (dims.time.values === null) { console.log('check to get time dimensions value from OGC Service later!!', resource); } } else if (name === 'elevation') { dims.elevation = this.getElevationDimension(resource.properties.dimensions); /** if dimensions are defined but the values are null */ if (dims.elevation.values === null) { console.log('check to get elevation dimensions value from OGC Service later!!', resource); } } else { dims[name] = d; } } return dims; } getTimeDimensions(dimensions) { let dim = { values: null, units: null }; const value = dimensions.find(d => d.name === 'time'); if (!value) { return; } const parsedValues = this.getTimeValueFromDimensions(value.values, value?.display?.period); dim = { values: null, units: value.units, display: {} }; /** check if is array or single value */ if (Array.isArray(parsedValues)) { dim.values = parsedValues; /** don't set dim.display.period if it is an array because there could be different periods */ // dim.display.period = ... } else if (parsedValues && typeof parsedValues !== 'string' && parsedValues.interval && parsedValues.periodicity) { dim.values = parsedValues; /** set dim.display.period from the parsed values */ if (parsedValues.periodicity) { dim.display.period = parsedValues.periodicity; } } if (value?.display?.format) { dim.display.format = value.display.format; } return dim; } getElevationDimension(dimensions) { const dim = { values: null, units: null }; const value = dimensions.find(d => d.name === 'elevation'); if (!value) { return; } else { dim.values = value.value; dim.units = value.units; if (value.display) { dim.display = value.display; } return dim; } } /** Offering --------------------------------------------------- */ getLayertypeFromOfferingCode(offering) { if (isWmsOffering(offering.code)) { return WmsLayertype; } else if (isWmtsOffering(offering.code)) { return WmtsLayertype; } else if (isWfsOffering(offering.code)) { return WfsLayertype; } else if (isKmlOffering(offering.code)) { return KmlLayertype; } else if (isGeoJsonOffering(offering.code)) { return GeojsonLayertype; } else if (isXyzOffering(offering.code)) { return XyzLayertype; } else if (isTMSOffering(offering.code)) { return TmsLayertype; } else { return offering.code; // an offering can also be any other string. } } checkIfServiceOffering(offering) { return (!offering.contents && offering.operations) ? true : false; } checkIfDataOffering(offering) { return (offering.contents && !offering.operations) ? true : false; } /** * Helper function to extract legendURL from project specific ows Context * @param offering layer offering */ getLegendUrl(offering) { let legendUrl = ''; if (offering.styles) { const defaultStyle = offering.styles.find(style => style.default); if (defaultStyle) { return defaultStyle.legendURL; } } else if (offering.legendUrl) { legendUrl = offering.legendUrl; } return legendUrl; } /** * Get all Layers from the IOwsContext. * * The order of the layers is reversed to get the context drawing order! */ getLayers(owc, targetProjection) { const layers$ = []; /** For the order of Layers see IOwsContext['features'] */ /** * LayerGroups * * e.g. if groupName: Layers/test -> a group "test" in the slot Layers will be created with the layer in it * e.g. if groupName: Overlays/test -> a group "test" in the slot Overlays will be created with the layer in it * if groupName is only: Layers | Overlays | Baselayers use layerResources */ const resources = this.getResources(owc); const groups = []; resources.forEach(r => { const lg = this.createLayerOrGroupFromResource(r, owc, targetProjection, groups); layers$.push(lg); }); return forkJoin(layers$) // making sure no undefined/null layers are returned .pipe(map(layers => layers.filter(layer => layer))) // reverse so layer order is like in the context .pipe(map(layers => layers.reverse())); } /** * Creates Layers or LayerGroups from IOwsResource and IOwsContext * Add uniqueGroups array to track already created groups */ createLayerOrGroupFromResource(resource, context, targetProjection, uniqueGroups) { const layergroupResources = this.getGroupResources(context); const groupName = this.getLayerGroupFromFolder(resource); /** Layers with folder property */ if (groupName) { /** unique layergroupResources */ if (!uniqueGroups.includes(groupName)) { uniqueGroups.push(groupName); /** reverse so layer order is like in the context */ const includedResources = layergroupResources.filter(r => this.getLayerGroupFromFolder(r) === groupName).reverse(); const layerGroup$ = this.createLayerGroup(groupName, includedResources, context, targetProjection); return layerGroup$; } else { return of(null); } } else { /** Single Layers */ const layer$ = this.createLayerFromDefaultOffering(resource, context, targetProjection); return layer$; } } /** * * @param groupName string | `${TFiltertypes}/string` */ createLayerGroup(groupName, includedResources, owc, targetProjection) { const layers$ = []; let filterType = null; for (const resource of includedResources) { filterType = this.getFilterType(resource); layers$.push(this.createLayerFromDefaultOffering(resource, owc, targetProjection)); } const layerGroup$ = forkJoin(layers$) // making sure no undefined layers are returned .pipe(map((layers) => layers.filter(layer => layer))) // putting layers in a LayerGroup .pipe(map((layers) => { if (layers.length) { /** if filterType is Baselayers -> create a merged Layer */ if (filterType === Filtertypes.Baselayers) { const descriptionLayers = layers.filter(l => l.description); // filter empty elements const mergedDescription = descriptionLayers.map(i => i.description); const legendImages = layers.map(i => i.legendImg).filter(d => d); // filter empty elements const layerOptions = { id: `${groupName}_${layers.map(i => i.id).join(' ')}`.replace(/\s/g, '_'), name: groupName, layers: layers, filtertype: Filtertypes.Baselayers }; if (mergedDescription.length) { layerOptions.description = mergedDescription.map((d, index) => this.generateAbstractFromLayerDescription(d, descriptionLayers[index].id)).join(';\r\n'); } if (legendImages) { layerOptions.legendImg = legendImages[0]; } const stackedLayer = new StackedLayer(layerOptions); return stackedLayer; } else { const layerGroup = new LayerGroup({ id: `${groupName}_${layers.map(i => i.id).join(' ')}`.replace(/\s/g, '_'), name: groupName, layers, filtertype: layers[0].filtertype // @TODO: can some layers have a different filter-type? -> All layers in a Group must be from the same filter type }); return layerGroup; } } })) // making sure no undefined layers are returned .pipe(filter(lg => lg instanceof LayerGroup || lg instanceof Layer)); return layerGroup$; } createLayerFromDefaultOffering(resource, owc, targetProjection) { const offerings = resource.properties?.offerings; if (offerings) { // TODO: allow Multiple offerings ??? const offering = offerings.find(o => isWmsOffering(o.code)) || offerings.find(o => isWmtsOffering(o.code)) || offerings.find(o => isWfsOffering(o.code)) || offerings.find(o => isTMSOffering(o.code)) || offerings[0]; return this.createLayerFromOffering(offering, resource, owc, targetProjection); } else { return of(null); } } createLayerFromOffering(offering, resource, context, targetProjection) { const layerType = this.getLayertypeFromOfferingCode(offering); if (this.isRasterLayerType(layerType) && this.isVectorLayerType(layerType)) { // Some layers (tms) can both be raster or vector so create both and filter out of(null) const raster = this.createRasterLayerFromOffering(offering, resource, context, targetProjection); const vector = this.createVectorLayerFromOffering(offering, resource, context, targetProjection); const layer = concat(raster, vector).pipe(filter(l => l instanceof Layer)); return layer; } else if (this.isRasterLayerType(layerType)) { return this.createRasterLayerFromOffering(offering, resource, context, targetProjection); } else if (this.isVectorLayerType(layerType)) { return this.createVectorLayerFromOffering(offering, resource, context, targetProjection); } else { console.warn(`This type of service (${layerType}) has not been implemented yet.`, offering); return of(null); } } createVectorLayerFromOffering(offering, resource, context, targetProjection) { const layerType = this.getLayertypeFromOfferingCode(offering); let vectorLayer$ = of(null); switch (layerType) { case WfsLayertype: vectorLayer$ = this.createWfsLayerFromOffering(offering, resource, context); break; case TmsLayertype: vectorLayer$ = this.createVectorTileLayerFromOffering(offering, resource, context, targetProjection); break; case GeojsonLayertype: vectorLayer$ = this.createDataVectorLayerFromOffering(offering, resource, context); break; case KmlLayertype: vectorLayer$ = this.createDataVectorLayerFromOffering(offering, resource, context); break; default: console.warn(`This type of layer '${layerType}' / offering '${offering.code}' cannot be converted into a VectorLayer`, offering); break; } return vectorLayer$; } /** * TmsLayertype can be raster and vector */ isVectorLayerType(type) { return [WfsLayertype, KmlLayertype, GeojsonLayertype, TmsLayertype].includes(type); } getVectorLayerOptions(offering, resource, context, targetProjection) { const layerOptions = this.getLayerOptions(offering, resource, context); if (this.isVectorLayerType(layerOptions.type)) { const { minZoom, maxZoom } = this.getResourceMinMaxZoom(resource, targetProjection); const subdomains = shardsExpand(this.getResourceShards(resource)); const vectorLayerOptions = { ...layerOptions, type: layerOptions.type, subdomains, maxZoom, minZoom }; return vectorLayerOptions; } else { console.error(`The layer ${layerOptions.id} is not a VectorLayer`, layerOptions); } } /** * https://opengeospatial.github.io/e-learning/wfs/text/operations.html#getfeature */ // offering, resource, context, targetProjection getWfsOptions(offering) { const getFeatureOperation = offering.operations.find(o => o.code === GetFeatureOperationCode); let layerUrl = null; /** check for mandatory wfs params */ if (getFeatureOperation) { const { url, searchParams } = this.checkWfsParams(offering); if (url && searchParams) { layerUrl = `${url}?${searchParams.toString()}`; } } return layerUrl; } checkWfsParams(offering) { const { url, searchParams } = this.parseOperationUrl(offering, GetFeatureOperationCode); const params = { typeNames: searchParams.get('TYPENAME') || searchParams.get('TYPENAMES'), version: searchParams.get('VERSION'), service: searchParams.get('SERVICE'), request: searchParams.get('REQUEST') }; if (!params.typeNames && !params.version || !params.service || !params.request) { console.warn(`URL does not contain the minimum required arguments for a WFS typeName: ${params.typeNames}, version: ${params.version}, service: ${params.service}, request: ${params.request}`, `${url}?${searchParams.toString()}`); return { url: null, searchParams: null }; } else { return { url, searchParams }; } } createRasterLayerFromOffering(offering, resource, context, targetProjection) { const layerType = this.getLayertypeFromOfferingCode(offering); let rasterLayer$ = of(null); switch (layerType) { case WmsLayertype: rasterLayer$ = this.createWmsLayerFromOffering(offering, resource, context, targetProjection); break; case WmtsLayertype: rasterLayer$ = this.createWmtsLayerFromOffering(offering, resource, context, targetProjection); break; case XyzLayertype: rasterLayer$ = this.createXyzLayerFromOffering(offering, resource, context, targetProjection); break; case TmsLayertype: rasterLayer$ = this.createTmsRasterLayerFromOffering(offering, resource, context, targetProjection); break; default: console.warn(`This type of offering '${offering.code}' cannot be converted into a RasterLayer.`, offering); break; } return rasterLayer$; } /** * TmsLayertype can be raster and vector */ isRasterLayerType(type) { return [WmsLayertype, WmtsLayertype, XyzLayertype, TmsLayertype].includes(type); } createVectorTileLayerFromOffering(offering, resource, context, targetProjection) { if (isTMSOffering(offering.code)) { const vectorTileOperation = offering.operations.find(o => o.type === 'application/vnd.mapbox-vector-tile'); if (vectorTileOperation) { const layerOptions = this.getVectorLayerOptions(offering, resource, context); const tmsServerUrl = offering.operations.find(o => o.code === RESTOperationCode).href; layerOptions.url = tmsServerUrl; if (offering.styles && offering.styles[0]?.content.type === 'OpenMapStyle') { const content = offering.styles[0].content; // we need the sourceKey to apply t5he style later if (content?.styleSource) { if (!layerOptions.options) { layerOptions.options = { styleSource: content.styleSource, style: null }; } else if (!layerOptions.options.style) { layerOptions.options.style = {}; layerOptions.options.styleSource = content.styleSource; } let styleObj$; if (content?.content) { if (typeof content.content === 'string') { styleObj$ = of(JSON.parse(content.content)); } else { styleObj$ = of(content.content); } } else if (content?.href) { const url = content.href; styleObj$ = this.http.get(url); } else { console.warn(`Couldn't find style for Tms-Offering`, offering); } if (styleObj$) { return styleObj$.pipe(map((obj) => { layerOptions.options.style = obj; const newLayer = new VectorLayer(layerOptions); return newLayer; })); } else { const newLayer = new VectorLayer(layerOptions); return of(newLayer); } } } else { const newLayer = new VectorLayer(layerOptions); return of(newLayer); } } else { return of(null); } } else { return of(null); } } createWfsLayerFromOffering(offering, resource, context) { // Case 1: service-offering let layerUrl; if (offering.operations) { /** currently, Ukis only supports wfs as service vector offering */ layerUrl = this.getWfsOptions(offering); const layerOptions = this.getVectorLayerOptions(offering, resource, context); layerOptions.url = layerUrl; const layer = new VectorLayer(layerOptions); if (resource.bbox) { layer.bbox = resource.bbox; } else if (context && context.bbox) { layer.bbox = context.bbox; } return of(layer); } if (layerUrl === null) { return of(null); } } createDataVectorLayerFromOffering(offering, resource, context) { // Case 2: data-offering if (offering.contents) { let data; let url; // currently, Ukis only supports geojson and kml as data-offering offering.contents.forEach(content => { if (content?.content) { if (content.type === 'application/geo+json') { if (typeof content.content === 'string') { data = JSON.parse(content.content); } else { data = content.content; } } else if (content.type === 'application/vnd.google-earth.kml+xml') { data = content.content; } } else if (content?.href) { url = content.href; } }); const layerOptions = this.getVectorLayerOptions(offering, resource, context); if (data) { layerOptions.data = data; } else if (url) { layerOptions.url = url; } if (resource.bbox) { layerOptions.bbox = resource.bbox; } else if (context && context.bbox) { layerOptions.bbox = context.bbox; } const layer = new VectorLayer(layerOptions); return of(layer); } else { return of(null); } } createTmsRasterLayerFromOffering(offering, resource, context, targetProjection) { if (isTMSOffering(offering.code)) { // url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', // subdomains: ['a', 'b', 'c'], OR shards?: string; a-d const rasterOperation = offering.operations.find(o => o.type === 'image/png' || o.type === 'image/jpeg'); if (rasterOperation) { const rasterOptions = this.getRasterLayerOptions(offering, resource, context, targetProjection); // TODO: use new function on map-ol to create tms not xyz type rasterOptions.type = 'xyz'; const layer = new RasterLayer(rasterOptions); return of(layer); } else { // no Raster TMS, maybe VectorTile return of(null); } } else { return of(null); } } createWmtsLayerFromOffering(offering, resource, context, targetProjection) { if (isWmtsOffering(offering.code)) { return this.getWmtsOptions(offering, resource, context, targetProjection).pipe(map((options) => { const layer = new WmtsLayer(options); return layer; })); } else { return of(null); } } createWmsLayerFromOffering(offering, resource, context, targetProjection) { if (isWmsOffering(offering.code)) { const options = this.getWmsOptions(offering, resource, context, targetProjection); const layer = new WmsLayer(options); return of(layer); } else { return of(null); } } createXyzLayerFromOffering(offering, resource, context, targetProjection) { if (isXyzOffering(offering.code)) { // url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', // subdomains: ['a', 'b', 'c'], OR shards?: string; a-d const rasterOptions = this.getRasterLayerOptions(offering, resource, context, targetProjection); rasterOptions.type = 'xyz'; const layer = new RasterLayer(rasterOptions); return of(layer); } else { return of(null); } } /** * https://docs.opengeospatial.org/is/13-082r2/13-082r2.html - OGC WMTS Simple Profile * http://schemas.opengis.net/wmts/1.0/wmtsGetTile_request.xsd * https://opengeospatial.github.io/e-learning/wmts/text/main.html#example-gettile-request */ getWmtsOptions(offering, resource, context, targetProjection) { const rasterOptions = this.getRasterLayerOptions(offering, resource, context, targetProjection); const { searchParams } = this.parseOperationUrl(offering, GetTileOperationCode); const params = { layer: searchParams.get('LAYER'), style: 'default', // (mandatory) -> 07-057r7_Web_Map_Tile_Service_Standard.pdf projection: targetProjection // TODO: alow this also from URL??? }; const defaultStyle = offering?.styles?.find(s => s.default); if (defaultStyle && defaultStyle.name) { params.style = defaultStyle.name; } else if (searchParams.has('STYLE')) { params.style = searchParams.get('STYLE'); } if (searchParams.has('FORMAT')) { params.format = searchParams.get('FORMAT'); } if (searchParams.has('VERSION')) { params.version = searchParams.get('VERSION'); } return this.getMatrixSetForWMTS(offering, targetProjection) .pipe(map((matrixSet) => { const wmtsOptions = { ...rasterOptions, type: 'wmts', params }; if (matrixSet) { const matrixSetOptions = { matrixSet: matrixSet.matrixSet, matrixIds: matrixSet.matrixIds, resolutions: matrixSet.resolutions }; wmtsOptions.params.matrixSetOptions = matrixSetOptions; } return wmtsOptions; })); } parseOperationUrl(offering, opCode) { const up = { url: null, searchParams: null }; if (offering.operations) { const operation = offering.operations.find(op => op.code === opCode); if (operation) { const { url, searchParams } = this.getJsonFromUri(operation.href); up.url = url; up.searchParams = searchParams; } else { console.error(`There is no ${opCode} -operation in the offering ${offering.code}.`, offering); } } else { console.error(`The offering ${offering.code} has no operations.`, offering); } return up; } /* TODO: check correctness of this function and add it to utils-ogc getDefaultMatrixSet(projection: { extent: [number, number, number, number], srs: string }, matrixSet: string, resolutions?: Array<string | number>, matrixIds?: Array<string | number>, resolutionLevels: number = 42, tileSize: number = 256, matrixIdPrefix: string = '') { const resolutionsFromExtent = (extent, optMaxZoom: number, ts: number) => { const maxZoom = optMaxZoom; const height = extent[3] - extent[1]; // getHeight const width = extent[2] - extent[0]; // getWidth const maxResolution = Math.max(width / ts, height / ts); const length = maxZoom + 1; const res = new Array(length); for (let z = 0; z < length; ++z) { res[z] = maxResolution / Math.pow(2, z); } return res; }; const matrixIdsFromResolutions = (resLev: number, maPre?: string) => { return Array.from(Array(resLev).keys()).map(l => { if (maPre) { return `${maPre}:${l} `; } else { return l; } }); }; const defaultResolutions = resolutionsFromExtent(projection.extent, resolutionLevels, tileSize); const defaultMatrixIds = matrixIdsFromResolutions(defaultResolutions.length, matrixIdPrefix); const defaultSet: IEocOwsWmtsMatrixSet = { srs: projection.srs, matrixSet, origin: { x: projection.extent[0], y: projection.extent[3] }, resolutions: resolutions || defaultResolutions, tilesize: { height: tileSize, width: tileSize }, matrixIds: matrixIds || defaultMatrixIds as any }; defaultSet.matrixIds = defaultSet.matrixIds.map(i => i.toString()); return defaultSet; } */ getMatrixSetForWMTS(offering, targetProjection) { // Observable<IEocOwsWmtsMatrixSet | null> vs. Observable<IEocOwsWmtsMatrixSet> https://github.com/ReactiveX/rxjs/issues/3388 if (offering?.matrixSets) { const matrixSet = offering.matrixSets.find(m => m.srs === targetProjection); return of(matrixSet); } else if (offering.matrixSets === null) { /** * If offering.matrixSets === null use a default set for EPSG:3857 and 256 tiles * Create this in the mapping library when the WMTS is created. */ return of(null); } else { const url = this.parseOperationUrl(offering, 'GetCapabilities').url; return this.wmtsClient.getCapabilities(url).pipe(map((capabilities) => { const matrixSets = capabilities.value.contents.tileMatrixSet; let matrixSet = matrixSets.find(ms => ms.identifier.value === targetProjection); if (!matrixSet && targetProjection === 'EPSG:3857') { const altTargetProjection = 'EPSG:900913'; matrixSet = matrixSets.find(ms => ms.identifier.value === altTargetProjection); } const owsMatrixSet = { srs: targetProjection, matrixSet: matrixSet['identifier']['value'], matrixIds: matrixSet['tileMatrix'].map(tm => tm['identifier']['value']), resolutions: matrixSet['tileMatrix'].map(tm => tm['scaleDenominator']), origin: { x: matrixSet['tileMatrix'][0]['topLeftCorner'][1], y: matrixSet['tileMatrix'][0]['topLeftCorner'][0] }, tilesize: matrixSet['tileMatrix'][0]['tileHeight'] }; return owsMatrixSet; })); } } /** * TODO: add more vendor params ?? * https://docs.geoserver.org/latest/en/user/services/wms/reference.html#getmap */ getWmsOptions(offering, resource, context, targetProjection) { const rasterOptions = this.getRasterLayerOptions(offering, resource, context, targetProjection); if (rasterOptions?.type === WmsLayertype) { const { searchParams } = this.parseOperatio