Source: map/filters/ui/TimeFilter.js

import { FilterUi } from "./FilterUi.js";
import { SliderHeader } from "../../../ui/slider/SliderHeader.js";
import { TimeSlider } from "../../../ui/slider/TimeSlider.js";
import { FilterSettings } from "../FilterSettings.js";
import "./TimeFilter.css";

export { TimeFilter };

/**
 * A class that defines the Ui for a Slider-based TimeFilter
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.map.filters.ui
 */
class TimeFilter 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: "Time Filter",
            column: "", // automatically identify time column if not defined
            schema: "ymd",
            allowMultipleSliderMode: true,
            activeSliders: "",
            operators: [
                "-",
                "=",
                ">",
                "≥",
                "<",
                "≤"
            ],
            initialOperator: "-",
            initialBegin: "1980-01-01T00:00:00Z",
            initialEnd: "2022-12-31T23:59:59Z",
            begin: "1980-01-01T00:00:00Z",
            end: "2022-12-31T23:59:59Z"
        }, options || {});

        super(target, options, layers);

        this.schemaRegex = new RegExp("^y(m(d)?)?$");
        this.sliderContainer_ = null;
        this.sliders_ = [];
        this.types_ = {
            "y": "YEAR",
            "m": "MONTH",
            "d": "DAY"
        };

        this.headerConfig = {
            // single types
            "YEAR": { pattern: '^([0-9]{4})$', maxLength: 4 },
            "MONTH": { pattern: '^(1[0-2]|0[1-9])$', maxLength: 2 },
            "DAY": { pattern: '^(3[0-1]|2[0-9]|1[0-9]|0[1-9])$', maxLength: 2 },
            // schemas
            "ymd": { pattern: '^([0-9]{4})(-((1[0-2]|0[1-9])(-(3[0-1]|2[0-9]|1[0-9]|0[1-9])?)?)?)?$', maxLength: 10 },
            "ym": { pattern: '^([0-9]{4})(-(1[0-2]|0[1-9])?)?$', maxLength: 7 },
            "y": { pattern: '^([0-9]{4})$', maxLength: 4 }
        };

        this.settingsClass_ = TimeFilterSettings;
        this.currentValue_ = null
        this.operator_ = options.initialOperator || options.operators[0];

        // validate active sliders and schema
        let schema = this.options_.schema ? this.options_.schema.toLowerCase() : 'ymd';
        let activeSliders = this.options_.activeSliders ? this.options_.activeSliders.toLowerCase() : '';
        if (!this.schemaRegex.test(activeSliders)) activeSliders = "";
        for (let i = 0; i < activeSliders.length; ++i) {
            if (!schema.includes(activeSliders[i])) {
                activeSliders = "";
                break;
            }
        }
        this.options_.activeSliders = activeSliders;
        this.options_.schema = schema;

        // check if time needs to be dynamic
        this.options_.begin = this.setTimeDynamically(this.options_.begin);
        this.options_.end = this.setTimeDynamically(this.options_.end);
        this.options_.initialBegin = this.setTimeDynamically(this.options_.initialBegin);
        this.options_.initialEnd = this.setTimeDynamically(this.options_.initialEnd);

        // initialize element and slider
        this.initFilterElement_();
        this.initSliders_();
    }

    /**
     * internal method for initializing the main
     * HTML structure of the element
     * 
     * @private
     */
    initFilterElement_() {
        this.setTitle(this.options_.title);

        const content = this.getContentContainer();
        content.classList.add("time-filter");

        this.sliderContainer_ = document.createElement("div");
        this.sliderContainer_.classList.add("slider-container");
        content.appendChild(this.sliderContainer_);
        content.appendChild(this.initMultipleSliderMode_());
    }

    /**
     * @private
     * @param {string} type 
     * @returns {string} validation regex pattern and max length
     */
    getHeaderConfig_(type) {
        if (type == "DATE") {
            let schema = this.options_.schema;
            if (!this.schemaRegex.test(schema)) schema = "ymd";

            return this.headerConfig[schema];
        } else {
            return this.headerConfig[type];
        }
    }

    /**
     * @private
     * @param {string} type 
     * @param {object} value 
     * @param {TimeSlider} slider 
     */
    parseHeaderValue_(type, value, slider) {
        let sliderValue = slider.getValue();
        let currentValue = sliderValue;

        if (slider.options_.handles == 2) {
            if (value.type == "begin") {
                currentValue = sliderValue.left;
            } else {
                currentValue = sliderValue.right;
            }
        }

        switch (type.toLowerCase()) {
            case "year":
                currentValue.setUTCFullYear(Number.parseInt(value.value));
                break;
            case "month":
                currentValue.setUTCDate(1);
                currentValue.setUTCMonth(Number.parseInt(value.value) - 1);
                break;
            case "day":
                currentValue.setUTCDate(Number.parseInt(value.value));
                break;
            default:
                const schema = this.options_.schema.toLowerCase();

                // initialize day as 1 to prevent month overflow
                currentValue.setUTCDate(1);

                // the year is always defined
                const year = Number.parseInt(value.value.substr(0, 4));
                currentValue.setUTCFullYear(year);

                // set default month and overwrite if it is set
                let month = (value.type == "begin") ? 0 : 11;
                if (schema.includes("m") && (value.value.length >= 7)) {
                    month = Number.parseInt(value.value.substr(5, 2)) - 1;
                }
                currentValue.setUTCMonth(month);

                // set default month and overwrite if it is set
                const daysInMonth = new Date(year, month + 1, 0).getDate();
                let day = (value.type == "begin") ? 1 : daysInMonth;
                if (schema.includes("d") && (value.value.length >= 10)) {
                    day = Number.parseInt(value.value.substr(8, 2));
                    if (day > daysInMonth) day = daysInMonth;
                }
                currentValue.setUTCDate(day);

                break;
        }

        if (slider.options_.handles == 2) {
            if (value.type == "begin") {
                sliderValue.left = currentValue;
            } else {
                sliderValue.right = currentValue;
            }
        } else {
            sliderValue = currentValue;
        }

        slider.setValue(sliderValue);
        slider.stop_();
    }

    /**
     * Init a slider header and its events including
     * validation and autocompletion of the input
     * 
     * @param {Slider} slider 
     * @param {string} type 
     * @param {string[]} operators 
     * @param {boolean} isLast 
     * @returns {SliderHeader} header
     */
    initHeader_(slider, type, operators, isLast) {
        const config = this.getHeaderConfig_(type);

        const header = new SliderHeader(null, {
            title: type,
            operators: operators,
            defaultOperator: ((isLast) ? this.operator_ : null),
            useInput: true,
            inputPattern: config.pattern,
            inputLength: config.maxLength
        });

        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);

            switch (type.toLowerCase()) {
                case "year":
                    if (value.includes("-")) value = str.replace("-", "");
                    maxLength = 4;
                    break;
                case "month":
                    if (value.includes("-")) value = str.replace("-", "");
                    maxLength = 2;
                    break;
                case "day":
                    if (value.includes("-")) value = str.replace("-", "");
                    maxLength = 2;
                    break;
                default:
                    //toDo
                    const schema = this.options_.schema.toLowerCase();
                    const parts = value.split("-");

                    if (schema.includes("m")) {
                        if ((value.length > 4) && ((parts.length == 1) || (parts[0].length > 4))) {
                            value = value.substring(0, 4) + "-" + value.substring(4);
                        }
                    }
                    if (schema.includes("d")) {
                        if ((value.length > 7) && (parts.length <= 2) && (value.charAt(7) != "-")) {
                            value = value.substring(0, 7) + "-" + value.substring(7);
                        }
                    }
                    break;
            }

            header.setValue(e.type, value);
        });

        // operator selection
        if (isLast) {
            header.on("select", operator => {
                this.operator_ = operator;
                this.initSliders_();
                this.fire("change", this);
            });
        }

        // apply the header value to the slider
        header.on("change", value => this.parseHeaderValue_(type, value, slider));

        // insert before slider
        slider.getElement().insertAdjacentElement("beforebegin", header.getElement())

        return header;
    }

    /**
     * method for creating a slider with a header
     * @param {string} mode 
     * @param {boolean} isLast 
     * @returns {object}
     * @private 
     */
    initSingleSlider_(mode, isLast) {
        const options = this.options_;
        const operators = (isLast) ? options.operators : ["="];

        const type = (this.types_[mode]) ? this.types_[mode] : "DATE"

        const sliderOptions = {
            min: new Date(options.begin).getTime(),
            max: new Date(options.end).getTime()
        }

        const previousLeft = (this.currentValue_ && (this.currentValue_.left instanceof Date)) ? this.currentValue_.left : this.currentValue_;
        const previousRight = (this.currentValue_ && (this.currentValue_.right instanceof Date)) ? this.currentValue_.right : null;

        if (isLast && ("-" == this.operator_)) {
            sliderOptions.handles = 2;
            sliderOptions.value = {
                left: new Date(previousLeft || options.initialBegin || options.begin).getTime(),
                right: new Date(previousRight || options.initialEnd || options.end).getTime()
            }
        } else {
            sliderOptions.handles = 1;
            sliderOptions.value = new Date(previousLeft || options.initialBegin || options.begin).getTime();
        }

        const container = document.createElement("div");
        const slider = new TimeSlider(container, sliderOptions);
        const header = this.initHeader_(slider, type, operators, isLast);

        const dispose = () => {
            header.dispose();
            slider.dispose();
            container.remove();
        };

        const item = {
            type: type,
            container: container,
            header: header,
            slider: slider,
            dispose: dispose
        };

        slider.on("change", (value) => {
            this.updateValues_(item);
        });
        slider.on("stop", (value) => {
            this.updateValues_(item);
            this.fire("change", this);
        });

        this.initArrowControls_(item);

        return item;
    }

    initArrowControls_(item) {

        const listener = (type, increment, incValue) => {
            const values = this.getValueForSchema_(true, item.slider.getValue());
            let value = values[type];

            let year = Number.parseInt(value.substring(0, 4));
            let month = Number.parseInt(value.substring(5, 7));
            let day = Number.parseInt(value.substring(8, 10));

            switch (item.type.toLowerCase()) {
                case "year":
                    value = year + ((increment) ? 1 : -1);
                    break;
                case "month":
                    value = month + ((increment) ? 1 : -1);
                    break;
                case "day":
                    value = day + ((increment) ? 1 : -1);
                    break;
                default:
                    const schema = this.options_.schema.toLowerCase();

                    if (schema.includes("d") && incValue === "d") {
                        day += (increment) ? 1 : -1;
                    }
                    if (schema.includes("m") && incValue === "m") {
                        month += (increment) ? 1 : -1;
                    }
                    if (schema.includes("y") && incValue === "y") {
                        year += (increment) ? 1 : -1;
                    }
                    if (schema.includes("ymd") && incValue === "e") {
                        day = this.options_.end.substring(8, 10);
                        month = this.options_.end.substring(5, 7);
                        year = this.options_.end.substring(0, 4);
                    }
                    if (schema.includes("ymd") && incValue === "p") {
                        day = this.options_.begin.substring(8, 10);
                        month = this.options_.begin.substring(5, 7);
                        year = this.options_.begin.substring(0, 4);
                    }

                    value = new Date(Date.UTC(year, month - 1, day)).toISOString().substring(0, 10) + value.substring(10);

                    break;
            }

            this.parseHeaderValue_(item.type, {
                type: type,
                value: value
            }, item.slider);
        }

        item.header.on("arrow_down", inputName => listener(inputName, false, "d"));
        item.header.on("arrow_up", inputName => listener(inputName, true, "d"));
        item.header.on("arrow_down_shift", inputName => listener(inputName, false, "m"));
        item.header.on("arrow_up_shift", inputName => listener(inputName, true, "m"));
        item.header.on("arrow_down_shift_ctrl", inputName => listener(inputName, false, "y"));
        item.header.on("arrow_up_shift_ctrl", inputName => listener(inputName, true, "y"));
        item.header.on("pos1", inputName => listener(inputName, true, "p"));
        item.header.on("end", inputName => listener(inputName, true, "e"));

        item.slider.on("arrow_left", handleName => listener((handleName == "left") ? "begin" : "end", false, "d"));
        item.slider.on("arrow_right", handleName => listener((handleName == "left") ? "begin" : "end", true, "d"));
        item.slider.on("ArrowRight_Shift", handleName => listener((handleName == "left") ? "begin" : "end", true, "m"));
        item.slider.on("ArrowLeft_Shift", handleName => listener((handleName == "left") ? "begin" : "end", false, "m"));
        item.slider.on("ArrowRight_Shift_cntrl", handleName => listener((handleName == "left") ? "begin" : "end", true, "y"));
        item.slider.on("ArrowLeft_Shift_cntrl", handleName => listener((handleName == "left") ? "begin" : "end", false, "y"));
        item.slider.on("home", handleName => listener((handleName == "left") ? "begin" : "end", false, "p"));
        item.slider.on("end", handleName => listener((handleName == "left") ? "begin" : "end", false, "e"));

    }

    /**
     * method to update the slider and header values
     * @param {object} source slider item
     * @param {boolean} init sliders are initialized and not updated
     * @private
     */
    updateValues_(source, init) {
        const options = this.options_;

        if ((!this.currentValue_) || init) this.currentValue_ = source.slider.getValue();

        const totalMin = new Date(options.begin).getTime();
        const totalMax = new Date(options.end).getTime();

        const newIso = this.getValueForSchema_(true, source.slider.getValue());
        const prevIso = this.getValueForSchema_(true);

        // transform currentValue depending on slider
        for (let i = 0; i < this.sliders_.length; ++i) {
            const item = this.sliders_[i];

            switch (item.type) {
                case "DATE":
                    this.currentValue_ = source.slider.getValue();
                    break;
                case "YEAR":
                    {
                        let beginYear = newIso.begin.substr(0, 4);
                        let endYear = newIso.end.substr(0, 4);

                        if (item.slider.options_.handles == 2) {
                            this.currentValue_ = {
                                left: new Date(beginYear + "-01-01T00:00:00Z"),
                                right: new Date(endYear + "-12-31T23:59:59Z")
                            };
                        } else {
                            this.currentValue_ = new Date(beginYear + "-01-01T00:00:00Z");
                        }
                    }
                    break;
                case "MONTH":
                    {
                        const year = newIso.begin.substr(0, 4);
                        let beginMonth = newIso.begin.substr(5, 2);
                        let endMonth = newIso.end.substr(5, 2);

                        if (item != source) {
                            beginMonth = prevIso.begin.substr(5, 2);
                            endMonth = prevIso.end.substr(5, 2);
                        }

                        const daysInEndMonth = new Date(year, endMonth, 0).getDate();

                        if (item.slider.options_.handles == 2) {
                            this.currentValue_ = {
                                left: new Date(year + "-" + beginMonth + "-01T00:00:00Z"),
                                right: new Date(year + "-" + endMonth + "-" + daysInEndMonth + "T23:59:59Z")
                            };
                        } else {
                            this.currentValue_ = new Date(year + "-" + beginMonth + "-01T00:00:00Z");
                        }
                    }
                    break;
                case "DAY":
                    {
                        const year = newIso.begin.substr(0, 4);
                        let month = newIso.begin.substr(5, 2);
                        let beginDay = newIso.begin.substr(8, 2);
                        let endDay = newIso.end.substr(8, 2);

                        if (item != source) {
                            beginDay = prevIso.begin.substr(8, 2);
                            endDay = prevIso.end.substr(8, 2);
                            if (source.type != "MONTH") month = prevIso.begin.substr(5, 2);
                        }

                        const daysInMonth = new Date(year, month, 0).getDate();

                        if (parseInt(beginDay) > daysInMonth) beginDay = daysInMonth;
                        if (parseInt(endDay) > daysInMonth) endDay = daysInMonth;

                        if (item.slider.options_.handles == 2) {
                            this.currentValue_ = {
                                left: new Date(year + "-" + month + "-" + beginDay + "T00:00:00Z"),
                                right: new Date(year + "-" + month + "-" + endDay + "T23:59:59Z")
                            }
                        } else {
                            this.currentValue_ = new Date(year + "-" + month + "-" + beginDay + "T00:00:00Z");
                        }
                    }
                    break;
            }
        }

        if (this.currentValue_ instanceof Date) {
            if (this.currentValue_ < totalMin) this.currentValue_ = totalMin;
            if (this.currentValue_ > totalMax) this.currentValue_ = totalMax;
        } else {
            if (this.currentValue_.left < totalMin) this.currentValue_.left = totalMin;
            if (this.currentValue_.left > totalMax) this.currentValue_.left = totalMax;
            if (this.currentValue_.right < totalMin) this.currentValue_.right = totalMin;
            if (this.currentValue_.right > totalMax) this.currentValue_.right = totalMax;
        }

        const isoValue = this.getValueForSchema_(true);
        const daysInMonth = new Date(isoValue.begin.substr(0, 4), isoValue.begin.substr(5, 2), 0).getDate();

        // update all values in headers and sliders
        for (let i = 0; i < this.sliders_.length; ++i) {
            const item = this.sliders_[i];

            let min = totalMin
            let max = totalMax
            switch (item.type) {
                case "DATE":
                    item.header.setValues(this.getValueForSchema_());
                    break;
                case "YEAR":
                    item.header.setValues({
                        begin: isoValue.begin.substr(0, 4),
                        end: isoValue.end.substr(0, 4)
                    });
                    break;
                case "MONTH":
                    min = new Date(isoValue.begin.substr(0, 5) + "01-01T00:00:00Z").getTime();
                    max = new Date(isoValue.begin.substr(0, 5) + "12-31T23:59:59Z").getTime();
                    item.header.setValues({
                        begin: isoValue.begin.substr(5, 2),
                        end: isoValue.end.substr(5, 2)
                    });
                    break;
                case "DAY":
                    min = new Date(isoValue.begin.substr(0, 8) + "01T00:00:00Z").getTime();
                    max = new Date(isoValue.begin.substr(0, 8) + daysInMonth + "T23:59:59Z").getTime();
                    item.header.setValues({
                        begin: isoValue.begin.substr(8, 2),
                        end: isoValue.end.substr(8, 2)
                    });
                    break;
            }
            item.slider.options_.min = (min >= totalMin) ? min : totalMin;
            item.slider.options_.max = (max <= totalMax) ? max : totalMax;

            if (init || (item != source)) {
                if (item.slider.options_.handles == 2) {
                    item.slider.options_.value = {
                        left: new Date(isoValue.begin).getTime(),
                        right: new Date(isoValue.end).getTime(),
                    };
                } else {
                    item.slider.options_.value = new Date(isoValue.begin).getTime();
                }

                item.slider.validate_();
                item.slider.updateHandles_();
            }
        }
    }

    /**
     * internal method for initializing the date slider.
     * the slider gets re-initialized everytime the
     * operator changes.
     * 
     * @private
     */
    initSliders_() {
        // remove all sliders
        for (let i in this.sliders_) this.sliders_[i].dispose();
        this.sliders_ = [];

        // adjust operator
        if (!this.options_.operators.includes(this.operator_)) {
            if (this.options_.operators.length == 0) this.options_.operators = ["-"];
            this.operator_ = this.options_.operators[0];
        }

        const activeSliders = this.options_.activeSliders;

        if (activeSliders.length == 0) {
            const item = this.initSingleSlider_(null, true);
            this.sliderContainer_.appendChild(item.container);
            this.sliders_.push(item);
        } else {
            for (let i = 0; i < activeSliders.length; ++i) {
                const isLast = (activeSliders.length == (i + 1));
                const item = this.initSingleSlider_(activeSliders[i], isLast);
                this.sliderContainer_.appendChild(item.container);
                this.sliders_.push(item);
            }
        }

        this.updateValues_(this.sliders_[this.sliders_.length - 1], true);
    }

    sortSchema_(input) {
        input = input.toLowerCase();
        let output = "";
        for (let i in this.types_) {
            if (input.includes(i)) {
                output += i;
            }
        }
        return output;
    }

    initMultipleSliderMode_() {
        const advanced = document.createElement("div");
        advanced.classList.add("advanced");
        advanced.style.display = "none";
        advanced.innerHTML = `
                <button class="btn-close"><i class='fas fa-times'></i></button>
                <div class="settings"></div>
            `;
        const settingsContainer = advanced.querySelector(".settings");

        // toggle advanced settings

        this.settingsOpen_ = false;
        const toggleSettings = () => {
            const tool = this.getElement().querySelector(".sidebar-element-tool.multiple-slider-mode");
            if (this.settingsOpen_) {
                advanced.style.display = "none";
                tool.classList.remove("enabled");
            } else {
                advanced.style.display = "flex";
                tool.classList.add("enabled")
            }
            this.settingsOpen_ = !this.settingsOpen_;
        }

        const closeButton = advanced.querySelector(".btn-close");
        closeButton.addEventListener("click", toggleSettings);
        this.addTool("vef vef-filter-settings multiple-slider-mode", toggleSettings, "Advanced Settings", true);

        const schema = this.sortSchema_(this.options_.schema);
        const activeSliders = this.sortSchema_(this.options_.activeSliders);

        for (let i = 0; i < schema.length; ++i) {
            if (schema[i] in this.types_) {
                const settingsItem = document.createElement("div");
                settingsItem.classList.add("settings-item");
                settingsItem.dataset.type = schema[i];
                settingsItem.innerHTML = `
                    <span class="fa-stack">
                        <i class="far fa-circle fa-stack-1x"></i>
                        <i class="fas fa-check fa-stack-1x"></i>
                    </span>
                    ${this.types_[schema[i]].toLowerCase()}
                `;
                settingsContainer.appendChild(settingsItem);
            }
        }

        const items = settingsContainer.querySelectorAll(".settings-item");
        for (let i = 0; i < items.length; ++i) {
            const item = items[i];

            const prev = item.previousSibling;
            if (activeSliders.includes(schema[i]) && (!prev || prev.classList.contains("checked"))) {
                item.classList.add("checked");
            }

            item.addEventListener("click", () => {
                if (item.classList.contains("checked")) {
                    item.classList.remove("checked");
                    let next = item.nextSibling;
                    while (next) {
                        next.classList.remove("checked");
                        next = next.nextSibling;
                    }
                } else {
                    item.classList.add("checked");
                    let prev = item.previousSibling;
                    while (prev) {
                        prev.classList.add("checked");
                        prev = prev.previousSibling;
                    }
                }
                let selectedSliders = "";
                for (let j = 0; j < items.length; ++j) {
                    if (items[j].classList.contains("checked")) selectedSliders += items[j].dataset.type;
                }
                this.options_.activeSliders = this.sortSchema_(selectedSliders);
                this.initSliders_();
                this.fire("change", this);
            });
        }


        if (!this.options_.allowMultipleSliderMode) this.toggleToolVisibility("multiple-slider-mode", false);

        return advanced;
    }


    /**
     * internal method for getting the selected date values
     * adjusted to the time resolution defined in options.schema
     * 
     * @param {boolean} iso returns full iso-datestring if true
     * @param {object | number} value (optional)
     * @returns {object} {begin, end}
     */
    getValueForSchema_(iso, value) {
        value = value || this.currentValue_;
        let begin = ((isFinite(value.left)) ? value.left : value).toISOString();
        let end = ((isFinite(value.right)) ? value.right : value).toISOString();

        let schema = this.options_.schema.toLowerCase();
        let activeSliders = this.options_.activeSliders.toLowerCase();
        if (!this.schemaRegex.test(schema)) schema = "ymd";
        if (!this.schemaRegex.test(activeSliders)) activeSliders = "";

        // "Y" -> Year is always required
        let validBegin = begin.substr(0, 4);
        let validEnd = end.substr(0, 4);

        if (schema.includes("m") && ((activeSliders.length == 0) || activeSliders.includes("m"))) {
            validBegin = begin.substr(0, 7);
            validEnd = end.substr(0, 7);
            if (schema.includes("d") && ((activeSliders.length == 0) || activeSliders.includes("d"))) {
                validBegin = begin.substr(0, 10);
                validEnd = end.substr(0, 10);
                if (iso) {
                    validBegin += "T00:00:00";
                    validEnd += "T23:59:59";
                }
            } else if (iso) {
                const daysInEndMonth = new Date(end.substr(0, 4), end.substr(5, 2), 0).getDate();
                validEnd += "-" + daysInEndMonth + "T23:59:59";
                validBegin += "-01T00:00:00";
            }
        } else if (iso) {
            validBegin += "-01-01T00:00:00";
            validEnd += "-12-31T23:59:59";
        }

        return {
            begin: validBegin,
            end: validEnd
        }
    }


    /**
     * Get the filter object to pass it on to the Layers.
     * 
     * @override
     * @returns {object} filter object
     */
    getActiveFilter() {
        let val = this.getValueForSchema_(true);

        // case "bt" and "eq" and as fallback for invalid operators
        let values = [["bt", val.begin, val.end]];

        switch (this.operator_) {
            case "gt":
            case ">":
                values = [["gt", val.begin]];
                break;
            case "gteq":
            case "≥":
                values = [["gteq", val.begin]];
                break;
            case "lt":
            case "<":
                values = [["lt", val.begin]];
                break;
            case "lteq":
            case "≤":
                values = [["lteq", val.begin]];
                break;
        }

        const result = {
            type: "time",
            column: this.options_.column || "",
            values: values,
            excludedLayers: this.getExcludedLayers()
        };
        return result;
    }

    /**
     * Get the filter's options-object
     */
    getOptions() {
        const activeFilterValues = this.getActiveFilter().values[0];
        const operator = this.operator_;
        const options = Object.assign({}, this.options_);

        options.initialOperator = operator;

        // assign active values
        if (operator == "-") {
            options.initialBegin = activeFilterValues[1];
            options.initialEnd = activeFilterValues[2];
        } else {
            options.initialBegin = activeFilterValues[1];
            options.initialEnd = "";
        }

        return options;
    }

    /**
     * method to reload options after applying settings
     */
    reloadOptions_() {
        this.setTitle(this.options_.title);
        this.toggleToolVisibility("btn-power", this.options_.deactivatable);
        this.toggleToolVisibility("multiple-slider-mode", this.options_.allowMultipleSliderMode);
        this.initSliders_();
        this.fire("change", this);
    }

    setTimeDynamically(time) {
        let access = false;
        let notTime = ["today"];
        notTime.forEach(element => {
            if ((typeof time == "string") && time.includes(element)) access = true;
        });

        // return time if it is not using the today-syntax and alway assume UTC
        if (!access) {
            if(time && !time.endsWith("Z") && !time.endsWith("z")) time += "Z";
            return time
        };

        if (time.includes("today")) {
            let newDate = new Date();
            time = time.replace("today", "");
            if (time == "") return newDate.toISOString();
            let addOrsub = time[0];
            time = time.replace(addOrsub, "")
            if (time == "") return newDate.toISOString();
            let typeOftime = time[0];
            time = time.replace(typeOftime, "")
            if (time == "") return newDate.toISOString();
            let SkipValue = Number(time);

            if (addOrsub === "+") {
                switch (typeOftime.toLowerCase()) {
                    case "d":
                        newDate.setUTCDate(newDate.getUTCDate() + SkipValue);
                        break;
                    case "m":
                        newDate.setUTCMonth(newDate.getUTCMonth() + SkipValue);
                        break;
                    case "y":
                        newDate.setUTCFullYear(newDate.getUTCFullYear() + SkipValue);
                        break;
                }
            } else if (addOrsub === "-") {
                switch (typeOftime.toLowerCase()) {
                    case "d":
                        newDate.setUTCDate(newDate.getUTCDate() - SkipValue);
                        break;
                    case "m":
                        newDate.setUTCMonth(newDate.getUTCMonth() - SkipValue);
                        break;
                    case "y":
                        newDate.setUTCFullYear(newDate.getUTCFullYear() - SkipValue);
                        break;
                }
            }
            return newDate.toISOString();
        }
        return time;
    }
}

