import { SliderHeader } from "../../../ui/slider/SliderHeader.js";
import { Slider } from "../../../ui/slider/Slider.js";
import { FilterUi } from "./FilterUi.js";
import { roundNumber } from "../../../utils/utils.js";
import { FilterSettings } from "../FilterSettings.js";
import { EditableTable } from "../../../ui/table/EditableTable.js";
import { Dropdown } from "../../../ui/Dropdown.js";
export { RangeFilter };
/**
* A class that defines the Ui for a Slider-based Range Filter
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.map.filters.ui
*/
class RangeFilter extends FilterUi {
/**
* @param {HTMLElement | string} target
* @param {object} options filter specific options
* @param {LayerManager} layers Used for included/excluded layers
*/
constructor(target, options, layers) {
// apply default options
options = Object.assign({
title: "Filter By Range",
allowShowNullValues: true,
showNullValues: true,
columns: [{
title: "Depth",
column: "depth",
decimals: 0,
begin: 0,
end: 1000,
}],
selectedIndex: 0,
operators: [
"-",
"=",
">",
"≥",
"<",
"≤"
],
initialOperator: "-",
initialBegin: 0,
initialEnd: 1000,
}, options || {});
super(target, options, layers);
this.slider = null;
this.columnDropdown = null;
this.settingsClass_ = RangeFilterSettings;
this.currentValue_ = null
this.operator_ = options.initialOperator || options.operators[0];
const container = this.getContentContainer();
container.classList.add("range-filter");
container.style.paddingBottom = "5px";
this.initSlider_();
this.setTitle(this.options_.title);
this.initShowNullButton_();
}
get column() {
return this.options_.columns?.[this.options_.selectedIndex]?.column || "";
}
get decimals() {
return parseFloat(this.options_.columns?.[this.options_.selectedIndex]?.decimals || 0);
}
get begin() {
return parseFloat(this.options_.columns?.[this.options_.selectedIndex]?.begin || 0);
}
get end() {
return parseFloat(this.options_.columns?.[this.options_.selectedIndex]?.end || 100);
}
/**
* Init a slider header and its events including
* validation of the input
*
* @param {Slider} slider
* @param {string} type
* @param {string[]} operators
* @returns {SliderHeader} header
*/
initHeader_(slider) {
const header = new SliderHeader(null, {
title: "VALUES",
operators: this.options_.operators,
defaultOperator: this.operator_,
useInput: true,
inputPattern: String.raw`^[\-+]?[0-9]*(?:[.,][0-9]+)?$`
});
const validCharacters = "-+0123456789.,";
const removeInvalidCharacters = (str) => {
for (let i = str.length - 1; i >= 0; --i) {
if (!validCharacters.includes(str[i])) str = str.replace(str[i], "");
}
return str;
}
// validation and auto-complete minus character or end
header.on("input", (e) => {
let value = removeInvalidCharacters(e.value);
if (value.includes(",")) value = value.replaceAll(",", ".");
if (value.startsWith(".")) value = "0" + value;
// remove additional decimal seperators
const parts = value.split(".");
if (parts.length > 2) {
value = "";
let addedSeperator = false;
for (let i = 0; i < parts.length; ++i) {
value += parts[i];
if (!addedSeperator) {
value += ".";
addedSeperator = true;
}
}
}
header.setValue(e.type, value);
});
// operator selection
header.on("select", operator => {
this.operator_ = operator;
this.initSlider_();
this.fire("change", this);
});
// apply the header value to the slider
header.on("change", value => this.parseHeaderValue_(value));
// insert before slider
slider.getElement().insertAdjacentElement("beforebegin", header.getElement())
return header;
}
initColumnSelection_() {
const options = this.options_;
if (this.columnDropdown) this.columnDropdown.dispose();
if (options.columns.length <= 1) {
return;
}
if (options.selectedIndex >= options.columns.length) options.selectedIndex = 0;
this.columnDropdown = new Dropdown(null, {
placeholder: "select a column...",
deselectLabel: null,
showDeselectItem: false,
search: false,
});
const items = {}
for (let i = 0; i < options.columns.length; ++i) {
items[options.columns[i].title] = i;
}
this.columnDropdown.setItems(items);
this.columnDropdown.select(options.columns[options.selectedIndex].title);
this.columnDropdown.on("select", e => {
options.selectedIndex = e.item;
options.initialBegin = this.begin;
options.initialEnd = this.end;
this.currentValue_ = null;
this.reloadOptions_();
});
const element = this.columnDropdown.getElement();
element.style.marginBottom = "10px";
this.content_.insertAdjacentElement("afterbegin", element);
}
/**
* internal method for initializing the slider.
* the slider gets re-initialized everytime the
* operator changes.
*
* @private
*/
initSlider_() {
const options = this.options_;
this.initColumnSelection_();
// adjust operator
if (!options.operators.includes(this.operator_)) {
if (options.operators.length == 0) options.operators = ["-"];
this.operator_ = options.operators[0];
}
// remove slider
if (this.slider_) this.slider_.dispose();
this.slider_ = null;
const sliderOptions = {
min: this.begin,
max: this.end
}
const previousLeft = (Number.isFinite(this.currentValue_) || (this.currentValue_ == null)) ? this.currentValue_ : this.currentValue_.left;
const previousRight = (Number.isFinite(this.currentValue_) || (this.currentValue_ == null)) ? null : this.currentValue_.right;
if ("-" == this.operator_) {
sliderOptions.handles = 2;
sliderOptions.value = {
left: previousLeft || options.initialBegin || this.begin,
right: previousRight || options.initialEnd || this.end
}
} else {
sliderOptions.handles = 1;
sliderOptions.value = previousLeft || options.initialBegin || this.begin;
}
const container = document.createElement("div");
const slider = new Slider(container, sliderOptions);
const header = this.initHeader_(slider)
const updateValue = val => {
let values = val;
if (Number.isFinite(val)) {
values = roundNumber(val, this.decimals)
} else {
values = {
left: roundNumber(val.left, this.decimals),
right: roundNumber(val.right, this.decimals),
};
}
header.setValues({
begin: (Number.isFinite(values)) ? values : values.left,
end: (Number.isFinite(values)) ? values : values.right
});
this.currentValue_ = values;
};
const dispose = () => {
header.dispose();
slider.dispose();
container.remove();
};
const item = {
container: container,
header: header,
slider: slider,
dispose: dispose
};
slider.on("change", (value) => {
updateValue(value);
});
slider.on("stop", (value) => {
updateValue(value);
this.fire("change", this);
});
slider.on("arrow_right", handleName => this.increment(handleName));
slider.on("arrow_left", handleName => this.decrement(handleName));
header.on("arrow_up", inputName => this.increment((inputName == "begin") ? "left" : "right"));
header.on("arrow_down", inputName => this.decrement((inputName == "begin") ? "left" : "right"));
updateValue(slider.getValue());
this.slider_ = item;
this.getContentContainer().appendChild(container);
}
reloadOptions_() {
this.setTitle(this.options_.title);
this.toggleToolVisibility("btn-power", this.options_.deactivatable);
this.toggleToolVisibility("btn-show-null", this.options_.allowShowNullValues);
this.initSlider_();
this.fire("change", this);
}
increment(handle) {
const value = this.slider_.slider.getValue();
if (Number.isFinite(value)) {
this.slider_.slider.setValue(value + 1);
} else {
++value[handle];
this.slider_.slider.setValue(value);
}
this.fire("change", this);
}
decrement(handle) {
const value = this.slider_.slider.getValue();
if (Number.isFinite(value)) {
this.slider_.slider.setValue(value - 1);
} else {
--value[handle];
this.slider_.slider.setValue(value);
}
this.fire("change", this);
}
/**
* @private
* @param {object} value
*/
parseHeaderValue_(value) {
const slider = this.slider_.slider;
const val = parseFloat(value.value);
if (slider.options_.handles == 2) {
const sliderValue = slider.getValue();
if (value.type == "begin") {
sliderValue.left = val;
} else {
sliderValue.right = val;
}
slider.setValue(sliderValue);
} else {
slider.setValue(val);
}
slider.stop_();
}
/**
* Get the filter object to pass it on to the Layers.
*
* @override
* @returns {object} filter object
*/
getActiveFilter() {
let values = null;
switch (this.operator_) {
case ">":
values = [["gt", this.currentValue_]];
break;
case "≥":
values = [["gteq", this.currentValue_]];
break;
case "<":
values = [["lt", this.currentValue_]];
break;
case "≤":
values = [["lteq", this.currentValue_]];
break;
case "=":
values = [["eq", this.currentValue_]];
break;
case "-":
values = [["bt", this.currentValue_.left, this.currentValue_.right]];
break;
}
if (!values) return {};
if ((values.length > 0) && this.options_.showNullValues) values.push(["null"])
return {
type: "attribute",
column: this.column,
values: values,
excludedLayers: this.getExcludedLayers()
};
}
/**
* Get the filter's options-object
*/
getOptions() {
const activeFilterValues = this.getActiveFilter().values[0];
const operator = this.operator_;
const options = super.getOptions();
options.initialOperator = operator;
// assign active values
if (operator == "-") {
options.initialBegin = activeFilterValues[1];
options.initialEnd = activeFilterValues[2];
} else {
options.initialBegin = activeFilterValues[1];
options.initialEnd = null;
}
return options;
}
}
/**
* A class that defines the Ui for a Slider-based Range Filter
*
* @memberof vef.map.filters.ui
* @private
*/
class RangeFilterSettings extends FilterSettings {
htmlExtension = `
<div class="table-container"></div>
<div>
<label class="input-label" style="width: 80px;">Operators:</label>
<label title="equals" style="margin-right:20px;"><input type="checkbox" name="operator-equals"/> =</label>
<label title="range" style="margin-right:20px;"><input type="checkbox" name="operator-range"/> -</label>
<label title="greater than" style="margin-right:20px;"><input type="checkbox" name="operator-gt"/> ></label>
<label title="greater thanor equal to" style="margin-right:20px;"><input type="checkbox" name="operator-gteq"/> ≥</label>
<label title="less than"><input type="checkbox" name="operator-lt"/> <</label>
<label title="less than or equal to"><input type="checkbox" name="operator-lteq"/> ≤</label>
</div>
<div><label><input type="checkbox" name="allow-show-null-values"/> Show "include NULL values" Button</label></div>
`;
/**
* @param {FilterUi} filter
*/
constructor(filter) {
super(filter);
this.query("form").insertAdjacentHTML("beforeend", this.htmlExtension);
this.table = new EditableTable(this.query(".table-container"), [
{
key: "title",
name: "Title"
},
{
key: "column",
name: "Column"
},
{
key: "decimals",
name: "Decimals"
},
{
key: "begin",
name: "Begin"
},
{
key: "end",
name: "End"
}
], filter.options_.columns);
}
/**
* Get the currently configured filter options
*/
getFilterOptions() {
const options = super.getFilterOptions();
options.columns = this.table.getValues();
options.allowShowNullValues = this.query("form input[name='allow-show-null-values']").checked;
const operators = [];
if (this.query("form input[name='operator-equals']").checked) operators.push("=");
if (this.query("form input[name='operator-range']").checked) operators.push("-");
if (this.query("form input[name='operator-gt']").checked) operators.push(">");
if (this.query("form input[name='operator-gteq']").checked) operators.push("≥");
if (this.query("form input[name='operator-lt']").checked) operators.push("<");
if (this.query("form input[name='operator-lteq']").checked) operators.push("≤");
options.operators = operators;
return options;
}
/**
* Update Ui based on the filters current options
*/
updateFilterOptions() {
super.updateFilterOptions();
const options = this.filter.options_;
this.table.clearTable();
this.table.Row(options.columns);
this.query("form input[name='allow-show-null-values']").checked = options.allowShowNullValues;
this.query("form input[name='operator-equals']").checked = options.operators.includes("=");
this.query("form input[name='operator-range']").checked = options.operators.includes("-");
this.query("form input[name='operator-gt']").checked = options.operators.includes(">");
this.query("form input[name='operator-gteq']").checked = options.operators.includes("≥");
this.query("form input[name='operator-lt']").checked = options.operators.includes("<");
this.query("form input[name='operator-lteq']").checked = options.operators.includes("≤");
}
}