@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
JavaScript
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