UNPKG

hslayers-ng

Version:
1,204 lines (1,199 loc) 192 kB
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, '&#47;'), 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 * @