import iro from '@jaames/iro';
/**
* A collection of color scale helper tools.
*/
/**
* Checks if a given string is a six digit hexadecimal color code
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*
* @param {string} color hex color code
* @returns {boolean} true if color is valid
*/
export function isHexColor(color) {
return /^#(?:[0-9a-fA-F]{6})$/.test(color);
}
/**
* Copy a color item to remove original pointers to instances
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*
* @param {object} item color item
* @returns {object} copied color item
*/
export function copyColorItem(item) {
return {
opacity: item.opacity,
value: item.value,
color: item.color
};
}
/**
* Copy a color scale to remove original pointers to instances
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*
* @param {object[]} scale color scale
* @returns {object[]} copied color scale
*/
export function copyColorScale(scale) {
const newScale = [];
for (let i = 0; i < scale.length; ++i) {
newScale.push(copyColorItem(scale[i]));
}
return newScale;
}
/**
* Validates a color item and throws error if it
* has invalid properties.
*
* exampleItem = {
* "color": "#000000",
* "value": 1
* "opacity": 1
* }
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*
* @param {object} item color item
* @returns {object} valid color item
*/
export function validateColorItem(item) {
if (typeof item != "object") throw new Error("invalid color object");
if (!Number.isFinite(item.opacity)) throw new Error("invalid opacity");
if (!Number.isFinite(item.value)) throw new Error("invalid value");
if (!isHexColor(item.color)) throw new Error("invalid color string");
return item;
}
/**
* Validates a color scale and throws error if it
* has invalid items.
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*
* @param {object[]} colorScale color scale
* @returns {object[]} valid color scale
*/
export function validateColorScale(colorScale) {
for (let i = 0; i < colorScale.length; ++i) {
validateColorItem(colorScale[i]);
}
return colorScale;
}
/**
* Set the color scale based on the existing WMS query string
* parameters of the O2A template styles.
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*
* @returns {object} color scale
*/
export function wmsParamsToColorScale(env) {
const items = [];
let nv = null;
env = env.trim();
if (env.length > 0) {
const groups = env.split(";");
for (let i = 0; i < groups.length; ++i) {
groups[i] = groups[i].split(":");
let name = groups[i][0];
let value = groups[i][1];
if (name.startsWith("col")) {
name = "color";
} else if (name.startsWith("val")) {
name = "value";
value = Number.parseFloat(value);
} else if (name.startsWith("opa")) {
name = "opacity";
value = Number.parseFloat(value);
} else {
continue;
}
let index = Number.parseInt(groups[i][0].substring(3));
if (Number.isNaN(index)) {
if (groups[i][0].substring(3) == "nv") {
nv = nv || {};
nv[name] = value;
} else {
throw "invalid variable name: " + name;
}
} else {
if (!items[index - 1]) items[index - 1] = {};
items[index - 1][name] = value;
}
}
}
return {
scale: items,
noValue: nv
}
}
/**
* Get the WMS query parameters for the the O2A color template styles
* from the given color scale
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*
* @param {object[]} scale color scale array
* @param {object} noValue
*
* @returns {object} {env, styles}
*
*/
export function colorScaleToWmsParams(scale, noValue) {
validateColorScale(scale);
const length = parseInt(scale.length);
// set style name
let styles = "";
if (length == 0) {
styles = "";
} else if (length <= 2) {
styles = "template_2col";
} else {
styles = `template_${length}col`;
}
// set style configuration
let env = "";
for (let i = 0; i < scale.length; ++i) {
const varIndex = parseInt((scale.length == 1) ? 2 : i + 1);
const item = scale[i];
if (item.color !== false) env += `col${varIndex}:${item.color};`;
if (item.value !== false) env += `val${varIndex}:${item.value};`;
if (item.opacity !== false) env += `opa${varIndex}:${item.opacity};`;
}
if (noValue) {
validateColorItem(noValue);
if (noValue.color !== false) env += `colnv:${noValue.color};`;
if (noValue.value !== false) env += `valnv:${noValue.value};`;
if (noValue.opacity !== false) env += `opanv:${noValue.opacity};`;
}
return {
styles: styles,
env: env
}
}
/**
* Stretch the color scale values linear with equal distance
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*
* @param {object[]} scale color scale array
* @param {number} min
* @param {number} max
*/
export function stretchColorScale(scale, min, max) {
validateColorScale(scale);
if (!Number.isFinite(min) || !Number.isFinite(max)) throw new Error("invalid min/max values");
const range = Math.abs(max - min);
const count = scale.length - 1;
for (let i = 0; i < scale.length; ++i) {
scale[i].value = min + ((i / count) * range);
}
return scale;
}
/**
* Invert the color scale value order
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*
* @param {object[]} scale color scale array
*/
export function invertColorScale(scale) {
validateColorScale(scale);
const halfLength = Math.floor(scale.length / 2);
for (let i = 0; i < halfLength; ++i) {
const j = scale.length - 1 - i;
const temp = scale[i].value;
scale[i].value = scale[j].value;
scale[j].value = temp;
}
return scale;
}
/**
* Generate a CSS-gradient based on the
* given color scale
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*
* @param {object[]} scale
* @param {number} min custom minimum value (optional)
* @param {number} max custom maximum value (optional)
*/
export function generateCssGradient(scale, min, max) {
let gradient = "";
max = (Number.isFinite(max)) ? max : scale[scale.length - 1].value;
min = (Number.isFinite(min)) ? min : scale[0].value;
for (let j = 0; j < scale.length; ++j) {
const percentage = parseInt((scale[j].value - min) / (max - min) * 100);
gradient += `,${scale[j].color} ${percentage}%`;
}
return `linear-gradient(to right ${gradient}`;
}
/**
* Get the matching color from a given scale for a value with
* linear interpolation
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*
* @param {object[]} scale
* @param {number} value
* @param {string} format "hexstring", "rgb", (default "hexstring")
*/
export function getColorFromScale(scale, value, format) {
let min = 0;
let max = scale.length - 1;
for (let i = 0; i < scale.length; ++i) {
if (scale[i].value <= value) min = i;
if (scale[i].value >= value) max = i;
}
if (value <= scale[min].value) {
const color = new iro.Color(scale[min].color);
return {
color: (format == "rgb") ? color.rgb : color.hexString,
opacity: scale[min].opacity,
};
} else if (value >= scale[max].value) {
const color = new iro.Color(scale[max].color);
return {
color: (format == "rgb") ? color.rgb : color.hexString,
opacity: scale[max].opacity,
};
} else {
const range = scale[max].value - scale[min].value;
const ratio = (value - scale[min].value) / range;
const opacityRange = scale[max].opacity - scale[min].opacity;
const opacity = scale[min].opacity + (opacityRange * ratio);
const colorMin = new iro.Color(scale[min].color).rgb;
const colorMax = new iro.Color(scale[max].color).rgb;
for (let i in colorMin) {
const channelRange = colorMax[i] - colorMin[i];
colorMin[i] = Math.floor(colorMin[i] + (channelRange * ratio));
}
const color = new iro.Color(colorMin);
return {
color: (format == "rgb") ? color.rgb : color.hexString,
opacity: opacity
}
}
}