UNPKG

hslayers-ng

Version:
1,168 lines (1,156 loc) 39.6 kB
import IDW from 'ol-ext/source/IDW'; import { Big } from 'big.js'; import { ImageWMS, TileArcGISRest, TileWMS, WMTS, XYZ, Cluster, Vector as Vector$1 } from 'ol/source'; import { isEmpty } from 'ol/extent'; import { Tile, Image, Vector, VectorImage } from 'ol/layer'; import { get, transform, METERS_PER_UNIT, transformExtent } from 'ol/proj'; import { getTitle, getShowInLayerManager, getEditor, getName, getCluster, getHighlighted } from 'hslayers-ng/common/extensions'; import { getArea, getDistance } from 'ol/sphere'; import * as i0 from '@angular/core'; import { inject, Injectable } from '@angular/core'; import { HsConfig } from 'hslayers-ng/config'; import Feature from 'ol/Feature'; import { fromExtent } from 'ol/geom/Polygon'; import { Fill, Style, Stroke } from 'ol/style'; import VectorSource from 'ol/source/Vector'; import VectorLayer from 'ol/layer/Vector'; /** * @param url - URL for which to determine port number * @returns Port number */ function getPortFromUrl(url) { try { const link = document.createElement('a'); link.setAttribute('href', url); if (link.port == '') { if (url.startsWith('https://')) { return '443'; } if (url.startsWith('http://')) { return '80'; } } return link.port; } catch (e) { console.error('Invalid URL provided to getPortFromUrl:', url); return ''; } } /** * Parse parameters and their values from URL string * @param str - URL to parse parameters from * @returns Object with parsed parameters as properties */ function getParamsFromUrl(str) { if (typeof str !== 'string') { return {}; } if (str.includes('?')) { str = str.substring(str.indexOf('?') + 1); } else { return {}; } return str .trim() .split('&') .reduce((ret, param) => { if (!param) { return ret; } const parts = param.replace(/\+/g, ' ').split('='); let key = parts[0]; let val = parts[1]; key = decodeURIComponent(key); // missing `=` should be `null`: // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters val = val === undefined ? null : decodeURIComponent(val); if (!ret.hasOwnProperty(key)) { ret[key] = val; } else if (Array.isArray(ret[key])) { ret[key].push(val); } else { ret[key] = [ret[key], val]; } return ret; }, {}); } /** * Create encoded URL string from object with parameters * @param params - Parameter object with parameter key-value pairs * @returns Joined encoded URL query string */ function paramsToURL(params) { const pairs = []; for (const key in params) { if (params.hasOwnProperty(key) && params[key] !== undefined) { pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); } } return pairs.join('&'); } /** * Insert every element in the set of matched elements after the target. * @param newNode - Element to insert * @param referenceNode - Element after which to insert */ function insertAfter(newNode, referenceNode) { if (newNode.length !== undefined && newNode.length > 0) { newNode = newNode[0]; } referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } /** * Create URL string from object with parameters without encoding * @param params - Parameter object with parameter key-value pairs * @returns Joined URL query string */ function paramsToURLWoEncode(params) { const pairs = []; for (const key in params) { if (params.hasOwnProperty(key)) { pairs.push(key + '=' + params[key]); } } return pairs.join('&'); } /** * Returns a function, that, as long as it continues to be * invoked, will not be triggered. * (https://davidwalsh.name/javascript-debounce-function) * @param func - Function to execute with throttling * @param wait - The function will be called after it stops * being called for N milliseconds. * @param immediate - If `immediate` is passed, trigger the * function on the leading edge, instead of the trailing. * @param context - Context element which stores the timeout handle * @returns Returns function which is debounced */ function debounce(func, wait, immediate, context) { // eslint-disable-next-line @typescript-eslint/no-this-alias context ??= this; return function (...args) { const later = function () { if (!immediate) { func.apply(context, args); } context.timeout = null; }; const callNow = immediate && !context.timeout; clearTimeout(context.timeout); context.timeout = setTimeout(later, wait); if (callNow) { func.apply(context, args); } }; } /** * Creates a deep copy of the input object * @param from - object to deep copy * @param to - optional target for copy * @returns a deep copy of input object */ function structuredClone(from, to) { if (from === null || typeof from !== 'object') { return from; } if (from.constructor != Object && from.constructor != Array) { return from; } if (from.constructor == Date || from.constructor == RegExp || from.constructor == Function || from.constructor == String || from.constructor == Number || from.constructor == Boolean) { return new from.constructor(from); } to = to || new from.constructor(); for (const key in from) { to[key] = typeof to[key] == 'undefined' ? structuredClone(from[key]) : to[key]; } return to; } /** * Check if object is a function * @param functionToCheck - object to check, presumably a function * @returns true when input is a function, false otherwise */ function isFunction(functionToCheck) { return (functionToCheck && {}.toString.call(functionToCheck) === '[object Function]'); } /** * Check if object is plain object (not function, not array, not class) * @returns true when input is plain old JavaScript object, false otherwise */ function isPOJO(objectToCheck) { return objectToCheck && {}.toString.call(objectToCheck) === '[object Object]'; } /** * Check if object is an instance of a specific class * @param obj - any object to check * @param type - class type itself * @returns true when obj is an instance of the provided type, false otherwise */ function instOf(obj, type) { return _instanceOf(obj, type); } function _instanceOf(obj, klass) { if (obj === undefined || obj === null) { return false; } if (klass.default) { klass = klass.default; } if (isFunction(klass)) { return obj instanceof klass; } obj = Object.getPrototypeOf(obj); while (obj !== null) { if (obj.constructor.name === klass) { return true; } obj = Object.getPrototypeOf(obj); } return false; } /** * Compute and format polygon area with correct units (m2/km2) * @returns area of polygon with used units */ function formatArea(polygon, sourceProj) { const area = Math.abs(getArea(polygon)); const output = { size: area, type: 'Area', unit: 'm', }; if (area > 10000) { output.size = Math.round((area / 1000000) * 100) / 100; output.unit = 'km'; } else { output.size = Math.round(area * 100) / 100; output.unit = 'm'; } return output; } /** * Compute and format line length with correct units (m/km) * @returns numeric length of line with used units */ function formatLength(line, sourceProj) { let length = 0; const coordinates = line.getCoordinates(); const sourceProjRegistered = get(sourceProj); for (let i = 0; i < coordinates.length - 1; ++i) { const c1 = sourceProjRegistered ? transform(coordinates[i], sourceProj, 'EPSG:4326') : coordinates[i]; const c2 = sourceProjRegistered ? transform(coordinates[i + 1], sourceProj, 'EPSG:4326') : coordinates[i + 1]; length += getDistance(c1, c2); } const output = { size: length, type: 'Length', unit: 'm', }; if (length > 100) { output.size = Math.round((length / 1000) * 100) / 100; output.unit = 'km'; } else { output.size = Math.round(length * 100) / 100; output.unit = 'm'; } return output; } /** * Check if element is overflown * @param element - Element to check * @returns true if element is overflown, false otherwise */ function isOverflown(element) { return (element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth); } /** * Replaces first string letter to UpperCase * @param target - Target string * @returns modified string */ function capitalizeFirstLetter(target) { return target.charAt(0).toUpperCase() + target.slice(1); } /** * Transforms string from camelCase to kebab-case */ function camelToKebab(str) { return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } /** * Returns undefined if string is undefined or empty */ function undefineEmptyString(str) { if (str === undefined) { return undefined; } return str.trim() != '' ? str : undefined; } /** * Determines if layer has properties needed for 'Zoom to layer' function. * @param layer - Selected layer * @returns True for layer with BoundingBox property, for * WMS layer or for layer, which has source with extent */ function layerIsZoomable(layer) { if (typeof layer == 'undefined') { return false; } if (layer.getExtent()) { return true; } if (isLayerWMS(layer)) { return true; } const src = layer.getSource(); if (src.getExtent && src.getExtent() && !isEmpty(src.getExtent())) { return true; } return false; } /** * Determines if layer has underlying layers. * @param layer - Selected layer * @returns True for layer with sub layers, for layer type * WMS layer */ function hasNestedLayers(layer) { if (layer === undefined) { return false; } return layer.Layer !== undefined; } /** * Determines if layer is a Vector layer and therefore stylable * @param layer - Selected layer * @returns True for ol.layer.Vector */ function layerIsStyleable(layer) { if (typeof layer == 'undefined') { return false; } return isLayerVectorLayer(layer, false); } /** * Test if layer is queryable (WMS layer with Info format) * @param layer - Selected layer * @returns True for ol.layer.Tile and ol.layer.Image with * INFO_FORMAT in params */ function isLayerQueryable(layer) { return isLayerWMS(layer) && !!getLayerParams(layer).INFO_FORMAT; } /** * Get title of selected layer * @param layer - to get layer title * @returns Layer title or "Void" */ function getLayerTitle(layer) { if (getTitle(layer) !== undefined && getTitle(layer) != '') { return getTitle(layer).replace(/&#47;/g, '/'); } return 'Void'; } function getURL(layer) { const src = layer.getSource(); if (instOf(src, ImageWMS)) { return src.getUrl(); } if (instOf(src, TileArcGISRest)) { return src.getUrls()[0]; } if (instOf(src, TileWMS)) { return src.getUrls()[0]; } if (instOf(src, WMTS)) { return src.getUrls()[0]; } if (instOf(src, XYZ)) { const urls = src.getUrls(); return urls ? urls[0] : ''; } if (src.getUrl) { const tmpUrl = src.getUrl(); if (typeof tmpUrl == 'string') { return tmpUrl; } if (isFunction(tmpUrl)) { return tmpUrl(); } } if (src.getUrls) { return src.getUrls()[0]; } } /** * Test if layer is WMS layer * @param layer - Selected layer * @returns True for ol.layer.Tile and ol.layer.Image */ function isLayerWMS(layer) { const isTileLayer = instOf(layer, Tile); const src = layer.getSource(); const isTileWMSSource = instOf(src, TileWMS); if (isTileLayer && isTileWMSSource) { return true; } const src2 = layer.getSource(); const isImageLayer = instOf(layer, Image); const isImageWMSSource = instOf(src2, ImageWMS); if (isImageLayer && isImageWMSSource) { return true; } return false; } function isLayerWMTS(layer) { const isTileLayer = instOf(layer, Tile); const src = layer.getSource(); const isWMTSSource = instOf(src, WMTS); if (isTileLayer && isWMTSSource) { return true; } return false; } function isLayerXYZ(layer) { const isTileLayer = instOf(layer, Tile); const src = layer.getSource(); const isXYZSource = instOf(src, XYZ); if (isTileLayer && isXYZSource) { return true; } return false; } function isLayerArcgis(layer) { const isTileLayer = instOf(layer, Tile); const src = layer.getSource(); const isArcgisSource = instOf(src, TileArcGISRest); if (isTileLayer && isArcgisSource) { return true; } return false; } function isLayerIDW(layer) { const isImageLayer = instOf(layer, Image); const src = layer.getSource(); const isIDWSource = instOf(src, IDW); if (isImageLayer && isIDWSource) { return true; } return false; } function getLayerSourceFormat(layer) { if (!isLayerVectorLayer(layer)) { return; } return layer.getSource()?.getFormat(); } /** * Test if layer is Vector layer * @param layer - Selected layer * @param includingClusters - Whether to treat clusters as Vectors or not, Defaults to true * @returns True for Vector layer */ function isLayerVectorLayer(layer, includingClusters = true) { if ((instOf(layer, Vector) || instOf(layer, VectorImage)) && /** * This part is not entirely correct as we cast both VectorLayer and VectorImage * as VectorLayer but the differences are not relevant for the sake of the check */ includingClusters ? instOf(layer.getSource(), Cluster) || instOf(layer.getSource(), Vector$1) : instOf(layer.getSource(), Vector$1)) { return true; } return false; } /** * Test if the features in the vector layer come from a GeoJSON source * @param layer - an OL vector layer * @returns true only if the GeoJSON format is explicitly specified in the source. False otherwise. */ async function isLayerGeoJSONSource(layer) { const GeoJSON = (await import('ol/format/GeoJSON')).default; if (instOf(getLayerSourceFormat(layer), GeoJSON)) { return true; } return false; } /** * Test if the features in the vector layer come from a TopoJSON source * @param layer - an OL vector layer * @returns true only if the TopoJSON format is explicitly specified in the source. False otherwise. */ async function isLayerTopoJSONSource(layer) { const TopoJSON = (await import('ol/format/TopoJSON')).default; if (instOf(getLayerSourceFormat(layer), TopoJSON)) { return true; } return false; } /** * Test if the features in the vector layer come from a KML source * @param layer - an OL vector layer * @returns true only if the KML format is explicitly specified in the source. False otherwise. */ async function isLayerKMLSource(layer) { const KML = (await import('ol/format/KML')).default; if (instOf(getLayerSourceFormat(layer), KML)) { return true; } return false; } /** * Test if the features in the vector layer come from a GPX source * @param layer - an OL vector layer * @returns true only if the GPX format is explicitly specified in the source. False otherwise. */ async function isLayerGPXSource(layer) { const GPX = (await import('ol/format/GPX')).default; if (instOf(getLayerSourceFormat(layer), GPX)) { return true; } return false; } /** * Test if layer is shown in layer switcher * (if not some internal hslayers layer like selected feature layer) * @param layer - Layer to check * @returns True if showInLayerManager attribute is set to true */ function isLayerInManager(layer) { return (getShowInLayerManager(layer) === undefined || getShowInLayerManager(layer) == true); } function getSourceParams(source) { return source.getParams(); } function getLayerParams(layer) { const src = layer.getSource(); if (instOf(src, ImageWMS)) { return getSourceParams(src); } if (instOf(src, TileWMS)) { return getSourceParams(src); } if (instOf(src, TileArcGISRest)) { return getSourceParams(src); } } function updateLayerParams(layer, params) { const src = layer.getSource(); if (instOf(src, ImageWMS)) { src.updateParams(params); } if (instOf(src, TileWMS)) { src.updateParams(params); } if (instOf(src, TileArcGISRest)) { src.updateParams(params); } } /** * Test if layer is has a title * @param layer - Layer to check * @returns True if layer is has a title */ function hasLayerTitle(layer) { return getTitle(layer) !== undefined && getTitle(layer) !== ''; } /** * Test if layers features are editable * @param layer - Layer to check * @returns True if layer has attribute 'editor' and in it * 'editable' property is set to true or missing */ function isLayerEditable(layer) { if (getEditor(layer) === undefined) { return true; } const editorConfig = getEditor(layer); if (editorConfig.editable === undefined) { return true; } return editorConfig.editable; } /** * Get user friendly name of layer based primary on title * and secondary on name attributes. * Is used in query service and hover popup. * @param layer - Layer to get the name for */ function getLayerName(layer) { if (layer === undefined || (getShowInLayerManager(layer) !== undefined && getShowInLayerManager(layer) === false)) { return ''; } const layerName = getTitle(layer) || getName(layer); return layerName; } /** * Highlight feature corresponding records inside a list * @param featuresUnder - Features under the cursor * @param layer - Layer to get features from */ function highlightFeatures(featuresUnder, list) { const featuresUnderIds = new Set(featuresUnder.map((feature) => feature?.getId()).filter(Boolean)); const recordsToUpdate = []; for (const record of list) { if (!record.featureId) { continue; } const shouldBeHighlighted = featuresUnderIds.has(record.featureId); // Only update if the highlight state is changing if (record.highlighted !== shouldBeHighlighted) { recordsToUpdate.push({ record, highlight: shouldBeHighlighted }); } } if (recordsToUpdate.length > 0) { // Apply all updates in a batch for (const { record, highlight } of recordsToUpdate) { record.highlighted = highlight; } } } /** * Checks if layer has a VectorSource object, if layer is * not internal for hslayers, if it has title and is shown in layer * switcher * @param layer - Layer to check * @returns True if layer is drawable vector layer */ function isLayerDrawable(layer, options = {}) { const checkVisible = options.checkVisible ?? true; return (isLayerVectorLayer(layer, false) && (checkVisible ? layer.getVisible() : true) && isLayerInManager(layer) && hasLayerTitle(layer) && isLayerEditable(layer)); } /** * Checks if layer's source has its own source * @param layer - Layer to check * @returns True if layer is clustered, false otherwise */ function isLayerClustered(layer) { return isLayerVectorLayer(layer) && getCluster(layer) && instOf(layer.getSource(), Cluster) ? true : false; } /** * Test if layers source is loaded * @param layer - Selected layer descriptor * @returns True loaded / False not (fully) loaded */ function layerLoaded(layer) { return layer.loadProgress?.loaded; } /** * Test if layers source is validly loaded (!true for invalid) * @param layer - Selected layer descriptor * @returns True invalid, false valid source */ function layerInvalid(layer) { if (!layer.layer?.getSource()) { return true; } return layer.loadProgress?.error; } function calculateResolutionFromScale(denominator, view) { if (!denominator) { return denominator; } const units = view.getProjection().getUnits(); const dpi = 25.4 / 0.28; const mpu = METERS_PER_UNIT[units]; return denominator / (mpu * 39.37 * dpi); } /** * List numeric attributes of the feature */ function listNumericAttributes(features) { return listAttributes(features, true); } const ATTRIBUTES_EXCLUDED_FROM_LIST = [ 'geometry', 'hs_normalized_IDW_value', ]; /** * List all attributes of the features apart from the geometry * Samples up to 33% of features with a hard limit of 400 features * to build a comprehensive attribute list */ function listAttributes(features, numericOnly = false, customExcludedAttributes) { if (features.length === 0) { return []; } const excludedAttributes = customExcludedAttributes || ATTRIBUTES_EXCLUDED_FROM_LIST; // Calculate sample size (33% with max 400) const sampleSize = Math.min(Math.ceil(features.length * 0.33), 400, features.length); const attributeSet = new Set(); const step = Math.max(1, Math.floor(features.length / sampleSize)); // Collect attributes using reservoir sampling for (let i = 0; i < features.length; i += step) { const feature = features[i]; Object.keys(feature.getProperties()).reduce((set, attr) => { if (!excludedAttributes.includes(attr) && (!numericOnly || !isNaN(Number(feature.get(attr))))) { set.add(attr); } return set; }, attributeSet); if (attributeSet.size >= sampleSize) { break; } } return Array.from(attributeSet); } // Coefficients of the polynomial (in reverse order for easy use in the loop) const COEFFICIENTS = [ new Big('-1.31228099e-15'), new Big('1.49629747e-11'), new Big('-4.93320288e-08'), new Big('1.22907821e-05'), new Big('1.19666463e-01'), ]; /** * Calculates a buffer factor based on polynomial evaluation using Horner's method. * * This function evaluates a 4th-degree polynomial with pre-calculated coefficients * to determine a buffer factor. The function is designed to return a value close to 0.12 * for smaller distances (approximately 0-300 kilometers) and gradually decrease to 0 as the * distance approaches 4000 kilometers. * * Note: This function is intended for use with input values up to 4000 meters. * Using values greater than 4000 kilometers may produce unexpected results */ function getPolynomialBufferFactor(x) { // Convert x to a Big object const xBig = new Big(x); // Calculate polynomial value using Horner's method with a for...of loop let result = new Big(0); for (const coefficient of COEFFICIENTS) { result = result.times(xBig).plus(coefficient); } // Return the result as a regular, positive JavaScript number return Math.abs(result.toNumber()); } /** * Buffer extent by `BUFFER_FACTOR` * NOTE: Not using OL because we want to extend width and height independently */ function bufferExtent(extent, currentMapProj) { if (!extent) { return undefined; } //EPSG:4087 world bounds const [pMinX, pMinY, pMaxX, pMaxY] = [ -20037508.342789, -10018754.171394, 20037508.342789, 10018754.171394, ]; //Transform into projection suitable for area manipulation const transformed = transformExtent(extent, currentMapProj, 'EPSG:4087'); //Calculate buffer values const extentWidth = Math.abs(transformed[2] - transformed[0]); const extentHeight = Math.abs(transformed[3] - transformed[1]); // Calculate diagonal length const diagonalLength = Math.sqrt(extentWidth ** 2 + extentHeight ** 2); const BUFFER_FACTOR = diagonalLength < 4000000 ? getPolynomialBufferFactor(diagonalLength / 1000) //convert to kilometers : 0.0001; // const bufferWidth = extentWidth * BUFFER_FACTOR; const bufferHeight = extentHeight * BUFFER_FACTOR; //Buffer extent and transform back to currentMapProj const extended = [ Math.max(pMinX, transformed[0] - bufferWidth), Math.max(pMinY, transformed[1] - bufferHeight), Math.min(pMaxX, transformed[2] + bufferWidth), Math.min(pMaxY, transformed[3] + bufferHeight), ]; const [extMinX, extMinY, extMaxX, extMaxY] = extended; // If the extent is geometrically invalid, use the projection bounds if (extMaxX <= extMinX || extMaxY <= extMinY) { console.warn('Invalid extent geometry detected, using projection bounds:', extended); return [pMinX, pMinY, pMaxX, pMaxY]; } return transformExtent(extended, 'EPSG:4087', currentMapProj); } /** * Mappings from Function name attribute to corresponding PropertyIs element name * This maps SLD function operators to their equivalent PropertyIs element names */ const FUNCTION_TO_PROPERTY_MAP = { 'equalTo': 'PropertyIsEqualTo', 'notEqualTo': 'PropertyIsNotEqualTo', 'like': 'PropertyIsLike', 'lessThan': 'PropertyIsLessThan', 'lessThanOrEqualTo': 'PropertyIsLessThanOrEqualTo', 'greaterThan': 'PropertyIsGreaterThan', 'greaterThanOrEqualTo': 'PropertyIsGreaterThanOrEqualTo', 'isNull': 'PropertyIsNull', 'between': 'PropertyIsBetween', }; /** * Converts SLD Function elements to equivalent PropertyIs elements * For example: <Function name="lessThan"> becomes <PropertyIsLessThan> * * This transformation is necessary for compatibility between different SLD implementations * such as those used by QGIS, GeoServer, and other OGC-compliant systems. * * @param sld - The SLD XML string to process * @returns The SLD string with Function elements converted to PropertyIs elements */ const normalizeSldComparisonOperators = (sld) => { if (!sld) { return sld; } // Early exit if no Function elements are present if (!sld.includes('<Function name=') && !sld.includes(':Function name=')) { return sld; } // Use a tag stack to track opening tags for proper nesting and matching const tagStack = []; // Accumulate the result in parts for efficient string building const parts = []; let lastIndex = 0; // Regex matches both opening and closing Function tags with optional namespaces // Captures the namespace prefix (group 1) and operator name (group 2, for opening tags only) const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*:)?Function(?:\s+name="([^"]+)")?>/g; let match; while ((match = tagRegex.exec(sld)) !== null) { const fullMatch = match[0]; const namespaceWithColon = match[1] || ''; const isClosingTag = fullMatch.charAt(1) === '/'; const operator = match[2]; // Undefined for closing tags // Preserve content between tags or part of SLD preceding the first Function tag parts.push(sld.substring(lastIndex, match.index)); lastIndex = match.index + fullMatch.length; if (isClosingTag) { // Process closing tag by finding its matching opening tag from the stack const openingTag = tagStack.pop(); if (openingTag && openingTag.operator) { const propertyElement = FUNCTION_TO_PROPERTY_MAP[openingTag.operator]; if (propertyElement) { // Create the corresponding PropertyIs closing tag parts.push(`</${namespaceWithColon}${propertyElement}>`); } else { // Keep original if no mapping exists parts.push(fullMatch); } } else { // Keep original if no matching opening tag (malformed XML) parts.push(fullMatch); } } else { // Process opening tag if (operator && FUNCTION_TO_PROPERTY_MAP[operator]) { // Push tag info to stack for later matching with closing tag tagStack.push({ tag: 'Function', namespace: namespaceWithColon, operator: operator, }); // Create the corresponding PropertyIs opening tag parts.push(`<${namespaceWithColon}${FUNCTION_TO_PROPERTY_MAP[operator]}>`); } else { // Keep original if not a supported operator parts.push(fullMatch); } } } // Add any remaining content after the last match if (lastIndex < sld.length) { parts.push(sld.substring(lastIndex)); } // Join all parts to form the intermediate result let result = parts.join(''); // Add ElseFilter to the last rule if it doesn't have a filter const ruleRegex = /<se:Rule[^>]*>([\s\S]*?)<\/se:Rule>/g; const rules = result.match(ruleRegex); if (rules && rules.length > 0) { const lastRule = rules[rules.length - 1]; if (!lastRule.includes('<se:Filter') && !lastRule.includes('<Filter')) { // Find the position of the last rule's closing tag const lastRuleEndIndex = result.lastIndexOf('</se:Rule>'); if (lastRuleEndIndex !== -1) { // Insert ElseFilter specifically before the last rule's closing tag result = result.slice(0, lastRuleEndIndex) + '<se:ElseFilter xmlns:se="http://www.opengis.net/se"/>' + result.slice(lastRuleEndIndex); } } } return result; }; class HsProxyService { constructor() { this.hsConfig = inject(HsConfig); } /** * Register Layman endpoints to avoid proxifying them * @param endpoints - Layman endpoints to register */ registerLaymanEndpoints(url) { this.laymanUrl = url; } /** * Proxify URL if enabled. * @param url - URL to proxify * @returns Encoded URL with path to hslayers-server proxy or original URL if proxification not needed */ proxify(url) { // Early returns for URLs that should never be proxified if (this.shouldSkipProxification(url)) { return url; } // Apply proxy if enabled if (this.hsConfig.useProxy === undefined || this.hsConfig.useProxy === true) { const proxyPrefix = this.hsConfig.proxyPrefix || '/proxy/'; return `${proxyPrefix}${url}`; } return url; } /** * Checks if URL should skip proxification based on predefined rules * @param url - URL to check * @returns boolean indicating if proxification should be skipped */ shouldSkipProxification(url) { // Don't proxify if it's already proxified if (this.hsConfig.proxyPrefix && url.startsWith(this.hsConfig.proxyPrefix)) { return true; } // Don't proxify Layman endpoints if (this.laymanUrl && url.startsWith(this.laymanUrl)) { return true; } // Don't proxify data URLs if (url.startsWith('data:application')) { return true; } // Don't proxify if URL is from the same origin if (this.isFromSameOrigin(url)) { return true; } return false; } /** * Checks if URL is from the same origin as the application * @param url - URL to check * @returns boolean indicating if URL is from the same origin */ isFromSameOrigin(url) { const windowUrlPosition = url.indexOf(window.location.origin); // Check if URL is not from the same origin (matching original logic) if (windowUrlPosition === -1 || windowUrlPosition > 7 || getPortFromUrl(url) !== getPortFromUrl(window.location.origin)) { return false; } return true; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsProxyService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsProxyService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: HsProxyService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); /** * @param record - Record of one dataset from Get Records response * Create extent features for displaying extent of loaded dataset records in map */ function addExtentFeature(record, mapProjection) { const attributes = { hs_notqueryable: true, highlighted: false, title: record.title || record.name, geometry: null, id: crypto.randomUUID(), }; let mapExtent = mapProjection.getExtent(); if (mapExtent === null) { console.warn('Map projection extent not found - fallback value used. To prevent unexpected results of app functionalities define it by yourself. Eg. mapExtent.setExtent([extent])'); mapProjection.setExtent(transformExtentValue(parseExtent([-180, -90, 180, 90]), mapProjection, true)); mapExtent = mapProjection.getExtent(); } const recordBBox = record.bbox || record.bounding_box; const b = parseExtent(recordBBox || ['180', '180', '180', '180']); //Check if height or Width covers the whole screen const extent = record.bounding_box //if from layman ? transformExtentValue(b, mapProjection, true) : transformExtentValue(b, mapProjection); if (b && ((extent[0] < mapExtent[0] && extent[2] > mapExtent[2]) || (extent[1] < mapExtent[1] && extent[3] > mapExtent[3]))) { return; } attributes.geometry = fromExtent(extent); const extentFeature = new Feature(attributes); extentFeature.setId(extentFeature.get('id')); return extentFeature; } function transformExtentValue(pairs, mapProjection, disableTransform) { if (!pairs) { return; } let first_pair; let second_pair; if (disableTransform || !get(mapProjection)) { first_pair = pairs[0]; second_pair = pairs[1]; } else { first_pair = transform(pairs[0], 'EPSG:4326', mapProjection); second_pair = transform(pairs[1], 'EPSG:4326', mapProjection); } const mapProjectionExtent = mapProjection.getExtent(); if (!isFinite(first_pair[0])) { first_pair[0] = mapProjectionExtent[0]; } if (!isFinite(first_pair[1])) { first_pair[1] = mapProjectionExtent[1]; } if (!isFinite(second_pair[0])) { second_pair[0] = mapProjectionExtent[2]; } if (!isFinite(second_pair[1])) { second_pair[1] = mapProjectionExtent[3]; } if (isNaN(first_pair[0]) || isNaN(first_pair[1]) || isNaN(second_pair[0]) || isNaN(second_pair[1])) { return; } return [first_pair[0], first_pair[1], second_pair[0], second_pair[1]]; } function parseExtent(bbox) { if (!bbox) { return; } let b; const pairs = []; if (typeof bbox === 'string') { b = bbox.split(' '); } else if (Array.isArray(bbox)) { b = bbox; } pairs.push([parseFloat(b[0]), parseFloat(b[1])]); pairs.push([parseFloat(b[2]), parseFloat(b[3])]); return pairs; } /** * Create new extent layer */ function createNewExtentLayer(title) { const fill = new Fill({ color: 'rgba(0, 0, 255, 0.01)', }); // Pre-create styles for highlighted and normal states to avoid recreating them on every render const normalStyle = new Style({ stroke: new Stroke({ color: '#005CB6', width: 1, }), fill, }); const highlightedStyle = new Style({ stroke: new Stroke({ color: '#005CB6', width: 4, }), fill, }); return new VectorLayer({ properties: { title, showInLayerManager: false, removable: false, }, source: new VectorSource(), style: function (feature, resolution) { return getHighlighted(feature) ? highlightedStyle : normalStyle; }, }); } /** * Get bounding box from object \{east: value, south: value, west: value, north: value\} * @param bbox - Bounding box * @returns Returns bounding box as number array */ function getBboxFromObject(bbox) { if (bbox && !Array.isArray(bbox)) { return [ parseFloat(bbox.east), parseFloat(bbox.south), parseFloat(bbox.west), parseFloat(bbox.north), ]; } return bbox; } /** * Replace Urls in text by anchor html tag with url, useful for attribution to be clickable * * @param url - String to look for Urls * @returns Text with added anchors */ function addAnchors(url) { if (!url) { return null; } const exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi; return url.replace(exp, "<a href='$1'>$1</a>"); } /** * Loop through list of formats returned by GetCapabilities and select first available from the list of available formats * * @param formats - List of formats available for service * @param preferredFormats - List of preferred formats for output * @returns Either one of preferred formats or first available format */ function getPreferredFormat(formats, preferredFormats) { for (let i = 0; i < preferredFormats.length; i++) { if (formats.indexOf(preferredFormats[i]) > -1) { return preferredFormats[i]; } } return formats[0]; } /** * Generated bundle index. Do not edit. */ export { ATTRIBUTES_EXCLUDED_FROM_LIST, COEFFICIENTS, HsProxyService, addAnchors, addExtentFeature, bufferExtent, calculateResolutionFromScale, camelToKebab, capitalizeFirstLetter, createNewExtentLayer, debounce, formatArea, formatLength, getBboxFromObject, getLayerName, getLayerParams, getLayerSourceFormat, getLayerTitle, getParamsFromUrl, getPolynomialBufferFactor, getPortFromUrl, getPreferredFormat, getSourceParams, getURL, hasLayerTitle, hasNestedLayers, highlightFeatures, insertAfter, instOf, isFunction, isLayerArcgis, isLayerClustered, isLayerDrawable, isLayerEditable, isLayerGPXSource, isLayerGeoJSONSource, isLayerIDW, isLayerInManager, isLayerKMLSource, isLayerQueryable, isLayerTopoJSONSource, isLayerVectorLayer, isLayerWMS, isLayerWMTS, isLayerXYZ, isOverflown, isPOJO, layerInvalid, layerIsStyleable, layerIsZoomable, layerLoaded, listAttributes, listNumericAttributes, normalizeSldComparisonOperators, paramsToURL, paramsToURLWoEncode, parseExtent, structuredClone, transformExtentValue, undefineEmptyString, updateLayerParams }; //# sourceMappingURL=hslayers-ng-services-utils.mjs.map