import { Converter } from 'showdown';
import { isPlainObject } from 'lodash';
import proj4 from "proj4";
import { formatLatLng as formatLatLng_ } from "../../../map/utils/formatLatLng.js";
import { addButtonToTableRow, initAnnotations } from "../../../ui/table/Utils.js";
import { EventManager } from "../../../events/EventManager.js";
import { isNullorUndefined, roundNumber } from "../../utils.js";
import { applyUnitConversion } from "../convertUnits.js";
import "./common.css";
/**
* Format lat/lng to a readable string with decimal degrees
*
*
*
* @param {*} lat
* @param {*} lng
* @returns {HTMLElement} formatted lat/lng in a span
*/
export function formatLatLng(lat, lng) {
if (isNullorUndefined(lat) || isNullorUndefined(lng)) return undefined;
const span = document.createElement("span");
span.innerText = formatLatLng_(lat, lng);
span.addEventListener("append", e => {
if ((span.parentNode.nodeName == "TD") && (span.parentNode.parentNode.nodeName == "TR")) {
let toggle = false;
addButtonToTableRow(span.parentNode.parentNode, ["fas", "fa-retweet"], "Change Format", null, (keyCell, valueCell) => {
span.innerText = formatLatLng_(lat, lng, (!toggle) ? "degreeminutes" : null);
valueCell.title = span.innerText;
toggle = !toggle;
});
}
})
return span;
}
/**
* Format lat/lng from the geometry object of a Feature
* to a readable string with decimal degrees
*
*
*
* @param {*} geometry
* @param {*} name
* @returns {HTMLElement} formatted lat/lng in a span
*/
export function formatGeometry(geometry, name) {
if ((geometry?.type?.toLowerCase() != "point") ||
(typeof name != "string")
) return undefined;
let crs = null; // don't assume 4326, if an unsupported Projection is used
["4326", "3995", "3031", "3857"].forEach(epsg => {
if (name.includes(epsg)) {
crs = `EPSG:${epsg}`;
}
});
if (!crs) return undefined;
const point = proj4(crs, "EPSG:4326", { x: geometry.coordinates[0], y: geometry.coordinates[1] });
return formatLatLng(point.y, point.x);
}
/**
* Add a button to request filtering an attribute
*
*
*
* @param {string} column
* @param {string} value
* @returns {string} span placeholder
*/
export function addFilterButton(column, value) {
if (isNullorUndefined(column) || isNullorUndefined(value)) return undefined;
const span = document.createElement("span");
span.addEventListener("append", e => {
if ((span.parentNode.nodeName == "TD") && (span.parentNode.parentNode.nodeName == "TR")) {
addButtonToTableRow(span.parentNode.parentNode, ["fas", "fa-filter"], "Add Filter", "", () => {
EventManager.fire("add_attribute_filter", {
column: column,
value: value
});
});
}
});
return span;
}
/**
* Make ISO Timestring more readable
*
*
*
* @param {string} date in ISO format
* @returns {HTMLElement} formatted date in a span
*/
export function formatDate(date) {
if (typeof date != "string") return undefined;
function _formatDate(value) {
value = value.replace("T", ", ");
return value.replace("Z", " UTC");
}
const span = document.createElement("span");
span.innerText = _formatDate(date);
const modes = [
//() => new Date(date).toLocaleString(), // unused, because the localeString is misleading and converrted to local timezone
() => date,
() => _formatDate(date),
];
span.addEventListener("append", e => {
if ((span.parentNode.nodeName == "TD") && (span.parentNode.parentNode.nodeName == "TR")) {
let mode = 0;
addButtonToTableRow(span.parentNode.parentNode, ["fas", "fa-retweet"], "Change Format", null, (keyCell, valueCell) => {
span.innerText = modes[mode]();
valueCell.title = span.innerText;
mode = (mode + 1) % modes.length;
});
}
})
return span;
}
/**
* Only returns the given string if the first value is not null or undefined
*
*
*
* @param {any} valueToCheck
* @param {any} then
* @param {any} else_
* @returns {any} given value
*/
export function isDefined(valueToCheck, then, else_) {
if (!isNullorUndefined(valueToCheck)) {
return then;
} else {
return else_ ?? undefined;
}
}
/**
* Concats values if every value is defined (not null or undefined)
*
*
*
* @param {string} values any amount of values: concat1, concat2, ...
* @returns {string} string
*/
export function concatIfDefined(values) {
let out = "";
for (let i = 0; i < arguments.length; ++i) {
if (!isNullorUndefined(arguments[i])) {
out += arguments[i];
} else {
return undefined;
}
}
return out;
}
/**
* Encode String to URL Compatible characters
*
*
*
* @param {string} str url compponent
* @returns {string} encoded string
*/
export function encodeURLComponent(str) {
if (typeof str != "string") return undefined;
return encodeURIComponent(str);
}
/**
* Returns the GRAY_INDEX for bathymetry raster data
*
*
*
* @param {object} currentProperties
* @param {object[]} allFeatures
* @returns {string} string
*/
export function getBathymetryGrayIndex(currentProperties, allFeatures) {
if (!isPlainObject(currentProperties) || !Array.isArray(allFeatures)) return undefined;
if (currentProperties?.GRAY_INDEX) throw "discard_template";
for (let i = 0; i < allFeatures.length; i++) {
const properties = allFeatures[i].properties;
if (('GRAY_INDEX' in properties) && (properties.GRAY_INDEX < 0)) return roundNumber(properties.GRAY_INDEX, 2) + " m";
}
return undefined;
}
/**
* Round a number to the given decimals
*
*
*
* @param {string} num
* @param {string} decimals
* @returns {string} formatted number
*/
export function round(num, decimals) {
if (isNullorUndefined(num)) return undefined;
if (typeof num === "string") num = Number.parseFloat(num);
if (!Number.isFinite(num)) return undefined;
decimals = Number.parseInt(decimals || 0);
if (!Number.isFinite(decimals)) decimals = 0;
return roundNumber(num, decimals);
}
/**
* Split a string at a substring and return the given index
*
*
*
* @param {string} str
* @param {string} subStr
* @param {number} index
* @returns {string} formatted string
*/
export function splitString(str, subStr, index) {
if (!str || !subStr) return undefined;
index = Number.parseInt(args[2]) || 0;
const parts = str.split(subStr);
return (index < parts.length) ? parts[index] : parts[0];
}
/**
* Split a string at an index and get the substring
*
*
*
* @param {string} str
* @param {number} startIndex
* @param {number} endIndex
* @returns {string} string
*/
export function substring(str, startIndex, endIndex) {
if (!str) return undefined;
startIndex = Number.parseInt(startIndex) || 0;
endIndex = Number.parseInt(endIndex) || 0;
return str.substring(startIndex, endIndex);;
}
/**
* Defines the title in data-title for the whole popup feature.
* The PopupRenderer will extract the title from that property to display
* it in the header. This is directly set in the container.
*
*
*
* @param {string} values multiple arguments to concat if needed
* @returns {function} callback function to set the containers title
*/
export function setTitle(values) {
const args = Array.from(arguments);
if (args.length > 0) {
return container => {
container.dataset.title = args.join("");
};
}
return undefined;
}
/**
* Parses a json string and outputs the value of the given property
*
*
* @param {sring} jsonstring
* @param {sring} keys any amount of keys
* @returns {string} resolved value
*/
export function parseJSON(jsonstring, keys) {
const args = Array.from(arguments);
if (args.length > 1) {
try {
let obj = JSON.parse(args[0]);
for (let i = 1; i < args.length; ++i) {
if (args[i] in obj) {
obj = obj[args[i]];
} else {
return undefined;
}
}
return obj;
} catch (e) {
return undefined;
}
}
return undefined;
}
/**
* Prevents the copy button to be displayed in a table row
*
* @returns {HTMLElement} temporary <span>
*
*/
export function noCopy() {
const span = document.createElement("span");
span.addEventListener("append", e => {
if ((span.parentNode.tagName == "TD") && (span.parentNode.parentNode.tagName == "TR")) {
span.parentNode.parentNode.classList.add("no-copy");
}
span.remove();
})
return span;
}
/**
* Prevents content from being rendered by inserting a comment if the site matches
*
* @returns {string} html comment open
*
*/
export function hideOnGalleryStart() {
// FixMe: HideOnSite does not work anymore. Workaround using global variable
return (window.vefGalleryVisible) ? "<!--" : "";
}
/**
* Prevents content from being rendered by inserting a comment if the site matches
*
* @returns {string} html comment close
*
*/
export function hideOnGalleryEnd() {
// FixMe: HideOnSite does not work anymore. Workaround using global variable
return (window.vefGalleryVisible) ? "-->" : "";
}
/**
* Display a layers legend graphic
*
* @param {Layer} layer
* @returns {string} Legend Graphic
*
*/
export function displayLayerLegend(layer) {
return layer.getLegendGraphic();
}
/**
* Join Array values as a string
*
*
*
* @param {string[]} inputArray
* @param {string} separator
* @returns {string}
*/
export function joinArray(inputArray, separator) {
if (inputArray === "string") {
try {
inputArray = JSON.parse(inputArray)
}
catch (error) {
inputArray = JSON.parse(inputArray.replaceAll("'", '"'))
}
}
if (Array.isArray(inputArray)) {
return inputArray.join(separator || ", ");
}
return inputArray || undefined;
}
/**
* Parse markdown
*
*
*
* @param {string} markdown
* @param {number} maxHeight
* @param {boolean} simplifiedAutoLink default is false
* @returns {HTMLElement} div containing parsed markdown
*/
export function parseMarkdown(markdown, maxHeight, simplifiedAutoLink = false) {
if (typeof markdown != "string") return undefined;
simplifiedAutoLink = ((simplifiedAutoLink === true) || (simplifiedAutoLink === "true"));
maxHeight = Number.parseFloat(maxHeight) || 0;
const div = document.createElement("div");
const setStyle = target => {
target.style.maxHeight = maxHeight + "px";
target.insertAdjacentHTML("beforeend", "<a class='markdown-readmore' href='#'><i class='fas fa-eye'></i> read more</a>");
const readMore = target.querySelector("a.markdown-readmore");
readMore.onclick = e => {
e.preventDefault();
readMore.remove();
target.style.maxHeight = null;
};
}
div.classList.add('invisible');
if (maxHeight) {
div.addEventListener("append", e => {
if (Number.isFinite(maxHeight)) {
//setTimeout(()=>{},0) is executed at the end of the event loop to ensure that the div is rendered in the DOM.
//Required for getting the client height.
setTimeout(() => {
if (div.clientHeight > maxHeight) {
setStyle(div);
div.classList.remove('invisible');
} else {
let duration = 0;
const intervalSize = 25;
const maxDuration = 60 * 1000;
const interval = setInterval(() => {
duration += intervalSize;
if (!div.clientHeight && (duration < maxDuration)) return;
if (div.clientHeight > maxHeight) {
setStyle(div);
}
div.classList.remove('invisible');
clearInterval(interval);
}, intervalSize);
}
}, 0)
}
});
} else {
div.classList.remove('invisible');
}
const markdownConverter = new Converter({
tables: true,
noHeaderId: true,
literalMidWordUnderscores: true,
simplifiedAutoLink: simplifiedAutoLink,
excludeTrailingPunctuationFromURLs: simplifiedAutoLink
});
div.classList.add("markdown-template");
div.innerHTML = markdownConverter.makeHtml(markdown);
return div;
}
/**
* Simple unit formatter. Find the best unit prefix.
*
*
* @param {number} value
* @param {string} unit
* @param {string} targetUnit
* @param {number} precision
* @returns {HTMLElement} span contianing result
*/
export function formatUnit(value, unit, targetUnit, precision) {
if (!value) return
if (!targetUnit) targetUnit = unit
if (!precision) precision = 3
const span = document.createElement('span');
span.innerText = applyUnitConversion(value, unit, targetUnit, precision)
return span;
}
/**
* Creates a dropdown filter element with given values.
*
* @param {string} values A string of values separated by the specified separator.
* @param {string} separator The character used to separate the values in the string.
* @param {number} long The longitude value to be used in the event.
* @param {number} lat The latitude value to be used in the event.
* @returns {HTMLSelectElement} The created dropdown element.
*/
export function createFilterDropdown(values, separator, long, lat) {
values = values.split(separator)
const dropdown = document.createElement("select");
dropdown.className = "pop-up_select_dropdown";
let item = document.createElement("option");
item.innerText = "- - -";
item.setAttribute("selected", "");
item.disabled = true;
dropdown.appendChild(item);
values.forEach(element => {
let item = document.createElement("option");
item.value = element;
item.innerText = element;
dropdown.appendChild(item);
});
async function caller() {
EventManager.fire("values_Magnitute_filter", {
magnitude: values,
longitude: long,
latitude: lat,
dropdown: dropdown
});
}
caller();
return dropdown
}
/**
* Creates a table element from the given properties object.
*
*
* @param {object} properties
* @param {number} count default = 0 to print all properties
* @param {string} excludedKeys comma seperated list of excluded keys
* @param {string} annotations default = "false"
*
* @returns {HTMLElement} The created table element.
*/
export function generateTable(properties, count, annotations = "false", excludedKeys = "") {
count = parseInt(count || 0);
excludedKeys = excludedKeys.split(",").map(key => key.trim());
annotations = (annotations === "true");
const table = document.createElement("table");
const thead = document.createElement("thead");
const tbody = document.createElement("tbody");
table.appendChild(thead);
table.appendChild(tbody);
let index = 0;
if (properties) {
for (const [key, value] of Object.entries(properties)) {
if (excludedKeys.includes(key)) continue;
if ((properties[key] === null) || properties[key] === undefined) continue;
if (key.startsWith("_")) continue;
if (count && index++ >= count) break;
const row = document.createElement("tr");
const keyCell = document.createElement("td");
const valueCell = document.createElement("td");
keyCell.innerText = key;
valueCell.innerText = value;
row.appendChild(keyCell);
row.appendChild(valueCell);
tbody.appendChild(row);
}
}
if (annotations) initAnnotations(table, properties, excludedKeys);
return table;
}
/**
* Converts a given string to uppercase.
*
* @param {string} value - The string to be converted to uppercase.
* @returns {string} The uppercase version of the input string.
*/
export function toUpperCase(value) {
return value.toUpperCase();
}
/**
* Converts the given string to lowercase.
*
* @param {string} value - The string to be converted to lowercase.
* @returns {string} The lowercase version of the input string.
*/
export function toLowerCase(value) {
return value.toLowerCase();
}