/**
 * Class representing the settings for the TimeFilter
 * @private
 */
class TimeFilterSettings extends FilterSettings {

    htmlExtension = `
        <div><label style="width: 100%;"><span class="input-label" style="width: 80px;">Column: </span><input style="width: calc(100% - 80px);" type="text" name="column" placeholder="Let empty to auto-choose a column for each layer"/></label></div>
        <div><label style="width: 100%;"><span class="input-label" style="width: 80px;">Schema: </span><select style="width: calc(100% - 80px);" name="schema">
            <option value="y">YYYY</option>
            <option value="ym">YYYY-MM</option>
            <option value="ymd">YYYY-MM-DD</option>
        </select></label></div>
        <div><label style="width: 100%;"><span class="input-label" style="width: 80px;">Minimum: </span><input style="width: calc(100% - 80px);" type="text" name="minimum"/></label></div>
        <div><label style="width: 100%;"><span class="input-label" style="width: 80px;">Maximum: </span><input style="width: calc(100% - 80px);" type="text" name="maximum"/></label></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"/> &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"/> &lt;</label>
        <label title="less than or equal to"><input type="checkbox" name="operator-lteq"/> ≤</label>
        </div>
        <div><label><input type="checkbox" name="allow-multiple-slider-mode"/> Show "Multiple Slider Mode" Button</label></div>
    `;

    /**
     * @param {FilterUi} filter
     */
    constructor(filter) {
        super(filter);

        this.query("form").insertAdjacentHTML("beforeend", this.htmlExtension);
    }

    /**
     * Get the currently configured filter options
     */
    getFilterOptions() {
        const options = super.getFilterOptions();

        const minimum = this.query("form input[name='minimum']");
        const maximum = this.query("form input[name='maximum']");

        if (!minimum.checkValidity() || !maximum.checkValidity()) return null;

        options.begin = minimum.value + "T00:00:00Z";
        options.end = maximum.value + "T23:59:59Z"

        options.column = this.query("form input[name='column']").value;
        options.schema = this.query("form select[name='schema']").value;
        options.allowMultipleSliderMode = this.query("form input[name='allow-multiple-slider-mode']").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.query("form input[name='column']").value = options.column;
        this.query("form input[name='allow-multiple-slider-mode']").checked = options.allowMultipleSliderMode;
        this.query("form select[name='schema']").value = options.schema;
        this.query("form input[name='minimum']").value = (options.begin.length > 9) ? options.begin.substring(0, 10) : options.end;
        this.query("form input[name='maximum']").value = (options.end.length > 9) ? options.end.substring(0, 10) : options.end;
        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("≤");
    }

}