hslayers-ng
Version:
HSLayers-NG mapping library
1,204 lines (1,199 loc) • 192 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, Injectable, computed, NgZone } from '@angular/core';
import { Subject, BehaviorSubject, takeUntil, finalize, timeout, map, catchError, of, debounceTime, switchMap, from, filter, forkJoin } from 'rxjs';
import { HsCommonEndpointsService } from 'hslayers-ng/services/endpoints';
import { HsCommonLaymanService, isLaymanUrl, HsCommonLaymanLayerService, getLaymanFriendlyLayerName, layerParamPendingOrStarting, awaitLayerSync } from 'hslayers-ng/common/layman';
import { HsConfig } from 'hslayers-ng/config';
import { HsMapService, DuplicateHandling } from 'hslayers-ng/services/map';
import { getBase, setCluster, setQueryCapabilities, setHighlighted, setDefinition } from 'hslayers-ng/common/extensions';
import { transform, transformExtent, get } from 'ol/proj';
import { HsLanguageService } from 'hslayers-ng/services/language';
import { HsLayoutService } from 'hslayers-ng/services/layout';
import { HsLogService } from 'hslayers-ng/services/log';
import { HsToastService } from 'hslayers-ng/common/toast';
import { HsDimensionService, HsArcgisGetCapabilitiesService, HsWfsGetCapabilitiesService, HsWmsGetCapabilitiesService, HsWmtsGetCapabilitiesService, HsXyzGetCapabilitiesService } from 'hslayers-ng/services/get-capabilities';
import { HsEventBusService } from 'hslayers-ng/services/event-bus';
import TileGrid from 'ol/tilegrid/TileGrid';
import { createXYZ } from 'ol/tilegrid';
import { Tile, VectorImage, Vector as Vector$1, Image } from 'ol/layer';
import { TileArcGISRest, XYZ, Vector, TileWMS, ImageWMS } from 'ol/source';
import { EsriJSON, WMSCapabilities, WMTSCapabilities } from 'ol/format';
import { tile } from 'ol/loadingstrategy.js';
import { addAnchors, getPreferredFormat, undefineEmptyString, bufferExtent, HsProxyService, paramsToURLWoEncode, isOverflown, getPortFromUrl, calculateResolutionFromScale, addExtentFeature, createNewExtentLayer, debounce, highlightFeatures, paramsToURL } from 'hslayers-ng/services/utils';
import { HttpClient } from '@angular/common/http';
import * as xml2Json from 'xml-js';
import { WfsSource, SparqlJson } from 'hslayers-ng/common/layers';
import WMTS, { optionsFromCapabilities } from 'ol/source/WMTS';
import { HsHistoryListService } from 'hslayers-ng/common/history-list';
import { HsLaymanService, HsSaveMapService } from 'hslayers-ng/services/save-map';
import { HsDialogContainerService, HsLayerOverwriteDialogComponent } from 'hslayers-ng/common/dialogs';
import { OverwriteResponse, isErrorHandlerFunction, EndpointErrorHandling, VectorLayerDescriptor, VectorSourceDescriptor } from 'hslayers-ng/types';
import { unByKey } from 'ol/Observable';
import { HsStylerService } from 'hslayers-ng/services/styler';
import { PROJECTIONS } from 'ol/proj/epsg4326';
import { timeout as timeout$1, map as map$1, catchError as catchError$1 } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
class HsAddDataService {
constructor() {
this.hsMapService = inject(HsMapService);
this.hsConfig = inject(HsConfig);
this.hsCommonEndpointsService = inject(HsCommonEndpointsService);
this.hsCommonLaymanService = inject(HsCommonLaymanService);
this.sidebarLoad = new Subject();
this.datasetSelected = new BehaviorSubject(undefined);
this.datasetTypeSelected = this.datasetSelected.asObservable();
}
addLayer(layer, underLayer) {
if (underLayer) {
const layers = this.hsMapService.getLayersArray();
const underZ = underLayer.getZIndex();
layer.setZIndex(underZ);
for (const iLayer of layers.filter((l) => !getBase(l))) {
if (iLayer.getZIndex() >= underZ) {
iLayer.setZIndex(iLayer.getZIndex() + 1);
}
}
const ix = layers.indexOf(underLayer);
this.hsMapService.getMap().getLayers().insertAt(ix, layer);
}
else {
this.hsMapService.getMap().addLayer(layer);
}
}
selectType(type) {
this.datasetSelected.next(type);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsAddDataService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsAddDataService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsAddDataService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class HsAddDataUrlService {
constructor() {
this.hsLog = inject(HsLogService);
this.hsLanguageService = inject(HsLanguageService);
this.hsLayoutService = inject(HsLayoutService);
this.hsMapService = inject(HsMapService);
this.hsToastService = inject(HsToastService);
this.connectFromParams = true;
}
/**
* Selects a service layer to be added (WMS | WMTS | ArcGIS Map Server)
* @param services - Layer group of a service to select a layer from
* @param layerToSelect - Layer to be selected (checked = true)
* @param selector - Layer selector. Can be either 'Name' or 'Title'. Differs in between different services
*/
selectLayerByName(layerToSelect, services, selector) {
if (!layerToSelect) {
return;
}
let selectedLayer;
if (Array.isArray(services)) {
for (const serviceLayer of services) {
selectedLayer = this.selectSubLayerByName(layerToSelect, serviceLayer, selector);
if (selectedLayer && serviceLayer[selector] == layerToSelect) {
return selectedLayer;
}
}
}
else {
return this.selectSubLayerByName(layerToSelect, services, selector);
}
}
/**
* Helper function for selectLayerByName()
*/
selectSubLayerByName(layerToSelect, serviceLayer, selector) {
let selectedLayer;
if (serviceLayer.Layer && serviceLayer[selector] != layerToSelect) {
selectedLayer = this.selectLayerByName(layerToSelect, serviceLayer.Layer, selector);
}
if (serviceLayer[selector] == layerToSelect) {
selectedLayer = this.setLayerCheckedTrue(layerToSelect, serviceLayer, selector);
}
return selectedLayer;
}
/**
* Helper function for selectLayerByName()
* Does the actual selection (checked = true)
*/
setLayerCheckedTrue(layerToSelect, serviceLayer, selector) {
if (serviceLayer[selector] == layerToSelect) {
serviceLayer.checked = true;
}
return serviceLayer;
}
searchForChecked(records) {
this.addingAllowed =
records?.some((l) => l.checked) ?? this.typeSelected == 'arcgis';
}
/**
* Display layers extent parsing error
*/
layerExtentParsingError() {
this.hsToastService.createToastPopupMessage('ADDLAYERS.capabilitiesParsingProblem', 'ADDLAYERS.layerExtentParsingProblem', {
serviceCalledFrom: 'HsAddDataUrlService',
type: 'warning',
});
}
/**
* Calculate cumulative bounding box which encloses all the provided layers (service layer definitions)
* Common for WMS/WMTS (WFS has its own implementation)
*/
calcAllLayersExtent(layers) {
if (layers.length == 0) {
return undefined;
}
try {
const layerExtents = layers.map((lyr) => [...(lyr?.getExtent() || [])]); //Spread need to not create reference
return this.calcCombinedExtent(layerExtents);
}
catch (error) {
this.layerExtentParsingError();
return undefined;
}
}
/**
* For given array of layers (service layer definitions) it calculates a cumulative bounding box which encloses all the layers
*/
calcCombinedExtent(extents) {
try {
const currentMapProj = this.hsMapService.getCurrentProj();
const bounds = transform([180, 90], 'EPSG:4326', currentMapProj);
const extent = extents.reduce((acc, curr) => {
//some services define layer bboxes beyond the canonical 180/90 degrees intervals, the checks are necessary then
const [west, south, east, north] = curr;
//minimum easting
if (bounds[1] * -1 <= west && west < acc[0]) {
acc[0] = west;
}
//minimum northing
if (bounds[0] * -1 <= south && south < acc[1]) {
acc[1] = south;
}
//maximum easting
if (bounds[1] >= east && east > acc[2]) {
acc[2] = east;
}
//maximum northing
if (bounds[0] >= north && north > acc[3]) {
acc[3] = north;
}
return acc;
});
return extent.length > 0 ? extent : undefined;
}
catch (error) {
this.layerExtentParsingError();
return undefined;
}
}
/**
* Zoom map to one layers or combined layer list extent
*/
zoomToLayers(data) {
if (data.extent) {
this.hsMapService.fitExtent(data.extent);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsAddDataUrlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsAddDataUrlService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsAddDataUrlService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class HsAddDataCommonService {
constructor() {
this.hsMapService = inject(HsMapService);
this.hsAddDataUrlService = inject(HsAddDataUrlService);
this.hsToastService = inject(HsToastService);
this.hsAddDataService = inject(HsAddDataService);
this.hsDimensionService = inject(HsDimensionService);
this.hsEventBusService = inject(HsEventBusService);
this.loadingInfo = false;
this.showDetails = false;
//TODO: all dimension related things need to be refactored into separate module
this.getDimensionValues = this.hsDimensionService.getDimensionValues;
this.serviceLayersCalled = new Subject();
this.hsEventBusService.cancelAddDataUrlRequest.subscribe(() => {
this.clearParams();
});
}
clearParams() {
this.layerToSelect = '';
this.loadingInfo = false;
this.showDetails = false;
this.url = '';
this.hsAddDataUrlService.typeSelected = null;
}
setPanelToCatalogue() {
this.hsAddDataService.selectType('catalogue');
}
/**
* For the sake of possible future implementation changes
* @param url - URL to be set
*/
updateUrl(url) {
this.url = url;
}
checkTheSelectedLayer(services, serviceType) {
if (!services) {
return;
}
const nameOrTitle = serviceType !== 'wmts';
for (const layer of services) {
const layerName = nameOrTitle
? (layer.Name?.toLowerCase() ??
layer.name?.toLowerCase() ??
layer.Title?.toLowerCase() ??
layer.title?.toLowerCase())
: layer.Identifier?.toLowerCase();
if (serviceType === 'arcgis') {
layer.checked = Array.isArray(this.layerToSelect)
? this.layerToSelect.some((lt) => layerName === lt.toLowerCase() ||
layer.id?.toString().toLowerCase() === lt.toLowerCase())
: layerName === this.layerToSelect.toLowerCase() ||
layer.id?.toString().toLowerCase() ===
this.layerToSelect.toLowerCase();
}
else {
const singleLayerSelected = !this.layerToSelect.includes(',');
/**
* If single layer is selected, check if the layer name matches the selected layer
* If multiple layers are selected (group), check if the layer name matches any of the selected layers
*/
layer.checked = singleLayerSelected
? layerName === this.layerToSelect.toLowerCase()
: this.layerToSelect
.split(',')
.some((lt) => layerName === lt.toLowerCase());
}
}
}
displayParsingError(e) {
let errorMessage = 'ADDLAYERS.capabilitiesParsingProblem';
const errorDetails = e?.message || e?.toString() || 'Unknown error';
if (e?.status === 401) {
errorMessage = 'ADDLAYERS.unauthorizedAccess';
}
else if (errorDetails && errorDetails.includes('Unsuccessful OAuth2')) {
errorMessage = 'COMMON.Authentication failed. Login to the catalogue.';
}
else if (errorDetails.includes('property')) {
errorMessage = 'ADDLAYERS.serviceTypeNotMatching';
}
else if (errorDetails.startsWith('ADDLAYERS.')) {
errorMessage = errorDetails;
}
else {
errorMessage = `ADDLAYERS.${errorDetails}`;
}
this.hsToastService.createToastPopupMessage('ADDLAYERS.capabilitiesParsingProblem', errorMessage, { serviceCalledFrom: 'HsAddDataCommonService', customDelay: 10000 });
}
throwParsingError(e) {
this.clearParams();
this.displayParsingError(e);
}
//NOTE* - Is this method even needed?
srsChanged(srs) {
setTimeout(() => {
return !this.currentProjectionSupported([srs]);
}, 0);
}
/**
* Test if current map projection is in supported projection list
*
* @param srss - List of supported projections
* @returns True if map projection is in list, false otherwise
*/
currentProjectionSupported(srss) {
if (!srss || srss.length === 0) {
return false;
}
let found = false;
for (const val of srss) {
if (!val) {
found = false;
}
else {
if (this.hsMapService
.getMap()
.getView()
.getProjection()
.getCode()
.toUpperCase() == val.toUpperCase()) {
found = true;
}
}
}
return found;
}
/**
* Constructs body of LAYER parameter for getMap request for grouped layer e.g.
* for a basemap or thematic layer with property group set to true
* @param layerOrLayers - layer object or layers received from capabilities. If no layer is provided
* merge all checked layer ids into one string
* @param property - layer property
*/
getGroupedLayerNames(layerOrLayers, property) {
const baseNameParts = [];
if (Array.isArray(layerOrLayers)) {
for (const layer of layerOrLayers) {
if (layer.checked) {
baseNameParts.push(layer[property]);
}
else if (layer.Layer) {
const nested = this.getGroupedLayerNames(layer.Layer, property);
if (nested.length > 0) {
baseNameParts.push(nested);
}
}
}
}
else {
baseNameParts[0] = layerOrLayers[property];
}
return baseNameParts.join();
}
getSublayerNames(service) {
if (service.Layer) {
return service.Layer.map((l) => {
let tmp = [];
if (l.Name) {
tmp.push(l.Name);
}
if (l.Layer) {
const children = this.getSublayerNames(l);
tmp = tmp.concat(children);
}
return tmp.join(',');
});
}
return '';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsAddDataCommonService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsAddDataCommonService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsAddDataCommonService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
class HsUrlArcGisService {
constructor() {
this.hsArcgisGetCapabilitiesService = inject(HsArcgisGetCapabilitiesService);
this.hsLayoutService = inject(HsLayoutService);
this.hsMapService = inject(HsMapService);
this.hsAddDataUrlService = inject(HsAddDataUrlService);
this.hsAddDataCommonService = inject(HsAddDataCommonService);
this.hsToastService = inject(HsToastService);
this.hsLanguageService = inject(HsLanguageService);
this.hasCachedTiles = false;
this.setDataToDefault();
}
/**
* Sets data to default values
*/
setDataToDefault() {
this.data = {
serviceExpanded: false,
map_projection: '',
tile_size: 512,
use_resampling: false,
useTiles: true,
title: 'Arcgis layer',
table: {
trackBy: 'id',
nameProperty: 'name',
},
};
}
/**
* List and return layers from Arcgis getCapabilities response
* @param wrapper - Capabilities response wrapper
*/
async listLayerFromCapabilities(wrapper, options) {
if (!wrapper.response && !wrapper.error) {
return;
}
if (wrapper.error) {
this.hsAddDataCommonService.throwParsingError(wrapper.response.message);
return;
}
try {
await this.createLayer(wrapper.response);
if (this.hsAddDataCommonService.layerToSelect) {
this.hsAddDataCommonService.checkTheSelectedLayer(this.data.layers, 'arcgis');
return this.getLayers(undefined, undefined, options);
}
}
catch (e) {
this.hsAddDataCommonService.throwParsingError(e);
}
}
/**
* Parse information received in Arcgis getCapabilities response
* @param response - getCapabilities response
*/
async createLayer(response) {
try {
const caps = response;
if (caps.error) {
this.hsToastService.createToastPopupMessage('ADDLAYERS.capabilitiesParsingProblem', this.hsLanguageService.getTranslationIgnoreNonExisting('ERRORMESSAGES', caps.error.code || '4O4', { url: this.data.get_map_url }), {
serviceCalledFrom: 'HsUrlArcGisService',
});
this.hsAddDataCommonService.loadingInfo = false;
return;
}
this.data.map_projection = this.hsMapService
.getMap()
.getView()
.getProjection()
.getCode()
.toUpperCase();
// Determine the layer title with fallback priority
this.data.title =
caps.name || caps.mapName || caps.documentInfo?.Title || 'Arcgis layer';
if (this.data.title === 'Arcgis layer' && caps.supportedQueryFormats) {
this.data.title = caps.layers?.[0]?.name;
}
this.data.description = addAnchors(caps.description);
this.data.version = caps.currentVersion;
this.data.image_formats = caps.supportedImageFormatTypes
? caps.supportedImageFormatTypes.split(',')
: [];
this.data.query_formats = caps.supportedQueryFormats
? caps.supportedQueryFormats.split(',')
: [];
this.data.srss = caps.spatialReference?.latestWkid
? [caps.spatialReference.latestWkid.toString()]
: [];
this.data.services = caps.services?.reduce((acc, s) => {
if (this.isValidService(s.type)) {
acc.push({
...s,
icon: s.type === 'FeatureServer' ? 'fa-draw-polygon' : 'fa-image',
});
}
return acc;
}, []);
/**
* Prioritize cached tiles eg. ignore layer structure
*/
this.hasCachedTiles = !!caps.tileInfo;
this.data.layers = this.hasCachedTiles
? [
{
name: caps.mapName || caps.name,
id: 0,
defaultVisibility: true,
icon: 'fa-image',
},
]
: caps.layers;
if (this.data.layers?.length > 0) {
this.data.layers = this.data.layers.map((l) => ({
...l,
icon: l.type === 'Feature Layer' ? 'fa-draw-polygon' : 'fa-image',
}));
}
this.hsAddDataUrlService.searchForChecked(this.data.layers ?? this.data.services);
this.data.srs =
this.data.srss.find((srs) => srs.includes(this.hsMapService.getCurrentProj().getCode())) || this.data.srss[0];
this.data.extent = caps.fullExtent;
if (this.hasCachedTiles || (caps.tileInfo && this.isImageService())) {
/**
* Tile grid definition in layers source srs
* */
this.tileGrid = new TileGrid({
origin: Object.values(caps.tileInfo.origin),
resolutions: caps.tileInfo.lods.map((lod) => lod.resolution),
extent: [
caps.fullExtent.xmin,
caps.fullExtent.ymin,
caps.fullExtent.xmax,
caps.fullExtent.ymax,
],
});
}
this.data.resample_warning = this.hsAddDataCommonService.srsChanged(this.data.srs);
this.data.image_format = getPreferredFormat(this.data.image_formats, [
'PNG32',
'PNG',
'GIF',
'JPG',
]);
this.data.query_format = getPreferredFormat(this.data.query_formats, [
'geoJSON',
'JSON',
]);
this.hsAddDataCommonService.loadingInfo = false;
}
catch (e) {
throw new Error(e);
}
}
/**
* Loop through the list of layers and call getLayer
* layerOptions - used to propagate props when loading a layers from composition
*/
async getLayers(checkedOnly, shallow, layerOptions) {
if (this.data.layers === undefined &&
this.data.services === undefined &&
!this.isImageService() &&
!this.isFeatureService()) {
return;
}
/*
* - When checkedOnly is explicitly false (not just falsy): use all layers (needed for FeatureServer type)
* - Otherwise: only use layers that have been checked by the user
* Important because FeatureServer requires explicit layer IDs,
* while MapServer and ImageServer can serve all layers when no specific IDs are provided.
*/
const checkedLayers = checkedOnly === false
? this.data.layers
: this.data.layers?.filter((l) => l.checked);
const collection = [
await this.getLayer(checkedLayers, {
title: this.data.title.replace(/\//g, '/'),
path: undefineEmptyString(this.data.folder_name),
imageFormat: this.data.image_format,
queryFormat: this.data.query_format,
tileSize: this.data.tile_size,
crs: this.data.srs,
base: this.data.base,
...layerOptions,
}),
];
if (!layerOptions?.fromComposition) {
this.hsAddDataUrlService.zoomToLayers(this.data);
}
this.data.base = false;
this.hsAddDataCommonService.clearParams();
this.setDataToDefault();
this.hsAddDataCommonService.setPanelToCatalogue();
if (collection.length > 0 && !layerOptions?.fromComposition) {
this.hsLayoutService.setMainPanel('layerManager');
}
return collection;
}
/**
* Get selected layer
* @param layer - capabilities layer object
* @param layerTitle - layer name in the map
* @param path - Path name
* @param imageFormat - Format in which to serve image. Usually: image/png
* @param queryFormat - See info_format in https://docs.geoserver.org/stable/en/user/services/wms/reference.html
* @param tileSize - Tile size in pixels
* @param crs - of the layer
* @param subLayers - Static sub-layers of the layer
*/
async getLayer(layers, options) {
const attributions = [];
const dimensions = {};
//Not being used right now
// const legends = [];
// Handle FeatureServer
if (this.isFeatureService()) {
return this.getFeatureLayer(layers, options);
}
// MapServer and ImageServer
const sourceParams = {
/**
* Cached tiles or image-service with cached tiles.
* Difference is in source type that will be used to create layer.
* image-service is currently being displayed using TileArcGISRest not XYZ
*/
url: this.hasCachedTiles
? this.isImageService()
? this.data.get_map_url
: this.createXYZUrl()
: this.data.get_map_url,
attributions,
projection: `EPSG:${this.data.srs}`,
params: Object.assign({
FORMAT: options.imageFormat,
}, {}),
crossOrigin: 'anonymous',
};
if (this.hasCachedTiles) {
sourceParams.tileGrid = this.tileGrid;
}
else if (!this.hasCachedTiles && !this.isImageService()) {
const LAYERS = layers.length > 0
? `show:${layers.map((l) => l.id).join(',')}`
: undefined;
Object.assign(sourceParams.params, { LAYERS });
}
const source = this.hasCachedTiles
? this.isImageService()
? new TileArcGISRest(sourceParams)
: new XYZ(sourceParams)
: new TileArcGISRest(sourceParams);
/**
* Use provided extent when displaying more than 3 layers or no layer are defined(all layers)
* calculate extent otherwise
*/
this.data.extent =
layers.length > 3 || layers.length === 0
? this.transformLayerExtent(this.data.extent, this.data)
: await this.calcAllLayersExtent(layers, options);
const layerParams = {
opacity: options.opacity ?? 1,
properties: {
title: options.title,
name: options.title,
removable: true,
path: options.path,
base: this.data.base,
extent: this.data.extent,
dimensions,
...options,
},
source,
};
if (!this.isImageService()) {
Object.assign(layerParams.properties, {
subLayers: layers?.map((l) => l.id).join(','),
});
}
return new Tile(layerParams);
}
/**
* Create a vector layer for feature service
* @param layers - layers to be displayed
* @param options - layer options
* @returns vector layer
*/
getFeatureLayer(layers, options) {
const layerIds = layers.length > 0 ? layers.map((l) => l.id) : [0];
const queryUrl = layerIds.length === 1
? `${this.data.get_map_url}/${layerIds[0]}/query`
: `${this.data.get_map_url}/query`;
const vectorSource = new Vector({
format: new EsriJSON(),
url: (extent, resolution, projection) => {
if (!resolution) {
return `${queryUrl}?f=json`;
}
// ArcGIS Server only wants the numeric portion of the projection ID.
const srid = projection
.getCode()
.split(/:(?=\d+$)/)
.pop();
const params = new URLSearchParams({
f: 'json',
returnGeometry: 'true',
spatialRel: 'esriSpatialRelIntersects',
geometry: `${extent.join(',')}`,
geometryType: 'esriGeometryEnvelope',
inSR: srid,
outFields: '*',
outSR: srid,
});
return `${queryUrl}?${params.toString()}`;
},
strategy: tile(createXYZ({
tileSize: 512,
})),
});
this.data.extent = bufferExtent(this.transformLayerExtent(this.data.extent, this.data), this.hsMapService.getMap().getView().getProjection());
const layerParams = {
opacity: options.opacity ?? 1,
properties: {
title: options.title,
name: options.title,
removable: true,
path: options.path,
base: this.data.base,
extent: this.data.extent,
showInLayerManager: true,
...options,
},
source: vectorSource,
};
return new VectorImage(layerParams);
}
/**
* Create XYZ layer URL
*/
createXYZUrl() {
return `${this.data.get_map_url}/tile/{z}/{y}/{x}`;
}
/**
* Calculate cumulative bounding box which encloses all the checked layers (ArcGISRestResponseLayer)
*/
async calcAllLayersExtent(layers, options) {
try {
const layersCaps = await Promise.all(layers.map(async (l) => {
return await this.hsArcgisGetCapabilitiesService.request(`${this.data.get_map_url}/${l.id}`);
}));
const layersExtents = layersCaps
.map((l) => {
if (Object.values(l.response.extent).filter((v) => !isNaN(v))
.length > 0) {
return this.transformLayerExtent(l.response.extent, this.data);
}
})
.filter((v) => v);
return this.hsAddDataUrlService.calcCombinedExtent(layersExtents);
}
catch (error) {
if (error.message.includes('getCode')) {
this.hsToastService.createToastPopupMessage('ADDLAYERS.capabilitiesParsingProblem', 'ADDLAYERS.OlDoesNotRecognizeProjection', {
serviceCalledFrom: 'HsUrlArcGisService',
details: [`${options.title}`, `EPSG: ${this.data.srs}`],
});
}
else {
this.hsToastService.createToastPopupMessage('ADDLAYERS.capabilitiesParsingProblem', 'ADDLAYERS.layerExtentParsingProblem', {
serviceCalledFrom: 'HsUrlArcGisService',
type: 'warning',
});
return this.transformLayerExtent(this.data.extent, this.data);
}
}
}
/**
* Loop through the list of layers and add them to the map
* @param layers - Layers selected
*/
addLayers(layers) {
for (const l of layers) {
this.hsMapService.addLayer(l, DuplicateHandling.RemoveOriginal);
}
}
/**
* Request services layers
* @param service - Service URL
*/
async expandService(service) {
const originalUrl = new URL(this.hsAddDataCommonService.url);
/**
There are cases when loaded services are loaded from folders,
problem is that folder name is also included inside the service.name
to avoid any uncertainties, let's remove everything starting from '/services/'
inside the url and rebuild it. We look for '/services/' to avoid matching
domain names that contain 'services' (e.g. services7.arcgis.com)
*/
let pathname = originalUrl.pathname;
if (originalUrl.pathname.includes('services')) {
const firstPart = originalUrl.pathname.slice(0, originalUrl.pathname.indexOf('services'));
const secondPart = ['services', service.name, service.type].join('/');
pathname = firstPart + secondPart;
}
this.data.get_map_url = new URL(pathname, originalUrl.origin).toString();
const wrapper = await this.hsArcgisGetCapabilitiesService.request(this.data.get_map_url);
this.data.serviceExpanded = true;
await this.listLayerFromCapabilities(wrapper);
}
/**
* Step back to the top layer of capabilities
*/
async collapseServices() {
this.data.get_map_url = this.hsAddDataCommonService.url;
const wrapper = await this.hsArcgisGetCapabilitiesService.request(this.data.get_map_url);
this.data.serviceExpanded = false;
await this.listLayerFromCapabilities(wrapper);
}
/**
* Add services layers
* @param services - Services selected
*/
async addServices(services) {
const originalRestUrl = this.hsAddDataCommonService.url;
for (const service of services.filter((s) => s.checked)) {
this.hsAddDataCommonService.url = originalRestUrl; //Because getLayers clears all params
await this.expandService(service);
const layers = await this.getLayers(!this.isFeatureService());
this.addLayers(layers);
}
}
/**
* Check if getCapabilities response is Image service layer
*/
isImageService() {
return this.data.get_map_url?.toLowerCase().includes('imageserver');
}
/**
* Check if getCapabilities response is Feature service layer
*/
isFeatureService() {
return this.data.get_map_url?.toLowerCase().includes('featureserver');
}
/**
* Check validity of service
*/
isValidService(str) {
return !['gpserver', 'sceneserver'].includes(str.toLowerCase());
}
/**
* Transforms provided extent to a map projection
*/
transformLayerExtent(extent, data) {
return transformExtent([extent.xmin, extent.ymin, extent.xmax, extent.ymax], 'EPSG:' + data.srs, data.map_projection);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsUrlArcGisService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsUrlArcGisService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsUrlArcGisService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
class HsUrlWfsService {
constructor() {
this.http = inject(HttpClient);
this.hsWfsGetCapabilitiesService = inject(HsWfsGetCapabilitiesService);
this.hsLog = inject(HsLogService);
this.hsMapService = inject(HsMapService);
this.hsEventBusService = inject(HsEventBusService);
this.hsLayoutService = inject(HsLayoutService);
this.hsAddDataCommonService = inject(HsAddDataCommonService);
this.hsAddDataUrlService = inject(HsAddDataUrlService);
this.hsCommonLaymanService = inject(HsCommonLaymanService);
this.hsProxyService = inject(HsProxyService);
this.requestCancelSubjects = new Map();
this.cancelUrlRequest = new Subject();
this.withCredentials = computed(() => {
const url = this.hsWfsGetCapabilitiesService.service_url();
return isLaymanUrl(url, this.hsCommonLaymanService.layman());
}, ...(ngDevMode ? [{ debugName: "withCredentials" }] : []));
this.setDataToDefault();
}
/**
* Sets data object to default
*/
setDataToDefault() {
this.data = {
add_all: null,
extent: null,
folder_name: 'WFS',
layers: [],
map_projection: undefined,
output_format: '',
output_formats: null,
services: [],
srs: null,
srss: [],
title: '',
version: '',
table: {
trackBy: 'Name',
nameProperty: 'Title',
},
};
this.definedProjections = [
'EPSG:3857',
'EPSG:5514',
'EPSG:4258',
'EPSG:4326',
];
}
/**
* List and return layers from WFS getCapabilities response
* @param wrapper - Capabilities response wrapper
*/
async listLayerFromCapabilities(wrapper, layerOptions) {
if (!wrapper.response && !wrapper.error) {
return;
}
if (wrapper.error) {
this.hsAddDataCommonService.throwParsingError(wrapper.response.message);
return;
}
try {
await this.parseCapabilities(wrapper.response);
if (this.hsAddDataCommonService.layerToSelect) {
this.hsAddDataCommonService.checkTheSelectedLayer(this.data.layers, 'wfs');
const collection = this.getLayers(true, false, layerOptions);
if (!layerOptions?.fromComposition) {
this.hsAddDataUrlService.zoomToLayers(this.data);
}
return collection;
}
}
catch (e) {
this.hsAddDataCommonService.throwParsingError(e);
}
}
/**
* Parse information received in WFS getCapabilities response
* @param response - A stringified XML response to getCapabilities request
*/
async parseCapabilities(response) {
try {
this.loadingFeatures = false;
this.data.map_projection ??= this.hsMapService
.getMap()
.getView()
.getProjection()
.getCode()
.toUpperCase();
let caps = xml2Json.xml2js(response, { compact: true });
if (caps['wfs:WFS_Capabilities']) {
caps = caps['wfs:WFS_Capabilities'];
}
else {
caps = caps['WFS_Capabilities'];
}
this.parseWFSJson(caps);
const serviceTitle = caps.ServiceIdentification?.Title;
this.data.title =
typeof serviceTitle === 'string'
? serviceTitle
: this.hsWfsGetCapabilitiesService
.service_url()
.split('//')[1]
.split('/')[0];
// this.description = addAnchors(caps.ServiceIdentification.Abstract);
this.data.version = caps.ServiceIdentification.ServiceTypeVersion;
const layer = Array.isArray(caps.FeatureTypeList.FeatureType)
? caps.FeatureTypeList.FeatureType.find((layer) => layer.Name == this.hsAddDataCommonService.layerToSelect)
: caps.FeatureTypeList.FeatureType;
this.data.layers = Array.isArray(caps.FeatureTypeList.FeatureType)
? caps.FeatureTypeList.FeatureType
: [caps.FeatureTypeList.FeatureType];
if (layer) {
this.data.extent = this.getLayerExtent(layer, this.data.map_projection);
const srsType = layer && layer.DefaultSRS ? 'SRS' : 'CRS';
if (layer['Default' + srsType] !== undefined) {
this.data.srss = [layer['Default' + srsType]];
}
else {
this.data.srss = [];
this.data.srss.push('urn:ogc:def:crs:EPSG::4326');
}
const otherSRS = layer['Other' + srsType];
if (otherSRS) {
if (typeof otherSRS == 'string') {
this.data.srss.push(otherSRS);
}
else {
for (const srs of layer['Other' + srsType]) {
this.data.srss.push(srs);
}
}
}
if (this.data.srss[0] === undefined) {
this.data.srss = [
caps.FeatureTypeList.FeatureType[0]['Default' + srsType],
];
for (const srs of caps.FeatureTypeList.FeatureType[0]['Other' + srsType]) {
this.data.srss.push(srs);
}
}
}
this.data.output_format = this.getPreferredFormat(this.data.version);
const fallbackProj = this.data.map_projection || 'EPSG:3857';
this.data.srss = this.parseEPSG(this.data.srss);
if (this.data.srss.length == 0) {
this.data.srss = [fallbackProj];
this.hsLog.warn(`While loading WFS from ${this.data.title} fallback projection ${fallbackProj} was used.`);
}
this.data.srs =
this.data.srss.find((srs) => srs.includes(fallbackProj)) ||
this.data.srss[0];
if (this.data.layers.length <= 10 ||
this.hsAddDataCommonService.layerToSelect) {
try {
const layers = this.hsAddDataCommonService.layerToSelect
? this.data.layers.filter((l) => l.Name === this.hsAddDataCommonService.layerToSelect)
: this.data.layers;
this.getFeatureCountForLayers(layers, this.hsAddDataCommonService.layerToSelect);
}
catch (e) {
throw new Error(e);
}
}
this.hsAddDataCommonService.loadingInfo = false;
}
catch (e) {
throw new Error(e);
}
}
getLayerExtent(lyr, crs) {
let bbox = lyr.WGS84BoundingBox || lyr.OutputFormats.WGS84BoundingBox;
const lowerCorner = bbox.LowerCorner.split(' ').map(Number);
const upperCorner = bbox.UpperCorner.split(' ').map(Number);
bbox = [...lowerCorner, ...upperCorner];
return transformExtent(bbox, 'EPSG:4326', crs);
}
/**
* For given array of layers (service layer definitions) it calculates a cumulative bounding box which encloses all the layers
*/
calcAllLayersExtent(layers) {
if (layers.length == 0) {
return undefined;
}
const selectedLayerNames = layers.map((l) => l.get('name'));
layers = this.data.layers.filter((lyr) => {
return selectedLayerNames.includes(lyr.Name);
});
const layerExtents = layers.map((lyr) => {
return this.getLayerExtent(lyr, this.data.map_projection);
});
return this.hsAddDataUrlService.calcCombinedExtent(layerExtents);
}
/**
* Get preferred GML version format
* @param version - GML version
*/
getPreferredFormat(version) {
switch (version) {
case '1.0.0':
return 'GML2';
case '1.1.0':
return 'GML3';
case '2.0.0':
return 'GML32';
default:
return 'GML3';
}
}
/**
* Construct and send WFS service getFeature-hits request for a set of layers
*/
getFeatureCountForLayers(layers, selectedLayer) {
for (const layer of layers) {
layer.loading = true;
const params = {
service: 'wfs',
version: this.data.version, //== '2.0.0' ? '1.1.0' : this.version,
request: 'GetFeature',
resultType: 'hits',
};
params[this.data.version.startsWith('1') ? 'typeName' : 'typeNames'] =
layer.Name;
const url = [
this.hsWfsGetCapabilitiesService.service_url().split('?')[0],
paramsToURLWoEncode(params),
].join('?');
this.parseFeatureCount(url, layer, selectedLayer);
}
}
/**
* Parse layer feature count and set feature limits
*/
parseFeatureCount(url, layer, selectedLayer) {
// Create a unique subject for this request
const cancelSubject = new Subject();
// Associate the cancel subject with the request URL
this.requestCancelSubjects.set(url, cancelSubject);
this.http
.get(this.hsProxyService.proxify(url), {
responseType: 'text',
withCredentials: this.withCredentials(),
})
.pipe(takeUntil(this.cancelUrlRequest), finalize(() => {
if (selectedLayer) {
setCluster(layer['olLayer'], layer.featureCount ? layer.featureCount > 5000 : true);
}
}))
.subscribe({
next: (response) => {
const oParser = new DOMParser();
const oDOM = oParser.parseFromString(response, 'application/xml');
const doc = oDOM.documentElement;
layer.featureCount = parseInt(doc.getAttribute('numberOfFeatures'));
//WFS 2.0.0
if (layer.featureCount == 0 || !layer.featureCount) {
layer.featureCount = parseInt(doc.getAttribute('numberMatched'));
}
layer.limitFeatureCount = layer.featureCount > 1000;
layer.loading = false;
this.requestCancelSubjects.delete(url);
},
error: (e) => {
this.cancelRequest(url);
layer.featureCount = -9999;
layer.loading = false;
//this.hsAddDataCommonService.throwParsingError(e);
},
});
}
/**
* Cancel a specific request based on URL as identifier
*/
cancelRequest(url) {
const cancelSubject = this.requestCancelSubjects.get(url);
if (cancelSubject) {
cancelSubject.next();
cancelSubject.complete();
this.requestCancelSubjects.delete(url);
}
}
/**
* Handle table row click event by getting layer feature count if necessary
*/
tableLayerChecked($event, layer) {
if (layer.featureCount === undefined &&
layer.checked) {
this.getFeatureCountForLayers([layer]);
}
}
/**
* Parse WFS json file
* @param json - JSON file
*/
parseWFSJson(json) {
try {
for (const key of Object.keys(json)) {
if (key.includes(':')) {
json[key.substring(4)] = json[key];
if (typeof json[key.substring(4)] == 'object') {
this.parseWFSJson(json[key]);
}
if (json[key.substring(4)] && json[key.substring(4)]['_text']) {
json[key.substring(4)] = json[key.substring(4)]['_text'];
}
delete json[key];
}
if (typeof json[key] == 'object') {
this.parseWFSJson(json[key]);
if (json[key] && json[key]['_text']) {
json[key] = json[key]['_text'];
}
}
}
}
catch (e) {
throw new Error(e);
}
}
/**
* Parse EPSG in usable formats
*/
parseEPSG(srss) {
srss.forEach((srs, index) => {
const epsgCode = srs.slice(-4);
srss[index] = 'EPSG:' + epsgCode;
if (!get(srss[index])) {
srss.splice(srss.indexOf(index), 1);
}
});
return [...Array.from(new Set(srss))].filter((srs) => this.definedProjections.includes(srs));
}
/**
* Finalize layer retrieval
* Calculates extent, zooms to layers, clears params, sets panel to catalogue and resets to default
* @param collection - Layers created and retrieved collection
* @param layerOptions - Layer options
*/
finalizeLayerRetrieval(collection, layerOptions) {
this.data.extent = this.calcAllLayersExtent(collection);
if (!layerOptions?.fromComposition) {
this.hsAddDataUrlService.zoomToLayers(this.data);
}
this.hsAddDataCommonService.clearParams();
this.setDataToDefault();
this.hsAddDataCommonService.setPanelToCatalogue();
}
/**
* Loop through the list of layers and call getLayer
* @