hslayers-ng
Version:
HSLayers-NG mapping library
1,168 lines (1,156 loc) • 39.6 kB
JavaScript
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(///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