@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
215 lines (214 loc) • 8.47 kB
JavaScript
import { SelectionMode } from '../../models/selection.js';
/**
* Checks if the system prefers dark mode.
* Falls back safely if matchMedia is not supported.
* @returns True if system is in dark mode.
*/
export const systemIsInDarkMode = () => {
return (typeof globalThis !== 'undefined' &&
typeof globalThis.matchMedia === 'function' &&
globalThis.matchMedia('(prefers-color-scheme: dark)').matches);
};
/**
* Checks if on Safari.
* @returns True if on Safari.
*/
export const isSafari = () => {
return navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome');
};
/**
* Checks if on Firefox.
* @returns True if on Firefox.
*/
export const isFirefox = () => {
return navigator.userAgent.includes('Mozilla') && !navigator.userAgent.includes('Chrome');
};
/**
* Returns a valid index within the given maximum index.
* Out of bound index loops back to the first valid index.
* @param index - The index to validate.
* @param maxIndex - The maximum index. If not provided or 0, returns 0.
* @returns The valid index.
*/
export const getValidIndex = (index, maxIndex) => {
if (!maxIndex) {
return 0;
}
return index < 0 ? maxIndex : index % (maxIndex + 1);
};
/**
* Returns a value bounded between a minimum and maximum limit.
* @returns The bounded value.
*/
export const minMax = (value, minLimit, maxLimit) => {
return Math.min(Math.max(value, minLimit), maxLimit);
};
/**
* Transforms a hex string into rgba values.
* @param hex - A hex color, starting with '#' and including 3 (=shorthand), 6 (=default) or 8 (=including alpha) values.
* @returns an array of rgba values: R, G, B [0-255], alpha [0-1]. Or null if the hex color is invalid.
*/
export const hexToRgbaArray = (hex) => {
if (!hex.startsWith('#') || (hex.length !== 4 && hex.length !== 7 && hex.length !== 9)) {
return null;
}
let r, g, b;
let a = 1;
// Expand shorthand form (e.g. "#03F") to full form (e.g. "#0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (_m, r, g, b) => '#' + r + r + g + g + b + b);
if (hex.length >= 7) {
r = Number.parseInt(hex.slice(1, 3), 16);
g = Number.parseInt(hex.slice(3, 5), 16);
b = Number.parseInt(hex.slice(5, 7), 16);
if (hex.length === 9) {
// Contains alpha value
a = Math.round(100 * (Number.parseInt(hex.slice(7, 9), 16) / 255)) / 100;
}
if ([r, g, b, a].includes(Number.NaN)) {
return null;
}
return [r, g, b, a];
}
return null;
};
/**
* Transforms a rgb string into rgba values.
* @param rgbaStr - A rgb or rgba color, starting with 'rgb' and including 3 or 4 (=including alpha) values.
* @returns an array of rgba values: R, G, B [0-255] and alpha [0-1]. Or null if the rgb string is invalid.
*/
export const rgbStrToRgbaArray = (rgbaStr) => {
if (!rgbaStr.startsWith('rgb')) {
return null;
}
let rgb;
const alpha = 1;
try {
rgb = rgbaStr
.replace(/[^\d,.-]/g, '')
.split(',')
.map((c, idx) => (idx < 3 ? Math.round(Number(c)) : Number(c)));
}
catch {
return null;
}
// Do some general validation
if (rgb.length === 3) {
rgb.push(alpha);
}
else if (rgb.length < 3 || rgb.length > 4) {
return null;
}
if (rgb.includes(NaN) || rgb.some((c) => c < 0 || c > 255) || rgb[3] > alpha) {
return null;
}
return rgb;
};
/**
* Transform a color string to an array of rgba values.
* @param color A string representing a color. Can be of type hex ('#0033ff') or rgb(a) ('rgba(255, 23, 15, 0.5)').
* @returns an array of rgba values: R, G, B [0-255] and alpha [0-1]. Or null if the color isn't in a valid format.
*/
export const colorToRgbaArray = (color) => {
let rgbaColor = hexToRgbaArray(color);
rgbaColor ??= rgbStrToRgbaArray(color);
return rgbaColor;
};
/**
* Check whether the provided text is a matching the correct email form
*/
export const isValidEmail = (email) => {
return /^[^\s@]+@(?:[^\s@.]+\.)+[^\s@.]+$/.test(email);
};
/**
* Applies the given Opacity to the given Layers (if they have such a Property).
* @param opacity Value of Opacity
* @param layers Layers to apply it
* @param callback Optional Callback after Opacity has been applied
*/
export const applyOpacityToLayers = (opacity, layers, callback) => {
for (const layer of layers) {
if (Object.keys(layer).includes('opacity')) {
layer.opacity = opacity;
callback?.(layer);
}
}
};
/**
* Applies the given Array of Features to the Selection (<code>state.selection.selectedFeatures</code>) depending on the
* given SelectionMode (via <code>state.selection.selectionMode</code>).
* @param features Array of Features to add/replace/remove to/from existing selected Features
* @param state The State to apply the given Features
*/
export const applyFeaturesToSelection = (features, state) => {
// Check if we have set a Geometry of the Selection (e.g. Polygon) and additionally filter the given Features
if (state.selection.selectionGeometry) {
features = features.filter((feature) => state.selection.selectionGeometry.intersectsExtent(feature.getGeometry().getExtent()));
}
switch (state.selection.selectionMode) {
case SelectionMode.Replace:
state.selection.selectedFeatures.push(...features);
break;
case SelectionMode.Add:
// Push/Add only Features that are not already in Selection (can happen if you select overlapping Regions
state.selection.selectedFeatures.push(...features.filter((newlySelectedFeature) => !state.selection.selectedFeatures.some((alreadySelectedFeature) => newlySelectedFeature.getId() === alreadySelectedFeature.getId())));
break;
case SelectionMode.Remove:
for (const featureToRemove of features) {
const idxOfGmlFeatureToRemove = state.selection.selectedFeatures.findIndex((gmlFeature, _idx, _feature) => featureToRemove.getId() === gmlFeature.getId());
if (idxOfGmlFeatureToRemove > -1) {
state.selection.selectedFeatures.splice(idxOfGmlFeatureToRemove, 1);
}
}
break;
}
const noFeaturesSelected = state.selection.selectedFeatures.length == 0;
state.interface.selectionComponentVisible = !noFeaturesSelected;
if (noFeaturesSelected) {
state.selection.highlightedFeatures = [];
}
};
const urlRegExp = /(https?:\/\/[^"'<]*?(?=\s|$|<\/[^a]>))/gi;
const telRegExp = /((\b(0041|0)|\B\+41)(\s?\(0\))?(\s)?[1-9]{2}(\s)?\d{3}(\s)?\d{2}(\s)?\d{2}\b)/gi;
const mailRegExp = /(^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$)/gi; //NOSONAR(typescript:S5843) As this is the official Regular Expression, we will not mess around with it
export const linkify = (str) => {
if (urlRegExp.test(str)) {
return str.replaceAll(urlRegExp, '<a href="$1" target="_blank">$1</a>');
}
if (mailRegExp.test(str)) {
return str.replaceAll(mailRegExp, '<a href="mailto:$1" target="_blank">$1</a>');
}
if (telRegExp.test(str)) {
return str.replaceAll(telRegExp, '<a href="tel:$1" target="_blank">$1</a>');
}
return str;
};
/**
* Apply default prefix to a relative URL
* Is usefull on the demo website for metadata or some images urls
* @param context
* @param metadataUrl
* @returns
*/
export function applyDefaultPrefixToUrl(context, metadataUrl) {
if (!metadataUrl) {
return undefined;
}
if (!metadataUrl.startsWith('http') && context.configManager.Config.general.defaultUrlPrefix) {
return `${context.configManager.Config.general.defaultUrlPrefix}${metadataUrl}`;
}
return metadataUrl;
}
/**
* Splits a String containing Numbers into an Array of Numbers. The Separator is a comma. Or returns
* <code>undefined</code> if the input is <code>undefined</code>
* @param numbersAsString The String containing Numbers.
*/
export const splitTrimAndConvertToNumber = (numbersAsString) => {
return numbersAsString
? numbersAsString
.split(',')
.map((e) => e.trim())
.map(Number)
: undefined;
};