UNPKG

@igo2/context

Version:
1,326 lines (1,318 loc) 281 kB
import * as i0 from '@angular/core'; import { inject, Injectable, input, Component, NgModule, output, computed, Directive, ChangeDetectionStrategy, viewChild, model, Input, DestroyRef, signal, ChangeDetectorRef } from '@angular/core'; import { DOCUMENT, AsyncPipe, NgClass, KeyValuePipe } from '@angular/common'; import * as i1$1 from '@angular/forms'; import { UntypedFormBuilder, Validators, FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms'; import * as i4 from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button'; import * as i1 from '@angular/material/button-toggle'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatOptionModule } from '@angular/material/core'; import { MatDividerModule } from '@angular/material/divider'; import * as i1$2 from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field'; import * as i2 from '@angular/material/input'; import { MatInputModule } from '@angular/material/input'; import * as i5 from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select'; import { SpinnerComponent } from '@igo2/common/spinner'; import { ConfigService } from '@igo2/core/config'; import { LanguageService, IgoLanguageModule } from '@igo2/core/language'; import { MessageService } from '@igo2/core/message'; import { BehaviorSubject, Subject, of, Observable, ReplaySubject, timer, catchError as catchError$1, tap as tap$1, switchMap as switchMap$1, map as map$1, EMPTY, combineLatest } from 'rxjs'; import { skip, first, catchError, tap, map, mergeMap, debounceTime, take, switchMap, filter, debounce } from 'rxjs/operators'; import { HttpParams, HttpClient } from '@angular/common/http'; import { AuthService } from '@igo2/auth'; import { RouteService } from '@igo2/core/route'; import { StorageService } from '@igo2/core/storage'; import { QueryFormat, isLayerGroupOptions, ID_GROUP_PREFIX, isLayerGroup, isLayerItem, getLayerOptionIdentifier, findParentId, generateIdFromSourceOptions, ExportService, FeatureDataSource, featureRandomStyleFunction, featureRandomStyle, VectorLayer, ClusterDataSource, MapBrowserComponent, LayerService, StyleListService, StyleService, sortLayersByZindex, mergeLayersOptions, MapService, isLayerItemOptions, moveToOlFeatures, FeatureMotion, FeatureDetailsComponent } from '@igo2/geo'; import { ObjectUtils, uuid, downloadContent } from '@igo2/utils'; import GeoJSON from 'ol/format/GeoJSON'; import olPoint from 'ol/geom/Point'; import Cluster from 'ol/source/Cluster'; import olVectorSource from 'ol/source/Vector'; import * as i2$1 from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import * as i3 from '@angular/material/list'; import { MatListModule } from '@angular/material/list'; import * as i5$1 from '@ngx-translate/core'; import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Clipboard } from '@angular/cdk/clipboard'; import * as i3$1 from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon'; import * as i6 from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MediaService } from '@igo2/core/media'; import * as i5$2 from '@angular/material/menu'; import { MatMenuTrigger, MatMenuModule } from '@angular/material/menu'; import { StopPropagationDirective } from '@igo2/common/stop-propagation'; import * as i8 from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialogRef, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialog } from '@angular/material/dialog'; import { ActionStore, ActionbarMode, ActionbarComponent } from '@igo2/common/action'; import { CollapsibleComponent } from '@igo2/common/collapsible'; import { ConfirmDialogService } from '@igo2/common/confirm-dialog'; import { SORT_ALPHA_ON_ICON, SORT_ALPHA_OFF_ICON, IgoIconComponent } from '@igo2/common/icon'; import { ListComponent, ListItemDirective } from '@igo2/common/list'; import * as i6$1 from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio'; import * as oleasing from 'ol/easing'; import * as olproj from 'ol/proj'; import { trigger, state, style, transition, animate } from '@angular/animations'; import { CustomHtmlComponent } from '@igo2/common/custom-html'; import { MatTabsModule } from '@angular/material/tabs'; import * as i1$3 from '@angular/material/sidenav'; import { MatSidenavModule } from '@angular/material/sidenav'; import { Title } from '@angular/platform-browser'; import { getEntityTitle } from '@igo2/common/entity'; import { FlexibleComponent } from '@igo2/common/flexible'; import { PanelComponent } from '@igo2/common/panel'; const ServiceType = [ 'wms', 'wmts', 'arcgisrest', 'imagearcgisrest', 'tilearcgisrest' ]; var ServiceTypeEnum; (function (ServiceTypeEnum) { ServiceTypeEnum[ServiceTypeEnum["wms"] = 0] = "wms"; ServiceTypeEnum[ServiceTypeEnum["wmts"] = 1] = "wmts"; ServiceTypeEnum[ServiceTypeEnum["arcgisrest"] = 2] = "arcgisrest"; ServiceTypeEnum[ServiceTypeEnum["imagearcgisrest"] = 3] = "imagearcgisrest"; ServiceTypeEnum[ServiceTypeEnum["tilearcgisrest"] = 4] = "tilearcgisrest"; })(ServiceTypeEnum || (ServiceTypeEnum = {})); const BaseLayerParamsKeys = [ 'visible', 'opacity', 'zIndex', 'parentId' ]; const LayerParamsKeys = [ 'id', 'index', 'names', 'type', 'version', 'queryString', ...BaseLayerParamsKeys ]; const GroupParamsKeys = [ 'title', 'id', 'expanded', ...BaseLayerParamsKeys ]; const PositionParamsKeys = [ 'center', 'zoom', 'rotation', 'projection' ]; const SHARE_MAP_KEYS_DEFAULT_OPTIONS = { context: 'ctx', urls: 'urls', position: 'pos', layers: 'layers', groups: 'groups', center: 'ctr', zoom: 'z', projection: 'p', rotation: 'r', opacity: 'o' }; function shareMapKeyDefs(options) { return { contextKey: options.context, urlsKey: options.urls, languageKey: options.languageKey, pos: { key: options.position, params: { zoom: { key: options.zoom, parse: (params) => parseIntergerParam(params, options.zoom) }, center: { key: '@', parse: parseCenter, stringify: stringifyCenter }, rotation: { key: options.rotation, parse: (params) => parseRotation(params, options.rotation), stringify: formatNumber }, projection: { key: options.projection, parse: (params) => extractParam(params, options.projection) } } }, layers: { key: options.layers, params: { index: undefined, id: { key: 'id', parse: (params) => extractParam(params, 'id'), stringify: (value) => `${value}` }, names: { key: 'n', parse: (params) => { const param = extractParam(params, 'n'); if (!param) return undefined; const unbracketed = param.slice(1, -1); return decodeURIComponent(unbracketed); }, stringify: (value) => `[${encodeURIComponent(value)}]` }, opacity: { key: 'o', parse: (params) => parseFloatParam(params, 'o') }, visible: { key: 'v', parse: (params) => parseBooleanParam(params, 'v'), stringify: stringifyBoolean }, type: { key: 't', parse: (params) => parseLayerType(params, 't'), stringify: stringifyType }, zIndex: { key: 'z', parse: (params) => parseIntergerParam(params, 'z') }, parentId: { key: 'pid', parse: (params) => extractParam(params, 'pid') }, version: { key: 'vrn', parse: (params) => extractParam(params, 'vrn') }, queryString: { key: 'q', parse: (params) => extractParam(params, 'q') } } }, groups: { key: options.groups, params: { id: { key: 'id', parse: (params) => extractIdParam(params, 'id') }, parentId: { key: 'pid', parse: (params) => extractIdParam(params, 'pid') }, title: { key: 't', parse: (params) => { const param = extractParam(params, 't'); return param ? decodeURIComponent(param) : undefined; }, stringify: (value) => encodeURIComponent(value) }, visible: { key: 'v', parse: (params) => parseBooleanParam(params, 'v'), stringify: stringifyBoolean }, opacity: { key: 'o', parse: (params) => parseFloatParam(params, 'o') }, zIndex: { key: 'z', parse: (params) => parseIntergerParam(params, 'z') }, expanded: { key: 'e', parse: (params) => parseBooleanParam(params, 'e'), stringify: stringifyBoolean } } } }; } function stringifyBoolean(value) { return value ? '1' : '0'; } function parseBoolean(params) { return !!parseInt(params); } function stringifyType(value) { return String(ServiceTypeEnum[value]); } function parseLayerType(params, key) { const param = extractParam(params, key); if (!param) { return; } const type = Number(param); return ServiceTypeEnum[type]; } function stringifyCenter(values) { return values.map(formatNumber).join(','); } function parseCenter(params) { if (!params?.startsWith('@')) { return; } const paramsSplitted = params.split(','); const index = paramsSplitted.findIndex((param) => param.startsWith('@')); return [ Number(paramsSplitted[index].slice(1)), Number(paramsSplitted[index + 1]) ]; } function parseRotation(params, key) { const param = extractParam(params, key); if (!param) { return; } const degree = parseInteger(param); return (degree * Math.PI) / 180; } function parseIntergerParam(params, key) { const param = extractParam(params, key); if (!param) { return; } return parseInteger(param); } function parseBooleanParam(params, key) { const param = extractParam(params, key); if (!param) { return; } return parseBoolean(param); } function parseFloatParam(params, key) { const param = extractParam(params, key); if (!param) { return; } return parseFloat(param); } function extractParam(params, suffix) { return params ?.split(',') .find((param) => param.endsWith(suffix)) ?.slice(0, -suffix.length); } function extractIdParam(params, suffix) { const paramsSplited = params.split(','); // Find the parameter that ends with exact id/pid and isn't part of another word const param = paramsSplited.find((param) => { const key = param.slice(-suffix.length); if (key !== suffix) { return false; } if (suffix === 'pid') { return true; } const prefixChar = param.slice(-suffix.length - 1, -suffix.length); return prefixChar !== 'p'; }); return param?.slice(0, -suffix.length); } function parseInteger(value) { return parseInt(value, 10); } function formatNumber(value) { return value.toFixed(5).replace(/\.([^0]+)0+$/, '.$1'); } function buildDataSourceOptions(type, url, layers, version) { const isLayerType = [ 'wmts', 'arcgisrest', 'imagearcgisrest', 'tilearcgisrest' ].includes(type); const arcgisClause = type === 'arcgisrest' || type === 'imagearcgisrest' || type === 'tilearcgisrest'; const params = type === 'wms' ? { LAYERS: layers.join(','), VERSION: version } : undefined; const layer = isLayerType ? layers.join(',') : undefined; const baseParams = { type: type, url, params, layer, version: type === 'wmts' ? '1.0.0' : undefined }; if (arcgisClause) { return { ...baseParams, queryable: true, queryFormat: QueryFormat.ESRIJSON }; } return baseParams; } function getFlattenOptions(options) { return options.reduce((accumulator, option) => { if (isLayerGroupOptions(option)) { const children = option.children ? getFlattenOptions(option.children) : []; accumulator.push(option, ...children); } else { accumulator.push(option); } return accumulator; }, []); } /** * Checks if the provided query parameters contain legacy parameter pairs * (e.g., layers and URLs) that indicate older configuration formats. */ function hasLegacyParams(params, optionsLegacy) { const { layersKey, wmsUrlKey, wmsLayersKey, wmtsUrlKey, wmtsLayersKey, arcgisUrlKey, arcgisLayersKey, iarcgisUrlKey, iarcgisLayersKey, tarcgisUrlKey, tarcgisLayersKey } = optionsLegacy; // Define valid legacy parameter pairs const legacyPairs = [ [layersKey, wmsUrlKey], [wmsLayersKey, wmsUrlKey], [wmtsLayersKey, wmtsUrlKey], [arcgisLayersKey, arcgisUrlKey], [iarcgisLayersKey, iarcgisUrlKey], [tarcgisLayersKey, tarcgisUrlKey] ].filter(([layer, url]) => layer && url); // Check if any legacy pair exists in the query parameters return legacyPairs.some(([layer, url]) => getParamValue(params, layer) && getParamValue(params, url)); } function hasModernShareParams(params, keysDefinitions) { const { contextKey, groups } = keysDefinitions; const hasGroups = !!getParamValue(params, groups.key); const hasContext = !!getParamValue(params, contextKey); return hasContext || hasGroups; } function getParamValue(params, key) { const value = params[key]; return value !== '' ? value : undefined; } class ShareMapEncoder { SHARE_MAP_DEFS; document; context; language; constructor(SHARE_MAP_DEFS, document) { this.SHARE_MAP_DEFS = SHARE_MAP_DEFS; this.document = document; } generateUrl(map, context) { this.context = context; const layers = [ map.layerController.baseLayer, ...map.layerController.layersFlattened ].filter(Boolean); const urlParams = this.getBaseUrlConfig(map.viewController); this.buildQueryUrl(layers, urlParams); const [baseUrl] = this.document.location.href.split('?'); const queryString = urlParams.toString(); return queryString !== '' ? `${baseUrl}?${queryString}` : baseUrl; } /** * Replaces local group IDs with unique IDs to avoid conflicts and have short URL. * This is necessary for sharing the map context. */ replaceGroupLocalIds(layers) { const idMap = new Map(); const existingIds = new Set(layers.map((layer) => layer.id).filter(Boolean)); // eslint-disable-next-line prefer-const let counter = 1; layers.forEach((layer) => { if (layer.id && layer.id.toString().includes(ID_GROUP_PREFIX)) { const newId = this.getUniqueId(existingIds, counter); idMap.set(layer.id, newId); layer.options.id = newId; } }); } getUniqueId(existingIds, counter) { while (existingIds.has(String(counter))) { counter++; } existingIds.add(String(counter)); return counter; } getCurrentContext() { return ObjectUtils.removeUndefined({ layers: this.context?.layers, center: this.context?.map.view.center, projection: this.context?.map.view.projection, zoom: this.context?.map.view.zoom, rotation: this.context?.map.view.rotation }); } /** * Filters layers to include only LayerGroups or LayerItems with a valid ServiceType. */ isLayerSharable(layers) { return layers.filter((layer) => isLayerGroup(layer) || (isLayerItem(layer) && ServiceType.includes(layer.dataSource?.options?.type))); } /** * Extracts only LayerGroup items from the filtered layers. */ getLayerGroups(layers) { return layers.filter(isLayerGroup); } /** * Extracts only LayerItem items from the filtered layers. */ getLayerItems(layers) { return layers.filter(isLayerItem); } buildQueryUrl(layers, urlParams) { const layersSharable = this.isLayerSharable(layers); this.replaceGroupLocalIds(layersSharable); const layersChanged = this.getFilteredMapLayers(layersSharable); const groups = this.getLayerGroups(layersChanged); const groupsQueryValue = this.buildGroupsQueryValue(groups); const layersByService = this.generateLayersOptionsByService(this.getLayerItems(layersChanged)); const [urls, layerParams] = this.buildLayersQueryValues(layersByService); if (urls.length > 0) { urlParams.set(this.SHARE_MAP_DEFS.urlsKey, urls.join(',')); } if (layerParams.length > 0) { urlParams.set(this.SHARE_MAP_DEFS.layers.key, layerParams.join(';')); } if (groupsQueryValue) { urlParams.set(this.SHARE_MAP_DEFS.groups.key, groupsQueryValue); } } /** * Retrieves the current context layers and maps them by identifier. */ getContextLayersMap() { const ctxLayers = this.getCurrentContext()?.layers || []; const ctxFlattened = getFlattenOptions(ctxLayers); return new Map(ctxFlattened.map((lctx) => { const identifier = getLayerOptionIdentifier(lctx); return [identifier, lctx]; })); } /** * Filters layers and context layers based on visibility, opacity, and expanded state. */ getFilteredMapLayers(filteredLayers) { const ctxLayersMap = this.getContextLayersMap(); return filteredLayers.filter((layer) => { const mapLayerIdentifier = getLayerOptionIdentifier(layer.options); if (!mapLayerIdentifier) return false; const ctxLayer = ctxLayersMap.get(mapLayerIdentifier); if (!ctxLayer) return true; const visibilityChange = layer.visible !== (ctxLayer.visible ?? true); const opacityChange = layer.opacity !== (ctxLayer.opacity ?? 1); const layerParentId = this.getIdsNestedParent(layer)?.join('.'); const ctxParentId = ctxLayer.parentId ?? findParentId(this.context.layers, ctxLayer); const parentIdChange = ctxParentId !== layerParentId; let expandedChange = false; let titleChange = false; if (isLayerGroup(layer) && isLayerGroupOptions(ctxLayer)) { // Check if the layer group was assign an local id // If so, we don't support any change for this case if (String(ctxLayer.id).includes(ID_GROUP_PREFIX)) { return false; } expandedChange = (ctxLayer.expanded ?? false) !== layer.expanded; titleChange = (ctxLayer.title ?? false) !== layer.title; } return (visibilityChange || opacityChange || expandedChange || titleChange || parentIdChange); }); } getIdsNestedParent(node, ids) { if (!node.parent) return ids; if (node.parent) { if (!ids) { ids = [node.parent.id]; } else { ids.unshift(node.parent.id); } return this.getIdsNestedParent(node.parent, ids); } return ids; } generateLayersOptionsByService(layers) { const layersByUrl = new Map(); layers.forEach((layer) => { const [url, params] = this.generateLayerOption(layer); if (!layersByUrl.has(url)) { layersByUrl.set(url, [params]); } else { layersByUrl.get(url).push(params); } }); let customIndex = 0; return Array.from(layersByUrl.entries()).map(([url, layerParams]) => { const hasNoId = layerParams.some((p) => !p.id); if (hasNoId) { layerParams.forEach((params) => { if (!params.id) { params.index = customIndex; } }); customIndex++; } return [url, layerParams]; }); } generateLayerOption(layer) { const dataSourceOptions = layer.dataSource.options; return [ this.concatUrlWithVersion(dataSourceOptions), this.getLayerParams(layer) ]; } getLayerNames(dataSourceOptions) { const type = dataSourceOptions.type.toLowerCase(); if (type === 'wms') { const params = dataSourceOptions .params; return params.LAYERS; } return 'layer' in dataSourceOptions ? dataSourceOptions.layer : ''; } getWmsVersion(dataSourceOptions) { const params = dataSourceOptions?.params; return params?.VERSION && params.VERSION !== '1.3.0' ? params.VERSION : undefined; } concatUrlWithVersion(dataSourceOptions) { const url = dataSourceOptions.url; if (dataSourceOptions.type.toLowerCase() === 'wms') { const version = this.getWmsVersion(dataSourceOptions); if (version) { const operator = url.includes('?') ? '&' : '?'; const { version: versionDef } = this.SHARE_MAP_DEFS.layers.params; return `${url}${operator}${versionDef.key}=${version}`; } } return dataSourceOptions.url; } getLayerParams(layer) { const dataSourceOptions = layer.dataSource.options; const isExisting = this.context?.layers ? this.hasLayerId(this.context.layers, layer.id) : false; return { index: undefined, ...(isExisting ? { id: layer.id } : { names: this.getLayerNames(dataSourceOptions), type: dataSourceOptions?.type }), opacity: this.getOpacity(layer.opacity), parentId: layer.parent?.id, visible: this.getVisibility(layer.visible), zIndex: layer.zIndex }; } /** Recursive */ hasLayerId(layersOptions, targetId) { if (targetId == null) return false; return layersOptions.some((l) => { if (l.id == null) return false; if (!isLayerGroupOptions(l)) { return String(l.id) === String(targetId); } if (isLayerGroupOptions(l) && Array.isArray(l.children)) { return this.hasLayerId(l.children, targetId); } return false; }); } getOpacity(opacity) { return opacity === 1 ? undefined : opacity; } getVisibility(visibility) { return !!visibility; } buildLayersQueryValues(layersByService) { const urls = []; const layerParams = []; for (const [url, layer] of layersByService) { let needUrl = false; for (const param of layer) { layerParams.push(this.stringifyLayerParams(param)); if (param.id == null) { needUrl = true; } } // If some layer have no id push the url service. if (needUrl) { urls.push(url); } } return [urls, layerParams]; } stringifyLayerParams(params) { const { index, ...restParams } = params; const stringifiedParams = this.stringifyDefinitions(restParams, this.SHARE_MAP_DEFS.layers.params); return restParams.id != null ? `${stringifiedParams}` : `${index},${stringifiedParams}`; } getBaseUrlConfig(viewController) { const { pos, contextKey, languageKey } = this.SHARE_MAP_DEFS; const href = this.document.location.href; const urlParams = this.getSanitizedParams(href); if (pos) { const positionStringified = this.stringifyPosition(this.getPosition(viewController)); urlParams.set(pos.key, positionStringified); } const contextUri = this.context?.uri; if (contextUri) urlParams.set(contextKey, contextUri); if (this.language && !urlParams.has(languageKey)) urlParams.set(languageKey, this.language); return urlParams; } getSanitizedParams(baseUrl) { const [, queryString] = baseUrl.split('?'); const params = new URLSearchParams(queryString); const keys = this.extractKeys(this.SHARE_MAP_DEFS); keys.forEach((key) => { params.delete(key); }); return params; } extractKeys(defs) { const keys = []; for (const key in defs) { if (Object.prototype.hasOwnProperty.call(defs, key)) { const value = defs[key]; if (typeof value === 'string') { keys.push(value); } else if (typeof value === 'object' && value !== null) { keys.push(value.key); } } } return keys; } getPosition(viewController) { return ObjectUtils.removeUndefined({ center: viewController.getCenter('EPSG:4326'), zoom: this.getZoom(viewController), rotation: this.getRotation(viewController), projection: this.getProjection(viewController) }); } getProjection(viewController) { const ctxProjection = this.getCurrentContext().projection; const mapProjection = viewController.getOlProjection().getCode(); return ctxProjection === mapProjection ? undefined : mapProjection; } getZoom(viewController) { const mapZoom = viewController.getZoom(); const ctxZoom = this.getCurrentContext().zoom; return ctxZoom === mapZoom ? undefined : mapZoom; } getRotation(viewController) { const rotationRadians = viewController.getRotation(); const rotationDegree = (rotationRadians * 180) / Math.PI; const ctxRotation = this.getCurrentContext().rotation; return rotationDegree === ctxRotation || rotationDegree === 0 ? undefined : rotationDegree; } stringifyPosition(position) { const definitions = this.SHARE_MAP_DEFS.pos.params; const { center, ...restPosition } = position; const stringifiedParams = this.stringifyDefinitions(restPosition, definitions); const result = [ `${definitions.center.key}${definitions.center.stringify(center)}`, stringifiedParams ].filter(Boolean); return result.join(','); } stringifyDefinitions(values, definitions) { const result = Object.keys(ObjectUtils.removeUndefined(values)) .map((key) => { const { key: defKey, stringify } = definitions[key]; const value = stringify ? stringify(values[key]) : values[key]; return `${value}${defKey}`; }) .join(','); return result === '' ? undefined : result; } buildGroupsQueryValue(layers) { if (layers.length === 0) return undefined; return layers .map((layer) => { const params = this.getLayerGroupParams(layer); return this.stringifyGroupParams(params); }) .join(';'); } getLayerGroupParams(layer) { return { id: layer.id, title: layer.title, zIndex: layer.zIndex, parentId: layer.parent?.id, visible: this.getVisibility(layer.visible), opacity: this.getOpacity(layer.opacity), expanded: layer.expanded }; } stringifyGroupParams(params) { return this.stringifyDefinitions(params, this.SHARE_MAP_DEFS.groups.params); } } class ShareMapLegacyParser { options; constructor(options) { this.options = options; } parseUrl(params) { const layerOptions = ServiceType.flatMap((type) => this.readLayersQueryParamsByType(params, type)).filter(Boolean); return layerOptions; } parsePosition(params) { const center = params[this.options.centerKey]; const projection = params[this.options.projectionKey]; const zoom = params[this.options.zoomKey]; const rotation = params[this.options.rotationKey]; return ObjectUtils.removeUndefined({ center: center?.split(',').map(Number), projection, zoom: zoom ? Number(zoom) : undefined, rotation: rotation ? Number(rotation) : undefined }); } readLayersQueryParamsByType(params, type) { const [nameParamLayersKey, urlsKey] = this.getQueryKeyByType(params, type); if (!nameParamLayersKey || !urlsKey) { return undefined; } const layersByService = params[nameParamLayersKey].split('),('); const urls = params[urlsKey].split(','); return urls .map((urlSrc, index) => { // Sanitize URL and extract version const [url, version] = this.sanitizeUrl(urlSrc); const layersConfig = this.extractLayersByService(this.removeParenthesis(layersByService[index])); // Generate layer options for the current service return this.extractLayersOptions(layersConfig, url, type, version, params); }) .flat(); } getQueryKeyByType(params, type) { let nameParamLayersKey; let urlsKey; const { layersKey, wmsUrlKey, wmsLayersKey, wmtsUrlKey, wmtsLayersKey, arcgisUrlKey, arcgisLayersKey, iarcgisUrlKey, iarcgisLayersKey, tarcgisUrlKey, tarcgisLayersKey } = this.options; switch (type) { case 'wms': if ((params[layersKey] || params[wmsLayersKey]) && params[wmsUrlKey]) { urlsKey = wmsUrlKey; nameParamLayersKey = params[wmsLayersKey] ? wmsLayersKey : layersKey; } break; case 'wmts': if (params[wmtsLayersKey] && params[wmtsUrlKey]) { urlsKey = wmtsUrlKey; nameParamLayersKey = wmtsLayersKey; } break; case 'arcgisrest': if (params[arcgisLayersKey] && params[arcgisUrlKey]) { urlsKey = arcgisUrlKey; nameParamLayersKey = arcgisLayersKey; } break; case 'imagearcgisrest': if (params[iarcgisLayersKey] && params[iarcgisUrlKey]) { urlsKey = iarcgisUrlKey; nameParamLayersKey = iarcgisLayersKey; } break; case 'tilearcgisrest': if (params[tarcgisLayersKey] && params[tarcgisUrlKey]) { urlsKey = tarcgisUrlKey; nameParamLayersKey = tarcgisLayersKey; } break; } if (!nameParamLayersKey || !urlsKey) { return [undefined, undefined]; } return [nameParamLayersKey, urlsKey]; } sanitizeUrl(url) { const version = this.getQueryParam('version', url.toLocaleLowerCase()) || undefined; if (version) { const versionRegex = new RegExp(`[?&]version=${version}`, 'i'); url = url.replace(versionRegex, '').replace(/[?&]$/, ''); } if (url.endsWith('?')) { url = url.substring(0, url.length - 1); } return [url, version]; } getQueryParam(name, url) { let paramValue; if (url.includes('?')) { const httpParams = new HttpParams({ fromString: url.split('?')[1] }); paramValue = httpParams.get(name); } return paramValue; } extractLayersByService(layersByService) { if (!layersByService.includes(':igoz')) { return [ { layers: layersByService.split(','), zIndex: null } ]; } const layers = layersByService.match(/([^(),:]+(?:,[^(),:]+)*(:[^(),:]+(?:[:][^(),:]+)*)?)/g); return layers.map((layer) => { const [names, zIndex] = layer.split(':igoz'); return { layers: names.split(','), zIndex: parseInt(zIndex) }; }); } removeParenthesis(value) { if (value.startsWith('(')) { value = value.substr(1); } if (value.endsWith(')')) { value = value.slice(0, -1); } return value; } extractLayersOptions(layersConfig, url, type, version, params) { return layersConfig.map((layerConfig) => { const sourceOptions = buildDataSourceOptions(type, url, layerConfig.layers, version); const id = generateIdFromSourceOptions(sourceOptions); const visible = this.computeLayerVisibilityFromUrl(params, id); return ObjectUtils.removeUndefined({ id, visible: visible, zIndex: layerConfig.zIndex, sourceOptions }); }); } computeLayerVisibilityFromUrl(params, currentLayerid) { const queryParams = params; let visible = true; if (!queryParams || !currentLayerid) { return visible; } let visibleOnLayersParams = ''; let visibleOffLayersParams = ''; let visiblelayers = []; let invisiblelayers = []; if (queryParams['visiblelayers']) { visibleOnLayersParams = queryParams['visiblelayers']; } if (queryParams['invisiblelayers']) { visibleOffLayersParams = queryParams['invisiblelayers']; } /* This order is important because to control whichever the order of * param. First whe open and close everything.*/ if (visibleOnLayersParams === '*') { visible = true; } if (visibleOffLayersParams === '*') { visible = false; } // After, managing named layer by id (context.json OR id from datasource) visiblelayers = visibleOnLayersParams.split(','); invisiblelayers = visibleOffLayersParams.split(','); if (visiblelayers.indexOf(currentLayerid) > -1 || visiblelayers.indexOf(currentLayerid.toString()) > -1) { visible = true; } if (invisiblelayers.indexOf(currentLayerid) > -1 || invisiblelayers.indexOf(currentLayerid.toString()) > -1) { visible = false; } return visible; } } class ShareMapParser { keysDefinitions; legacyOptions; legacy; constructor(keysDefinitions, legacyOptions) { this.keysDefinitions = keysDefinitions; this.legacyOptions = legacyOptions; this.legacy = new ShareMapLegacyParser(legacyOptions); } parseLayers(params) { if (!hasModernShareParams(params, this.keysDefinitions) && hasLegacyParams(params, this.legacyOptions)) { return this.legacy.parseUrl(params); } const { urlsKey, layers: layersDef, groups: groupsDef } = this.keysDefinitions; const layersArray = this.splitParam(getParamValue(params, layersDef.key), ';'); const urlsArray = this.splitParam(getParamValue(params, urlsKey), ','); const groupsArray = this.splitParam(getParamValue(params, groupsDef.key), ';'); const groupsOptions = groupsArray.map((layer) => this.parseGroup(layer)); const layersOptions = layersArray .map((layer) => this.parseLayer(layer, urlsArray)) .filter(Boolean); return [...groupsOptions, ...layersOptions]; } splitParam(value, delimiter) { return value ? value.split(delimiter) : []; } parsePosition(params) { const position = decodeURIComponent(params[this.keysDefinitions.pos.key]); if (!position) { return this.legacy.parsePosition(params); } const { center, zoom, rotation, projection } = this.keysDefinitions.pos.params; return ObjectUtils.removeUndefined({ center: center.parse(position), zoom: zoom.parse(position), rotation: rotation.parse(position), projection: projection.parse(position) }); } parseLayer(layer, urls) { const { zIndex, visibility, type, opacity, parentId } = this.extractLayerProperties(layer); const base = { visible: visibility, zIndex, opacity, parentId }; const layerNames = this.extractLayerNames(layer); if (layerNames) { const urlIndex = this.extractUrlIndex(layer); if (urlIndex === undefined) return undefined; const url = urls[urlIndex]; const version = this.extractVersionFromUrl(url); const sourceOptions = buildDataSourceOptions(type, url, layerNames, version); return ObjectUtils.removeUndefined({ id: undefined, sourceOptions, ...base }); } const id = this.extractLayerId(layer); if (!id) return undefined; return ObjectUtils.removeUndefined({ id, sourceOptions: undefined, ...base }); } parseGroup(properties) { const { params } = this.keysDefinitions.groups; return ObjectUtils.removeUndefined({ id: params.id.parse(properties), title: params.title.parse(properties), zIndex: params.zIndex.parse(properties), visible: params.visible.parse(properties), opacity: params.opacity.parse(properties), parentId: params.parentId.parse(properties), expanded: params.expanded.parse(properties), type: 'group' }); } extractVersionFromUrl(url) { const versionDef = this.keysDefinitions.layers.params.version; return versionDef.parse(url); } extractLayerId(layer) { const { id } = this.keysDefinitions.layers.params; const regex = new RegExp(`([a-zA-Z0-9_]+)${id.key}\\b`); const match = layer.match(regex); return match ? match[1] : undefined; } extractUrlIndex(layer) { const { index } = this.keysDefinitions.layers.params; const regex = index ? new RegExp(`([\\d.]+)${index}`) : /([\d.]+)/; const match = layer.match(regex); return match ? parseInt(match[1], 10) : undefined; } extractLayerNames(layer) { const { names } = this.keysDefinitions.layers.params; const pattern = new RegExp(`\\[.*?\\]${names.key}`, 'g'); const matches = layer.match(pattern); if (!matches) return undefined; return matches .map((match) => decodeURIComponent(match.slice(1, -`]${names.key}`.length))) .join('\n') .split(','); } extractLayerProperties(properties) { const { params } = this.keysDefinitions.layers; return { zIndex: params.zIndex.parse(properties), visibility: params.visible.parse(properties), type: params.type.parse(properties), opacity: params.opacity.parse(properties), parentId: params.parentId.parse(properties) }; } } class ShareMapService { routeService = inject(RouteService); document = inject(DOCUMENT); get language() { return this._language; } set language(value) { this._language = value; if (this.encoder) { this.encoder.language = value; } } _language = ''; options; optionsLegacy; keysDefinitions; encoder; parser; constructor() { this.options = SHARE_MAP_KEYS_DEFAULT_OPTIONS; this.optionsLegacy = this.routeService.legacyOptions; this.keysDefinitions = shareMapKeyDefs({ ...SHARE_MAP_KEYS_DEFAULT_OPTIONS, languageKey: this.routeService.options.languageKey }); this.encoder = new ShareMapEncoder(this.keysDefinitions, this.document); this.parser = new ShareMapParser(this.keysDefinitions, this.routeService.legacyOptions); this.routeService.queryParams.subscribe((params) => { const language = params[this.keysDefinitions.languageKey]; if (language) { this.language = language; } }); } generateUrl(map, context) { return this.encoder.generateUrl(map, context); } parsePosition(params) { return this.parser.parsePosition(params); } parseLayers(params) { return this.parser.parseLayers(params); } sanitizeBaseUrl(baseUrl) { const params = this.encoder.getSanitizedParams(baseUrl); const [base] = baseUrl.split('?'); const queryString = params.toString(); return queryString !== '' ? `${base}?${queryString}&` : `${base}?`; } getContext(params) { return (params[this.options.context] ?? params[this.optionsLegacy.contextKey]); } getZoom(params) { return this.parsePosition(params).zoom; } getUrlWithApi(formValues) { const loc = this.document.location; const origin = loc.origin; const pathname = loc.pathname; const search = loc.search; const params = new URLSearchParams(search); params.set('context', formValues.uri); if (this.language) { params.set('lang', this.language); } return `${origin}${pathname}?${params.toString()}`; } hasPositionParams(params) { const { projectionKey, rotationKey, zoomKey, centerKey } = this.optionsLegacy; const { pos } = this.keysDefinitions; return Boolean(params[pos.key] || params[projectionKey] || params[rotationKey] || params[zoomKey] || params[centerKey]); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ShareMapService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ShareMapService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: ShareMapService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class ContextService { http = inject(HttpClient); authService = inject(AuthService); languageService = inject(LanguageService); config = inject(ConfigService); messageService = inject(MessageService); storageService = inject(StorageService); exportService = inject(ExportService); shareMapService = inject(ShareMapService); route = inject(RouteService, { optional: true }); context$ = new BehaviorSubject(undefined); contexts$ = new BehaviorSubject({ ours: [] }); defaultContextId$ = new BehaviorSubject(undefined); editedContext$ = new BehaviorSubject(undefined); importedContext = []; toolsChanged$ = new Subject(); mapViewFromRoute = {}; options; baseUrl; // Until the ContextService is completely refactored, this is needed // to track the current tools tools; toolbar; get defaultContextUri() { return (this.storageService.get('favorite.context.uri') || this._defaultContextUri || this.options.defaultContextUri); } set defaultContextUri(uri) { this._defaultContextUri = uri; } _defaultContextUri; constructor() { this.options = Object.assign({ basePath: 'contexts', contextListFile: '_contexts.json', defaultContextUri: '_default' }, this.config.getConfig('context')); this.baseUrl = this.options.url ?? ''; if (this.authService.hasAuthService) { this.authService.logged$.subscribe((logged) => { if (logged) { this.contexts$.pipe(skip(1), first()).subscribe(() => { this.handleContextsChange(); }); this.loadContexts(); } }); } else { this.loadContexts(); this.handleContextsChange(false); } } get(permissions, hidden) { let url = this.baseUrl + '/contexts'; if (permissions && this.authService.authenticated) { url += '?permission=' + permissions.join(); if (hidden) { url += '&hidden=true'; } } return this.http.get(url); } getById(id) { const url = this.baseUrl + '/contexts/' + id; return this.http.get(url); } getDetails(id) { const url = `${this.baseUrl}/contexts/${id}/details`; return this.http.get(url).pipe(catchError((res) => { this.handleError(res, id); throw res; })); } getDetailsByUri(uri) { const url = `${this.baseUrl}/contexts/uri/${uri}/details`; return this.http.get(url).pipe(catchError((res) => { this.handleError(res, uri); throw res; })); } getDefault() { if (this.authService.authenticated) { const url = this.baseUrl + '/contexts/default'; return this.http.get(url).pipe(tap((context) => { this.defaultContextId$.next(context.id); })); } else { const uri = this.storageService.get('favorite.context.uri'); this.defaultContextId$.next(uri); return this.getContextByUri(uri); } } getProfilByUser() { if (this.baseUrl) { const url = this.baseUrl + '/profils?'; return this.http.get(url); } return of([]); } setDefault(id) { if (this.authService.authenticated) { const url = this.baseUrl + '/contexts/default'; return this.http.post(url, { defaultContextId: id }); } else { return this.setDefaultLocalStorage(id); } } setDefaultLocalStorage(id) { const selectedId = this.storageService.get('favorite.context.uri'); if (selectedId === id) { this.storageService.remove('favorite.context.uri'); } else { this.storageService.set('favorite.context.uri', id); } return of(selectedId === id ? undefined : id); } hideContext(id) { const url = this.baseUrl + '/contexts/' + id + '/hide'; return this.http.post(url, {}); } showContext(id) { const url = this.baseUrl + '/contexts/' + id + '/show'; return this.http.post(url, {}); } delete(id, imported = false) { const contexts = { ours: [] }; Object.keys(this.contexts$.value).forEach((key) => (contexts[key] = this.contexts$.value[key].filter((c) => c.id !== id))); if (imported) { this.importedContext = this.importedContext.filter((c) => c.id !== id); return of(this.contexts$.next(contexts)); } const url = this.baseUrl + '/contexts/' + id; return this.http.delete(url).pipe(tap(() => { this.contexts$.next(contexts); this.loadContext(this.defaultContextUri); })); } create(context) { const url = this.baseUrl + '/contexts'; return this.http.post(url, context).pipe(map((contextCreated) => { if (