/**
 * Visuals - Industrial Visualization Framework for JavaScript
 *
 * Copyright © 2012-2023 Smart HMI GmbH
 *
 * All rights reserved
 *
 * No part of this website or any of its contents may be reproduced, copied, modified or
 * adapted, without the prior written permission of Smart HMI.
 *
 * Commercial use and distribution of the contents of the website is not allowed without
 * express and prior written permission of Smart HMI.
 *
 *
 * Web: http://www.smart-hmi.de
 *
 * @version 2.14.0 01e8af5.36320 21-04-2023 09:29:26
 */

/**
 * Alarm History
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "alarm-history",
 *     "name": null,
 *     "template": "default/alarm-history"
 * }
 *
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 */
(function() {
    'use strict';

    //variables for reference in control definition
    var className = "AlarmHistory", //control name in camel-case
        uiType = "alarm-history", //control keyword (data-ui)
        isContainer = true;

    //example - default configuration
    var defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/" + uiType,
        "label": uiType,
        "dateformat": "${alarmlist_dateformat}"
    };

    //setup module-logger
    var ENABLE_LOGGING = false,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    //declare private variables - START
    var alarmGridConfig = {
            "name": "alarms-historic",
            "fields": ["id", "index", "level", "come", "gone", "group", "needAck", "isAck", "alarmAck", "ackUser", "json"]
        },
        alarmTableConfig = {
            "label": "${alarmlist_history_title}",
            "table": alarmGridConfig.name,
            "name": "alarm-table",
            "class-name": "complex-table2 alarms",
            "field-datagrid-col-map": {
                "level": 2,
                "description": 1,
                "alarm-start": 3,
                "alarm-end": 4,
                "alarm-group": 5,
                "detail": 10
            },
            "select-mode": "SINGLE",
            "default-field-control-map": {
                "level": {
                    "ui-type": "toggle-display",
                    "config": {
                        "template": "default/alarm-history/toggle-display-alarm-levels",
                        "conditions": [
                            "%VALUE%==0",
                            "%VALUE%==1",
                            "%VALUE%==2"
                        ],
                        "class-name": "toggle-display icon-only no-background"
                    }
                },
                "description": {
                    "ui-type": "text2",
                    "config": {
                        "class-name": "text2 multiline",
                        "options": [],
                        "pattern": "${alarm_title_<%= VALUE %>}"
                    }
                },
                "alarm-start": {
                    "ui-type": "text2-date",
                    "config": {
                        "class-name": "text2 text2-date multiline",
                        "dateformat": "$DD.$MM.$YYYY, $HH:$mm:$ss"
                    }
                },
                "alarm-end": {
                    "ui-type": "text2-date",
                    "config": {
                        "class-name": "text2 text2-date multiline",
                        "dateformat": "$DD.$MM.$YYYY, $HH:$mm:$ss"
                    }
                },
                "alarm-group": {
                    "ui-type": "text2",
                    "config": {
                        "class-name": "text2",
                        "options": [],
                        "pattern": "${alarm_group_<%= VALUE %>}"
                    }
                },
                "detail": {
                    "ui-type": "local-script",
                    "config": {
                        "module": "visuals.tools.alarms.ls.alarmDetails"
                    }
                }
            },
            "default-field-headers": {
                "level": "${alarmlist_level_header}",
                "description": "${alarmlist_title_header}",
                "alarm-start": "${alarmlist_come_header}",
                "alarm-end": "${alarmlist_gone_header}",
                "alarm-group": "${alarmlist_group_header}",
                "detail": "${alarmlist_detail_header}"
            },
            "_comment": "expr is passed to expr parameter array of shmi.visuals.core.DataGridManager.setFilter",
            "filters": [{
                "label": "${alarmlist_level_info}",
                "template": "default/alarm-history/level-service",
                "class-name": "toggle-button icon-and-text",
                "field": "level",
                "expr": 0
            }, {
                "label": "${alarmlist_level_warning}",
                "template": "default/alarm-history/level-warning",
                "class-name": "toggle-button icon-and-text",
                "field": "level",
                "expr": 1
            }, {
                "label": "${alarmlist_level_alarm}",
                "template": "default/alarm-history/level-alarm",
                "class-name": "toggle-button icon-and-text",
                "field": "level",
                "expr": 2
            }],
            "default-layout": {
                "class-name": "layout-std",
                "_comment": "default == no additional css layout class",
                "column-org": {
                    "col1": {
                        "fields": ["level"],
                        "width": "5%"
                    },
                    "col2": {
                        "fields": ["description"],
                        "width": "20%"
                    },
                    "col3": {
                        "fields": ["alarm-start"],
                        "width": "20%"
                    },
                    "col4": {
                        "fields": ["alarm-end"],
                        "width": "20%"
                    },
                    "col5": {
                        "fields": ["alarm-group"],
                        "width": "20%"
                    },
                    "col6": {
                        "fields": ["detail"],
                        "width": "15%"
                    }
                },
                "line-height": "39px"
            },
            "sortable-fields": [
                "level",
                "description",
                "alarm-start",
                "alarm-end",
                "alarm-group"
            ],
            "delete-selected-rows": false,
            "show-nof-rows": true,
            "show-buttons-table-min-width-px": 400,
            "text-mode": "SINGLELINE",
            "responsive-layouts": [
                {
                    "class-name": "layout-compact",
                    "table-max-width-px": 700,
                    "v-scroll-options": ["V_SWIPE"],
                    "select-mode": "SINGLE",
                    "column-org": {
                        "col1": {
                            "fields": ["level"],
                            "width": "15%"
                        },
                        "col2": {
                            "fields": ["description"],
                            "width": "30%"
                        },
                        "col3": {
                            "fields": ["alarm-start", "alarm-end"],
                            "width": "25%"
                        },
                        "col4": {
                            "fields": ["detail"],
                            "width": "25%"
                        }
                    },
                    "line-height": "79px"
                }
            ],
            "default-nof-buffered-rows": 150,
            "buffer-size": 500
        };
    //declare private variables - END

    //declare private functions - START
    function getAnchorElements(self) {
        self.vars.anchors.from = shmi.getUiElement("from-anchor", self.element);
        self.vars.anchors.to = shmi.getUiElement("to-anchor", self.element);
        self.vars.anchors.table = shmi.getUiElement("table-anchor", self.element);
        self.vars.anchors.filterButton = shmi.getUiElement("filter-button", self.element);
        self.vars.anchors.filterOverlay = shmi.getUiElement("filter-overlay", self.element);
    }

    function createControls(self) {
        var anchors = self.vars.anchors,
            controls = self.vars.controls;

        controls.table = shmi.createControl("complex-table2", anchors.table, alarmTableConfig, "DIV");
        controls.from.date = shmi.createControl("select-date", anchors.from, { "label": "start" }, "DIV");
        controls.from.time = shmi.createControl("select-time", anchors.from, { "label": "start", "isUTC": true }, "DIV");
        controls.to.date = shmi.createControl("select-date", anchors.to, { "label": "end" }, "DIV");
        controls.to.time = shmi.createControl("select-time", anchors.to, { "label": "end", "isUTC": true }, "DIV");

        controls.from.time.setValue(0);
        controls.to.time.setValue(0);
    }

    function computeTimestamp(dateTimestamp, hours, minutes, seconds) {
        var date = new Date(dateTimestamp * 1000);
        date.setHours(hours, minutes, seconds, 0);

        return date.getTime() / 1000;
    }

    function computeTimestampFromControls(ctrlGroup) {
        return computeTimestamp(
            ctrlGroup.date.getValue(),
            ctrlGroup.time.getHours(),
            ctrlGroup.time.getMinutes(),
            ctrlGroup.time.getSeconds()
        );
    }

    function setFilter(self) {
        var from = computeTimestampFromControls(self.vars.controls.from),
            to = computeTimestampFromControls(self.vars.controls.to),
            dt = shmi.requires("visuals.tools.date");
        shmi.addClass(self.element, "filter-active");
        self.vars.activeFilterLabel.textContent = shmi.localize("${alarmlist_timefilter}");
        self.vars.activeFilterFrom.textContent = dt.formatDateTime(from, { datestring: shmi.localize(self.config.dateformat) });
        self.vars.activeFilterTo.textContent = dt.formatDateTime(to, { datestring: shmi.localize(self.config.dateformat) });
        self.vars.grid.setFilter(3, [from, to]);
    }

    function clearFilter(self) {
        self.vars.grid.clearFilter(3);
        shmi.removeClass(self.element, "filter-active");
    }

    function attachChangeListeners(self) {
        var toks = self.vars.tokens,
            from = self.vars.controls.from,
            to = self.vars.controls.to;

        toks.push(from.date.listen("change", function(evt) {
            log(uiType, "from date:", evt.detail.value);
            log(uiType, "total from:", new Date(computeTimestampFromControls(from) * 1000));
            to.date.setValue(from.date.getValue() + 86400);
            to.time.setValue(from.time.getValue());
        }));
        toks.push(from.time.listen("change", function(evt) {
            log(uiType, "from time:", evt.detail.value);
            log(uiType, "total from:", new Date(computeTimestampFromControls(from) * 1000));
        }));

        toks.push(to.date.listen("change", function(evt) {
            log(uiType, "to date:", evt.detail.value);
        }));
        toks.push(to.time.listen("change", function(evt) {
            log(uiType, "to time:", evt.detail.value);
        }));
    }

    //declare private functions - END

    //definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            grid: null,
            table: null,
            startDate: null,
            endDate: null,
            todayButton: null,
            resetButton: null,
            timeFilterLabel: null,
            activeFilterLabel: null,
            activeFilterFrom: null,
            activeFilterTo: null,
            anchors: {
                from: null,
                to: null,
                table: null,
                filterButton: null,
                filterOverlay: null
            },
            controls: {
                table: null,
                from: {
                    date: null,
                    time: null
                },
                to: {
                    date: null,
                    time: null
                }
            },
            tokens: [],
            listeners: []
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            /* example - add import via function call */
            qm: function() {
                return shmi.visuals.session.QueryManager;
            },
            dgm: "visuals.session.DataGridManager",
            iter: "visuals.tools.iterate.iterateObject"
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this,
                    grid = self.imports.dgm.getGrid(alarmGridConfig.name),
                    iter = self.imports.iter,
                    missingElements = false,
                    io = shmi.requires("visuals.io");

                if (!grid) {
                    grid = self.imports.dgm.grids[alarmGridConfig.name] = new shmi.visuals.core.DataGridDB(alarmGridConfig.name, shmi.c(alarmGridConfig.db), alarmGridConfig.table, alarmGridConfig.fields, alarmGridConfig.conditions);
                    log("grid created:", alarmGridConfig.name);
                } else {
                    console.debug(uiType, "grid exists:", alarmGridConfig.name);
                }
                self.vars.grid = grid;
                self.vars.resetButton = shmi.getUiElement("reset-button", self.element);
                var resetButtonHandler = {
                    onClick: function() {
                        clearFilter(self);
                    }
                };
                self.vars.timeFilterLabel = shmi.getUiElement("filter-label", self.element);
                self.vars.timeFilterLabel.textContent = shmi.localize("${alarmlist_set_timefilter}");
                self.vars.activeFilterLabel = shmi.getUiElement("time-frame-label", self.element);
                self.vars.activeFilterFrom = shmi.getUiElement("start-date", self.element);
                self.vars.activeFilterTo = shmi.getUiElement("end-date", self.element);

                var resetMl = new io.MouseListener(self.vars.resetButton, resetButtonHandler),
                    resetTl = new io.TouchListener(self.vars.resetButton, resetButtonHandler);
                self.vars.listeners.push(resetMl, resetTl);

                var dateformat = shmi.localize(self.config.dateformat);
                alarmTableConfig["default-field-control-map"]["alarm-start"].config.dateformat = dateformat;
                alarmTableConfig["default-field-control-map"]["alarm-end"].config.dateformat = dateformat;

                getAnchorElements(self);
                iter(self.vars.anchors, function(elem, name) {
                    if (!elem) {
                        console.error(className, "element not found:", name + "-anchor");
                        missingElements = true;
                    }
                });

                if (!missingElements) {
                    createControls(self);
                    var filterHandler = {
                            onClick: function() {
                                var filterClass = "filter-open";
                                if (!self.locked) {
                                    if (!shmi.hasClass(self.element, filterClass)) {
                                        shmi.addClass(self.element, filterClass);
                                        if (!self.vars.firstOpened) {
                                            self.vars.controls.to.date.setValue(self.vars.controls.from.date.getValue() + 86400);
                                            self.vars.controls.to.time.setValue(self.vars.controls.from.time.getValue());
                                            self.vars.firstOpened = true;
                                        }
                                    } else {
                                        shmi.removeClass(self.element, filterClass);
                                        setFilter(self);
                                    }
                                }
                            }
                        },
                        ml = new io.MouseListener(self.vars.anchors.filterButton, filterHandler),
                        tl = new io.TouchListener(self.vars.anchors.filterButton, filterHandler);
                    self.vars.listeners.push(ml, tl);

                    var overlayHandler = {
                            onClick: function() {
                                var filterClass = "filter-open";
                                shmi.removeClass(self.element, filterClass);
                                setFilter(self);
                            }
                        },
                        mlo = new io.MouseListener(self.vars.anchors.filterOverlay, overlayHandler),
                        tlo = new io.TouchListener(self.vars.anchors.filterOverlay, overlayHandler);
                    self.vars.listeners.push(mlo, tlo);
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                var self = this,
                    from = self.vars.controls.from,
                    nowTime = Math.floor((new Date(shmi.getServerTime() * 1000)).getTime() / 1000),
                    nowTimeDate = Math.floor((nowTime) / 86400) * 86400;

                self.controls.forEach(function(c) {
                    c.enable();
                });

                from.date.setValue(nowTimeDate - 86400);
                from.time.setValue(nowTime);

                attachChangeListeners(self);
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                var self = this;
                self.controls.forEach(function(c) {
                    c.disable();
                });
                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.firstOpened = false;
                self.vars.tokens = [];
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

(function() {
    /**
     * module to display alarm-detail messages in alarm-table.
     *
     */

    var MODULE_NAME = "visuals.tools.alarms.ls.alarmDetails",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME);

    // MODULE CODE - START

    /* private variables */

    /* private functions */

    /**
     * Implements local-script run function.
     *
     * This function will be called each time a local-script will be enabled.
     *
     * @author Felix Walter <walter@smart-hmi.de>
     * @param {LocalScript} self instance reference of local-script control
     */
    module.run = function(self) {
        var tokens = [],
            detailsButton = null,
            im = shmi.requires("visuals.session.ItemManager"),
            nv = shmi.requires("visuals.tools.numericValues"),
            uc = shmi.visuals.tools.unitClasses,
            itemHandler = im.getItemHandler(),
            alarmInfo = null;
        self.vars = self.vars || {};
        detailsButton = shmi.createControl("button", self.element.parentNode, { label: "${alarmlist_show_detail}" }, "DIV");
        itemHandler.setValue = function(value) {
            alarmInfo = JSON.parse(value);
            if (alarmInfo && Array.isArray(alarmInfo.items)) {
                alarmInfo.items.forEach(function(info, idx) {
                    info.formattedValue = nv.formatNumber(value, {
                        unit: info.unit,
                        precision: info.digits
                    });

                    if (typeof info.unit === "number") {
                        var adapter = uc.getSelectedAdapter(info.unit);
                        if (adapter) {
                            info.unit = adapter.unitText;
                            info.value = adapter.outFunction(info.value);
                        }
                    }

                    if (typeof info.digits === "number" && info.digits >= 0) {
                        info.value = info.value.toFixed(info.digits);
                    }
                });
            }
        };
        tokens.push(detailsButton.listen("click", function() {
            var localeVar = shmi.evalString("${alarm_msg_<%= VALUE %>}", { VALUE: alarmInfo.index }),
                translated = shmi.localize(localeVar);
            shmi.notify(shmi.evalString(translated, alarmInfo));
        }));
        if (self.config.item) {
            tokens.push(im.subscribeItem(self.config.item, itemHandler));
        }
        /* called when this local-script is disabled */
        self.onDisable = function() {
            self.run = false; // from original .onDisable function of LocalScript control
            tokens.forEach(function(t) {
                t.unlisten();
            });
            tokens = [];
            shmi.deleteControl(detailsButton, true);
        };
    };
    // MODULE CODE - END

    fLog("module loaded");
})();

/**
 * Control "alarm-info""
 *
 * Configuration options (default):
 *
 * {
        "class-name": uiType,
        "name": null,
        "template": "default/alarm-info",
        "label": uiType,
        "noAlarm": null,
        "action": null,
        "lastAlarmsNum": null,
        "lastAlarmMsg": null,
        "lastAlarmClass": null,
        "userGroups": "",
        "enableCycle": true,
        "cycleInterval": 1000,
        "showAlarm": true,
        "showWarn": true,
        "showInfo": true
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 */
(function() {
    'use strict';

    //variables for reference in control definition
    const className = "AlarmInfo", //control name in camel-case
        uiType = "alarm-info", //control keyword (data-ui)
        isContainer = false;

    //example - default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/alarm-info",
        "label": uiType,
        "noAlarm": null,
        "action": null,
        "userGroups": null,
        "enableCycle": true,
        "cycleInterval": 1000,
        "showAlarm": true,
        "showWarn": true,
        "showInfo": true,
        "groupFilter": null
    };

    //setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        log = logger.log;

    // Constants and private fields
    const ALARM_STATE_NAMES = ["service", "preWarn", "warn"];

    //declare private functions - START

    /**
     * hide - hide element (apply CSS class "hidden")
     *
     * @param {HTMLElement} el element to hide
     */
    function hide(el) {
        if (el) {
            shmi.addClass(el, "hidden");
        }
    }

    /**
     * show - show element (remove CSS class "hidden")
     *
     * @param {HTMLElement} el element to hide
     */
    function show(el) {
        if (el) {
            shmi.removeClass(el, "hidden");
        }
    }

    /**
     * parseAlarmGroupRange - parse range of alarm groups
     *
     * @param {string} rangeText input text (e.g. `3-7`)
     * @returns {number[2]|null} range start & end tuple or `null` if none could be parsed
     */
    function parseAlarmGroupRange(rangeText) {
        const tuple = rangeText.split("-");

        if (tuple.length === 2) {
            const start = parseInt(tuple[0]),
                end = parseInt(tuple[1]);

            if (!(isNaN(start) || isNaN(end)) && start > 0 && end > 1 && end > start) {
                return [start, end];
            }
        }

        return null;
    }

    /**
     * parseAlarmGroupFilter - parse group ID filter from config string
     *
     * @param {string} groupFilter comma separated group ID filter
     * @returns {number[]} array of group IDs
     */
    function parseAlarmGroupFilter(groupFilter) {
        if (typeof groupFilter !== "string") {
            return null;
        }

        let entries = groupFilter.split(",");
        entries = entries.map((entry) => {
            if (entry.includes("-")) {
                return parseAlarmGroupRange(entry);
            }

            const groupId = parseInt(entry);
            if (!isNaN(groupId) && groupId >= 0) {
                return groupId;
            }

            return null;
        });
        entries = entries.filter((entry) => entry !== null);
        const rangeEntries = [];
        entries.forEach((entry) => {
            if (Array.isArray(entry)) {
                let idx = entry[0];
                while (idx <= entry[1]) {
                    if (!(rangeEntries.includes(idx) && entries.includes(idx))) {
                        rangeEntries.push(idx);
                    }
                    idx += 1;
                }
            }
        });
        entries.push(...rangeEntries);

        return entries.length ? entries : null;
    }

    /**
     * testAlarmState - test & update state of displayed alarm
     *
     * @param {object} self instance reference
     */
    function testAlarmState(self) {
        const alarmArray = Object.values(self.imports.am.alarms).map((value) => value.properties);
        if (Array.isArray(alarmArray)) {
            if (alarmArray.length < 1) {
                clearAlarmMsg(self);
            } else {
                clearInt(self);
                if (!self.config.enableCycle) {
                    // standard mode: lookup for last Alarm of highest class
                    self.vars.activeAlarmList = getLastHighest(self, alarmArray);
                } else {
                    // cycleMode: cycle display of all active alarms
                    self.vars.activeAlarmList = getEnabledAlarms(self, alarmArray);
                    if (self.vars.activeAlarmList.length > 1) {
                        self.vars.interval = setInterval(displayAlarms.bind(null, self), self.config.cycleInterval);
                    }
                }
                displayAlarms(self);
            }
        }
    }

    // helper: displays alarms in activeAlarmList
    function displayAlarms(self) {
        if (self.vars.activeAlarmList.length === 0) {
            clearAlarmMsg(self);
        }
        if (self.vars.currentAlarm >= self.vars.activeAlarmList.length) {
            self.vars.currentAlarm = 0;
        }
        clearAlarmState(self);
        const alarm = self.vars.activeAlarmList[self.vars.currentAlarm++];
        let msg = null;
        if (alarm) { // evaluate context items etc.
            msg = shmi.evalString("${alarm_title_<%= value %>}", { value: alarm.index });
            msg = shmi.localize(msg);
            msg = shmi.evalString(msg, alarm);
            if (!isNaN(alarm.severity)) {
                setAlarmState(alarm.severity, self);
            }
        } else {
            msg = shmi.localize(self.config.noAlarm);
        }
        self.vars.lastAlarmMsg.textContent = msg;

        if (self.vars.lastAlarmsNum) {
            self.vars.lastAlarmsNum.textContent = self.vars.lastAlarmsNumValue;
        }
    }

    // helper: get alarm object of the last Alarm and highest class (=severity)
    function getLastHighest(self, alarmList) {
        let lastAlarm = null;
        getEnabledAlarms(self, alarmList).forEach(function(alarmInfo) {
            // Find latest alarm with the highest occurring severity
            if (lastAlarm === null) {
                lastAlarm = alarmInfo;
            } else if (alarmInfo.severity >= lastAlarm.severity && alarmInfo.timestamp_in > lastAlarm.timestamp_in) {
                lastAlarm = alarmInfo;
            }
        });

        return lastAlarm === null ? [] : [lastAlarm];
    }
    // helper: select alarms to be displayed dependig on the config.notShowWarn and config.notShowInfo parameters
    function getEnabledAlarms(self, alarmList) {
        const enabledAlarms = alarmList.filter(function(alarmInfo) {
            if (Array.isArray(self.vars.groupFilter)) {
                if (!self.vars.groupFilter.includes(alarmInfo.group)) {
                    return false;
                }
            }

            //filter inactive alarms
            if (!alarmInfo.active) {
                if (!alarmInfo.acknowledgeable) {
                    return false;
                } else if (alarmInfo.acknowledged) {
                    return false;
                }
            }

            // Filter alarms based on control configuration
            return (alarmInfo.severity === 2 && self.config.showAlarm) ||
                (alarmInfo.severity === 1 && self.config.showWarn) ||
                (alarmInfo.severity === 0 && self.config.showInfo);
        });
        self.vars.lastAlarmsNumValue = enabledAlarms.length;
        return enabledAlarms;
    }
    // helper: clears the alarm message
    function clearAlarmMsg(self) {
        clearInt(self);
        self.vars.currentAlarm = 0;
        clearAlarmState(self);
        self.vars.lastAlarmMsg.textContent = shmi.localize(self.config.noAlarm);
    }
    // helper: clears the interval
    function clearInt(self) {
        if (self.vars.interval) {
            clearInterval(self.vars.interval);
            self.vars.interval = null;
        }
    }

    // helper: clears the alarm class (state)
    function clearAlarmState(self) {
        self.vars.stateIcons.forEach(hide);
        ALARM_STATE_NAMES.forEach(function(lvl) {
            shmi.removeClass(self.element, lvl);
        });
    }
    // helper: sets alarm class (state)
    function setAlarmState(state, self) {
        if (state in self.vars.stateIcons) {
            show(self.vars.stateIcons[state]);
        }
        shmi.addClass(self.element, ALARM_STATE_NAMES[state]);
    }
    //declare private functions - END

    //definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            hidden: false,
            clickElement: null,
            lastAlarmsNum: null,
            lastAlarmsNumValue: 0,
            lastAlarmMsg: null,
            lastAlarmMsgValue: 0,
            lastAlarmClass: null,
            lastAlarmClassValue: 0,
            listeners: [],
            tokens: [],
            stateIcons: [],
            activeAlarmList: [],
            currentAlarm: 0,
            interval: 0,
            alarmSubscriber: null,
            groupFilter: null
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            /* example - add import via function call */
            am: "visuals.session.AlarmManager"
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    io = shmi.requires("visuals.io");

                let ml = null, // MouseListener
                    tl = null, // TouchListener
                    handler = null,
                    userGroups = null,
                    um = null,
                    user = null;

                if (typeof self.config.userGroups === "string" && self.config.userGroups.length > 0) {
                    userGroups = self.config.userGroups.split(",");
                    um = shmi.requires("visuals.session.UserManager");
                    user = um.currentUser;
                    if (user.groupList.filter(function(n) {
                        return userGroups.indexOf(n) !== -1;
                    }).length < 1) {
                        // disable and hide
                        hide(shmi.getUiElement('alarm-info-wrapper', self.element));
                        self.vars.hidden = true;
                        return;
                    }
                }

                self.vars.groupFilter = parseAlarmGroupFilter(self.config.groupFilter);

                self.vars.lastAlarmsNum = shmi.getUiElement('last-alarms-num', self.element);
                if (!self.vars.lastAlarmsNum) {
                    log(uiType, "last-alarms-num not found in template");
                }
                self.vars.lastAlarmMsg = shmi.getUiElement('last-alarm-msg', self.element);
                if (!self.vars.lastAlarmMsg) {
                    log(uiType, "last-alarm-msg not found in template");
                } else {
                    self.vars.lastAlarmMsg.textContent = shmi.localize(self.config.noAlarm);
                }
                self.vars.clickElement = shmi.getUiElement('alert-list-button', self.element);
                if (self.vars.clickElement && Array.isArray(self.config.action)) {
                    handler = {
                        onClick: function(x, y, e) {
                            const core = shmi.requires("visuals.core"),
                                action = new core.UiAction(self.config.action);
                            action.execute();
                        }
                    };
                    ml = new io.MouseListener(self.vars.clickElement, handler);
                    tl = new io.TouchListener(self.vars.clickElement, handler);
                    self.vars.listeners.push(ml, tl);
                } else if (!self.vars.clickElement) {
                    log(uiType, "click element not found in template");
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this;
                if (self.vars.hidden) {
                    log("Alarm Info is disabled");
                    return;
                }
                self.vars.stateIcons = shmi.getUiElements('state', self.element) || [];
                self.vars.alarmSubscriber = self.imports.am.subscribeAlarms(self, function(alarm, isLast) {
                    if (isLast) {
                        testAlarmState(self);
                    }
                });
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.imports.am.unsubscribeAlarms(self.vars.alarmSubscriber);
                clearInt(self);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();

/**
 * Alpha Num Keyboard
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * label {string}: label for the overlay container
 * key-preview {boolean}, shows button name preview on touch pressed only
 * auto-key-focus {boolean}: false, // sets tabindex="0" on each key
 * show-enter {boolean]: false, // show the enter button, also disable enter to apply input
 * password-input {boolean}: false, display input as password --> input via real keyboard disabled
 * select-box-enabled {boolean}: true, show keyboard select box
 * value {}: null, // existing value of e.g. input-field
 * callback {function}: null, used to pass value to other origin-control
 *
 * Additional information about the configuration is located in the tutorial: keyboard-layout-configuration
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "AlphaNumKeyBoard", // control name in camel-case
        uiType = "alpha-num-keyboard", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    var defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/alpha-num-keyboard",
        "label": "",
        "key-preview": true, // shown on touch only
        "show-enter": false,
        "auto-key-focus": false, // set tabindex="0" to key
        "password-input": false,
        "select-box-enabled": true,
        "keyboards": {}
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            listeners: [],
            controls: [],
            tokens: [],
            keys: [],
            keyboardMap: {},
            activeModifiers: {},
            activeKeyboard: null,
            activePalette: null,
            activeKeys: null,
            currentText: "",
            keyTypeListeners: {},
            previewContainer: null,
            shiftKeyActive: false,
            showPassword: false
        },
        /* imports added at runtime */
        imports: {
        },
        events: [],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                // listeners add by shmi mouse and touch listeners
                self.vars.keyListeners = {
                    onPress: onKeyPressListener.bind(null, self),
                    onClick: onKeyClickListener.bind(null, self),
                    onRelease: onKeyReleaseListener.bind(null, self)
                };

                // custom listeners added with browser method 'addEventListener'
                self.vars.eventListeners = {
                    mouseOver: onKeyMouseInListener.bind(null, self),
                    mouseOut: onKeyMouseOutListener.bind(null, self),
                    mouseUp: onKeyMouseUpListener.bind(null, self),
                    mouseDown: onKeyMouseDownListener.bind(null, self),
                    touchStart: onKeyTouchStartListener.bind(null, self),
                    touchEnd: onKeyTouchEndListener.bind(null, self),
                    touchMove: onKeyTouchMoveListener.bind(null, self),
                    keyDown: onKeyDownListener.bind(null, self),
                    keyUp: onKeyUpListener.bind(null, self)
                };

                // html element to insert keyboard
                self.vars.keyboardContainer = shmi.getUiElement('keyboard-container', self.element);

                // get configurations
                var keyboardConfig = shmi.requires("shmi.visuals.session.keyboards"),
                    localeInfo = shmi.requires("visuals.session.localeInfo"),
                    um = shmi.requires("visuals.session.UserManager");

                if (!keyboardConfig) {
                    return;
                }
                self.config.keyboards = keyboardConfig;

                // get and set current locale
                if (um.currentUser && um.currentUser.loggedIn && keyboardConfig && localeInfo.locales[um.currentUser.locale] &&
                    localeInfo.locales[um.currentUser.locale].keyboard && keyboardConfig[localeInfo.locales[um.currentUser.locale].keyboard]) {
                    self.vars.currentLocale = localeInfo.locales[um.currentUser.locale].keyboard;
                } else if (localeInfo.locales[localeInfo.default] && localeInfo.locales[localeInfo.default].keyboard &&
                    keyboardConfig[localeInfo.locales[localeInfo.default].keyboard]) {
                    self.vars.currentLocale = localeInfo.locales[localeInfo.default].keyboard;
                } // if currentLocale coun't be set the first keyboard configuration is used

                // init keyboard
                var languages = Object.keys(keyboardConfig); // keys/names of all configured keyboard languages

                // initialize seperate keyboard control like keyboard language selection
                initKeyboardControls(self);

                languages.forEach(function(langName) {
                    // create keyboards for each language registered
                    var keyboard = createKeyboard(self, langName, keyboardConfig[langName]);
                    self.vars.keyboardContainer.appendChild(keyboard.element);
                    // save the keyboard in a map with name as key (keyboard is not a control!)
                    self.vars.keyboardMap[langName] = keyboard;
                });

                // has been temporary preview container, can be used as final preview container if needed
                self.vars.previewContainer = shmi.getUiElement('text-preview', self.element);
                self.vars.currentText = self.config.value || self.vars.currentText;
                updatePreviewTextContent(self);
                self.vars.previewContainer.focus();

                var activePaletteElement = null;
                // set initially active keyboard and palette name
                if (self.vars.currentLocale) {
                    self.vars.activeKeyboard = self.vars.keyboardMap[self.vars.currentLocale];
                } else {
                    self.vars.activeKeyboard = self.vars.keyboardMap[Object.keys(self.vars.keyboardMap)[0]];
                }
                activePaletteElement = Object.keys(self.vars.activeKeyboard.paletteMap)[0];

                // set initial activePalette and activeKeyMap to be referenced later
                self.vars.activePalette = self.vars.activeKeyboard.paletteMap[activePaletteElement];
                self.vars.activeKeyMap = self.vars.activePalette.keyMap;

                // initially show the first keyboard while all others are hidden by default with display: none!
                self.vars.activeKeyboard.element.style.display = 'block';

                if (self.config['key-preview']) {
                    // add css class "key-preview-enabled" to enable the popup of the key name preview
                    shmi.addClass(self.element, 'key-preview-enabled');
                }

                if (self.config['password-input']) {
                    shmi.addClass(self.element, 'password-input');
                }

                if (!self.config['show-enter']) {
                    shmi.addClass(self.element, 'hide-enter');
                }

                // set/enable listeners for each key in active key map
                enableActiveKeyMapListeners(self);
            },
            /**
             * Enables the Alpha Num Keyboard
             */
            onEnable: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                self.vars.controls.forEach(function(c) {
                    c.enable();
                });

                document.addEventListener('keydown', self.vars.eventListeners.keyDown);
                document.addEventListener('keyup', self.vars.eventListeners.keyUp);
                shmi.log("[Alpha Num Keyboard] enabled", 1);
            },
            /**
             * Disables the Alpha Num Keyboard
             *
             */
            onDisable: function() {
                var self = this;
                self.hide();

                document.removeEventListener('keydown', self.vars.eventListeners.keyDown);
                document.removeEventListener('keyup', self.vars.eventListeners.keyUp);

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.vars.controls.forEach(function(c) {
                    c.disable();
                });
                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                shmi.log("[Alpha Num Keyboard] disabled", 1);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.vars.controls.forEach(function(c) {
                    c.lock();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                self.vars.controls.forEach(function(c) {
                    c.unlock();
                });
            },
            /**
             * Displays the Alpha Num Keyboard
             */
            show: function() {
                shmi.removeClass(this.element, 'closed');
                shmi.addClass(this.element, 'open');
            },
            /**
             * Hides the Alpha Num Keyboard from DOM
             */
            hide: function() {
                shmi.removeClass(this.element, 'open');
                shmi.addClass(this.element, 'closed');
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(val, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    // #        Building the Keyboard       # //

    /**
     * initiate creation of keyboard
     *
     * @param {object} self
     * @param {string} keyboardName name of keyboard - de,us,...
     * @param {object} keyboardConfig configuration of keyboard
     * @returns {{paletteNames: string[], name: *, element: HTMLElement}} Object that contains the palette names, the keyboard name and element
     */
    function createKeyboard(self, keyboardName, keyboardConfig) {
        var paletteNames = Object.keys(keyboardConfig),
            keyboardElement = document.createElement('DIV'),
            keyboardPaletteMap = {};

        shmi.addClass(keyboardElement, 'keyboard');
        shmi.addClass(keyboardElement, 'keyboard-' + keyboardName.toLowerCase());
        keyboardElement.setAttribute('data-name', 'keyboard-' + keyboardName);

        paletteNames.forEach(function(paletteName, idx) {
            var paletteConfig = keyboardConfig[paletteName],
                palette = createKeyboardPalette(self, paletteName, paletteConfig),
                id = getUnusedId(keyboardPaletteMap);
            palette.id = id;
            palette.element.setAttribute('data-key-id', id);

            // sets display on first palette of the keyboard to flex and others to none to show the first one initially
            palette.element.style.display = (idx === 0 ? 'flex' : 'none');
            keyboardElement.appendChild(palette.element);
            keyboardPaletteMap[id] = palette;
        });

        return {
            paletteMap: keyboardPaletteMap,
            name: keyboardName,
            element: keyboardElement
        };
    }

    /**
     * creates a keyboard palette, wich represents one face
     * (e.g. letter, numbers, special characters, ...)
     *
     * @param {object} self
     * @param {string} paletteName name of palette
     * @param {object} paletteRows configuration of key-rows from palette
     * @returns {object} palette with name, keymap and element
     */
    function createKeyboardPalette(self, paletteName, paletteRows) {
        var paletteElement = document.createElement('DIV'),
            createdPaletteRows = [],
            keyboardPaletteKeyMap = {};

        shmi.addClass(paletteElement, 'keyboard-palette');
        shmi.addClass(paletteElement, 'keyboard-palette-' + paletteName.toLowerCase());
        paletteElement.setAttribute('data-name', 'palette-' + paletteName);

        // creating rows of keys for the palette
        paletteRows.forEach(function(rowConfig) {
            var paletteRow = createKeyboardRow(self, rowConfig);
            paletteElement.appendChild(paletteRow.element);
            createdPaletteRows.push(paletteRow);
            // creating keys for each row
            paletteRow.keys.forEach(function(key) {
                var id = getUnusedId(keyboardPaletteKeyMap);
                key["data-key-id"] = id;
                key.element.setAttribute('data-key-id', id);
                keyboardPaletteKeyMap[key["data-key-id"]] = key;
            });
        });

        return {
            name: paletteName,
            element: paletteElement,
            keyMap: keyboardPaletteKeyMap
        };
    }

    /**
     * create a keyboard row, which contains an variable amount of keys
     *
     * @param {object} self
     * @param {object} rowConfig configuration of the keys contained in this row
     * @returns {object} row with element and created keys
     */
    function createKeyboardRow(self, rowConfig) {
        var keyRow = document.createElement('DIV'),
            rowKeys = [];
        shmi.addClass(keyRow, 'key-row');
        rowConfig.forEach(function(keyConfig, idx) {
            var key = createKeyboardKey(self, keyConfig.name, keyConfig, (idx < rowConfig.length / 2));
            keyRow.appendChild(key.element);
            rowKeys.push(key);
            if (key.alternatives) {
                key.alternatives.forEach(function(alt) {
                    rowKeys.push(alt);
                });
            }
        });

        return {
            keys: rowKeys,
            element: keyRow
        };
    }

    /**
     * create a keyboard key
     *
     * @param {object} self
     * @param {string} keyName  id to identify key later
     * @param {object} keyConfig configuration of key
     * @returns {object} key with name, id, configuration and element
     */
    function createKeyboardKey(self, keyName, keyConfig, leftSide) {
        var keyBox = document.createElement('DIV'),
            key = document.createElement('DIV'),
            keyPrev = null,
            keyPrevContent = null,
            alternativeKeys = null;
        if (self.config['key-preview']) {
            keyPrev = document.createElement('DIV');
            keyPrevContent = document.createElement('SPAN');
        }

        // register all used key types to the listeners array
        self.vars.keyTypeListeners[keyConfig.type] = self.vars.keyTypeListeners[keyConfig.type] || {};

        var escapedKeyName = String(keyConfig.name || keyName).replace(/"/g, '\\"');
        shmi.addClass(keyBox, 'key-box');
        shmi.addClass(key, 'key');
        shmi.addClass(key, 'key-' + escapedKeyName);
        key.setAttribute('data-key-name', escapedKeyName);

        if (self.config["auto-key-focus"]) {
            key.setAttribute('tabindex', 0);
        }
        if (keyConfig['class-name']) {
            shmi.addClass(key, keyConfig['class-name']);
        }
        if (Number(keyConfig.size) !== 0) {
            shmi.addClass(keyBox, 'key-box-flex-' + keyConfig.size);
        }
        shmi.addClass(key, 'key-' + keyConfig.type);
        key.setAttribute('data-key-type', keyConfig.type);
        key.innerText = keyConfig.name || '';

        keyBox.appendChild(key);

        // create preview container
        if (keyPrev) {
            shmi.addClass(keyPrev, 'key-preview');
            keyPrevContent.innerText = keyConfig.name || '';
            keyBox.appendChild(keyPrev);
            keyPrev.appendChild(keyPrevContent);
        }

        // create alternative-key container and keys
        if (keyConfig.alternatives) {
            alternativeKeys = document.createElement('DIV');
            shmi.addClass(alternativeKeys, 'alt-keys');
            if (leftSide) {
                shmi.addClass(alternativeKeys, 'left-side');
            } else {
                shmi.addClass(alternativeKeys, 'right-side');
            }
            keyConfig.alternatives.forEach(function(config) {
                var altKey = createKeyboardKey(self, config.name, config);
                alternativeKeys.appendChild(altKey.element);
            });
            shmi.addClass(alternativeKeys, 'numofKeys-' + keyConfig.alternatives.length);
            keyBox.appendChild(alternativeKeys);
        }
        keyConfig.element = keyBox;
        return keyConfig;
    }

    /**
     * update key config when modifier (shift) is pressed
     *
     * @param {object} self
     * @param {object} keyConfig configuration of key
     * @param {object} values modifier values
     */
    function setKeyConfig(self, keyConfig, values) {
        // set html key value
        var newName = values.name || keyConfig.name;
        if (newName) {
            keyConfig.element.firstChild.textContent = newName;
            if (self.config['key-preview']) {
                var prevElem = keyConfig.element.getElementsByTagName('span')[0];
                if (prevElem) {
                    prevElem.innerText = newName;
                }
            }
        }

        // set class for key element
        var newClassName = values['class-name'];
        if (newClassName) {
            if (newClassName.charAt(0) === '!') {
                shmi.removeClass(keyConfig.element, newClassName.substring(1));
            } else {
                shmi.addClass(keyConfig.element, newClassName);
            }
        }

        // set html key size by class name
        var css = keyConfig.element.className,
            size = values.size || keyConfig.size,
            sizeClassName = size ? 'key-box-flex-' + size : null;
        if (keyConfig.element.className.indexOf(sizeClassName) === -1 && sizeClassName) {
            keyConfig.element.className = (" " + css + " ").replace(/\bkey-box-flex-\S*\b/g, ' ');
            shmi.addClass(keyConfig.element, sizeClassName);
        }

        // set html key type
        var type = values.type || keyConfig.type;
        keyConfig.element.firstChild.setAttribute('data-key-type', type);
    }

    /**
     * enable listener of currently displayed keys
     *
     * @param {object} self
     */
    function enableActiveKeyMapListeners(self) {
        Object.keys(self.vars.activeKeyMap).forEach(function(key) {
            var ml = new shmi.visuals.io.MouseListener(self.vars.activeKeyMap[key].element, self.vars.keyListeners),
                tl = new shmi.visuals.io.TouchListener(self.vars.activeKeyMap[key].element, self.vars.keyListeners),
                keyElem = self.vars.activeKeyMap[key].element.firstChild;
            ml.enable();
            tl.enable();
            self.vars.listeners.push(ml, tl);

            // mouse
            keyElem.addEventListener('mouseover', self.vars.eventListeners.mouseOver);
            keyElem.addEventListener('mouseout', self.vars.eventListeners.mouseOut);
            keyElem.addEventListener('mousedown', self.vars.eventListeners.mouseDown);
            keyElem.addEventListener('mouseup', self.vars.eventListeners.mouseUp);
        });

        // touch
        self.vars.activePalette.element.addEventListener('touchstart', self.vars.eventListeners.touchStart);
    }

    /**
     * disable listener of currently displayed keys
     *
     * @param {object} self
     */
    function disableActiveKeyMapListeners(self) {
        self.vars.listeners.forEach(function(listener) {
            listener.disable();
        });
        self.vars.listeners = [];

        Object.keys(self.vars.activeKeyMap).forEach(function(key) {
            var keyElem = self.vars.activeKeyMap[key].element.firstChild;
            keyElem.removeEventListener('mouseover', self.vars.eventListeners.mouseOver);
            keyElem.removeEventListener('mousedown', self.vars.eventListeners.mouseDown);
            keyElem.removeEventListener('mouseout', self.vars.eventListeners.mouseOut);
            keyElem.removeEventListener('mouseup', self.vars.eventListeners.mouseUp);

            self.vars.activePalette.element.removeEventListener('touchstart', self.vars.eventListeners.touchStart);
        });
    }

    /**
     * init Keyboard Controls
     *  - selectBox for keyboard selection
     *  - submit button
     *  - close/abort button
     *
     * @param {object} self
     */
    function initKeyboardControls(self) {
        if (self.config["select-box-enabled"] && Object.keys(self.config.keyboards).length > 1) {
            // selectbox if multiple keyboards are available
            // language selection
            var localeInfo = shmi.requires("visuals.session.localeInfo"),
                languageCtrl = shmi.getUiElement('language-control', self.element),
                selectBoxConfig = {
                    "label": "",
                    "options": []
                },
                languages = Object.keys(self.config.keyboards);

            if (languages && languages.length > 0) {
                languages.forEach(function(langName, idx) {
                    selectBoxConfig.options.push({
                        "label": localeInfo.keyboards[langName].label,
                        "value": langName
                    });
                });
                var selectBox = shmi.createControl("select-box", languageCtrl, selectBoxConfig, "DIV");
                shmi.waitOnInit(selectBox, function() {
                    // default selection
                    if (self.vars.currentLocale) {
                        selectBox.setValue(self.vars.currentLocale);
                    } else {
                        self.config['default-keyboard'] = self.config['default-keyboard'] || languages[0];
                        selectBox.setValue(self.config['default-keyboard']);
                    }

                    self.vars.tokens.push(selectBox.listen("change", function onChange(evt) {
                        // disable and hide current keyboard
                        disableActiveKeyMapListeners(self);
                        self.vars.activePalette.element.style.display = 'none';
                        self.vars.activeKeyboard.element.style.display = 'none';

                        // reset active keyboard and palette
                        self.vars.activeKeyboard = self.vars.keyboardMap[evt.detail.value];
                        var activePaletteElement = null;
                        activePaletteElement = Object.keys(self.vars.activeKeyboard.paletteMap)[0];
                        self.vars.activePalette = self.vars.activeKeyboard.paletteMap[activePaletteElement];
                        self.vars.activeKeyMap = self.vars.activePalette.keyMap;

                        // make keyboard visible
                        self.vars.activeKeyboard.element.style.display = 'block';
                        self.vars.activePalette.element.style.display = 'flex';

                        enableActiveKeyMapListeners(self);
                    }));
                });
                shmi.addClass(languageCtrl, 'enabled');
                self.vars.controls.push(selectBox);
            }
        }

        // close button
        var closeButtonContainer = shmi.getUiElement('close-button', self.element),
            closeButtonConfig = {
                "class-name": "button icon-only",
                "label": "Cancel",
                "template": "default/button_with_bg_pic"
            },
            closeButton = shmi.createControl("button", closeButtonContainer, closeButtonConfig, "DIV");

        self.vars.tokens.push(
            closeButton.listen("click", function onclick(evt) {
                log("Fire: Close Keyboard");
                shmi.fire('keyboard-cancel', {
                    "success": false,
                    "input": self.vars.currentText
                });
            })
        );
        self.vars.controls.push(closeButton);

        // submit button
        var submitButtonContainer = shmi.getUiElement('submit-button', self.element),
            submitButtonConfig = {
                "class-name": "button icon-only",
                "template": "default/button_with_bg_pic"
            },
            submitButton = shmi.createControl("button", submitButtonContainer, submitButtonConfig, "DIV");

        self.vars.tokens.push(
            submitButton.listen("click", function onclick(evt) {
                log("Fire: Submit Keyboard");
                shmi.fire('keyboard-submit', {
                    "success": true,
                    "input": self.vars.currentText
                });
            })
        );
        self.vars.controls.push(submitButton);

        // pw button
        if (self.config["password-input"]) {
            const pwButtonContainer = shmi.getUiElement('pw-button', self.element),
                pwButtonConfig = {
                    "class-name": "button icon-only",
                    "template": "default/button_with_bg_pic"
                },
                pwButton = shmi.createControl("button", pwButtonContainer, pwButtonConfig, "DIV");

            self.vars.tokens.push(
                pwButton.listen("click", function onclick(evt) {
                    self.vars.showPassword = !self.vars.showPassword;
                    updatePreviewTextContent(self);
                })
            );
            self.vars.controls.push(pwButton);
        }
    }

    var idAlphabet = [
        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
        "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
    ];

    /**
     * getRandomId - generates random ID string of variable length
     *
     * @param  {number} length length of generated ID string
     * @return {string}        random ID
     */
    function getRandomId(length) {
        var rid = "";
        if (length === undefined) {
            length = 3;
        }
        for (var i = 0; i < length; i++) {
            rid += idAlphabet[Math.floor(Math.random() * (idAlphabet.length - 1))];
        }
        return rid;
    }

    /**
     * Get a unused random id
     *
     * @param obj unused in this context
     * @return    unused id
     */
    function getUnusedId(obj) {
        var id = null;
        do {
            id = getRandomId(8);
        } while (shmi.objectHasOwnProperty(obj, id));
        return id;
    }

    // #        Keyboard handler        # //

    /**
     * fireKeyboardClick - fires action of a pressed/clicked keyboard key
     *
     * @param {object} self
     * @param {object} key key where the event was triggert
     * @param {object} event event from mouse, key or touch event
     */
    function fireKeyboardClick(self, key, event) {
        // execute action depending on the type of the pressed/clicked key
        switch (key.type) {
        case 'letter':
        case 'number':
        case 'character':
            // simply add the value of the pressed key to the preview text content
            //addLetterToCurrentText(self, key);
            addLetterIntoCurrentText(self, key);
            break;
        case 'palette':
            // a key of type palette has to have a palette attribute with the name of the palette to be activated
            if (key.palette) {
                setKeyboardPalette(self, key);
            } else {
                console.error('[alpha-num-keyboard] Missing palette attribute value on key of type palette');
            }
            break;
        case 'modifier':
            // a key of type modifier has to have a property value of modify
            var modConfig = key.modify ? key.modify.split(':') : null,
                type, mode, name;
            // modify value has to contain type:mode:name
            if (modConfig && modConfig.length === 3) {
                type = modConfig[0];
                mode = modConfig[1];
                name = modConfig[2];
                if (!isModifierActive(self, name)) {
                    setModifier(self, name, mode, type, event);
                } else {
                    // Check if this is unset twice in case of the clip or clipOnce callback fires too!
                    // The unsetModifier method only executes if the modifier is still active
                    shmi.removeClass(event.target, 'clipped');
                    unsetModifier(self, name, mode, type, event);
                }
            }
            break;
        case 'resolver':
            keyResolver(self, key);
            break;
        default:
            log('uncatched keyboard event: ' + key.type);
        }
    }

    /**
     * is fired when a key with type: resolver is pressed
     * (e.g. backspace, submit, ..)
     *
     * @param {object} self
     * @param {object} key key where the event was triggert
     */
    function keyResolver(self, key) {
        var resolve = key.resolve;
        if (resolve) {
            // define addition actions on resolve which is meant to modify the current text
            // add any resolve case with further methods
            switch (resolve) {
            case 'backspace':
                deleteLetterFromCurrentText(self);
                break;
            case 'submit':
                log("Fire: Submit Keyboard");
                shmi.fire('keyboard-submit', {
                    "success": true,
                    "input": self.vars.currentText
                });
                break;
            default:
                log("Missing Default Case");
            }
        }
    }

    /**
     * set Modifier
     * a modifier is somthing like `shift` or `alt`
     *
     * @param {object} self
     * @param {string} name  name of modifiers, like `shift`
     * @param {string} mode modifier-mode for the switch-case, like `clip` or `clipOnce`
     * @param {string} type type of keys that can be modified by this modifier (e.g. `letter`)
     * @param {object} setEvent event from mouse, key or touch event
     */
    function setModifier(self, name, mode, type, setEvent) {
        if (!isModifierActive(self, name)) {
            var id = getActivePaletteId(self);
            self.vars.activeModifiers[id] = self.vars.activeModifiers[id] || [];
            self.vars.activeModifiers[id].push(name);
            // the unsetModifier is registered with the modification mode and type listener
            createModificationTypeListeners(self, name, mode, type, setEvent);
            Object.keys(self.vars.activeKeyMap).forEach(function(key) {
                if (self.vars.activeKeyMap[key].type === type) {
                    var values = evaluateKeyModifiers(self, self.vars.activeKeyMap[key]);
                    setKeyConfig(self, self.vars.activeKeyMap[key], values);
                }
            });
        }
    }

    /**
     * unset Modifiers
     * a modifier is somthing like `shift` or `alt`
     *
     * @param {object} self
     * @param {string} name  name of modifiers, like `shift`
     * @param {string} mode modifier-mode for the switch-case, like `clip` or `clipOnce`
     * @param {string} type type of keys that can be modified by this modifier (e.g. `letter`)
     * @param {object} event event from mouse, key or touch event
     */
    function unsetModifier(self, name, mode, type, event) {
        if (isModifierActive(self, name)) {
            var id = getActivePaletteId(self),
                idx = self.vars.activeModifiers[id].indexOf(name);
            // remove the of the active modifiers array
            self.vars.activeModifiers[id].splice(idx, 1);
            Object.keys(self.vars.activeKeyMap).forEach(function(key) {
                var values = evaluateKeyModifiers(self, self.vars.activeKeyMap[key]);
                setKeyConfig(self, self.vars.activeKeyMap[key], values);
            });
        }
    }

    /**
     * create function for when modifier-mode is `clip`
     * (deutsch: dauerhaftes Feststell dieser Taste)
     *
     * @param {object} self
     * @param {string} name name of modifiers, like `shift`
     * @param {string} mode modifier-mode, `clip`
     * @param {string} type type of keys that can be modified by this modifier (e.g. `letter`)
     * @param {string} id   identifier
     * @param {object} event event from mouse, key or touch event
     */
    function createClipModifierFunction(self, name, mode, type, id, event) {
        return function modClip(unsetEvent, key) {
            // listener callback gets triggered on each click on a button of the specified type
            if (unsetEvent !== event && key.modify === type + ':' + mode + ':' + name) {
                // only unset the clip modifier if key has the same modify type
                delete self.vars.keyTypeListeners[type][mode][id]; // remove registered listener callback
                unsetModifier(self, name, mode, type, event); // unset the modifer
                return false; // prevent any further actions: boolean
            }
            return true;
        };
    }

    /**
     * create function for when modifier-mode is `clipOnce`
     * (deutsch: einmaliges Feststell dieser Taste)
     *
     * @param {object} self
     * @param {string} name name of modifiers, like `shift`
     * @param {string} mode modifier-mode, `clipOnce`
     * @param {string} type type of keys that can be modified by this modifier (e.g. `letter`)
     * @param {string} id   identifier
     * @param {object} event event from mouse, key or touch event
     */
    function createClipOnceModifierFunction(self, name, mode, type, id, event) {
        return function modClipOnce(unsetEvent, key) {
            // listener callback gets triggered on each click on a button of the specified type
            if (unsetEvent !== event) {
                // only unset the modifier if it did not get set by the current event
                if (key.type === type || key.modify === type + ':' + mode + ':' + name) {
                    // only unset modifier if the pressed key is of type being modified or has same modify type
                    shmi.removeClass(event.target, 'clipped');
                    delete self.vars.keyTypeListeners[type][mode][id];
                    unsetModifier(self, name, mode, type, event);
                    return false; // prevent any further actions: boolean
                }
            }
            return true;
        };
    }

    /**
     * creates listener and function that is called, when button is pressed and a modifier is active
     *
     * @param {object} self
     * @param {string} name name of modifiers, like `shift`
     * @param {string} mode modifier-mode for the switch-case, like `clip` or `clipOnce`
     * @param {string} type type of keys that can be modified by this modifier (e.g. `letter`)
     * @param {object} event event from mouse, key or touch event
     */
    function createModificationTypeListeners(self, name, mode, type, event) {
        var id = getRandomId(8);
        if (self && name && mode && type && event) {
            // these are different types of modifier modes
            switch (mode) {
            // the clip modifier mode keeps the modifier enabled until the button gets pressed again
            case 'clip':
                // ensure the mode is an object
                self.vars.keyTypeListeners[type][mode] = self.vars.keyTypeListeners[type][mode] || {};
                // define callback in listener for a pressed button of specific type and mode identified by a random id
                self.vars.keyTypeListeners[type][mode][id] = createClipModifierFunction(self, name, mode, type, id, event);
                break;
            // the clipOnce modifier mode keeps the modifier enable until one other button is pressed
            case 'clipOnce':
                // add class 'clipped', contains the same style as 'pressed', doesn't get removed on release
                shmi.addClass(event.target, 'clipped');
                // ensure key type listener for mode is an object
                self.vars.keyTypeListeners[type][mode] = self.vars.keyTypeListeners[type][mode] || {};
                // define callback in listener for a pressed button of specific type and mode identified by a random id
                self.vars.keyTypeListeners[type][mode][id] = createClipOnceModifierFunction(self, name, mode, type, id, event);
                break;
            default:
                log("Undefined modifier mode", mode);
            }
        }
    }

    /**
     * get all modifier of given key
     *
     * @param {object} self
     * @param {object} key key context
     * @returns {object} modifier of key
     */
    function evaluateKeyModifiers(self, key) {
        const values = {},
            modifiers = shmi.cloneObject(key.modifiers),
            iterObj = shmi.requires("visuals.tools.iterate.iterateObject");

        iterObj(modifiers || {}, (mapped, modifier) => {
            const modVars = modifier.split(':');
            if (modVars.length !== 2) {
                console.error("[Keyboard] - modifier not configured right", key);
            } else if (isModifierActive(self, modVars[0])) {
                // add class if modifier is active
                values[modVars[1]] = mapped;
            } else if (modVars[1] === 'class-name') {
                // and remove it if the modifier is inactive
                values[modVars[1]] = '!' + mapped;
            }
        });

        return values;
    }

    /**
     * set keyboard palette by given key config
     *
     * @param {object} self
     * @param {object} key key, that initiates palatte change
     */
    function setKeyboardPalette(self, key) {
        var paletteName = key.palette,
            newPalette = null;

        // search for palette with paletteName within available palettes of active keyboard
        newPalette = Object.values(self.vars.activeKeyboard.paletteMap).filter(function(palette) {
            return palette && (palette.name === (paletteName));
        })[0];

        if (newPalette) {
            // hide current palette
            self.vars.activePalette.element.style.display = 'none';

            // disable all listeners of the current active palette
            disableActiveKeyMapListeners(self);

            self.vars.activePalette = newPalette;
            self.vars.activeKeyMap = self.vars.activePalette.keyMap;

            // show new palette
            // important: shown palette element has to be display flex
            self.vars.activePalette.element.style.display = 'flex';

            // enable the new active palette key listeners
            enableActiveKeyMapListeners(self);
        } else {
            console.error('[Keyboard] - can not find palette with name: "' + paletteName + '"');
        }
    }

    /**
     * check if given modifier is active
     *
     * @param {object} self
     * @param {string} modifierName name of modifier
     * @returns {boolean} if modifier by modifier name is currently active or not
     */
    function isModifierActive(self, modifierName) {
        var id = getActivePaletteId(self);
        return self.vars.activeModifiers[id] && self.vars.activeModifiers[id].indexOf(modifierName) !== -1;
    }

    /**
     * get id of active palette
     *
     * @returns {string}
     */
    function getActivePaletteId(self) {
        return self.vars.activeKeyboard.name.toLowerCase() + ':' + self.vars.activePalette.name.toLowerCase();
    }

    /**
     * removeAllPressedHighlights - removes all `pressed` classes.
     * (this is why we have a clipped class with the same styles too ;) )
     *
     * @param {object} self
     */
    function removeAllPressedHighlights(self) {
        self.vars.activePalette.element.querySelectorAll('.key.pressed').forEach(function(pressedElement) {
            shmi.removeClass(pressedElement, 'pressed');
        });
    }

    /**
     * removeAllAltKeys - removes all `show-alt-keys` classes.
     *
     * @param {object} self
     */
    function removeAllAltKeys(self) {
        clearTimeout(self.vars.altKeysTimeout); // removes timeout to show alternative keys
        self.vars.activePalette.element.querySelectorAll('.key.show-alt-keys').forEach(function(pressedElement) {
            shmi.removeClass(pressedElement, 'show-alt-keys');
        });
    }

    /**
     * fire type listener
     *
     * @param {object} self
     * @param {object} key key configuration
     * @param {object} event event from mouse, key or touch event
     * @returns {boolean} if further actions should be prevented or be allowed
     */
    function fireTypeListeners(self, key, event) {
        var iter = shmi.requires("visuals.tools.iterate").iterateObject;
        var type = key.type,
            prevented = false;
        if (type) {
            var typeListeners = self.vars.keyTypeListeners[type];
            iter(typeListeners, function(listenerCallbacks, mode) {
                iter(listenerCallbacks, function(callback, id) {
                    if (callback(event, key) === false) {
                        prevented = true;
                    }
                });
            });
        }
        return !prevented;
    }

    /**
     * get key by value
     *
     * @param {object} self
     * @param {object} event event from mouse, key or touch event
     * @returns {object} key with the given value
     */
    function getKeyByValue(self, event) {
        var keyMap = null;
        keyMap = Object.values(self.vars.activeKeyMap).filter(function(key) {
            var eventKey = event.key.toLowerCase(),
                keyName = key.name;
            return keyName === eventKey;
        })[0];
        return keyMap ? self.vars.activeKeyMap[keyMap["data-key-id"]] : null;
    }

    /**
     * delete letter from current text
     *
     * @param {object} self
     */
    function deleteLetterFromCurrentText(self, forwardMode) {
        var oldText = String(self.vars.currentText),
            newText = oldText,
            newIndex = 0;

        if (self.vars.previewContainer.selectionStart === self.vars.previewContainer.selectionEnd) {
            if (self.vars.previewContainer.selectionStart !== 0) {
                if (forwardMode) {
                    if (getSymbols(oldText.substr(self.vars.previewContainer.selectionStart, self.vars.previewContainer.selectionStart + 2))[1] === "") {
                        newText = oldText.substr(0, self.vars.previewContainer.selectionStart) + oldText.substr(self.vars.previewContainer.selectionEnd + 2);
                        newIndex = self.vars.previewContainer.selectionStart;
                    } else {
                        newText = oldText.substr(0, self.vars.previewContainer.selectionStart) + oldText.substr(self.vars.previewContainer.selectionEnd + 1);
                        newIndex = self.vars.previewContainer.selectionStart;
                    }
                } else if (getSymbols(oldText.substr(self.vars.previewContainer.selectionStart - 2, self.vars.previewContainer.selectionStart))[1] === "") {
                    newText = oldText.substr(0, self.vars.previewContainer.selectionStart - 2) + oldText.substr(self.vars.previewContainer.selectionEnd);
                    newIndex = self.vars.previewContainer.selectionStart - 2;
                } else {
                    newText = oldText.substr(0, self.vars.previewContainer.selectionStart - 1) + oldText.substr(self.vars.previewContainer.selectionEnd);
                    newIndex = self.vars.previewContainer.selectionStart - 1;
                }
            }
        } else {
            newText = oldText.substr(0, self.vars.previewContainer.selectionStart) + oldText.substr(self.vars.previewContainer.selectionEnd);
            newIndex = self.vars.previewContainer.selectionStart;
        }

        self.vars.currentText = newText;
        updatePreviewTextContent(self);
        self.vars.previewContainer.selectionStart = self.vars.previewContainer.selectionEnd = newIndex;
    }

    /**
     * function to seperate all symbols in a string
     * Source: https://mathiasbynens.be/notes/javascript-unicode
     * this is needed to separete UTF-16 encoded characters like emoji's. Could be handy with different languages as well
     *
     * @param {string} string string where symboles should be found
     * @returns {object[]} array of found symboles
     */
    function getSymbols(string) {
        var index = 0;
        var length = string.length;
        var output = [];
        for (; index < length - 1; ++index) {
            var charCode = string.charCodeAt(index);
            if (charCode >= 0xD800 && charCode <= 0xDBFF) {
                charCode = string.charCodeAt(index + 1);
                if (charCode >= 0xDC00 && charCode <= 0xDFFF) {
                    output.push(string.slice(index, index + 2));
                    ++index;
                    continue;
                }
            }
            output.push(string.charAt(index));
        }
        output.push(string.charAt(index));
        return output;
    }

    /**
     * add letter into current text - This method just adds the given letter to the existing content (of the preview)
     * text. It manages also the position of the inserted letter
     *
     * @param {object} self
     * @param {object} key key to be inserted
     */
    function addLetterIntoCurrentText(self, key) {
        var modValues = evaluateKeyModifiers(self, key),
            letter = modValues.val || key.val,
            oldText = String(self.vars.currentText),
            newText = null,
            index = self.vars.previewContainer.selectionStart;
        if (letter && letter.length === 1) {
            newText = oldText.substr(0, self.vars.previewContainer.selectionStart) + letter + oldText.substr(self.vars.previewContainer.selectionEnd);
            self.vars.currentText = newText;
            updatePreviewTextContent(self);
            self.vars.previewContainer.selectionStart = self.vars.previewContainer.selectionEnd = index + 1;
        }
        if (letter && letter.length === 2) {
            newText = oldText.substr(0, self.vars.previewContainer.selectionStart) + letter + oldText.substr(self.vars.previewContainer.selectionEnd);
            self.vars.currentText = newText;
            updatePreviewTextContent(self);
            self.vars.previewContainer.selectionStart = self.vars.previewContainer.selectionEnd = index + 2;
        }
    }

    /**
     * Updates text content of PreviewTextContainer to current text
     * if password-input is enabled the string is replaced with an equal number of *
     *
     * @param {object} self
     */
    function updatePreviewTextContent(self) {
        if (self.vars.previewContainer) {
            if (self.config["password-input"]) {
                let str = '';
                if (self.vars.showPassword === true) {
                    str = self.vars.currentText;
                } else {
                    for (let i = 0; i < self.vars.currentText.length; i++) {
                        str += '●';
                    }
                }
                self.vars.previewContainer.value = str;
            } else {
                self.vars.previewContainer.value = self.vars.currentText;
            }
        }
    }

    // #        Event Listener      # //

    /**
     * onKeyPressListener
     *
     * @param {object} self
     * @param {number} x x postion of cursor
     * @param {number} y y postion of cursor
     * @param {object} event event that is triggered by mouse
     */
    function onKeyPressListener(self, x, y, event) {
        if (event.type.indexOf('mouse') !== -1 && event.type.indexOf('touch') === -1) {
            shmi.addClass(self.element, 'hover-enabled');
        } else {
            shmi.removeClass(self.element, 'hover-enabled');
        }
        self.vars.keyPressed = event.target.getAttribute('data-key-name');

        if (self.config["password-input"]) {
            event.preventDefault();
        } else {
            shmi.addClass(event.target, 'pressed');
        }
    }

    /**
     * onKeyClickListener
     *
     * @param {object} self
     * @param {number} x x postion of cursor
     * @param {number} y y postion of cursor
     * @param {object} event event that is triggered by mouse
     */
    function onKeyClickListener(self, x, y, event) {
        var keyElement = event.target.parentNode,
            keyElementId = keyElement.getAttribute('data-key-id'),
            key = self.vars.activeKeyMap[keyElementId];
        if (keyElement && key) {
            self.vars.keyPressed = false;
            // click = press and release -> set mouse move to null
            self.vars.mouseMoveKey = null;
            fireKeyboardClick(self, key, event);
            fireTypeListeners(self, key, event); // What the heck is this function doing? HELP!
            // |^| returns the true for continue or false for preventing further actions
            // could be used as if(fireTypeListeners(self, key, event)) { fireKeyboardClick(self, key, event); }

            if (self.config["auto-key-focus"] && document.activeElement !== keyElement) {
                // focus clicked key if auto key focus is enabled and key element has a tabindex attribute
                keyElement.focus();
            }
        }
    }

    /**
     * onKeyReleaseListener
     *
     * @param {object} self
     * @param {number} x x postion of cursor
     * @param {number} y y postion of cursor
     * @param {object} event event that is triggered by mouse
     */
    function onKeyReleaseListener(self, x, y, event) {
        if (self.vars.keyPressed) {
            var el = event.target;
            shmi.removeClass(el, 'pressed');
        }
    }

    /**
     * onKeyMouseDownListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyMouseDownListener(self, event) {
        shmi.removeClass(self.element, 'key-preview-enabled');
        var keyNameAttr = event.target.getAttribute('data-key-name') || false,
            key = self.vars.activeKeyMap[event.target.parentElement.getAttribute('data-key-id')];
        self.vars.keyPressed = keyNameAttr;
        self.vars.mouseMoveKey = null;

        if (self.vars.keyPressed && key && key.hold && !key.alternatives) {
            setTimeout(function() {
                if (self.vars.keyPressed === keyNameAttr) {
                    self.vars.keyHeld = true;
                    var clickAndHoldIV = setInterval(function() {
                        if (self.vars.keyPressed === keyNameAttr) {
                            fireKeyboardClick(self, key, event);
                            fireTypeListeners(self, key, event);
                        } else {
                            clearInterval(clickAndHoldIV);
                        }
                    }, 120);
                }
            }, 1000);
        }
        if (key && key.alternatives) {
            // if alternative keys are available show them after 500ms
            self.vars.altKeysTimeout = setTimeout(function() {
                if (self.vars.keyPressed === keyNameAttr) {
                    shmi.removeClass(self.element, 'key-preview-enabled');
                    shmi.addClass(event.target, 'show-alt-keys');
                }
            }, 500);
        }
    }

    /**
     * onKeyMouseUpListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyMouseUpListener(self, event) {
        if (self.vars.keyPressed) {
            self.vars.keyPressed = false;
            var key = event.target;
            shmi.removeClass(event.target, 'pressed');
            self.vars.previewContainer.focus();
            if (self.config["auto-key-focus"] && document.activeElement !== key) {
                key.focus();
            }
            key = self.vars.activeKeyMap[key.parentElement.getAttribute('data-key-id')];
            if (key && self.vars.mouseMoveKey) {
                fireKeyboardClick(self, key, event);
                fireTypeListeners(self, key, event);
                self.vars.mouseMoveKey = null;
            }
        }
        removeAllAltKeys(self); // remove displayed alternatives
    }

    /**
     * onKeyMouseInListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyMouseInListener(self, event) {
        if (self.vars.keyPressed && !self.config["password-input"]) {
            var el = event.target;
            self.vars.mouseMoveKey = !self.vars.keyHeld ? el : null;
            self.vars.keyPressed = el.getAttribute('data-key-name');
            shmi.addClass(el, 'pressed');
        }
    }

    /**
     * onKeyMouseOutListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyMouseOutListener(self, event) {
        if (self.vars.keyPressed) {
            var el = event.target;
            self.vars.mouseMoveKey = null;
            shmi.removeClass(el, 'pressed');
        }
    }

    /**
     * onKeyTouchStartListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyTouchStartListener(self, event) {
        if (self.config['key-preview']) {
            shmi.addClass(self.element, 'key-preview-enabled');
        }
        var keyNameAttr = event.target.getAttribute('data-key-name'),
            key = self.vars.activeKeyMap[event.target.parentElement.getAttribute('data-key-id')];
        self.vars.keyPressed = keyNameAttr;
        self.vars.mouseMoveKey = null;
        self.vars.activePalette.element.addEventListener('touchend', self.vars.eventListeners.touchEnd);
        self.vars.activePalette.element.addEventListener('touchmove', self.vars.eventListeners.touchMove);

        if (keyNameAttr && key && key.hold && !key.alternatives) {
            setTimeout(function() {
                if (self.vars.keyPressed === keyNameAttr) {
                    self.vars.keyHeld = true;
                    var pushAndHoldIV = setInterval(function() {
                        if (self.vars.keyPressed === keyNameAttr) {
                            fireKeyboardClick(self, key, event);
                            fireTypeListeners(self, key, event);
                        } else {
                            clearInterval(pushAndHoldIV);
                        }
                    }, 120);
                }
            }, 1000);
        }
        if (key && key.alternatives) {
            // if alternative keys are available show them after 500ms
            self.vars.altKeysTimeout = setTimeout(function() {
                if (self.vars.keyPressed === keyNameAttr) {
                    shmi.removeClass(self.element, 'key-preview-enabled');
                    shmi.addClass(event.target, 'show-alt-keys');
                }
            }, 500);
        }
    }

    /**
     * onKeyTouchMoveListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyTouchMoveListener(self, event) {
        event.preventDefault();
        event.stopPropagation();
        if (self.vars.keyPressed) {
            var clientY = event.touches[0].clientY,
                clientX = event.touches[0].clientX,
                keyElement = document.elementFromPoint(clientX, clientY);
            if (keyElement) {
                var keyNameAttr = keyElement.getAttribute('data-key-name');
                if (keyNameAttr && keyElement !== self.vars.dragCurrentKey) {
                    if (!self.vars.keyHeld && keyNameAttr !== self.vars.keyPressed) {
                        removeAllPressedHighlights(self);
                        if (!self.config["password-input"]) {
                            shmi.addClass(keyElement, 'pressed');
                        }
                    }
                    self.vars.dragCurrentKey = keyElement;
                    self.vars.keyPressed = keyNameAttr;
                }
            }
        }
    }

    /**
     * onKeyTouchEndListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyTouchEndListener(self, event) {
        self.vars.keyPressed = false;
        self.vars.activePalette.element.removeEventListener('touchend', self.vars.eventListeners.touchEnd);
        self.vars.activePalette.element.removeEventListener('touchmove', self.vars.eventListeners.touchMove);

        if (self.vars.dragCurrentKey && !self.vars.keyHeld) {
            removeAllPressedHighlights(self);
            self.vars.previewContainer.focus();
            if (self.config["auto-key-focus"] && document.activeElement !== event.target) {
                self.vars.dragCurrentKey.focus();
            }
            var key = self.vars.activeKeyMap[self.vars.dragCurrentKey.parentElement.getAttribute('data-key-id')];
            if (key) {
                fireKeyboardClick(self, key, event);
                fireTypeListeners(self, key, event);
            }
            self.vars.dragCurrentKey = null;
        } else {
            self.vars.keyHeld = false;
        }
        removeAllAltKeys(self); // remove all displayed alternatives
    }

    /**
     * onKeyDownListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyDownListener(self, event) {
        if (self.vars.lastKey && self.vars.lastKey === event.key) {
            if (self.config["password-input"]) {
                event.preventDefault();
            }
            return;
        }
        self.vars.lastKey = event.key;
        // do not show the key preview if a hardware keyboard event is triggered
        shmi.removeClass(self.element, 'key-preview-enabled');
        shmi.removeClass(self.element, 'hover-enabled');
        var key = getKeyByValue(self, event);
        if (key && !self.config["password-input"]) {
            // highlighting the pressed key
            shmi.addClass(key.element.firstChild, 'pressed');
            if (self.vars.previewContainer && document.activeElement !== self.vars.previewContainer) {
                if (!self.config['auto-key-focus']) {
                    self.vars.previewContainer.focus();
                }
            }
            // get updated String from keyboard input
            self.vars.currentText = self.vars.previewContainer.value;
        }
        if (event.key === "Enter" && !self.config['show-enter']) {
            shmi.fire('keyboard-submit', {
                "success": true,
                "input": self.vars.currentText
            });
        }

        if (self.config["password-input"]) {
            if ((event.key === "v") && event.ctrlKey === true) {
                navigator.clipboard.readText().then(function(clipText) {
                    for (var i = 0; i < clipText.length; i++) {
                        key = clipText.charAt(i);
                        handleKeyHit(key, null, self);
                    }
                });
                event.preventDefault();
                return;
            } else if (event.ctrlKey === true) {
                event.preventDefault();
                return;
            } else if (event.key !== "ArrowLeft" && event.key !== "ArrowRight" && event.key !== "ArrowUp" && event.key !== "ArrowDown" && event.key !== "Home" && event.key !== "End") {
                event.preventDefault();
            }
            handleKeyHit(event.key, event, self);
        }
        updatePreviewTextContent(self);
    }

    /**
     * handleKeyHit fires keyboard hits for the current key press
     *
     * @param {string} key key
     * @param {object} event event that is triggered by the user
     * @param {object} self
     */
    function handleKeyHit(key, event, self) {
        let hit = false;

        for (var palette in self.vars.activeKeyboard.paletteMap) {
            for (var entry in self.vars.activeKeyboard.paletteMap[palette].keyMap) {
                const qkey = self.vars.activeKeyboard.paletteMap[palette].keyMap[entry];
                if (hit) {
                    break;
                }
                if ((qkey.val === key) && (qkey.type === "letter" || qkey.type === "character" || qkey.type === "number")) {
                    fireKeyboardClick(self, qkey, event);
                    hit = true;
                } else if ((qkey.modifiers && qkey.modifiers["shift:val"] && qkey.modifiers["shift:val"] === key) && (qkey.type === "letter" || qkey.type === "character" || qkey.type === "number")) {
                    setModifier(self, "shift", "clipOnce", "letter", event);
                    fireKeyboardClick(self, qkey, event);
                    unsetModifier(self, "shift", "clipOnce", "letter", event);
                    hit = true;
                } else if (qkey.resolve && qkey.resolve.toLowerCase() === key.toLowerCase()) {
                    fireKeyboardClick(self, qkey, event);
                    hit = true;
                } else if (key === "Delete") {
                    deleteLetterFromCurrentText(self, true);
                    hit = true;
                }
            }
        }
    }

    /**
     * onKeyUpListener
     *
     * @param {object} self
     * @param {object} event event that is triggered by mouse
     */
    function onKeyUpListener(self, event) {
        self.vars.lastKey = null;
        if (event.key.toLowerCase() === 'escape') {
            log("Fire: Close Keyboard");
            shmi.fire('keyboard-cancel', {
                "success": false,
                "input": self.vars.currentText
            });
        }

        var key = getKeyByValue(self, event);
        if (key && !self.config["password-input"]) {
            shmi.removeClass(key.element.firstChild, 'pressed');
        }
        if (self.config["password-input"]) {
            event.preventDefault();
        } else {
            // get updated String from keyboard input
            self.vars.currentText = self.vars.previewContainer.value;
        }
        updatePreviewTextContent(self);
    }

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/* handler for Keyboard slide in */
(function() {
    // this queue handler is copied from the numpad and has initially been a queue for the notification dialog
    var keyboardEvent = null,
        keyboard = null;

    function createKeyboard() {
        var keyboardConfig = {},
            iter = shmi.requires("visuals.tools.iterate.iterateObject"),
            okTok = null,
            closeTok = null,
            enableTok = null;

        iter(keyboardEvent.detail, function(val, key) {
            keyboardConfig[key] = keyboardEvent.detail[key];
        });
        if (keyboardConfig.label) {
            keyboardConfig.label = shmi.evalString(shmi.localize(keyboardConfig.label), keyboardEvent.detail.param);
        }

        keyboard = shmi.createControl("alpha-num-keyboard", document.body, keyboardConfig, 'DIV');

        okTok = shmi.listen('keyboard-submit', function(evt) {
            okTok.unlisten();
            closeTok.unlisten();
            okTok = closeTok = null;
            deleteKeyboard();
            shmi.log("Keyboard closed, changes submited", evt);
            if (keyboardConfig.callback && typeof keyboardConfig.callback === "function") {
                keyboardConfig.callback(/*success:*/ true, /*input:*/ evt.detail.input);
            }
        });
        closeTok = shmi.listen('keyboard-cancel', function(evt) {
            closeTok.unlisten();
            okTok.unlisten();
            closeTok = okTok = null;
            deleteKeyboard();
            shmi.log("Keyboard closed, changes aborted", evt);
            if (keyboardConfig.callback && typeof keyboardConfig.callback === "function") {
                keyboardConfig.callback(/*success:*/ false, /*input:*/ null);
            }
        });

        function onKeyboardEnable() {
            if (enableTok) {
                enableTok.unlisten();
                enableTok = null;
            }
            keyboard.show();
        }

        if (keyboard.isActive()) {
            onKeyboardEnable();
        } else {
            enableTok = keyboard.listen("enable", onKeyboardEnable);
        }

        keyboard.enable();
    }

    function deleteKeyboard() {
        if (keyboard) {
            shmi.deleteControl(keyboard);
            keyboard = null;
        }
        keyboardEvent = null;
    }

    function defaultkeyboardHandler(event) {
        if (keyboard !== null) {
            return;
        }
        try {
            shmi.requires("visuals.handler.default.keyboard");
            keyboardEvent = event;
            if (keyboardEvent) {
                createKeyboard();
            }
        } catch (exc) {
            shmi.log("[Keyboard] - error trying to load controls: Keyboard\n" + exc);
        }
    }

    var module = shmi.pkg("visuals.handler.default.keyboard"),
        keyboardToken = null;

    module.register = function() {
        if (keyboardToken) {
            console.debug("Keyboard handler was active!");
            keyboardToken.unlisten();
            keyboardToken = null;
        }
        keyboardToken = shmi.listen('keyboard-request', defaultkeyboardHandler);
    };

    module.deregister = function() {
        if (!keyboardToken) {
            console.debug("Keyboard handler not active!");
        } else {
            keyboardToken.unlisten();
            keyboardToken = null;
        }
    };
}());

/**
 *
 */
(function() {
    /**
     * Displays an keyboard slide in to the user.
     * paramObj : {
     *      "key-preview": true,        // shown on touch only
     *      "auto-key-focus": false     // set tabindex="0" to key
     *      "select-box-enabled": true, // display selectbox to change language
     *      "value": null,
     *      "callback": function(Boolean success, String input) {
     *          if (success) {
     *              console.log("Keyboard successfully closed", input);
     *          } else {
     *              console.log("Keyboard aborted");
     *          }
     *      }
     * }
     *
     * @param {object} paramObj parameter object
     */
    shmi.keyboard = function(paramObj) {
        var nofListeners = shmi.fire('keyboard-request', paramObj, shmi);
        /* use native alert function if alpha num keyboard are not handled */
        if (nofListeners === 0) {
            shmi.log("[Keyboard] - Keyboard not found");
        }
    };
}());

shmi.pkg("visuals.controls");
/**
 * Button to execute UI actions
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - the base element of the control
 * @param config - configuration of the control
 */
shmi.visuals.controls.Button = function(element, config) {
    /* check for required packages */
    shmi.requires("visuals.core.UiAction");
    shmi.requires("visuals.io.MouseListener");
    shmi.requires("visuals.io.TouchListener");

    var self = this;

    self.element = element;

    self.config = config || {};

    /*set default options */
    self.parseAttributes();
    shmi.def(self.config, 'class-name', 'button');
    shmi.def(self.config, 'template', 'default/button');
    shmi.def(self.config, 'name', null);
    shmi.def(self.config, 'icon-src', null);
    shmi.def(self.config, 'icon-title', null);
    shmi.def(self.config, 'show-text', true);
    shmi.def(self.config, 'show-icon', false);
    shmi.def(self.config, 'label-from-item', false);
    shmi.def(self.config, 'icon-class', null);
    shmi.def(self.config, 'precision', -1);
    shmi.def(self.config, 'label', "button");
    shmi.def(self.config, 'type', shmi.c("TYPE_STRING"));
    shmi.def(self.config, 'unit-text', null);
    shmi.def(self.config, 'disable-item-lock', false);
    shmi.def(self.config, 'monoflop', false);
    shmi.def(self.config, 'monoflop-interval', 100);
    shmi.def(self.config, 'monoflop-value', 1);

    shmi.def(self.config, 'decimal-delimiter', ".");
    shmi.def(this.config, 'auto-precision', true);
    shmi.def(this.config, 'auto-label', true);
    shmi.def(this.config, 'auto-unit-text', true);
    shmi.def(this.config, 'auto-type', true);

    self.config['show-text'] = shmi.toBoolean(self.config['show-text']);
    self.config['show-icon'] = shmi.toBoolean(self.config['show-icon']);
    self.config['label-from-item'] = shmi.toBoolean(self.config['label-from-item']);

    self.mouselistener = null;
    self.touchlistener = null;
    self.active = false;
    self.initialized = false;
    self.labelElement = null;
    self.valueElement = null;
    self.rafId = 0;
    self._subscriptionTargetId = null;
    self.vars = {
        tokens: [],
        monoFlopInterval: 0,
        conditional: null
    };

    self.startup();
};

shmi.visuals.controls.Button.prototype = {
    uiType: "button",
    events: ['press', 'release', 'click'],
    tooltipProperties: ['icon-title'],
    getClassName: function() {
        return "Button";
    },
    /**
     * Initializes the Button control.
     *
     */
    onInit: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.initValueSettings(self);

        var label = shmi.getUiElement('button-label', self.element);
        if (!label) {
            shmi.log('[Button] no button-label element provided', 1);
        } else {
            self.labelElement = label;
        }

        self.valueElement = shmi.getUiElement('button-value', self.element);

        var icon = shmi.getUiElement('button-icon', self.element);
        if (!icon) {
            shmi.log('[Button] no button-icon element provided', 1);
        } else if (self.config['icon-src']) {
            try {
                icon.setAttribute('src', self.config['icon-src']);
            } catch (exc) {
                shmi.log("[Button] Exception setting icon-src: " + exc, 2);
            }
        } else if (self.config['icon-class']) {
            if (icon.tagName === "IMG") {
                /* switch img element for div when icon-class and no icon-src is configured */
                var icnDiv = document.createElement('div');
                shmi.addClass(icnDiv, "button-icon");
                icon.parentNode.insertBefore(icnDiv, icon);
                icon.parentNode.removeChild(icon);
                icon = icnDiv;
            }
            var icon_class = self.config['icon-class'].trim().split(" ");
            icon_class.forEach(function(cls) {
                shmi.addClass(icon, cls);
            });
        }
        /* all required elements found */
        if (label && self.config.label) {
            label.textContent = shmi.localize(self.config.label);
        }
        if (self.config.action) {
            self.action = new shmi.visuals.core.UiAction(self.config.action, self);
        }

        if ((self.config['show-text'] === true) && (self.config['show-icon'] === true)) {
            shmi.addClass(self.element, 'icon-and-text');
        } else if (self.config['show-icon'] === true) {
            shmi.addClass(self.element, 'icon-only');
        }

        var unitText = shmi.getUiElement('unit-text', self.element);
        if (unitText && self.config['unit-text']) {
            unitText.textContent = shmi.localize(self.config['unit-text']);
        }

        var btnFuncs = {},
            ses = shmi.visuals.session;
        btnFuncs.onClick = function(x, y, event) {
            if (ses.FocusElement !== null) {
                ses.FocusElement.blur();
                ses.FocusElement = null;
            }
            if (self.element instanceof HTMLElement) {
                self.element.focus();
                shmi.log("[Button] focused", 1);
            } else {
                shmi.log("[Button] only HTMLElements may be focused, type: " + (self.element.constructor), 1);
            }

            var event_detail = {
                x: x,
                y: y,
                event: event
            };
            self.fire('click', event_detail);

            if (self.action) {
                self.action.execute();
            }

            if (self.onClick) {
                self.onClick(self);
            }
        };
        btnFuncs.onPress = function(x, y, event) {
            shmi.addClass(this.element, 'pressed');
            var event_detail = {
                x: x,
                y: y,
                event: event
            };
            self.fire('press', event_detail);
        }.bind(self);
        btnFuncs.onRelease = function(x, y, event) {
            shmi.removeClass(self.element, 'pressed');
            var event_detail = {
                x: x,
                y: y,
                event: event
            };
            self.fire('release', event_detail);
        };
        var io = shmi.visuals.io;
        self.mouselistener = new io.MouseListener(self.element, btnFuncs);
        self.touchlistener = new io.TouchListener(self.element, btnFuncs);

        self.vars.keyUpListener = function(e) {
            var valid = self.vars.keyDownOnMe;
            if (valid && (e.code === 'Enter' || e.code === 'NumpadEnter') && self.element === document.activeElement) {
                shmi.removeClass(self.element, 'pressed');
                var rect = self.element.getBoundingClientRect();
                btnFuncs.onClick(rect.left + (rect.width / 2), rect.top + (rect.height / 2), e);
                self.vars.keyDownOnMe = false;
            }
        };

        self.vars.keyDownListener = function(e) {
            if ((e.code === 'Enter' || e.code === 'NumpadEnter') && self.element === document.activeElement) {
                shmi.addClass(self.element, 'pressed');
                self.vars.keyDownOnMe = true;
            }
        };
    },
    onEnable: function() {
        var self = this;
        if (self.vars.conditional !== null) {
            if (self.vars.conditional.item !== self.config.item) {
                self.vars.conditional.item = self.config.item;
            }
        } else if (self.config.item) {
            self.vars.conditional = shmi.createConditional(self.element, self.config.item);
        }

        if (self.vars.conditional) {
            self.vars.conditional.enable();
        }

        self.element.setAttribute('tabindex', '0');

        self.mouselistener.enable();
        self.touchlistener.enable();

        var im = shmi.requires("visuals.session.ItemManager");
        if (self.config.item) {
            if (self.config["disable-item-lock"]) {
                var h = im.getItemHandler();
                h.setValue = function(value) {
                    self.setValue(value);
                };
                self._subscriptionTargetId = im.subscribeItem(self.config.item, h);
            } else {
                self._subscriptionTargetId = im.subscribeItem(self.config.item, self);
            }
            if (self.config.monoflop) {
                var pTok = self.listen("press", function() {
                    clearInterval(self.vars.monoFlopInterval);
                    self.vars.monoFlopInterval = setInterval(function() {
                        var itemValues = {};
                        itemValues[self.config.item] = self.config['monoflop-value'];
                        im.writeDirect(itemValues, function(status, data) {
                            if (status !== 0) {
                                console.error("[ItemManager] failed to write item:", self.config.item, status, data);
                            }
                        });
                    }, self.config['monoflop-interval']);
                });
                var rTok = self.listen("release", function() {
                    clearInterval(self.vars.monoFlopInterval);
                });
                self.vars.tokens.push(pTok, rTok);
            }
        }

        self.element.addEventListener('keyup', self.vars.keyUpListener, false);
        self.element.addEventListener('keydown', self.vars.keyDownListener, false);

        shmi.log("[Button] enabled", 1);
    },
    onDisable: function() {
        var self = this;
        if (self.vars.conditional) {
            self.vars.conditional.disable();
        }

        if (self.config.item) {
            shmi.visuals.session.ItemManager.unsubscribeItem(self.config.item, self._subscriptionTargetId);
        }

        self.element.removeAttribute('tabindex');

        self.vars.tokens.forEach(function(t) {
            t.unlisten();
        });
        self.vars.tokens = [];

        self.mouselistener.disable();
        self.touchlistener.disable();

        self.element.removeEventListener('keyup', self.vars.keyUpListener);
        self.element.removeEventListener('keydown', self.vars.keyDownListener);

        shmi.log("[Button] disabled", 1);
    },
    onSetValue: function(value) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        if (self.config['label-from-item'] === true) {
            if (self.labelElement) {
                shmi.caf(self.rafId);
                self.rafId = shmi.raf(function() {
                    self.labelElement.textContent = shmi.localize(nv.formatOutput(value, self));
                });
            }
        } else if (self.valueElement) {
            shmi.caf(self.rafId);
            self.rafId = shmi.raf(function() {
                self.valueElement.textContent = nv.formatOutput(value, self);
            });
        }
    },
    /**
     * Sets properties for subscribed data
     *
     * @param min - min value
     * @param max - max value
     * @param step - value stepping
     */
    onSetProperties: function(min, max, step) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.setProperties(self, arguments);
    },
    onLock: function() {
        var self = this;
        self.mouselistener.disable();
        self.touchlistener.disable();
        shmi.addClass(self.element, 'locked');
        shmi.log("[Button] locked", 1);
        self.element.removeAttribute('tabindex');
        self.element.removeEventListener('keyup', self.vars.keyUpListener);
        self.element.removeEventListener('keydown', self.vars.keyDownListener);
    },
    onUnlock: function() {
        var self = this;
        self.mouselistener.enable();
        self.touchlistener.enable();
        shmi.removeClass(self.element, 'locked');
        shmi.log("[Button] unlocked", 1);
        self.element.setAttribute('tabindex', 0);
        self.element.addEventListener('keyup', self.vars.keyUpListener, false);
        self.element.addEventListener('keydown', self.vars.keyDownListener, false);
    },
    setLabel: function(labelText) {
        var self = this;
        if (self.config['auto-label'] && self.labelElement) {
            self.labelElement.textContent = shmi.localize(labelText);
        }
    },
    setUnitText: function(unitText) {
        var self = this,
            unitTextElement = shmi.getUiElement('unit-text', self.element);
        if (self.config['auto-unit-text'] && unitTextElement) {
            unitTextElement.textContent = shmi.localize(unitText);
        }
    }
};

shmi.extend(shmi.visuals.controls.Button, shmi.visuals.core.BaseControl);

(function() {
    shmi.pkg("visuals.controls");
    /**
     * Creates a new CheckBox control.
     *
     * @constructor
     * @extends shmi.visuals.core.BaseControl
     * @param element - the base element of this control
     * @param config - configuration of this control
     */
    shmi.visuals.controls.CheckBox = function(element, config) {
        /* check for required packages */
        shmi.requires("visuals.io.MouseListener");
        shmi.requires("visuals.io.TouchListener");

        var self = this;

        self.element = element;
        self.config = config || {};

        self.parseAttributes();

        shmi.def(self.config, 'class-name', 'checkbox');
        shmi.def(self.config, 'template', 'default/checkbox');
        shmi.def(self.config, 'name', null);
        shmi.def(self.config, 'on-value', 1);
        shmi.def(self.config, 'off-value', 0);
        shmi.def(self.config, 'pressedClass', 'pressed');
        shmi.def(self.config, 'icon-src', null);
        shmi.def(self.config, 'icon-title', null);
        shmi.def(self.config, 'icon-class', null);
        shmi.def(self.config, 'label', "checkbox");
        shmi.def(self.config, 'auto-label', true);
        shmi.def(self.config, 'show-icon', false);
        shmi.def(self.config, 'show-text', true);
        shmi.def(this.config, 'confirm-off-text', '${V_CONFIRM_OFF}');
        shmi.def(this.config, 'confirm-on-text', '${V_CONFIRM_ON}');
        shmi.def(this.config, 'confirm-on', false);
        shmi.def(this.config, 'confirm-off', false);

        self.value = 0;
        self.initialized = false;
        self.active = false;
        self._subscriptionTargetId = null;
        self.mouseListener = null;
        self.touchListener = null;

        self.startup();
    };

    function setBoxValue(self, value) {
        var im = shmi.requires("visuals.session.ItemManager");
        if (self.config.item) {
            im.writeValue(self.config.item, value);
        } else {
            self.setValue(value);
        }
    }

    shmi.visuals.controls.CheckBox.prototype = {
        uiType: "checkbox",
        events: ["change"],
        tooltipProperties: ["icon-title"],
        getClassName: function() {
            return "CheckBox";
        },
        /**
         * Initializes the control.
         *
         */
        onInit: function() {
            var self = this,
                icon = shmi.getUiElement('checkbox-icon', self.element);
            if (!icon) {
                shmi.log('[Checkbox] no button-icon element provided', 1);
            } else if (self.config['icon-src']) {
                try {
                    icon.setAttribute('src', self.config['icon-src']);
                } catch (exc) {
                    shmi.log("[Checkbox] Exception setting icon-src: " + exc, 2);
                }
            } else if (self.config['icon-class']) {
                if (icon.tagName === "IMG") {
                    /* switch img element for div when icon-class and no icon-src is configured */
                    var icnDiv = document.createElement('div');
                    shmi.addClass(icnDiv, "checkbox-icon");
                    icon.parentNode.insertBefore(icnDiv, icon);
                    icon.parentNode.removeChild(icon);
                    icon = icnDiv;
                }
                var icon_class = self.config['icon-class'].trim().split(" ");
                icon_class.forEach(function(cls) {
                    shmi.addClass(icon, cls);
                });
            }
            if (!self.element) {
                shmi.log('[CheckBox] no base element provided', 3);
                return;
            }

            if (self.config['show-text'] && self.config['show-icon']) {
                shmi.addClass(self.element, "icon-and-text");
            } else if (self.config['show-icon']) {
                shmi.addClass(self.element, "icon-only");
            }

            self.handleElement = shmi.getUiElement('checkbox-handle', self.element);
            if (!self.handleElement) {
                shmi.log('[CheckBox] no checkbox-handle found in base element', 3);
                return;
            }
            self.backgroundElement = shmi.getUiElement('checkbox-background', self.element);
            if (!self.backgroundElement) {
                shmi.log('[CheckBox] no checkbox-background found in base element', 3);
                return;
            }
            self.labelElement = shmi.getUiElement('checkbox-label', self.element);
            if (self.labelElement && self.config.label) {
                self.labelElement.textContent = shmi.localize(self.config.label);
            }

            shmi.addClass(self.handleElement, 'hidden');

            var ioFuncs = {};
            ioFuncs.onClick = function() {
                if (self.value === self.config['on-value']) {
                    if (self.config['confirm-off']) {
                        shmi.confirm(self.config['confirm-off-text'], function onConfirmed(confirmed) {
                            if (confirmed) {
                                setBoxValue(self, self.config['off-value']);
                            }
                        });
                    } else {
                        setBoxValue(self, self.config['off-value']);
                    }
                } else if (self.config['confirm-on']) {
                    shmi.confirm(self.config['confirm-on-text'], function onConfirmed(confirmed) {
                        if (confirmed) {
                            setBoxValue(self, self.config['on-value']);
                        }
                    });
                } else {
                    setBoxValue(self, self.config['on-value']);
                }
            };
            ioFuncs.onPress = function() {
                shmi.addClass(self.element, self.config.pressedClass);
            };
            ioFuncs.onRelease = function() {
                shmi.removeClass(self.element, self.config.pressedClass);
            };
            self.mouseListener = new shmi.visuals.io.MouseListener(self.element, ioFuncs);
            self.touchListener = new shmi.visuals.io.TouchListener(self.element, ioFuncs);
            shmi.log("[Checkbox] initialized", 1);
        },
        /**
         * Enables the CheckBox
         *
         */
        onEnable: function() {
            var self = this;
            if (self.config.item) {
                self._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(self.config.item, self);
            }

            self.mouseListener.enable();
            self.touchListener.enable();

            shmi.log("[CheckBox] enabled", 1);
        },
        /**
         * Disables the CheckBox
         *
         */
        onDisable: function() {
            var self = this;
            if (self.config.item) {
                shmi.visuals.session.ItemManager.unsubscribeItem(self.config.item, self._subscriptionTargetId);
            }

            self.mouseListener.disable();
            self.touchListener.disable();

            shmi.log("[CheckBox] disabled", 1);
        },
        /**
         * Locks the CheckBox
         *
         */
        onLock: function() {
            var self = this;
            self.mouseListener.disable();
            self.touchListener.disable();
            shmi.addClass(self.element, 'locked');
            shmi.log("[CheckBox] locked", 1);
        },
        /**
         * Unlocks the CheckBox
         *
         */
        onUnlock: function() {
            var self = this;
            self.mouseListener.enable();
            self.touchListener.enable();
            shmi.removeClass(self.element, 'locked');
            shmi.log("[CheckBox] unlocked", 1);
        },
        /**
         * Sets the underlying value of the CheckBox
         *
         * @param value - the new value to set for the CheckBox
         */
        onSetValue: function(value) {
            var self = this,
                changed = false;
            if (value === self.config['on-value']) {
                self.value = self.config['on-value'];
                changed = true;
                shmi.removeClass(self.handleElement, 'hidden');
            } else {
                self.value = self.config['off-value'];
                changed = true;
                shmi.addClass(self.handleElement, 'hidden');
            }
            if (changed) {
                self.fire("change", {
                    value: self.value
                });
            }
        },
        /**
         * Retrieves the current value of the CheckBox
         *
         * @return value current value of the CheckBox
         */
        getValue: function() {
            var self = this;
            return self.value;
        },
        setLabel: function(labelText) {
            var self = this;
            if (self.labelElement && self.config['auto-label']) {
                self.labelElement.textContent = shmi.localize(labelText);
            }
        }
    };

    shmi.extend(shmi.visuals.controls.CheckBox, shmi.visuals.core.BaseControl);
}());

(function() {
    shmi.pkg("visuals.controls");

    var DIRECTION_UP = -1,
        DIRECTION_DOWN = 1;

    /////////////////////////////////////////

    /**
     * Checks whether the combination of lhs and rhs is in `knownObjects`.
     *
     * @param {*} lhs
     * @param {*} rhs
     * @param {{lhs: any, rhs: any}[]} knownObjects List of known combinations
     *  of lhs and rhs.
     * @returns {boolean} `true` if { lhs, rhs } is in `knownObjects`, `false`
     *  else.
     */
    function isKnown(lhs, rhs, knownObjects) {
        return knownObjects.findIndex((entry) => lhs === entry.lhs && rhs === entry.rhs) !== -1;
    }

    /**
     * Checks if two objects have the same keys.
     *
     * @param {object} lhs
     * @param {object} rhs
     * @returns {boolean} `true` if both objects have the same keys, `false`
     *  else.
     */
    function objectsHaveSameKeys(lhs, rhs) {
        const lhsKeys = Object.keys(lhs),
            rhsKeys = Object.keys(rhs);

        if (lhsKeys.length !== rhsKeys.length) {
            return false;
        }

        return lhsKeys.every((value) => rhsKeys.includes(value));
    }

    /**
     * Checks if the two values are equal, checking arrays and objects as well.
     *
     * Note that "equal" does not mean "the same". Objects and arrays having
     * equal contents are considered equal themselves. The values do not need
     * to reference the same object.
     *
     * @param {*} lhs Left-hand side value for the comparison.
     * @param {*} rhs Right-hand side value for the comparison.
     * @param {{lhs: any, rhs: any}[]} knownObjects List of known combinations
     *  of lhs and rhs.
     * @returns {boolean} `true` if lhs and rhs are considered equal, `false` else.
     */
    function isEqualDeepImpl(lhs, rhs, knownObjects = []) {
        if (lhs === rhs) {
            return true;
        } else if (typeof lhs !== typeof rhs || typeof lhs !== "object") {
            return false;
        } else if (Array.isArray(lhs) !== Array.isArray(rhs)) {
            return false;
        } else if (isKnown(lhs, rhs, knownObjects)) {
            return true;
        } else if (Array.isArray(lhs)) {
            // Arrays

            // Arrays need to have the same length to be considered equal.
            if (lhs.length !== rhs.length) {
                return false;
            }

            knownObjects.push({ lhs, rhs });
            return lhs.every((val, idx) => isEqualDeepImpl(val, rhs[idx], knownObjects));
        } else {
            // Objects
            const keys = Object.keys(lhs);

            // If the objects have different keys then they can't be equal.
            if (!objectsHaveSameKeys(lhs, rhs)) {
                return false;
            }

            knownObjects.push({ lhs, rhs });
            return keys.every((key) => isEqualDeepImpl(lhs[key], rhs[key], knownObjects));
        }
    }

    /**
     * Checks if the two values are equal, checking arrays and objects as well.
     *
     * Note that "equal" does not mean "the same". Objects and arrays having
     * equal contents are considered equal themselves. The values do not need
     * to reference the same object.
     *
     * @param {*} lhs Left-hand side value for the comparison.
     * @param {*} rhs Right-hand side value for the comparison.
     * @returns {boolean} `true` if lhs and rhs are considered equal, `false` else.
     */
    function isEqualDeep(lhs, rhs) {
        return isEqualDeepImpl(lhs, rhs, []);
    }

    /////////////////////////////////////////

    /**
     * getRowOffset - get offset of row within datagrid data
     *
     * @param {object} self control instance reference
     * @param {number} rowId row ID
     * @return {number} row offset or `null` if row ID not found
     */
    function getRowOffset(self, rowId) {
        let offset = null;

        const dgm = shmi.requires("visuals.session.DataGridManager"),
            g = dgm.getGrid(self.cConfig.table),
            subId = self.dgSubscription.id;

        if (rowId >= 0) {
            const gridOffset = self.pageBuf.bufferOffset,
                currentIds = g.getCurrentIDs(subId);
            const rowIndex = currentIds.findIndex((id) => id === rowId);

            if (rowIndex >= 0) {
                offset = gridOffset + rowIndex;
            }
        }

        return offset;
    }

    /**
     * setLastClicked - set info on last clicked row and its offset
     *
     * @param {object} self control instance reference
     * @param {number} rowId row ID
     */
    function setLastClicked(self, rowId) {
        if (rowId >= 0) {
            const rowOffset = getRowOffset(self, rowId);
            if (rowOffset !== null) {
                self.lastClicked.rowId = rowId;
                self.lastClicked.offset = rowOffset;
                return;
            }
        }

        self.lastClicked.rowId = -1;
        self.lastClicked.offset = 0;
    }

    /**
     * loadRange - load row data range
     *
     * @param {object} self control instance reference
     * @param {number} rowId row ID of last clicked row
     * @param {number} start data start offset
     * @param {number} length number of rows to fetch
     * @param {function} callback function called on completion
     */
    function loadRange(self, rowId, start, length, callback) {
        startLoading(self);
        const dgm = shmi.requires("visuals.session.DataGridManager"),
            sub = dgm.subscribePage(self.cConfig.table, start, length, (dgInfo) => {
                const currentIds = dgm.getCurrentIDs(self.cConfig.table, sub.id);
                currentIds.forEach((id) => {
                    self.addRowIdToSelection(id);
                });

                dgm.unsubscribe(self.cConfig.table, sub.id);
                setLastClicked(self, rowId);
                stopLoading(self);

                self.updateSelTypeFromSelRows();
                self.updateDelRowsBtnEnable();
                self.updateSelectionView();
                clearTimeout(self.sel_fire_to);
                const selectionData = shmi.cloneObject(self.selection);
                self.sel_fire_to = setTimeout(function() {
                    self.fireSelChanged(selectionData);
                }, shmi.c("ACTION_RETRY_TIMEOUT"));
                callback();
            });
    }

    /**
     * addSelectionRange - add range from last clicked row to row ID to current selection
     *
     * @param {object} self control instance reference
     * @param {number} rowId row ID
     * @return {promise} promise resolving when selection range has been added
     */
    function addSelectionRange(self, rowId) {
        return new Promise((resolve, reject) => {
            const rowOffset = getRowOffset(self, rowId);
            if (rowOffset === null) {
                reject(new Error("Could not find row offset"));
            } else {
                let start, end;
                if (rowOffset >= self.lastClicked.offset) {
                    end = rowOffset;
                    start = self.lastClicked.offset;
                } else {
                    end = self.lastClicked.offset;
                    start = rowOffset;
                }

                const length = (end - start) + 1;
                loadRange(self, rowId, start, length, resolve);
            }
        });
    }

    /**
     * createTableRow - create individual table row
     *
     * @param {ComplexTable2} self table control instance reference
     * @param {number} rowIdx buffer row index
     * @param {HTMLElement} tbodyElem table body element to generate row into
     * @param {boolean} isFirst true when row is first row of tbody
     *
     * @returns {undefined}
     */
    function createTableRow(self, rowIdx, tbodyElem, isFirst) {
        var nofCols = self.cConfig.resp["nof-cols"],
            trElem = null,
            tdElem = null,
            isMultiSelectMode = (self.cConfig.resp["select-mode"] === "MULTI"),
            isSingleSelectMode = (self.cConfig.resp["select-mode"] === "SINGLE"),
            isItemSelectMode = (self.cConfig.resp["select-mode"] === "ITEM"),
            tabCellElem = null,
            bufferRow = self.pageBuf.rows[rowIdx];

        if (bufferRow) {
            trElem = document.createElement("tr");
            if (bufferRow.rowId === null) {
                trElem.style.visibility = "hidden";
                bufferRow.visible = false;
            }
            tbodyElem.appendChild(trElem);
            bufferRow.trElem = trElem;
            if (((self.pageBuf.offset + rowIdx) % 2) === 0) {
                shmi.addClass(trElem, self.uiCssCl.evenRow);
            }
            if (isMultiSelectMode) {
                if (self.config["show-select-boxes"]) {
                    tdElem = document.createElement("td");
                    tabCellElem = document.createElement("div");
                    bufferRow.selCB = self.createSelCBox(tabCellElem, rowIdx, false, self);
                    bufferRow.selCB.setChangeCallback(function(pageBufRow, state) {
                        if (!self.locked) {
                            self.selectCBoxValChanged(pageBufRow, state, false, false, true);
                        }
                    });
                    tdElem.appendChild(tabCellElem);
                    trElem.appendChild(tdElem);
                }

                bufferRow.trSelCtrl = self.createSelRowCtrl(trElem, rowIdx, self);
                bufferRow.trSelCtrl.setChangeCallback(function(pageBufRow, state, shiftPressed, ctrlPressed) {
                    if (!self.locked) {
                        self.selectCBoxValChanged(pageBufRow, state, shiftPressed, ctrlPressed, false);
                    }
                });
            } else if (isSingleSelectMode || isItemSelectMode) {
                bufferRow.trSelCtrl = self.createSelRowCtrl(trElem, rowIdx, self);
                bufferRow.trSelCtrl.setChangeCallback(function(pageBufRow, state, shiftPressed, ctrlPressed) {
                    if (!self.locked) {
                        self.selectRowValChanged(pageBufRow, state, shiftPressed, ctrlPressed);
                    }
                });
            }
            for (var col = 0; col < nofCols; col++) {
                createTableCell(self, rowIdx, col, trElem, isFirst);
            }
        } else {
            console.error("[ComplexTable2]", "missing row descriptor for row ID:", rowIdx);
        }
    }

    /**
     * createTableCell - create individual table cell
     *
     * @param {ComplexTable2} self table control instance reference
     * @param {number} rowIdx buffer row index
     * @param {number} columnIdx table column index
     * @param {HTMLElement} trElem table row element to generate cell into
     * @param {boolean} isFirst true if is first row of tbody
     *
     * @returns {undefined}
     */
    function createTableCell(self, rowIdx, columnIdx, trElem, isFirst) {
        var columnConfig = self.cConfig.resp["column-org"][columnIdx],
            tabCellElem = document.createElement("div"),
            tdElem = document.createElement("td"),
            nofCtrls = columnConfig.fields.length;
        tdElem.appendChild(tabCellElem);
        trElem.appendChild(tdElem);
        shmi.addClass(tabCellElem, self.uiCssCl.tdCont);
        if (isFirst) {
            if (columnConfig.fields[0]["min-width"]) {
                tabCellElem.style.minWidth = columnConfig.fields[0]["min-width"];
            }
        }
        for (var ctrlIdx = 0; ctrlIdx < nofCtrls; ctrlIdx++) {
            createCellControl(self, rowIdx, columnIdx, ctrlIdx, columnConfig, tabCellElem);
        }
    }

    /**
     * createCellControl - create control configured for specific table cell
     *
     * @param {ComplexTable2} self table control instance reference
     * @param {number} rowIdx buffer row index
     * @param {number} columnIdx table column index
     * @param {number} controlIdx column control index
     * @param {object} columnConfig table column configuration
     * @param {HTMLElement} cellElem element to display cell content
     *
     * @returns {undefined}
     */
    function createCellControl(self, rowIdx, columnIdx, controlIdx, columnConfig, cellElem) {
        var controlConfig = shmi.cloneObject(columnConfig.fields[controlIdx].ctrl.config),
            bufferRow = self.pageBuf.rows[rowIdx],
            itemName = (bufferRow.rowId === null) ? null : self.makeItem(bufferRow.rowId, columnConfig.fields[controlIdx].dgCol),
            ia = shmi.requires("visuals.tools.item-adapter"),
            columnAdapter = self.getColumnAdapter(columnConfig.fields[controlIdx].fieldName),
            ctrl = null;

        controlConfig.item = itemName;

        ctrl = shmi.createControl(columnConfig.fields[controlIdx].ctrl["ui-type"], cellElem, controlConfig, "div");
        if (itemName !== null) {
            if (columnAdapter === null) {
                ia.unsetAdapter(itemName);
            } else {
                ia.setAdapter(itemName, columnAdapter);
            }
        }

        bufferRow.cols[columnIdx].ctrls.push(ctrl);
    }

    /**
     * createRowDescriptor - create empty row descriptor for use in page buffer
     *
     * @param {number} datagridId  dotagrid row-ID or `null` when not in use
     * @param {number} columnCount number of table columns
     *
     * @returns {object} row descriptor
     */
    function createRowDescriptor(datagridId, columnCount) {
        var rowDescr = {
            rowId: datagridId,
            trElem: null,
            trSelCtrl: null,
            selCB: null,
            cols: [],
            visible: true
        };

        for (var col = 0; col < columnCount; col++) {
            rowDescr.cols.push({ ctrls: [] });
        }

        return rowDescr;
    }

    /**
     * onQuickSearch - called to perform quicksearch filter update
     *
     * @param  {ComplexTable2} self table control instance reference
     * @param  {HTMLElement} valueElement value element of quicksearch input-field
     * @return {undefined}
     */
    function onQuickSearch(self, valueElement) {
        self.qSearchInterval = self.qSearchInterval || null;
        if (self.qSearchInterval === null) {
            self.qSearchInterval = setTimeout(function() {
                if (valueElement.textContent !== self.qSearchOld) {
                    self.quicksearchChanged(valueElement.textContent);
                }
                self.qSearchOld = valueElement.textContent;
                self.qSearchInterval = null;
            }, 300);
        }
    }

    /**
     * setElementOffset - set partial table offset relative to scrolling area content
     *
     * @param  {HTMLElement} element partial table element
     * @param  {number} offset  top offset in px
     * @return {undefined}
     */
    function setElementOffset(element, offset) {
        var useTranslate = false,
            template = "translate(<%= X %>px, <%= Y %>px)";
        if (useTranslate) {
            element.style.transform = shmi.evalString(template, {
                X: 0,
                Y: offset
            });
        } else {
            element.style.top = offset + "px";
        }
    }

    /**
     * setPagePositions - update position of partial table elements
     *
     * @param  {ComplexTable2} self table control instance reference
     * @return {undefined}
     */
    function setPagePositions(self) {
        var tableParts = [self.tbodyElement[0], self.tbodyElement[1], self.tbodyElement[2]],
            linesPerPage = self.cConfig["nof-buffered-rows"] / 3,
            pageHeight = self.metricsData.lineHeightPx * linesPerPage;
        self.vars.pageOrder.forEach(function(pageIndex) {
            setElementOffset(tableParts[pageIndex], self.vars.pageOffsets[pageIndex] * pageHeight);
        });
    }

    /**
     * reinitPages - reinitialize partial table elements
     *
     * @param  {ComplexTable2} self table control instance reference
     * @param  {number} scrollTop current scrolling offset
     * @return {undefined}
     */
    function reinitPages(self, scrollTop) {
        var lineHeight = self.metricsData.lineHeightPx,
            totalHeight = self.pageBuf.nofTotalRows * lineHeight,
            linesPerPage = self.cConfig["nof-buffered-rows"] / 3,
            pageHeight = self.metricsData.lineHeightPx * linesPerPage,
            numPages = Math.ceil(totalHeight / pageHeight),
            offsetPages = scrollTop / pageHeight,
            curPage = Math.floor(offsetPages);

        if ((curPage >= 1) && (numPages > 3)) {
            self.vars.pageOffsets = [curPage - 1, curPage, curPage + 1];
        } else {
            self.vars.pageOffsets = [0, 1, 2];
        }
        self.vars.pageOrder = [0, 1, 2];
        setPagePositions(self);
    }

    /**
     * turnPage - turn table element paging forwards/backwards
     *
     * @param {object} self control instance reference
     * @param {boolean} [backwards=false] `false` to page forwards, `true` for backwards
     */
    function turnPage(self, backwards = false) {
        const curPage = self.vars.pageOffsets[backwards ? self.vars.pageOrder[0] : self.vars.pageOrder[2]];
        if (backwards) {
            self.vars.pageOrder.unshift(self.vars.pageOrder.pop());
        } else {
            self.vars.pageOrder.push(self.vars.pageOrder.shift());
        }
        self.vars.pageOffsets[self.vars.pageOrder[0]] = curPage - 1;
        self.vars.pageOffsets[self.vars.pageOrder[1]] = curPage;
        self.vars.pageOffsets[self.vars.pageOrder[2]] = curPage + 1;
        setPagePositions(self);
    }

    /**
     * updatePages - update position of partial table elements when scrolling changed
     *
     * @param  {ComplexTable2} self table control instance reference
     * @param  {number} scrollTop scrolling offset
     * @return {undefined}
     */
    function updatePages(self, scrollTop) {
        var lineHeight = self.metricsData.lineHeightPx,
            totalHeight = self.pageBuf.nofTotalRows * lineHeight,
            linesPerPage = self.cConfig["nof-buffered-rows"] / 3,
            viewportHeight = self.metricsData.viewportHeight,
            pageHeight = lineHeight * linesPerPage,
            numPages = Math.ceil(totalHeight / pageHeight),
            topY = scrollTop,
            bottomY = topY + viewportHeight,
            dy = scrollTop - self.vars.scrollTop,
            direction = Math.sign(dy),
            pageThreshold = null,
            pageOffset = null,
            oldOffset = self.pageBuf.offset,
            newOffset = null,
            oldBufferOffset = self.pageBuf.bufferOffset,
            oldPageOrder = null,
            reinited = false;

        if ((numPages > 3) && (Math.abs(dy) < pageHeight / 2)) {
            //turn pages after small scroll
            if (direction === DIRECTION_UP) {
                pageOffset = self.vars.pageOffsets[self.vars.pageOrder[0]] * pageHeight;
                pageThreshold = pageOffset + (pageHeight / 2);
                if ((pageOffset > 0) && (topY < pageThreshold)) {
                    //pageUp
                    turnPage(self, true);
                }
            } else if (direction === DIRECTION_DOWN) {
                pageOffset = self.vars.pageOffsets[self.vars.pageOrder[2]] * pageHeight;
                pageThreshold = pageOffset + (pageHeight / 2);
                if ((pageOffset > 0) && (bottomY > pageThreshold)) {
                    //pageDown
                    turnPage(self, false);
                }
            }
        } else if ((numPages > 3) || self.vars.pageOffsets.some((pOffset) => pOffset > 2)) {
            //reorder after big scroll or old page offsets out of range
            oldPageOrder = self.vars.pageOrder.toString();
            reinitPages(self, scrollTop);
            reinited = true;
        }

        newOffset = Math.floor((self.vars.pageOffsets[self.vars.pageOrder[0]] * pageHeight) / lineHeight);
        self.vars.scrollTop = scrollTop;

        if (newOffset !== oldOffset) {
            self.pageBuf.offset = newOffset;
            self.pageBuf.bufferOffset = calcBufferOffset(self);
            if (oldBufferOffset !== self.pageBuf.bufferOffset) {
                startLoading(self);
                self.dgMan.setOffset(self.cConfig.table, self.pageBuf.bufferOffset, self.dgSubscription.id);
            } else {
                self.onDataGridChange(self.dgSubscription.changeInfo);
            }
        } else if (reinited && (oldPageOrder !== self.vars.pageOrder.toString())) {
            self.onDataGridChange(self.dgSubscription.changeInfo);
        }
    }

    /**
     * calcBufferOffset - calculate new (data-)buffer offset based on current (view-buffer-)offset.
     *
     * @param  {ComplexTable2} self table control instance reference
     * @return {number} new data buffer offset
     */
    function calcBufferOffset(self) {
        var newBufferOffset = self.pageBuf.bufferOffset;
        if (self.pageBuf.offset < self.pageBuf.bufferOffset) {
            newBufferOffset = Math.max(0, (self.pageBuf.offset + self.pageBuf.viewportSize) - self.pageBuf.bufferSize);
        } else if (self.pageBuf.offset + self.pageBuf.viewportSize > self.pageBuf.bufferOffset + self.pageBuf.bufferSize) {
            newBufferOffset = self.pageBuf.offset;
        }
        return newBufferOffset;
    }

    /**
     * getTableId - create unique ID to create instance specific inline stylesheet
     *
     * @param  {ComplexTable2} self table control instance reference
     * @return {string} instance ID
     */
    function getTableId(self) {
        var idPrefix = "ct2-instance-",
            numId = 0;
        while (shmi.visuals.controls.ComplexTable2.prototype._tableIds[idPrefix + numId] !== undefined) {
            numId += 1;
        }
        shmi.visuals.controls.ComplexTable2.prototype._tableIds[idPrefix + numId] = true;
        return idPrefix + numId;
    }

    /**
     * releaseTableId - release registered instance ID
     *
     * @param  {ComplexTable2} self table control instance reference
     * @param  {string} tableId instance ID
     * @return {undfined}
     */
    function releaseTableId(self, tableId) {
        delete shmi.visuals.controls.ComplexTable2.prototype._tableIds[tableId];
    }

    /**
     * applyLineHeight - apply configured line-height
     *
     * @param  {ComplexTable2} self table control instance reference
     * @return {undefined}
     */
    function applyLineHeight(self) {
        var styleSheet = null;
        if (self.cConfig.resp["line-height"]) {
            if (!self.styleSheet) {
                styleSheet = document.createElement("style");
                self.styleSheet = styleSheet;
            }
            self.styleSheet.innerHTML = "#" + self.tableId + " .ct2-tdcont { height: " + self.cConfig.resp["line-height"] + "; }";
            if (styleSheet) {
                self.element.insertBefore(self.styleSheet, self.element.firstChild);
            }
        }
    }

    /**
     * startLoading - start loading state
     *
     * @param {object} self control instance reference
     */
    function startLoading(self) {
        self.loadingCount += 1;

        if (self.loadingCount === 1) {
            shmi.addClass(self.element, "loading");
        }
    }

    /**
     * stopLoading - stop loading state
     *
     * @param object} self control instance reference
     */
    function stopLoading(self) {
        self.loadingCount -= 1;
        if (self.loadingCount <= 0) {
            self.loadingCount = 0;
            shmi.removeClass(self.element, "loading");
        }
    }

    /**
     * Creates a new ComplexTable2
     *
     * @constructor
     * @extends shmi.visuals.core.BaseControl
     * @param {Element} element the base element of the control
     * @param {Object} config control configuration
     */
    shmi.visuals.controls.ComplexTable2 = function(element, config) {
        // generate a unique id for this CT2 instance
        this.instID = Date.now();
        while (shmi.visuals.controls.ComplexTable2.instIDs.indexOf(this.instID) !== -1) {
            this.instID++;
        }
        shmi.visuals.controls.ComplexTable2.instIDs.push(this.instID);

        // short cuts to static "const" definitions for CT2
        this.c = shmi.visuals.controls.ComplexTable2.c;
        this.uiElName = shmi.visuals.controls.ComplexTable2.uiElName;
        this.uiCssCl = shmi.visuals.controls.ComplexTable2.uiCssCl;

        this.element = element;
        this.config = config || {};
        this.loadingCount = 0;
        this.lastClicked = {
            rowId: null,
            offset: 0
        };
        this.parseAttributes();

        shmi.def(this.config, "class-name", "complex-table2");
        shmi.def(this.config, "template", "default/complex-table2");
        shmi.def(this.config, "name", null);
        shmi.def(this.config, "label", "complex-table2");
        shmi.def(this.config, "table", "undefined-table");
        shmi.def(this.config, "item-selected-row", null);
        shmi.def(this.config, "rows-items-from-1", true);
        shmi.def(this.config, "field-datagrid-col-map", []);
        shmi.def(this.config, "default-field-control-map", {});
        shmi.def(this.config, "default-field-headers", {});
        shmi.def(this.config, "default-layout", {});
        shmi.def(this.config, "select-mode", "MULTI");
        shmi.def(this.config, "show-select-boxes", true);
        shmi.def(this.config, "text-mode", "NORMAL");
        shmi.def(this.config, "show-nof-rows", false);
        shmi.def(this.config, "delete-selected-rows", false);
        shmi.def(this.config, "useDgIndex", false);
        shmi.def(this.config, "col-width-changeable", false);
        shmi.def(this.config, "cols-show-hide", false);
        shmi.def(this.config, "cols-movable", false);
        shmi.def(this.config, "rows-movable", false);
        shmi.def(this.config, "sortable-fields", []);
        shmi.def(this.config, "default-text-filter-fields", []);
        shmi.def(this.config, "filters", []);
        shmi.def(this.config, "default-nof-buffered-rows", this.c.MIN_BUF_SIZE);
        shmi.def(this.config, "show-buttons-table-min-width-px", 300);
        shmi.def(this.config, "edit-mode-active", false);
        shmi.def(this.config, "fieldIcons", {});
        shmi.def(this.config, "headerMode", "ICON");
        shmi.def(this.config, "adapterSelectConfig", {
            "class-name": "select-box no-label"
        });
        shmi.def(this.config, "adapterSelectClass", "adapter-select");

        //using standard "input-field" control for edit-mode
        shmi.def(this.config, "edit-mode-map", {});

        // "browser-dependencies" may be undefined
        // "responsive-layouts" may be undefined

        this.cConfig = {};
        this.filterData = {};
        this.colTextFilterData = {};
        this.ctrlPanelData = {};
        this.header = {};
        this.colTxtFilters = {};
        this.pageBuf = {};
        this.asyncProcData = {};
        this.metricsData = {};
        this.selection = {
            type: this.c.SELECT_TYPE_NOTHING,
            selRows: [],
            selRowIndex: []
        };
        this.lastSelection = null;
        this.scrollBar = null;
        this.dgMan = null;
        this.dgSubscription = {};
        this.selChgEventListeners = {};

        this.tcElement = null;
        this.bodyViewportElement = null;
        this.tabCCElement = null;
        this.tabHCElement = null;
        this.theadElement = null;
        this.txtFiltersHeadElement = null;
        this.tbodyElement = [];
        this.labelElement = null;
        this.footerElement = null;
        this.ctrlPanelElement = null;

        this.resizeDebounceTimeout = null;
        this.sbSliderDebounceTimeout = null;
        this.vpReleaseDebounceTimeout = null;
        this.swipeVertDebounceTimeout = null;

        this.cLayoutClass = null;
        this.isTableToCleanup = false;
        this.isTouchDevice = false;

        this.lastTableVpWidth = -1; // force init 1st time
        this.lastTableVpHeight = -1; // force init 1st time
        this.lastLayoutId = -100; // force init 1st time
        this.applyFiltersEnable = false;
        this.subscrIdItemSelRow = null;
        this.selRowByItem = 0;

        this.editMode = false;
        this.editRow = -1;

        this.headerMode = "TEXT"; // "TEXT" / "ICON"

        this.controls = [];

        this.vars = {
            lastUpdate: 0,
            updateTimeout: 0,
            scrollTop: 0,
            pageOffsets: [
                0,
                1,
                2
            ],
            pageOrder: [
                0,
                1,
                2
            ]
        };

        this.startup();
    };

    shmi.visuals.controls.ComplexTable2.prototype = {

        /*-- BaseControl method interface --*/
        _tableIds: {},
        uiType: "complex-table2",
        isContainer: true,
        events: ["select"],
        getClassName: function() {
            return "ComplexTable2";
        },
        setEditMode: function(editModeEnabled) {
            var self = this;
            self.editMode = editModeEnabled;
        },
        /**
         * Retrieves information about the currently selected rows.
         *
         * @returns {Object} the selection info, for structure refer clearSelection method
         */
        getSelectedRows: function() {
            return shmi.cloneObject(this.selection);
        },
        /**
         * Gets the datagrid row data for the current selection
         *
         * @param {Object} selection
         * @returns {Object} the datagrid data for the selection
         */
        getSelectionRowData: function(selection) {
            var self = this,
                answer = [];
            if (selection && selection.selRows.length > 0) {
                answer = selection.selRows.map((row) => self.dgMan.getRowData(self.config.table, row));
            }
            return answer;
        },
        /**
         * Sets the current selection
         *
         * @param {Object} selection the selection to set, for structure refer clearSelection method
         */
        setSelectedRows: function(selection) {
            shmi.checkArg("selection", selection, "object");
            shmi.checkArg("selection.type", selection.type, "number");
            shmi.checkArg("selection.selRows", selection.selRows, "array");
            shmi.checkArg("selection.selRowIndex", selection.selRowIndex, "array");
            this.selection = shmi.cloneObject(selection);
            this.updateSelectionView();

            var self = this;
            clearTimeout(self.sel_fire_to);
            self.sel_fire_to = setTimeout(function() {
                self.fireSelChanged(shmi.cloneObject(self.selection));
            }, shmi.c("ACTION_RETRY_TIMEOUT"));
        },
        /**
         * Adds a listener for selection changes
         *
         * @param {Object} listenerObj the listener to add, must implement callback method <listener>.tableSelectionChange(selection);
         * @returns {number} the listener id
         */
        addSelChgEventListener: function(listenerObj) {
            var id = Date.now();
            while (this.selChgEventListeners[id] !== undefined) {
                id++;
            }
            this.selChgEventListeners[id] = {
                listener: listenerObj
            };
            shmi.log("[ComplexTable2] addSelChgEventListener called, listenerObj: " + listenerObj + " - id:" + id, 1);
            return id;
        },
        /**
         * Removes a listener for selection changes.
         *
         * @param {number} id the id of the listener to remove
         */
        removeSelChgEventListener: function(id) {
            if (this.selChgEventListeners[id] !== undefined) {
                delete this.selChgEventListeners[id];
            } else {
                shmi.log("[ComplexTable2] removeSelChgEventListener - id " + id + " does not exist", 2);
            }
            shmi.log("[ComplexTable2] removeSelChgEventListener called, id: " + id, 1);
        },
        onInit: function() {
            var self = this,
                i = 0;
            self.headerMode = (self.config.headerMode === "ICON") ? "ICON" : "TEXT";
            self.tableId = getTableId(self);
            self.element.setAttribute("id", self.tableId);
            if (shmi.toBoolean(self.config["edit-mode-active"]) === true) {
                self.editMode = true;
            }

            this.isTouchDevice = this.detectTouchDevice();
            if (this.isTouchDevice) {
                shmi.addClass(this.element, this.uiCssCl.touchDev);
            } else {
                shmi.addClass(this.element, this.uiCssCl.noTouchDev);
            }

            this.buildCCfgRespIndep();
            this.buildCCfgRespDep();

            /* init all members, create child controls, get references to the necessary elements and ui-controls */
            this.dgMan = shmi.visuals.session.DataGridManager;
            this.tcElement = shmi.getUiElement(this.uiElName.tabContainer, this.element);
            if (!this.tcElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.tabContainer + " element provided (required)", 3);
                return;
            }

            this.theadElement = shmi.getUiElement(this.uiElName.thead, this.element);
            if (!this.theadElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.thead + " element provided (required)", 3);
                return;
            }
            this.txtFiltersHeadElement = shmi.getUiElement(this.uiElName.txtFiltersHead, this.element);
            if (!this.txtFiltersHeadElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.txtFiltersHead + " element provided (optional)", 1);
            }

            this.tableElement = shmi.getUiElement(this.uiElName.table, this.element);
            if (!this.tableElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.table + " element provided (required)", 3);
                return;
            }
            var tableBodyParts = shmi.getUiElements(this.uiElName.tbody, this.element);
            this.tbodyElement = [];
            for (i = 0; i < tableBodyParts.length; i++) {
                this.tbodyElement.push(tableBodyParts[i]);
            }
            if (!this.tbodyElement[0]) {
                shmi.log("[ComplexTable2] no " + this.uiElName.tbody + " element provided (required)", 3);
                return;
            }
            this.bodyViewportElement = shmi.getUiElement(this.uiElName.tbodyContainer, this.element);
            if (!this.bodyViewportElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.tbodyContainer + " element provided (required)", 3);
                return;
            }

            this.tabCCElement = shmi.getUiElement(this.uiElName.tabContContainer, this.element);
            if (!this.tabCCElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.tabContContainer + " element provided (required)", 3);
                return;
            }

            this.labelElement = shmi.getUiElement(this.uiElName.label, this.element);
            if (!this.labelElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.label + " element provided (optional)", 1);
            } else {
                this.labelElement.textContent = shmi.localize(this.cConfig.label);
            }
            this.footerElement = shmi.getUiElement(this.uiElName.footer, this.element);
            if (!this.footerElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.footer + " element provided (optional)", 1);
            }
            this.ctrlPanelElement = shmi.getUiElement(this.uiElName.ctrlPanel, this.element);
            if (!this.ctrlPanelElement) {
                shmi.log("[ComplexTable2] no " + this.uiElName.ctrlPanelElement + " element provided (optional)", 1);
            }

            this.initScrollHandler();
            this.updateSize = function() {
                clearTimeout(this.resizeDebounceTimeout);
                this.resizeDebounceTimeout = setTimeout(function() {
                    this.resizeDebounceTimeout = null;
                    this.processEvtOnResize();
                }.bind(this), this.c.DEBOUNCE_TO_RESIZE);
            }.bind(this);
            applyLineHeight(self);
            this.cleanupTabContent();
            this.cleanupAsyncProcessing();
            this.clearSelection();
            this.clearFilterData();
            this.clearColTextFilterData();
            this.isTabSizeChanged(); // init last..
            this.isLayoutChanged(); // init last..

            shmi.log("[ComplexTable2] initialized", 1);

            self = this;
            var tabContCtrls;
            if (shmi.isRegistered(this.element, this.uiType)) {
                tabContCtrls = shmi.parseControls(this.tcElement, false, this.element);
                for (i = 0; i < tabContCtrls.length; i++) {
                    self.controls.push(tabContCtrls[i]);
                }
            } else {
                var l_id = this.listen('register', function() {
                    tabContCtrls = shmi.parseControls(self.tcElement, false, self.element);
                    for (i = 0; i < tabContCtrls.length; i++) {
                        self.controls.push(tabContCtrls[i]);
                    }
                    self.unlisten('register', l_id);
                });
            }

            self.adapterItems = [];
            var iterObj = shmi.requires("visuals.tools.iterate.iterateObject");
            iterObj(self.config.adapterSettings, function(val, prop) {
                var adapterEntry = {
                    field: prop,
                    item: null,
                    gridName: self.config.table,
                    options: val
                };
                //function(name, type, min, max, value, setvalueCallback)
                var iName = "virtual:" + self.getAdapterNS() + ":" + prop;
                adapterEntry.item = shmi.createVirtualItem(iName, shmi.c("TYPE_INT"), 0, 1000, 0, self._getAdapterHandler(self, adapterEntry));
                self.adapterItems.push(adapterEntry);
            });
        },
        _getAdapterHandler: function(self, adapterEntry) {
            return function(value) {
                var adapter = null,
                    im = shmi.visuals.session.ItemManager,
                    ia = shmi.requires("visuals.tools.item-adapter"),
                    filterItems = [],
                    iterObj = shmi.requires("visuals.tools.iterate.iterateObject"),
                    fieldIdx = self.getDataColNo(adapterEntry.field);

                adapterEntry.options.forEach(function(option, idx) {
                    if (option.value === value) {
                        adapter = option.adapter;
                    }
                });

                iterObj(im.items, function(itm, itmName) {
                    if (itmName.indexOf("virtual:grid:" + adapterEntry.gridName) === 0) {
                        var nameParts = itmName.split(":");
                        if (parseInt(nameParts[4]) === fieldIdx) {
                            filterItems.push(itm);
                        }
                    }
                });

                if (adapter === null) {
                    filterItems.forEach(function(itm, idx) {
                        ia.unsetAdapter(itm.name);
                    });
                } else {
                    filterItems.forEach(function(itm, idx) {
                        ia.setAdapter(itm.name, adapter);
                    });
                }
            };
        },
        onEnable: function() {
            const self = this;
            let grid = null,
                sizesCalculated = false;

            this.enableEvents();
            this.updateRespDepContent(true);
            this.cleanupMetrics();

            if (this.cConfig.table) {
                grid = this.dgMan.getGrid(this.cConfig.table);
                if (grid) {
                    grid.clearFilter(-1);

                    if (self.cConfig.quicksearch && self.cConfig.quicksearch.enable) {
                        if (self.cConfig.quicksearch.remember) {
                            if (typeof self.qSearchOld === "string") {
                                self.ctrlPanelData.qSearchInput.setValue(self.qSearchOld);
                                self.quicksearchChanged(self.qSearchOld);
                            }
                        }
                    }
                }
            }

            this.setApplyFiltersEnable(true);
            if (this.cConfig.table && this.dgMan.getGrid(this.cConfig.table)) {
                startLoading(self);
                this.dgSubscription = this.dgMan.subscribePage(
                    this.cConfig.table, 0, this.cConfig["buffer-size"],
                    (dgChgInfo) => {
                        stopLoading(self);
                        if (this.testlog) console.log("***** datagridChange", dgChgInfo);
                        this.dgSubscription.changeInfo = dgChgInfo;
                        this.onDataGridChange(dgChgInfo);
                        if (!sizesCalculated) {
                            sizesCalculated = true;
                            self.processEvtOnResize();
                        }
                    });
                this.checkDgIndexFields();
                if (this.cConfig["item-selected-row"] && (this.cConfig["item-selected-row"] !== "undefined-item")) {
                    this.selRowByItem = 0; // at startup always 0
                    this.subscrIdItemSelRow = shmi.visuals.session.ItemManager.subscribeItem(this.cConfig["item-selected-row"], this);
                }
            }

            window.addEventListener("resize", this.updateSize, true);
            window.addEventListener("visuals-layout-change", this.updateSize, true);

            this.controls.forEach(function(ctrl) {
                ctrl.enable();
            });

            shmi.log("[ComplexTable2] enabled", 1);
        },
        /**
         * resetQuickSearch - reset quick search input
         *
         */
        resetQuickSearch: function() {
            var self = this;
            if (self.cConfig.quicksearch && self.cConfig.quicksearch.enable) {
                self.qSearchOld = null;
                if (self.isActive()) {
                    self.ctrlPanelData.qSearchInput.setValue("");
                    self.quicksearchChanged("");
                }
            }
        },
        /**
         * setQuickSearch - set quick search input
         *
         */
        setQuickSearch: function(value) {
            var self = this,
                setValue = value;

            if (self.cConfig.quicksearch && self.cConfig.quicksearch.enable) {
                if (setValue === undefined || setValue === null) {
                    setValue = "";
                } else if (typeof setValue !== "string") {
                    setValue = String(setValue);
                }
                if (self.isActive() && self.qSearchOld !== setValue) {
                    self.ctrlPanelData.qSearchInput.setValue(setValue);
                    self.quicksearchChanged(setValue);
                }
            }
        },
        onDisable: function() {
            if (this.pageBuf.rows) {
                this.pageBuf.rows.forEach(function(row, idx) {
                    if (row.trSelCtrl && row.trSelCtrl.rsMouseListener) {
                        row.trSelCtrl.rsMouseListener.disable();
                        row.trSelCtrl.rsTouchListener.disable();
                    }
                    if (row.selCB && row.selCB.cbMouseListener) {
                        row.selCB.cbMouseListener.disable();
                        row.selCB.cbTouchListener.disable();
                    }
                });
            }

            this.cleanupAsyncProcessing();
            if (this.dgSubscription) {
                this.dgMan.unsubscribe(this.cConfig.table, this.dgSubscription.id);
                this.dgSubscription = null;
            }
            this.disableEvents();
            this.cleanupTabContent();
            if (this.subscrIdItemSelRow) {
                shmi.visuals.session.ItemManager.unsubscribeItem(this.cConfig["item-selected-row"], this.subscrIdItemSelRow);
            }

            if (this.quicksearchHandler && this.quicksearchValueElement) {
                this.quicksearchValueElement.removeEventListener('keyup', this.quicksearchHandler);
                if (this.vars.qSearchToken) {
                    this.vars.qSearchToken.unlisten();
                }
                this.quicksearchValueElement = null;
                this.quicksearchHandler = null;
            }

            if (!(this.cConfig.quicksearch && this.cConfig.quicksearch.enable && this.cConfig.quicksearch.remember)) {
                this.qSearchOld = null;
            }

            this.selection = {
                type: this.c.SELECT_TYPE_NOTHING,
                selRows: [],
                selRowIndex: []
            };
            this.lastSelection = null;
            setLastClicked(this, -1);

            window.removeEventListener("resize", this.updateSize, true);
            window.removeEventListener("visuals-layout-change", this.updateSize, true);
            shmi.log("[ComplexTable2] disabled", 1);
        },
        onDelete: function() {
            var self = this;
            if (self.header.cols) {
                self.header.cols.forEach(function(col, idx) {
                    if (col.ctrls[0].cbMouseListener) {
                        col.ctrls[0].cbMouseListener.disable();
                        col.ctrls[0].cbTouchListener.disable();
                    }
                });
            }
            if (self.ctrlPanelData.filter) {
                self.ctrlPanelData.filter.items.forEach(function(item, idx) {
                    shmi.visuals.session.ItemManager.removeItem(item.name);
                });
            }
            if (self.onBodyScroll && self.bodyViewportElement) {
                self.bodyViewportElement.removeEventListener('scroll', self.onBodyScroll);
            }
            releaseTableId(self, self.tableId);
            self.element.removeAttribute("id");
        },

        /*-- DataGrid callback method interface --*/

        onDataGridChange: function(dgChgInfo) {
            var self = this;
            if (dgChgInfo && (dgChgInfo.status === "OK")) {
                if (self.isTableToCleanup) {
                    self.cleanupTabContent();
                    self.isTableToCleanup = false;
                }
                var currDgIds = self.dgMan.getCurrentIDs(self.cConfig.table, self.dgSubscription.id);

                if (self.pageBuf.rows.length === self.pageBuf.viewportSize) {
                    if ((currDgIds.length <= self.pageBuf.bufferSize) && (dgChgInfo.offset === self.pageBuf.bufferOffset)) {
                        self.pageBuf.size = currDgIds.length;
                        if (self.pageBuf.nofTotalRows !== dgChgInfo.totalRows) {
                            self.pageBuf.nofTotalRows = dgChgInfo.totalRows;
                            self.tableElement.style.height = (self.pageBuf.nofTotalRows * self.metricsData.lineHeightPx) + "px";
                        }
                        self.updateTable(dgChgInfo, currDgIds); // change item refs only, reconnect
                    } else {
                        self.cleanupTabContent();
                        self.reinitTable(dgChgInfo, currDgIds, true);
                    }
                } else {
                    self.cleanupTabContent();
                    self.reinitTable(dgChgInfo, currDgIds);
                }
                self.findSelectionAfterRefresh();
                self.updateNofRowsDisplay();
                self.updateSelectionView();
            }
        },

        // callback interface for optional items
        onSetValue: function(value, type, name) {
            var rowVal = -1;

            if (name === this.cConfig["item-selected-row"]) {
                if (value !== -1) {
                    if (this.cConfig["rows-items-from-1"]) {
                        rowVal = value - 1;
                    } else {
                        rowVal = value;
                    }
                }
                this.selRowByItem = rowVal;
                this.updateSelectionView();
            }
        },

        /*-- private helper methods - usage within CT2 only !! --*/

        /* config processors */
        /* note: use this.cConfig instead of this.Config only! Some properties are pre-processed for
                 a faster access at runtime. this.cConfig is divided in a responsive-independent and responsive-
                 dependent part, the last one may be changed at runtime!!
          */
        /**
         * @private
         */
        buildCCfgRespIndep: function() {
            this.cConfig.name = this.config.name;
            this.cConfig.label = this.config.label;
            this.cConfig["class-name"] = this.config["class-name"];
            this.cConfig.template = this.config.template;
            this.cConfig.table = this.config.table;
            this.cConfig["item-selected-row"] = this.config["item-selected-row"];
            this.cConfig["rows-items-from-1"] = this.config["rows-items-from-1"];
            if (this.config["field-datagrid-col-map"]) {
                this.cConfig["field-datagrid-col-map"] = this.config["field-datagrid-col-map"];
            } else {
                this.cConfig["field-datagrid-col-map"] = null;
            }
            this.cConfig["delete-selected-rows"] = shmi.toBoolean(this.config["delete-selected-rows"]);
            this.cConfig.filters = this.config.filters;
            this.cConfig.quicksearch = this.config.quicksearch;
            this.cConfig["nof-buffered-rows"] = shmi.toNumber(this.config["default-nof-buffered-rows"]);
            if (this.config["buffer-size"] && this.config["buffer-size"] > this.c.MIN_BUF_SIZE) {
                this.cConfig["buffer-size"] = this.config["buffer-size"];
            } else {
                this.cConfig["buffer-size"] = this.c.MIN_BUF_SIZE;
            }

            if (this.config["delete-selected-rows-button-config"]) {
                this.cConfig["delete-selected-rows-button-config"] = this.config["delete-selected-rows-button-config"];
            }
            if (this.config["default-filter-button-config"]) {
                this.cConfig["default-filter-button-config"] = this.config["default-filter-button-config"];
            }

            if (this.config["browser-dependencies"]) {
                var browserDeps = this.config["browser-dependencies"],
                    userAgent = navigator.userAgent;
                for (var i = 0; i < browserDeps.length; i++) {
                    var regEx = new RegExp(browserDeps[i].userAgentDetectRegEx);
                    if (regEx.test(userAgent)) {
                        if (browserDeps[i]["nof-buffered-rows"]) {
                            this.cConfig["nof-buffered-rows"] = shmi.toNumber(browserDeps[i]["nof-buffered-rows"]);
                        }
                        break;
                    }
                }
            }

            this.cConfig["show-buttons-table-min-width-px"] = shmi.toNumber(this.config["show-buttons-table-min-width-px"]);

            // nof-buffered-rows must be even
            var bufSize = this.cConfig["nof-buffered-rows"],
                sizeTemp = 0;
            if ((bufSize % 3) !== 0) {
                sizeTemp = Math.ceil(bufSize / 3) * 3;
                this.cConfig["nof-buffered-rows"] = sizeTemp;
                if (this.cConfig["nof-buffered-rows"] > this.cConfig["buffer-size"]) {
                    this.cConfig["buffer-size"] = this.cConfig["nof-buffered-rows"];
                }
                console.info("[ComplexTable2] nof-buffered-rows must be a multiple of 3, has been corrected to " + sizeTemp);
            }
        },
        /**
         * @private
         */
        buildCCfgRespDep: function() {
            const iterObj = shmi.requires("visuals.tools.iterate.iterateObject");
            var i;
            // init the default resp config
            this.cConfig.resp = {};
            this.cConfig.resp["v-scroll-options"] = this.config["v-scroll-options"];
            this.cConfig.resp["h-scroll-options"] = this.config["h-scroll-options"];
            if (this.cConfig["item-selected-row"] && (this.cConfig["item-selected-row"] !== "undefined-item")) {
                this.cConfig.resp["select-mode"] = "ITEM";
            } else {
                this.cConfig.resp["select-mode"] = this.config["select-mode"];
            }
            this.cConfig.resp["text-mode"] = this.config["text-mode"];
            this.cConfig.resp["show-nof-rows"] = this.config["show-nof-rows"];
            this.cConfig.resp["col-width-changeable"] = this.config["col-width-changeable"];
            this.cConfig.resp["cols-show-hide"] = this.config["cols-show-hide"];
            this.cConfig.resp["cols-movable"] = this.config["cols-movable"];
            this.cConfig.resp["rows-movable"] = this.config["rows-movable"];
            this.cConfig.resp["sortable-fields"] = this.config["sortable-fields"];
            this.cConfig.resp["text-filter-fields"] = this.config["default-text-filter-fields"];
            if (this.cConfig.quicksearch) {
                if (this.cConfig.quicksearch.enable) {
                    this.cConfig.resp["text-filter-fields"] = this.cConfig.quicksearch.fields;
                }
            }
            if (this.config["default-text-filter-input-field-config"]) {
                this.cConfig.resp["text-filter-input-field-config"] = this.config["default-text-filter-input-field-config"];
            }
            if (this.config["default-text-filter-clear-button-config"]) {
                this.cConfig.resp["text-filter-clear-button-config"] = this.config["default-text-filter-clear-button-config"];
            }
            //--
            var defLayout = this.config["default-layout"];
            if (defLayout["class-name"]) {
                this.cConfig.resp["layout-class-name"] = defLayout["class-name"];
            } else {
                this.cConfig.resp["layout-class-name"] = null;
            }

            if (defLayout["line-height"]) {
                this.cConfig.resp["line-height"] = defLayout["line-height"];
            } else {
                this.cConfig.resp["line-height"] = "40px";
            }
            // example of this.cConfig.resp["column-org"]
            //[
            //    {
            //        "fields": [
            //            { "dgCol": 0, "header": "headCol1Fld0", "isSortable": true, "hasTextFilter": true, "ctrl":
            // { "ui-type": "text2", "config": { "config-name": "ct2-text2" } } },
            //            { "dgCol": 2, "header": "headCol1Fld1", "isSortable": false, "hasTextFilter": false, "ctrl":
            // { "ui-type": "text2", "config": { "config-name": "ct2-text2" } } }
            //        ],
            //        "hasTextFilter": true // is true if at least one field of the ct2 column has a text filter
            //    },
            //    {
            //        "fields": [
            //            { "dgCol": 5, "header": "headCol2Fld0", "isSortable": true, "hasTextFilter": false, "ctrl":
            // { "ui-type": "text2", "config": { "config-name": "ct2-text2" } } }
            //        ],
            //        "hasTextFilter": false // is true if at least one field of the ct2 column has a text filter
            //    }
            //]
            this.cConfig.resp["column-org"] = [];
            var cfgSrcColOrg = defLayout["column-org"],
                cCfgDestColOrg = this.cConfig.resp["column-org"],
                nofSrcCols = 0,
                colNo;

            iterObj(cfgSrcColOrg, (srcCol, col) => {
                // assume that col is of format col<number>, e.g. "col1", "col17"
                if (col.substr(0, 3).toLowerCase() === "col") {
                    colNo = parseInt(col.substring(3)) - 1; // 0..nofCols-1
                    const destCol = cCfgDestColOrg[colNo] = {},
                        srcFields = srcCol.fields,
                        nofFields = srcFields.length;
                    let colHasTextFilter = false;

                    destCol.fields = [];

                    for (i = 0; i < nofFields; i++) {
                        destCol.fields[i] = {};

                        destCol.fields[i]["min-width"] = srcCol["min-width"];
                        destCol.fields[i]["column-width"] = srcCol["column-width"];
                        destCol.fields[i].fieldName = srcFields[i];
                        destCol.fields[i].dgCol = this.getDataColNo(srcFields[i]);
                        destCol.fields[i].header = this.getHeaderCaption(srcFields[i]);
                        destCol.fields[i].icon = this.config.fieldIcons[srcFields[i]];
                        destCol.fields[i].isSortable = this.getFldSortable(srcFields[i]);
                        destCol.fields[i].hasTextFilter = this.getFldHasTextFilter(srcFields[i]);
                        if (destCol.fields[i].hasTextFilter) {
                            colHasTextFilter = true;
                        }
                        destCol.fields[i].ctrl = this.getCtrlDef(srcFields[i]);
                    }
                    destCol.hasTextFilter = colHasTextFilter;
                    nofSrcCols++;
                }
            });

            var nofCols = cCfgDestColOrg.length;
            this.cConfig.resp["nof-cols"] = nofCols;
            if (nofCols !== nofSrcCols) {
                shmi.log("[ComplexTable2] buildCCfgRespDep - error in default-layout: there must be defined col1 till coln without gaps and duplicates", 3);
            }
            this.cConfig.resp["layout-id"] = -1; // default layout

            // overwrite the default resp config with "responsive-layouts" settings if necessary
            if (this.config["responsive-layouts"]) {
                var mLayout = null,
                    cWidth = this.getTableWidth(),
                    respLayouts = this.config["responsive-layouts"],
                    nofRespLayouts = respLayouts.length;
                for (i = 0; i < nofRespLayouts; i++) {
                    if (cWidth <= respLayouts[i]["table-max-width-px"]) {
                        mLayout = respLayouts[i];
                        this.cConfig.resp["layout-id"] = i;
                        break;
                    }
                }
                if (mLayout) {
                    if (mLayout.headerMode === "ICON") {
                        //self.headerMode = "ICON";
                        this.cConfig.resp.headerMode = "ICON";
                    } else {
                        //self.headerMode = "TEXT";
                        this.cConfig.resp.headerMode = "TEXT";
                    }

                    if (mLayout["line-height"]) {
                        this.cConfig.resp["line-height"] = mLayout["line-height"];
                    }

                    if (mLayout["v-scroll-options"] !== undefined) {
                        this.cConfig.resp["v-scroll-options"] = mLayout["v-scroll-options"];
                    }
                    if (mLayout["h-scroll-options"] !== undefined) {
                        this.cConfig.resp["h-scroll-options"] = mLayout["h-scroll-options"];
                    }
                    if (mLayout["select-mode"] !== undefined) {
                        this.cConfig.resp["select-mode"] = mLayout["select-mode"];
                    }
                    if (mLayout["text-mode"] !== undefined) {
                        this.cConfig.resp["text-mode"] = mLayout["text-mode"];
                    }
                    if (mLayout["show-nof-rows"] !== undefined) {
                        this.cConfig.resp["show-nof-rows"] = mLayout["show-nof-rows"];
                    }
                    if (mLayout["col-width-changeable"] !== undefined) {
                        this.cConfig.resp["col-width-changeable"] = mLayout["col-width-changeable"];
                    }
                    if (mLayout["cols-show-hide"] !== undefined) {
                        this.cConfig.resp["cols-show-hide"] = mLayout["cols-show-hide"];
                    }
                    if (mLayout["cols-movable"] !== undefined) {
                        this.cConfig.resp["cols-movable"] = mLayout["cols-movable"];
                    }
                    if (mLayout["rows-movable"] !== undefined) {
                        this.cConfig.resp["rows-movable"] = mLayout["rows-movable"];
                    }
                    if (mLayout["sortable-fields"] !== undefined) {
                        this.cConfig.resp["sortable-fields"] = mLayout["sortable-fields"];
                    }
                    if (mLayout["class-name"] !== undefined) {
                        this.cConfig.resp["layout-class-name"] = mLayout["class-name"];
                    }
                    if (mLayout["text-filter-fields"] !== undefined) {
                        this.cConfig.resp["text-filter-fields"] = mLayout["text-filter-fields"];
                    }
                    if (mLayout["text-filter-input-field-config"] !== undefined) {
                        this.cConfig.resp["text-filter-input-field-config"] = mLayout["text-filter-input-field-config"];
                    }
                    if (mLayout["text-filter-clear-button-config"] !== undefined) {
                        this.cConfig.resp["text-filter-clear-button-config"] = mLayout["text-filter-input-field-config"];
                    }

                    cfgSrcColOrg = mLayout["column-org"];
                    cCfgDestColOrg = this.cConfig.resp["column-org"];

                    iterObj(cfgSrcColOrg, (srcCol, col) => {
                        // assume that col is of format col<number>, e.g. "col1", "col17"
                        if (col.substr(0, 3).toLowerCase() === "col") {
                            colNo = parseInt(col.substring(3)) - 1; // 0..nofCols-1
                            srcCol = cfgSrcColOrg[col];
                            if ((typeof srcCol === "string") || (srcCol instanceof String)) {
                                // copy src colNo to dest colNo
                                var srcColNo = parseInt(srcCol.substring(3)) - 1; // 0..nofCols-1
                                cCfgDestColOrg[colNo] = cCfgDestColOrg[srcColNo];
                            } else {
                                const destCol = cCfgDestColOrg[colNo] = {},
                                    srcFields = srcCol.fields,
                                    nofFields = srcFields.length;
                                let colHasTextFilter = false;

                                destCol.fields = [];

                                for (i = 0; i < nofFields; i++) {
                                    destCol.fields[i] = {};
                                    destCol.fields[i].fieldName = srcFields[i];
                                    destCol.fields[i].dgCol = this.getDataColNo(srcFields[i]);
                                    var respHeaders = null;
                                    if (srcCol["field-headers"]) {
                                        respHeaders = srcCol["field-headers"];
                                    }
                                    destCol.fields[i].header = this.getHeaderCaption(srcFields[i], respHeaders);
                                    destCol.fields[i].icon = this.config.fieldIcons[srcFields[i]];
                                    destCol.fields[i].isSortable = this.getFldSortable(srcFields[i]);
                                    destCol.fields[i].hasTextFilter = this.getFldHasTextFilter(srcFields[i]);
                                    if (destCol.fields[i].hasTextFilter) {
                                        colHasTextFilter = true;
                                    }
                                    var respColCtrlMap = null;
                                    if (srcCol["field-control-map"]) {
                                        respColCtrlMap = srcCol["field-control-map"];
                                    }
                                    destCol.fields[i].ctrl = this.getCtrlDef(srcFields[i], respColCtrlMap);
                                }
                                destCol.hasTextFilter = colHasTextFilter;
                            }
                        }
                    });

                    nofCols = colNo + 1;
                    if (nofCols < cCfgDestColOrg.length) {
                        // cleanup col defs for col >= nofCols
                        cCfgDestColOrg.splice(nofCols, cCfgDestColOrg.length - nofCols);
                        this.cConfig.resp["nof-cols"] = nofCols;
                    } else if (nofCols > this.cConfig.resp["nof-cols"]) {
                        shmi.log("[ComplexTable2] buildCCfgRespDep, it is impossible to extent the number of columns in responsive-layouts", 3);
                    }
                }
            }
        },
        /**
         * @private
         */
        checkDgIndexFields: function() {
            var grid = this.dgMan.getGrid(this.cConfig.table);
            if (grid && (typeof grid.getIndexFields === "function")) {
                this.cConfig.indexFields = grid.getIndexFields();
                if (this.cConfig.indexFields.length > 0) {
                    this.cConfig.useDgIndex = true;
                }
            } else {
                shmi.log("[ComplexTable2] No index found in datagrid", 1);
            }
        },
        /**
         * @private
         */
        getDataColNo: function(fldName) {
            var colNo = this.cConfig["field-datagrid-col-map"][fldName];
            if (colNo === undefined) {
                colNo = 0;
                shmi.log("[ComplexTable2] getDataColNo, unknown fldName " + fldName, 3);
            } else {
                colNo = shmi.toNumber(colNo);
            }
            return colNo;
        },
        /**
         * @private
         */
        getFldSortable: function(fldName) {
            var sortable = false;
            if (this.cConfig.resp["sortable-fields"].indexOf(fldName) !== -1) {
                sortable = true;
            }
            return sortable;
        },
        /**
         * @private
         *
         * @param {string} fldName
         * @returns {boolean} has text filter true/false
         */
        getFldHasTextFilter: function(fldName) {
            var hasTxtFilter = false;
            if (this.cConfig.resp["text-filter-fields"].indexOf(fldName) !== -1) {
                hasTxtFilter = true;
            }
            return hasTxtFilter;
        },
        /**
         * @private
         *
         * @param {string} fldName
         * @param {Object} [ctrlMap] may be null. If defined it's checked for fldName at 1st.
         * @returns {Object}
         */
        getCtrlDef: function(fldName, ctrlMap) {
            var ctrlDef;
            if (ctrlMap) {
                ctrlDef = ctrlMap[fldName];
                if (!ctrlDef) {
                    ctrlDef = this.config["default-field-control-map"][fldName];
                }
            } else {
                ctrlDef = this.config["default-field-control-map"][fldName];
            }
            if (!ctrlDef) {
                ctrlDef = {};
                shmi.log("[ComplexTable2] getCtrlDef, unknown fldName " + fldName, 3);
            }
            return ctrlDef;
        },
        /**
         * @private
         *
         * @param {string} fldName
         * @param {Object} [headerMap] may be null. If defined it's checked for fldName at 1st.
         * @returns {string} caption, may be null
         */
        getHeaderCaption: function(fldName, headerMap) {
            var caption;
            if (headerMap) {
                caption = headerMap[fldName];
                if (caption === null) {
                    return null; // legal case: e.g. the default caption is overwritten by null in a responsive layout
                } else if (!caption) {
                    caption = this.config["default-field-headers"][fldName];
                }
            } else {
                caption = this.config["default-field-headers"][fldName];
            }
            if (!caption) {
                caption = "";
                shmi.log("[ComplexTable2] getHeaderCaption, unknown fldName " + fldName, 3);
            }
            return caption;
        },
        /*
         * @private
         */
        getColumnAdapter: function(fldName) {
            var self = this,
                adapter = null;
            if (self.config.adapterSettings && (self.config.adapterSettings[fldName] !== undefined)) {
                /* {
                    field: prop,
                    item: null,
                    gridName: self.config.table,
                    options: val
                } */
                var settings = self.config.adapterSettings[fldName],
                    curVal = null;
                self.adapterItems.forEach(function(aEntry, idx) {
                    if (aEntry.field === fldName) {
                        curVal = aEntry.item.readValue();
                    }
                });
                settings.forEach(function(entry, idx) {
                    if (curVal === entry.value) {
                        adapter = entry.adapter;
                    }
                });
            }
            return adapter;
        },
        // *ConfigVal may be null or undefined
        // returns null if both keys do not exist in config
        /**
         * @private
         */
        getChildCtrlConfig: function(primConfigVal, secConfigVal) {
            var cfg = null;
            if (primConfigVal) {
                cfg = shmi.cloneObject(primConfigVal);
            } else if (secConfigVal) {
                cfg = shmi.cloneObject(secConfigVal);
            }
            return cfg;
        },
        /**
         * @private
         */
        processAsyncMetricsUpdate: function(scrollTop) {
            var self = this,
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            self.controls.forEach(function(ctrl, idx) {
                if (!ctrl.isActive()) {
                    var t = tm.createTask("table-control-#" + (idx + 1));
                    t.run = function() {
                        var enableTok = ctrl.listen("enable", function(evt) {
                            enableTok.unlisten();
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            tl = tm.createTaskList(tasks, false);

            tl.onComplete = function() {
                var tHeight = 0;
                if (self.active) {
                    self.updateMetricsData(scrollTop);
                    self.processTabReinitDependencies();

                    if (typeof scrollTop === "number") {
                        tHeight = self.tableElement.getBoundingClientRect().height;
                        if (tHeight >= scrollTop + self.metricsData.viewportHeight) {
                            self.bodyViewportElement.scrollTop = scrollTop;
                        } else {
                            self.bodyViewportElement.scrollTop = Math.max(0, tHeight - self.metricsData.viewportHeight);
                        }
                    }
                }
            };

            if (tasks.length === 0) {
                tl.onComplete();
            } else {
                tl.run();
            }
        },
        /**
         * @private
         */
        processEvtOnResize: function() {
            var self = this;
            if (this.initialized) {
                this.buildCCfgRespDep();
                applyLineHeight(self);
                if (this.isTabSizeChanged()) {
                    // do the same table processing as onInit -> onEnable
                    this.currentScrollEvtType = this.c.SCROLL_EVT_TYPE_RESIZE;
                    this.cleanupTabContent();
                    this.cleanupAsyncProcessing();
                    this.findSelectionAfterRefresh();
                    this.clearFilterData();
                    this.clearColTextFilterData();
                    this.updateRespDepContent(true);
                    this.cleanupMetrics();
                    // force onDataGridChange callback in every case:
                    if (this.dgSubscription && this.dgSubscription.changeInfo) {
                        self.cleanupTabContent();
                        self.reinitTable(this.dgSubscription.changeInfo, self.dgMan.getCurrentIDs(self.cConfig.table, self.dgSubscription.id));
                    }
                }
                //}
            }
        },
        /* select processing */
        /**
         * @private
         */
        clearSelection: function() {
            var self = this;
            this.selection.type = this.c.SELECT_TYPE_NOTHING; // || .._ALL || .._SEL_ROWS
            this.selection.selRows = [];
            this.selection.selRowIndex = [];
            this.updateDelRowsBtnEnable();

            if (self.editRow !== -1) {
                self.resetBufferRow(self.editRow);
                self.editRow = -1;
            }
            clearTimeout(self.sel_fire_to);
            self.sel_fire_to = setTimeout(function() {
                self.fireSelChanged(shmi.cloneObject(self.selection));
            }, shmi.c("ACTION_RETRY_TIMEOUT"));
        },
        /**
         * @private
         */
        findSelectionAfterRefresh: function() {
            const self = this,
                grid = self.dgMan.getGrid(self.cConfig.table);

            if (grid && this.cConfig.useDgIndex) {
                if (this.selection.type === this.c.SELECT_TYPE_SEL_ROWS) {
                    const selectionClone = shmi.cloneObject(this.selection);

                    selectionClone.selRowIndex.forEach(function(el, idx) {
                        const newRow = grid.searchIndexRowId(el);
                        if (newRow === -1) {
                            self.removeRowIdFromSelection(selectionClone.selRows[idx]);
                        } else if (newRow !== selectionClone.selRows[idx]) {
                            const currentSelectionIndex = self.selection.selRows.indexOf(selectionClone.selRows[idx]);
                            if (currentSelectionIndex > -1) {
                                self.selection.selRows[currentSelectionIndex] = newRow;
                            }
                        }
                    });
                }

                if (this.selection && this.selection.selRows.length > 0) {
                    this.selection.type = this.c.SELECT_TYPE_SEL_ROWS;
                } else {
                    this.selection.type = this.c.SELECT_TYPE_NOTHING;
                }

                this.updateDelRowsBtnEnable();
                if (self.editRow !== -1) {
                    self.resetBufferRow(self.editRow);
                    self.editRow = -1;
                }
                clearTimeout(self.sel_fire_to);
                self.sel_fire_to = setTimeout(function() {
                    self.fireSelChanged(shmi.cloneObject(self.selection));
                }, shmi.c("ACTION_RETRY_TIMEOUT"));
            } else {
                this.clearSelection();
            }
        },
        /**
         * @private
         */
        updateSelectionView: function() {
            if (this.testlog) console.log("*** update selection view", this.selection.selRows, this.selection.selRowIndex.toString());
            var isMultiSelectMode = (this.cConfig.resp["select-mode"] === "MULTI"),
                isSingleSelectMode = (this.cConfig.resp["select-mode"] === "SINGLE"),
                isItemSelectMode = (this.cConfig.resp["select-mode"] === "ITEM");
            if (isItemSelectMode) {
                this.setAllRowSelectors(this.c.CB_UNCHK);
                this.setRowSelectorByDgRow(this.selRowByItem, this.c.CB_CHK);
            } else {
                switch (this.selection.type) {
                case this.c.SELECT_TYPE_NOTHING:
                    if (isMultiSelectMode) {
                        this.setAllCheckBoxes(this.c.CB_UNCHK);
                        this.setAllRowSelectors(this.c.CB_UNCHK);
                    } else if (isSingleSelectMode) {
                        this.setAllRowSelectors(this.c.CB_UNCHK);
                    }
                    break;
                case this.c.SELECT_TYPE_ALL:
                    if (isMultiSelectMode) {
                        this.setAllCheckBoxes(this.c.CB_CHK);
                        this.setAllRowSelectors(this.c.CB_CHK);
                    } else {
                        shmi.log("[ComplexTable2] updateSelectionView, it is impossible to select all rows in single select mode", 2);
                    }
                    break;
                default: // .._SEL_ROWS
                    if (isMultiSelectMode) {
                        this.setAllCheckBoxes(this.c.CB_UNCHK);
                        this.setAllRowSelectors(this.c.CB_UNCHK);
                        if (this.header.selAllCBox) {
                            this.header.selAllCBox.setState(this.c.CB_UNDEF);
                        }
                        var currDgIds = this.dgMan.getCurrentIDs(this.cConfig.table, this.dgSubscription.id);
                        for (var i = 0; i < this.selection.selRows.length; i++) {
                            if (currDgIds.indexOf(this.selection.selRows[i]) !== -1) {
                                this.setCheckBox(this.selection.selRows[i], this.c.CB_CHK);
                                this.setRowSelector(this.selection.selRows[i], this.c.CB_CHK);
                            }
                        }
                    } else if (isSingleSelectMode) {
                        this.setAllRowSelectors(this.c.CB_UNCHK);
                        /*if (this.selection.selRows.length > 1) {
                            shmi.log("[ComplexTable2] updateSelectionView, it is impossible to select more than one row in single select mode", 2);
                        }*/
                        /*if (this.selection.selRows.length === 1) {
                            this.setRowSelector(this.selection.selRows[0], this.c.CB_CHK);
                        }*/
                        this.selection.selRows.forEach(function(el) {
                            this.setRowSelector(el, this.c.CB_CHK);
                        }.bind(this));
                    }
                    break;
                }
            }
        },
        /**
         * @private
         */
        setAllCheckBoxes: function(selVal) {
            var self = this;
            if (self.header.selAllCBox) {
                self.header.selAllCBox.setState(selVal);
            }
            self.pageBuf.rows.forEach(function(r) {
                if (r.selCB) {
                    r.selCB.setState(selVal);
                }
            });
        },
        /**
         * @private
         */
        setCheckBox: function(rowId, selVal) {
            var bufRow = this.searchRowId(rowId);
            if (bufRow !== -1) {
                if (this.pageBuf.rows[bufRow] && this.pageBuf.rows[bufRow].selCB) {
                    this.pageBuf.rows[bufRow].selCB.setState(selVal);
                }
            }
        },
        /**
         * @private
         */
        setAllRowSelectors: function(selVal) {
            var self = this;
            self.pageBuf.rows.forEach(function(r) {
                if (r.trSelCtrl) {
                    r.trSelCtrl.setState(selVal);
                }
            });
        },
        /**
         * @private
         */
        setRowSelector: function(rowId, selVal) {
            var self = this,
                bufRow = this.searchRowId(rowId);
            if (bufRow !== -1) {
                if (this.pageBuf.rows[bufRow] && this.pageBuf.rows[bufRow].trSelCtrl) {
                    this.pageBuf.rows[bufRow].trSelCtrl.setState(selVal);
                    if (self.editMode) {
                        var cc = shmi.requires("visuals.tools.control-config");
                        if (self.editRow !== -1) {
                            self.resetBufferRow(self.editRow);
                        }
                        self.editRow = bufRow;
                        this.pageBuf.rows[bufRow].cols.forEach(function(colEntry, idx) {
                            colEntry.ctrls.forEach(function(ctrl, jdx) {
                                var cfgCol = self.cConfig.resp["column-org"][idx],
                                    cConf = cc.getConfig(ctrl),
                                    iterObj = shmi.requires("visuals.tools.iterate.iterateObject");
                                if (self.config['edit-mode-map'][cfgCol.fields[jdx].fieldName] !== undefined) {
                                    var editCfg = shmi.cloneObject(self.config['edit-mode-map'][cfgCol.fields[jdx].fieldName]);
                                    iterObj(editCfg, function(val, prop) {
                                        cConf[prop] = val;
                                    });
                                    var lTok = shmi.listen('control-reconfiguration', function(evt) {
                                        lTok.unlisten();
                                        colEntry.ctrls[jdx] = evt.detail.control;
                                    }, {
                                        "detail.oldControl": ctrl
                                    });
                                    cc.setConfig(ctrl, cConf);
                                }
                            });
                        });
                        self.updateSize();
                    }
                }
            }
        },
        /**
         * @private
         */
        resetBufferRow: function(bufRow) {
            var self = this,
                cc = shmi.requires("visuals.tools.control-config");
            if (self.pageBuf.rows[bufRow] === undefined) {
                self.editRow = -1;
                return;
            }
            self.pageBuf.rows[bufRow].cols.forEach(function(colEntry, idx) {
                var cfgCol = self.cConfig.resp["column-org"][idx];
                colEntry.ctrls.forEach(function(ctrl, jdx) {
                    var oldCfg = cc.getConfig(ctrl),
                        cfg = shmi.cloneObject(cfgCol.fields[jdx].ctrl.config);
                    cfg.item = oldCfg.item;
                    cfg.ui = cfgCol.fields[jdx].ctrl["ui-type"];
                    shmi.removeClass(ctrl.element, oldCfg["class-name"]);
                    var lTok = shmi.listen('control-reconfiguration', function(evt) {
                        lTok.unlisten();
                        colEntry.ctrls[jdx] = evt.detail.control;
                    }, {
                        "detail.oldControl": ctrl
                    });
                    cc.setConfig(ctrl, cfg);
                });
            });
        },
        /**
         * @private
         */
        setRowSelectorByDgRow: function(dgRow, selVal) {
            var bufRow = dgRow - this.pageBuf.offset;
            if ((bufRow >= 0) && (bufRow < this.pageBuf.viewportSize)) {
                if (this.pageBuf.rows[bufRow] && this.pageBuf.rows[bufRow].trSelCtrl) {
                    this.pageBuf.rows[bufRow].trSelCtrl.setState(selVal);
                }
            }
        },
        /**
         * @private
         */
        // ..SEL_ROWS and bufRow === -1 means "do not change selected rows"
        updateSelectionData: function(type, bufRow, state, shiftPressed, ctrlPressed, fromCheckbox) {
            var self = this,
                isMultiSelectMode = (this.cConfig.resp["select-mode"] === "MULTI"),
                isSingleSelectMode = (this.cConfig.resp["select-mode"] === "SINGLE"),
                isItemSelectMode = (this.cConfig.resp["select-mode"] === "ITEM");

            if (isItemSelectMode) {
                // write always - independent of chk/unchk
                var im = shmi.visuals.session.ItemManager;
                var selRowVal = this.pageBuf.offset + bufRow;
                var selRowsCopy = null;
                if (this.cConfig["rows-items-from-1"]) {
                    selRowVal++;
                }
                im.writeValue(this.cConfig["item-selected-row"], selRowVal);
            } else {
                switch (type) {
                case this.c.SELECT_TYPE_NOTHING:
                    this.selection.type = this.c.SELECT_TYPE_NOTHING;
                    this.selection.selRows = [];
                    this.selection.selRowIndex = [];
                    break;
                case this.c.SELECT_TYPE_ALL:
                    if (isMultiSelectMode) {
                        this.selection.type = this.c.SELECT_TYPE_ALL;
                    }
                    // else: impossible
                    break;
                default: // .._SEL_ROWS
                    if (bufRow !== -1) {
                        if (isMultiSelectMode) {
                            if (shiftPressed && this.lastClicked.rowId > -1) {
                                if (!ctrlPressed) {
                                    this.selection.selRows = [];
                                    this.selection.selRowIndex = [];
                                }
                                addSelectionRange(this, this.pageBuf.rows[bufRow].rowId).then().catch((e) => {
                                    shmi.notify("Error fetching selection data: " + e.message, "${V_ERROR}");
                                });
                                this.selection.lastClickedRow = bufRow;
                                setLastClicked(this, this.pageBuf.rows[bufRow].rowId);
                                return;
                            } else if (fromCheckbox || ctrlPressed) {
                                if (state === this.c.CB_CHK) {
                                    this.addRowIdToSelection(this.pageBuf.rows[bufRow].rowId);
                                } else {
                                    this.removeRowIdFromSelection(this.pageBuf.rows[bufRow].rowId);
                                }
                                this.selection.lastClickedRow = bufRow;
                                setLastClicked(this, this.pageBuf.rows[bufRow].rowId);
                            } else {
                                selRowsCopy = shmi.cloneObject(self.selection.selRows);
                                selRowsCopy.forEach(function(rid) {
                                    self.removeRowIdFromSelection(rid);
                                });
                                if (state === this.c.CB_CHK) {
                                    self.addRowIdToSelection(this.pageBuf.rows[bufRow].rowId);
                                }
                                setLastClicked(this, this.pageBuf.rows[bufRow].rowId);
                            }
                        } else if (isSingleSelectMode) {
                            this.selection.selRows = [];
                            this.selection.selRowIndex = [];

                            if (state === this.c.CB_CHK) {
                                this.addRowIdToSelection(this.pageBuf.rows[bufRow].rowId);
                            } else {
                                this.removeRowIdFromSelection(this.pageBuf.rows[bufRow].rowId);
                            }
                        }
                    }
                    this.updateSelTypeFromSelRows();
                    break;
                }
                this.updateDelRowsBtnEnable();

                clearTimeout(self.sel_fire_to);
                self.sel_fire_to = setTimeout(function() {
                    self.fireSelChanged(shmi.cloneObject(self.selection));
                }, shmi.c("ACTION_RETRY_TIMEOUT"));
            }
        },
        /**
         * @private
         */
        getRowIndex: function(rowId) {
            var rowIndex = null,
                grid = this.dgMan.getGrid(this.cConfig.table);

            if (grid) {
                rowIndex = grid.getRowIndex(rowId);
            }

            return rowIndex;
        },
        /**
         * @private
         */
        addRowIdToSelection: function(rowId) {
            if (this.selection.selRows.indexOf(rowId) === -1) {
                this.selection.selRows.push(rowId);
                if (this.cConfig.useDgIndex) {
                    var rowIndex = this.getRowIndex(rowId);
                    this.selection.selRowIndex.push(rowIndex);
                }
            }
        },
        /**
         * @private
         */
        removeRowIdFromSelection: function(rowId) {
            var self = this,
                idx = this.selection.selRows.indexOf(rowId);
            if (idx !== -1) {
                this.selection.selRows.splice(idx, 1);
                if (this.cConfig.useDgIndex) {
                    this.selection.selRowIndex.splice(idx, 1);
                }
            } else if (self.editRow !== -1) {
                self.resetBufferRow(self.editRow);
                self.editRow = -1;
                self.updateSize();
            }
        },
        /**
         * @private
         */
        updateSelTypeFromSelRows: function() {
            if (this.selection.selRows.length === this.pageBuf.nofTotalRows) {
                this.selection.type = this.c.SELECT_TYPE_SEL_ROWS;
            } else if (this.selection.selRows.length === 0) {
                this.selection.type = this.c.SELECT_TYPE_NOTHING;
            } else {
                this.selection.type = this.c.SELECT_TYPE_SEL_ROWS;
            }
        },
        /**
         * @private
         */
        selectAllCBoxValChanged: function(state) {
            var type;
            switch (state) {
            case this.c.CB_UNCHK:
                type = this.c.SELECT_TYPE_NOTHING;
                break;
            case this.c.CB_CHK:
                type = this.c.SELECT_TYPE_ALL;
                loadRange(this, -1, 0, this.dgSubscription.changeInfo.totalRows, () => {
                    /* all rows loaded */
                });
                return;
            default:
                type = this.c.SELECT_TYPE_SEL_ROWS;
                break;
            }
            this.updateSelectionData(type, -1, this.c.CB_UNDEF, false, false, true);
            this.updateSelectionView();
        },
        /**
         * @private
         */
        selectCBoxValChanged: function(bufRow, state, shiftPressed, ctrlPressed, fromCheckbox) {
            if (this.c.MULTI_SELECT_MODE === "3-state-clear") {
                if (this.header.selAllCBox && this.header.selAllCBox.getState() === this.c.CB_UNCHK) {
                    this.clearSelection();
                }
            }
            this.updateSelectionData(this.c.SELECT_TYPE_SEL_ROWS, bufRow, state, shiftPressed, ctrlPressed, fromCheckbox);
            this.updateSelectionView();
        },
        /**
         * @private
         */
        selectRowValChanged: function(bufRow, state, shiftPressed, ctrlPressed) {
            this.updateSelectionData(this.c.SELECT_TYPE_SEL_ROWS, bufRow, state, shiftPressed, ctrlPressed, false);
            this.updateSelectionView();
        },
        /**
         * @private
         */
        deleteSelectedRows: function() {
            var self = this;
            shmi.confirm(self.config["msg-confirm-delete"] || "${ct2_confirm_delete_rows}", function(confirmOk) {
                self.deleteSelectedRows_confirm(confirmOk);
            });
        },
        /**
         * @private
         */
        deleteSelectedRows_confirm: function(confirmOk) {
            if (confirmOk) {
                switch (this.selection.type) {
                case this.c.SELECT_TYPE_ALL:
                    setLastClicked(this, -1);
                    this.dgMan.deleteAll(this.cConfig.table);
                    break;
                case this.c.SELECT_TYPE_SEL_ROWS:
                    setLastClicked(this, -1);
                    this.dgMan.deleteRow(this.cConfig.table, this.selection.selRows);
                    break;
                default:
                    break;
                }
                this.clearSelection();
            }
        },
        /**
         * @private
         */
        fireSelChanged: function(selection) {
            const iterObj = shmi.requires("visuals.tools.iterate.iterateObject");

            iterObj(this.selChgEventListeners, (listener) => {
                listener.listener.tableSelectionChange(selection);
            });

            if (!this.lastSelection || !isEqualDeep(this.lastSelection, selection)) {
                this.lastSelection = shmi.cloneObject(selection);
                this.updateNofRowsDisplay();
                this.fire("select", selection);
            }
        },

        /* sort processing */
        /**
         * @private
         */
        sortHeaderChanged: function(dgCol, state) {
            var sortCol,
                dir;
            switch (state) {
            case this.c.SORT_UP:
                sortCol = dgCol;
                dir = "DESC";
                break;
            case this.c.SORT_DOWN:
                sortCol = dgCol;
                dir = "ASC";
                break;
            default:
                sortCol = -1;
                dir = "ASC";
                break;
            }
            setLastClicked(this, -1);
            this.dgMan.sort(this.cConfig.table, sortCol, dir);
            this.clearSortHeaderExcept(dgCol);
            this.clearSelection();
        },
        /**
         * @private
         */
        clearSortHeaderExcept: function(dgCol) {
            var nofCols = this.cConfig.resp["nof-cols"];
            for (var col = 0; col < nofCols; col++) {
                var hCtrls = this.header.cols[col].ctrls;
                for (var i = 0; i < hCtrls.length; i++) {
                    if (hCtrls[i].id !== dgCol) {
                        hCtrls[i].setState(this.c.NO_SORT);
                    }
                }
            }
        },

        /* filter processing */
        /**
         * @private
         */
        txtFiltersUsed: function() {
            var txtFlUsed = false;
            if (this.config["default-text-filter-fields"].length > 0) {
                txtFlUsed = true;
            }
            return txtFlUsed;
        },
        /**
         * @private
         */
        clearFilterData: function() {
            this.filterData = {};
            var nofFilters = this.cConfig.filters.length;
            for (var fId = 0; fId < nofFilters; fId++) {
                var fCfg = this.cConfig.filters[fId],
                    dgColProp = this.getDataColNo(fCfg.field).toString(),
                    fData = this.filterData[dgColProp];
                if (!fData) {
                    fData = {};
                    fData.expr = [];
                    fData.changed = false;
                    this.filterData[dgColProp] = fData;
                }
            }
        },
        /**
         * @private
         */
        clearColTextFilterData: function() {
            this.colTextFilterData = {};
            var colTxtFilterFields = this.cConfig.resp["text-filter-fields"];
            for (var fIdx = 0; fIdx < colTxtFilterFields.length; fIdx++) {
                var dgColProp = this.getDataColNo(colTxtFilterFields[fIdx]).toString(),
                    fData = this.colTextFilterData[dgColProp];
                if (!fData) {
                    fData = {};
                    fData.expr = "";
                    fData.changed = false;
                    this.colTextFilterData[dgColProp] = fData;
                }
            }
        },
        /**
         * @private
         */
        quicksearchChanged: function(value) {
            var fExprVal = (typeof value === "string") ? value.trim() : value;
            var fields = this.cConfig.quicksearch.fields;
            if (fExprVal === "") {
                this.removeQuicksearchFilterExpr(fields);
            } else {
                this.setQuicksearchFilterExpr(fExprVal, fields);
            }
            this.applyFilters();
            this.clearSelection();
            this.resetScroll();
        },
        /**
         * @private
         */
        filterChanged: function(value, type, vItemName) {
            if (value === 1) {
                this.setFilterExpr(vItemName);
            } else {
                this.removeFilterExpr(vItemName);
            }
            this.applyFilters();
            this.clearSelection();
            this.resetScroll();
        },
        /**
         * @private
         */
        colTxtFilterChanged: function(value, type, vItemName) {
            var fExprVal = (typeof value === "string") ? value.trim() : value;
            if (fExprVal === "") {
                var fCol = this.getFilterId(vItemName);
                this.removeColTxtFilterExpr(fCol);
            } else {
                this.setColTxtFilterExpr(vItemName, fExprVal);
            }
            this.applyFilters();
            this.clearSelection();
            this.resetScroll();
        },
        /**
         * @private
         */
        colTxtFilterReset: function(fCol) {
            var colTxtFilItem = this.colTxtFilters.items[fCol];
            if (colTxtFilItem) {
                var im = shmi.visuals.session.ItemManager;
                im.writeValue(colTxtFilItem.name, "");
            }
        },
        /**
         * @private
         */
        setFilterExpr: function(vItemName) { // if not yet set !!
            var fId = this.getFilterId(vItemName),
                fIdInt = Number(fId);
            if (fIdInt !== -1) {
                var fCfg = this.cConfig.filters[fId];
                if (fCfg) {
                    var fCfgExpr = fCfg.expr,
                        dgColProp = this.getDataColNo(fCfg.field).toString(),
                        fData = this.filterData[dgColProp];
                    if (fData.expr.indexOf(fCfgExpr) === -1) {
                        fData.expr.push(fCfgExpr);
                        fData.changed = true;
                    }
                }
            }
        },
        /**
         * @private
         */
        removeFilterExpr: function(vItemName) { // if exists !!
            var fId = this.getFilterId(vItemName),
                fIdInt = Number(fId);
            if (fIdInt !== -1) {
                var fCfg = this.cConfig.filters[fId];
                if (fCfg) {
                    var fCfgExpr = fCfg.expr,
                        dgColProp = this.getDataColNo(fCfg.field).toString(),
                        fData = this.filterData[dgColProp],
                        exprIdx = fData.expr.indexOf(fCfgExpr);
                    if (exprIdx !== -1) {
                        fData.expr.splice(exprIdx, 1);
                        fData.changed = true;
                    }
                }
            }
        },
        /**
         * @private
         */
        setColTxtFilterExpr: function(vItemName, vItemValue) {
            var col = this.getFilterId(vItemName),
                dgColNos = this.getColTxtFilterDgCols(col);
            for (var i = 0; i < dgColNos.length; i++) {
                var dgColProp = dgColNos[i].toString(),
                    fData = this.colTextFilterData[dgColProp];
                if (fData) {
                    fData.expr = "%" + vItemValue + "%";
                    fData.changed = true;
                } else {
                    shmi.log("[ComplexTable2] setColTxtFilterExpr, no filter data available for data grid col: " + dgColProp, 3);
                }
            }
        },
        /**
         * @private
         */
        removeColTxtFilterExpr: function(col) {
            var dgColNos = this.getColTxtFilterDgCols(col);
            for (var i = 0; i < dgColNos.length; i++) {
                var dgColProp = dgColNos[i].toString(),
                    fData = this.colTextFilterData[dgColProp];
                if (fData) {
                    fData.expr = "";
                    fData.changed = true;
                } else {
                    shmi.log("[ComplexTable2] removeColTxtFilterExpr, no filter data available for data grid col: " + dgColProp, 3);
                }
            }
        },
        /**
         * @private
         */
        setQuicksearchFilterExpr: function(value, fields) {
            fields.forEach(function(fname) {
                var dgColNos = this.getColTxtFilterDgCols(this.getDataColIndex(fname));
                for (var i = 0; i < dgColNos.length; i++) {
                    var dgColProp = dgColNos[i].toString(),
                        fData = this.colTextFilterData[dgColProp];
                    if (fData) {
                        fData.expr = "%" + value + "%";
                        fData.changed = true;
                    } else {
                        shmi.log("[ComplexTable2] setQuicksearchFilterExpr, no filter data available for data grid col: " + dgColProp, 3);
                    }
                }
            }.bind(this));
        },
        /**
         * @private
         */
        removeQuicksearchFilterExpr: function(fields) {
            fields.forEach(function(fname) {
                var dgColNos = this.getColTxtFilterDgCols(this.getDataColIndex(fname));
                for (var i = 0; i < dgColNos.length; i++) {
                    var dgColProp = dgColNos[i].toString(),
                        fData = this.colTextFilterData[dgColProp];
                    if (fData) {
                        fData.expr = "";
                        fData.changed = true;
                    } else {
                        shmi.log("[ComplexTable2] removeQuicksearchFilterExpr, no filter data available for data grid col: " + dgColProp, 3);
                    }
                }
            }.bind(this));
        },
        /**
         * @private
         */
        getDataColIndex: function(fname) {
            var i = 0;
            var colNo;

            for (var e in this.cConfig["field-datagrid-col-map"]) {
                if (e === fname) {
                    colNo = i;
                }
                i++;
            }

            if (colNo === undefined) {
                colNo = 0;
                shmi.log("[ComplexTable2] getDataColNo, unknown fldName " + fname, 3);
            } else {
                colNo = shmi.toNumber(colNo);
            }
            return colNo;
        },
        /**
         * @private
         */
        getFilterId: function(vItemName) {
            var id = -1,
                pos = vItemName.lastIndexOf(":"),
                posInt = Number(pos);
            if (posInt !== -1) {
                id = shmi.toNumber(vItemName.substring(pos + 1));
            } else {
                shmi.log("[ComplexTable2] getFilterId, unexpected format of virtual item name: " + vItemName, 3);
            }
            return id;
        },
        /**
         * @private
         */
        getColTxtFilterDgCols: function(col) {
            var fDgCols = [],
                cfgCol = this.cConfig.resp["column-org"][col];
            if (cfgCol) {
                var nofFields = cfgCol.fields.length;
                for (var fldIdx = 0; fldIdx < nofFields; fldIdx++) {
                    if (cfgCol.fields[fldIdx].hasTextFilter) {
                        fDgCols.push(cfgCol.fields[fldIdx].dgCol);
                    }
                }
            }
            return fDgCols;
        },
        /**
         * @private
         */
        applyFilters: function() {
            if (this.isApplyFiltersEnabled()) {
                var dgBtnFilCols = Object.keys(this.filterData),
                    dgTxtFilCols = Object.keys(this.colTextFilterData),
                    dgSumFilCols = dgBtnFilCols.slice(); // clone it
                for (var i = 0; i < dgTxtFilCols.length; i++) {
                    if (dgSumFilCols.indexOf(dgTxtFilCols[i]) === -1) { // not yet contained
                        dgSumFilCols.push(dgTxtFilCols[i]);
                    }
                }

                for (i = 0; i < dgSumFilCols.length; i++) {
                    var btnFilProps = this.filterData[dgSumFilCols[i]],
                        txtFilProps = this.colTextFilterData[dgSumFilCols[i]],
                        processFilterCol = false;
                    if (btnFilProps && btnFilProps.changed) {
                        processFilterCol = true;
                    } else if (txtFilProps && txtFilProps.changed) {
                        processFilterCol = true;
                    }
                    if (processFilterCol) {
                        var fExpr = [];
                        if (btnFilProps) {
                            fExpr = btnFilProps.expr.slice();
                            btnFilProps.changed = false;
                        }
                        if (txtFilProps) {
                            if (txtFilProps.expr !== "") {
                                fExpr.push(txtFilProps.expr);
                            }
                            txtFilProps.changed = false;
                        }
                        var dgCol = shmi.toNumber(dgSumFilCols[i]);
                        if (fExpr.length === 0) {
                            setLastClicked(this, -1);
                            this.dgMan.clearFilter(this.cConfig.table, dgCol);
                        } else {
                            setLastClicked(this, -1);
                            this.dgMan.setFilter(this.cConfig.table, dgCol, fExpr);
                        }
                    }
                }
            }
        },
        /**
         * @private
         */
        isApplyFiltersEnabled: function() {
            return this.applyFiltersEnable;
        },
        /**
         * @private
         */
        setApplyFiltersEnable: function(enable) {
            this.applyFiltersEnable = enable;
        },
        /**
         * @private
         */
        resetFilterChangeFlags: function() {
            var dgBtnFilCols = Object.keys(this.filterData);
            for (var i = 0; i < dgBtnFilCols.length; i++) {
                this.filterData[dgBtnFilCols[i]].changed = false;
            }
        },
        /**
         * @private
         */
        resetColTxtFilterChangeFlags: function() {
            var dgTxtFilCols = Object.keys(this.colTextFilterData);
            for (var i = 0; i < dgTxtFilCols.length; i++) {
                this.colTextFilterData[dgTxtFilCols[i]].changed = false;
            }
        },

        /* other helpers */
        /**
         * @private
         */
        clearCtrlPanelData: function() {
            if (!this.ctrlPanelData.filter) { // 1st time
                this.ctrlPanelData.filter = {};
                this.ctrlPanelData.filter.names = [];
                this.ctrlPanelData.filter.items = [];
            }

            // reset filter items
            var restoreApplyFiltersEnable = this.isApplyFiltersEnabled();
            this.setApplyFiltersEnable(false);
            var im = shmi.visuals.session.ItemManager;
            for (var i = 0; i < this.ctrlPanelData.filter.items.length; i++) {
                im.writeValue(this.ctrlPanelData.filter.items[i].name, 0);
            }
            this.resetFilterChangeFlags();
            this.setApplyFiltersEnable(restoreApplyFiltersEnable);

            if (this.ctrlPanelData.filter.btns) {
                for (i = 0; i < this.ctrlPanelData.filter.btns.length; i++) {
                    shmi.deleteControl(this.ctrlPanelData.filter.btns[i]);
                }
            }
            this.ctrlPanelData.filter.btns = [];

            if (this.ctrlPanelData.delRowsBtn) {
                shmi.deleteControl(this.ctrlPanelData.delRowsBtn);
            }
            this.ctrlPanelData.delRowsBtn = null;
        },
        /**
         * @private
         */
        clearColTextFilterCtrls: function() {
            if (!this.colTxtFilters.items) { // 1st time
                this.colTxtFilters.items = []; // 0..maxCols of this instance - on demand extended
            }

            // reset filter items
            var restoreApplyFiltersEnable = this.isApplyFiltersEnabled();
            this.setApplyFiltersEnable(false);
            var im = shmi.visuals.session.ItemManager;
            for (var i = 0; i < this.colTxtFilters.items.length; i++) {
                if (this.colTxtFilters.items[i]) {
                    im.writeValue(this.colTxtFilters.items[i].name, "");
                }
            }
            this.resetColTxtFilterChangeFlags();
            this.setApplyFiltersEnable(restoreApplyFiltersEnable);

            var ctrls = [];
            if (this.colTxtFilters.cols) {
                for (var col = 0; col < this.colTxtFilters.cols.length; col++) {
                    if (this.colTxtFilters.cols[col]) {
                        ctrls = this.colTxtFilters.cols[col].ctrls;
                        for (i = 0; i < ctrls.length; i++) {
                            shmi.deleteControl(ctrls[i]);
                        }
                    }
                }
            }
            this.colTxtFilters.cols = []; // 0..nofCols of current layout
        },
        /**
         * @private
         */
        clearCtrlPanelDOM: function() {
            var self = this;
            if (self.ctrlPanelElement) {
                if (self.ctrlPanelData && self.ctrlPanelData.qSearchInput) {
                    shmi.deleteControl(self.ctrlPanelData.qSearchInput);
                    self.ctrlPanelData.qSearchInput = null;
                }
                var childEl = self.ctrlPanelElement.firstChild;
                while (childEl) {
                    self.ctrlPanelElement.removeChild(childEl);
                    childEl = self.ctrlPanelElement.firstChild;
                }
            }
        },
        /**
         * @private
         */
        updateCtrlPanel: function() {
            if (this.ctrlPanelElement) {
                var cWidth = this.getTableWidth();
                if (cWidth < this.cConfig["show-buttons-table-min-width-px"]) {
                    shmi.removeClass(this.ctrlPanelElement, this.uiCssCl.ctrlPanelNormal);
                    shmi.addClass(this.ctrlPanelElement, this.uiCssCl.ctrlPanelMin);
                } else {
                    shmi.removeClass(this.ctrlPanelElement, this.uiCssCl.ctrlPanelMin);
                    shmi.addClass(this.ctrlPanelElement, this.uiCssCl.ctrlPanelNormal);
                }

                this.clearCtrlPanelData();
                this.clearCtrlPanelDOM();

                if (this.cConfig.quicksearch && this.cConfig.quicksearch.enable) {
                    this.enableQuickSearch();
                } else {
                    this.enableFilterButtons();
                }

                if (this.cConfig["delete-selected-rows"]) {
                    var delBtnElem = document.createElement("div");
                    this.ctrlPanelElement.appendChild(delBtnElem);
                    var config = this.getChildCtrlConfig(this.cConfig["delete-selected-rows-button-config"]);
                    if (!config) {
                        config = {
                            "label": "${ct2_delete_rows}"
                        };
                    }
                    this.ctrlPanelData.delRowsBtn = shmi.createControl("button", delBtnElem, config, "div");
                    this.ctrlPanelData.delRowsBtn.onClick = function() {
                        this.deleteSelectedRows();
                    }.bind(this);
                    this.updateDelRowsBtnEnable();
                }
            }
        },
        /**
         * @private
         */
        enableFilterButtons: function() {
            var fData = this.ctrlPanelData.filter, // shortcut
                fCfg = this.cConfig.filters, // shortcut
                nofFilters = fCfg.length,
                fId,
                itemNamespace = this.getFilterItemNS();
            if (fData.items.length === 0) { // 1st time
                for (fId = 0; fId < nofFilters; fId++) {
                    var fName = shmi.localize(fCfg[fId].label);
                    fData.names.push(fName);
                    var iName = "virtual:" + itemNamespace + ":" + fId,
                        fItem = shmi.createVirtualItem(iName, 2, 0, 1, 0, function(value, type, vItemName) {
                            this.filterChanged(value, type, vItemName);
                        }.bind(this));
                    fData.items.push(fItem);
                }
            }
            for (fId = 0; fId < nofFilters; fId++) {
                var fBtnElem = document.createElement("div");
                this.ctrlPanelElement.appendChild(fBtnElem);
                var cfg = this.getChildCtrlConfig(fCfg[fId]["filter-button-config"],
                    this.cConfig["default-filter-button-config"]);
                if (!cfg) {
                    cfg = {
                        "off-value": 0,
                        "on-value": 1
                    };
                    cfg["off-label"] = cfg["on-label"] = fData.names[fId];
                    if (fCfg[fId].template) {
                        cfg.template = fCfg[fId].template;
                    }
                    if (fCfg[fId]["class-name"]) {
                        cfg["class-name"] = fCfg[fId]["class-name"];
                    }
                }
                cfg.item = fData.items[fId].name;
                var fBtn = shmi.createControl("toggle-button", fBtnElem, cfg, "div");
                fData.btns.push(fBtn);
            }
        },
        /**
         * @private
         */
        enableQuickSearch: function() {
            var self = this;
            if (self.cConfig.quicksearch.chaining === "AND" || self.cConfig.quicksearch.chaining === "OR") {
                shmi.visuals.session.DataGridManager.setFilterChaining(self.cConfig.table, self.cConfig.quicksearch.chaining);
            } else {
                shmi.visuals.session.DataGridManager.setFilterChaining(self.cConfig.table, "OR");
            }

            if (self.ctrlPanelData.qSearchInput) {
                return; //no need to setup quick search input-field again when it already exists
            }

            var qSearchElem = document.createElement("div"),
                qSearchClearElem = document.createElement("div");

            shmi.addClass(qSearchElem, "qsearch-box");
            self.ctrlPanelElement.appendChild(qSearchElem);
            var conf = {
                "label": "${ct2_quicksearch}",
                "class-name": "input-field label-beside"
            };
            self.ctrlPanelData.qSearchInput = shmi.createControl("input-field", qSearchElem, conf, "div");

            qSearchElem.appendChild(qSearchClearElem);
            shmi.addClass(qSearchClearElem, "ct2-qsearch-clear");
            qSearchClearElem.addEventListener('click', function() {
                if (self.isActive() && !self.locked) {
                    self.ctrlPanelData.qSearchInput.setValue("");
                    self.qSearchOld = null;
                    self.quicksearchHandler();
                }
            });

            shmi.onActive([self.ctrlPanelData.qSearchInput], function() {
                if (self.isActive()) {
                    var valueElement = self.ctrlPanelData.qSearchInput.valueElement;

                    if (self.qSearchOld !== null) {
                        self.ctrlPanelData.qSearchInput.setValue(self.qSearchOld);
                    }

                    self.quicksearchHandler = onQuickSearch.bind(null, self, valueElement);
                    self.quicksearchValueElement = valueElement;
                    valueElement.addEventListener('keyup', self.quicksearchHandler);
                    self.vars.qSearchToken = self.ctrlPanelData.qSearchInput.listen("change", self.quicksearchHandler);

                    if (self.cConfig.quicksearch && self.cConfig.quicksearch.initialFocus) {
                        self.ctrlPanelData.qSearchInput.valueElement.focus();
                    }
                }
            });

            if (self.isLocked()) {
                self.ctrlPanelData.qSearchInput.lock();
            }
        },
        /**
         * @private
         */
        updateDelRowsBtnEnable: function() {
            var enabled = true;
            if (this.ctrlPanelData.delRowsBtn) {
                if ((this.selection.type === this.c.SELECT_TYPE_NOTHING) || ((this.selection.type === this.c.SELECT_TYPE_SEL_ROWS) && (this.selection.selRows.length === 0))) {
                    enabled = false;
                }
                if (enabled) {
                    this.ctrlPanelData.delRowsBtn.unlock();
                } else {
                    this.ctrlPanelData.delRowsBtn.lock();
                }
            }
        },
        /**
         * @private
         */
        getFilterItemNS: function() {
            return "ct2-filter-" + this.instID;
        },
        /**
         * @private
         */
        getColTxtFilterItemNS: function() {
            return "ct2-col-txt-filter-" + this.instID;
        },
        getAdapterNS: function() {
            return "ct2-adapter-" + this.instID;
        },
        /**
         * @private
         */
        updateHeader: function() {
            var self = this;
            // cleanup this.header
            if (this.header.cols) {
                this.header.cols.forEach(function(col, idx) {
                    if (col.ctrls[0].cbMouseListener) {
                        col.ctrls[0].cbMouseListener.disable();
                        col.ctrls[0].cbTouchListener.disable();
                    }
                });
            }
            this.header.selAllCBox = null;
            this.header.cols = [];
            // cleanup DOM
            var childElem = this.theadElement.firstChild;
            while (childElem) {
                this.theadElement.removeChild(childElem);
                childElem = this.theadElement.firstChild;
            }
            // reinit header table
            var nofCols = this.cConfig.resp["nof-cols"],
                isMultiSelectMode = (this.cConfig.resp["select-mode"] === "MULTI"),
                colGroupElem = this.createColGroup(nofCols, isMultiSelectMode);
            this.theadElement.appendChild(colGroupElem);
            var trElem = document.createElement("tr"),
                thElem = null,
                tabCellElem = null;
            if (isMultiSelectMode && this.config["show-select-boxes"]) {
                thElem = document.createElement("th");
                tabCellElem = document.createElement("div");
                this.header.selAllCBox = this.createSelCBox(tabCellElem, -1, true, this);
                this.header.selAllCBox.setChangeCallback(function(id, state) {
                    if (!self.locked) {
                        self.selectAllCBoxValChanged(state);
                    }
                });
                thElem.appendChild(tabCellElem);
                trElem.appendChild(thElem);
            }
            this.theadElement.appendChild(trElem);
            for (var col = 0; col < nofCols; col++) {
                var cfgCol = this.cConfig.resp["column-org"][col];
                thElem = document.createElement("th");
                tabCellElem = document.createElement("div");
                this.header.cols[col] = {};
                this.header.cols[col].ctrls = this.createHeaderCtrls(tabCellElem, col);
                thElem.appendChild(tabCellElem);
                trElem.appendChild(thElem);

                if (cfgCol.fields[0]["min-width"]) {
                    tabCellElem.style.minWidth = cfgCol.fields[0]["min-width"];
                }
                this.header.cols[col].ctrls.forEach(function(hCtrl, idx) {
                    var fldName = hCtrl.fieldName;
                    if (self.config.adapterSettings && self.config.adapterSettings[fldName]) {
                        var settings = self.config.adapterSettings[fldName],
                            itemName = null;
                        self.adapterItems.forEach(function(aEntry, index) {
                            if (aEntry.field === fldName) {
                                itemName = aEntry.item.name;
                            }
                        });

                        var selectElem = document.createElement('div');
                        thElem.appendChild(selectElem);
                        var selectConfig = shmi.cloneObject(self.config.adapterSelectConfig);
                        selectConfig.item = itemName;
                        selectConfig.options = settings;
                        shmi.createControl("select-box", selectElem, selectConfig, 'DIV', 'from');
                        shmi.addClass(thElem, self.config.adapterSelectClass);
                    }
                });
            }
        },
        /**
         * @private
         */
        updateColTextFilters: function() {
            var self = this;
            if (this.txtFiltersHeadElement && this.txtFiltersUsed()) {
                // cleanup filter data
                this.clearColTextFilterCtrls();
                // cleanup DOM
                var childElem = this.txtFiltersHeadElement.firstChild;
                while (childElem) {
                    this.txtFiltersHeadElement.removeChild(childElem);
                    childElem = this.txtFiltersHeadElement.firstChild;
                }

                // init the virtual items
                var fData = this.colTxtFilters,
                    col;
                if (fData.items.length === 0) { // 1st time
                    var maxCols = Object.keys(this.cConfig["field-datagrid-col-map"]).length, // maxCols === number of fields
                        itemNamespace = this.getColTxtFilterItemNS();
                    for (col = 0; col < maxCols; col++) {
                        var iName = "virtual:" + itemNamespace + ":" + col;
                        this.colTxtFilters.items[col] = shmi.createVirtualItem(iName, 0, 0, 1, "", function(value, type, vItemName) {
                            this.colTxtFilterChanged(value, type, vItemName);
                        }.bind(this));
                    }
                }

                // reinit filter header table
                var nofCols = this.cConfig.resp["nof-cols"],
                    isMultiSelectMode = (this.cConfig.resp["select-mode"] === "MULTI"),
                    colGroupElem = self.createColGroup(nofCols, isMultiSelectMode);

                this.txtFiltersHeadElement.appendChild(colGroupElem);
                var trElem = document.createElement("tr");
                this.txtFiltersHeadElement.appendChild(trElem);
                var thElem = null,
                    tabCellElem = null;
                if (isMultiSelectMode) {
                    thElem = document.createElement("th");
                    trElem.appendChild(thElem);
                }
                for (col = 0; col < nofCols; col++) {
                    var cfgCol = this.cConfig.resp["column-org"][col];
                    thElem = document.createElement("th");
                    tabCellElem = document.createElement("div");
                    thElem.appendChild(tabCellElem);
                    trElem.appendChild(thElem);
                    if (cfgCol.hasTextFilter) {
                        this.colTxtFilters.cols[col] = {};
                        this.colTxtFilters.cols[col].ctrls = this.createColTxtFilterCtrls(tabCellElem, col);
                    }
                }
            }
        },
        /**
         * @private
         */
        getDgFilterSettings: function() {
            // clear all filters and sort for the associated grid - async.
            var self = this;
            clearTimeout(self.reset_sort_filters_to);
            self.reset_sort_filters_to = setTimeout(function() {
                self.resetSortFilters();
            }, shmi.c("ACTION_RETRY_TIMEOUT"));
        },
        /**
         * @private
         */
        getColTxtFilterVal: function(fExpr) {
            var txtFlVal = "",
                exprLen = fExpr.length;
            if ((exprLen >= 3) && (fExpr.charAt(0) === "%") && (fExpr.charAt(exprLen - 1) === "%")) {
                txtFlVal = fExpr.slice(1, exprLen - 1);
            } else {
                shmi.log("[ComplexTable2] getColTxtFilterVal, unexpected format of filter expression: " + fExpr, 2);
            }
            return txtFlVal;
        },
        /**
         * @private
         */
        getBtnFilter: function(fExpr, dgCol) {
            var btnFId = -1,
                fCfg = this.cConfig.filters;
            for (var fId = 0; fId < fCfg.length; fId++) {
                var fDgCol = this.getDataColNo(fCfg[fId].field);
                if ((fDgCol === dgCol) && (fExpr === fCfg[fId].expr)) {
                    btnFId = fId;
                    break;
                }
            }
            return btnFId;
        },
        /**
         * @private
         */
        resetSortFilters: function() {
            setLastClicked(this, -1);
            this.dgMan.sort(this.cConfig.table, -1, "ASC");
            this.dgMan.clearFilter(this.cConfig.table, -1);
        },
        /**
         * @private
         */
        updateNofRowsDisplay: function() {
            if (this.cConfig.resp["show-nof-rows"] && this.footerElement) {
                this.footerElement.textContent = this.pageBuf.nofTotalRows + " " + shmi.localize("${ct2_nof_rows}");
            }
        },
        /**
         * @private
         */
        updateRespDepContent: function(layoutChanged) {
            var self = this;
            if (layoutChanged) {
                if (self.cConfig.resp.headerMode === "ICON") {
                    self.headerMode = "ICON";
                } else {
                    self.headerMode = "TEXT";
                }

                if (this.cLayoutClass) {
                    shmi.removeClass(this.element, this.cLayoutClass);
                }
                this.cLayoutClass = this.cConfig.resp["layout-class-name"];
                if (this.cLayoutClass) {
                    shmi.addClass(this.element, this.cLayoutClass);
                }
                if (this.cConfig.resp["show-nof-rows"]) {
                    shmi.removeClass(this.element, this.uiCssCl.rowDisplInvisible);
                    shmi.addClass(this.element, this.uiCssCl.rowDisplVisible);
                } else {
                    shmi.removeClass(this.element, this.uiCssCl.rowDisplVisible);
                    shmi.addClass(this.element, this.uiCssCl.rowDisplInvisible);
                }
                if (this.cConfig.resp["select-mode"] === "MULTI") {
                    shmi.addClass(this.element, this.uiCssCl.multiselect);
                } else {
                    shmi.removeClass(this.element, this.uiCssCl.multiselect);
                }
            }
            this.updateCtrlPanel();
            this.updateHeader();
            if (this.cConfig.quicksearch) {
                if (!this.cConfig.quicksearch.enable) {
                    this.updateColTextFilters();
                }
            } else {
                this.updateColTextFilters();
            }
        },

        /**
         * @private
         */
        cleanupTabContent: function() {
            this.clearPgBuf();
            this.clearTableDOM();
        },
        /**
         * @private
         */
        clearPgBuf: function() {
            if (this.pageBuf.rows) { // obj is already initialized
                for (var bufRow = 0; bufRow < this.pageBuf.rows.length; bufRow++) {
                    if (this.pageBuf.rows[bufRow].trSelCtrl) {
                        if (this.pageBuf.rows[bufRow].trSelCtrl.rsMouseListener) {
                            this.pageBuf.rows[bufRow].trSelCtrl.rsMouseListener.disable();
                            this.pageBuf.rows[bufRow].trSelCtrl.rsTouchListener.disable();
                        }
                    }
                    for (var col = 0; col < this.pageBuf.nofCols; col++) {
                        var ctrls = this.pageBuf.rows[bufRow].cols[col].ctrls;
                        for (var ctrlIdx = 0; ctrlIdx < ctrls.length; ctrlIdx++) {
                            shmi.deleteControl(ctrls[ctrlIdx]);
                        }
                    }
                }
            }
            this.pageBuf.offset = 0;
            this.pageBuf.nofTotalRows = 0;
            this.pageBuf.size = 0;
            this.pageBuf.nofCols = 0;
            this.pageBuf.firstVpRow = 0; // 0 .. .size-1
            this.pageBuf.avgNofVpLines = 0;
            this.pageBuf.rows = [];
        },
        /**
         * @private
         */
        clearTableDOM: function() {
            this.tbodyElement.forEach(function(el) {
                var childEl = el.firstChild;
                while (childEl) {
                    el.removeChild(childEl);
                    childEl = el.firstChild;
                }
            });
        },
        /**
         * @private
         */
        cleanupMetrics: function() {
            if (this.metricsData.isValid) {
                this.metricsData.last = shmi.cloneObject(this.metricsData);
            } else {
                this.metricsData.last = {};
            }
            this.metricsData.isValid = false;
            this.metricsData.lineHeightPx = 0;
        },
        /**
         * @private
         */
        cleanupAsyncProcessing: function() {
            if (this.asyncProcData.onDgChgRetryTO) {
                clearTimeout(this.asyncProcData.onDgChgRetryTO);
            }
            if (this.asyncProcData.onEvtResizeRetryTO) {
                clearTimeout(this.asyncProcData.onEvtResizeRetryTO);
            }
            if (this.asyncProcData.processMetricsTO) {
                clearTimeout(this.asyncProcData.processMetricsTO);
            }
            this.asyncProcData.onDgChgRetryTO = null;
            this.asyncProcData.onEvtResizeRetryTO = null;
            this.asyncProcData.processMetricsTO = null;
        },

        /**
         * @private
         */
        reinitTable: function(dataGridChgInfo, currentDataGridIds, keepScrollTop) {
            var self = this,
                initialSize = this.pageBuf.rows.length;
            // precondition: table + pageBuf ist empty (cleanupTabContent has already been processed)
            if (initialSize !== 0) {
                console.warn("[ComplexTable2] reinitTable, table is not empty, length:", initialSize);
                return;
            }

            if (dataGridChgInfo.totalRows !== 0) {
                shmi.removeClass(this.element, this.uiCssCl.emptyTab);

                //-- init pageBuf --
                this.pageBuf.bufferOffset = dataGridChgInfo.offset;
                this.pageBuf.nofTotalRows = dataGridChgInfo.totalRows;
                self.tableElement.style.height = (self.pageBuf.nofTotalRows * self.metricsData.lineHeightPx) + "px";
                this.pageBuf.size = currentDataGridIds.length; //number of data rows currently loaded
                this.pageBuf.viewportSize = this.cConfig["nof-buffered-rows"]; //total number of available data rows
                this.pageBuf.bufferSize = this.cConfig["buffer-size"];
                if (this.pageBuf.offset < this.pageBuf.bufferOffset) {
                    this.pageBuf.offset = this.pageBuf.bufferOffset;
                } else if (this.pageBuf.offset > this.pageBuf.bufferOffset + this.pageBuf.bufferSize) {
                    this.pageBuf.offset = this.pageBuf.bufferOffset;
                }
                var nofCols = this.pageBuf.nofCols = this.cConfig.resp["nof-cols"];
                for (var bufRow = initialSize; bufRow < this.pageBuf.viewportSize; bufRow++) {
                    //NOTE - here all row descriptors should be initialized & controls created,
                    // but controls cannot yet be connected to items and rows should be hidden
                    this.pageBuf.rows[bufRow] = createRowDescriptor((typeof currentDataGridIds[bufRow] === "number") ? currentDataGridIds[bufRow] : null, nofCols);
                }

                if (!keepScrollTop) {
                    this.bodyViewportElement.scrollTop = 0; // set scrollTop back to top of table
                }

                //-- create table DOM and cell controls --
                var isMultiSelectMode = (this.cConfig.resp["select-mode"] === "MULTI");

                this.tbodyElement.forEach(function(el, idx) {
                    var rowsPerPage = self.pageBuf.viewportSize / 3,
                        colGroupElem = self.createColGroup(nofCols, isMultiSelectMode);
                    el.appendChild(colGroupElem);
                    for (var i = 0; i < rowsPerPage; i++) {
                        createTableRow(self, (idx * rowsPerPage) + i, el, i === 0);
                    }
                });

                this.processAsyncMetricsUpdate(keepScrollTop ? self.vars.scrollTop : null);
            } else {
                this.pageBuf.nofCols = this.cConfig.resp["nof-cols"];
                shmi.addClass(this.element, this.uiCssCl.emptyTab);
            }
        },
        /**
         * @private
         */
        processTabReinitDependencies: function() {
            this.updateNofRowsDisplay();
            this.updateSelectionView();
        },
        doScrollUpdate: function doScrollUpdate() {
            var self = this,
                scrollTop = self.bodyViewportElement.scrollTop;

            updatePages(self, scrollTop);
        },
        /*
         * @private
         */
        initScrollHandler: function() {
            var self = this,
                scrollUpdateTimeoutLength = 100;
            self.bodyViewportElement.style["overflow-y"] = "scroll";
            self.onBodyScroll = function(evt) {
                clearTimeout(self.vars.updateTimeout);
                self.vars.updateTimeout = 0;

                if (!self.isActive() || self.locked) {
                    //reset scroll - event is not cancelable with preventDefault()
                    self.bodyViewportElement.scrollTop = 0;
                    return;
                }

                if (Date.now() - self.vars.lastUpdate > scrollUpdateTimeoutLength) {
                    self.doScrollUpdate();
                    self.vars.lastUpdate = Date.now();
                } else {
                    self.vars.updateTimeout = setTimeout(function() {
                        self.doScrollUpdate();
                        self.vars.lastUpdate = Date.now();
                    }, scrollUpdateTimeoutLength);
                }
            };
            self.bodyViewportElement.addEventListener('scroll', self.onBodyScroll);
        },
        /*
         * @private
         */
        resetScroll: function() {
            this.bodyViewportElement.scrollTop = 0; // set scrollTop back to top of table
        },
        /*
         * @private
         */
        updateTable: function(dataGridChgInfo, currentDataGridIds) {
            var self = this,
                linesPerPage = self.cConfig["nof-buffered-rows"] / 3,
                usedBufferIndices = [];
            // precondition: pageBuf.size is not changed and !== 0
            if ((self.pageBuf.rows.length === 0) || (self.pageBuf.bufferSize < currentDataGridIds.length)) {
                console.error("[ComplexTable2]", "updateTable, table is empty or buffer size has been changed");
                return;
            }
            //self.pageBuf.offset >= self.pageBuf.bufferOffset
            var indexOffset = self.pageBuf.offset - self.pageBuf.bufferOffset;
            currentDataGridIds.forEach(function(dgId, idx) {
                var currentIndex = idx - indexOffset;
                if ((currentIndex < 0) || (currentIndex >= (linesPerPage * 3))) {
                    return;
                }
                var curPage = Math.floor(currentIndex / linesPerPage),
                    bufIdx = (self.vars.pageOrder[curPage] * linesPerPage) + (currentIndex % linesPerPage),
                    curRow = self.pageBuf.rows[bufIdx];

                if (curRow) {
                    usedBufferIndices.push(bufIdx);
                    if (curRow.rowId !== dgId || !curRow.visible) {
                        self.writeRow(bufIdx, currentDataGridIds, idx);
                    }
                } else {
                    console.error("[ComplexTable2]", "invalid buffer index:", bufIdx);
                }
            });

            self.pageBuf.rows.forEach(function(r, idx) {
                if (usedBufferIndices.indexOf(idx) === -1) {
                    if (r.visible) {
                        r.visible = false;
                        r.trElem.style.visibility = "hidden";
                    }
                } else if (!r.visible) {
                    r.visible = true;
                    r.trElem.style.visibility = "";
                }
            });
        },
        /*
         * @private
         */
        writeRow: function(rId, currentDataGridIds, bufRow) {
            var rowId = 0,
                item = "",
                cfgCol = null;

            if (this.pageBuf.rows[rId]) {
                rowId = this.pageBuf.rows[rId].rowId = currentDataGridIds[bufRow];

                for (var col = 0; col < this.pageBuf.nofCols; col++) {
                    cfgCol = this.cConfig.resp["column-org"][col];
                    var ctrls = this.pageBuf.rows[rId].cols[col].ctrls;
                    for (var ctrlIdx = 0; ctrlIdx < ctrls.length; ctrlIdx++) {
                        item = this.makeItem(rowId, cfgCol.fields[ctrlIdx].dgCol);

                        var ia = shmi.requires("visuals.tools.item-adapter"),
                            columnAdapter = this.getColumnAdapter(cfgCol.fields[ctrlIdx].fieldName);
                        if (shmi.visuals.session.ItemManager.getItem(item) !== null) {
                            if (columnAdapter === null) {
                                ia.unsetAdapter(item);
                            } else {
                                ia.setAdapter(item, columnAdapter);
                            }
                        }
                        ctrls[ctrlIdx].setItem(item);
                    }
                }
            }
        },
        /**
         * @private
         */
        updateMetricsData: function(scrollTop) {
            var self = this;
            if (self.pageBuf.size > 0) {
                self.metricsData.lineHeightPx = self.tbodyElement[0].getBoundingClientRect().height / self.tbodyElement[0].rows.length;

                self.tableElement.style.height = (self.pageBuf.nofTotalRows * self.metricsData.lineHeightPx) + "px";
                self.tableElement.style.overflow = "hidden";
                self.metricsData.viewportHeight = self.bodyViewportElement.getBoundingClientRect().height;
                self.metricsData.viewportWidth = self.bodyViewportElement.getBoundingClientRect().width;

                var minBufRows = Math.ceil(self.metricsData.viewportHeight / self.metricsData.lineHeightPx) * 3;
                if (self.cConfig["nof-buffered-rows"] < minBufRows) {
                    self.cConfig["nof-buffered-rows"] = minBufRows;
                    console.info("[ComplexTable2]", "buffer rows configured too low, corrected to:", minBufRows);
                }
                var nofRows = self.cConfig["nof-buffered-rows"];
                self.pageBuf.nofTotalPages = self.pageBuf.nofTotalRows / (nofRows / 3);
            } else if (self.metricsData.last.isValid) { // empty table
                self.metricsData = shmi.cloneObject(self.metricsData.last);
            } else {
                self.metricsData.lineHeightPx = self.c.DEFAULT_LINE_HEIGHT;
            }
            self.metricsData.isValid = true;
            if (typeof scrollTop === "number") {
                reinitPages(self, scrollTop);
            } else {
                self.vars.scrollTop = 0;
                reinitPages(self, 0);
            }
        },
        /**
         * @private
         */
        disableEvents: function() {
            if (this.header.selAllCBox) {
                this.header.selAllCBox.setEventsEnabled(false);
            }
        },
        /**
         * @private
         */
        enableEvents: function() {
            if (this.header.selAllCBox) {
                this.header.selAllCBox.setEventsEnabled(true);
            }
        },
        /**
         * @private
         */
        // $note: col 0..nofCols-1
        createHeaderCtrls: function(tabCellElem, col) {
            var self = this,
                hCtrls = [];
            shmi.addClass(tabCellElem, this.uiCssCl.thCont);
            var cfgCol = this.cConfig.resp["column-org"][col],
                nofFields = cfgCol.fields.length;
            for (var headIdx = 0; headIdx < nofFields; headIdx++) {
                var hCtrlElem = document.createElement("div"),
                    icon = cfgCol.fields[headIdx].icon,
                    hCaption = cfgCol.fields[headIdx].header || " ";
                if (hCaption) {
                    var lhCaption = shmi.localize(hCaption);
                    if (headIdx > 0) {
                        lhCaption = " / " + lhCaption;
                    }
                    var hCtrl = new shmi.visuals.controls.ComplexTable2.FieldHeader(
                        hCtrlElem,
                        cfgCol.fields[headIdx].dgCol, // id
                        cfgCol.fields[headIdx].isSortable,
                        this.c.NO_SORT,
                        lhCaption,
                        self,
                        cfgCol.fields[headIdx].fieldName, icon);
                    hCtrl.setChangeCallback(function(id, state) {
                        if (!self.locked) {
                            self.sortHeaderChanged(id, state);
                        }
                    });
                    hCtrls.push(hCtrl);
                    tabCellElem.appendChild(hCtrlElem);
                }
            }

            return hCtrls;
        },
        /**
         * @private
         */
        // $note: col 0..nofCols-1
        createColTxtFilterCtrls: function(tabCellElem, col) {
            var cfCtrls = [],
                cfContainerElem = document.createElement("div"),
                cfLineElem = document.createElement("div");
            cfContainerElem.appendChild(cfLineElem);
            tabCellElem.appendChild(cfContainerElem);

            shmi.addClass(tabCellElem, this.uiCssCl.thCont);
            shmi.addClass(cfContainerElem, this.uiCssCl.colTxtFilterCont);
            shmi.addClass(cfLineElem, this.uiCssCl.colTxtFilterLineCont);

            var fImgElem = document.createElement("img");
            cfLineElem.appendChild(fImgElem);
            fImgElem.src = "pics/system/controls/complex-table2/filter.svg";
            shmi.addClass(fImgElem, this.uiCssCl.colTxtFilterIcon);

            var iName = this.colTxtFilters.items[col].name,
                cfg = this.getChildCtrlConfig(this.cConfig.resp["text-filter-input-field-config"]);
            if (!cfg) {
                cfg = {
                    "class-name": "input-field if-filter"
                };
            }
            cfg.item = iName;
            var fInpFld = shmi.createControl("input-field", cfLineElem, cfg, "div");
            cfCtrls.push(fInpFld);

            cfg = this.getChildCtrlConfig(this.cConfig.resp["text-filter-clear-button-config"]);
            if (!cfg) {
                cfg = {
                    "class-name": "button icon-only reset-filter",
                    "label": "X",
                    "icon-src": "pics/system/controls/complex-table2/cross_white.svg"
                };
            }
            var fResetFilterBtn = shmi.createControl("button", cfLineElem, cfg, "div");
            fResetFilterBtn.onClick = function() {
                this.colTxtFilterReset(col);
            }.bind(this);
            cfCtrls.push(fResetFilterBtn);

            return cfCtrls;
        },
        /**
         * @private
         */
        createSelCBox: function(tabCellElem, id, is3state, ct2ref) {
            return new shmi.visuals.controls.ComplexTable2.CheckBox(tabCellElem, id, is3state, this.c.CB_UNCHK, ct2ref);
        },
        /**
         * @private
         */
        createSelRowCtrl: function(trElem, id, ct2ref) {
            return new shmi.visuals.controls.ComplexTable2.RowSelector(trElem, id, this.c.CB_UNCHK, ct2ref);
        },
        /**
         * @private
         */
        createColGroup: function(nofCols, isMultiSelectMode) {
            var colGroupElem = document.createElement("colgroup");
            var columns = [];
            if (isMultiSelectMode && this.config["show-select-boxes"]) {
                columns.push(document.createElement("col"));
                columns[columns.length - 1].setAttribute("class", this.uiCssCl.colCb);
            }
            for (var i = 0; i < nofCols; i++) {
                var cfgCol = this.cConfig.resp["column-org"][i];
                columns.push(document.createElement("col"));
                columns[columns.length - 1].setAttribute("class", this.uiCssCl.colPfx + (i + 1));
                if (cfgCol.fields[0]["column-width"]) {
                    columns[columns.length - 1].style.width = cfgCol.fields[0]["column-width"];
                }
            }
            columns.forEach(function(c) {
                colGroupElem.appendChild(c);
            });
            return colGroupElem;
        },
        /**
         * @private
         */
        getTableWidth: function() {
            var width = 0;
            if (this.tcElement) {
                width = this.tcElement.offsetWidth;
            } else {
                var tcElem = shmi.getUiElement(this.uiElName.tabContainer, this.element);
                if (!tcElem) {
                    shmi.log("[ComplexTable2] no " + this.uiElName.tabContainer + " element provided (required)", 3);
                } else {
                    width = tcElem.offsetWidth;
                }
            }
            return width;
        },
        /**
         * @private
         */
        getTableVpWidth: function() {
            return this.bodyViewportElement.offsetWidth;
        },
        /**
         * @private
         */
        isTabSizeChanged: function() {
            var changed = false;
            if ((this.getTableVpWidth() !== this.lastTableVpWidth) || (this.element.offsetHeight !== this.lastTableVpHeight)) {
                changed = true;
                this.lastTableVpWidth = this.getTableVpWidth();
                this.lastTableVpHeight = this.element.offsetHeight;
            }
            return changed;
        },
        /**
         * @private
         */
        isLayoutChanged: function() {
            var changed = false;
            if (this.cConfig.resp["layout-id"] !== this.lastLayoutId) {
                changed = true;
                this.lastLayoutId = this.cConfig.resp["layout-id"];
            }
            return changed;
        },
        /**
         * @private
         */
        makeItem: function(dgRowId, dgCol) {
            return this.dgSubscription.prefix + dgRowId + ":" + dgCol;
        },
        /**
         * @private
         */
        searchRowId: function(rowId) {
            var bufRowFound = -1;
            for (var bufRow = 0; bufRow < this.pageBuf.viewportSize; bufRow++) {
                if (this.pageBuf.rows[bufRow] && this.pageBuf.rows[bufRow].rowId === rowId) {
                    bufRowFound = bufRow;
                    break;
                }
            }
            return bufRowFound;
        },
        /**
         * @private
         */
        detectTouchDevice: function() {
            return !!("ontouchstart" in document.documentElement);
        },
        onLock: function() {
            var self = this;
            shmi.addClass(self.element, "locked");
            if (self.cConfig.quicksearch && self.cConfig.quicksearch.enable && self.ctrlPanelData.qSearchInput) {
                self.ctrlPanelData.qSearchInput.lock();
            }
        },
        onUnlock: function() {
            var self = this;
            shmi.removeClass(self.element, "locked");
            if (self.cConfig.quicksearch && self.cConfig.quicksearch.enable && self.ctrlPanelData.qSearchInput) {
                self.ctrlPanelData.qSearchInput.unlock();
            }
        }
    };

    shmi.extend(shmi.visuals.controls.ComplexTable2, shmi.visuals.core.BaseControl);
    shmi.registerControlType("complex-table2", shmi.visuals.controls.ComplexTable2, true);

    /*-- CT2 helper classes --*/

    /**
     * Creates a new ComplexTable2.CheckBox control.
     *
     * @param {HTMLElement} element <div></div>
     * @param {number} id bufRow 0..n, -1 for summary CB
     * @param {boolean} is3State
     * @param {number} initialState shmi.visuals.controls.ComplexTable2.c.CB_??
     * @param {object} ct2ref ct2 reference
     *
     * @constructor
     */
    shmi.visuals.controls.ComplexTable2.CheckBox = function(element, id, is3State, initialState, ct2ref) {
        this.ct2c = shmi.visuals.controls.ComplexTable2.c; // short cut
        this.css = shmi.visuals.controls.ComplexTable2.CheckBox.uiCssCl; // short cut
        this.element = element;
        this.id = id;
        this.is3tate = is3State;
        this.state = initialState;
        this.chgCallb = null;
        this.cbMouseListener = null;
        this.cbTouchListener = null;

        shmi.addClass(this.element, shmi.visuals.controls.ComplexTable2.uiCssCl.selCb);

        var cbEvents = {};
        cbEvents.onPress = function() {
            if (!ct2ref.locked) {
                shmi.addClass(this.element, this.css.sel);
            }
        }.bind(this);
        cbEvents.onRelease = function() {
            if (!ct2ref.locked) {
                shmi.removeClass(this.element, this.css.sel);
            }
        }.bind(this);
        cbEvents.onClick = function() {
            // process event
            if (!ct2ref.locked) {
                if (this.is3tate) {
                    switch (this.state) {
                    case this.ct2c.CB_UNCHK:
                        this.state = this.ct2c.CB_CHK;
                        break;
                    case this.ct2c.CB_CHK:
                        this.state = this.ct2c.CB_UNDEF;
                        break;
                    default:
                        this.state = this.ct2c.CB_UNCHK;
                        break;
                    }
                } else {
                    this.state = (this.state === this.ct2c.CB_UNCHK) ? this.ct2c.CB_CHK : this.ct2c.CB_UNCHK;
                }
                this.updateState();
                this.fireCbChg();
            }
        }.bind(this);
        this.cbMouseListener = new shmi.visuals.io.MouseListener(this.element, cbEvents);
        this.cbTouchListener = new shmi.visuals.io.TouchListener(this.element, cbEvents);

        this.updateState();
        this.setEventsEnabled(true);
    };
    shmi.visuals.controls.ComplexTable2.CheckBox.prototype = {
        setChangeCallback: function(listener) { // null: no event listening
            this.chgCallb = listener;
        },
        setEventsEnabled: function(enabled) {
            if (enabled) {
                this.cbMouseListener.enable();
                this.cbTouchListener.enable();
            } else {
                this.cbMouseListener.disable();
                this.cbTouchListener.disable();
            }
        },
        setState: function(state) {
            this.state = state;
            this.updateState();
        },
        getState: function() {
            return this.state;
        },
        /**
         * @private
         */
        updateState: function() {
            shmi.removeClass(this.element, this.css.ck);
            shmi.removeClass(this.element, this.css.unck);
            shmi.removeClass(this.element, this.css.undef);
            switch (this.state) {
            case this.ct2c.CB_UNCHK:
                shmi.addClass(this.element, this.css.unck);
                break;
            case this.ct2c.CB_CHK:
                shmi.addClass(this.element, this.css.ck);
                break;
            default:
                shmi.addClass(this.element, this.css.undef);
                break;
            }
        },
        /**
         * @private
         */
        fireCbChg: function() {
            if (this.chgCallb) {
                this.chgCallb(this.id, this.state);
            }
        }
    };

    /**
     * Creates a new ComplexTable2.RowSelector control.
     *
     * @param {HTMLElement} element <tr>...</tr>
     * @param {number} id bufRow 0..n
     * @param {number} initialState shmi.visuals.controls.ComplexTable2.c.CB_??
     * @param {object} ct2ref ct2 reference
     *
     * @constructor
     */
    shmi.visuals.controls.ComplexTable2.RowSelector = function(element, id, initialState, ct2ref) {
        var self = this;
        self.ct2c = shmi.visuals.controls.ComplexTable2.c; // short cut
        self.css = shmi.visuals.controls.ComplexTable2.RowSelector.uiCssCl; // short cut
        self.element = element;
        self.id = id;
        self.state = initialState;
        self.chgCallb = null;
        self.rsMouseListener = null;
        self.rsTouchListener = null;

        var rsEvents = {};
        rsEvents.onPress = function() {
            if (!ct2ref.locked) {
                shmi.addClass(self.element, self.css.sel);
            }
        };
        rsEvents.onRelease = function() {
            if (!ct2ref.locked) {
                shmi.removeClass(self.element, self.css.sel);
            }
        };
        rsEvents.onClick = function(x, y, evt) {
            // process event
            if (!ct2ref.locked) {
                self.shiftPressed = evt.shiftKey;
                self.ctrlPressed = evt.ctrlKey;
                self.state = (self.state === self.ct2c.CB_UNCHK) ? self.ct2c.CB_CHK : self.ct2c.CB_UNCHK;
                self.updateState();
                self.fireRsChg();
            }
        };
        self.rsMouseListener = new shmi.visuals.io.MouseListener(self.element, rsEvents);
        self.rsTouchListener = new shmi.visuals.io.TouchListener(self.element, rsEvents);

        self.updateState();
        self.setEventsEnabled(true);
    };
    shmi.visuals.controls.ComplexTable2.RowSelector.prototype = {
        setChangeCallback: function(listener) { // null: no event listening
            this.chgCallb = listener;
        },
        setEventsEnabled: function(enabled) {
            if (enabled) {
                this.rsMouseListener.enable();
                this.rsTouchListener.enable();
            } else {
                this.rsMouseListener.disable();
                this.rsTouchListener.disable();
            }
        },
        setState: function(state) {
            this.state = state;
            this.updateState();
        },
        getState: function() {
            return this.state;
        },
        /**
         * @private
         */
        updateState: function() {
            shmi.removeClass(this.element, this.css.ck);
            shmi.removeClass(this.element, this.css.unck);
            if (this.state === this.ct2c.CB_CHK) {
                shmi.addClass(this.element, this.css.ck);
            } else {
                shmi.addClass(this.element, this.css.unck);
            }
        },
        /**
         * @private
         */
        fireRsChg: function() {
            if (this.chgCallb) {
                this.chgCallb(this.id, this.state, this.shiftPressed, this.ctrlPressed);
            }
        }
    };

    /**
     * Creates a new ComplexTable2.FieldHeader control.
     *
     * @param {HTMLElement} element <div></div>
     * @param {number} id
     * @param {boolean} isSortable
     * @param {number} initialState shmi.visuals.controls.ComplexTable2.c.SORT_??
     * @param {string} caption (already localized !!)
     *
     * @constructor
     */
    shmi.visuals.controls.ComplexTable2.FieldHeader = function(element, id, isSortable, initialState, caption, ct2Inst, fldName, icon) {
        this.ct2c = shmi.visuals.controls.ComplexTable2.c; // short cut
        this.css = shmi.visuals.controls.ComplexTable2.FieldHeader.uiCssCl; // short cut
        this.ct2Inst = ct2Inst;
        this.element = element;
        this.id = id;
        this.isSortable = isSortable;
        this.state = initialState;
        this.caption = caption;
        this.chgCallb = null;
        this.cbMouseListener = null;
        this.cbTouchListener = null;
        this.fieldName = (fldName !== undefined) ? fldName : null;
        this.icon = (icon === undefined) ? null : icon;

        shmi.addClass(this.element, shmi.visuals.controls.ComplexTable2.uiCssCl.fldHeader);
        //element.textContent = this.caption;
        if (this.ct2Inst.headerMode === "ICON") {
            if (this.icon !== null) {
                shmi.addClass(element, this.icon);
            }
        } else {
            element.textContent = this.caption;
        }

        if (this.isSortable) {
            shmi.addClass(this.element, this.css.sortable);

            var cbEvents = {};
            cbEvents.onPress = function() {
                if (!this.ct2Inst.locked) {
                    shmi.addClass(this.element, this.css.sel);
                }
            }.bind(this);
            cbEvents.onRelease = function() {
                if (!this.ct2Inst.locked) {
                    shmi.removeClass(this.element, this.css.sel);
                }
            }.bind(this);
            cbEvents.onClick = function() {
                if (!this.ct2Inst.locked) {
                    // process event
                    switch (this.state) {
                    case this.ct2c.SORT_UP:
                        this.state = this.ct2c.SORT_DOWN;
                        break;
                    case this.ct2c.SORT_DOWN:
                        this.state = this.ct2c.NO_SORT;
                        break;
                    default:
                        this.state = this.ct2c.SORT_UP; // in all other cases, it's impossible to reach NO_SORT by event
                        break;
                    }
                    this.updateState();
                    this.fireSortChg();
                }
            }.bind(this);
            this.cbMouseListener = new shmi.visuals.io.MouseListener(this.element, cbEvents);
            this.cbTouchListener = new shmi.visuals.io.TouchListener(this.element, cbEvents);
        }

        this.updateState();
        this.setEventsEnabled(true);
    };
    shmi.visuals.controls.ComplexTable2.FieldHeader.prototype = {
        setChangeCallback: function(listener) { // null: no event listening
            this.chgCallb = listener;
        },
        setEventsEnabled: function(enabled) {
            if (this.cbMouseListener) {
                if (enabled) {
                    this.cbMouseListener.enable();
                    this.cbTouchListener.enable();
                } else {
                    this.cbMouseListener.disable();
                    this.cbTouchListener.disable();
                }
            }
        },
        setState: function(state) {
            this.state = state;
            this.updateState();
        },
        getState: function() {
            return this.state;
        },
        /**
         * @private
         */
        updateState: function() {
            if (this.isSortable) {
                shmi.removeClass(this.element, this.css.noSort);
                shmi.removeClass(this.element, this.css.sortUp);
                shmi.removeClass(this.element, this.css.sortDn);
                switch (this.state) {
                case this.ct2c.SORT_UP:
                    shmi.addClass(this.element, this.css.sortUp);
                    break;
                case this.ct2c.SORT_DOWN:
                    shmi.addClass(this.element, this.css.sortDn);
                    break;
                default: // NO_SORT
                    shmi.addClass(this.element, this.css.noSort);
                    break;
                }
            } else {
                shmi.addClass(this.element, this.css.noSort);
            }
        },
        /**
         * @private
         */
        fireSortChg: function() {
            if (this.chgCallb) {
                this.chgCallb(this.id, this.state);
            }
        }
    };

    shmi.visuals.controls.ComplexTable2.instIDs = [];

    shmi.visuals.controls.ComplexTable2.uiElName = {};
    (function() {
        /* The following ui element names are used in CT2's JS code. Change it here if necessary.
         All other ui element names may be changed in the CT2 template(s).
         */
        var __ct2sc = shmi.visuals.controls.ComplexTable2.uiElName;
        __ct2sc.tabContContainer = "ct2-table-content-container";
        __ct2sc.tabHeadContainer = "ct2-table-header-container";
        __ct2sc.tabContainer = "ct2-table-container";
        __ct2sc.tbodyContainer = "ct2-table-body-container";
        __ct2sc.thead = "ct2-table-header";
        __ct2sc.table = "ct2-table";
        __ct2sc.txtFiltersHead = "ct2-table-filters";
        __ct2sc.tbody = "ct2-table-body";
        __ct2sc.label = "ct2-label";
        __ct2sc.ctrlPanel = "ct2-ctrl-panel";
        __ct2sc.footer = "ct2-footer";

        shmi.visuals.controls.ComplexTable2.uiCssCl = {};
        __ct2sc = shmi.visuals.controls.ComplexTable2.uiCssCl;
        /* The following ui element class names are used in CT2's JS code. Change it here if necessary.
         All other ui element class names be changed in the CT2 template(s).
         */
        // (static) styles for dyn. created elements
        // !!! keep them short !!! because they are used in each table row / cell
        __ct2sc.colCb = "ct2-colcb";
        __ct2sc.colPfx = "ct2-col"; // prefix, the classes are ct2-col1, ct2-col2, ..., ct2-col<n>
        __ct2sc.thCont = "ct2-thcont";
        __ct2sc.tdCont = "ct2-tdcont";
        __ct2sc.selCb = "ct2-selcb";
        __ct2sc.evenRow = "ct2-evenRow";
        __ct2sc.fldHeader = "ct2-fhead";
        __ct2sc.colTxtFilterCont = "ct2-filter";
        __ct2sc.colTxtFilterLineCont = "line";
        __ct2sc.colTxtFilterIcon = "filter-icon";
        //__ct2sc.activeRowMarker = "ct2-active-row-marker";
        // dynamically switched styles
        __ct2sc.multiline = "multiline";
        __ct2sc.multiselect = "ct2-multisel";
        __ct2sc.sbNotUsed = "ct2-sb-not-used";
        __ct2sc.sbMinimized = "ct2-sb-minimized";
        __ct2sc.sbNormal = "ct2-sb-normal";
        __ct2sc.rowDisplVisible = "ct2-row-display-visible";
        __ct2sc.rowDisplInvisible = "ct2-row-display-invisible";
        __ct2sc.emptyTab = "ct2-empty-table";
        __ct2sc.ctrlPanelMin = "ct2-ctrl-panel-minimized";
        __ct2sc.ctrlPanelNormal = "ct2-ctrl-panel-normal";
        __ct2sc.activeRow = "ct2-active-row";
        __ct2sc.touchDev = "touch";
        __ct2sc.noTouchDev = "notouch";

        shmi.visuals.controls.ComplexTable2.c = {};
        __ct2sc = shmi.visuals.controls.ComplexTable2.c;
        //--
        __ct2sc.MIN_BUF_SIZE = 150;
        __ct2sc.CONST_LINE_HEIGHT = false;
        __ct2sc.DEBOUNCE_TO_RESIZE = 150;
        __ct2sc.DEFAULT_LINE_HEIGHT = 20;
        __ct2sc.MULTI_SELECT_MODE = "3-state-clear"; // "3-state", "2-state" (NYI)
        __ct2sc.SCROLL_EVT_TYPE_NONE = -1;
        __ct2sc.SCROLL_EVT_TYPE_TOUCH = 11;
        __ct2sc.SCROLL_EVT_TYPE_RELEASE = 12;
        __ct2sc.SCROLL_EVT_TYPE_SB_BTN_UP = 101;
        __ct2sc.SCROLL_EVT_TYPE_SB_BTN_DOWN = 102;
        __ct2sc.SCROLL_EVT_TYPE_SB_SLIDER_CHG = 103;
        __ct2sc.SCROLL_EVT_TYPE_SB_LARGE_CHG = 104;
        __ct2sc.SCROLL_EVT_TYPE_V_DRAG_UP = 201;
        __ct2sc.SCROLL_EVT_TYPE_V_DRAG_DOWN = 202;
        __ct2sc.SCROLL_EVT_TYPE_V_SWIPE_UP = 301;
        __ct2sc.SCROLL_EVT_TYPE_V_SWIPE_DOWN = 302;
        __ct2sc.SCROLL_EVT_TYPE_H_SWIPE_LEFT = 401;
        __ct2sc.SCROLL_EVT_TYPE_H_SWIPE_RIGHT = 402;
        __ct2sc.SCROLL_EVT_TYPE_RESIZE = 501;
        __ct2sc.SCROLL_EVT_TYPE_RESIZE_LAYOUT_CHG = 502;
        //--
        __ct2sc.SELECT_TYPE_NOTHING = -1;
        __ct2sc.SELECT_TYPE_ALL = 1;
        __ct2sc.SELECT_TYPE_SEL_ROWS = 2;
        //--
        __ct2sc.CB_UNCHK = -1;
        __ct2sc.CB_CHK = 1;
        __ct2sc.CB_UNDEF = 2;
        __ct2sc.NO_SORT = -1;
        __ct2sc.SORT_UP = 1;
        __ct2sc.SORT_DOWN = 2;
        //--

        shmi.visuals.controls.ComplexTable2.CheckBox.uiCssCl = {};
        __ct2sc = shmi.visuals.controls.ComplexTable2.CheckBox.uiCssCl;
        // dyn. created checkbox classes
        // !!! keep them short !!! because they are used in each table row
        __ct2sc.sel = "ct2-cb-sel";
        __ct2sc.ck = "ct2-cb-ck";
        __ct2sc.unck = "ct2-cb-unck";
        __ct2sc.undef = "ct2-cb-undef";

        shmi.visuals.controls.ComplexTable2.RowSelector.uiCssCl = {};
        __ct2sc = shmi.visuals.controls.ComplexTable2.RowSelector.uiCssCl;
        // dyn. created row selector classes
        // !!! keep them short !!! because they are used in each table row
        __ct2sc.sel = "ct2-rs-sel";
        __ct2sc.ck = "ct2-rs-ck";
        __ct2sc.unck = "ct2-rs-unck";

        shmi.visuals.controls.ComplexTable2.FieldHeader.uiCssCl = {};
        __ct2sc = shmi.visuals.controls.ComplexTable2.FieldHeader.uiCssCl;
        // dyn. created (sortable) header field classes
        __ct2sc.sel = "ct2-fh-sel";
        __ct2sc.sortUp = "ct2-fh-sort-up";
        __ct2sc.sortDn = "ct2-fh-sort-down";
        __ct2sc.noSort = "ct2-fh-no-sort";
        __ct2sc.sortable = "ct2-fh-sortable";
    })();
}());

(function() {
    'use strict';

    const modifiers = {
        MODE: {
            FLOAT: null,
            STACKED: "stacked-layout",
            INLINE: "inline-layout",
            FLEX: "flex-layout",
            IQ_FLEX: "iq-flex-layout"
        },
        ALIGN: {
            HORIZONTAL: {
                LEFT: "left-aligned",
                CENTER: "centered",
                RIGHT: "right-aligned"
            },
            VERTICAL: {
                TOP: "top",
                MIDDLE: "middle",
                BOTTOM: "bottom"
            },
            PRIMARY: {
                START: "flex-primary-align-start",
                CENTER: "flex-primary-align-center",
                END: "flex-primary-align-end",
                SPACE_BETWEEN: "flex-primary-align-space-between",
                SPACE_AROUND: "flex-primary-align-space-around",
                SPACE_EVENLY: "flex-primary-align-space-evenly"
            },
            SECONDARY: {
                START: "flex-secondary-align-start",
                CENTER: "flex-secondary-align-center",
                END: "flex-secondary-align-end",
                STRETCH: "flex-secondary-align-stretch"
            },
            LINE: {
                START: "flex-line-align-start",
                CENTER: "flex-line-align-center",
                END: "flex-line-align-end",
                STRETCH: "flex-line-align-stretch",
                SPACE_BETWEEN: "flex-line-align-space-between",
                SPACE_AROUND: "flex-line-align-space-around"
            }
        },
        AUTO: {
            WIDTH: "auto-width",
            MARGIN: "auto-margin"
        },
        FLEX: {
            DISTRIBUTE: "distribute-evenly",
            WRAP: "flex-wrap",
            ALL: "flex-all",
            COLUMN: "column-orientation",
            NONE: "flex-none"
        }
    };

    /**
     * Maps config options to modifiers.
     */
    const modifierMap = {
        "auto-width": [
            { value: true, modifier: modifiers.AUTO.WIDTH }
        ],
        "auto-margin": [
            { value: true, modifier: modifiers.AUTO.MARGIN }
        ],
        "h-alignment": [
            { value: "left", modifier: modifiers.ALIGN.HORIZONTAL.LEFT },
            { value: "center", modifier: modifiers.ALIGN.HORIZONTAL.CENTER },
            { value: "right", modifier: modifiers.ALIGN.HORIZONTAL.RIGHT }
        ],
        "v-alignment": [
            { value: "top", modifier: modifiers.ALIGN.VERTICAL.TOP },
            { value: "middle", modifier: modifiers.ALIGN.VERTICAL.MIDDLE },
            { value: "bottom", modifier: modifiers.ALIGN.VERTICAL.BOTTOM }
        ],
        "flex-orientation": [
            { value: "column", modifier: modifiers.FLEX.COLUMN }
        ],
        "flex-distribute": [
            { value: true, modifier: modifiers.FLEX.DISTRIBUTE }
        ],
        "flex-all": [
            { value: true, modifier: modifiers.FLEX.ALL }
        ],
        "flex-none": [
            { value: true, modifier: modifiers.FLEX.NONE }
        ],
        "flex-wrap": [
            { value: true, modifier: modifiers.FLEX.WRAP }
        ],
        "flex-primary-align": [
            { value: "start", modifier: modifiers.ALIGN.PRIMARY.START },
            { value: "center", modifier: modifiers.ALIGN.PRIMARY.CENTER },
            { value: "end", modifier: modifiers.ALIGN.PRIMARY.END },
            { value: "space-between", modifier: modifiers.ALIGN.PRIMARY.SPACE_BETWEEN },
            { value: "space-around", modifier: modifiers.ALIGN.PRIMARY.SPACE_AROUND },
            { value: "space-evenly", modifier: modifiers.ALIGN.PRIMARY.SPACE_EVENLY }
        ],
        "flex-secondary-align": [
            { value: "start", modifier: modifiers.ALIGN.SECONDARY.START },
            { value: "center", modifier: modifiers.ALIGN.SECONDARY.CENTER },
            { value: "end", modifier: modifiers.ALIGN.SECONDARY.END },
            { value: "stretch", modifier: modifiers.ALIGN.SECONDARY.STRETCH }
        ],
        "flex-line-align": [
            { value: "start", modifier: modifiers.ALIGN.LINE.START },
            { value: "center", modifier: modifiers.ALIGN.LINE.CENTER },
            { value: "end", modifier: modifiers.ALIGN.LINE.END },
            { value: "stretch", modifier: modifiers.ALIGN.LINE.STRETCH },
            { value: "space-between", modifier: modifiers.ALIGN.LINE.SPACE_BETWEEN },
            { value: "space-around", modifier: modifiers.ALIGN.LINE.SPACE_AROUND }
        ]
    };

    const containerVariants = {
        float: {
            modifierSettings: ["auto-width", "auto-margin", "h-alignment"]
        },
        stacked: {
            baseModifier: modifiers.MODE.STACKED,
            modifierSettings: ["auto-margin"]
        },
        inline: {
            baseModifier: modifiers.MODE.INLINE,
            modifierSettings: ["auto-width", "auto-margin", "h-alignment", "v-alignment"]
        },
        flex: {
            baseModifier: modifiers.MODE.FLEX,
            modifierSettings: ["auto-width", "auto-margin", "flex-orientation", "h-alignment", "v-alignment", "flex-distribute", "flex-all", "flex-none"]
        },
        iqflex: {
            baseModifier: modifiers.MODE.IQ_FLEX,
            modifierSettings: ["flex-orientation", "flex-wrap", "flex-primary-align", "flex-secondary-align", "flex-line-align", "flex-none"]
        }
    };

    function clearAllModifiers(element) {
        const allModifiers = [
            ...Object.values(modifiers.MODE),
            ...Object.values(modifiers.ALIGN.HORIZONTAL),
            ...Object.values(modifiers.ALIGN.VERTICAL),
            ...Object.values(modifiers.AUTO),
            ...Object.values(modifiers.FLEX)
        ].filter((val) => val !== null);

        allModifiers.forEach((val) => shmi.removeClass(element, val));
    }

    function applyModifiers(element, config) {
        const variantSettings = containerVariants[config.type];
        if (!variantSettings) {
            console.error("[Container]", "unknown layout type:", config.type);
            return;
        }

        // Add base modifier if required
        if (variantSettings.baseModifier) {
            shmi.addClass(element, variantSettings.baseModifier);
        }

        // Add conditional modifiers
        variantSettings.modifierSettings.forEach((modifierKey) => {
            const modifierConditions = modifierMap[modifierKey];
            if (!modifierConditions || !Array.isArray(modifierConditions)) {
                console.error("[Container]", "missing modifier info:", modifierKey);
                return;
            }

            // Get modifier for the config value.
            const modifierInfo = modifierConditions.find(({ value }) => config[modifierKey] === value);
            if (modifierInfo) {
                shmi.addClass(element, modifierInfo.modifier);
            }
        });
    }

    function getMarginCompensator(self) {
        let marginCompensator = self.element.querySelector("div.margin-compensator"),
            oldHtml = null;

        if (!marginCompensator || (marginCompensator.parentNode !== self.element)) {
            marginCompensator = document.createElement("DIV");
            shmi.addClass(marginCompensator, "margin-compensator");
            oldHtml = self.element.innerHTML;
            self.element.innerHTML = "";
            marginCompensator.innerHTML = oldHtml;
            self.element.appendChild(marginCompensator);
        }

        return marginCompensator;
    }

    shmi.visuals.controls.Container = function(element, config) {
        var self = this;
        self.element = element;
        self.config = config || {};

        self.parseAttributes();

        shmi.def(self.config, 'class-name', "container");
        shmi.def(self.config, 'name', null);
        shmi.def(self.config, 'type', "stacked"); //"stacked", "float", "inline", "flex"
        shmi.def(self.config, 'auto-width', false);
        shmi.def(self.config, 'auto-margin', false);
        shmi.def(self.config, 'h-alignment', "left"); //"left", "center", "right"
        shmi.def(self.config, 'v-alignment', "top"); //"top", "middle", "bottom"
        shmi.def(self.config, 'flex-orientation', "row"); //"row", "column"
        shmi.def(self.config, 'flex-distribute', false);
        shmi.def(self.config, 'flex-all', false);
        shmi.def(self.config, 'flex-none', false);
        shmi.def(self.config, 'flex-wrap', false);
        shmi.def(self.config, 'flex-primary-align', "start");
        shmi.def(self.config, 'flex-secondary-align', "stretch");
        shmi.def(self.config, 'flex-line-align', "start");

        self.controls = [];
        self.marginCompensator = null;

        self.startup();
    };

    shmi.visuals.controls.Container.prototype = {
        uiType: "container",
        isContainer: true,
        getClassName: function() {
            return "Container";
        },
        /**
         * Parses child controls when control is registered in layout
         *
         * @param {function} onDone function to call on completion
         * @returns {undefined}
         */
        onRegister: function(onDone) {
            var self = this;

            //clear active css-modifiers, in case of reconfiguration
            clearAllModifiers(self.element);
            applyModifiers(self.element, self.config);
            self.marginCompensator = getMarginCompensator(self);
            self.parseChildren(self.marginCompensator, onDone);
        },
        onEnable: function() {
            const self = this;
            self.controls.forEach((c) => c.enable());
        },
        onDisable: function() {
            const self = this;
            self.controls.forEach((c) => c.disable());
        },
        onLock: function() {

        },
        onUnlock: function() {

        },
        onAddControl: function(options, callback) {
            const self = this,
                cm = shmi.requires("visuals.tools.controller"),
                controls = [];

            let initToken = null;

            if (!self.initialized) {
                throw new Error("Control not initialized yet.");
            }

            if (!Array.isArray(options)) {
                options = [options];
            }
            options.forEach(function(option) {
                const control = shmi.createControl(option.ui, self.marginCompensator, option.config, "DIV", null, false);
                if (control !== null) {
                    if (option.style && typeof option.style === "object") {
                        Object.assign(control.element.style, option.style);
                    }
                    controls.push(control);
                }
            });
            initToken = shmi.waitOnInit(controls, function() {
                if (initToken) {
                    const idx = self._init_.tokens.indexOf(initToken);
                    if (idx !== -1) {
                        self._init_.tokens.splice(idx, 1);
                    }
                    initToken.unlisten();
                    initToken = null;
                }

                const promises = options.map(function(opt, jdx) {
                    const control = controls[jdx];

                    if (opt.controller) {
                        cm.create(opt.controller.name, opt.controller, control.getName());
                    }

                    if (self.isActive()) {
                        control.enable();
                    }

                    if (!Array.isArray(opt.children) || !opt.children.length) {
                        return Promise.resolve();
                    }

                    return new Promise((resolve) => {
                        control.addControl(opt.children, (childErr) => resolve(childErr));
                    });
                });

                Promise.all(promises).then((result) => {
                    const errors = result.filter((r) => !!r);
                    callback(errors.length ? errors[0] : null, controls);
                });
            });
            if (initToken) {
                self._init_.tokens.push(initToken);
            }
        }
    };

    shmi.extend(shmi.visuals.controls.Container, shmi.visuals.core.BaseControl);
}());

/**
 * datetime-display control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "datetime-display",
 *     "name": null,
 *     "item": null,
 *     "display-utc": false,
 *     "display-format": "$YYYY-$MM-$DD $HH:$mm:$ss",
 *     "input-format": "$X",
 *     "invalid-text": "---"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * item {string}: Name of the item from which to get the timestamp to display
 * display-utc {boolean}: Whether or not the displayed time should be UTC or not
 * display-format {string}: Format string for the displayed date
 * input-format {string}: Format string to use when parsing the items value
 * invalid-text {string}: String to display if the item does not contain a valid
 *  timestamp.
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "datetime-display", // control name in camel-case
        uiType = "datetime-display", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    var defConfig = {
        "class-name": "text2 datetime-display",
        "name": null,
        "item": null,
        "display-utc": false,
        "display-format": "$YYYY-$MM-$DD $HH:$mm:$ss",
        "input-format": "$X",
        "invalid-text": "---"
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START

    /**
     * Sets the displayed text and tooltip of the control.
     *
     * @param {*} self Reference to the control.
     * @param {string} str Text to display.
     */
    function setText(self, str) {
        self.element.textContent = str;
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            subscriptionListener: null,
            localizedDisplayFormat: null,
            localizedInvalidText: null,
            rafRunning: false,
            lastValue: null
        },
        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            dtUtils: "visuals.tools.date"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                this.vars.localizedDisplayFormat = shmi.localize(this.config["display-format"]);
                this.vars.localizedInvalidText = shmi.localize(this.config["invalid-text"]);
                setText(this, this.vars.localizedInvalidText);
            },
            /* called when control is enabled */
            onEnable: function() {
                if (this.config["item"] && !this.vars.subscriptionListener) {
                    this.vars.subscriptionListener = this.imports.im.subscribeItem(this.config["item"], this);
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                if (this.vars.subscriptionListener) {
                    this.vars.subscriptionListener.unlisten();
                }
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type) {
                if (type === shmi.c("TYPE_INT") || type === shmi.c("TYPE_FLOAT")) {
                    this.vars.lastValue = this.imports.dtUtils.parseDateTime(String(value), this.config["input-format"] || "$X");
                } else if (!this.config["input-format"]) {
                    this.vars.lastValue = null;
                } else {
                    this.vars.lastValue = this.imports.dtUtils.parseDateTime(String(value), this.config["input-format"]);
                }

                // Only attempt to update the DOM during an animation frame.
                // Prevents floods of redraws and may perform better anyways.
                if (!this.vars.rafRunning) {
                    this.vars.rafRunning = true;
                    shmi.raf(function onDraw() {
                        if (!this.vars.lastValue) {
                            setText(this, this.vars.localizedInvalidText);
                        } else {
                            setText(this, this.imports.dtUtils.formatDateTime(this.vars.lastValue, {
                                datestring: this.vars.localizedDisplayFormat,
                                utc: this.config["display-utc"]
                            }));
                        }

                        this.vars.rafRunning = false;
                    }.bind(this));
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

shmi.pkg("visuals.controls");
/**
 * DialogBox control to display content in an overlay window.
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - base element of the DialogBox
 * @param config - configuration of the DialogBox
 */
shmi.visuals.controls.DialogBox = function(element, config) {
    /* check for required packages */
    shmi.requires("visuals.gfx.Movable");
    shmi.requires("visuals.io.MouseListener");
    shmi.requires("visuals.io.TouchListener");
    this.element = element;

    /* control configuration */
    this.config = config || {};

    /* parse option attributes from html element */
    this.parseAttributes();
    /* set default options */
    shmi.def(this.config, 'class-name', 'dialog-box');
    shmi.def(this.config, 'template', 'default/dialog-box');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'class-name', 'dialog-box');
    shmi.def(this.config, 'initial-state', 'hidden');
    shmi.def(this.config, 'collapsible', false);
    shmi.def(this.config, 'draggable', false);
    shmi.def(this.config, 'top-level', false);
    shmi.def(this.config, 'content-template', null);
    shmi.def(this.config, 'tab-limit', true);

    /* internal variables */
    this.collapsed = false;
    this.hidden = true;
    this.active = false;
    this.controls = [];
    this.childrenParsed = false;
    this.bgDiv = null;
    this.dragging = false;
    this.mouselistener = null;
    this.touchlistener = null;
    this.contentElement = null;
    this.originalParent = null;
    this.tokens = [];

    this.element.style.visibility = "hidden";

    /* control startup */
    this.startup();
};

shmi.visuals.controls.DialogBox.prototype = {
    uiType: "dialog-box",
    isContainer: true,
    getClassName: function() {
        return "DialogBox";
    },
    events: ["open", "close"],
    /**
     * Initializes the DialogBox
     *
     * @override BaseControl.init
     */
    onTemplate: function(response, failed, onDone) {
        var self = this;
        if (!failed) {
            self.element.innerHTML = response;
            self.contentElement = shmi.getUiElement("dialog-box-content", self.element);
            if (self.contentElement && self.config["content-template"]) {
                var templateUrl = (self.config["content-template"].indexOf(shmi.c("RES_URL_PREFIX")) === 0) ?
                    self.config["content-template"] :
                    shmi.c("TEMPLATE_PATH") + self.config["content-template"] + shmi.c("TEMPLATE_EXT");
                shmi.loadResource(templateUrl, function(contentResponse, contentFailed) {
                    if (!contentFailed) {
                        self.contentElement.innerHTML = contentResponse;
                    }
                    onDone();
                });
            } else {
                onDone();
            }
        } else {
            onDone();
        }
    },
    onRegister: function(onDone) {
        var self = this;

        if (self.contentElement) {
            self.parseChildren(self.contentElement, onDone);
        } else {
            onDone();
        }
    },
    onInit: function() {
        var self = this,
            inl = shmi.requires("visuals.tools.iterate.iterateNodeList"),
            closeButtons = [];

        if (self.contentElement) {
            closeButtons = shmi.getUiElements("button", self.element);
            inl(closeButtons, function(el) {
                var but = null;
                if (el.parentNode && !shmi.testParentChild(self.contentElement, el)) {
                    but = shmi.createControl("button", el, {}, "DIV", "from");
                    but.onClick = function() {
                        self.hide();
                    };
                }
            });
        }

        this.titleElement = shmi.getUiElement('dialog-box-title', this.element);
        if (!this.titleElement) {
            shmi.log('[DialogBox] no dialog-box-title element provided', 1);
        }
        this.frameElement = shmi.getUiElement('dialog-box-frame', this.element);
        if (!this.frameElement) {
            shmi.log('[DialogBox] no dialog-box-frame element provided', 3);
            return;
        }

        /* all required elements found */
        this.hidden = this.config['initial-state'] !== 'visible';
        if (this.hidden) {
            shmi.addClass(this.element, 'hidden');
        }
        this.element.style.visibility = "";

        this.containerElement = shmi.getUiElement('dialog-content', this.element);
        if (!this.containerElement) {
            shmi.log('[DialogBox] no dialog-content element provided', 3);
            return;
        }

        this.element.setAttribute('tabindex', '-1');
        this.mover = new shmi.visuals.gfx.Movable(this.containerElement);
        this.mover.priority = true;
        var functions = {};
        functions.onClick = function(x, y) {
            if (this.config['close-on-click']) {
                this.hide();
            }
        }.bind(this);
        if (this.config.draggable === true) {
            functions.onDrag = function(dx, dy, event) {
                event.preventDefault();
                this.mover.translate(dx, dy);
                for (var i = 0; i < this.controls.length; i++) {
                    try {
                        this.controls[i].offsetX = this.mover.startX + this.mover.tx;
                    } catch (exc) {
                        shmi.log("[Dialog-Box] - Can not set control offset", 2);
                    }
                }
                if (!this.dragging) {
                    this.dragging = true;
                }
            }.bind(this);
        }
        functions.onRotate = function(rot) {
            if (this.config['rotation-enabled']) {
                this.mover.rotate(rot * 57.3);
            }
        }.bind(this);
        functions.onScale = function(s) {
            if (this.config['scaling-enabled']) {
                this.mover.scale(s);
            }
        }.bind(this);
        if (this.titleElement) {
            if (this.config.title) {
                this.titleElement.textContent = shmi.localize(this.config.title);
            }
        }

        functions.onPress = function(x, y, event) {
            //event.preventDefault();
            shmi.addClass(this.element, 'pressed');
            if (document.activeElement !== this.element) {
                this.element.focus();
            }
            shmi.log("[DialogBox] pressed", 1);
        }.bind(this);
        functions.onRelease = function() {
            this.dragging = false;
            shmi.removeClass(this.element, 'pressed');
            shmi.log("[DialogBox] released", 1);
        }.bind(this);
        this.mouselistener = new shmi.visuals.io.MouseListener(this.containerElement, functions);
        this.touchlistener = new shmi.visuals.io.TouchListener(this.containerElement, functions);
    },
    /**
     * Displays the DialogBox
     *
     */
    show: function() {
        var self = this;
        if (!self.active) {
            shmi.log("[DialogBox] cannot show dialog-box without enabling it first", 3);
            return;
        }
        if (!self.hidden) {
            return;
        }
        self.hidden = false;

        if (self.config['top-level'] === true) {
            self.originalParent = self.element.parentNode;
            self.element = document.body.appendChild(self.element);
        }

        if (self.config['cover-background']) {
            if (self.bgDiv) {
                shmi.removeClass(self.bgDiv, 'hidden');
            } else {
                self.bgDiv = document.createElement('div');
                shmi.addClass(self.bgDiv, 'dialog-box-overlay');
                self.bgDiv.setAttribute('tabindex', '-1');
                document.body.appendChild(self.bgDiv);
                shmi.removeClass(self.bgDiv, 'hidden');
            }
        }

        shmi.removeClass(self.element, 'hidden');

        if (self.config['tab-limit']) {
            // Enable element tabulator
            var tabulator = shmi.requires("visuals.tools.tabulator");
            self.tabLimit = tabulator.setTabParent(self.element);
        }

        self.enableControls();
        self.fire('open', {});
    },
    /**
     * Removes the DialogBox from DOM
     *
     */
    hide: function() {
        var self = this;
        if (this.hidden) {
            return;
        }
        this.hidden = true;
        if (!this.config['leave-controls-enabled']) {
            this.disableControls();
        }
        if (self.config['top-level'] === true) {
            self.element = self.originalParent.appendChild(self.element);
        }
        shmi.addClass(this.element, 'hidden');
        if (self.tabLimit) {
            self.tabLimit.unsetTabParent();
            self.tabLimit = null;
        }
        this.reset();
        if (this.config['cover-background']) {
            if (this.bgDiv) {
                shmi.addClass(this.bgDiv, 'hidden');
            }
        }
        this.fire('close', {});
    },
    reset: function() {
        if (this.active && this.mover) {
            this.mover.tx = 0;
            this.mover.ty = 0;
            this.mover.rot = 0;
            this.mover.s = 1.0;
            this.mover.sx = 1.0;
            this.mover.sy = 1.0;
            this.mover.update();
        }
    },
    /**
     * Enables the DialogBox
     *
     */
    onEnable: function() {
        var self = this;

        self.mouselistener.enable();
        self.touchlistener.enable();
        self.tokens.push(shmi.listen("enable", function(evt) {
            if ((evt.source.getName().indexOf(self.getName()) !== -1) && (evt.source.getName() !== self.getName())) {
                if (evt.source.element.getAttribute("data-function") === "close") {
                    evt.source.onClick = function onClick() {
                        self.hide();
                    };
                }
            }
        }, { "source.uiType": "button" }));

        if (!self.hidden) {
            /* set hidden status to prevent show() to return early */
            self.hidden = true;
            self.show();
        }
    },
    /**
     * Disables the DialogBox
     *
     */
    onDisable: function() {
        var self = this;
        self.hide();
        self.mouselistener.disable();
        self.touchlistener.disable();

        // remove overlay background element if present
        if (this.bgDiv) {
            this.bgDiv.parentNode.removeChild(this.bgDiv);
            this.bgDiv = null;
        }

        self.tokens.forEach(function(t) {
            t.unlisten();
        });
        self.tokens = [];
    },
    /**
     * Enables child controls of the DialogBox
     *
     */
    enableControls: function() {
        for (var i = 0; i < this.controls.length; i++) {
            try {
                this.controls[i].enable();
            } catch (exc) {
                console.error("[DialogBox]", "error enabling control:", this.controls[i], this.getName(), "Exception:", exc);
            }
        }
    },
    /**
     * Disables child controls of the DialogBox
     *
     */
    disableControls: function() {
        for (var i = 0; i < this.controls.length; i++) {
            try {
                this.controls[i].disable();
            } catch (exc) {
                console.error("[DialogBox]", "error disabling control:", this.controls[i], this.getName(), "Exception:", exc);
            }
        }
    },
    onAddControl: function(options, callback) {
        var self = this,
            cm = shmi.requires("visuals.tools.controller"),
            controls = [],
            classLabel = "[" + self.getClassName() + "] ",
            initToken = null;

        if (!self.initialized) {
            throw new Error(classLabel + "Control not initialized yet.");
        }

        if (!Array.isArray(options)) {
            options = [options];
        }

        if (!self.contentElement) {
            callback(new Error(classLabel + "Content element missing from template."), controls);
            return;
        }

        options.forEach(function(option) {
            var control = shmi.createControl(option.ui, self.contentElement, option.config, "DIV", null, false);
            if (control !== null) {
                if (option.style && typeof option.style === "object") {
                    let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                    iter(option.style, (value, name) => {
                        control.element.style[name] = value;
                    });
                }
                controls.push(control);
            }
        });

        initToken = shmi.waitOnInit(controls, function() {
            var idx = -1,
                errors = [],
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken.unlisten();
                initToken = null;
            }

            options.forEach(function(opt, jdx) {
                var t = null,
                    control = controls[jdx];

                if (opt.controller) {
                    cm.create(opt.controller.name, opt.controller, control.getName());
                }
                if (self.isActive() && !self.hidden) {
                    control.enable();
                }
                if (Array.isArray(opt.children) && opt.children.length) {
                    t = tm.createTask("child control");
                    t.run = function() {
                        control.addControl(opt.children, function(childErr, childOptions) {
                            if (childErr) {
                                errors.push(childErr);
                            }
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            if (tasks.length) {
                tl = tm.createTaskList(tasks, false);
                tl.onComplete = function() {
                    callback(errors.length ? errors[0] : null, controls);
                };
                tl.run();
            } else {
                callback(errors.length ? errors[0] : null, controls);
            }
        });
        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    }
};

shmi.extend(shmi.visuals.controls.DialogBox, shmi.visuals.core.BaseControl);

/**
 * duration-display control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "duration-display",
 *     "name": null,
 *     "target-ts-item": null,
 *     "target-ts-format": "$X",
 *     "current-ts-item": "Systemzeit",
 *     "current-ts-format": "$X",
 *     "display-preset": "compact",
 *     "invalid-text": "---"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * item {string}: Name of the item from which to get the timestamp to display
 * invalid-text {string}: String to display if the item does not contain a valid
 *  timestamp.
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "duration-display", // control name in camel-case
        uiType = "duration-display", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    var defConfig = {
        "class-name": "text2 duration-display",
        "name": null,
        "target-ts-item": null,
        "target-ts-format": "$X",
        "current-ts-item": "Systemzeit",
        "current-ts-format": "$X",
        "display-preset": "compact",
        "invalid-text": "---"
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START

    /**
     * Sets the displayed text and tooltip of the control.
     *
     * @param {*} self Reference to the control.
     * @param {string} str Text to display.
     */
    function setText(self, str) {
        self.element.textContent = str;
    }

    function doUpdate(self) {
        if (self.vars.tsTarget === null || self.vars.tsCurrent === null) {
            self.vars.lastValue = null;
        } else {
            self.vars.lastValue = self.vars.tsTarget - self.vars.tsCurrent;
        }

        if (!self.vars.rafRunning) {
            self.vars.rafRunning = true;
            shmi.raf(function onDraw() {
                if (!self.vars.lastValue) {
                    setText(self, self.vars.localizedInvalidText);
                } else {
                    setText(self, self.imports.dtUtils.formatDuration(self.vars.lastValue, self.config["display-preset"] || "compact"));
                }

                self.vars.rafRunning = false;
            });
        }
    }

    function makeItemHandler(self, configFormatName, destVarName) {
        return {
            setValue: function setValue(value, type) {
                if (type === shmi.c("TYPE_INT") || type === shmi.c("TYPE_FLOAT")) {
                    self.vars[destVarName] = self.imports.dtUtils.parseDateTime(String(value), self.config[configFormatName] || "$X");
                } else if (!self.config[configFormatName]) {
                    self.vars[destVarName] = null;
                } else {
                    self.vars[destVarName] = self.imports.dtUtils.parseDateTime(String(value), self.config[configFormatName]);
                }

                doUpdate(self);
            }
        };
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            subscriptions: [],
            tsTarget: null,
            tsCurrent: null,
            localizedInvalidText: null,
            rafRunning: false,
            lastValue: null
        },
        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            dtUtils: "visuals.tools.date"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                this.vars.localizedInvalidText = shmi.localize(this.config["invalid-text"]);
                setText(this, this.vars.localizedInvalidText);
            },
            /* called when control is enabled */
            onEnable: function() {
                if (this.config["target-ts-item"] && this.config["current-ts-item"] && this.vars.subscriptions.length === 0) {
                    this.vars.subscriptions.push(this.imports.im.subscribeItem(this.config["target-ts-item"], makeItemHandler(this, "target-ts-format", "tsTarget")));
                    this.vars.subscriptions.push(this.imports.im.subscribeItem(this.config["current-ts-item"], makeItemHandler(this, "current-ts-format", "tsCurrent")));
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                this.vars.subscriptions.forEach(function(sub) {
                    sub.unlisten();
                });
                this.vars.subscriptions = [];
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

shmi.pkg("visuals.controls");
/**
 * Creates a new FlipSwitch
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - the base element of the control
 * @param config - configuration of the control
 */
shmi.visuals.controls.FlipSwitch = function(element, config) {
    /* check for required packages */
    shmi.requires("visuals.io.MouseListener");
    shmi.requires("visuals.io.TouchListener");
    shmi.requires("visuals.gfx.Movable");

    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'flip-switch');
    shmi.def(this.config, 'template', 'default/flip-switch');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'label', "flip-switch");
    shmi.def(this.config, 'on-label', "I");
    shmi.def(this.config, 'off-label', "O");
    shmi.def(this.config, 'on-value', 1);
    shmi.def(this.config, 'off-value', 0);
    shmi.def(this.config, 'transitionStyle', 'all .16s linear');
    shmi.def(this.config, 'confirm-off-text', '${V_CONFIRM_OFF}');
    shmi.def(this.config, 'confirm-on-text', '${V_CONFIRM_ON}');
    shmi.def(this.config, 'confirm-on', false);
    shmi.def(this.config, 'confirm-off', false);
    shmi.def(this.config, 'icon-src', null);
    shmi.def(this.config, 'icon-title', null);
    shmi.def(this.config, 'icon-class', null);
    shmi.def(this.config, 'auto-label', true);

    this.config['show-icon'] = shmi.toBoolean(this.config['show-icon']);

    this.value = 0;
    this.active = false;
    this.initialized = false;
    this._subscriptionTargetId = null;
    this.movable = null;
    this._dragging = false;

    this.startup();
};

shmi.visuals.controls.FlipSwitch.prototype = {
    uiType: "flip-switch",
    getClassName: function() {
        return "FlipSwitch";
    },
    events: ["change"],
    tooltipProperties: ["icon-title"],
    onInit: function() {
        var self = this;

        var icon = shmi.getUiElement('flip-switch-icon', self.element);
        if (!icon) {
            shmi.log('[Flip Switch] no button-icon element provided', 1);
        } else if (self.config['icon-src']) {
            try {
                icon.setAttribute('src', self.config['icon-src']);
            } catch (exc) {
                shmi.log("[Flip Switch] Exception setting icon-src: " + exc, 2);
            }
        } else if (self.config['icon-class']) {
            if (icon.tagName === "IMG") {
                /* switch img element for div when icon-class and no icon-src is configured */
                var icnDiv = document.createElement('div');
                shmi.addClass(icnDiv, "flip-switch-icon");
                icon.parentNode.insertBefore(icnDiv, icon);
                icon.parentNode.removeChild(icon);
                icon = icnDiv;
            }
            var icon_class = self.config['icon-class'].trim().split(" ");
            icon_class.forEach(function(cls) {
                shmi.addClass(icon, cls);
            });
        }

        this.handleElement = shmi.getUiElement('flip-switch-handle', this.element);
        if (!this.handleElement) {
            shmi.log('[FlipSwitch] no flip-switch-handle element provided (required)', 3);
            return;
        }
        this.handleBoxElement = shmi.getUiElement('handle-box', this.element);
        if (!this.handleBoxElement) {
            shmi.log('[FlipSwitch] no handle-box element provided (required)', 3);
            return;
        }
        shmi.log('[FlipSwitch] flip-switch width: ' + this.handleBoxElement.offsetWidth, 0);
        shmi.log('[FlipSwitch] handle width: ' + this.handleElement.offsetWidth, 0);

        this.labelElement = shmi.getUiElement('label', this.element);
        if (!this.labelElement) {
            shmi.log("[FlipSwitch] no label element provided (optional)", 1);
        }

        this.onLabelElement = shmi.getUiElement('flip-switch-label-on', this.element);
        if (!this.onLabelElement) {
            shmi.log('[FlipSwitch] no flip-switch-label-on element provided (optional)', 1);
        }

        this.offLabelElement = shmi.getUiElement('flip-switch-label-off', this.element);
        if (!this.offLabelElement) {
            shmi.log('[FlipSwitch] no flip-switch-label-off element provided (optional)', 1);
        }

        /* set on-label if present in template and defined in config */
        if (this.onLabelElement) {
            if (this.config['on-label']) {
                this.onLabelElement.textContent = shmi.localize(this.config['on-label']);
            }
        }
        /* set off-label if present in template and defined in config */
        if (this.offLabelElement) {
            if (this.config['off-label']) {
                this.offLabelElement.textContent = shmi.localize(this.config['off-label']);
            }
        }
        /* set control label if present in template and defined in config */
        if (this.labelElement) {
            if (this.config.label) {
                this.labelElement.textContent = shmi.localize(this.config.label);
            }
        }

        /* IO Functionality */
        this.width = this.handleBoxElement.offsetWidth;
        this.handleWidth = this.handleElement.offsetWidth;
        this.movable = new shmi.visuals.gfx.Movable(this.handleElement);
        this.movable.setTransition(true);
        this.movable.transitionStyle = this.config.transitionStyle;
        var ioFuncs = {};
        ioFuncs.onDrag = function(dx, dy, event) {
            event.preventDefault();
            if (((this.movable.tx + dx) >= 0) && ((this.movable.tx + dx + this.handleWidth) <= this.handleBoxElement.offsetWidth)) {
                if (!this._dragging) {
                    this._dragging = true;
                }
                this.movable.translate(dx, 0);
            }
        }.bind(this);
        ioFuncs.onPress = function() {
            this.movable.setTransition(false);
            shmi.addClass(this.handleElement, 'pressed');
        }.bind(this);
        ioFuncs.onRelease = function() {
            if (this._dragging) {
                this._dragging = false;
                if (this.movable.tx > ((this.handleBoxElement.offsetWidth - this.handleElement.offsetWidth) / 2)) {
                    if (this.config['confirm-on']) {
                        shmi.confirm(this.config['confirm-on-text'], function(conf) {
                            if (conf) {
                                self.movable.tx = self.handleBoxElement.offsetWidth - self.handleElement.offsetWidth;
                                if (self.config.item) {
                                    shmi.visuals.session.ItemManager.writeValue(self.config.item, self.config['on-value']);
                                } else {
                                    self.setValue(self.config['on-value']);
                                }
                            } else {
                                self.setValue(self.value);
                            }
                        });
                    } else {
                        this.movable.tx = this.handleBoxElement.offsetWidth - this.handleElement.offsetWidth;
                        if (this.config.item) {
                            shmi.visuals.session.ItemManager.writeValue(this.config.item, this.config['on-value']);
                        } else {
                            this.setValue(this.config['on-value']);
                        }
                    }
                } else if (this.config['confirm-off']) {
                    shmi.confirm(this.config['confirm-off-text'], function(conf) {
                        if (conf) {
                            self.movable.tx = 0;
                            if (self.config.item) {
                                shmi.visuals.session.ItemManager.writeValue(self.config.item, self.config['off-value']);
                            } else {
                                self.setValue(self.config['off-value']);
                            }
                        } else {
                            self.setValue(self.value);
                        }
                    });
                } else {
                    this.movable.tx = 0;
                    if (this.config.item) {
                        shmi.visuals.session.ItemManager.writeValue(this.config.item, this.config['off-value']);
                    } else {
                        this.setValue(this.config['off-value']);
                    }
                }
                this.movable.update();
            }
            this.movable.setTransition(true);
            shmi.removeClass(this.handleElement, 'pressed');
        }.bind(this);
        ioFuncs.onClick = function() {
            if (this.value === this.config['on-value']) {
                if (this.config['confirm-off']) {
                    shmi.confirm(this.config['confirm-off-text'], function(conf) {
                        if (conf) {
                            if (self.config.item) {
                                shmi.visuals.session.ItemManager.writeValue(self.config.item, self.config['off-value']);
                            } else {
                                self.setValue(self.config['off-value']);
                            }
                        }
                    });
                } else if (this.config.item) {
                    shmi.visuals.session.ItemManager.writeValue(this.config.item, this.config['off-value']);
                } else {
                    this.setValue(this.config['off-value']);
                }
            } else if (this.config['confirm-on']) {
                shmi.confirm(this.config['confirm-on-text'], function(conf) {
                    if (conf) {
                        if (self.config.item) {
                            shmi.visuals.session.ItemManager.writeValue(self.config.item, self.config['on-value']);
                        } else {
                            self.setValue(self.config['on-value']);
                        }
                    }
                });
            } else if (this.config.item) {
                shmi.visuals.session.ItemManager.writeValue(this.config.item, this.config['on-value']);
            } else {
                this.setValue(this.config['on-value']);
            }
        }.bind(this);
        var ioClickFunc = {};
        ioClickFunc.onClick = ioFuncs.onClick;
        this.mouseListener = new shmi.visuals.io.MouseListener(this.handleElement, ioFuncs);
        this.touchListener = new shmi.visuals.io.TouchListener(this.handleElement, ioFuncs);
        this.mouseListener2 = new shmi.visuals.io.MouseListener(this.handleBoxElement, ioClickFunc);
        this.touchListener2 = new shmi.visuals.io.TouchListener(this.handleBoxElement, ioClickFunc);
    },
    /**
     * Enables the FlipSwitch
     *
     */
    onEnable: function() {
        this.width = this.handleBoxElement.offsetWidth;
        this.handleWidth = this.handleElement.offsetWidth;
        if (this.config.item) {
            this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
        }
        shmi.log("[FlipSwitch] hwidth: " + this.handleWidth + " width: " + this.handleBoxElement.offsetWidth, 0);
        this.mouseListener.enable();
        this.touchListener.enable();
        this.mouseListener2.enable();
        this.touchListener2.enable();
        shmi.log("[FlipSwitch] enabled", 1);
    },
    /**
     * Disables the FlipSwitch
     *
     */
    onDisable: function() {
        if (this.config.item) {
            shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
        }
        this.mouseListener.disable();
        this.touchListener.disable();
        this.mouseListener2.disable();
        this.touchListener2.disable();
        shmi.log("[FlipSwitch] disabled", 1);
    },
    /**
     * Locks the FlipSwitch
     *
     */
    onLock: function() {
        shmi.log("[FlipSwitch] locked", 1);
        this.mouseListener.disable();
        this.touchListener.disable();
        this.mouseListener2.disable();
        this.touchListener2.disable();
        shmi.addClass(this.element, 'locked');
    },
    /**
     * Unlocks the FlipSwitch
     *
     */
    onUnlock: function() {
        shmi.log("[FlipSwitch] unlocked", 1);
        this.mouseListener.enable();
        this.touchListener.enable();
        this.mouseListener2.enable();
        this.touchListener2.enable();
        shmi.removeClass(this.element, 'locked');
    },
    /**
     * Sets a new value for the FlipSwitch
     *
     * @param value - new value to set
     */
    onSetValue: function(value) {
        if (this._dragging) {
            return;
        }
        var on = false,
            old_value = this.value;
        if (value === this.config['on-value']) {
            this.value = this.config['on-value'];
            shmi.addClass(this.element, 'on');
            on = true;
        } else {
            this.value = this.config['off-value'];
            shmi.removeClass(this.element, 'on');
        }
        if (on) {
            this.movable.tx = (this.handleBoxElement.offsetWidth - this.handleWidth);
        } else {
            this.movable.tx = 0;
        }

        if (old_value !== this.value) {
            this.fire("change", {
                value: this.value
            });

            if (this.onChange) {
                this.onChange(this.value);
            }
        }

        this.movable.update();
    },
    /**
     * Retrieves the current value of the FlipSwitch
     *
     * @return value - current value
     */
    getValue: function() {
        return this.value;
    },
    setLabel: function(labelText) {
        var self = this;
        if (self.labelElement && self.config['auto-label']) {
            self.labelElement.textContent = shmi.localize(labelText);
        }
    }
};

shmi.extend(shmi.visuals.controls.FlipSwitch, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");

/**
 * Creates a new Form control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - base element of the control
 * @param config - configuration of the control
 */
shmi.visuals.controls.Form = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'form');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'template', null);
    shmi.def(this.config, 'fields', []);
    shmi.def(this.config, 'id-selector', null);
    shmi.def(this.config, 'datagrid', null);

    this.values = [];
    this.controls = [];
    this.fieldItems = [];
    this.fieldControls = {};
    this.loadedFields = {};
    this.applyCallback = null;
    this.cancelCallback = null;
    this.sel_list_id = null;
    /* selection listener id */

    this.startup();
};

shmi.visuals.controls.Form.prototype = {
    uiType: "form",
    isContainer: true,
    getClassName: function() {
        return "Form";
    },
    onInit: function() {},
    onRegister: function(onDone) {
        var self = this,
            form_id = Date.now(),
            nameToken = null,
            s = shmi.visuals.session;

        s.FormIDs = s.FormIDs || [];
        while (s.FormIDs.indexOf(form_id) !== -1) {
            form_id++;
        }
        s.FormIDs.push(form_id);
        self.form_id = form_id;

        nameToken = shmi.listen("register-name", function(evt) {
            var src_ctrl = evt.source;
            if (shmi.testParentChild(self.element, src_ctrl.element)) {
                if ((self.config.fields.indexOf(src_ctrl.config.field) !== -1) && (!self.loadedFields[src_ctrl.config.field])) {
                    self.fieldControls[src_ctrl.config.field] = src_ctrl;
                    /* create virtual item-name to track changed values */
                    var vItemName = "virtual:form:" + self.form_id + ":" + src_ctrl.config.field;
                    /* set item name on field-control */
                    src_ctrl.setItem(vItemName);
                    self.loadedFields[src_ctrl.config.field] = true;
                }
            }
        });
        self._init_.tokens.push(nameToken);

        self.parseChildren(self.element, function() {
            nameToken.unlisten();
            self._init_.tokens.splice(self._init_.tokens.indexOf(nameToken), 1);
            nameToken = null;
            onDone();
        });
    },
    /**
     * Sets the specified values to the Form controls.
     *
     * @param values - [] values for Form controls
     * @param applyCallback - callback to run when applying the Form
     * @param cancelCallback - callback to run when canceling the Form
     */
    setValues: function(values, applyCallback, cancelCallback, force) {
        /* delay execution until control finished initializing */
        if (!this.initialized) {
            return;
        }

        /* cancel execution and let user confirm that modified values will be lost */
        if (!force && (this.checkModified() !== 0)) {
            shmi.askSave("${V_ASK_SAVE_MSG}", function(save, cont) {
                /* only set new values if user agreed to lose modifications */
                if (save === true) {
                    shmi.log("[Form] applying changes...", shmi.c("LOG_MSG"));
                    this.apply();
                } else {
                    shmi.log("[Form] discarding changes...", shmi.c("LOG_MSG"));
                }
                if (cont === true) {
                    this.setValues(values, applyCallback, cancelCallback, true);
                } else {
                    this.unlock();
                }
            }.bind(this));
            return;
        }

        this.values = values;
        this.applied = false;
        this.canceled = false;
        if (this.values.length !== Object.keys(this.fieldControls).length) {
            shmi.log("[Form] values length (" + this.values.length +
                ") does not match number of controls(" + Object.keys(this.fieldControls).length + ")", 2);
        }

        this.deleteFieldItems();

        for (var i = 0; i < this.values.length; i++) {
            if (Array.isArray(this.values[i])) { // old style
                if (this.values[i].length !== 2) {
                    shmi.log("[Form] invalid value set: " + this.values[i], 2);
                    continue;
                }
                try {
                    this.controls[i].type = parseInt(this.values[i][1]);
                    this.controls[i].setValue(this.values[i][0]);
                    shmi.log("[Form] value set: " + this.values[i][0], 2);
                } catch (exc) {
                    shmi.log("[Form] exception setting form data: " + exc, 3);
                }
            } else { //new style
                var val = this.values[i],
                    vItemName = "virtual:form:" + this.form_id + ":" + val.field;
                if ((val.type === null) || (val.type === undefined)) {
                    val.type = shmi.c("TYPE_STRING");
                }
                var v_item = shmi.createVirtualItem(vItemName, val.type, Number.NaN, Number.NaN, val.value, this.getFieldValueCallback(val.field));
                this.fieldItems.push(v_item);
                v_item.writeValue(val.value);
                val.modified = false;
                v_item.notifyUpdateTargets();
                if (this.fieldControls[val.field] !== undefined) {
                    shmi.removeClass(this.fieldControls[val.field].element, 'modified');
                }
                //this.fieldControls[val.field].type = parseInt(val.type);
                //this.fieldControls[val.field].setValue(val.value);
            }
        }

        if (applyCallback) {
            this.applyCallback = applyCallback;
            var modified = this.checkModified();
            if (modified !== 0) {
                if (this.onChange) {
                    this.onChange(this);
                }
            }
        }
        if (cancelCallback) {
            this.cancelCallback = cancelCallback;
        }

        /* unlock if values are set on form */
        this.unlock();
    },
    deleteFieldItems: function() {
        this.fieldItems = [];
    },
    /* creates callback functions for virtual items used for form-fields */
    getFieldValueCallback: function(field) {
        return function(value, type, name) {
            var f_name = field;
            for (var i = 0; i < this.values.length; i++) {
                if (this.values[i].field === f_name) {
                    if (this.values[i].value !== value) {
                        if (this.fieldControls[f_name] !== undefined) {
                            shmi.addClass(this.fieldControls[f_name].element, 'modified');
                        }
                        this.values[i].modified = true;
                    } else {
                        if (this.fieldControls[f_name] !== undefined) {
                            shmi.removeClass(this.fieldControls[f_name].element, 'modified');
                        }
                        this.values[i].modified = false;
                    }
                    break; // stop if field is found
                }
            }

            if (this.onChange) {
                this.onChange(this);
            }
        }.bind(this);
    },
    checkModified: function() {
        var modified = 0;
        if (this.values === undefined) {
            this.values = [];
        }
        for (var i = 0; i < this.values.length; i++) {
            if (this.values[i].modified === true) {
                modified++;
            }
        }
        return modified;
    },
    /**
     * Applies the Form
     *
     */
    apply: function() {
        var i;
        for (i = 0; i < this.values.length; i++) {
            try {
                this.values[i].value = this.fieldControls[this.values[i].field].getValue();
            } catch (exc) {
                shmi.log("[Form] control has no value: " + exc.toString(), 2);
            }
        }
        this.applied = true;
        if (this.applyCallback) {
            this.applyCallback(this.values);
        } else {
            shmi.log("[Form] no apply callback defined", 2);
        }

        /* reset modified states */
        for (i = 0; i < this.values.length; i++) {
            if (this.fieldControls[this.values[i].field] !== undefined) {
                shmi.removeClass(this.fieldControls[this.values[i].field].element, 'modified');
            }
            this.values[i].modified = false;
        }
    },
    /**
     * Cancels the Form
     *
     */
    cancel: function() {
        this.canceled = true;
        if (this.cancelCallback) {
            this.cancelCallback();
        }
    },
    /**
     * Enables the Form
     *
     */
    onEnable: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].enable();
        }

        if (this.config['id-selector']) {
            this.id_selector = shmi.ctrl(this.config['id-selector']);
            if (this.id_selector) {
                var self = this;
                var selection_handler = {
                    tableSelectionChange: function(selection) {
                        self.setDataGridSelection(selection);
                    }
                };
                /* get current selection */
                selection_handler.tableSelectionChange(self.id_selector.getSelectedRows());
                /* register selection-change listener */
                this.sel_list_id = this.id_selector.addSelChgEventListener(selection_handler);
            }
        }

        shmi.log("[Form] enabled", 1);
    },
    getFormReset: function(data) {
        var self = this;
        return function() {
            self.setValues(data, self.applyCallback, self.cancelCallback, true);
        };
    },
    setDataGridSelection: function(selection) {
        var self = this,
            dgm = shmi.visuals.session.DataGridManager,
            i;
        if ((selection.type === 2) && (selection.selRows.length > 0) && self.config.datagrid) {
            self.unlock();
            var grid = dgm.grids[self.config.datagrid];
            if (grid) {
                var values = [],
                    data;
                if (selection.selRows.length > 1) {
                    var data_null = grid.getRowData(selection.selRows[0]);
                    for (var j = 0; j < data_null.length; j++) {
                        var all_equal = true,
                            last_val = null;
                        for (var k = 0; k < selection.selRows.length; k++) {
                            data = grid.getRowData(selection.selRows[k]);
                            if (k === 0) {
                                last_val = data[j].value;
                            } else if (data[j].value === last_val) {
                                continue;
                            } else {
                                all_equal = false;
                                break;
                            }
                        }
                        if (all_equal) {
                            values.push({
                                value: last_val,
                                field: grid.fields[j]
                            });
                        } else {
                            values.push({
                                value: "",
                                field: grid.fields[j]
                            });
                        }
                    }
                } else {
                    data = grid.getRowData(selection.selRows[0]);
                    for (i = 0; i < data.length; i++) {
                        values.push({
                            value: data[i].value,
                            field: grid.fields[i]
                        });
                    }
                }
                self.setValues(values, self.getDataGridApply(grid, selection.selRows), self.getFormReset(values), false);
            }
        } else {
            var vals = [];
            for (i = 0; i < self.config.fields.length; i++) {
                var val = {
                    type: shmi.c("TYPE_STRING"),
                    field: self.config.fields[i],
                    value: ""
                };
                vals.push(val);
            }
            self.setValues(vals, null, null);
            self.lock();
        }
    },
    getDataGridApply: function(grid, sel_rows) {
        var self = this,
            i;

        var apply_row = function(sel_row_idx) {
            for (i = 0; i < self.config.fields.length; i++) {
                var f_ctrl = self.fieldControls[self.config.fields[i]];
                if (f_ctrl && self.values[i].modified) {
                    var grid_field_index = grid.fields.indexOf(self.config.fields[i]);
                    if (grid_field_index !== -1) {
                        grid.data[sel_rows[sel_row_idx]][grid_field_index].item.writeValue(f_ctrl.getValue());
                    }
                }
            }
        };

        if (sel_rows.length === 1) {
            return function() {
                apply_row(0);
            };
        } else if (sel_rows.length > 1) {
            return function() {
                for (i = 0; i < sel_rows.length; i++) {
                    apply_row(i);
                }
            };
        } else {
            return function() {
                console.trace("NO_SELECT_STUB");
            };
        }
    },
    /**
     * Disables the Form
     *
     */
    onDisable: function() {
        var self = this;
        for (var i = 0; i < self.controls.length; i++) {
            self.controls[i].disable();
        }
        if ((self.sel_list_id !== null) && self.id_selector) {
            self.id_selector.removeSelChgEventListener(self.sel_list_id);
            self.sel_list_id = null;
        }
        if (self.field_ctrl_lid) {
            shmi.unlisten("register-name", self.field_ctrl_lid);
            self.field_ctrl_lid = null;
        }
        shmi.log("[Form] disabled", 1);
    },
    /**
     * Locks the Form
     *
     */
    onLock: function() {
        var self = this;

        shmi.addClass(self.element, 'locked');

        function recLock(ctrl) {
            ctrl.lock();
            if (Array.isArray(ctrl.controls)) {
                ctrl.controls.forEach(recLock);
            }
        }

        self.controls.forEach(function(ctrl) {
            recLock(ctrl);
        });
        shmi.log("[Form] locked", 1);
    },
    /**
     * Unlocks the Form
     *
     */
    onUnlock: function() {
        var self = this;

        shmi.removeClass(self.element, 'locked');

        function recUnlock(ctrl) {
            ctrl.unlock();
            if (Array.isArray(ctrl.controls)) {
                ctrl.controls.forEach(recUnlock);
            }
        }

        self.controls.forEach(function(ctrl) {
            recUnlock(ctrl);
        });
        shmi.log("[Form] unlocked", 1);
    },
    /**
     * Sets value to the Form
     *
     * @param value - new value to set
     */
    onSetValue: function(value) {
        shmi.log("[Form] value set: " + value, 0);
    }
};

shmi.extend(shmi.visuals.controls.Form, shmi.visuals.core.BaseControl);

// version https://88.198.203.47/svn//project/branches/naming_scheme - von Meike am 28.08.2014 (war unver??ndert zu Stand 5_CT2)
// ++ patches, fixes Peer 28.08. ff.

shmi.pkg("visuals.controls");
/**
 * General purpose scroll bar.
 *
 * An instance of this control changes values between min and max and fires the related events.
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {Element} element the base element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.GpScrollBar = function(element, config) {
    // short cuts to static "const" definitions for GPSB
    this.c = shmi.visuals.controls.GpScrollBar.c;
    this.uiElName = shmi.visuals.controls.GpScrollBar.uiElName;
    this.uiCssCl = shmi.visuals.controls.GpScrollBar.uiCssCl;

    this.element = element;

    this.config = config || {};
    /* set default options */
    shmi.def(this.config, "class-name", "gp-scroll-bar");
    shmi.def(this.config, "template", "default/gp-scroll-bar");
    shmi.def(this.config, "name", "Unnamed GpScrollBar");
    shmi.def(this.config, "label", "Label?"); // NYI
    shmi.def(this.config, "max", 100);
    shmi.def(this.config, "min", 0);
    shmi.def(this.config, "precision", 0); // tbd, NYI
    shmi.def(this.config, "step", 0); //, NYI
    shmi.def(this.config, "vertical", true); // currently is true impl. only !!
    shmi.def(this.config, "buttons", true);
    shmi.def(this.config, "inverse", false);

    this.parseAttributes();

    //this.initialized = false;
    this.isMinimized = false; // true means that GBSB is used as indicator only, events are disabled
    this.isEnabled = false;
    this.min = 0;
    this.max = 0;
    this.sliderVal = 0;
    this.sliderWidth = 0;
    this.sliderTrackElem = null;
    this.sliderHandleElem = null;
    this.sliderHandleVisibleElem = null;
    this.sliderHandleMovable = null;
    this.btnUpElem = null;
    this.btnDownElem = null;
    this.sliderHandleMouseListener = null;
    this.sliderHandleTouchListener = null;
    this.sliderTrackMouseListener = null;
    this.sliderTrackTouchListener = null;
    this.btnUpMouseListener = null;
    this.btnUpTouchListener = null;
    this.btnDownMouseListener = null;
    this.btnDownTouchListener = null;
    this.sbEventListeners = {};
    this.enableTO = null;
    this.btnStartAutoRepeatTO = null;
    this.btnProcessAutoRepeatTO = null;
    this.isBtnAutoRepeatProcessing = false;

    // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    this.debug = {};
    this.debug.msgFIFO = [];
    this.testlog_msg("startup");
    // $test !!!!!!!!!!!!!!!

    this.startup();
};

shmi.visuals.controls.GpScrollBar.prototype = {
    /*-- BaseControl method interface --*/
    uiType: "gp-scroll-bar",
    getClassName: function() {
        return "GpScrollBar";
    },
    onInit: function() {
        /* config post processing (boolean and number values) */
        this.config["max"] = shmi.toNumber(this.config["max"]);
        this.config["min"] = shmi.toNumber(this.config["min"]);
        this.config["precision"] = shmi.toNumber(this.config["precision"]);
        this.config["step"] = shmi.toNumber(this.config["step"]);
        this.config["vertical"] = shmi.toBoolean(this.config["vertical"]);
        this.config["buttons"] = shmi.toBoolean(this.config["buttons"]);
        this.config["inverse"] = shmi.toBoolean(this.config["inverse"]);

        /* init all members, get references to the necessary elements */
        this.min = this.config["min"];
        this.max = this.config["max"];
        this.sliderVal = this.min; // 1st valid init
        this.sliderWidth = (this.max - this.min) / 10; // 1st valid init
        this.sliderTrackElem = shmi.getUiElement(this.uiElName.sliderTrack, this.element);
        if (!this.sliderTrackElem) {
            shmi.log("[GpScrollBar] no " + this.uiElName.sliderTrack + " element provided (required)", 3);
            return;
        }
        this.sliderHandleElem = shmi.getUiElement(this.uiElName.sliderHandle, this.element);
        if (!this.sliderHandleElem) {
            shmi.log("[GpScrollBar] no " + this.uiElName.sliderHandle + " provided (required)", 3);
            return;
        }
        this.sliderHandleMovable = new shmi.visuals.gfx.Movable(this.sliderHandleElem);
        shmi.addClass(this.sliderHandleElem, this.uiCssCl.sliderAni);
        this.sliderHandleVisibleElem = shmi.getUiElement(this.uiElName.sliderHandleVisible, this.element);
        if (!this.sliderHandleVisibleElem) {
            shmi.log("[GpScrollBar] no " + this.uiElName.sliderHandleVisibleElem + " provided (required)", 3);
            return;
        }

        if (this.config["buttons"]) {
            shmi.addClass(this.element, this.uiCssCl.withBtns);
            this.btnUpElem = shmi.getUiElement(this.uiElName.btnUp, this.element);
            if (!this.btnUpElem) {
                shmi.log("[GpScrollBar] no " + this.uiElName.btnUp + " element provided (required)", 3);
                return;
            }
            this.btnDownElem = shmi.getUiElement(this.uiElName.btnDown, this.element);
            if (!this.btnDownElem) {
                shmi.log("[GpScrollBar] no " + this.uiElName.btnDown + " element provided (required)", 3);
                return;
            }
        } else {
            shmi.addClass(this.element, this.uiCssCl.withoutBtns);
            var btnContElem = shmi.getUiElement(this.uiElName.btnPanel, this.element);
            if (btnContElem) {
                btnContElem.parentNode.removeChild(btnContElem);
            }
        }

        /* define and route the event handlers, filter events */
        var sliderHandleEvents = {};
        sliderHandleEvents.onDrag = function(dx, dy, event) {
            event.preventDefault();
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //console.log("sliderHandleEvents.onDrag(..) fired - dx:" + dx + " dy:" + dy);
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            this.updateVal(this.sliderHandleMovable.ty + dy);
            this.updateSlider();
            this.fireValChanged(this.sliderVal, false);
        }.bind(this);
        sliderHandleEvents.onPress = function() {
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //console.log("sliderHandleEvents.onPress() fired");
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            shmi.removeClass(this.sliderHandleElem, this.uiCssCl.sliderAni);
            //shmi.addClass(this.sliderHandleElem, this.uiCssCl.btnSel);
            shmi.addClass(this.sliderHandleVisibleElem, this.uiCssCl.btnSel);
        }.bind(this);
        sliderHandleEvents.onRelease = function() {
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //console.log("sliderHandleEvents.onRelease() fired");
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            shmi.addClass(this.sliderHandleElem, this.uiCssCl.sliderAni);
            //shmi.removeClass(this.sliderHandleElem, this.uiCssCl.btnSel);
            shmi.removeClass(this.sliderHandleVisibleElem, this.uiCssCl.btnSel);
        }.bind(this);
        this.sliderHandleMouseListener = new shmi.visuals.io.MouseListener(this.sliderHandleElem, sliderHandleEvents);
        this.sliderHandleTouchListener = new shmi.visuals.io.TouchListener(this.sliderHandleElem, sliderHandleEvents);
        var sliderTrackEvents = {};
        sliderTrackEvents.onPress = function(x, y, event) {
            shmi.addClass(this.sliderTrackElem, this.uiCssCl.trackSel);
        }.bind(this);
        sliderTrackEvents.onRelease = function() {
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //console.log("sliderTrackEvents.onRelease() fired");
            // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            shmi.removeClass(this.sliderTrackElem, this.uiCssCl.trackSel);
        }.bind(this);
        sliderTrackEvents.onClick = function(x, y, event) {
            var pos = shmi.getAbsPosition(this.sliderTrackElem);
            var deltaY = y - pos.y;
            this.updateVal(deltaY);
            this.updateSlider();
            this.fireValChanged(this.sliderVal, true);
        }.bind(this);
        this.sliderTrackMouseListener = new shmi.visuals.io.MouseListener(this.sliderTrackElem, sliderTrackEvents);
        this.sliderTrackTouchListener = new shmi.visuals.io.TouchListener(this.sliderTrackElem, sliderTrackEvents);
        if (this.btnUpElem) {
            var btnUpEvents = {};
            btnUpEvents.onPress = function() {
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                //console.log("btnUpEvents.onPress() fired");
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                shmi.addClass(this.btnUpElem, this.uiCssCl.btnSel);
                if (this.btnStartAutoRepeatTO) {
                    clearTimeout(this.btnStartAutoRepeatTO);
                }
                this.btnStartAutoRepeatTO = setTimeout(function() {
                    this.startBtnAutoRepeat("up");
                }.bind(this), this.c.START_BTN_AUTO_REPEAT_TO);
                this.fireBtnAction("up");
            }.bind(this);
            btnUpEvents.onRelease = function() {
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                //console.log("btnUpEvents.onRelease() fired");
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                if (this.btnStartAutoRepeatTO) {
                    clearTimeout(this.btnStartAutoRepeatTO);
                }
                this.stopBtnAutoRepeat();
                shmi.removeClass(this.btnUpElem, this.uiCssCl.btnSel);
            }.bind(this);
            this.btnUpMouseListener = new shmi.visuals.io.MouseListener(this.btnUpElem, btnUpEvents);
            this.btnUpTouchListener = new shmi.visuals.io.TouchListener(this.btnUpElem, btnUpEvents);
            var btnDownEvents = {};
            btnDownEvents.onPress = function() {
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                //console.log("btnDownEvents.onPress() fired");
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                shmi.addClass(this.btnDownElem, this.uiCssCl.btnSel);
                if (this.btnStartAutoRepeatTO) {
                    clearTimeout(this.btnStartAutoRepeatTO);
                }
                this.btnStartAutoRepeatTO = setTimeout(function() {
                    this.startBtnAutoRepeat("down");
                }.bind(this), this.c.START_BTN_AUTO_REPEAT_TO);
                this.fireBtnAction("down");
            }.bind(this);
            btnDownEvents.onRelease = function() {
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                //console.log("btnDownEvents.onRelease() fired");
                // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                if (this.btnStartAutoRepeatTO) {
                    clearTimeout(this.btnStartAutoRepeatTO);
                }
                this.stopBtnAutoRepeat();
                shmi.removeClass(this.btnDownElem, this.uiCssCl.btnSel);
            }.bind(this);
            this.btnDownMouseListener = new shmi.visuals.io.MouseListener(this.btnDownElem, btnDownEvents);
            this.btnDownTouchListener = new shmi.visuals.io.TouchListener(this.btnDownElem, btnDownEvents);
        }
        this.resizeListener = function() {
            this.processEvtOnResize();
        }.bind(this);

        shmi.log("[GpScrollBar] initialized", 1);
    },
    onEnable: function() {
        // test !!!!!!!!!!!!!!!
        this.testlog_msg("onEnable");
        // test !!!!!!!!!!!!!!!
        window.addEventListener("resize", this.resizeListener, true);
        this.updateSlider();
        this.isEnabled = true;
        this.updateEvtEnable();
        shmi.log("[GpScrollBar] enabled", 1);
    },
    onDisable: function() {
        // test !!!!!!!!!!!!!!!
        this.testlog_msg("onEnable");
        // test !!!!!!!!!!!!!!!
        window.removeEventListener("resize", this.resizeListener, true);
        this.isEnabled = false;
        this.updateEvtEnable();
        shmi.log("[GpScrollBar] disabled", 1);
    },
    onDelete: function() {},

    /*-- additional method interface --*/

    /*
     * $todo: noch JSDoc method descriptions add
     *
     * val, width are numbers in the range of [min .. max]
     */
    setSliderVal: function(val) {
        this.sliderVal = val;
        this.updateSlider();
        shmi.log("[GpScrollBar] setSliderVal called, val: " + val, 1);
    },
    setSliderWidth: function(width) {
        this.sliderWidth = width;
        this.updateSlider();
        shmi.log("[GpScrollBar] setSliderWidth called, width: " + width, 1);
    },
    setSliderProps: function(val, width) {
        this.sliderVal = val;
        this.sliderWidth = width;
        this.updateSlider();
        shmi.log("[GpScrollBar] setSliderProps called, val: " + val + " - width: " + width, 1);
    },
    getSliderVal: function() {
        shmi.log("[GpScrollBar] getSliderVal called, return: " + this.sliderVal, 1);
        return this.sliderVal;
    },
    getSliderWidth: function() {
        shmi.log("[GpScrollBar] getSliderVal called, return: " + this.sliderWidth, 1);
        return this.sliderWidth;
    },
    setMin: function(min) {
        this.min = min;
        this.updateSlider();
        shmi.log("[GpScrollBar] setMin called, min: " + min, 1);
    },
    setMax: function(max) {
        this.max = max;
        this.updateSlider();
        shmi.log("[GpScrollBar] setMax called, min: " + max, 1);
    },
    setMinMax: function(min, max) {
        this.min = min;
        this.max = max;
        this.updateSlider();
        shmi.log("[GpScrollBar] setMinMax called, min: " + min + " - max: " + max, 1);
    },
    getMin: function() {
        shmi.log("[GpScrollBar] getMin called, return: " + this.min, 1);
        return this.min;
    },
    getMax: function() {
        shmi.log("[GpScrollBar] getMax called, return: " + this.max, 1);
        return this.max;
    },
    setAllProps: function(val, width, min, max) {
        this.sliderVal = val;
        this.sliderWidth = width;
        this.min = min;
        this.max = max;
        this.updateSlider();
        shmi.log("[GpScrollBar] setAllProps called, , val: " + val + " - width: " + width + " - min: " + min + " - max: " + max, 1);
    },
    setMinimized: function(min) {
        this.isMinimized = min;
        this.updateEvtEnable();
        if (min) {
            shmi.removeClass(this.element, this.uiCssCl.normal);
            shmi.addClass(this.element, this.uiCssCl.minimized);
        } else {
            shmi.removeClass(this.element, this.uiCssCl.minimized);
            shmi.addClass(this.element, this.uiCssCl.normal);
        }
    },
    getMinimized: function() {
        return this.isMinimized;
    },

    /*-- event listener control interface --*/

    /*  $todo: noch JSDoc method descriptions add
     *
     *  note: listenerObj must implement the event handler callbacks
     *   .gpsbOnValChange(val) (val in range [min..max])
     *   .gpsbOnBtnAction(btn)  (btn in range ["up", "down"])
     */
    addEventListener: function(listenerObj) {
        var id = Date.now();
        while (this.sbEventListeners[id] !== undefined) {
            id++;
        }
        this.sbEventListeners[id] = {};
        this.sbEventListeners[id].listener = listenerObj;
        shmi.log("[GpScrollBar] addEventListener called, listenerObj: " + listenerObj + " - id:" + id, 1);
        return id;
    },
    removeEventListener: function(id) {
        if (this.sbEventListeners[id] !== undefined) {
            delete this.sbEventListeners[id];
        } else {
            shmi.log("[GpScrollBar] removeEventListener - id " + id + " does not exist", 2);
        }
        shmi.log("[GpScrollBar] removeEventListener called, id: " + id, 1);
    },

    /*-- private helper methods - usage within GPSB only !! --*/

    /**
     * @private
     */
    startBtnAutoRepeat: function(btn) {
        // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //console.log(">>> startBtnAutoRepeat() - btn: " + btn);
        // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        this.isBtnAutoRepeatProcessing = true;
        if (this.btnProcessAutoRepeatTO) {
            clearTimeout(this.btnProcessAutoRepeatTO);
        }
        this.btnProcessAutoRepeatTO = setTimeout(function() {
            this.processBtnAutoRepeat(btn);
        }.bind(this), this.c.PROCESS_BTN_AUTO_REPEAT_TO);
    },
    processBtnAutoRepeat: function(btn) {
        if (this.isBtnAutoRepeatProcessing) {
            // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //console.log(">>> processBtnAutoRepeat() - btn: " + btn);
            // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            this.fireBtnAction(btn);
            this.btnProcessAutoRepeatTO = setTimeout(function() {
                this.processBtnAutoRepeat(btn);
            }.bind(this), this.c.PROCESS_BTN_AUTO_REPEAT_TO);
        }
    },
    /**
     * @private
     */
    stopBtnAutoRepeat: function() {
        // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //console.log("<<< stopBtnAutoRepeat()");
        // $test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

        this.isBtnAutoRepeatProcessing = false;
        if (this.btnProcessAutoRepeatTO) {
            clearTimeout(this.btnProcessAutoRepeatTO);
        }
    },
    /**
     * @private
     */
    processEvtOnResize: function() {
        this.updateSlider();
        //console.log("GpScrollBar processEvtOnResize");
    },
    /**
     * @private
     */
    updateSlider: function() {
        if (this.initialized) {
            var slHeight = (this.sliderWidth * this.sliderTrackElem.offsetHeight) / (this.max - this.min);
            if (this.isMinimized) {
                if (slHeight < this.c.MIN_SLIDER_SIZE_PX_MINIMIZED) {
                    slHeight = this.c.MIN_SLIDER_SIZE_PX_MINIMIZED;
                }
            } else if (slHeight < this.c.MIN_SLIDER_SIZE_PX) {
                slHeight = this.c.MIN_SLIDER_SIZE_PX;
            }
            var slAvailTrackHeight = this.sliderTrackElem.offsetHeight - slHeight,
                slTop = ((this.max - this.sliderVal) * slAvailTrackHeight) / (this.max - this.min);

            // $todo: vor Einstellung der neuen Slider-TopPos/-Height -> range it ??
            this.sliderHandleElem.style.height = slHeight + "px";
            if (!this.config["inverse"]) {
                slTop = slAvailTrackHeight - slTop;
            }
            this.sliderHandleMovable.translate(0, slTop - this.sliderHandleMovable.ty);
        }
    },
    /**
     * @private
     */
    updateVal: function(newSlTop) {
        if (this.initialized) {
            var newVal = this.max - (newSlTop * (this.max - this.min)) / (this.sliderTrackElem.offsetHeight - this.sliderHandleElem.offsetHeight);
            if (!this.config["inverse"]) {
                newVal = this.max - newVal;
            }
            if (newVal < this.min) {
                this.sliderVal = this.min;
            } else if (newVal > this.max) {
                this.sliderVal = this.max;
            } else {
                this.sliderVal = newVal;
            }
        }
    },
    /**
     * @private
     */
    updateEvtEnable: function() {
        var enabled = (this.isEnabled && !this.isMinimized);
        if (this.initialized) {
            // test !!!!!!!!!!!!!!!
            this.testlog_msg("init ok - en: " + this.isEnabled + " - min: " + this.isMinimized);
            // test !!!!!!!!!!!!!!!
            if (enabled) {
                this.sliderHandleMouseListener.enable();
                this.sliderHandleTouchListener.enable();
                this.sliderTrackMouseListener.enable();
                this.sliderTrackTouchListener.enable();
                if (this.btnUpElem) {
                    this.btnUpMouseListener.enable();
                    this.btnUpTouchListener.enable();
                    this.btnDownMouseListener.enable();
                    this.btnDownTouchListener.enable();
                }
            } else {
                this.sliderHandleMouseListener.disable();
                this.sliderHandleTouchListener.disable();
                this.sliderTrackMouseListener.disable();
                this.sliderTrackTouchListener.disable();
                if (this.btnUpElem) {
                    this.btnUpMouseListener.disable();
                    this.btnUpTouchListener.disable();
                    this.btnDownMouseListener.disable();
                    this.btnDownTouchListener.disable();
                }
            }
        } else {
            // test !!!!!!!!!!!!!!!
            this.testlog_msg("init ! ok - repeat");
            // test !!!!!!!!!!!!!!!
            if (this.enableTO) {
                clearTimeout(this.enableTO);
            }
            this.enableTO = setTimeout(function() {
                this.updateEvtEnable();
            }.bind(this), this.c.ENABLE_TO);
        }
    },
    /**
     * @private
     */
    fireValChanged: function(val, fromTrack) {
        const iterObj = shmi.requires("visuals.tools.iterate.iterateObject");

        iterObj(this.sbEventListeners, (listener) => {
            listener.listener.gpsbOnValChange(val, fromTrack);
        });
    },
    /**
     * @private
     */
    fireBtnAction: function(btn) {
        const iterObj = shmi.requires("visuals.tools.iterate.iterateObject");

        iterObj(this.sbEventListeners, (listener) => {
            listener.listener.gpsbOnBtnAction(btn);
        });
    },

    // test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    /**
     * @private
     */
    testlog_msg: function(msg) {
        var msgDisplay = document.getElementById("debugDisplEvents");
        if (msgDisplay) {
            var displInfo = [];
            msgDisplay.innerHTML = "";
            displInfo.push("last debug messages:");
            displInfo.push("---");
            console.log(msg);
            this.debug.msgFIFO.push(msg);
            if (this.debug.msgFIFO.length > 10) {
                this.debug.msgFIFO.shift();
            }

            for (var i = this.debug.msgFIFO.length - 1; i >= 0; i--) {
                displInfo.push(this.debug.msgFIFO[i]);
            }
            displInfo.push("--------------------------------");
            for (i = 0; i < displInfo.length; i++) {
                msgDisplay.innerHTML += displInfo[i] + "<br>";
            }
        }
    }
};

shmi.extend(shmi.visuals.controls.GpScrollBar, shmi.visuals.core.BaseControl);
shmi.registerControlType("gp-scroll-bar", shmi.visuals.controls.GpScrollBar, true);

// $todo Meike begin - ++++++++++++++++
// $todo -> hier ggfs. element names und class names anpassen fuer Vereinheitlichung/ueberarbeitung CSS/Templates der Controls
shmi.visuals.controls.GpScrollBar.uiElName = {};
(function() {
    var __gpsbsc = shmi.visuals.controls.GpScrollBar.uiElName;
    /* The following ui element names are used in CT2's JS code. Change it here if necessary.
     All other ui element names may be changed in the CT2 template(s).
     */
    __gpsbsc.sliderTrack = "gpsb-slider-track";
    __gpsbsc.sliderHandle = "gpsb-slider-handle"; // this is the slider touch area
    __gpsbsc.sliderHandleVisible = "gpsb-slider-handle-visible"; // this is the visible slider button
    __gpsbsc.btnPanel = "gpsb-btn-panel";
    __gpsbsc.btnUp = "gpsb-btn-up";
    __gpsbsc.btnDown = "gpsb-btn-down";

    shmi.visuals.controls.GpScrollBar.uiCssCl = {};
    __gpsbsc = shmi.visuals.controls.GpScrollBar.uiCssCl;
    /* The following ui element class names are used in CT2's JS code. Change it here if necessary.
     All other ui element class names be changed in the CT2 template(s).
     */
    // dynamically switched styles
    __gpsbsc.withBtns = "gpsb-with-btns";
    __gpsbsc.withoutBtns = "gpsb-without-btns";
    __gpsbsc.btnSel = "gpsb-btn-selected";
    __gpsbsc.trackSel = "gpsb-track-selected";
    __gpsbsc.minimized = "gpsb-minimized";
    __gpsbsc.normal = "gpsb-normal";
    // transitions/animations
    __gpsbsc.sliderAni = "gpsb-slider-ani";
    // $todo Meike end - ++++++++++++++++

    shmi.visuals.controls.GpScrollBar.c = {};
    __gpsbsc = shmi.visuals.controls.GpScrollBar.c;
    __gpsbsc.MIN_SLIDER_SIZE_PX = 60;
    __gpsbsc.MIN_SLIDER_SIZE_PX_MINIMIZED = 5;
    __gpsbsc.ENABLE_TO = 500;
    __gpsbsc.START_BTN_AUTO_REPEAT_TO = 1000;
    __gpsbsc.PROCESS_BTN_AUTO_REPEAT_TO = 100;
})();

shmi.pkg("visuals.controls");
/**
 * Creates a new control Group
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - base element of the control group
 * @param config - configuration of the control group
 */
shmi.visuals.controls.Group = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'group');
    shmi.def(this.config, 'template', null);
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'replacers', {});

    this.initialized = false;
    this.controls = [];

    this.startup();
};

shmi.visuals.controls.Group.prototype = {
    uiType: "group",
    isContainer: true,
    getClassName: function() {
        return "Group";
    },
    /**
     * parses child controls
     */
    onRegister: function(onDone) {
        var self = this;
        self.parseChildren(self.element, onDone);
    },
    /**
     * initializes control
     */
    onInit: function() {},
    /**
     * Enables the Group
     *
     */
    onEnable: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].enable();
        }
        shmi.log("[Group] enabled", 1);
    },
    /**
     * Disables the Group
     *
     */
    onDisable: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].disable();
        }
        shmi.log("[Group] disabled", 1);
    },
    /**
     * Locks the Group
     *
     */
    onLock: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].lock();
        }
        shmi.addClass(this.element, 'locked');
        shmi.log("[Group] locked", 1);
    },
    /**
     * Unlocks the Group
     *
     */
    onUnlock: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].unlock();
        }

        shmi.removeClass(this.element, 'locked');
        shmi.log("[Group] locked", 1);
    },
    /**
     * Sets value to the Group
     *
     * @param value - new value to set
     */
    onSetValue: function(value) {
        shmi.log("[Group] value set: " + value, 0);
    }
};

shmi.extend(shmi.visuals.controls.Group, shmi.visuals.core.BaseControl);

/**
 * WebIQ visuals control template.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "html",
 *     "name": null,
 *     "template": "default/html"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "Html", // control name in camel-case
        uiType = "html", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    var defConfig = {
        "class-name": "html",
        "name": null,
        "template": "default/html",
        "use-shadow-dom": false,
        "html": ""
    };

    // declare private functions - START
    /**
     * Parses an html string and creates a fragment which can be attached to
     * another node.
     *
     * @param {string} html HTML string to parse.
     * @returns {DocumentFragment}
     */
    function documentFragmentFromHTML(html) {
        const templateNode = document.createElement("template");

        templateNode.innerHTML = html;

        return templateNode.content.cloneNode(true);
    }
    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            html: null
        },
        /* imports added at runtime */
        imports: {},

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    htmlContentNode = shmi.getUiElement("html", self.element);

                if (!htmlContentNode) {
                    console.log('Error: "html" element is missing in template!');
                    return;
                }

                // Get the node to which to append the widgets contents.
                const rootNode = (() => {
                    if (self.config["use-shadow-dom"]) {
                        return htmlContentNode.attachShadow({ mode: "open" });
                    }

                    return htmlContentNode;
                })();

                // Store a reference to the root node to preserve backward
                // compatibility with code that does something it's not
                // supposed to (reading "private" widget state).
                self.vars.html = rootNode;

                // Parse the content and attach it to the root node.
                rootNode.appendChild(documentFragmentFromHTML(self.config.html));
            },

            /**
             * Returns the root of the HTML content.
             *
             * @returns {Node & InnerHTML & ParentNode}
             */
            getRootNode: function() {
                return this.vars.html;
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals control template.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iframe",
 *     "name": null,
 *     "template": "default/iframe"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "Iframe", // control name in camel-case
        uiType = "iframe", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    var defConfig = {
        "class-name": "iframe",
        "name": null,
        "template": "default/iframe ",
        "src": "about:blank",
        "disable-scrollbar": false
    };

    // declare private functions - START

    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            iframe: null
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            /* example - add import via function call */
            qm: function() {
                return shmi.visuals.session.QueryManager;
            }
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;
                self.vars.iframe = self.element.querySelector("iframe");
                if (self.vars.iframe) {
                    self.vars.iframe.onerror = function(e) {
                        shmi.notify("Error loading IFrame: " + e.toString());
                    };
                    self.vars.iframe.setAttribute("src", self.config.src);
                    if (self.config["disable-scrollbar"]) {
                        self.vars.iframe.setAttribute("scrolling", "no");
                        self.vars.iframe.style.overflow = 'hidden';
                    }
                }
            },
            /* called when control is enabled */
            onEnable: function() {

            },
            /* called when control is disabled */
            onDisable: function() {

            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                if (self.vars.iframe) {
                    self.vars.iframe.style.pointerEvents = "none";
                }
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                if (self.vars.iframe) {
                    self.vars.iframe.style.pointerEvents = "";
                }
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step) {

            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * ImageChanger control - displays different images depending on
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "image-changer",
 *     "name": null,
 *     "template": "default/image-changer",
 *     "options": [],
 *     "default-image": "pics/system/icons/placeholder.svg",
 *     "default-title": null
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * options {object[]}: Array of configurable images to display for each value
 * default-image {string}: URL of image to display as default
 * default-title {string}: Default image title
 *
 * @version 1.0.0
 */
(function() {
    'use strict';

    //variables for reference in control definition
    var className = "ImageChanger", //control name in camel-case
        uiType = "image-changer", //control keyword (data-ui)
        isContainer = false;

    //example - default configuration
    var defConfig = {
        "class-name": "image-changer",
        "name": null,
        "template": "default/image-changer",
        "options": [],
        "default-image": "pics/system/icons/placeholder.svg",
        "default-title": null
    };

    //setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    //declare private functions - START

    function getActive(self, value) {
        var opt = self.config.options.find(function(o) {
            return o.value === value;
        });

        if (!opt) {
            opt = {
                "icon-src": self.config["default-image"],
                "label": self.config["default-title"] || null
            };
        }
        self.vars.activeOption = opt;

        self.vars.elements.img.setAttribute("src", opt["icon-src"]);
        self.setTooltip(self.getTooltip());
    }

    //declare private functions - END

    //definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            tokens: [],
            listeners: [],
            value: null,
            activeOption: null,
            elements: {
                img: null
            }
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            /* example - add import via function call */
            qm: function() {
                return shmi.visuals.session.QueryManager;
            }
        },
        /* array of custom event types fired by this control */
        events: [],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            getTooltip: function() {
                var self = this,
                    superTooltip = shmi.visuals.core.BaseControl.prototype.getTooltip.call(this);

                if (superTooltip) {
                    return superTooltip;
                } else if (self.activeOption && self.activeOption.label) {
                    return self.activeOption.label;
                }

                return null;
            },
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this,
                    elements = self.vars.elements;

                elements.img = shmi.getUiElement("image", self.element);
                getActive(self, null); //initialize default image
            },
            /* called when control is enabled */
            onEnable: function() {
                var self = this,
                    im = shmi.requires("visuals.session.ItemManager");

                if (self.config.item) {
                    self.vars.tokens.push(im.subscribeItem(self.config.item, self));
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                var self = this;
                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {

            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {

            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this;
                self.vars.value = value;
                getActive(self, self.vars.value);
            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * Creates a new Image control.
 *
 * The Image control can be used to execute arbitrary javascript code at
 * predefined places in an hmi-app. The configured script will be run every time
 * the Image control is enabled.
 *
 * @constructor
 * @param {HTMLElement} element root element of control
 * @param {object} config control configuration object
 */
shmi.visuals.controls.Image = function(element, config) {
    this.element = element;
    this.config = config || {};
    this.run = false;
    this.loaded = false;

    shmi.def(this.config, 'image-src', 'pics/system/icons/placeholder.svg');
    shmi.def(this.config, 'image-alt', null);
    shmi.def(this.config, 'image-title', null);
    shmi.def(this.config, 'class-name', 'image');
    shmi.def(this.config, 'template', 'default/image');
    shmi.def(this.config, 'label', "image");

    this.listeners = [];

    this.parseAttributes();

    this.imgElement = null;

    this.startup();
};

shmi.visuals.controls.Image.prototype = {
    uiType: "image",
    isContainer: false,
    events: ["click"],
    tooltipProperties: ["image-title"],
    getClassName: function() {
        return "Image";
    },
    /**
     * loads configured script file
     *
     */
    onInit: function() {
        var self = this,
            io = shmi.visuals.io,
            handler = null,
            ml = null,
            tl = null;

        this.imgElement = shmi.getUiElement("image", this.element);
        this.labelElement = shmi.getUiElement("label", this.element);
        if (this.imgElement) {
            this.imgElement.setAttribute("draggable", "false");
            if (this.config['image-src']) {
                this.imgElement.setAttribute("src", this.config['image-src']);
            }
            if (this.config['image-alt']) {
                this.imgElement.setAttribute("alt", this.config['image-alt']);
            }

            handler = {
                onClick: function(x, y, event) {
                    self.fire("click", {
                        x: x,
                        y: y,
                        event: event
                    });
                }
            };
            ml = new io.MouseListener(self.imgElement, handler);
            tl = new io.TouchListener(self.imgElement, handler);
            self.listeners.push(ml, tl);
        }

        if (this.labelElement) {
            this.labelElement.textContent = shmi.localize(this.config.label);
        }
    },
    /**
     * run when control is enabled. executes configured script.
     *
     */
    onEnable: function() {
        var self = this;
        self.listeners.forEach(function(l) {
            l.enable();
        });
    },
    /**
     * run when control is disabled
     *
     */
    onDisable: function() {
        var self = this;
        self.listeners.forEach(function(l) {
            l.disable();
        });
    },
    onLock: function() {
        var self = this;
        self.listeners.forEach(function(l) {
            l.disable();
        });
        shmi.addClass(self.element, "locked");
    },
    onUnlock: function() {
        var self = this;
        self.listeners.forEach(function(l) {
            l.enable();
        });
        shmi.removeClass(self.element, "locked");
    },
    setLabel: function(labelText) {
        var self = this;
        if (self.labelElement) {
            self.labelElement.textContent = shmi.localize(labelText);
        }
    }
};

shmi.extend(shmi.visuals.controls.Image, shmi.visuals.core.BaseControl);
shmi.registerControlType('image', shmi.visuals.controls.Image, false);

(function() {
    shmi.pkg("visuals.controls");
    /**
     * Creates a new Input Field control
     *
     * @constructor
     * @extends shmi.visuals.core.BaseControl
     * @param element - base element of the control
     * @param config - configuration of the control
     */
    shmi.visuals.controls.InputField = function(element, config) {
        this.element = element;
        this.value = null;
        this.config = config || {};

        this.parseAttributes();

        shmi.def(this.config, 'class-name', 'input-field');
        shmi.def(this.config, 'template', 'default/input-field');
        shmi.def(this.config, 'name', null);
        shmi.def(this.config, 'item', null);
        shmi.def(this.config, 'value-alignment', "left"); //"left" |"right" | "auto"
        shmi.def(this.config, 'numeric-class', 'numeric');
        shmi.def(this.config, 'min', Number.NEGATIVE_INFINITY);
        shmi.def(this.config, 'auto-min', true);
        shmi.def(this.config, 'max', Number.POSITIVE_INFINITY);
        shmi.def(this.config, 'auto-max', true);
        shmi.def(this.config, 'step', 0);
        shmi.def(this.config, 'auto-step', false);
        shmi.def(this.config, 'precision', -1);
        shmi.def(this.config, 'auto-precision', true);
        shmi.def(this.config, 'unit-scale', 1);
        shmi.def(this.config, 'decimal-delimiter', '.');
        shmi.def(this.config, 'label', "input-field");
        shmi.def(this.config, 'auto-label', true);
        shmi.def(this.config, 'unit-text', null);
        shmi.def(this.config, 'auto-unit-text', true);
        shmi.def(this.config, 'type', shmi.c("TYPE_STRING"));
        shmi.def(this.config, 'auto-type', true);
        shmi.def(this.config, 'numpad-enabled', false);
        shmi.def(this.config, 'multiline', false);
        shmi.def(this.config, 'value-as-tooltip', false);

        this.type = shmi.c("TYPE_STRING");

        this.valueElement = null;
        this.unitElement = null;
        this.textareaElement = null;
        this.mouseListener = null;
        this.touchListener = null;

        this.active = false;
        this._subscriptionTargetId = null;
        this._timeout = 0;
        this.vars = {
            to_enter: 0,
            wasClicked: false
        };

        this.startup();
    };

    function insertValue(self, value) {
        if (self.valueElement.tagName === "INPUT") {
            self.valueElement.value = "";
            self.valueElement.value = value;
        } else if (!self.config.multiline) {
            self.valueElement.textContent = value;
            if (!self.valueElement.firstChild) {
                self.valueElement.appendChild(document.createTextNode(""));
            }
        } else {
            self.valueElement.value = value;
        }

        if (self.config["value-as-tooltip"]) {
            if (value !== null && typeof value !== "undefined" && value !== "") {
                self.setTooltip(String(value));
            } else {
                self.setTooltip(self.getTooltip());
            }
        }
    }

    function retrieveValue(self) {
        if (self.valueElement.tagName === "INPUT") {
            return self.valueElement.value;
        } else if (!self.config.multiline) {
            return self.valueElement.textContent;
        } else {
            return self.valueElement.value;
        }
    }

    function selectContent(self) {
        if (self.valueElement.tagName === 'INPUT') {
            self.valueElement.select();
        } else {
            var range = document.createRange();
            shmi.addClass(self.valueElement, "selectableText");
            range.setStart(self.valueElement.firstChild, 0);
            range.setEnd(self.valueElement.lastChild, self.valueElement.lastChild.length);
            window.getSelection().removeAllRanges();
            window.getSelection().addRange(range);
        }
    }

    /**
     * handleFocus - handle focus of input element when control is clicked or tabbed into
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function handleFocus(self) {
        /* prevent focussing if control is locked */
        if (self.locked || self.focused) {
            return;
        }

        if ((!self.focused) && (shmi.visuals.session.FocusElement !== null)) {
            shmi.visuals.session.FocusElement.blur();
            shmi.visuals.session.FocusElement = null;
        }
        shmi.log("[InputField] focused", 1);
        if (showNumpad(self)) {
            return;
        } else if (showKeyboard(self)) {
            return;
        }

        selectContent(self);

        self.focused = true;
        self.valueElement.focus();
        shmi.visuals.session.FocusElement = self.valueElement;

        clearTimeout(self.vars.to_enter);
        self.vars.to_enter = setTimeout(function() {
            self.fire("enter", {
                value: self.value
            });
        }, 250);
    }

    /**
     * @param {object} self
     * @return {boolean} if numpad will be shown
     */
    function showNumpad(self) {
        if (!self.config["numpad-enabled"]) { //does nothing if not enabled
            return false;
        }

        var nv = shmi.requires("visuals.tools.numericValues"),
            vs = null,
            params = null,
            im = shmi.visuals.session.ItemManager;

        if (!(self.vars && self.vars.valueSettings)) {
            nv.initValueSettings(self);
        }

        params = {
            "decimal-delimiter": self.config["decimal-delimiter"],
            "unit": (self.vars.unit !== undefined) ? self.vars.unit : self.config["unit-text"],
            "label": (self.vars.label !== undefined) ? self.vars.label : self.config.label,
            "value": retrieveValue(self),
            "callback": function(res) {
                if (self.config.item) {
                    if (self.config.multiline) res = res.toString();
                    im.writeValue(self.config.item, res);
                } else {
                    self.setValue(res);
                }
            }
        };

        vs = self.vars.valueSettings;
        params.min = vs.min;
        params.max = vs.max;
        params.type = vs.type;
        params.precision = vs.precision;

        shmi.numpad(params);
        return true;
    }

    /**
    * @param {object} self
    * @return {boolean} if keyboard will be shown
    */
    function showKeyboard(self) {
        var appConfig = shmi.requires("visuals.session.config"),
            keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`
        if (!keyboardEnabled) { //does nothing if not enabled
            return false;
        }

        var im = shmi.visuals.session.ItemManager,
            params = {
                "value": retrieveValue(self),
                "select-box-enabled": appConfig.keyboard["language-selection"],
                "password-input": self.valueElement && self.valueElement.type && self.valueElement.type.toLowerCase() === "password",
                "show-enter": self.config.multiline,
                "callback": function(success, input) {
                    if (success) {
                        if (self.config.item) {
                            im.writeValue(self.config.item, input);
                        } else {
                            self.setValue(input);
                        }
                    }
                }
            };

        shmi.keyboard(params);
        return true;
    }

    shmi.visuals.controls.InputField.prototype = {
        uiType: "input-field",
        events: ["change", "enter"],
        getClassName: function() {
            return "InputField";
        },
        /**
         * Initializes control
         *
         * @returns {unresolved}
         */
        onInit: function() {
            var self = this,
                c = shmi.Constants;

            this.valueElement = shmi.getUiElement('input-field-value', this.element);
            if (!this.valueElement) {
                shmi.log('[InputField] no input-field-value element provided', 3);
                return;
            }
            this.unitElement = shmi.getUiElement('input-field-unit', this.element);
            if (!this.unitElement) {
                shmi.log('[InputField] no input-field-unit element provided', 1);
            }

            this.labelElement = shmi.getUiElement('input-field-label', this.element);
            if (!this.labelElement) {
                shmi.log('[InputField] no input-field-label element provided', 1);
            } else if (this.config.label !== undefined) {
                self.vars = self.vars || {};
                self.vars.label = this.config.label;
                this.labelElement.textContent = shmi.localize(this.config.label);
            }
            if (this.config.multiline) {
                this.valueBox = this.element.getElementsByClassName('value-box')[0];
                if (!this.valueBox) {
                    shmi.log('[InputField] no value-box element provided', 3);
                    return;
                }
                this.valueElement.remove();
                var el = document.createElement("textarea");
                el.setAttribute('data-ui', 'input-field-value');
                el.disabled = true;
                this.textareaElement = el;
                shmi.addClass(el, 'input-field-value');
                this.valueElement = this.valueBox.appendChild(el);
                shmi.addClass(self.element, 'textarea');
            } else {
                this.textareaElement = null;
            }
            /* all required elements found */
            this.valueElement.setAttribute('tabindex', '0');
            insertValue(self, "");

            var type = parseInt(this.config.type);
            if (!isNaN(type)) {
                this.type = type;
            }

            if (this.config.action) {
                this.action = new shmi.visuals.core.UiAction(this.config.action, this);
            }

            if (!self.config['auto-type'] && (self.config['value-alignment'] === "auto")) {
                if ([c.TYPE_BOOL, c.TYPE_INT, c.TYPE_FLOAT].indexOf(self.type) !== -1) {
                    shmi.addClass(self.element, self.config['numeric-class']);
                }
            } else if (self.config['value-alignment'] === "right") {
                shmi.addClass(self.element, self.config['numeric-class']);
            }

            var ioFuncs = {};
            ioFuncs.onPress = function(sx, sy, event) {
                if ((this.valueElement.tagName !== 'INPUT') && (!this.focused)) {
                    event.preventDefault();
                } else if (this.focused) {
                    window.getSelection().removeAllRanges();
                }
            }.bind(this);

            ioFuncs.onClick = function() {
                self.vars.wasClicked = true;
                handleFocus(self);
                self.vars.wasClicked = false;
            };
            this.mouseListener = new shmi.visuals.io.MouseListener(this.valueElement, ioFuncs);
            this.touchListener = new shmi.visuals.io.TouchListener(this.valueElement, ioFuncs, true);
            this.valueElement.addEventListener('focus', function() {
                if (self.config.multiline) {
                    self.focused = true;
                } else if (!self.vars.wasClicked) {
                    handleFocus(self);
                }
            });
            this.valueElement.addEventListener('keypress', function(event) {
                if (((event.keyCode === 13) && (!self.config.multiline)) || (event.keyCode === 9)) {
                    event.preventDefault();
                    window.getSelection().removeAllRanges();
                    this.valueElement.blur();
                }
            }.bind(this));

            this.valueElement.addEventListener('blur', function() {
                if (!this.focused) {
                    return;
                }
                shmi.log("[InputField] blur event", 1);
                window.getSelection().removeAllRanges();

                shmi.removeClass(this.valueElement, 'selectableText');
                this.validate(this.valueElement);
                this.focused = false;
                shmi.visuals.session.FocusElement = null;
            }.bind(this));
            // "validation lite" for the delimeters.
            this.valueElement.addEventListener('keydown', function(evt) {
                if (self.type === shmi.c("TYPE_FLOAT")) {
                    var wrongDelimeter = (self.config['decimal-delimiter'] === ".") ? "," : ".";
                    if (evt.key === wrongDelimeter) {
                        evt.preventDefault();
                    }
                }
            });

            if (this.config['unit-text']) {
                self.vars = self.vars || {};
                self.vars.unit = this.config['unit-text'];
                if (this.unitElement) this.unitElement.textContent = shmi.localize(this.config['unit-text']);
            }
            if (this.config['unit-scale']) {
                this.config['unit-scale'] = parseFloat(shmi.localize(this.config['unit-scale']));
            }
        },
        /**
         * Validates the content of the specified element
         *
         * @param element - element to validate
         */
        validate: function(element) {
            const self = this,
                im = shmi.visuals.session.ItemManager,
                nv = shmi.requires("visuals.tools.numericValues"),
                inputString = retrieveValue(self),
                oldValue = self.value;

            if (inputString === oldValue) {
                return;
            }

            if (!self.config['auto-type']) {
                // No automatic type inference.
            } else if (self.config.multiline) {
                // Multi-line is always string.
                self.type = shmi.c("TYPE_STRING");
            } else if (self.config.item) {
                const item = im.getItem(self.config.item);
                if (item) {
                    self.type = item.type;
                }
            }

            const newValue = (() => {
                if (self.config.multiline) {
                    // Multi-line is always string, therefore no check and
                    // formatting is performed on it.
                    return inputString;
                } else if (![shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")].includes(self.type)) {
                    return inputString;
                } else if (!self.floatRegexp.test(inputString.replace(self.config['decimal-delimiter'], '.'))) {
                    return oldValue;
                }

                return nv.applyInputSettings(inputString, self);
            })();

            self.value = newValue;
            if (typeof newValue === "undefined" || newValue === null) {
                insertValue(self, "");
            } else {
                insertValue(self, nv.formatOutput(newValue, self));
            }

            // Emit change event, write item and call UI actions only when something has changed
            if (newValue !== oldValue) {
                if (self.config.item) {
                    im.writeValue(self.config.item, newValue);
                }

                // UI Action
                if (self.action) {
                    self.action.execute(newValue);
                }

                self.fire("change", {
                    value: self.value
                });

                if (self.onChange) {
                    self.onChange(self.value);
                }
            }
        },
        /**
         * Sets min & max values and stepping of subscribed variable
         *
         * @param min - min value
         * @param max - max value
         * @param step - value stepping
         */
        onSetProperties: function(min, max, step, name, type) {
            var self = this,
                c = shmi.Constants,
                nv = shmi.requires("visuals.tools.numericValues");

            if (self.config['auto-type'] && (self.config['value-alignment'] === "auto")) {
                if ([c.TYPE_BOOL, c.TYPE_INT, c.TYPE_FLOAT].indexOf(type)) {
                    shmi.addClass(self.element, self.config['numeric-class']);
                }
            }

            nv.setProperties(self, arguments);

            shmi.log("[InputField] min: " + min + " max: " + max + " step: " + step, 1);
        },
        /**
         * Sets the current value
         *
         * @param value - new value to set
         */
        onSetValue: function(value) {
            var self = this,
                im = shmi.visuals.session.ItemManager,
                nv = shmi.requires("visuals.tools.numericValues"),
                item = null,
                oldValue = self.value;

            if (self.config.item && self.config['auto-type']) {
                item = im.getItem(self.config.item);
                if (item) {
                    self.type = item.type;
                } else if (shmi.visuals.session.config.debug) {
                    console.debug(self.getClassName(), "configured item not available:", self.config.item);
                }
            }

            self.value = value;

            insertValue(self, nv.formatOutput(self.value, self));
            if (document.activeElement === self.valueElement) {
                selectContent(self);
            }
            shmi.log("[InputField] new value: " + this.value, 0);

            if (this.value !== oldValue) {
                this.fire("change", {
                    value: this.value
                });

                if (this.onChange) {
                    this.onChange(this.value);
                }
            }
        },
        /**
         * Retrieves to current value
         *
         * @return value
         */
        getValue: function() {
            var type = this.type;
            if (this.config.item && this.config['auto-type']) {
                type = shmi.visuals.session.ItemManager.items[this.config.item].type;
            }
            if (type === shmi.c("TYPE_INT")) {
                return Math.round(this.value / this.config['unit-scale']);
            } else if (type === shmi.c("TYPE_FLOAT")) {
                return (this.value / this.config['unit-scale']);
            } else {
                return this.value;
            }
        },
        /**
         * Enables the InputField
         *
         */
        onEnable: function() {
            this.valueElement.setAttribute('contenteditable', true);
            if (!this.config.multiline) {
                this.mouseListener.enable();
                this.touchListener.enable();
            }
            if (this.config.item) {
                this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
            }
            if (this.textareaElement) this.textareaElement.disabled = false;
            shmi.log("[InputField] enabled", 1);
        },
        /**
         * Disables the InputField
         *
         */
        onDisable: function() {
            this.valueElement.setAttribute('contenteditable', false);
            shmi.removeClass(this.valueElement, 'selectableText');
            if (this.config.item) {
                shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
            }
            if (!this.config.multiline) {
                this.mouseListener.disable();
                this.touchListener.disable();
            }
            shmi.log("[InputField] disabled", 1);
        },
        /**
         * Lockes the InputField
         *
         */
        onLock: function() {
            this.locked = true;
            shmi.log("[InputField] locked", 1);
            if (this.valueElement.tagName === 'INPUT') {
                this.valueElement.setAttribute('disabled', true);
            } else {
                this.valueElement.setAttribute('contenteditable', false);
            }
            shmi.removeClass(this.valueElement, 'selectableText');
            if (!this.config.multiline) {
                this.mouseListener.disable();
                this.touchListener.disable();
            }
            if (this.textareaElement) this.textareaElement.disabled = true;
            shmi.addClass(this.element, 'locked');
        },
        /**
         * Unlockes the InputField
         *
         */
        onUnlock: function() {
            this.locked = false;
            shmi.log("[InputField] unlocked", 1);
            if (this.valueElement.tagName === 'INPUT') {
                this.valueElement.removeAttribute('disabled');
            } else {
                this.valueElement.setAttribute('contenteditable', true);
            }
            if (!this.config.multiline) {
                this.mouseListener.enable();
                this.touchListener.enable();
            }
            if (this.textareaElement) this.textareaElement.disabled = false;
            shmi.removeClass(this.element, 'locked');
        },
        setLabel: function(labelText) {
            var self = this;
            if (self.config['auto-label'] && self.labelElement) {
                self.vars = self.vars || {};
                self.vars.label = labelText;
                self.labelElement.textContent = shmi.localize(labelText);
            }
        },
        setUnitText: function(unitText) {
            var self = this;
            if (self.config['auto-unit-text'] && self.unitElement) {
                self.vars = self.vars || {};
                self.vars.unit = unitText;
                self.unitElement.textContent = shmi.localize(unitText);
            }
        },
        floatRegexp: /(^[+-]?[0-9]([.][0-9]*)?$|^[+-]?[1-9]+[0-9]*([.][0-9]*)?$)/,
        intRegexp: /(^[+-]?[0-9]$|^[+-]?[1-9]+[0-9]*$)/
    };

    shmi.extend(shmi.visuals.controls.InputField, shmi.visuals.core.BaseControl);
}());

/**
 * Control IQ Alarm-Info
 *
 * Configuration options (default):
 *
 * {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-alarm-info",
        "noAlarm": null,
        "action": null,
        "enableCycle": true,
        "cycleInterval": 1000,
        "showAlarm": true,
        "showWarn": true,
        "showInfo": true,
        "groupFilter": null,
        "icon-info": "pics/system/controls/iq-alarm-info/icon-info.svg",
        "icon-warning": "pics/system/controls/iq-alarm-info/icon-warning.svg",
        "icon-alarm": "pics/system/controls/iq-alarm-info/icon-alarm.svg"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 */
(function() {
    'use strict';

    //variables for reference in control definition
    const className = "IQAlarmInfo", //control name in camel-case
        uiType = "iq-alarm-info", //control keyword (data-ui)
        isContainer = false;

    //example - default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-alarm-info",
        "noAlarm": null,
        "action": null,
        "enableCycle": true,
        "cycleInterval": 1000,
        "showAlarm": true,
        "showWarn": true,
        "showInfo": true,
        "groupFilter": null,
        "icon-info": "pics/system/controls/iq-alarm-info/icon-info.svg",
        "icon-warning": "pics/system/controls/iq-alarm-info/icon-warning.svg",
        "icon-alarm": "pics/system/controls/iq-alarm-info/icon-alarm.svg",
        "icon-idle": null
    };

    //setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        log = logger.log;

    // Constants and private fields
    const ALARM_STATE_SELECTORS = ["severity-info", "severity-warning", "severity-alarm"];

    //declare private functions - START

    /**
     * parseAlarmGroupRange - parse range of alarm groups
     *
     * @param {string} rangeText input text (e.g. `3-7`)
     * @returns {number[2]|null} range start & end tuple or `null` if none could be parsed
     */
    function parseAlarmGroupRange(rangeText) {
        const tuple = rangeText.split("-");

        if (tuple.length === 2) {
            const start = parseInt(tuple[0]),
                end = parseInt(tuple[1]);

            if (!(isNaN(start) || isNaN(end)) && start >= 0 && end >= start) {
                return [start, end];
            }
        }

        return null;
    }

    /**
     * parseAlarmGroupFilter - parse group ID filter from config string
     *
     * @param {string} groupFilter comma separated group ID filter
     * @returns {array} array of group IDs and ranges
     */
    function parseAlarmGroupFilter(groupFilter) {
        if (typeof groupFilter !== "string") {
            return null;
        }

        let entries = groupFilter.split(",");
        entries = entries.map((entry) => {
            if (entry.includes("-")) {
                return parseAlarmGroupRange(entry);
            }

            const groupId = parseInt(entry);
            if (!isNaN(groupId) && groupId >= 0) {
                return groupId;
            }

            return null;
        });
        entries = entries.filter((entry) => entry !== null);
        const rangeEntries = [];
        entries.forEach((entry) => {
            if (Array.isArray(entry)) {
                let idx = entry[0];
                while (idx <= entry[1]) {
                    if (!(rangeEntries.includes(idx) && entries.includes(idx))) {
                        rangeEntries.push(idx);
                    }
                    idx += 1;
                }
            }
        });
        entries.push(...rangeEntries);

        return entries.length ? entries : null;
    }

    /**
     * testAlarmState - test & update state of displayed alarm
     *
     * @param {object} self instance reference
     */
    function testAlarmState(self) {
        const alarmArray = Object.values(self.imports.am.alarms).map((value) => value.properties);
        if (Array.isArray(alarmArray)) {
            if (alarmArray.length < 1) {
                clearAlarmMsg(self);
            } else {
                clearInt(self);
                if (!self.config.enableCycle) {
                    // standard mode: lookup for last Alarm of highest class
                    self.vars.activeAlarmList = getLastHighest(self, alarmArray);
                } else {
                    // cycleMode: cycle display of all active alarms
                    self.vars.activeAlarmList = getEnabledAlarms(self, alarmArray);
                    self.vars.alarmCount = self.vars.activeAlarmList.length;
                    if (self.vars.alarmCount > 1) {
                        self.vars.interval = setInterval(displayAlarms.bind(null, self), self.config.cycleInterval);
                    }
                }
                displayAlarms(self);
            }
        }
    }

    // helper: displays alarms in activeAlarmList
    function displayAlarms(self) {
        if (self.vars.activeAlarmList.length === 0) {
            clearAlarmMsg(self);
        }
        if (self.vars.currentAlarm >= self.vars.activeAlarmList.length) {
            self.vars.currentAlarm = 0;
        }
        const alarm = self.vars.activeAlarmList[self.vars.currentAlarm++];
        let msg = null,
            details = "";
        if (alarm) { // evaluate context items etc.
            msg = shmi.evalString(shmi.localize(`\${alarm_title_${alarm.index}}`), alarm);
            details = shmi.evalString(shmi.localize(`\${alarm_msg_${alarm.index}}`), alarm);
        } else {
            msg = shmi.localize(self.config.noAlarm);
        }
        setAlarmStyle(alarm ? alarm.severity : -1, self);
        setTextContent(self, "alarmMessage", msg);
        setTextContent(self, "alarmDetails", details);
        setTextContent(self, "alarmCount", self.vars.alarmCount);
    }

    // helper: get alarm object of the last Alarm and highest class (=severity)
    function getLastHighest(self, alarmList) {
        let lastAlarm = null;
        const enabledAlarms = getEnabledAlarms(self, alarmList);
        self.vars.alarmCount = enabledAlarms.length;
        enabledAlarms.forEach(function(alarmInfo) {
            // Find latest alarm with the highest occurring severity
            if (lastAlarm === null) {
                lastAlarm = alarmInfo;
            } else if (alarmInfo.severity >= lastAlarm.severity && alarmInfo.timestamp_in > lastAlarm.timestamp_in) {
                lastAlarm = alarmInfo;
            }
        });

        return lastAlarm === null ? [] : [lastAlarm];
    }

    // helper: select alarms to be displayed dependig on the config.notShowWarn and config.notShowInfo parameters
    function getEnabledAlarms(self, alarmList) {
        const enabledAlarms = alarmList.filter(function(alarmInfo) {
            if (Array.isArray(self.vars.groupFilter)) {
                if (!self.vars.groupFilter.includes(alarmInfo.group)) {
                    return false;
                }
            }

            //filter inactive alarms
            if (!alarmInfo.active && (!alarmInfo.acknowledgeable || alarmInfo.acknowledged)) {
                return false;
            }

            // Filter alarms based on control configuration
            return (alarmInfo.severity === 2 && self.config.showAlarm) ||
                (alarmInfo.severity === 1 && self.config.showWarn) ||
                (alarmInfo.severity === 0 && self.config.showInfo);
        });

        return enabledAlarms;
    }

    // helper: clears the alarm message
    function clearAlarmMsg(self) {
        clearInt(self);
        self.vars.currentAlarm = 0;
        setAlarmStyle(-1, self);
        setTextContent(self, "alarmMessage", shmi.localize(self.config.noAlarm));
    }

    // helper: clears the interval
    function clearInt(self) {
        if (self.vars.interval) {
            clearInterval(self.vars.interval);
            self.vars.interval = null;
        }
    }

    /**
     * setAlarmIcon - set active alarm state icon, hide icon if none set
     *
     * @param {object} self control instance
     * @param {string} optionName icon option name
     */
    function setAlarmIcon(self, optionName) {
        if (self.vars.elements.stateIcon) {
            if (self.config[optionName]) {
                self.vars.elements.stateIcon.style.visibility = "";
                self.vars.elements.stateIcon.style.backgroundImage = `url(${self.config[optionName]})`;
            } else {
                self.vars.elements.stateIcon.style.visibility = "hidden";
                self.vars.elements.stateIcon.style.backgroundImage = "";
            }
        }
    }
    // helper: sets alarm class (state)
    function setAlarmStyle(state, self) {
        const wrapper = self.vars.elements.wrapper;

        switch (state) {
        case 0:
            setAlarmIcon(self, "icon-info");
            break;
        case 1:
            setAlarmIcon(self, "icon-warning");
            break;
        case 2:
            setAlarmIcon(self, "icon-alarm");
            break;
        default:
            setAlarmIcon(self, "icon-idle");
        }
        if (wrapper) {
            shmi.removeClass(wrapper, ALARM_STATE_SELECTORS.join(" "));
            if (ALARM_STATE_SELECTORS[state]) {
                shmi.addClass(wrapper, ALARM_STATE_SELECTORS[state]);
            }
        }
    }

    /**
     * setTextContent - set text content of specified element
     *
     * @param {object} self control instance
     * @param {string} elementName name of element
     * @param {string} text text to set
     */
    function setTextContent(self, elementName, text) {
        if (self.vars.elements[elementName]) {
            self.vars.elements[elementName].textContent = text;
        }
    }
    //declare private functions - END

    //definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            elements: {
                stateIcon: null,
                alarmCount: null,
                alarmMessage: null,
                button: null,
                wrapper: null
            },
            alarmCount: 0,
            listeners: [],
            tokens: [],
            activeAlarmList: [],
            currentAlarm: 0,
            interval: 0,
            alarmSubscriber: null,
            groupFilter: null
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via function call */
            am: "visuals.session.AlarmManager"
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                self.vars.groupFilter = parseAlarmGroupFilter(self.config.groupFilter);

                self.vars.elements.alarmCount = shmi.getUiElement('alarm-count', self.element);
                if (!self.vars.elements.alarmCount) {
                    log(uiType, "alarm-count not found in template");
                }

                self.vars.elements.wrapper = shmi.getUiElement('alarm-info-wrapper', self.element);
                if (!self.vars.elements.wrapper) {
                    log(uiType, "alarm-info-wrapper not found in template");
                }

                self.vars.elements.stateIcon = shmi.getUiElement('state-icon', self.element);
                if (!self.vars.elements.stateIcon) {
                    log(uiType, "state-icon not found in template");
                }

                self.vars.elements.alarmMessage = shmi.getUiElement('alarm-message', self.element);
                if (!self.vars.elements.alarmMessage) {
                    log(uiType, "alarm-message not found in template");
                } else {
                    setTextContent(self, "alarmMessage", shmi.localize(self.config.noAlarm));
                }

                self.vars.elements.alarmDetails = shmi.getUiElement('alarm-details', self.element);
                if (!self.vars.elements.alarmDetails) {
                    log(uiType, "alarm-details not found in template");
                } else {
                    setTextContent(self, "alarmDetails", "");
                }

                self.vars.elements.button = shmi.getUiElement('alert-list-button', self.element);
                if (self.vars.elements.button && Array.isArray(self.config.action) && self.config.action.length) {
                    const core = shmi.requires("visuals.core"),
                        io = shmi.requires("visuals.io"),
                        action = new core.UiAction(self.config.action),
                        handler = {
                            onClick: () => action.execute()
                        },
                        ml = new io.MouseListener(self.vars.elements.button, handler), //MouseListener
                        tl = new io.TouchListener(self.vars.elements.button, handler); //TouchListener

                    shmi.addClass(self.element, "has-action");
                    self.vars.listeners.push(ml, tl);
                } else if (!self.vars.elements.button) {
                    log(uiType, "click element not found in template");
                }

                setAlarmStyle(-1, self);
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.stateIcons = shmi.getUiElements('state', self.element) || [];
                self.vars.alarmSubscriber = self.imports.am.subscribeAlarms(self, (alarm, isLast) => {
                    if (isLast) {
                        testAlarmState(self);
                    }
                });
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.imports.am.unsubscribeAlarms(self.vars.alarmSubscriber);
                clearInt(self);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                shmi.addClass(self.element, "locked");
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                shmi.removeClass(self.element, "locked");
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();

/**
 * iq-alarm-list
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-alarm-list",
 *     "name": null,
 *     "template": "default/iq-alarm-list"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */

/*eslint-env node */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-alarm-list", // control name in camel-case
        uiType = "iq-alarm-list", // control keyword (data-ui)
        isContainer = true;

    // default configuration
    const defConfig = {
        "class-name": "iq-alarm-list",
        "name": null,
        "template": "default/iq-alarm-list.variant-01",
        "label": '[Label]',
        "show-text": false,
        "group-filter": null,
        "alarm-image": null,
        "warn-image": null,
        "notify-image": null,
        "numberOfColumns": 7,
        "col1-selectBox": null,
        "col1-width": null,
        "col2-selectBox": null,
        "col2-width": null,
        "col3-selectBox": null,
        "col3-width": null,
        "col4-selectBox": null,
        "col4-width": null,
        "col5-selectBox": null,
        "col5-width": null,
        "col6-selectBox": null,
        "col6-width": null,
        "col7-selectBox": null,
        "col7-width": null,
        "dateformat": "${alarmlist_dateformat}",
        "allow-mode-change": false,
        "allow-group-filter-change": false,
        "display-mode": "all",
        "disable-commit": false,
        "detail-template": null,
        "comment-mode": "disabled"
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    //declare private functions - START

    /**
     * parseAlarmGroupRange - parse range of alarm groups
     *
     * @param {string} rangeText input text (e.g. `3-7`)
     * @returns {number[2]|null} range start & end tuple or `null` if none could be parsed
     */
    function parseAlarmGroupRange(rangeText) {
        const tuple = rangeText.split("-");

        if (tuple.length === 2) {
            const start = parseInt(tuple[0]),
                end = parseInt(tuple[1]);

            if (!(isNaN(start) || isNaN(end)) && start >= 0 && end >= start) {
                return [start, end];
            }
        }

        return null;
    }

    /**
     * parseAlarmGroupFilter - parse group ID filter from config string
     *
     * @param {string} groupFilter comma separated group ID filter
     * @returns {array} array of group IDs and ranges
     */
    function parseAlarmGroupFilter(groupFilter) {
        if (typeof groupFilter !== "string") {
            return null;
        }

        const entries = groupFilter.split(",").map((entry) => {
            if (entry.includes("-")) {
                return parseAlarmGroupRange(entry);
            }

            const groupId = parseInt(entry);
            if (!isNaN(groupId) && groupId >= 0) {
                return groupId;
            }

            return null;
        });

        const out = [];
        entries.forEach((entry) => {
            if (Array.isArray(entry)) {
                let idx = entry[0];
                while (idx <= entry[1]) {
                    if (!(out.includes(idx) && entries.includes(idx))) {
                        out.push(idx);
                    }
                    idx += 1;
                }
            } else if (typeof entry === "number") {
                out.push(entry);
            }
        });

        return out.length ? out : null;
    }

    /**
     * Observes startup-sequence of control
     *
     * @param {object} self Control-Obj
     * @returns {bool} Startup succeeded or not
     */
    function controlStartUp(self) {
        let anchorsFound = false,
            controlsCreated = false;
        const io = shmi.requires("visuals.io");

        self.vars.timeFilterLabel = shmi.getUiElement("filter-label", self.element);
        self.vars.activeFilterLabel = shmi.getUiElement("time-frame-label", self.element);
        self.vars.activeFilterFrom = shmi.getUiElement("start-date", self.element);
        self.vars.activeFilterTo = shmi.getUiElement("end-date", self.element);
        self.vars.resetButton = shmi.getUiElement("reset-button", self.element);
        self.vars.detailsRight = shmi.getUiElement("alarmListDetails", self.element);

        if (!self.vars.timeFilterLabel || !self.vars.activeFilterLabel || !self.vars.activeFilterFrom || !self.vars.activeFilterTo || !self.vars.resetButton) {
            return false;
        }

        self.vars.timeFilterLabel.textContent = shmi.localize("${alarmlist_set_timefilter}");

        const resetButtonHandler = {
            onClick: function() {
                clearFilter(self);
            }
        };
        self.vars.listeners.push(
            new io.MouseListener(self.vars.resetButton, resetButtonHandler),
            new io.TouchListener(self.vars.resetButton, resetButtonHandler)
        );

        self.vars.configs.alarmTableConfig["default-field-control-map"]["come"].config["display-format"] = self.config.dateformat;
        initAlarmTableGrid(self);

        anchorsFound = gatherAnchors(self);
        if (anchorsFound) controlsCreated = createChildControls(self);

        if (!self.config['allow-mode-change']) {
            self.vars.anchorEls.modeSelectBox.style.display = "none";
        }

        if (!self.config['allow-group-filter-change'] && self.vars.anchorEls.filterGroupSelectBox) {
            self.vars.anchorEls.filterGroupSelectBox.style.display = "none";
        }

        if (self.config["disable-commit"]) {
            shmi.addClass(self.vars.controls.confirmSelAlarmButton.element, "hidden locked");
            shmi.addClass(self.vars.controls.confirmAllAlarmsButton.element, "hidden locked");
        }

        if (!self.config["comment-mode"] || self.config["comment-mode"] === "disabled") {
            shmi.addClass(self.vars.controls.createThreadButton.element, "hidden locked");
        }

        const groupFilter = parseAlarmGroupFilter(self.config["group-filter"]);
        self.vars.groupFilter = groupFilter;
        if (groupFilter) {
            self.vars.grid.setFilter(7, groupFilter);
        }

        return !(!anchorsFound || !controlsCreated);
    }

    /**
     * generates a random id
     *
     * @returns {string}
     */
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    /**
     * Manages to set or remove Filter on datagrid
     *
     * @param {object} self Control-Obj
     */
    function updateTableFilter(self) {
        const filterSet = self.vars.filterSet,
            valAr = [];

        if (filterSet.error) valAr.push(2);
        if (filterSet.warn) valAr.push(1);
        if (filterSet.advise) valAr.push(0);

        self.vars.controls.alarmList.resetScroll();
        if (valAr.length > 0) {
            self.vars.grid.setFilter(3, valAr);
        } else {
            self.vars.grid.clearFilter(3);
        }
    }

    /**
     * Checks if needed grid already exists, if not grid will be created
     *
     * @param {object} self Control-Obj
     */
    function initAlarmTableGrid(self) {
        do {
            self.vars.dataGridId = "iqalarms-" + getRandomId();
        } while (self.imports.dgm.getGrid(self.vars.dataGridId) !== null);

        self.imports.dgm.grids[self.vars.dataGridId] = new shmi.visuals.core.DataGridIQAlarms(self.config["display-mode"], self.vars.dataGridId);
        self.vars.grid = self.imports.dgm.getGrid(self.vars.dataGridId);
    }

    /**
     * toggle The filter set between true and false
     *
     * @param {object} self Control-Obj
     * @param {string} type severity - 'error' or 'warn' or 'advise'
     */
    function toggleFilterSet(self, type) {
        const filterSet = self.vars.filterSet;
        filterSet[type] = !filterSet[type];
    }

    /**
     * Sets needed listener for all childcontrols etc.
     *
     * @param {object} self Control-Obj
     * @returns {bool}
     */
    function setListener(self) {
        const { controls, tokens: listeners } = self.vars,
            { showAlarmDetails, openCreateThreadDialogForAlarmInfo } = shmi.requires("visuals.tools.alarms.ls.alarmDetailsIQ");

        if (!controls.confirmSelAlarmButton || !controls.confirmAllAlarmsButton || !controls.createThreadButton || !controls.modeSelectBox || !controls.filterCheckBoxError || !controls.filterCheckBoxWarn || !controls.filterCheckBoxAdvise) {
            return false;
        }
        listeners.push(controls.confirmSelAlarmButton.listen("click", function(event) {
            const rowData = self.vars.grid.getRowData(controls.alarmList.selection.selRows[0]);
            if (rowData) {
                const alarmInfo = JSON.parse(rowData[8].value);
                if (alarmInfo.acknowledgeable && !alarmInfo.acknowledged) {
                    self.imports.am.ackAlarm(alarmInfo.real_id);
                }
            }
        }));
        listeners.push(controls.confirmAllAlarmsButton.listen("click", function(event) {
            const filters = self.vars.grid.getFilters();

            if (filters && Array.isArray(filters[7])) {
                self.imports.am.ackAlarmGroups(filters[7]);
            } else {
                self.imports.am.ackAlarm(-1);
            }
        }));
        listeners.push(controls.createThreadButton.listen("click", async function(event) {
            const rowData = self.vars.grid.getRowData(controls.alarmList.selection.selRows[0]);

            if (rowData) {
                const alarmInfo = JSON.parse(rowData[8].value);
                await openCreateThreadDialogForAlarmInfo(self, alarmInfo);

                if (self.vars.detailsRight) {
                    showAlarmDetails(alarmInfo, "inline", self.vars.detailsRight, self);
                }
            }
        }));
        if (controls.filterGroupSelectBox) {
            listeners.push(controls.filterGroupSelectBox.listen("change", function(event) {
                const { groupFilter } = self.vars;

                if (event.detail && event.detail.value !== null) {
                    self.vars.grid.setFilter(7, [event.detail.value]);
                } else if (groupFilter) {
                    self.vars.grid.setFilter(7, groupFilter);
                } else {
                    self.vars.grid.clearFilter(7);
                }
            }));
        }
        listeners.push(controls.modeSelectBox.listen("change", function(event) {
            self.vars.grid.setDisplayMode(event.detail.value);
        }));
        listeners.push(controls.filterCheckBoxError.listen("change", function(event) {
            toggleFilterSet(self, "error");
            updateTableFilter(self);
        }));
        listeners.push(controls.filterCheckBoxWarn.listen("change", function(event) {
            toggleFilterSet(self, "warn");
            updateTableFilter(self);
        }));
        listeners.push(controls.filterCheckBoxAdvise.listen("change", function(event) {
            toggleFilterSet(self, "advise");
            updateTableFilter(self);
        }));
        return true;
    }

    /**
     * Gathers all anchor-elements to append controls
     *
     * @param {object} self Control-Obj
     * @returns {bool}
     */
    function gatherAnchors(self) {
        const anchors = self.vars.anchorEls,
            io = shmi.requires("visuals.io");

        anchors.iconError = shmi.getUiElement('iconError', self.element);
        anchors.filterCheckBoxError = shmi.getUiElement('filterCheckBoxError', self.element);
        anchors.iconWarn = shmi.getUiElement('iconWarn', self.element);
        anchors.filterCheckBoxWarn = shmi.getUiElement('filterCheckBoxWarn', self.element);
        anchors.iconAdvise = shmi.getUiElement('iconAdvise', self.element);
        anchors.filterCheckBoxAdvise = shmi.getUiElement('filterCheckBoxAdvise', self.element);
        anchors.filterGroupSelectBox = shmi.getUiElement('filterGroupSelectBox', self.element);
        anchors.modeSelectBox = shmi.getUiElement("mode-select-box", self.element);
        anchors.alarmList = shmi.getUiElement('alarmList', self.element);
        anchors.confirmButtons = shmi.getUiElement('confirmButtons', self.element);
        anchors.filterButton = shmi.getUiElement("filter-button", self.element);

        if (anchors.filterButton) {
            const filterHandler = {
                    onClick: function() {
                        handleDialog(self);
                    }
                },
                ml = new io.MouseListener(anchors.filterButton, filterHandler),
                tl = new io.TouchListener(anchors.filterButton, filterHandler);
            self.vars.listeners.push(ml, tl);
        }

        return true;
    }
    /**
     * Gathers all anchor-elements to append controls
     *
     * @param {object} self Control-Obj
     * @param {object} anchors anchors object
     * @param {object} configs configuration holder object
     * @returns {bool}
     */
    function configureAlarmList(self, anchors, configs) {
        configs.alarmTableConfig.table = self.vars.dataGridId;

        if (shmi.hasClass(anchors.alarmList, "two-rows")) {
            configs.alarmTableConfig["default-layout"]["line-height"] = "78px";
        }

        if (self.config["notify-image"]) {
            configs.iconAdvise["image-src"] = self.config["notify-image"];
            configs.alarmTableConfig["default-field-control-map"].level.config.options[0]["icon-src"] = self.config["notify-image"];
        }
        if (self.config["warn-image"]) {
            configs.iconWarn["image-src"] = self.config["warn-image"];
            configs.alarmTableConfig["default-field-control-map"].level.config.options[1]["icon-src"] = self.config["warn-image"];
        }
        if (self.config["alarm-image"]) {
            configs.iconError["image-src"] = self.config["alarm-image"];
            configs.alarmTableConfig["default-field-control-map"].level.config.options[2]["icon-src"] = self.config["alarm-image"];
        }

        if (self.config["detail-template"]) {
            configs.alarmTableConfig["default-field-control-map"].json.config["detail-template"] = self.config["detail-template"];
        }

        if (self.config["comment-template"]) {
            configs.alarmTableConfig["default-field-control-map"].json.config["comment-template"] = self.config["comment-template"];
        }

        if (self.config["comment-mode"]) {
            configs.alarmTableConfig["default-field-control-map"].json.config["comment-mode"] = self.config["comment-mode"];
        }

        if (self.config["dateformat"]) {
            configs.alarmTableConfig["default-field-control-map"].json.config["dateformat"] = self.config["dateformat"];
        }

        Object.assign(configs.alarmTableConfig["default-field-control-map"].json.config, {
            iconAdvise: configs.iconAdvise,
            iconWarn: configs.iconWarn,
            iconError: configs.iconError
        });

        for (let i = 1; i <= self.config.numberOfColumns; i++) {
            if (self.config["col" + i + "-width"] !== null && self.config["col" + i + "-selectBox"]) {
                configs.alarmTableConfig["default-layout"]["column-org"]["col" + i]["column-width"] = self.config["col" + i + "-width"] + self.config["col" + i + "-selectBox"];
            }
        }

        if (self.vars.detailsRight) {
            //remove details button from CT2 and show empty detail win
            delete (configs.alarmTableConfig["default-layout"]["column-org"].col7);
            const alarmDetailsIQ = shmi.requires("visuals.tools.alarms.ls.alarmDetailsIQ");
            alarmDetailsIQ.showAlarmDetails(null, "inline", self.vars.detailsRight, self);
        }

        if (self.config["disable-commit"]) {
            //remove alarm commit button from CT2
            if (configs.alarmTableConfig["default-layout"]["column-org"].col7) {
                configs.alarmTableConfig["default-layout"]["column-org"].col6 = configs.alarmTableConfig["default-layout"]["column-org"].col7;
                delete (configs.alarmTableConfig["default-layout"]["column-org"].col7);
            } else {
                delete (configs.alarmTableConfig["default-layout"]["column-org"].col6);
            }
        }
    }
    /**
     * Creates Childcontrols
     *
     * @param {object} self Control-Obj
     * @returns {bool}
     */
    function createChildControls(self) {
        const anchors = self.vars.anchorEls,
            controls = self.vars.controls,
            configs = self.vars.configs;

        configureAlarmList(self, anchors, configs);

        controls.iconError = shmi.createControl('iq-image', anchors.iconError, configs.iconError, 'DIV');
        controls.filterCheckBoxError = shmi.createControl('iq-checkbox', anchors.filterCheckBoxError, configs.filterCheckBoxError, 'DIV');
        controls.iconWarn = shmi.createControl('iq-image', anchors.iconWarn, configs.iconWarn, 'DIV');
        controls.filterCheckBoxWarn = shmi.createControl('iq-checkbox', anchors.filterCheckBoxWarn, configs.filterCheckBoxWarn, 'DIV');
        controls.iconAdvise = shmi.createControl('iq-image', anchors.iconAdvise, configs.iconAdvise, 'DIV');
        controls.filterCheckBoxAdvise = shmi.createControl('iq-checkbox', anchors.filterCheckBoxAdvise, configs.filterCheckBoxAdvise, 'DIV');

        if (anchors.filterGroupSelectBox) {
            controls.filterGroupSelectBox = shmi.createControl('iq-select-box', anchors.filterGroupSelectBox, configs.filterGroupSelectBox, 'DIV');
        }
        controls.modeSelectBox = shmi.createControl('iq-select-box', anchors.modeSelectBox, configs.modeSelectBox, 'DIV');

        controls.confirmSelAlarmButton = shmi.createControl('iq-button', anchors.confirmButtons, configs.confirmSelAlarmButton, 'DIV');
        controls.confirmAllAlarmsButton = shmi.createControl('iq-button', anchors.confirmButtons, configs.confirmAllAlarmsButton, 'DIV');
        controls.createThreadButton = shmi.createControl('iq-button', anchors.confirmButtons, configs.createThreadButton, 'DIV');

        controls.alarmList = shmi.createControl('complex-table2', anchors.alarmList, configs.alarmTableConfig, 'DIV');

        return true;
    }
    /**
     * evaluate new unix timestamp with time data
     *
     * @param {number} dateTimestamp unix timestamp
     * @param {number} hours hours
     * @param {number} minutes minutes
     * @param {number} seconds seconds
     * @returns {number} unix timestamp
     */
    function evaluateUnixTimestamp(dateTimestamp, hours, minutes, seconds) {
        const date = new Date(dateTimestamp * 1000);
        date.setHours(hours, minutes, seconds, 0);

        return date.getTime() / 1000;
    }

    /**
     * evaluate timestamp from date/time controls
     *
     * @param {object} control group
     * @returns {number} unix timestamp
     */
    function evaluateTimestampFromControls(ctrlGroup) {
        return evaluateUnixTimestamp(
            ctrlGroup.date.getValue(),
            ctrlGroup.time.getHours(),
            ctrlGroup.time.getMinutes(),
            ctrlGroup.time.getSeconds()
        );
    }
    /**
     * handler for filter button - creates the dialog
     *
     * @param {object} self Control-Obj
     */
    function handleDialog(self) {
        const oldDialogHolder = shmi.ctrl(".alarmTimeFilterDialogHolder"),
            containerConfig = {
                "name": "alarmTimeFilterDialogHolder"
            };

        if (oldDialogHolder) {
            shmi.deleteControl(oldDialogHolder);
        }

        const dialogHolder = shmi.createControl("container", self.element, containerConfig, "DIV");
        dialogHolder.addControl([
            {
                ui: "dialog-box",
                controller: {
                    name: "alarmTimeFilterController",
                    slots: {
                        dialog: {
                            ui: "dialog-box",
                            events: ["close"]
                        },
                        selectDateStart: {
                            ui: "iq-select-date",
                            events: ["change"]
                        },
                        selectTimeStart: {
                            ui: "iq-select-time",
                            events: ["change"]
                        },
                        selectDateEnd: {
                            ui: "iq-select-date",
                            events: ["change"]
                        },
                        selectTimeEnd: {
                            ui: "iq-select-time",
                            events: ["change"]
                        },
                        cancelButton: {
                            ui: "iq-button",
                            events: ["click"]
                        },
                        applyButton: {
                            ui: "iq-button",
                            events: ["click"]
                        }
                    },
                    onChange: function(state) {
                    },
                    onDisable: function(state) {
                    },
                    onEnable: function(state) {
                        const nowTime = Math.floor((new Date(shmi.getServerTime() * 1000)).getTime() / 1000),
                            nowTimeDate = Math.floor((nowTime) / 86400) * 86400;

                        state.getInstance("selectDateStart").setValue(nowTimeDate - 86400);
                        state.getInstance("selectTimeStart").setValue(nowTime);
                        state.getInstance("selectDateEnd").setValue(nowTimeDate);
                        state.getInstance("selectTimeEnd").setValue(nowTime);
                    },
                    onEvent: function(state, slot, type, event) {
                        //...handle events generated by slot controls
                        switch (slot) {
                        case "selectDateStart":
                            state.getInstance("selectDateEnd").setValue(event.detail.value + 86400);
                            state.getInstance("selectTimeEnd").setValue(state.getInstance("selectTimeStart").getValue());
                            break;
                        case "applyButton":
                            setFilter(self, state);
                            state.getInstance("dialog").hide();
                            break;
                        case "cancelButton":
                            state.getInstance("dialog").hide();
                            break;
                        default:
                        }
                    }
                },
                config: {
                    "title": "${alarmlist_select_time_range}",
                    "class-name": "dialog-box iq-alarm-list iq-alarm-list-filter-dialog",
                    "template": "default/dialog-box",
                    "name": "alarmTimeFilterDialog",
                    "top-level": true,
                    "content-template": null,
                    "_controllers_": [
                        {
                            "name": "alarmTimeFilterController",
                            "slot": "dialog"
                        }
                    ]
                },
                children: [
                    {
                        ui: "container",
                        config: {
                            "class-name": "container",
                            "name": "container",
                            "template": null
                        },
                        children: [
                            {
                                ui: "iq-label",
                                config: {
                                    "class-name": "iq-label",
                                    "template": "default/iq-label.iq-variant-01",
                                    "text": "${alarmlist_from}"
                                }
                            },
                            {

                                ui: "iq-select-date",
                                config: {
                                    "label": "",
                                    "template": "default/iq-select-date.iq-variant-01",
                                    "class-name": "iq-select-date iq-variant-02",
                                    "_controllers_": [
                                        {
                                            "name": "alarmTimeFilterController",
                                            "slot": "selectDateStart"
                                        }
                                    ]
                                }
                            },
                            {
                                ui: "iq-select-time",
                                config: {
                                    "label": "",
                                    "class-name": "iq-select-time iq-variant-01 iq-icon-variant-01",
                                    "isUTC": true,
                                    "_controllers_": [
                                        {
                                            "name": "alarmTimeFilterController",
                                            "slot": "selectTimeStart"
                                        }
                                    ]
                                }
                            },
                            {
                                ui: "iq-label",
                                config: {
                                    "class-name": "iq-label",
                                    "template": "default/iq-label.iq-variant-01",
                                    "text": "${alarmlist_to}"
                                }
                            },
                            {
                                ui: "iq-select-date",
                                config: {
                                    "label": "",
                                    "template": "default/iq-select-date.iq-variant-01",
                                    "class-name": "iq-select-date iq-variant-02",
                                    "_controllers_": [
                                        {
                                            "name": "alarmTimeFilterController",
                                            "slot": "selectDateEnd"
                                        }
                                    ]
                                }
                            },
                            {
                                ui: "iq-select-time",
                                config: {
                                    "label": "",
                                    "class-name": "iq-select-time iq-variant-01 iq-icon-variant-01",
                                    "isUTC": true,
                                    "_controllers_": [
                                        {
                                            "name": "alarmTimeFilterController",
                                            "slot": "selectTimeEnd"
                                        }
                                    ]
                                }
                            },
                            {
                                ui: "container",
                                config: {
                                    "class-name": "container footer",
                                    "type": "float",
                                    "auto-width": false,
                                    "auto-margin": false,
                                    "h-alignment": "right",
                                    "v-alignment": "top",
                                    "flex-orientation": "row",
                                    "flex-distribute": false,
                                    "flex-all": false,
                                    "name": "container",
                                    "template": null
                                },
                                children: [
                                    {
                                        ui: "iq-button",
                                        config: {
                                            "class-name": "iq-button cancel-button",
                                            "label": "${cancel}",
                                            "_controllers_": [
                                                {
                                                    "name": "alarmTimeFilterController",
                                                    "slot": "cancelButton"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        ui: "iq-button",
                                        config: {
                                            "class-name": "iq-button apply-button",
                                            "label": "${okay}",
                                            "_controllers_": [
                                                {
                                                    "name": "alarmTimeFilterController",
                                                    "slot": "applyButton"
                                                }
                                            ]
                                        }
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ], function(err, controls) {
            if (err) {
                console.error("Error creating controls");
            } else {
                controls[0].show();
            }
        });
    }
    /**
     * set time filter
     *
     * @param {object} self Control-Obj
     * @param {object} state controller state
     */
    function setFilter(self, state) {
        const from = evaluateTimestampFromControls({ time: state.getInstance("selectTimeStart"), date: state.getInstance("selectDateStart") }),
            to = evaluateTimestampFromControls({ time: state.getInstance("selectTimeEnd"), date: state.getInstance("selectDateEnd") }),
            dt = shmi.requires("visuals.tools.date");
        shmi.addClass(self.element, "filter-active");
        self.vars.activeFilterLabel.textContent = shmi.localize("${alarmlist_timefilter}");
        self.vars.activeFilterFrom.textContent = dt.formatDateTime(from, { datestring: shmi.localize(self.config.dateformat) });
        self.vars.activeFilterTo.textContent = dt.formatDateTime(to, { datestring: shmi.localize(self.config.dateformat) });
        self.vars.controls.alarmList.resetScroll();
        self.vars.grid.setFilter(4, [from, to]);
    }
    /**
     * clear time filter
     *
     * @param {object} self Control-Obj
     */
    function clearFilter(self) {
        self.vars.controls.alarmList.resetScroll();
        self.vars.grid.clearFilter(4);
        shmi.removeClass(self.element, "filter-active");
    }
    /**
     * attach listener to selection of rows
     *
     * @param {object} self Control-Obj
     * @param {object} ct complex table reference
     */
    function attachSelectListener(self, ct) {
        const controls = [];

        if (self.vars.controls.confirmSelAlarmButton) {
            controls.push(self.vars.controls.confirmSelAlarmButton);
        }

        if (self.vars.controls.createThreadButton) {
            controls.push(self.vars.controls.createThreadButton);
        }

        self.vars.tokens.push(ct.listen("select", function(evt) {
            if (evt.detail.selRows.length === 1) {
                if (self.vars.detailsRight) {
                    const alarmInfoColumn = 8,
                        alarmInfo = JSON.parse(ct.getSelectionRowData(evt.detail)[0][alarmInfoColumn].value),
                        alarmDetailsIQ = shmi.requires("visuals.tools.alarms.ls.alarmDetailsIQ");

                    alarmDetailsIQ.showAlarmDetails(alarmInfo, "inline", self.vars.detailsRight, self);
                }

                controls.forEach((c) => c.unlock());
            } else {
                controls.forEach((c) => c.lock());
            }
        }));
    }
    /**
     * Sets the headline text
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined" || !self.config['show-text']) {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
        }
    }

    /**
     * Updates the options of the group filter select-box with available alarm
     * groups returned from the backend.
     *
     * @param {object} self Reference to the iq-alarm-list widget
     */
    function updateGroupFilterSelectbox(self) {
        const { controls: { filterGroupSelectBox }, groupFilter } = self.vars;

        if (!filterGroupSelectBox) {
            return;
        }

        shmi.visuals.session.ConnectSession.requestPromise("alarm.get_groups", {}).then((groups) => {
            const oldValue = filterGroupSelectBox.getValue();
            filterGroupSelectBox.setOptions(
                [
                    ...self.vars.configs.filterGroupSelectBox.options,
                    ...groups.filter((groupId) => !groupFilter || groupFilter.includes(groupId)).map((groupId) => ({
                        label: `\${alarm_group_${groupId}}`,
                        value: groupId
                    }))
                ]
            );
            filterGroupSelectBox.setValue(oldValue);
        }).catch((e) => {
            console.error(`[${className}] Unable to get list of alarm groups`, e);

            filterGroupSelectBox.setOptions(self.vars.configs.filterGroupSelectBox.options);
            filterGroupSelectBox.setValue(null);
        });
    }

    //declare private functions - END

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            tempAlarmGrid: {},
            alarmSubscription: null,
            alarmLevelTemplate: null,
            grid: null,
            dataGridId: "",
            groupFilter: null,
            cancelable: null,
            filterSet: {
                warn: false,
                error: false,
                advise: false
            },
            anchorEls: {
                iconError: null,
                filterCheckBoxError: null,
                iconWarn: null,
                filterCheckBoxWarn: null,
                iconAdvise: null,
                filterCheckBoxAdvise: null,
                filterGroupSelectBox: null,
                modeSelectBox: null,
                alarmList: null,
                confirmButtons: null,
                filterButton: null,
                filterOverlay: null
            },
            controls: {
                iconError: null,
                filterCheckBoxError: null,
                iconWarn: null,
                filterCheckBoxWarn: null,
                iconAdvise: null,
                filterCheckBoxAdvise: null,
                filterGroupSelectBox: null,
                modeSelectBox: null,
                alarmList: null,
                confirmSelAlarmButton: null,
                confirmAllAlarmsButton: null,
                createThreadButton: null
            },
            configs: {
                alarmLevelConfig: {
                    "alarm-icon": "pics/system/controls/iq-alarm-list/ico_alarm_outline.svg",
                    "warn-icon": "pics/system/controls/iq-alarm-list/ico_warning_outline.svg",
                    "notify-icon": "pics/system/controls/iq-alarm-list/ico_service_outline.svg"
                },
                alarmTableConfig: {
                    "label": "${alarmlist_history_title}",
                    "table": "iqalarms",
                    "name": "alarm-table",
                    "class-name": "complex-table2 alarms",
                    "field-datagrid-col-map": {
                        "level": 3,
                        "id": 1,
                        "description": 9,
                        "come": 4,
                        "group": 10,
                        "isAck": 8,
                        "json": 8
                    },
                    "select-mode": "SINGLE",
                    "default-field-control-map": {
                        "level": {
                            "ui-type": "iq-image-changer",
                            "config": {
                                "class-name": "iq-image-changer iq-variant-01",
                                "template": "default/iq-image-changer.iq-variant-01",
                                "options": [
                                    {
                                        "label": "label",
                                        "value": 0,
                                        "icon-src": "pics/system/controls/iq-alarm-list/ico_service_outline.svg",
                                        "icon-title": null,
                                        "icon-class": null
                                    },
                                    {
                                        "label": "label",
                                        "value": 1,
                                        "icon-src": "pics/system/controls/iq-alarm-list/ico_warning_outline.svg",
                                        "icon-title": null,
                                        "icon-class": null
                                    },
                                    {
                                        "label": "label",
                                        "value": 2,
                                        "icon-src": "pics/system/controls/iq-alarm-list/ico_alarm_outline.svg",
                                        "icon-title": null,
                                        "icon-class": null
                                    }
                                ],
                                "scaling-mode": "fit-height"
                            }
                        },
                        "id": {
                            "ui-type": "iq-label",
                            "config": {
                                "class-name": "iq-label",
                                "template": "default/iq-label.iq-variant-01",
                                "options": [],
                                "pattern": "<%= VALUE %>",
                                "value-as-tooltip": true
                            }
                        },
                        "description": {
                            "ui-type": "iq-label",
                            "config": {
                                "class-name": "iq-label",
                                "template": "default/iq-label.iq-variant-01",
                                "options": [],
                                "pattern": null,
                                "value-as-tooltip": true
                            }
                        },
                        "come": {
                            "ui-type": "iq-date-time",
                            "config": {
                                "class-name": "iq-date-time multiline",
                                "template": "default/iq-date-time.iq-variant-01",
                                "display-format": "$DD.$MM.$YYYY, $HH:$mm:$ss",
                                "value-as-tooltip": true
                            }
                        },
                        "group": {
                            "ui-type": "iq-label",
                            "config": {
                                "class-name": "iq-label",
                                "template": "default/iq-label.iq-variant-01",
                                "options": [],
                                "pattern": null,
                                "value-as-tooltip": true
                            }
                        },
                        "isAck": {
                            "ui-type": "local-script",
                            "config": {
                                "module": "visuals.tools.alarms.ls.alarmCommitButton"
                            }
                        },
                        "json": {
                            "ui-type": "local-script",
                            "config": {
                                "module": "visuals.tools.alarms.ls.alarmDetailsIQ",
                                "detail-template": null,
                                "comment-template": null,
                                "dateformat": null
                            }
                        }
                    },
                    "default-field-headers": {
                        "level": "${alarmlist_header_level}",
                        "id": "${alarmlist_header_id}",
                        "description": "${alarmlist_header_description}",
                        "come": "${alarmlist_header_come}",
                        "group": "${alarmlist_header_group}",
                        "isAck": "${alarmlist_header_isack}",
                        "json": "${alarmlist_header_json}"
                    },
                    "_comment": "expr is passed to expr parameter array of shmi.visuals.core.DataGridManager.setFilter",
                    "filters": [],
                    "default-layout": {
                        "class-name": "layout-std",
                        "_comment": "default == no additional css layout class",
                        "column-org": {
                            "col1": {
                                "fields": ["level"],
                                "column-width": "10%"
                            },
                            "col2": {
                                "fields": ["id"],
                                "column-width": "10%"
                            },
                            "col3": {
                                "fields": ["description"]
                            },
                            "col4": {
                                "fields": ["come"],
                                "column-width": "15%"
                            },
                            "col5": {
                                "fields": ["group"]
                            },
                            "col6": {
                                "fields": ["isAck"],
                                "column-width": "90px"
                            },
                            "col7": {
                                "fields": ["json"],
                                "column-width": "90px"
                            }
                        },
                        "line-height": "39px"
                    },
                    "sortable-fields": [
                        "come",
                        "id",
                        "group"
                    ],
                    "delete-selected-rows": false,
                    "show-nof-rows": true,
                    "show-buttons-table-min-width-px": 400,
                    "text-mode": "MULTILINE",
                    "default-nof-buffered-rows": 60,
                    "buffer-size": 500
                },
                iconError: {
                    'class-name': 'iq-image iq-variant-01',
                    'template': 'default/iq-image.iq-variant-01',
                    'image-src': 'pics/system/controls/iq-alarm-list/ico_alarm_outline.svg'
                },
                filterCheckBoxError: {
                    'class-name': 'iq-checkbox iq-variant-01',
                    'template': 'default/iq-checkbox.iq-variant-01',
                    'label': '${alarmlist_level_errors}'
                },
                iconWarn: {
                    'class-name': 'iq-image iq-variant-01',
                    'template': 'default/iq-image.iq-variant-01',
                    'image-src': 'pics/system/controls/iq-alarm-list/ico_warning_outline.svg'
                },
                filterCheckBoxWarn: {
                    'class-name': 'iq-checkbox iq-variant-01',
                    'template': 'default/iq-checkbox.iq-variant-01',
                    'label': '${alarmlist_level_warnings}'
                },
                iconAdvise: {
                    'class-name': 'iq-image iq-variant-01',
                    'template': 'default/iq-image.iq-variant-01',
                    'image-src': 'pics/system/controls/iq-alarm-list/ico_service_outline.svg'
                },
                filterCheckBoxAdvise: {
                    'class-name': 'iq-checkbox iq-variant-01',
                    'template': 'default/iq-checkbox.iq-variant-01',
                    'label': '${alarmlist_level_notifications}'
                },
                filterGroupSelectBox: {
                    "name": "filter-group-select-box",
                    'class-name': 'iq-select-box',
                    'show-text': false,
                    'options': [
                        {
                            'label': '${alarmlist_filter_group_all}',
                            'value': null
                        }
                    ]
                },
                modeSelectBox: {
                    'name': "mode-select-box",
                    'class-name': 'iq-select-box',
                    'show-text': false,
                    'options': [{
                        'label': '${alarmlist_mode_all}',
                        'value': 'all'
                    },
                    {
                        'label': '${alarmlist_mode_live}',
                        'value': 'live'
                    },
                    {
                        'label': '${alarmlist_mode_historic}',
                        'value': 'historic'
                    }]
                },
                alarmList: {},
                confirmSelAlarmButton: {
                    'label': '${alarmlist_commit}',
                    'name': 'confirm-sel-al-btn',
                    'class-name': 'iq-button icon-and-text',
                    'icon-src': 'pics/system/controls/iq-alarm-list/ButtonIcon_AcknSingle_default.svg'
                },
                confirmAllAlarmsButton: {
                    'label': '${alarmlist_commit_all}',
                    'name': 'confirm-all-al-btn',
                    'class-name': 'iq-button icon-and-text',
                    'icon-src': 'pics/system/controls/iq-alarm-list/ButtonIcon_AcknAll_default.svg'
                },
                createThreadButton: {
                    'label': '${alarmlist_create_thread}',
                    'name': 'create-thread-btn',
                    'class-name': 'iq-button icon-and-text',
                    'icon-src': 'pics/system/controls/iq-alarm-list/ButtonIcon_CreateThread_default.svg'
                }
            },
            listeners: [],
            tokens: [],
            editListeners: []
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            dgm: 'visuals.session.DataGridManager',
            am: 'visuals.session.AlarmManager'
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                if (!controlStartUp(self)) {
                    shmi.log('Init Failed!');
                } else {
                    shmi.log('Init Succeeded!');
                }
                /***********/
                /*** DOM ***/
                /***********/
                self.vars.labelEl = shmi.getUiElement('label', self.element);

                // Label
                setLabelImpl(self, self.config.label);
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.listeners.forEach((l) => l.enable());

                setListener(self);
                attachSelectListener(self, self.vars.controls.alarmList);

                for (const key in self.vars.controls) {
                    if (self.vars.controls[key] && self.vars.controls[key].enable) {
                        self.vars.controls[key].enable();
                    }
                }

                if (self.vars.controls.confirmSelAlarmButton) {
                    self.vars.controls.confirmSelAlarmButton.lock();
                }

                if (self.vars.controls.createThreadButton) {
                    self.vars.controls.createThreadButton.lock();
                }

                self.vars.cancelable = shmi.onReady({
                    controls: {
                        modeSelectBox: self.getName() + ".mode-select-box",
                        filterGroupSelectBox: self.getName() + ".filter-group-select-box"
                    }
                }, function(ref) {
                    ref.controls.modeSelectBox.setValue(self.config["display-mode"]);
                    ref.controls.filterGroupSelectBox.setValue(null);
                    updateGroupFilterSelectbox(self);
                });

                shmi.log("[IQ:iq-alarm-list] Enabled", 1);
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.vars.listeners.forEach((l) => l.disable());

                for (const key in self.vars.controls) {
                    if (self.vars.controls[key] && self.vars.controls[key].disable) {
                        self.vars.controls[key].disable();
                    }
                }

                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.tokens = [];

                self.vars.editListeners.forEach((l) => l.disable());
                self.vars.editListeners = [];

                if (self.vars.cancelable) {
                    self.vars.cancelable.cancel();
                    self.vars.cancelable = null;
                }

                shmi.log("[IQ:iq-alarm-list] disabled", 1);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach((l) => l.disable());

                for (const key in self.vars.controls) {
                    if (self.vars.controls[key] && self.vars.controls[key].lock) {
                        self.vars.controls[key].lock();
                    }
                }

                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-alarm-list] Locked", 1);
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach((l) => l.enable());

                for (const key in self.vars.controls) {
                    if (self.vars.controls[key] && self.vars.controls[key].unlock) {
                        self.vars.controls[key].unlock();
                    }
                }

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-alarm-list] unlocked", 1);
            },
            setLabel: function(labelText) {
                const self = this;
                setLabelImpl(self, labelText);
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

(function() {
    /**
     * module to display alarm-detail messages in alarm-table.
     */

    const MODULE_NAME = "visuals.tools.alarms.ls.alarmCommitButton",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME);

    // MODULE CODE - START

    /* private variables */

    /* private functions */

    /**
     * Implements local-script run function.
     *
     * This function will be called each time a local-script will be enabled.
     *
     * @author Matthias Weber <weber@smart-hmi.de>
     * @param {LocalScript} self instance reference of local-script control
     */
    module.run = function(self) {
        const im = shmi.requires("visuals.session.ItemManager"),
            am = shmi.requires("visuals.session.AlarmManager"),
            itemHandler = im.getItemHandler();
        let alarmInfo = null,
            ackButton = null,
            tokens = [];

        self.vars = self.vars || {};
        itemHandler.setValue = function(value) {
            alarmInfo = JSON.parse(value);
            if (ackButton) shmi.deleteControl(ackButton, true);
            if (alarmInfo.active) {
                self.element.parentElement.parentElement.parentElement.style["font-weight"] = "bold";
            } else {
                self.element.parentElement.parentElement.parentElement.style["font-weight"] = "";
            }
            if (alarmInfo.acknowledgeable) {
                if (alarmInfo.acknowledged) {
                    ackButton = shmi.createControl("iq-button", self.element.parentNode, { "icon-src": "pics/system/controls/iq-alarm-list/ButtonIcon_AcknSingle_default.svg", "label": "", "show-icon": true, "show-text": false }, "DIV");
                    shmi.addClass(ackButton.element, "alarm-ack");
                } else {
                    ackButton = shmi.createControl("iq-button", self.element.parentNode, { label: "${alarmlist_single_commit}" }, "DIV");
                    shmi.addClass(ackButton.element, "alarm-ack");
                    tokens.push(ackButton.listen("click", function() {
                        // ACK ALARM
                        if (alarmInfo.real_id) {
                            am.ackAlarm(alarmInfo.real_id);
                        }
                    }));
                }
                if (!alarmInfo.active) {
                    shmi.addClass(ackButton.element, "locked");
                }
            } else {
                ackButton = shmi.createControl("iq-label", self.element.parentNode, { "text": "---", "template": "default/iq-label.iq-variant-01" }, "DIV");
                shmi.addClass(ackButton.element, "alarm-no-ack");
            }
        };

        if (self.config.item) {
            tokens.push(im.subscribeItem(self.config.item, itemHandler));
        }
        /* called when this local-script is disabled */
        self.onDisable = function() {
            self.run = false; // from original .onDisable function of LocalScript control
            tokens.forEach(function(t) {
                t.unlisten();
            });
            tokens = [];
            if (ackButton) shmi.deleteControl(ackButton, true);
        };
    };
    // MODULE CODE - END

    fLog("module loaded");
})();

(function() {
    /**
     * module to display alarm-detail messages in alarm-table.
     */

    const MODULE_NAME = "visuals.tools.alarms.ls.alarmDetailsIQ",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        module = shmi.pkg(MODULE_NAME);

    // MODULE CODE - START

    /* private variables */

    /* private functions */

    /**
     * format time to display
     *
     * @param {Number} timestamp timestamp
     */
    function formatTime(timestamp) {
        const date = new Date(timestamp * 1000);
        if (timestamp === null) {
            return "---";
        } else {
            return date.toLocaleString();
        }
    }
    /**
     * replace zero with '---''
     *
     * @param {Number} value
     */
    function zeroOutNull(value) {
        if (value === null) {
            return "---";
        } else {
            return value;
        }
    }

    /**
     * Opens the create thread dialog box for the given alarm.
     *
     * @param {number} referenceId
     * @param {"alarm"|"alarm_type"} referenceType
     * @returns {Promise<number|null>}
     */
    function openCreateThreadDialogForAlarm(referenceId, referenceType) {
        const { create: createThread } = shmi.requires("visuals.tools.threads");

        return createThread(referenceType, referenceId, null, {
            dialogTitle: "${alarmlist_create_thread}",
            titleLabel: "${alarmlist_threads_title}",
            messageLabel: "${alarmlist_threads_message}",
            applyLabel: "${alarmlist_threads_create_btn}"
        });
    }

    /**
     * Opens the create thread dialog box for the given alarm info according to
     * the widget configuration.
     *
     * @param {*} self
     * @param {object} alarmInfo
     * @returns {Promise<number|null>}
     */
    function openCreateThreadDialogForAlarmInfo(self, alarmInfo) {
        switch (self.config["comment-mode"]) {
        case "alarm":
            return openCreateThreadDialogForAlarm(alarmInfo.id, "alarm");
        case "alarm_type":
            return openCreateThreadDialogForAlarm(alarmInfo.index, "alarm_type");
        case "disabled":
        default:
            return Promise.resolve(null);
        }
    }

    /**
     * Checks whether comments are enabled or not.
     *
     * @param {*} self
     * @returns {boolean}
     */
    function isCommentModeEnabled(self) {
        return self.config["comment-mode"] === "alarm" || self.config["comment-mode"] === "alarm_type";
    }

    /**
     * @param {*} self
     * @param {HTMLTemplateElement} contentTemplate
     * @param {object} alarmInfo
     */
    function openAlarmDetailsPopup(self, contentTemplate, alarmInfo) {
        const dialogBox = shmi.createControl("dialog-box", self.element.parentNode, {
            "name": "alarmDetailsPopup",
            "class-name": "dialog-box iq-alarm-list",
            "top-level": true,
            "title": "Alarm Details"
        }, "DIV");

        let tokens = [];
        const onDialogBoxActive = () => {
            const container = shmi.createControl("container", dialogBox.contentElement, {}, "DIV"),
                buttonContainer = shmi.createControl("container", dialogBox.contentElement, {
                    "class-name": "iq-container",
                    "type": "iqflex"
                }, "DIV"),
                createThreadButton = isCommentModeEnabled(self) ? shmi.createControl("iq-button", buttonContainer.marginCompensator, {
                    "label": "${alarmlist_create_thread}"
                }, "DIV") : null,
                closeBtn = shmi.createControl("iq-button", buttonContainer.marginCompensator, {
                    "label": "${close}"
                }, "DIV"),
                targetContainer = container.marginCompensator || container.element;

            tokens.push(
                closeBtn.listen("click", () => dialogBox.hide()),
                dialogBox.listen("close", () => {
                    tokens.forEach((token) => token.unlisten());
                    tokens = [];

                    shmi.deleteControl(dialogBox);
                })
            );

            if (createThreadButton) {
                tokens.push(createThreadButton.listen("click", async () => {
                    const newThreadId = await openCreateThreadDialogForAlarmInfo(self, alarmInfo);
                    if (newThreadId !== null) {
                        dialogBox.hide();
                        showAlarmDetails(alarmInfo, "popup", null, self);
                    }
                }));
            }

            const detailContent = contentTemplate.content.cloneNode(true);

            targetContainer.appendChild(detailContent);
            initCommentEditing(self, targetContainer, dialogBox, alarmInfo);
            dialogBox.show();
        };

        if (dialogBox.active) {
            onDialogBoxActive();
        } else {
            tokens.push(dialogBox.listen("enable", onDialogBoxActive));
        }
    }

    /**
     * Loads a HTML template from a url and returns it as template-tag.
     *
     * @param {string} url Url to the HTML template.
     * @param {?object} [placeholders]
     * @param {object} [loadOptions] Options to pass to the resource loader.
     * @returns {Promise<HTMLTemplateElement>}
     */
    async function loadHTMLTemplate(url, placeholders = null, loadOptions = {}) {
        const templateData = await shmi.loadResourcePromise(url, loadOptions),
            template = document.createElement("template");

        if (placeholders) {
            template.innerHTML = shmi.localize(shmi.evalString(templateData, placeholders));
        } else {
            template.innerHTML = shmi.localize(templateData);
        }

        return template;
    }

    /**
     * Loads comments for the given alarm using either the alarm id or alarm
     * index depending on the widget configuration.
     *
     * @param {object} self Control-Obj
     * @param {object?} alarmInfo Alarm info
     * @returns {Promise<object[]>}
     */
    async function loadCommentsForAlarm(self, alarmInfo) {
        const { list } = shmi.requires("visuals.tools.threads");

        if (!alarmInfo) {
            return Promise.resolve([]);
        }

        try {
            switch (self.config["comment-mode"]) {
            case "alarm":
                return (await list("alarm", alarmInfo.id, {
                    loadReplies: true,
                    loadUserDisplayName: true
                })).threads;
            case "alarm_type":
                return (await list("alarm_type", alarmInfo.index, {
                    loadReplies: true,
                    loadUserDisplayName: true
                })).threads;
            case "disabled":
            default:
            }
        } catch (e) {
            console.error(`[${MODULE_NAME}]`, "Failed to load comments:", e);
        }

        return [];
    }

    /**
     * initialize thread-edit element to edit threads created by current user
     *
     * @param {object} self alarmlist or local-script instance depending on alarm-detail mode
     * @param {HTMLElement} element UI element to trigger thread editing
     * @param {object} [dialog=null] dialog instance in case of "popup" alarm-detail-mode
     * @param {object} [alarmInfo=null] alarm info data
     */
    function initEditElement(self, element, dialog = null, alarmInfo = null) {
        const userId = element.getAttribute("data-user-id"),
            threadId = parseInt(element.getAttribute("data-thread-id")),
            um = shmi.requires("visuals.session.UserManager"),
            { MouseListener, TouchListener } = shmi.requires("visuals.io"),
            { currentUser } = um;
        if (!isNaN(threadId) && currentUser && userId === currentUser.id) {
            //
            const { edit } = shmi.requires("visuals.tools.threads"),
                handler = {
                    onClick: async () => {
                        const new_threadId = await edit(threadId, {
                            dialogTitle: "${alarmlist_edit_thread}",
                            titleLabel: "${alarmlist_threads_title}",
                            messageLabel: "${alarmlist_threads_message}",
                            applyLabel: "${alarmlist_threads_edit_btn}",
                            deleteLabel: "${alarmlist_threads_delete_btn}"
                        });
                        if (new_threadId !== null) {
                            if (dialog) {
                                dialog.hide();
                                showAlarmDetails(alarmInfo, "popup", null, self);
                            } else if (self.vars.detailsRight) {
                                showAlarmDetails(alarmInfo, "inline", self.vars.detailsRight, self);
                            }
                        }
                    }
                },
                ml = new MouseListener(element, handler),
                tl = new TouchListener(element, handler);

            ml.enable();
            tl.enable();
            self.vars.editListeners.push(ml, tl);
            shmi.addClass(element, "editable");
        }
    }

    /**
     * initialize thread-edit capabilities
     *
     * @param {object} self alarmlist or local-script instance depending on alarm-detail mode
     * @param {HTMLElement} detailContent parent element of alarm-detail content
     * @param {object} [dialog=null] dialog instance in case of "popup" alarm-detail-mode
     * @param {object} [alarmInfo=null] alarm info data
     */
    async function initCommentEditing(self, detailContent, dialog = null, alarmInfo = null) {
        const session = shmi.requires("visuals.session"),
            request = session.ConnectSession.requestPromise.bind(session.ConnectSession);

        let editElements = [],
            mayEdit = false;

        //workaround - test if user may edit thread
        try {
            await request("thread.modify", { thread_id: -1, title: null, message: "" });
        } catch (err) {
            if (!(err.category === "shmi:connect:api:generic" && err.errc === 4)) {
                mayEdit = true;
            }
        }

        self.vars.editListeners.forEach((l) => {
            l.disable();
        });
        self.vars.editListeners = [];

        if (mayEdit) {
            editElements = [...detailContent.querySelectorAll("[data-ui=thread-edit]")];
            editElements.forEach((e) => {
                initEditElement(self, e, dialog, alarmInfo);
            });
        }
    }

    /**
     * show the alarm details
     *
     * @param {object} alarmInfo alarm info object
     * @param {string} mode display mode for alarm details
     * @param {object} target target element
     * @param {object} self Control-Obj
     */
    async function showAlarmDetails(alarmInfo, mode, target, self) {
        const { renderThreadList } = shmi.requires("visuals.tools.threads.render");
        let templateLocation = "templates/default/iq-alarm-list/detail-win.html";

        const threadRenderOptions = {
            renderReplies: true
        };

        if (self.config["detail-template"]) {
            templateLocation = `templates/${self.config["detail-template"]}.html`;
        }

        if (self.config["comment-template"]) {
            threadRenderOptions.templateUrl = self.config["comment-template"];
        }

        function getSeverityAlarmIconSrc(severity) {
            const cfg = (self.vars && self.vars.configs ? self.vars.configs : self.config);

            switch (severity) {
            case 0:
                return cfg.iconAdvise["image-src"];
            case 1:
                return cfg.iconWarn["image-src"];
            case 2:
                return cfg.iconError["image-src"];
            default:
                return "";
            }
        }

        let alarmObj = {
            ALARM_SEVERITY: -1,
            ALARM_SEVERITY_ICON_SRC: "",
            ALARM_COME: "",
            ALARM_GONE: "",
            ALARM_ACK: "",
            ALARM_ACKBY: "",
            ALARM_INDEX: "",
            ALARM_TITLE: "",
            ALARM_URLS: "",
            ALARM_URLS_MATCHED: false,
            ALARM_TEXT: ""
        };

        if (alarmInfo) {
            const urlAttributes = Object.entries(alarmInfo.attributes).filter(([key, value]) => typeof value === "string" && value.startsWith("[URL]"));
            const alarmUrls = urlAttributes.map(([key, value]) => `${key}: <a href='${value.substr(5)}' target='_blank'>${value.substr(5)}</a><br />`).join("");

            alarmObj = {
                ALARM_SEVERITY: alarmInfo.severity,
                ALARM_SEVERITY_ICON_SRC: getSeverityAlarmIconSrc(alarmInfo.severity),
                ALARM_COME: formatTime(alarmInfo.timestamp_in),
                ALARM_GONE: formatTime(alarmInfo.timestamp_out),
                ALARM_ACK: formatTime(alarmInfo.timestamp_acknowledged),
                ALARM_ACKBY: zeroOutNull(alarmInfo.acknowledged_by),
                ALARM_INDEX: alarmInfo.index,
                ALARM_TITLE: shmi.evalString(shmi.localize("${alarm_title_" + alarmInfo.index + "}"), alarmInfo),
                ALARM_URLS: alarmUrls,
                ALARM_URLS_MATCHED: urlAttributes.length > 0,
                ALARM_TEXT: shmi.evalString(shmi.localize("${alarm_msg_" + alarmInfo.index + "}"), alarmInfo)
            };
        }

        const [template, threads] = await Promise.all([
            loadHTMLTemplate(templateLocation, alarmObj),
            loadCommentsForAlarm(self, alarmInfo)
        ]);

        const commentElement = shmi.getUiElement("comments", template.content);
        if (commentElement) {
            commentElement.append(await renderThreadList(threads, threadRenderOptions));
            if (threads.length) {
                shmi.addClass(commentElement, "has-comments");
            }
        }

        if (!alarmObj.ALARM_URLS_MATCHED) {
            const linkContainer = shmi.getElementsByAttribute("class", "link-container", template.content);
            if (linkContainer[0]) {
                linkContainer[0].style.display = "none";
            }
        }

        if (mode === "popup") {
            openAlarmDetailsPopup(self, template, alarmInfo);
        } else {
            // Clear detail window.
            while (target.lastChild) {
                target.removeChild(target.lastChild);
            }
            const detailContent = template.content.cloneNode(true);
            target.appendChild(detailContent);
            initCommentEditing(self, target, null, alarmInfo);
        }
    }

    /**
     * Implements local-script run function.
     *
     * This function will be called each time a local-script will be enabled.
     *
     * @param {LocalScript} self instance reference of local-script control
     */
    module.run = function(self) {
        const im = shmi.requires("visuals.session.ItemManager"),
            itemHandler = im.getItemHandler();
        let alarmInfo = null,
            tokens = [];

        self.vars = self.vars || {};
        self.vars.editListeners = [];

        const detailsButton = shmi.createControl("iq-button", self.element.parentNode, { label: "${alarmlist_show_detail}" }, "DIV");
        itemHandler.setValue = function(value) {
            try {
                alarmInfo = JSON.parse(value);
            } catch (e) {
                console.error("Error parsing alarmInfo data");
            }
        };

        tokens.push(detailsButton.listen("click", function() {
            showAlarmDetails(alarmInfo, "popup", null, self);
        }));

        if (self.config.item) {
            tokens.push(im.subscribeItem(self.config.item, itemHandler));
        }

        /* called when this local-script is disabled */
        self.onDisable = function() {
            self.run = false; // from original .onDisable function of LocalScript control
            tokens.forEach(function(t) {
                t.unlisten();
            });
            tokens = [];
            self.vars.editListeners.forEach((l) => {
                l.disable();
            });
            self.vars.editListeners = [];
            shmi.deleteControl(detailsButton, true);
        };
    };
    /**
     * @function
     * show the alarm details
     *
     * @param {object} alarmInfo alarm info object
     * @param {string} mode display mode for alarm details
     * @param {object} target target element
     */
    module.showAlarmDetails = showAlarmDetails;

    module.openCreateThreadDialogForAlarmInfo = openCreateThreadDialogForAlarmInfo;

    // MODULE CODE - END

    fLog("module loaded");
})();

/**
 * WIQ Rocker Button
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-button-rocker",
 *     "name": null,
 *     "template": "custom/controls/iq-button-rocker"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "min": Minimum value
 * "max": Maximum value
 * "step": Step width
 * "precision": Precision
 * "decimal-delimiter": Decimal delimiter
 * "type": Type is INT
 * "unit-text": Unit Text
 * "auto-label": Whether to use the auto-label (from item)
 * "auto-unit-text": Whether to use the auto-unit (from item)
 * "auto-min": Whether to use auto-min (from item)
 * "auto-max": Whether to use auto-max (from item)
 * "auto-step": Wheter to use auto-step (from item)
 * "auto-precision": Whether to use the auto-precision (from item)
 * "auto-type": Whether to use the auto-type (from item)
 * "numpad-enabled": Whether the numpad is enabled
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-button-rocker", // control name in camel-case
        uiType = "iq-button-rocker", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-button-rocker",
        "name": null,
        "template": "default/iq-button-rocker.iq-variant-01",
        "label": '[Label]',
        "item": null,
        "min": Number.NEGATIVE_INFINITY,
        "max": Number.POSITIVE_INFINITY,
        "step": 1,
        "precision": -1,
        "decimal-delimiter": ".",
        "type": shmi.c("TYPE_INT"),
        "unit-text": "[Unit]",
        "auto-label": true,
        "auto-unit-text": true,
        "auto-min": true,
        "auto-max": true,
        "auto-step": true,
        "auto-precision": true,
        "auto-type": true,
        "numpad-enabled": false,
        "show-icon": false,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "auto-repeat": false,
        "auto-repeat-interval": 250
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the unit text and handles toggling the `no-unit` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} unitText Unit text to set
     */
    function setUnitTextImpl(self, unitText) {
        if (!self.vars.unitEl) {
            // Nothing to do.
        } else if (unitText === "" || unitText === null || typeof unitText === "undefined") {
            self.vars.unit = "";
            self.vars.unitEl.textContent = "";
            shmi.addClass(self.element, "no-unit");
        } else {
            self.vars.unit = unitText;
            self.vars.unitEl.textContent = shmi.localize(unitText);
            shmi.removeClass(self.element, "no-unit");
        }
    }

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * getButtonStepping - get step interval for increment/decrement buttons. Defaults to 1 when stepping is unset or <=0
     *
     * @param {object} self control instance
     * @returns {number} increment/decrement stepping
     */
    function getButtonStepping(self) {
        if (self.vars.valueSettings && typeof self.vars.valueSettings.step === "number" && self.vars.valueSettings.step > 0) {
            return self.vars.valueSettings.step;
        }
        return 1;
    }

    /**
     * getValueSettings - retrieve active value-settings with fallback to configuration when not available
     *
     * @param {object} self control instance
     * @returns {object} value settings
     */
    function getValueSettings(self) {
        const config = self.getConfig();

        //remove step setting from value settings to allow input independent of button stepping
        const inputSettings = {
            vars: {
                valueSettings: self.vars.valueSettings ? shmi.cloneObject(self.vars.valueSettings) : config
            },
            config: config
        };
        inputSettings.vars.valueSettings.step = 0;

        return inputSettings;
    }

    /**
     * incValue - increment current value by specified amount
     *
     * @param {object} self control instance
     * @param {number} increment increment
     */
    function incValue(self, increment) {
        if (!self.vars.valueSettings) {
            self.imports.nv.initValueSettings(self);
        }
        self.vars.currentValueEl.blur();
        self.vars.value = self.imports.nv.applyInputSettings(Number(self.vars.value) + increment, getValueSettings(self));
        self.updateValue();
        if (typeof self.vars.value !== 'undefined') {
            self.setCurrentElementValue();
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            prevEl: null,
            nextEl: null,
            currentValueEl: null,
            labelEl: null,
            unitEl: null,
            iconEl: null,

            label: null,
            isInputTag: null,
            valueSettings: null,
            buttonInterval: 0
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.prevEl = shmi.getUiElement('previous', self.element);
                self.vars.nextEl = shmi.getUiElement('next', self.element);
                self.vars.currentValueEl = shmi.getUiElement('current', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.unitEl = shmi.getUiElement('unit', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);

                if (self.vars.currentValueEl) {
                    self.vars.isInputTag = self.vars.currentValueEl.tagName === 'INPUT';
                }

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-button-rocker] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    self.vars.iconEl.style.backgroundImage = `url(${self.config['icon-src']})`;
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    const iconClasses = self.config['icon-class'].trim().split(" ");
                    iconClasses.forEach(function(cls) {
                        shmi.addClass(self.vars.iconEl, cls);
                    });
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                // Label is optional, but prev, next and current are req'd for all variants
                if (self.vars.prevEl === null || self.vars.nextEl === null || self.vars.currentValueEl === null) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-button-rocker] One of the elements is missing (previous, next, current)", 3);
                    self.vars.prevEl = null;
                    self.vars.nextEl = null;
                    self.vars.currentValueEl = null;
                    self.vars.labelEl = null;
                    self.vars.unitEl = null;
                    self.vars.iconEl = null;
                    return;
                }

                // Unit
                setUnitTextImpl(self, self.config['unit-text']);

                // Label
                setLabelImpl(self, self.config.label);

                // Current value
                if (typeof self.vars.value !== 'undefined') {
                    self.setCurrentElementValue();
                }
                self.createInputField(self.vars.currentValueEl, self.validate.bind(self));

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                let intervalTriggered = false;

                // PREV BUTTON
                var prevFuncs = {
                    onPress: function(x, y, event) {
                        shmi.addClass(self.vars.prevEl, 'pressed');
                        if (self.config["auto-repeat"] === true) {
                            clearInterval(self.vars.buttonInterval);
                            intervalTriggered = false;
                            self.vars.buttonInterval = setInterval(() => {
                                intervalTriggered = true;
                                incValue(self, -getButtonStepping(self));
                            }, self.config["auto-repeat-interval"]);
                        }
                    },
                    onRelease: function() {
                        shmi.removeClass(self.vars.prevEl, 'pressed');
                        if (self.config["auto-repeat"] === true) {
                            clearInterval(self.vars.buttonInterval);
                        }
                    },
                    onClick: function() {
                        if (!intervalTriggered) {
                            incValue(self, -getButtonStepping(self));
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.prevEl, prevFuncs));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.prevEl, prevFuncs));

                // NEXT BUTTON
                var nextFuncs = {
                    onPress: function(x, y, event) {
                        shmi.addClass(self.vars.nextEl, 'pressed');
                        if (self.config["auto-repeat"] === true) {
                            clearInterval(self.vars.buttonInterval);
                            intervalTriggered = false;
                            self.vars.buttonInterval = setInterval(() => {
                                intervalTriggered = true;
                                incValue(self, getButtonStepping(self));
                            }, self.config["auto-repeat-interval"]);
                        }
                    },
                    onRelease: function() {
                        shmi.removeClass(self.vars.nextEl, 'pressed');
                        if (self.config["auto-repeat"] === true) {
                            clearInterval(self.vars.buttonInterval);
                        }
                    },
                    onClick: function() {
                        if (!intervalTriggered) {
                            incValue(self, getButtonStepping(self));
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.nextEl, nextFuncs));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.nextEl, nextFuncs));

                // KEYDOWN
                // "Validation light" for delimiters.
                self.vars.currentValueEl.addEventListener('keydown', function(evt) {
                    if (self.config.type === shmi.c("TYPE_FLOAT")) {
                        var wrongDelimiter = (self.config['decimal-delimiter'] === ".") ? "," : ".";
                        if (evt.key === wrongDelimiter) {
                            evt.preventDefault();
                        }
                    }
                });
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-button-rocker] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-button-rocker] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                self.vars.currentValueEl.blur();
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(this.element, 'locked');

                if (self.vars.isInputTag) {
                    self.vars.currentValueEl.disabled = true;
                }

                shmi.log("[IQ:iq-button-rocker] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                if (self.vars.isInputTag) {
                    self.vars.currentValueEl.disabled = false;
                }

                shmi.log("[IQ:iq-button-rocker] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this;
                self.vars.value = value;
                self.setCurrentElementValue();
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                var self = this;
                return self.vars.value;
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step) {
                var self = this;
                self.imports.nv.setProperties(self, arguments);
            },

            setUnitText: function(unitText) {
                var self = this;
                if (self.vars.unitEl && self.config['auto-unit-text']) {
                    setUnitTextImpl(self, unitText);
                }
            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Creates the input field for the IQ Rocker Button
             *
             * @param element - base element of input field
             * @param validateFunc - function to use for validation
             */
            createInputField: function(element, validateFunc) {
                var self = this;

                // VALIDATE: BASE ELEMENT
                if (!element) {
                    shmi.log('[IQ:iq-button-rocker] No base element provided', 3);
                    return;
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                // All required elements found
                var fieldFuncs = {
                    onClick: function() {
                        var appConfig = shmi.requires("visuals.session.config"),
                            keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

                        if (self.config["numpad-enabled"] || keyboardEnabled) {
                            var im = shmi.visuals.session.ItemManager,
                                nv = shmi.requires("visuals.tools.numericValues"),
                                vs = null,
                                params = {
                                    "decimal-delimiter": self.config["decimal-delimiter"],
                                    "unit": (self.vars.unit !== undefined) ? self.vars.unit : self.config["unit-text"],
                                    "label": (self.vars.label !== undefined) ? self.vars.label : self.config.label,
                                    "value": self.vars.value,
                                    "callback": function(val) {
                                        self.vars.value = val;
                                        self.updateValue();
                                        self.setCurrentElementValue();
                                    }
                                };

                            if (!(self.vars && self.vars.valueSettings)) {
                                nv.initValueSettings(self);
                            }

                            vs = self.vars.valueSettings;
                            params.min = vs.min;
                            params.max = vs.max;
                            params.type = vs.type;
                            params.precision = vs.precision;
                            params.item = (typeof self.config.item === "string" && self.config.item.length > 0) ? self.config.item : null;

                            if (self.config.item && im.getItem(self.config.item) && self.config['auto-type']) {
                                self.type = im.items[self.config.item].type;
                            }

                            shmi.numpad(params);
                            return;
                        }

                        // Auto-select all text on click
                        shmi.addClass(element, 'selectableText');
                        if (!self.vars.isInputTag) {
                            element.setAttribute('contenteditable', true);

                            var range = document.createRange();
                            element.focus();
                            if (element.firstChild && (element.firstChild instanceof Text)) {
                                range.setStart(element.firstChild, 0);
                                range.setEnd(element.firstChild, element.firstChild.length);
                                window.getSelection().removeAllRanges();
                                window.getSelection().addRange(range);
                            } else {
                                shmi.log("[IQ:iq-button-rocker] Element not found", 0);
                            }
                        } else {
                            self.vars.currentValueEl.setSelectionRange(0, self.vars.currentValueEl.value.length);
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(element, fieldFuncs));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(element, fieldFuncs));

                element.addEventListener('keypress', function(event) {
                    if (event.keyCode === 13) {
                        event.preventDefault();
                        window.getSelection().removeAllRanges();
                        element.blur();
                    }
                });

                element.addEventListener('blur', function() {
                    if (!self.vars.isInputTag) {
                        element.setAttribute('contenteditable', false);
                    }
                    window.getSelection().removeAllRanges();
                    shmi.removeClass(element, 'selectableText');
                    shmi.log("[IQ:iq-button-rocker] Blur event", 0);
                    validateFunc(element);
                });
            },

            /**
             * Validation
             *
             * @param element - element to validate the content of
             */
            validate: function(element) {
                var self = this,
                    exp = null,
                    val = null,
                    type = (self.config.item && self.config['auto-type']) ? self.vars.valueSettings.type : self.config.type;

                if (self.vars.isInputTag) {
                    val = self.vars.currentValueEl.value;
                } else {
                    val = self.vars.currentValueEl.textContent;
                }

                var inputString = String(val).replace(self.config['decimal-delimiter'], ".");

                if ([shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")].indexOf(type) !== -1) {
                    exp = new RegExp(self.floatRegexp);
                } else {
                    shmi.log("[IQ:iq-button-rocker] Invalid value type '" + self.config.type + "' configured", 3);
                    self.setCurrentElementValue();
                    return;
                }
                if (exp.test(inputString)) {
                    shmi.log("[IQ:iq-button-rocker] Valid value", 1);
                    self.vars.value = self.imports.nv.applyInputSettings(inputString, getValueSettings(self));
                    self.updateValue();
                } else {
                    shmi.log("[IQ:iq-button-rocker] Invalid value", 1);
                }

                self.setCurrentElementValue();
            },

            /**
             * Writes current value to connected data source
             */
            updateValue: function() {
                var self = this;
                self.fire("change", { value: self.vars.value });
                if (self.config.item) {
                    shmi.visuals.session.ItemManager.writeValue(self.config.item, self.vars.value);
                }
            },

            /**
             * Sets the element value
             */
            setCurrentElementValue: function() {
                var self = this;
                if (self.vars.isInputTag) {
                    self.vars.currentValueEl.value = self.imports.nv.formatOutput(self.vars.value, self);
                } else {
                    self.vars.currentValueEl.textContent = self.imports.nv.formatOutput(self.vars.value, self);
                }
            },
            floatRegexp: "(^[+-]?[0-9]([.][0-9]*)?$|^[+-]?[1-9]+[0-9]*([.][0-9]*)?$)"
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();
/**
 * iq-button-toggle
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-button-toggle",
 *     "name": null,
 *     "template": "default/iq-button-toggle.variant-01"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "auto-label": Whether to use the auto-label (from item)
 * "tooltip": Tooltip
 * "on-value": ON value (Item)
 * "off-value": OFF value (Item)
 * "confirm-on": Whether to confirm switching it on
 * "confirm-off": Whether to confirm switching it off
 * "on-label": Label for ON
 * "off-label": Label for OFF
 * "on-icon-src": Icon source for ON
 * "on-icon-class": Icon class for ON
 * "on-icon-title": Icon title for ON
 * "off-icon-src": Icon source for OFF
 * "off-icon-class": Icon class for OFF
 * "off-icon-title": Icon title for OFF
 * "confirm-on-text": ON confirmation question,
 * "confirm-off-text": OFF confirmation question
 * "show-icon": Whether to show an icon
 * "show-text": Whether to show text
 * "on-action": ON UI actions
 * "off-action": OFF UI actions
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-button-toggle", // control name in camel-case
        uiType = "iq-button-toggle", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-button-toggle",
        "name": null,
        "template": "default/iq-button-toggle.variant-01",
        "label": '[Label]',
        "item": null,
        "auto-label": true,
        "tooltip": null,
        "on-value": 1,
        "off-value": 0,
        "confirm-on": false,
        "confirm-off": false,
        "on-label": "ON",
        "off-label": "OFF",
        "on-show-text": true,
        "on-show-icon": false,
        "on-icon-src": null,
        "on-icon-class": null,
        "on-tooltip": null,
        "off-show-text": true,
        "off-show-icon": false,
        "off-icon-src": null,
        "off-icon-class": null,
        "off-tooltip": null,
        "confirm-on-text": "${V_CONFIRM_ON}",
        "confirm-off-text": "${V_CONFIRM_OFF}",
        "show-icon": false,
        "show-text": true,
        "on-action": [],
        "off-action": [],
        "icon-src": null,
        "icon-class": null
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined" || !self.config['show-text']) {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            labelEl: null,
            buttonEl: null,
            offStateEl: null,
            onStateEl: null,
            onLabelEl: null,
            offLabelEl: null,
            iconEl: null,
            onIconEl: null,
            offIconEl: null,

            initialized: false,
            subscriptionTargetId: null,
            onAction: null,
            offAction: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this,
                    core = shmi.requires("visuals.core");

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.buttonEl = shmi.getUiElement('button', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);

                self.vars.offStateEl = shmi.getUiElement('off-state', self.element);
                if (self.vars.offStateEl) {
                    self.vars.offLabelEl = shmi.getUiElement('label', self.vars.offStateEl);
                    self.vars.offIconEl = shmi.getUiElement('toggle-icon', self.vars.offStateEl);

                    if (!self.vars.offLabelEl || !self.config['off-label'] || !self.config['off-show-text']) {
                        shmi.addClass(self.vars.offStateEl, "no-label");
                    } else {
                        self.vars.offLabelEl.textContent = shmi.localize(self.config['off-label']);
                    }

                    if (!self.vars.offIconEl || !self.config['off-show-icon'] || !(self.config['off-icon-class'] || self.config['off-icon-src'])) {
                        shmi.addClass(self.vars.offStateEl, "no-icon");
                    } else {
                        self.setupIconElement(self.vars.offIconEl, self.config['off-icon-class'], self.config['off-icon-src'], self.config['off-show-icon'], self.config['off-show-text']);
                    }
                }

                self.vars.onStateEl = shmi.getUiElement('on-state', self.element);
                if (self.vars.onStateEl) {
                    self.vars.onLabelEl = shmi.getUiElement('label', self.vars.onStateEl);
                    self.vars.onIconEl = shmi.getUiElement('toggle-icon', self.vars.onStateEl);

                    if (!self.vars.onLabelEl || !self.config['on-label'] || !self.config['on-show-text']) {
                        shmi.addClass(self.vars.onStateEl, "no-label");
                    } else {
                        self.vars.onLabelEl.textContent = shmi.localize(self.config['on-label']);
                    }

                    if (!self.vars.onIconEl || !self.config['on-show-icon'] || !(self.config['on-icon-class'] || self.config['on-icon-src'])) {
                        shmi.addClass(self.vars.onStateEl, "no-icon");
                    } else {
                        self.setupIconElement(self.vars.onIconEl, self.config['on-icon-class'], self.config['on-icon-src'], self.config['on-show-icon'], self.config['on-show-text']);
                    }
                }

                if (!self.vars.offStateEl || !self.vars.onStateEl) {
                    shmi.log("[IQ:iq-button-toggle] At least one state element is missing", 1);
                    return;
                }

                // Label
                setLabelImpl(self, self.config.label);

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.addClass(self.element, "no-icon");
                    shmi.log('[IQ:iq-button-rocker] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    self.vars.iconEl.style.backgroundImage = `url(${self.config['icon-src']})`;
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    var iconClasses = self.config['icon-class'].trim().split(" ");
                    iconClasses.forEach(function(cls) {
                        shmi.addClass(self.vars.iconEl, cls);
                    });
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                var eventFunctions = {
                    onPress: function() {
                        shmi.log("[IQ:iq-button-toggle] Button pressed", 0);
                        shmi.addClass(self.element, 'pressed');
                    },
                    onRelease: function() {
                        shmi.log("[IQ:iq-button-toggle] Button released", 0);
                        shmi.removeClass(self.element, 'pressed');
                    },
                    onClick: function() {
                        if ((self.vars.value === self.config['on-value'])) {
                            // TURN OFF
                            if (self.config['confirm-off']) {
                                shmi.confirm(self.config['confirm-off-text'], function(conf) {
                                    if (conf) {
                                        self.toggle();
                                    }
                                });
                            } else {
                                self.toggle();
                            }
                            // TURN ON
                        } else if (self.config['confirm-on']) {
                            shmi.confirm(self.config['confirm-on-text'], function(conf) {
                                if (conf) {
                                    self.toggle();
                                }
                            });
                        } else {
                            self.toggle();
                        }
                    }
                };

                if (!self.vars.buttonEl) {
                    shmi.log('[IQ:iq-button-rocker] no button element provided', 1);
                } else {
                    self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.buttonEl, eventFunctions));
                    self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.buttonEl, eventFunctions));
                }

                // Reset on- and off-values in case they are set and only an event is set
                if (!self.config.item && self.config.event) {
                    self.config['on-value'] = 1;
                    self.config['off-value'] = 0;
                }

                // Initialize with OFF value
                self.vars.value = self.config['off-value'];

                /******************/
                /*** UI Actions ***/
                /******************/
                if (Array.isArray(self.config["on-action"]) && self.config["on-action"].length) {
                    self.vars.onAction = new core.UiAction(self.config["on-action"], self);
                }
                if (Array.isArray(self.config["off-action"]) && self.config["off-action"].length) {
                    self.vars.offAction = new core.UiAction(self.config["off-action"], self);
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-button-toggle] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-button-toggle] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-button-toggle] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-button-toggle] unlocked", 1);
            },

            /**
             * Toggles the state of the Toggle Button
             */
            toggle: function() {
                var self = this,
                    turnOn = (self.vars.value !== self.config["on-value"]);

                // Item / value
                if (self.config.item) {
                    if (turnOn) {
                        self.imports.im.writeValue(self.config.item, self.config['on-value']);
                    } else {
                        self.imports.im.writeValue(self.config.item, self.config['off-value']);
                    }
                } else if (!turnOn) {
                    self.setValue(self.config['off-value']);
                } else {
                    self.setValue(self.config['on-value']);
                }

                // UI Actions
                if (turnOn && self.vars.onAction) {
                    self.vars.onAction.execute();
                } else if (!turnOn && self.vars.offAction) {
                    self.vars.offAction.execute();
                }
            },

            /**
             * Gets the best tooltip value for the buttons current state.
             *
             * @override
             */
            getTooltip: function() {
                var self = this,
                    superTooltip = shmi.visuals.core.BaseControl.prototype.getTooltip.call(this);

                if (superTooltip) {
                    return superTooltip;
                } else if (self.vars.value === self.config['off-value']) {
                    if (shmi.objectHasOwnProperty(self.config, "off-tooltip")) {
                        return self.config['off-tooltip'];
                    }
                } else if (self.vars.value === self.config['on-value']) {
                    if (shmi.objectHasOwnProperty(self.config, "on-tooltip")) {
                        return self.config['on-tooltip'];
                    }
                }

                return null;
            },

            /**
             * Sets the current value of the Toggle Button
             *
             * @param {string|int|float} value - new value to set
             * @param {string} type
             * @param {string} name
             */
            onSetValue: function(value, type, name) {
                var self = this,
                    oldValue = self.vars.value;

                self.vars.value = parseFloat(value);

                // Show/hide elements
                if (self.vars.value === self.config['off-value']) {
                    shmi.removeClass(self.vars.buttonEl, "iq-on-state");
                    shmi.addClass(self.vars.buttonEl, "iq-off-state");
                } else {
                    shmi.removeClass(self.vars.buttonEl, "iq-off-state");
                    shmi.addClass(self.vars.buttonEl, "iq-on-state");
                }

                // Has changed -> fire events
                if (self.vars.value !== oldValue) {
                    self.setTooltip(self.getTooltip());
                    self.fire("change", {
                        value: self.vars.value
                    });
                }
            },

            /**
             * Retrieves the current value ot the Toggle Button
             *
             * @return value - current value
             */
            getValue: function() {
                return this.vars.value;
            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Sets up the icon depending on the configuration
             *
             * @param {object} iconEl
             * @param {string} iconClass
             * @param {string} iconSrc
             */
            setupIconElement: function(iconEl, iconClass, iconSrc) {
                if (iconClass) {
                    var onIconClass = iconClass.trim().split(" ");
                    onIconClass.forEach(function(cls) {
                        shmi.addClass(iconEl, cls);
                    });
                } else if (iconSrc) {
                    iconEl.style.backgroundImage = `url(${iconSrc})`;
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-button
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-button",
 *     "name": null,
 *     "template": "default/iq-button"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "min": Minimum value
 * "max": Maximum value
 * "precision": Precision
 * "decimal-delimiter": Decimal delimiter
 * "type": Type is INT
 * "auto-label": Whether to use the auto-label (from item)
 * "auto-min": Whether to use auto-min (from item)
 * "auto-max": Whether to use auto-max (from item)
 * "auto-precision": Whether to use the auto-precision (from item)
 * "auto-type": Whether to use the auto-type (from item)
 * "numpad-enabled": Whether the numpad is enabled
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 * "action-release" UI Action on button release
 * "action-while-pressed": UI Action while pressed
 * "interval-while-pressed": UI Action interval while pressed
 * "disable-alarms": Whether to prevent the default alarm styling when alarm/prewarn limits are reached. By disabling you can manually set the classes warn/preWarn via a condition on the widget.
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-button", // control name in camel-case
        uiType = "iq-button", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-button",
        "name": null,
        "template": "default/iq-button.iq-variant-01",

        "item": null,

        "label": '[Label]',
        "auto-label": true,
        "label-from-item": false,

        "icon-src": null,
        "icon-class": null,

        "action": null,
        "onClick": null,

        "tooltip": null,

        "write-bool": false,
        "on-value": 1,
        "off-value": 0,

        "action-release": null,
        "action-while-pressed": null,
        "interval-while-pressed": null,
        "action-pressed": null,

        "disable-alarms": false,

        "show-icon": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        tooltipProperties: ['tooltip'],

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            buttonEl: null,
            labelEl: null,
            iconEl: null,

            label: null,
            action: null,
            actionPress: null,
            actionRelease: null,
            actionWhilePressed: null,
            actionWhilePressedTimer: null,

            // For handling keyboard input on button (ENTER)
            keyDownListener: null,
            keyUpListener: null,
            keyDownOnMe: null,

            mouseListener: null,
            touchListener: null,
            initialized: false,
            rafId: 0,
            subscriptionTargetId: null,

            monoFlopInterval: 0,

            // Conditional Control class
            conditional: null,

            tokens: []
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: ['press', 'release', 'click'],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /***************/
            /*** ON INIT ***/
            /***************/
            onInit: function() {
                var self = this;

                self.imports.nv.initValueSettings(self);

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.buttonEl = shmi.getUiElement('button', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-button] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    self.vars.iconEl.style.backgroundImage = `url(${self.config['icon-src']})`;
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    var iconClasses = self.config['icon-class'].trim().split(" ");
                    iconClasses.forEach(shmi.addClass.bind(shmi, self.vars.iconEl));
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                // Label and icon are optional, but button element is required
                if (!self.vars.buttonEl) {
                    shmi.log("[IQ:iq-button] template is missing button element", 3);
                    self.vars.buttonEl = null;
                    self.vars.labelEl = null;
                    self.vars.iconEl = null;
                    return;
                }

                // Label
                setLabelImpl(self, self.config.label);

                // UI Actions
                if (self.config.action) {
                    self.vars.action = new shmi.visuals.core.UiAction(self.config.action, self);
                }
                if (self.config['action-press']) {
                    self.vars.actionPress = new shmi.visuals.core.UiAction(self.config['action-press'], self);
                }
                if (self.config['action-release']) {
                    self.vars.actionRelease = new shmi.visuals.core.UiAction(self.config['action-release'], self);
                }
                if (self.config['action-while-pressed']) {
                    self.vars.actionWhilePressed = new shmi.visuals.core.UiAction(self.config['action-while-pressed'], self);
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                // PREV BUTTON
                var session = shmi.visuals.session;
                var btnFuncs = {
                    onClick: function(x, y, e) {
                        if (session.FocusElement !== null) {
                            session.FocusElement.blur();
                            session.FocusElement = null;
                        }
                        if (self.vars.buttonEl instanceof HTMLElement) {
                            self.vars.buttonEl.focus();
                            session.FocusElement = self.vars.buttonEl;
                            shmi.log("[IQ:iq-button] focused", 1);
                        } else {
                            shmi.log("[IQ:iq-button] only HTMLElements may be focused, type: " + (self.vars.buttonEl.constructor), 1);
                        }

                        self.fire('click', {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.vars.action) {
                            self.vars.action.execute();
                        }

                        if (self.onClick) {
                            self.onClick(self);
                        }
                    },
                    onRelease: function(x, y, e) {
                        shmi.removeClass(self.vars.buttonEl, 'pressed');
                        self.fire('release', {
                            x: x,
                            y: y,
                            event: e
                        });
                        if (self.config['write-bool'] && self.config.item) {
                            self.writeValue(self.config["off-value"]);
                        }

                        // Stop any running timer on release
                        if (self.vars.actionWhilePressedTimer) {
                            clearInterval(self.vars.actionWhilePressedTimer);
                        }

                        if (self.vars.actionRelease) {
                            self.vars.actionRelease.execute();
                        }
                    },
                    onPress: function(x, y, e) {
                        shmi.addClass(self.vars.buttonEl, 'pressed');
                        self.fire('press', {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.config['write-bool'] && self.config.item) {
                            self.writeValue(self.config["on-value"]);
                        }

                        // UI Action: onPress
                        if (self.vars.actionPress) {
                            self.vars.actionPress.execute();
                        }

                        // UI Action while pressed
                        if (self.vars.actionWhilePressed && self.config['interval-while-pressed']) {
                            if (self.vars.actionWhilePressedTimer) {
                                clearInterval(self.vars.actionWhilePressedTimer);
                            }

                            // While pressed timer
                            self.vars.actionWhilePressedTimer = setInterval(function() {
                                self.vars.actionWhilePressed.execute();
                            }, self.config['interval-while-pressed']);
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.buttonEl, btnFuncs));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.buttonEl, btnFuncs));

                // Key-up listener
                self.vars.keyUpListener = function(e) {
                    var valid = self.vars.keyDownOnMe;
                    if (valid && (e.code === 'Enter' || e.code === 'NumpadEnter') && self.element === document.activeElement) {
                        shmi.removeClass(self.element, 'pressed');
                        var rect = self.element.getBoundingClientRect();
                        btnFuncs.onClick(rect.left + (rect.width / 2), rect.top + (rect.height / 2), e);
                        self.vars.keyDownOnMe = false;
                    }
                };

                // Key-down listener
                self.vars.keyDownListener = function(e) {
                    if ((e.code === 'Enter' || e.code === 'NumpadEnter') && self.element === document.activeElement) {
                        shmi.addClass(self.element, 'pressed');
                        self.vars.keyDownOnMe = true;
                    }
                };
            },

            /*****************/
            /*** ON ENABLE ***/
            /*****************/
            onEnable: function() {
                var self = this;

                // Conditional
                if (self.vars.conditional !== null) {
                    if (self.vars.conditional.item !== self.config.item) {
                        self.vars.conditional.item = self.config.item;
                    }
                } else if (self.config.item && !self.config['disable-alarms']) {
                    self.vars.conditional = shmi.createConditional(self.element, self.config.item);
                }

                if (self.vars.conditional) {
                    self.vars.conditional.enable();
                }

                self.element.setAttribute('tabindex', '0');

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                // Has Item
                if (self.config.item) {
                    // Item Lock
                    if (self.config["disable-item-lock"]) {
                        var h = self.imports.im.getItemHandler();
                        h.setValue = function(value) {
                            self.setValue(value);
                        };
                        self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, h);
                    } else {
                        self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                    }

                    // Monoflop
                    if (self.config.monoflop) {
                        var pTok = self.listen("press", function() {
                            clearInterval(self.vars.monoFlopInterval);
                            self.vars.monoFlopInterval = setInterval(function() {
                                self.imports.im.writeValue(self.config.item, self.config['monoflop-value'], { skipSameValueCheck: true });
                            }, self.config['monoflop-interval']);
                        });
                        var rTok = self.listen("release", function() {
                            clearInterval(self.vars.monoFlopInterval);
                        });
                        self.vars.tokens.push(pTok, rTok);
                    }
                }

                // Key listeners
                self.element.addEventListener('keyup', self.vars.keyUpListener, false);
                self.element.addEventListener('keydown', self.vars.keyDownListener, false);

                shmi.log("[IQ:iq-button] Enabled", 1);
            },

            /******************/
            /*** ON DISABLE ***/
            /******************/
            onDisable: function() {
                var self = this;

                // Conditional
                if (self.vars.conditional) {
                    self.vars.conditional.disable();
                }

                // Unsubscribe Item
                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                // Restore tabindex
                self.element.removeAttribute('tabindex');

                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];

                // Disable listeners
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.element.removeEventListener('keyup', self.vars.keyUpListener);
                self.element.removeEventListener('keydown', self.vars.keyDownListener);

                // Disable timer
                if (self.vars.actionWhilePressedTimer) {
                    clearInterval(self.vars.actionWhilePressedTimer);
                    self.vars.actionWhilePressedTimer = null;
                }

                shmi.log("[IQ:iq-button] disabled", 1);
            },

            /***************/
            /*** ON LOCK ***/
            /***************/
            onLock: function() {
                var self = this;

                // Disable listeners
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.vars.buttonEl) {
                    self.vars.buttonEl.removeAttribute('tabindex');
                    self.vars.buttonEl.removeEventListener('keyup', self.vars.keyUpListener);
                    self.vars.buttonEl.removeEventListener('keydown', self.vars.keyDownListener);
                    self.vars.buttonEl.blur();
                }

                // CSS
                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-button] Locked", 1);
            },

            /*****************/
            /*** ON UNLOCK ***/
            /*****************/
            onUnlock: function() {
                var self = this;

                // Enable listeners
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                // CSS
                shmi.removeClass(self.element, 'locked');

                if (self.vars.buttonEl) {
                    self.vars.buttonEl.addEventListener('keyup', self.vars.keyUpListener, false);
                    self.vars.buttonEl.addEventListener('keydown', self.vars.keyDownListener, false);
                    self.vars.buttonEl.setAttribute('tabindex', 0);
                }

                shmi.log("[IQ:iq-button] unlocked", 1);
            },

            /*******************/
            /** ON SET VALUE ***/
            /*******************/
            onSetValue: function(value, type, name) {
                var self = this;
                self.vars.value = value;
                if (self.config['label-from-item'] === true) {
                    shmi.caf(self.rafId);
                    self.rafId = shmi.raf(function() {
                        setLabelImpl(self, shmi.localize(self.imports.nv.formatOutput(value, self)));
                    });
                }
            },

            /*****************/
            /*** GET VALUE ***/
            /*****************/
            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                var self = this;
                return self.vars.value;
            },

            /*************************/
            /*** ON SET PROPERTIES ***/
            /*************************/
            onSetProperties: function(min, max, step) {
                var self = this;
                self.imports.nv.setProperties(self, arguments);
            },

            /********************/
            /*** ON SET LABEL ***/
            /********************/
            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Writes 1/0 to connected data source
             */
            writeValue: function(value) {
                var self = this;
                if (self.config.item) {
                    self.imports.im.writeValue(self.config.item, value, { skipSameValueCheck: true });
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-checkbox
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-checkbox",
 *     "name": null,
 *     "template": "default/iq-checkbox"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "type": Type is INT
 * "auto-label": Whether to use the auto-label (from item)
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-checkbox", // control name in camel-case
        uiType = "iq-checkbox", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-checkbox",
        "name": null,
        "template": "default/iq-checkbox.variant-01",
        "label": '[Label]',
        "item": null,
        "type": shmi.c("TYPE_INT"),
        "auto-label": true,
        "show-icon": false,
        "show-text": true,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "on-value": 1,
        "off-value": 0,
        "pressed-class": "pressed",
        "confirm-off-text": "${V_CONFIRM_OFF}",
        "confirm-on-text": "${V_CONFIRM_ON}",
        "confirm-on": false,
        "confirm-off": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            checkboxEl: null,
            labelEl: null,
            iconEl: null,
            containerEl: null,

            label: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: ['change'],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.checkboxEl = shmi.getUiElement('checkbox', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);
                self.vars.containerEl = shmi.getUiElement('iq-checkbox-container', self.element);

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-checkbox] no button-icon element provided', 1);
                    shmi.addClass(self.element, "no-icon");
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    self.vars.iconEl.style.backgroundImage = `url(${self.config['icon-src']})`;
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    shmi.addClass(self.vars.iconEl, self.config['icon-class']);
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                self.vars.value = self.config["off-value"];

                // Label is optional, but prev, next and current are req'd for all variants
                if (self.vars.checkboxEl === null) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-checkbox] Checkbox element is missing", 3);
                    self.vars.checkboxEl = null;
                    self.vars.labelEl = null;
                    self.vars.iconEl = null;
                    return;
                }

                // Label
                if (self.config["show-text"]) {
                    setLabelImpl(self, self.config.label);
                } else {
                    setLabelImpl(self, null);
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                if (self.vars.containerEl) {
                    var fieldFuncs = {
                        onPress: function(x, y, event) {
                            shmi.addClass(self.element, self.config["pressed-class"]);
                        },
                        onRelease: function() {
                            shmi.removeClass(self.element, self.config["pressed-class"]);
                        },
                        onClick: function(x, y, e) {
                            e.preventDefault();
                            if (self.vars.value === self.config['on-value']) {
                                // IS ON -> OFF
                                if (self.config['confirm-off']) {
                                    shmi.confirm(self.config['confirm-off-text'], function onConfirmed(confirmed) {
                                        if (confirmed) {
                                            self.updateValue(self.config['off-value']);
                                        }
                                    });
                                } else {
                                    self.updateValue(self.config['off-value']);
                                }
                            } else if (self.config['confirm-on']) {
                                // IS OFF -> ON
                                shmi.confirm(self.config['confirm-on-text'], function onConfirmed(confirmed) {
                                    if (confirmed) {
                                        self.updateValue(self.config['on-value']);
                                    }
                                });
                            } else {
                                self.updateValue(self.config['on-value']);
                            }
                        }
                    };
                    self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.containerEl, fieldFuncs));
                    self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.containerEl, fieldFuncs));
                } else {
                    console.error("[IQ:iq-checkbox] 'iq-checkbox-container' element is missing, cannot attach listener.");
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-checkbox] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-checkbox] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                self.vars.checkboxEl.blur();
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(this.element, 'locked');
                self.vars.checkboxEl.disabled = true;

                shmi.log("[IQ:iq-checkbox] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');
                self.vars.checkboxEl.disabled = false;

                shmi.log("[IQ:iq-checkbox] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this,
                    changed = false;

                if (value === self.config['on-value']) {
                    self.vars.value = self.config['on-value'];
                    changed = true;
                    shmi.addClass(self.element, 'checked');
                } else {
                    self.vars.value = self.config['off-value'];
                    changed = true;
                    shmi.removeClass(self.element, 'checked');
                }
                if (changed) {
                    self.fire("change", {
                        value: self.vars.value
                    });
                }
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                return this.vars.value;
            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label'] && self.config['show-text']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Writes current value to connected data source
             */
            updateValue: function(value) {
                var self = this;
                if (self.config.item) {
                    self.imports.im.writeValue(self.config.item, value);
                } else {
                    self.setValue(value);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();
/**
 * iq-date-time
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-date-time",
 *     "name": null,
 *     "template": "custom/controls/iq-date-time"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * display-utc {boolean}: Whether or not the displayed time should be UTC or not
 * display-format {string}: Format string for the displayed date
 * input-format {string}: Format string to use when parsing the items value
 * invalid-text {string}: String to display if the item does not contain a valid timestamp.
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iqDateTime", // control name in camel-case
        uiType = "iq-date-time", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-date-time",
        "name": null,
        "template": "default/iq-date-time.variant-01",
        "item": null,
        "display-utc": false,
        "display-format": "$YYYY-$MM-$DD $HH:$mm:$ss",
        "input-format": "$X",
        "invalid-text": "${date-time.invalid-time}",
        "tooltip": null,
        "value-as-tooltip": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            domEl: null,

            subscriptionTargetId: null,
            localizedDisplayFormat: null,
            localizedInvalidText: null,
            rafRunning: false,
            lastValue: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            dt: "visuals.tools.date"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.domEl = shmi.getUiElement('datetime', self.element);
                if (!self.vars.domEl) {
                    shmi.log("[IQ:iq-date-time] Element is missing", 3);
                    return;
                }

                self.vars.localizedDisplayFormat = shmi.localize(self.config["display-format"]);
                self.vars.localizedInvalidText = shmi.localize(self.config["invalid-text"]);

                self.vars.domEl.textContent = self.vars.localizedInvalidText;
                if (self.config["value-as-tooltip"]) {
                    self.setTooltip(self.vars.localizedInvalidText);
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-date-time] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                if (self.vars.subscriptionTargetId) {
                    self.vars.subscriptionTargetId.unlisten();
                    self.vars.subscriptionTargetId = null;
                }

                shmi.log("[IQ:iq-date-time] disabled", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type) {
                var self = this;

                if (type === shmi.c("TYPE_INT") || type === shmi.c("TYPE_FLOAT")) {
                    self.vars.lastValue = self.imports.dt.parseDateTime(String(value), self.config["input-format"] || "$X");
                } else if (!self.config["input-format"]) {
                    self.vars.lastValue = null;
                } else {
                    self.vars.lastValue = self.imports.dt.parseDateTime(String(value), self.config["input-format"]);
                }

                // Only attempt to update the DOM during an animation frame.
                // Prevents floods of redraws and may perform better anyway
                if (!self.vars.rafRunning) {
                    self.vars.rafRunning = true;
                    shmi.raf(function onDraw() {
                        let localizedText = null;
                        if (!self.vars.lastValue) {
                            localizedText = self.vars.localizedInvalidText;
                        } else {
                            localizedText = self.imports.dt.formatDateTime(self.vars.lastValue, {
                                datestring: self.vars.localizedDisplayFormat,
                                utc: self.config["display-utc"]
                            });
                        }

                        self.vars.domEl.textContent = localizedText;
                        self.vars.domEl.innerHTML = self.vars.domEl.innerHTML.replace(/\n/g, "<br>");
                        if (self.config["value-as-tooltip"]) {
                            self.setTooltip(localizedText.replace(/(\\n)+/g, " "));
                        }

                        self.vars.rafRunning = false;
                    });
                }
            },
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-duration-display
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-duration-display",
 *     "name": null,
 *     "template": "custom/controls/iq-duration-display"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "target-ts-item": The target time item
 * "target-ts-format": The date format of the target time item
 * "current-ts-item": The current time item
 * "current-ts-format": The date format of the current time item
 * "display-preset": Different display formats
 * "invalid-text": Text shown when an invalid time occurs
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-duration-display", // control name in camel-case
        uiType = "iq-duration-display", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-duration-display",
        "name": null,
        "template": "default/iq-duration-display.variant-01",
        "label": '[Label]',
        "target-ts-item": null,
        "target-ts-format": "$X",
        "current-ts-item": "Systemzeit",
        "current-ts-format": "$X",
        "display-preset": "compact",
        "invalid-text": "${duration-display.invalid-duration}",
        "tooltip": null,
        "value-as-tooltip": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the displayed text and tooltip of the control.
     *
     * @param {*} self Reference to the control.
     * @param {string} str Text to display.
     */
    function setText(self, str) {
        if (self.vars.durationEl) {
            self.vars.durationEl.textContent = str;
            if (self.config["value-as-tooltip"]) {
                self.setTooltip(str);
            }
        }
    }

    function doUpdate(self) {
        if (self.vars.tsTarget === null || self.vars.tsCurrent === null) {
            self.vars.lastValue = null;
        } else {
            self.vars.lastValue = self.vars.tsTarget - self.vars.tsCurrent;
        }

        if (!self.vars.rafRunning) {
            self.vars.rafRunning = true;
            shmi.raf(function onDraw() {
                if (!self.vars.lastValue) {
                    setText(self, self.vars.localizedInvalidText);
                } else {
                    setText(self, self.imports.dt.formatDuration(self.vars.lastValue, self.config["display-preset"] || "compact"));
                }

                self.vars.rafRunning = false;
            });
        }
    }

    function makeItemHandler(self, configFormatName, destVarName) {
        return {
            setValue: function setValue(value, type) {
                if (type === shmi.c("TYPE_INT") || type === shmi.c("TYPE_FLOAT")) {
                    self.vars[destVarName] = self.imports.dt.parseDateTime(String(value), self.config[configFormatName] || "$X");
                } else if (!self.config[configFormatName]) {
                    self.vars[destVarName] = null;
                } else {
                    self.vars[destVarName] = self.imports.dt.parseDateTime(String(value), self.config[configFormatName]);
                }

                doUpdate(self);
            }
        };
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            tsTarget: null,
            tsCurrent: null,
            localizedInvalidText: null,
            rafRunning: false,
            lastValue: null,

            // DOM elements
            durationEl: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            dt: "visuals.tools.date"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                self.vars.durationEl = shmi.getUiElement('duration', self.element);
                if (!self.vars.durationEl) {
                    shmi.log("[IQ:iq-duration] Element is missing", 3);
                    return;
                }

                self.vars.localizedInvalidText = shmi.localize(self.config["invalid-text"]);
                setText(self, self.vars.localizedInvalidText);
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;
                if (self.config["target-ts-item"] && self.config["current-ts-item"] && self.vars.listeners.length === 0) {
                    self.vars.listeners.push(self.imports.im.subscribeItem(self.config["target-ts-item"], makeItemHandler(self, "target-ts-format", "tsTarget")));
                    self.vars.listeners.push(self.imports.im.subscribeItem(self.config["current-ts-item"], makeItemHandler(self, "current-ts-format", "tsCurrent")));
                }
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;
                self.vars.listeners.forEach(function(sub) {
                    sub.unlisten();
                });
                self.vars.listeners = [];
            },
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();
/**
 * iq-flip-switch
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-flip-switch",
 *     "name": null,
 *     "template": "default/iq-flip-switch"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 * "on-value": The ON value
 * "off-value": The OFF value
 * "transitionStyle": Legacy
 * "confirm-off-text": OFF confirmation text
 * "confirm-on-text": ON confirmation text
 * "confirm-on": Whether to confirm ON
 * "confirm-off": Whether to confirm OFF
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-flip-switch", // control name in camel-case
        uiType = "iq-flip-switch", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-flip-switch",
        "name": null,
        "template": "default/iq-flip-switch.variant-01",
        "label": '[Label]',
        "auto-label": true,
        "on-label": "${flip-switch.label-on}",
        "off-label": "${flip-switch.label-off}",
        "item": null,
        "show-icon": false,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "on-value": 1,
        "off-value": 0,
        "transitionStyle": "all .16s linear",
        "confirm-off-text": "${V_CONFIRM_OFF}",
        "confirm-on-text": "${V_CONFIRM_ON}",
        "confirm-on": false,
        "confirm-off": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            shmi.addClass(self.element, "no-label");
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * Updates the handle position based on the widgets current value.
     *
     * @param {*} self Reference to the widget
     */
    function updateHandle(self) {
        const value = self.getValue(),
            handleWidth = self.vars.handleEl ? Math.ceil(self.vars.handleEl.getBoundingClientRect().width) : 0,
            boxWidth = self.vars.handleBoxEl ? self.vars.handleBoxEl.clientWidth : 0;

        if (self.vars.isDragging || !self.vars.handleBoxEl) {
            return;
        }

        if (value === self.config['on-value']) {
            self.vars.handleDragOffset = (boxWidth - handleWidth);
        } else {
            self.vars.handleDragOffset = 0;
        }
    }

    /**
     * Creates a listener for the `window.resize` event.
     *
     * @param {*} self Reference to the widget
     * @param {function} handlerFunc Event handler
     * @returns {object}
     */
    function makeResizeListener(self, handlerFunc) {
        return {
            disable: () => window.removeEventListener("resize", handlerFunc),
            enable: () => window.addEventListener("resize", handlerFunc)
        };
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            iconEl: null,
            labelEl: null,
            labelOnEl: null,
            labelOffEl: null,
            handleEl: null,
            handleBoxEl: null,

            initialized: false,
            subscriptionTargetId: null,
            isDragging: false,
            width: null,
            handleWidth: null,
            handleDragOffset: 0
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.iconEl = shmi.getUiElement('icon', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.labelOnEl = shmi.getUiElement('label-on', self.element);
                self.vars.labelOffEl = shmi.getUiElement('label-off', self.element);
                self.vars.handleEl = shmi.getUiElement('handle', self.element);
                self.vars.handleBoxEl = shmi.getUiElement('handle-box', self.element);

                // Validate req'd elements
                if (!self.vars.handleEl || !self.vars.handleBoxEl) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-flip-switch] Handle or handle box element is missing!", 3);
                    self.vars.labelEl = null;
                    self.vars.labelOnEl = null;
                    self.vars.labelOffEl = null;
                    self.vars.handleEl = null;
                    self.vars.handleBoxEl = null;
                    self.vars.iconEl = null;
                    return;
                }

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.addClass(self.element, "no-icon");
                    shmi.log('[IQ:iq-flip-switch] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    self.vars.iconEl.style.backgroundImage = `url(${self.config['icon-src']})`;
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    var iconClasses = self.config['icon-class'].trim().split(" ");
                    iconClasses.forEach(function(cls) {
                        shmi.addClass(self.vars.iconEl, cls);
                    });
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                /**************/
                /*** LABELS ***/
                /**************/
                setLabelImpl(self, self.config.label);

                if (self.vars.labelOnEl) {
                    if (self.config['on-label']) {
                        self.vars.labelOnEl.textContent = shmi.localize(self.config['on-label']);
                    }
                }

                if (self.vars.labelOffEl) {
                    if (self.config['on-label']) {
                        self.vars.labelOffEl.textContent = shmi.localize(self.config['off-label']);
                    }
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                let handleDragAfId = null;
                // Handle element listeners
                var handleEventFunctions = {
                    onDrag: function(dx, dy, e) {
                        const handleWidth = self.vars.handleEl ? Math.ceil(self.vars.handleEl.getBoundingClientRect().width) : 0,
                            boxWidth = self.vars.handleBoxEl ? self.vars.handleBoxEl.clientWidth : 0;

                        e.preventDefault();
                        if (self.vars.handleDragOffset + dx < 0) {
                            self.vars.handleDragOffset = 0;
                        } else if (self.vars.handleDragOffset + dx + handleWidth > boxWidth) {
                            self.vars.handleDragOffset = boxWidth - handleWidth;
                        } else {
                            self.vars.handleDragOffset += dx;
                        }

                        if (handleDragAfId) {
                            shmi.caf(handleDragAfId);
                        }

                        handleDragAfId = shmi.raf(() => {
                            self.vars.handleEl.style.transform = `translate3d(${self.vars.handleDragOffset}px, 0, 0)`;
                        });

                        if (!self.vars.isDragging) {
                            self.vars.isDragging = true;
                        }
                    },
                    onPress: function() {
                        shmi.addClass(self.vars.handleEl, 'pressed');
                    },
                    onRelease: function() {
                        if (self.vars.isDragging) {
                            self.vars.isDragging = false;
                            if (self.vars.handleDragOffset > ((self.vars.handleBoxEl.offsetWidth - self.vars.handleEl.offsetWidth) / 2)) {
                                if (self.config['confirm-on']) {
                                    shmi.confirm(self.config['confirm-on-text'], function(confirmed) {
                                        self.handleOnOff(true, confirmed);
                                    });
                                } else {
                                    self.handleOnOff(true, null);
                                }
                            } else if (self.config['confirm-off']) {
                                shmi.confirm(self.config['confirm-off-text'], function(confirmed) {
                                    self.handleOnOff(false, confirmed);
                                });
                            } else {
                                self.handleOnOff(false, null);
                            }
                        }

                        if (handleDragAfId) {
                            shmi.caf(handleDragAfId);
                        }
                        self.vars.handleEl.style.transform = '';
                        shmi.removeClass(self.vars.handleEl, 'pressed');
                    },
                    onClick: function() {
                        if (self.vars.value === self.config['on-value']) {
                            if (self.config['confirm-off']) {
                                shmi.confirm(self.config['confirm-off-text'], function(conf) {
                                    if (conf) {
                                        self.turnOnOff(false);
                                    }
                                });
                            } else {
                                self.turnOnOff(false);
                            }
                        } else if (self.config['confirm-on']) {
                            shmi.confirm(self.config['confirm-on-text'], function(conf) {
                                if (conf) {
                                    self.turnOnOff(true);
                                }
                            });
                        } else {
                            self.turnOnOff(true);
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.handleEl, handleEventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.handleEl, handleEventFunctions));

                // Handle box listeners
                var handleBoxEventFunctions = {
                    onClick: handleEventFunctions.onClick
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.handleBoxEl, handleBoxEventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.handleBoxEl, handleBoxEventFunctions));

                self.vars.listeners.push(makeResizeListener(self, updateHandle.bind(null, self)));
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.log("[IQ:iq-flip-switch] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                if (self.vars.subscriptionTargetId) {
                    self.vars.subscriptionTargetId.unlisten();
                    self.vars.subscriptionTargetId = null;
                }

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.log("[IQ:iq-flip-switch] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-flip-switch] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-flip-switch] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this,
                    oldValue = self.vars.value;

                if (self.vars.isDragging || !self.vars.handleBoxEl) {
                    return;
                }

                if (value === self.config['on-value']) {
                    self.vars.value = self.config['on-value'];
                    shmi.addClass(self.element, 'on');
                } else {
                    self.vars.value = self.config['off-value'];
                    shmi.removeClass(self.element, 'on');
                }

                if (oldValue !== self.vars.value) {
                    self.fire("change", {
                        value: self.vars.value
                    });

                    if (self.onChange) {
                        self.onChange(self.vars.value);
                    }
                }

                updateHandle(self);
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                return this.vars.value;
            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            },

            /* Handles ON/OFF dragging */
            handleOnOff: function(turnOn, confirmed) {
                var self = this,
                    newValue = turnOn ? self.config['on-value'] : self.config['off-value'];

                // Only do this when it has been confirmed or no confirmation was enabled
                if (confirmed === true || confirmed === null) {
                    updateHandle(self);

                    if (self.config.item) {
                        shmi.visuals.session.ItemManager.writeValue(self.config.item, newValue);
                    } else {
                        self.setValue(newValue);
                    }
                } else {
                    // Reset to previous value as not confirmed
                    self.setValue(self.vars.value);
                }
            },

            /* Handles ON/OFF click */
            turnOnOff: function(turnOn) {
                var self = this,
                    newValue = turnOn ? self.config['on-value'] : self.config['off-value'];

                if (self.config.item) {
                    self.imports.im.writeValue(self.config.item, newValue);
                } else {
                    self.setValue(newValue);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-heartbeat
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-heartbeat",
 *     "name": null,
 *     "template": "custom/controls/iq-heartbeat"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "item": The item
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iqHeartbeat", // control name in camel-case
        uiType = "iq-heartbeat", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-heartbeat",
        "name": null,
        "template": "default/iq-heartbeat.variant-01",
        "item": null,
        "item-plc": null,
        "timeout-plc": null,
        "image-ok-src": null,
        "image-ok-title": null,
        "image-fail-src": null,
        "image-fail-title": null,
        "action-fail": null,
        "action-ok": null,
        "item-hmi": null,
        "timeout-hmi": null,
        "scaling-mode": "fit-height"
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Handles the set scaling mode
     *
     * @param {object} self
     */
    function applyScalingMode(self) {
        let scalingMode = "fit-height";

        switch (self.config["scaling-mode"]) {
        case "fit-height":
        case "fit-width":
        case "stretch":
        case "contain":
            scalingMode = self.config["scaling-mode"];
            break;
        default:
        }

        shmi.addClass(self.element, scalingMode);
    }

    /**
     * Tries as long as required until the update is successful
     *
     * @param self
     * @param oldVal
     * @param newVal
     * @param callback
     */
    function performCompareExchange(self, oldVal, newVal, callback) {
        if (self.vars.isEnabled) {
            self.imports.im.compareExchange(self.config['item-hmi'], oldVal, newVal, function(expectedValue, retry, error) {
                if (error && retry) {
                    newVal = expectedValue + 1;
                    if (newVal > Number.MAX_SAFE_INTEGER) {
                        newVal = 0;
                    }

                    performCompareExchange(self, expectedValue, newVal, callback);
                } else if (typeof callback === 'function') {
                    callback(!error);
                }
            });
        }
    }

    /**
     * called if plc item timer elapses
     *
     * @param {object} self control instance
     */
    function onPlcTimerElapsed(self) {
        self.vars.isDisconnected = true;
        self.vars.imgEl.src = self.vars.imagePathFail;
        self.setTooltip(self.config['image-fail-title']);
        if (self.vars.actionFail) {
            self.vars.actionFail.execute();
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            active: false,

            // DOM Elements
            imgEl: null,

            tokens: [],
            plcItemValue: null,
            plcItemTimer: null,
            hmiItemValue: null,
            hmiItemTimer: null,
            hmiItemTimerActive: false,
            isDisconnected: false,
            actionOk: null,
            actionFail: null,

            // Images
            imagePathOk: null,
            imagePathFail: null,

            isEnabled: false
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.imgEl = shmi.getUiElement('image', self.element);
                if (!self.vars.imgEl) {
                    self.log('Image element is missing!', 3);
                    return;
                }

                // Images from layout template - allows us to use different images per layout
                if (self.vars.imgEl.dataset.imgOk && self.vars.imgEl.dataset.imgFail) {
                    self.vars.imagePathOk = self.vars.imgEl.dataset.imgOk;
                    self.vars.imagePathFail = self.vars.imgEl.dataset.imgFail;
                }

                // Override images if defined by user
                if (self.config['image-ok-src']) {
                    self.vars.imagePathOk = self.config['image-ok-src'];
                }
                if (self.config['image-fail-src']) {
                    self.vars.imagePathFail = self.config['image-fail-src'];
                    self.setTooltip(self.config['image-fail-title']);
                }

                self.vars.imgEl.src = self.vars.imagePathFail; // Initial status

                // UI Actions
                if (self.config['action-fail']) {
                    self.vars.actionFail = new shmi.visuals.core.UiAction(self.config['action-fail'], self);
                }
                if (self.config['action-ok']) {
                    self.vars.actionOk = new shmi.visuals.core.UiAction(self.config['action-ok'], self);
                }

                applyScalingMode(self);
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.vars.isEnabled = true;

                /**************************/
                /*** PLC ITEM (MONITOR) ***/
                /**************************/
                if (self.config['item-plc']) {
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config["item-plc"], self));

                    // Start timer
                    self.vars.plcItemTimer = setTimeout(onPlcTimerElapsed.bind(null, self), self.config['timeout-plc']);
                }

                /************************/
                /*** HMI ITEM (WRITE) ***/
                /************************/
                if (self.config['item-hmi'] && self.config['timeout-hmi']) {
                    let hmiHandler = self.imports.im.getItemHandler();
                    hmiHandler.setValue = function(value) {
                        self.vars.hmiItemValue = parseInt(value);
                    };
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config["item-hmi"], hmiHandler));

                    // Start timer for HMI item update
                    self.vars.hmiItemTimer = setInterval(function() {
                        if (self.vars.hmiItemTimerActive) {
                            return;
                        }
                        self.vars.hmiItemTimerActive = true;

                        // Increment the value
                        let newVal = self.vars.hmiItemValue;
                        if (newVal === null) {
                            newVal = 0;
                        }
                        if (newVal === Number.MAX_VALUE) {
                            newVal = 0;
                        }

                        newVal += 1;

                        // We intentionally do not monitor the result as we have no way to do anything with it
                        performCompareExchange(self, self.vars.hmiItemValue, newVal, function(success) {
                            self.vars.hmiItemTimerActive = false;
                        });
                    }, self.config['timeout-hmi']);
                }

                self.log('Enabled', 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.isEnabled = false;

                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];

                // HMI -> PLC
                if (self.vars.hmiItemTimer) {
                    clearInterval(self.vars.hmiItemTimer);
                    self.vars.hmiItemTimer = null;
                }

                // PLC -> HMI
                if (self.vars.plcItemTimer) {
                    clearTimeout(self.vars.plcItemTimer);
                    self.vars.plcItemTimer = null;
                }

                self.log('Disabled', 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value) {
                var self = this;

                // Don't do anything if no plc timeout is set.
                if (!self.config['timeout-plc']) {
                    return;
                }

                // Stop the running timer as we just got an update!
                if (self.vars.plcItemTimer) {
                    clearTimeout(self.vars.plcItemTimer);
                    self.vars.plcItemTimer = null;
                }

                // Have we been disconnected before?
                if (self.vars.isDisconnected) {
                    self.vars.isDisconnected = false;
                    if (self.vars.actionOk) {
                        self.vars.actionOk.execute();
                    }
                }

                if (self.vars.imgEl.src !== self.vars.imagePathOk) {
                    self.setTooltip(self.config['image-ok-title']);
                    self.vars.imgEl.src = self.vars.imagePathOk;
                }

                //restart timer
                self.vars.plcItemTimer = setTimeout(onPlcTimerElapsed.bind(null, self), self.config['timeout-plc']);
            },

            log: function(msg, level) {
                shmi.log('[IQ:iq-heartbeat] ' + msg, level);
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-image-changer
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-image-changer",
 *     "name": null,
 *     "template": "custom/controls/iq-image-changer"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "item": The item
 * "tooltip": Tooltip
 * "options": Array of options
 * "default-image": The default image to show when none has been configured yet
 * "default-title": The default title
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-image-changer", // control name in camel-case
        uiType = "iq-image-changer", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-image-changer",
        "name": null,
        "template": "default/iq-image-changer.variant-01",
        "item": null,
        "tooltip": null,
        "options": [],
        "default-image": "pics/system/icons/placeholder.svg",
        "default-title": null,
        "scaling-mode": "fit-height",
        "action": null,
        "action-release": null,
        "action-while-pressed": null,
        "interval-while-pressed": null,
        "action-pressed": null,
        "write-bool": false,
        "on-value": 1,
        "off-value": 0
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    function applyScalingMode(self) {
        let scalingMode = "fit-height";

        switch (self.config["scaling-mode"]) {
        case "fit-height":
        case "fit-width":
        case "stretch":
        case "contain":
            scalingMode = self.config["scaling-mode"];
            break;
        default:
        }

        shmi.addClass(self.element, scalingMode);
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            imageEl: null,
            action: null,
            actionRelease: null,
            actionWhilePressed: null,
            actionWhilePressedTimer: null,
            mouseListener: null,
            touchListener: null,

            activeOption: null,
            subscriptionTargetId: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager"
        },

        /* array of custom event types fired by this control */
        events: ['press', 'release', 'click'],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.imageEl = shmi.getUiElement('image', self.element);

                // UI Actions
                if (self.config.action) {
                    self.vars.action = new shmi.visuals.core.UiAction(self.config.action, self);
                }
                if (self.config['action-press']) {
                    self.vars.actionPress = new shmi.visuals.core.UiAction(self.config['action-press'], self);
                }
                if (self.config['action-release']) {
                    self.vars.actionRelease = new shmi.visuals.core.UiAction(self.config['action-release'], self);
                }
                if (self.config['action-while-pressed']) {
                    self.vars.actionWhilePressed = new shmi.visuals.core.UiAction(self.config['action-while-pressed'], self);
                }

                if (Array.isArray(self.config.action) && self.config.action.length > 0) {
                    shmi.addClass(self.vars.imageEl, 'clickable');
                } else {
                    shmi.removeClass(self.vars.imageEl, 'clickable');
                }

                applyScalingMode(self);

                self.getActive(null);

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                const session = shmi.visuals.session;
                var eventFunctions = {
                    onClick: function(x, y, e) {
                        if (session.FocusElement !== null) {
                            session.FocusElement.blur();
                            session.FocusElement = null;
                        }
                        if (self.vars.imageEl instanceof HTMLElement) {
                            self.vars.imageEl.focus();
                            session.FocusElement = self.vars.imageEl;
                            shmi.log("[IQ:iq-image-changer] focused", 1);
                        } else {
                            shmi.log("[IQ:iq-image-changer] only HTMLElements may be focused, type: " + (self.vars.imageEl.constructor), 1);
                        }

                        self.fire("click", {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.vars.action) {
                            self.vars.action.execute();
                        }
                    },
                    onRelease: function(x, y, e) {
                        self.fire('release', {
                            x: x,
                            y: y,
                            event: e
                        });
                        if (self.config['write-bool'] && self.config.item) {
                            self.writeValue(self.config["off-value"]);
                        }

                        // Stop any running timer on release
                        if (self.vars.actionWhilePressedTimer) {
                            clearInterval(self.vars.actionWhilePressedTimer);
                        }

                        if (self.vars.actionRelease) {
                            self.vars.actionRelease.execute();
                        }
                    },
                    onPress: function(x, y, e) {
                        self.fire('press', {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.config['write-bool'] && self.config.item) {
                            self.writeValue(self.config["on-value"]);
                        }

                        // UI Action: onPress
                        if (self.vars.actionPress) {
                            self.vars.actionPress.execute();
                        }

                        // UI Action while pressed
                        if (self.vars.actionWhilePressed && self.config['interval-while-pressed']) {
                            if (self.vars.actionWhilePressedTimer) {
                                clearInterval(self.vars.actionWhilePressedTimer);
                            }

                            // While pressed timer
                            self.vars.actionWhilePressedTimer = setInterval(function() {
                                self.vars.actionWhilePressed.execute();
                            }, self.config['interval-while-pressed']);
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.imageEl, eventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.imageEl, eventFunctions));
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.log("[IQ:iq-image-changer] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                // Disable timer
                if (self.vars.actionWhilePressedTimer) {
                    clearInterval(self.vars.actionWhilePressedTimer);
                    self.vars.actionWhilePressedTimer = null;
                }

                shmi.log("[IQ:iq-image-changer] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(self.element, 'locked');

                // Disable timer
                if (self.vars.actionWhilePressedTimer) {
                    clearInterval(self.vars.actionWhilePressedTimer);
                    self.vars.actionWhilePressedTimer = null;
                }

                shmi.log("[IQ:iq-image-changer] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-image-changer] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this;
                self.vars.value = value;
                self.getActive(self.vars.value);
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                return this.value;
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision) {
            },

            /**
             * Custom function
             *
             * @param {object} value
             */
            getActive: function(value) {
                const self = this;
                let opt = value !== null ? self.config.options.find((o) => {
                    if (Number.isSafeInteger(value) && Number.isSafeInteger(o.mask) && Number.isSafeInteger(o.value)) {
                        return (BigInt(value) & BigInt(o.mask)) === BigInt(o.value);
                    }

                    return o.value === value;
                }) : null;

                if (!opt) {
                    opt = {
                        "icon-src": self.config["default-image"],
                        "label": self.config["default-title"] || null
                    };
                }
                self.vars.activeOption = opt;

                self.vars.imageEl.setAttribute("src", opt["icon-src"]);
                self.setTooltip(self.getTooltip());
            },

            /**
             * Custom function
             *
             * @returns {null|definition.prototypeExtensions.activeOption.label}
             */
            getTooltip: function() {
                var self = this,
                    superTooltip = shmi.visuals.core.BaseControl.prototype.getTooltip.call(this);

                if (superTooltip) {
                    return superTooltip;
                } else if (self.vars.activeOption && self.vars.activeOption.label) {
                    return self.vars.activeOption.label;
                }

                return null;
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-image
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-image",
 *     "name": null,
 *     "template": "custom/controls/iq-image"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "tooltip": Tooltip
 * "image-src": The image source
 *  "image-title": The title
 *  "mg-alt": The alt text
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-image", // control name in camel-case
        uiType = "iq-image", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-image",
        "name": null,
        "template": "default/iq-image.variant-01",
        "tooltip": null,
        "image-src": "pkg://iq-image/placeholder.svg",
        "image-alt": null,
        "image-title": null,
        "scaling-mode": "fit-height",
        "action": null,
        "action-release": null,
        "action-while-pressed": null,
        "interval-while-pressed": null,
        "action-pressed": null,
        "write-bool": false,
        "on-value": 1,
        "off-value": 0
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    function applyScalingMode(self) {
        let scalingMode = "fit-height";

        switch (self.config["scaling-mode"]) {
        case "fit-height":
        case "fit-width":
        case "stretch":
        case "contain":
            scalingMode = self.config["scaling-mode"];
            break;
        default:
        }

        shmi.addClass(self.element, scalingMode);
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            imageEl: null,
            action: null,
            actionRelease: null,
            actionWhilePressed: null,
            actionWhilePressedTimer: null,
            mouseListener: null,
            touchListener: null
        },

        /* imports added at runtime */
        imports: {
        },

        /* array of custom event types fired by this control */
        events: ['press', 'release', 'click'],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.imageEl = shmi.getUiElement('image', self.element);

                if (!self.vars.imageEl) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-image] Missing image element", 3);
                    return;
                }

                self.vars.imageEl.setAttribute('draggable', 'false');
                if (self.config['image-src']) {
                    self.vars.imageEl.setAttribute('src', self.config['image-src']);
                }
                if (self.config['image-alt']) {
                    self.vars.imageEl.setAttribute('alt', self.config['image-alt']);
                }

                // UI Actions
                if (self.config.action) {
                    self.vars.action = new shmi.visuals.core.UiAction(self.config.action, self);
                }
                if (self.config['action-press']) {
                    self.vars.actionPress = new shmi.visuals.core.UiAction(self.config['action-press'], self);
                }
                if (self.config['action-release']) {
                    self.vars.actionRelease = new shmi.visuals.core.UiAction(self.config['action-release'], self);
                }
                if (self.config['action-while-pressed']) {
                    self.vars.actionWhilePressed = new shmi.visuals.core.UiAction(self.config['action-while-pressed'], self);
                }

                if (Array.isArray(self.config.action) && self.config.action.length > 0) {
                    shmi.addClass(self.vars.imageEl, 'clickable');
                } else {
                    shmi.removeClass(self.vars.imageEl, 'clickable');
                }

                applyScalingMode(self);

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                const session = shmi.visuals.session;
                var eventFunctions = {
                    onClick: function(x, y, e) {
                        if (session.FocusElement !== null) {
                            session.FocusElement.blur();
                            session.FocusElement = null;
                        }
                        if (self.vars.imageEl instanceof HTMLElement) {
                            self.vars.imageEl.focus();
                            session.FocusElement = self.vars.imageEl;
                            shmi.log("[IQ:iq-image] focused", 1);
                        } else {
                            shmi.log("[IQ:iq-image] only HTMLElements may be focused, type: " + (self.vars.imageEl.constructor), 1);
                        }

                        self.fire("click", {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.vars.action) {
                            self.vars.action.execute();
                        }
                    },
                    onRelease: function(x, y, e) {
                        self.fire('release', {
                            x: x,
                            y: y,
                            event: e
                        });
                        if (self.config['write-bool'] && self.config.item) {
                            self.writeValue(self.config["off-value"]);
                        }

                        // Stop any running timer on release
                        if (self.vars.actionWhilePressedTimer) {
                            clearInterval(self.vars.actionWhilePressedTimer);
                        }

                        if (self.vars.actionRelease) {
                            self.vars.actionRelease.execute();
                        }
                    },
                    onPress: function(x, y, e) {
                        self.fire('press', {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.config['write-bool'] && self.config.item) {
                            self.writeValue(self.config["on-value"]);
                        }

                        // UI Action: onPress
                        if (self.vars.actionPress) {
                            self.vars.actionPress.execute();
                        }

                        // UI Action while pressed
                        if (self.vars.actionWhilePressed && self.config['interval-while-pressed']) {
                            if (self.vars.actionWhilePressedTimer) {
                                clearInterval(self.vars.actionWhilePressedTimer);
                            }

                            // While pressed timer
                            self.vars.actionWhilePressedTimer = setInterval(function() {
                                self.vars.actionWhilePressed.execute();
                            }, self.config['interval-while-pressed']);
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.imageEl, eventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.imageEl, eventFunctions));
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.log("[IQ:iq-image] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                // Disable timer
                if (self.vars.actionWhilePressedTimer) {
                    clearInterval(self.vars.actionWhilePressedTimer);
                    self.vars.actionWhilePressedTimer = null;
                }

                shmi.log("[IQ:iq-image] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(self.element, 'locked');

                // Disable timer
                if (self.vars.actionWhilePressedTimer) {
                    clearInterval(self.vars.actionWhilePressedTimer);
                    self.vars.actionWhilePressedTimer = null;
                }

                shmi.log("[IQ:iq-image] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-image] unlocked", 1);
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-input-field
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-input-field",
 *     "name": null,
 *     "template": "default/iq-input-field"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "min": Minimum value
 * "max": Maximum value
 * "precision": Precision
 * "decimal-delimiter": Decimal delimiter
 * "type": Type is INT
 * "unit-text": Unit Text
 * "auto-label": Whether to use the auto-label (from item)
 * "auto-unit-text": Whether to use the auto-unit (from item)
 * "auto-min": Whether to use auto-min (from item)
 * "auto-max": Whether to use auto-max (from item)
 * "auto-precision": Whether to use the auto-precision (from item)
 * "auto-type": Whether to use the auto-type (from item)
 * "numpad-enabled": Whether the numpad is enabled
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "IqInputField", // control name in camel-case
        uiType = "iq-input-field", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-input-field.variant-01",
        "label": '[Label]',
        "item": null,
        "numeric-class": "numeric",
        "min": Number.NEGATIVE_INFINITY,
        "max": Number.POSITIVE_INFINITY,
        "step": 1,
        "precision": -1,
        "decimal-delimiter": ".",
        "type": shmi.c("TYPE_STRING"),
        "unit-text": "[Unit]",
        "auto-label": true,
        "auto-unit-text": true,
        "unit-scale": 1,
        "auto-min": true,
        "auto-max": true,
        "auto-step": true,
        "auto-precision": true,
        "auto-type": true,
        "numpad-enabled": false,
        "show-icon": false,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "multiline": false,
        "notResizable": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the unit text and handles toggling the `no-unit` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} unitText Unit text to set
     */
    function setUnitTextImpl(self, unitText) {
        if (!self.vars.unitEl) {
            // Nothing to do.
        } else if (unitText === "" || unitText === null || typeof unitText === "undefined") {
            self.vars.unit = "";
            self.vars.unitEl.textContent = "";
            shmi.addClass(self.element, "no-unit");
        } else {
            self.vars.unit = unitText;
            self.vars.unitEl.textContent = shmi.localize(unitText);
            shmi.removeClass(self.element, "no-unit");
        }
    }

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: null,
            active: false,

            // DOM Elements
            currentValueEl: null,
            labelEl: null,
            unitEl: null,
            iconEl: null,
            inputEl: null, // Either currentValueEl || textAreaEl depending on the config
            textAreaEl: null,

            label: null,

            // Legacy
            domListeners: [],
            subscriptionTargetId: null,
            toEnterTmr: 0,
            wasClicked: false,
            valueSettings: null,
            uiAction: null,
            focused: false,
            locked: false,
            floatRegexp: "(^[+-]?[0-9]([.][0-9]*)?$|^[+-]?[1-9]+[0-9]*([.][0-9]*)?$)",
            intRegexp: "(^[+-]?[0-9]$|^[+-]?[1-9]+[0-9]*$)",
            type: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: ["change", "enter"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this,
                    c = shmi.Constants;

                self.vars.floatRegexp = new RegExp(self.vars.floatRegexp);

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.currentValueEl = shmi.getUiElement('input', self.element);
                self.vars.textareaEl = shmi.getUiElement('textarea', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.unitEl = shmi.getUiElement('unit', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);

                if (!self.vars.currentValueEl || !self.vars.textareaEl) {
                    shmi.log('[IQ:iq-input-field] no input and/or element in HTML', 3);
                    return;
                }

                // Multiline handling
                if (self.config.multiline) {
                    self.vars.inputEl = self.vars.textareaEl;
                    self.vars.currentValueEl.remove();
                } else {
                    self.vars.inputEl = self.vars.currentValueEl;
                    self.vars.textareaEl.remove();
                }
                self.vars.inputEl.style.display = 'block';

                // Resizable handling
                if (self.config.notResizable) {
                    shmi.addClass(self.element, "not-resizable");
                } else {
                    shmi.removeClass(self.element, "not-resizable");
                }

                /* all required elements found */
                self.vars.inputEl.setAttribute('tabindex', '0');
                self.insertValue("");

                var type = parseInt(self.config.type);
                if (!isNaN(type)) {
                    self.vars.type = type;
                }

                if (self.config.action) {
                    self.vars.uiAction = new shmi.visuals.core.UiAction(self.config.action, this);
                }

                if (!self.config['auto-type']) {
                    if ([c.TYPE_BOOL, c.TYPE_INT, c.TYPE_FLOAT].indexOf(self.vars.type) !== -1) {
                        shmi.addClass(self.element, self.config['numeric-class']);
                    }
                }

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                var eventFuncs = {
                    onPress: function(sx, sy, e) {
                        if (self.vars.focused) {
                            window.getSelection().removeAllRanges();
                        }
                    },
                    onClick: function() {
                        self.vars.wasClicked = true;
                        self.handleFocus();
                        self.vars.wasClicked = false;
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.inputEl, eventFuncs));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.inputEl, eventFuncs));

                // Custom DOM Listeners
                self.vars.domListeners.push(self.createEventListener(self.vars.inputEl, 'focus', function(e) {
                    if (self.config.multiline) {
                        self.vars.focused = true;
                    } else if (!self.vars.wasClicked) {
                        self.handleFocus();
                    }
                }));
                self.vars.domListeners.push(self.createEventListener(self.vars.inputEl, 'keypress', function(e) {
                    if (((e.keyCode === 13) && (!self.config.multiline)) || (e.keyCode === 9)) {
                        e.preventDefault();
                        window.getSelection().removeAllRanges();
                        self.vars.currentValueEl.blur();
                    }
                }));
                self.vars.domListeners.push(self.createEventListener(self.vars.inputEl, 'keydown', function(e) {
                    if (self.vars.type === shmi.c("TYPE_FLOAT")) {
                        var wrongDelimeter = (self.config['decimal-delimiter'] === ".") ? "," : ".";
                        if (e.key === wrongDelimeter) {
                            e.preventDefault();
                        }
                    }
                }));
                self.vars.domListeners.push(self.createEventListener(self.vars.inputEl, 'blur', function(e) {
                    if (!self.vars.focused) {
                        return;
                    }
                    shmi.log("[IQ:iq-input-field] blur event", 1);
                    window.getSelection().removeAllRanges();

                    self.validateAndSet(self.vars.inputEl);
                    self.vars.focused = false;
                    shmi.visuals.session.FocusElement = null;
                }));

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-input-field] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    self.vars.iconEl.style.backgroundImage = `url(${self.config['icon-src']})`;
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    var iconClasses = self.config['icon-class'].trim().split(" ");
                    iconClasses.forEach(function(cls) {
                        shmi.addClass(self.vars.iconEl, cls);
                    });
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                // Unit
                setUnitTextImpl(self, self.config['unit-text']);
                if (self.config['unit-scale']) {
                    self.config['unit-scale'] = parseFloat(shmi.localize(self.config['unit-scale']));
                }

                // Label
                setLabelImpl(self, self.config.label);
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                self.vars.inputEl.disabled = false;
                self.enableAllListeners();

                if (document.activeElement === self.vars.inputEl) {
                    self.vars.focused = true;
                }

                shmi.log("[IQ:iq-input-field] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                self.disableAllListeners();

                shmi.log("[IQ:iq-input-field] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                self.vars.locked = true;
                self.vars.inputEl.disabled = true;

                shmi.addClass(self.element, 'locked');

                self.vars.inputEl.blur();
                self.disableAllListeners();

                shmi.log("[IQ:iq-input-field] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                self.vars.locked = false;
                self.vars.inputEl.disabled = false;

                shmi.removeClass(self.element, 'locked');

                self.enableAllListeners();

                shmi.log("[IQ:iq-input-field] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this,
                    oldValue = self.vars.value;

                self.vars.value = value;

                self.insertValue(self.imports.nv.formatOutput(self.vars.value, self));
                if (document.activeElement === self.vars.inputEl) {
                    self.selectContent();
                }
                shmi.log("[IQ:iq-input-field] new value: " + self.vars.value, 0);
                if (self.vars.value !== oldValue) {
                    self.fire("change", {
                        value: self.vars.value
                    });
                }
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                var self = this,
                    type = self.vars.type;

                if (self.config.item && self.config['auto-type']) {
                    type = self.imports.im.items[self.config.item].type;
                }

                if (type === shmi.c("TYPE_INT")) {
                    return Math.round(self.vars.value / self.config['unit-scale']);
                } else if (type === shmi.c("TYPE_FLOAT")) {
                    return (self.vars.value / self.config['unit-scale']);
                } else {
                    return self.vars.value;
                }
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision) {
                var self = this,
                    c = shmi.Constants;

                if (self.config['auto-type']) {
                    self.vars.type = type;
                    if ([c.TYPE_BOOL, c.TYPE_INT, c.TYPE_FLOAT].indexOf(type)) {
                        shmi.addClass(self.element, self.config['numeric-class']);
                    }
                }

                self.imports.nv.setProperties(self, arguments);
            },

            setUnitText: function(unitText) {
                var self = this;

                if (self.vars.unitEl && self.config['auto-unit-text']) {
                    setUnitTextImpl(self, unitText, false);
                }
            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText, false);
                }
            },

            /**
             * Writes current value to connected data source
             *
             */
            updateValue: function() {
                if (this.config.item) {
                    shmi.visuals.session.ItemManager.writeValue(this.config.item, this.vars.value);
                }
            },

            /**
             * Legacy
             *
             * @param value
             */
            insertValue: function(value) {
                var self = this;
                self.vars.inputEl.value = value;
            },

            /**
             * Legacy
             *
             * @returns {string|*|string|*}
             */
            retrieveValue: function() {
                var self = this;
                return self.vars.inputEl.value;
            },

            /**
             * Legacy
             */
            selectContent: function() {
                var self = this;
                self.vars.inputEl.select();
            },

            /**
             * Legacy
             *
             * handleFocus - handle focus of input element when control is clicked or tabbed into
             */
            handleFocus: function() {
                var self = this;

                /* prevent focusing if control is locked */
                if (self.vars.locked) {
                    return;
                }

                if ((!self.vars.focused) && (shmi.visuals.session.FocusElement !== null)) {
                    shmi.visuals.session.FocusElement.blur();
                    shmi.visuals.session.FocusElement = null;
                }
                shmi.log("[IQ:iq-input-field] focused", 1);

                if (self.showNumpad()) {
                    return;
                } else if (self.showKeyboard()) {
                    return;
                }

                if (!self.vars.wasClicked) self.selectContent();

                self.vars.focused = true;
                self.vars.inputEl.focus();
                shmi.visuals.session.FocusElement = self.vars.inputEl;

                if (self.vars.toEnterTmr) {
                    clearTimeout(self.vars.toEnterTmr);
                }

                self.vars.toEnterTmr = setTimeout(function() {
                    self.fire("enter", {
                        value: self.vars.value
                    });
                }, 250);
            },

            /**
             * Legacy
             *
             * @return {boolean} if numpad will be shown
             */
            showNumpad: function() {
                const self = this,
                    nv = shmi.requires("visuals.tools.numericValues");

                if (!self.config["numpad-enabled"]) { //does nothing if not enabled
                    return false;
                }

                if (!(self.vars && self.vars.valueSettings)) {
                    //initialize value settings in case no item is configured
                    nv.initValueSettings(self);
                }

                shmi.numpad(
                    {
                        "decimal-delimiter": self.config["decimal-delimiter"],
                        "unit": self.vars.unitEl ? self.vars.unitEl.textContent : self.config["unit-text"],
                        "label": self.vars.labelEl ? self.vars.labelEl.textContent : self.config.label,
                        "value": self.retrieveValue(),
                        "callback": function(res) {
                            if (self.config.item) {
                                if (self.config.multiline) {
                                    res = res.toString();
                                }
                                self.imports.im.writeValue(self.config.item, res);
                            } else {
                                self.setValue(res);
                            }
                        },
                        "min": self.vars.valueSettings.min,
                        "max": self.vars.valueSettings.max,
                        "type": self.vars.valueSettings.type,
                        "precision": self.vars.valueSettings.precision,
                        "item": (typeof self.config.item === "string" && self.config.item.length > 0) ? self.config.item : null
                    }
                );

                return true;
            },

            /**
             * Legacy
             *
             * @return {boolean} if keyboard will be shown
             */
            showKeyboard: function() {
                var self = this,
                    appConfig = shmi.requires("visuals.session.config"),
                    keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

                if (!keyboardEnabled) {
                    return false;
                }

                shmi.keyboard(
                    {
                        "value": self.retrieveValue(),
                        "select-box-enabled": appConfig.keyboard["language-selection"],
                        "password-input": self.vars.currentValueEl && self.vars.currentValueEl.type && self.vars.currentValueEl.type.toLowerCase() === "password",
                        "show-enter": self.config.multiline,
                        "callback": function(success, input) {
                            if (success) {
                                if (self.config.item) {
                                    self.imports.im.writeValue(self.config.item, input);
                                } else {
                                    self.setValue(input);
                                }
                            }
                        }
                    });

                return true;
            },

            /**
             * Validates the content of the element and sets the value accordingly
             */
            validateAndSet: function() {
                const self = this,
                    inputString = self.retrieveValue(),
                    oldValue = self.vars.value;

                if (inputString === oldValue) {
                    return;
                }

                const newValue = (() => {
                    if (self.config.multiline) {
                        // Multi-line is always string, therefore no check and
                        // formatting is performed on it.
                        return inputString;
                    } else if (![shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")].includes(self.vars.type)) {
                        return inputString;
                    } else if (!self.vars.floatRegexp.test(inputString.replace(self.config['decimal-delimiter'], '.'))) {
                        return oldValue;
                    }

                    return self.imports.nv.applyInputSettings(inputString, self);
                })();

                self.vars.value = newValue;
                if (typeof newValue === "undefined" || newValue === null) {
                    self.insertValue("");
                } else {
                    self.insertValue(self.imports.nv.formatOutput(newValue, self));
                }

                // Emit change event, write item and call UI actions only when something has changed
                if (newValue !== oldValue) {
                    if (self.config.item) {
                        self.imports.im.writeValue(self.config.item, newValue);
                    }

                    // UI Action
                    if (self.vars.uiAction) {
                        self.vars.uiAction.execute(newValue);
                    }

                    self.fire("change", {
                        value: self.vars.value
                    });
                }
            },

            /**
             * Helper functions
             */
            enableAllListeners: function() {
                var self = this;
                self.vars.domListeners.forEach(function(obj) {
                    obj.element.addEventListener(obj.event, obj.callback);
                });
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            disableAllListeners: function() {
                var self = this;
                self.vars.domListeners.forEach(function(obj) {
                    obj.element.removeEventListener(obj.event, obj.callback);
                });
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            createEventListener: function(element, event, callback) {
                return {
                    'event': event,
                    'callback': callback,
                    'element': element
                };
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-label
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-label",
 *     "name": null,
 *     "template": "custom/controls/iq-label"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "precision": Precision
 * "decimal-delimiter": Decimal delimiter
 * "type": Type is INT
 * "auto-precision": Whether to use the auto-precision (from item)
 * "auto-type": Whether to use the auto-type (from item)
 * "tooltip": Tooltip
 * "options": The options
 * "pattern": The pattern
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-label", // control name in camel-case
        uiType = "iq-label", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-label",
        "name": null,
        "template": "default/iq-label.variant-01",
        "item": null,
        "precision": -1,
        "decimal-delimiter": ".",
        "type": shmi.c("TYPE_INT"),
        "auto-precision": true,
        "auto-type": true,
        "tooltip": null,
        "options": [],
        "pattern": null,
        "value-as-tooltip": false,
        "text": "${label.no-value}"
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            value: 0,
            active: false,

            // DOM Elements
            labelEl: null,

            rafId: null,
            subscriptionTargetId: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                // Configure NV
                self.imports.nv.initValueSettings(self);

                // DOM Element
                self.vars.labelEl = shmi.getUiElement('label', self.element);

                if (self.config.text) {
                    self.updateText(shmi.localize(self.config.text));
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-label] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                if (self.vars.subscriptionTargetId) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-label] disabled", 1);
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                var self = this;
                return self.vars.value;
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision) {
                var self = this;
                self.imports.nv.setProperties(self, arguments);
            },

            /* Called by ItemManager when subscribed item changes and once on initial subscription
             *
             * "options" : [
             *  { "label" : "True",  "value" : 0 },
             *  { "label" : "False", "value" : 1 },
             *  { "label" : "=",     "value" : 2 },
             *  { "label" : ">",     "value" : 3 },
             *  { "label" : ">=",    "value" : 4 },
             *  { "label" : "<",     "value" : 5 },
             *  { "label" : "<=",    "value" : 6 },
             *  { "label" : "&",     "value" : 7 },
             *  { "label" : "|",     "value" : 8 },
             *  { "label" : "Null",  "value" : 9 }
             * ]
            **/
            onSetValue: function(value, type, name) {
                var self = this;

                shmi.caf(self.vars.rafId);
                self.vars.rafId = shmi.raf(function() {
                    var newValue = value;

                    if (self.config.options.length) {
                        const selectedOption = self.config.options.find((o) => {
                            if (Number.isSafeInteger(value) && Number.isSafeInteger(o.mask) && Number.isSafeInteger(o.value)) {
                                return (BigInt(value) & BigInt(o.mask)) === BigInt(o.value);
                            }

                            return o.value === value;
                        });

                        if (selectedOption) {
                            newValue = shmi.localize(selectedOption.label);
                        }
                    } else if ((typeof self.config.pattern === "string") && self.config.pattern.trim().length) {
                        newValue = shmi.localize(shmi.evalString(self.config.pattern, { VALUE: value }));
                    } else {
                        newValue = self.imports.nv.formatOutput(value, self);
                    }

                    self.updateText(newValue);
                });
            },

            /**
             * Updates the text
             *
             * @param newValue
             */
            updateText: function(newValue) {
                var self = this;

                self.vars.labelEl.textContent = newValue;
                if (self.config["value-as-tooltip"]) {
                    self.setTooltip(newValue);
                }
            },
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-linear-gauge
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-linear-gauge",
 *     "name": null,
 *     "template": "custom/controls/iq-linear-gauge"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "tooltip": Tooltip
 * "text": The text
 * "item": Items to be used inside
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqLinearGauge", // control name in camel-case
        uiType = "iq-linear-gauge", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-linear-gauge.variant-01",
        "item": null,
        "tooltip": null,
        "type": shmi.c("TYPE_INT"),
        "precision": 0,
        "min": Number.NEGATIVE_INFINITY,
        "max": Number.POSITIVE_INFINITY,
        "label": "[Label]",
        "unit-text": "[Unit]",
        "decimal-delimiter": ".",
        "default-value": "---",
        "show-text": true,
        "show-icon": false,
        "icon-src": null,
        "icon-class": null,

        "auto-min": true,
        "auto-max": true,
        "auto-label": true,
        "auto-unit-text": true,
        "auto-precision": true,

        "animation-duration": 100,
        "fill-reverse": false,
        "fill-inverse": false
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.dom.label) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined" || !self.config["show-text"]) {
            self.vars.label = "";
            self.vars.dom.label.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.dom.label.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * Sets the unit text and handles toggling the `no-unit` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} unitText Unit text to set
     */
    function setUnitTextImpl(self, unitText) {
        if (!self.vars.dom.unit) {
            // Nothing to do.
        } else if (unitText === "" || unitText === null || typeof unitText === "undefined" || !self.config["show-text"]) {
            self.vars.unit = "";
            self.vars.dom.unit.textContent = "";
            shmi.addClass(self.element, "no-unit");
        } else {
            self.vars.unit = unitText;
            self.vars.dom.unit.textContent = shmi.localize(unitText);
            shmi.removeClass(self.element, "no-unit");
        }
    }

    /**
     * Returns the bar fill percentage between 0 and 1 for the given value. If
     * no value is given, the widgets current value is used instead.
     *
     * @param {*} self
     * @param {number} [value]
     * @returns {number} Bar fill percentage between 0 and 1.
     */
    function getFillPercentage(self, value) {
        const { min, max } = self.vars.valueSettings,
            percentage = (value - min) / (max - min);

        if (isNaN(percentage) || min === max) {
            return 0;
        }

        if (self.config["fill-inverse"]) {
            return 1 - Math.min(Math.max(percentage, 0), 1);
        }

        return Math.min(Math.max(percentage, 0), 1);
    }

    /**
     * Updates bar.
     *
     * @param {*} self
     * @param {?number} param1.actValue
     */
    function updateWidgetBar(self, { actValue }) {
        if (self.vars.dom.bar) {
            self.vars.dom.bar.style.setProperty("--internal-fill-level", `${getFillPercentage(self, actValue) * 100}%`);
        }
    }

    /**
     * Updates value display.
     *
     * @param {*} self
     */
    function updateWidgetText(self) {
        if (self.vars.dom.value) {
            if (self.getValue() === null) {
                self.vars.dom.value.textContent = shmi.localize(self.config["default-value"]);
            } else {
                self.vars.dom.value.textContent = self.imports.nv.formatOutput(self.getValue(), self);
            }
        }
    }

    /**
     * Queues a widget update for the next animation frame. Will only queue one
     * update per frame.
     *
     * @param {*} self
     */
    function queueWidgetTextUpdate(self) {
        if (self.vars.rafId) {
            return;
        }

        self.vars.rafId = shmi.raf(() => {
            updateWidgetText(self);

            self.vars.rafId = null;
        });
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            dom: {
                value: null,
                bar: null,
                unit: null,
                label: null,
                icon: null
            },
            valueSettings: {
                min: 0,
                max: 100
            },
            animationBundle: null,
            rafId: null,
            value: null,
            tokens: []
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues",
            gfx: "visuals.gfx"
        },

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                // DOM
                Object.assign(self.vars.dom, {
                    value: shmi.getUiElement("value", self.element),
                    bar: shmi.getUiElement("bar", self.element),
                    unit: shmi.getUiElement("unit", self.element),
                    label: shmi.getUiElement("label", self.element),
                    icon: shmi.getUiElement("icon", self.element)
                });

                self.imports.nv.initValueSettings(self);

                // Icon
                if (self.vars.dom.icon && self.config['icon-src'] && self.config['show-icon']) {
                    self.vars.dom.icon.style.backgroundImage = `url(${self.config['icon-src']})`;
                } else if (self.vars.dom.icon && self.config['icon-class'] && self.config['show-icon']) {
                    const iconClasses = self.config['icon-class'].trim().split(" ");
                    iconClasses.forEach((cls) => shmi.addClass(self.vars.dom.icon, cls));
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                // Unit
                setUnitTextImpl(self, self.config['unit-text']);
                if (self.config['unit-scale']) {
                    self.config['unit-scale'] = parseFloat(shmi.localize(self.config['unit-scale']));
                }

                // Label
                setLabelImpl(self, self.config.label);

                // Fill orientation
                if (self.vars.dom.bar) {
                    if (self.config["fill-reverse"]) {
                        shmi.addClass(self.vars.dom.bar, "reversed");
                    }
                }

                self.vars.animationBundle = new self.imports.gfx.AnimationBundle(updateWidgetBar.bind(null, self));
                self.vars.animationBundle.prepare("actValue", null, "linear");

                updateWidgetText(self);
            },

            /* Called when control is enabled */
            onEnable: function() {
                const self = this;

                if (self.config.item) {
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config.item, self));
                }
            },

            /* Called when control is disabled */
            onDisable: function() {
                const self = this;

                if (self.vars.rafId) {
                    shmi.caf(self.vars.rafId);
                    self.vars.rafId = null;
                }

                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.tokens = [];
                self.vars.animationBundle.cancel("actValue");
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                const self = this;

                return self.vars.value;
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this,
                    supportedTypes = [shmi.c("TYPE_BOOL"), shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")],
                    newValue = (supportedTypes.includes(type) ? value : Number.NaN);

                if (self.vars.value === newValue) {
                    return;
                }

                self.vars.value = newValue;
                self.vars.animationBundle.start(newValue, self.config["animation-duration"], "actValue");
                queueWidgetTextUpdate(self);
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(...args) {
                const self = this;
                //const [min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision] = args;

                self.imports.nv.setProperties(self, args);
                self.vars.animationBundle.refresh();
                queueWidgetTextUpdate(self);
            },

            setUnitText: function(unitText) {
                const self = this;

                if (self.config['auto-unit-text']) {
                    setUnitTextImpl(self, unitText);
                }
            },

            setLabel: function(labelText) {
                const self = this;

                if (self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-progress-info
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-progress-info",
 *     "name": null,
 *     "template": "custom/controls/iq-progress-info"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "tooltip": Tooltip
 * "text": The text
 * "item": Items to be used inside
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqProgressInfo", // control name in camel-case
        uiType = "iq-progress-info", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-progress-info.variant-01",
        "displayFormat": "($baseItem / $comparisonItem) - $percentValue%",
        "baseItem": null,
        "comparisonItem": null,
        "tooltip": null,
        "precision": -1,
        "decimal-delimiter": ".",
        "default-value": "---",

        "auto-precision": true,

        "animation-duration": 100,
        "fill-reverse": false,
        "fill-inverse": false
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    /**
     * Creates placeholder elements in the value element for the format string.
     *
     * @param {*} self
     * @param {string} displayFormat
     */
    function setupValueElements(self, displayFormat) {
        const { value: valueEl } = self.vars.dom;

        if (!valueEl) {
            return;
        }

        valueEl.innerHTML = displayFormat.
            replace(/[$]baseItem/g, `<span class="baseValue"></span>`).
            replace(/[$]comparisonItem/g, `<span class="compareValue"></span>`).
            replace(/[$]percentValue/g, `<span class="percentValue"></span>`);

        self.vars.placeholders = {
            baseItem: Array.from(valueEl.getElementsByClassName("baseValue")),
            comparisonItem: Array.from(valueEl.getElementsByClassName("compareValue")),
            percentValue: Array.from(valueEl.getElementsByClassName("percentValue"))
        };
    }

    /**
     * Sets values of placeholder elements.
     *
     * @param {*} self
     * @param {number} baseValue
     * @param {number} comparisonValue
     */
    function setPlaceholderValues(self, baseValue, comparisonValue) {
        const {
                nv: { formatOutput },
                iter: { iterateObject }
            } = self.imports,
            data = {
                baseItem: formatOutput(baseValue, self),
                comparisonItem: formatOutput(comparisonValue, self),
                percentValue: Math.round(baseValue / comparisonValue * 100)
            };

        if (!self.vars.placeholders) {
            return;
        }

        iterateObject(data, (value, key) => {
            const placeholders = self.vars.placeholders[key];
            if (Array.isArray(placeholders)) {
                placeholders.forEach((el) => el.textContent = value);
            }
        });
    }

    /**
     * Returns the bar fill percentage between 0 and 1 for the given value.
     *
     * @param {number} value
     * @param {number} comparisonValue
     * @param {boolean} [invert]
     * @returns {number} Bar fill percentage between 0 and 1.
     */
    function getFillPercentage(value, comparisonValue, invert) {
        const percentage = value / comparisonValue;

        if (isNaN(percentage)) {
            return 0;
        } else if (invert) {
            return 1 - Math.min(Math.max(percentage, 0), 1);
        }

        return Math.min(Math.max(percentage, 0), 1);
    }

    /**
     * Updates value display.
     *
     * @param {*} self
     */
    function updateWidgetText(self) {
        if (!self.vars.dom.value) {
            // Nothing to do
        } else if (self.getValue() === null || self.getComparisonValue() === null) {
            self.vars.dom.value.textContent = shmi.localize(self.config["default-value"]);
            self.vars.placeholders = null;
        } else {
            if (!self.vars.placeholders) {
                setupValueElements(self, self.config.displayFormat);
            }

            setPlaceholderValues(self, self.getValue(), self.getComparisonValue());
        }
    }

    /**
     * Updates bar.
     *
     * @param {*} self
     * @param {?number} param1.baseValue
     * @param {?number} param1.compValue
     */
    function updateWidgetBar(self, { baseValue, compValue }) {
        const valuePercentage = getFillPercentage(baseValue, compValue, self.config["fill-inverse"]);

        if (self.vars.dom.bar) {
            self.vars.dom.bar.style.setProperty("--internal-fill-level", `${valuePercentage * 100}%`);
        }
    }

    /**
     * Queues a widget update for the next animation frame. Will only queue one
     * update per frame.
     *
     * @param {*} self
     */
    function queueWidgetTextUpdate(self) {
        if (self.vars.rafId || !self.vars.dom.value) {
            return;
        }

        self.vars.rafId = shmi.raf(() => {
            updateWidgetText(self);

            self.vars.rafId = null;
        });
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            dom: {
                value: null,
                bar: null,
                barContainer: null
            },
            placeholders: null,
            valueSettings: {
                min: 0,
                max: 100
            },
            animationBundle: null,
            rafId: null,
            value: null,
            valueComp: null,
            listeners: [],
            tokens: []
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            iter: "visuals.tools.iterate",
            nv: "visuals.tools.numericValues",
            gfx: "visuals.gfx"
        },

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                // DOM
                Object.assign(self.vars.dom, {
                    value: shmi.getUiElement("value", self.element),
                    bar: shmi.getUiElement("bar", self.element),
                    barContainer: shmi.getUiElement("bar-container", self.element)
                });

                self.imports.nv.initValueSettings(self);

                // Fill orientation
                if (self.vars.dom.bar) {
                    if (self.config["fill-reverse"]) {
                        shmi.addClass(self.vars.dom.bar, "reversed");
                    }

                    self.vars.animationBundle = new self.imports.gfx.AnimationBundle(updateWidgetBar.bind(null, self));
                    self.vars.animationBundle.prepare("baseValue", null, "linear");
                    self.vars.animationBundle.prepare("compValue", null, "linear");
                }

                updateWidgetText(self);
            },

            /* Called when control is enabled */
            onEnable: function() {
                const self = this;

                if (self.config.baseItem) {
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config.baseItem, self));
                }

                if (self.config.comparisonItem) {
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config.comparisonItem, {
                        setValue: self.onSetCompValue.bind(self)
                    }));
                }
            },

            /* Called when control is disabled */
            onDisable: function() {
                const self = this;

                if (self.vars.rafId) {
                    shmi.caf(self.vars.rafId);
                    self.vars.rafId = null;
                }

                if (self.vars.animationBundle) {
                    self.vars.animationBundle.cancel("baseValue");
                    self.vars.animationBundle.cancel("compValue");
                }

                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.tokens = [];
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            },

            /**
             * Retrieves current value of the progress info
             *
             * @return value - current value
             */
            getValue: function() {
                const self = this;

                return self.vars.value;
            },

            /**
             * Retrieves current comparison value of the progress info
             *
             * @return value - current comparison value
             */
            getComparisonValue: function() {
                const self = this;

                return self.vars.valueComp;
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this,
                    supportedTypes = [shmi.c("TYPE_BOOL"), shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")],
                    newValue = (supportedTypes.includes(type) ? value : Number.NaN);

                if (self.vars.value === newValue) {
                    return;
                }

                self.vars.value = newValue;
                if (self.vars.animationBundle) {
                    self.vars.animationBundle.start(newValue, self.config["animation-duration"], "baseValue");
                }

                queueWidgetTextUpdate(self);
            },

            onSetCompValue: function(value, type) {
                const self = this,
                    supportedTypes = [shmi.c("TYPE_BOOL"), shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")],
                    newValue = (supportedTypes.includes(type) ? value : Number.NaN);

                if (self.vars.valueComp === newValue) {
                    return;
                }

                self.vars.valueComp = newValue;
                if (self.vars.animationBundle) {
                    self.vars.animationBundle.start(newValue, self.config["animation-duration"], "compValue");
                }

                queueWidgetTextUpdate(self);
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(...args) {
                const self = this;
                //const [min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision] = args;

                self.imports.nv.setProperties(self, args);
                queueWidgetTextUpdate(self);
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-quality-display
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-quality-display",
 *     "name": null,
 *     "template": "custom/controls/iq-quality-display"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "tooltip": Tooltip
 * "text": The text
 * "item": Items to be used inside
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqQualityDisplay", // control name in camel-case
        uiType = "iq-quality-display", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-quality-display.variant-01",
        "item": null,
        "item-ctrl": null,
        "tooltip": null,
        "type": shmi.c("TYPE_INT"),
        "precision": 0,
        "min": 0,
        "max": 100,
        "label": "[Label]",
        "unit-text": "[Unit]",
        "decimal-delimiter": ".",
        "default-value": "---",
        "show-icon": false,
        "show-text": true,
        "icon-src": null,
        "icon-class": null,
        "auto-type": true,
        "auto-min": true,
        "auto-max": true,
        "auto-label": true,
        "auto-unit-text": true,
        "auto-precision": true,

        "animation-duration": 100
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.dom.label) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined" || !self.config["show-text"]) {
            self.vars.label = "";
            self.vars.dom.label.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.dom.label.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    /**
     * Sets the unit text and handles toggling the `no-unit` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} unitText Unit text to set
     */
    function setUnitTextImpl(self, unitText) {
        if (!self.vars.dom.unit) {
            // Nothing to do.
        } else if (unitText === "" || unitText === null || typeof unitText === "undefined" || !self.config["show-text"]) {
            self.vars.unit = "";
            self.vars.dom.unit.textContent = "";
            shmi.addClass(self.element, "no-unit");
        } else {
            self.vars.unit = unitText;
            self.vars.dom.unit.textContent = shmi.localize(unitText);
            shmi.removeClass(self.element, "no-unit");
        }
    }

    /**
     * Returns the bar fill percentage between 0 and 1 for the given value. If
     * no value is given, the widgets current value is used instead.
     *
     * @param {*} self
     * @param {number} value
     * @returns {number} Bar fill percentage between 0 and 1.
     */
    function getFillPercentage(self, value) {
        const { min, max } = self.vars.valueSettings,
            percentage = (value - min) / (max - min);

        if (isNaN(percentage) || max === min) {
            return 0;
        }

        return Math.min(Math.max(percentage, 0), 1);
    }

    /**
     * Updates value display.
     *
     * @param {*} self
     */
    function updateWidgetText(self) {
        if (self.vars.dom.value) {
            if (self.getValue() === null) {
                self.vars.dom.value.textContent = shmi.localize(self.config["default-value"]);
            } else {
                self.vars.dom.value.textContent = self.imports.nv.formatOutput(self.getValue(), self);
            }
        }

        if (self.vars.dom.controlValue) {
            if (self.getControlValue() === null) {
                self.vars.dom.controlValue.textContent = shmi.localize(self.config["default-value"]);
            } else {
                self.vars.dom.controlValue.textContent = self.imports.nv.formatOutput(self.getControlValue(), self);
            }
        }
    }

    /**
     * Updates bar and control value position.
     *
     * @param {*} self
     * @param {?number} param1.actValue
     * @param {?number} param1.ctrlValue
     */
    function updateWidgetBars(self, { actValue, ctrlValue }) {
        const valuePercentage = getFillPercentage(self, actValue),
            valueControlPercentage = getFillPercentage(self, ctrlValue);

        const {
            barContainer: barContainerEl,
            indicator: indicatorEl,
            controlValue: controlValueEl
        } = self.vars.dom;

        if (barContainerEl) {
            barContainerEl.style.setProperty("--internal-fill-level-set", `${valueControlPercentage * 100}%`);
            barContainerEl.style.setProperty("--internal-fill-level", `${valuePercentage * 100}%`);
        }

        if (indicatorEl) {
            indicatorEl.style.setProperty("--internal-fill-level-set", `${valueControlPercentage * 100}%`);
            indicatorEl.style.setProperty("--internal-fill-level", `${valuePercentage * 100}%`);
        }

        if (barContainerEl && controlValueEl) {
            const barBoundingRect = barContainerEl.getBoundingClientRect();

            controlValueEl.style.setProperty("--internal-fill-level-set", `${valueControlPercentage * 100}%`);
            controlValueEl.style.setProperty("--internal-fill-level", `${valuePercentage * 100}%`);
            controlValueEl.style.setProperty("--internal-bar-offset-h", `${barBoundingRect.width * valueControlPercentage}px`);
            controlValueEl.style.setProperty("--internal-bar-offset-v", `${barBoundingRect.height * (1 - valueControlPercentage)}px`);
        }
    }

    /**
     * Queues a widget update for the next animation frame. Will only queue one
     * update per frame.
     *
     * @param {*} self
     */
    function queueWidgetTextUpdate(self) {
        if (self.vars.rafId) {
            return;
        }

        self.vars.rafId = shmi.raf(() => {
            updateWidgetText(self);

            self.vars.rafId = null;
        });
    }

    /**
     * Computes orientation and anchor of the gauge bar.
     *
     * @param {*} self
     * @returns {object}
     */
    function getFillProperties(self) {
        if (!self.vars.dom.bar) {
            return null;
        }

        const computed = getComputedStyle(self.vars.dom.bar),
            [transformOriginX, transformOriginY] = computed.transformOrigin.replace("px", "").split(" ").map(parseFloat),
            isHorizontalScaling = transformOriginY <= parseFloat(computed.height.replace("px", "")) / 2,
            fromLeft = transformOriginX === 0,
            fromTop = transformOriginY === 0;

        return {
            orientation: isHorizontalScaling ? "horizontal" : "vertical",
            reverse: isHorizontalScaling ? !fromTop : !fromLeft,
            anchor: (() => {
                if (isHorizontalScaling) {
                    return fromLeft ? "left" : "right";
                } else {
                    return fromTop ? "top" : "bottom";
                }
            })()
        };
    }

    /**
     * Creates a Visuals token-like observer that listens for changes that
     * require the widget to be updated.
     *
     * @param {*} self
     * @param {function} callback
     */
    function makeUpdateToken(self, callback) {
        let throttled = false;

        const throttledCallback = () => {
                throttled = false;
                Promise.resolve().then(() => {
                    if (!throttled) {
                        callback();
                        throttled = true;
                    }
                });
            },
            // We only use the ResizeObserver if it's supported. Otherwise we
            // have to rely on the `resize` event only :(.
            // eslint-disable-next-line compat/compat
            resizeObserver = window.ResizeObserver ? new ResizeObserver(throttledCallback) : null;

        window.addEventListener("resize", throttledCallback);
        if (resizeObserver && self.vars.dom.barContainer) {
            resizeObserver.observe(self.vars.dom.barContainer);
        }

        return {
            unlisten: () => {
                window.removeEventListener("resize", throttledCallback);
                if (resizeObserver && self.vars.dom.barContainer) {
                    resizeObserver.unobserve(self.vars.dom.barContainer);
                }
            }
        };
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            dom: {
                value: null,
                bar: null,
                barContainer: null,
                unit: null,
                label: null,
                icon: null,
                indicator: null,
                indicatorLine: null,
                controlValue: null
            },
            valueSettings: {
                min: 0,
                max: 100
            },
            animationBundle: null,
            rafId: null,
            value: null,
            valueCtrl: null,
            orientationInfo: null,
            listeners: [],
            tokens: []
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues",
            gfx: "visuals.gfx"
        },

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                // DOM
                Object.assign(self.vars.dom, {
                    value: shmi.getUiElement("value", self.element),
                    bar: shmi.getUiElement("bar", self.element),
                    barContainer: shmi.getUiElement("bar-container", self.element),
                    unit: shmi.getUiElement("unit", self.element),
                    label: shmi.getUiElement("label", self.element),
                    icon: shmi.getUiElement("icon", self.element),
                    indicator: shmi.getUiElement("indicator", self.element),
                    indicatorLine: shmi.getUiElement("indicator-line", self.element),
                    controlValue: shmi.getUiElement("control-value", self.element)
                });

                self.imports.nv.initValueSettings(self);

                // Icon
                if (self.vars.dom.icon && self.config['icon-src'] && self.config['show-icon']) {
                    self.vars.dom.icon.style.backgroundImage = `url(${self.config['icon-src']})`;
                } else if (self.vars.dom.icon && self.config['icon-class'] && self.config['show-icon']) {
                    const iconClasses = self.config['icon-class'].trim().split(" ");
                    iconClasses.forEach((cls) => shmi.addClass(self.vars.dom.icon, cls));
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                // Unit
                setUnitTextImpl(self, self.config['unit-text']);
                if (self.config['unit-scale']) {
                    self.config['unit-scale'] = parseFloat(shmi.localize(self.config['unit-scale']));
                }

                // Label
                setLabelImpl(self, self.config.label);

                // Fill orientation
                if (self.vars.dom.bar) {
                    self.vars.orientationInfo = getFillProperties(self);
                }

                self.vars.animationBundle = new self.imports.gfx.AnimationBundle(updateWidgetBars.bind(null, self));
                self.vars.animationBundle.prepare("actValue", null, "linear");
                self.vars.animationBundle.prepare("ctrlValue", null, "linear");

                updateWidgetText(self);
            },

            /* Called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.tokens.push(makeUpdateToken(self, () => {
                    queueWidgetTextUpdate(self);
                    self.vars.animationBundle.refresh();
                }));

                if (self.config.item) {
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config.item, self));
                }

                if (self.config["item-ctrl"]) {
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config["item-ctrl"], {
                        setValue: self.onSetCtrlValue.bind(self)
                    }));
                }
            },

            /* Called when control is disabled */
            onDisable: function() {
                const self = this;

                if (self.vars.rafId) {
                    shmi.caf(self.vars.rafId);
                    self.vars.rafId = null;
                }

                self.vars.animationBundle.cancel("actValue");
                self.vars.animationBundle.cancel("ctrlValue");

                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.tokens = [];
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            },

            /**
             * Retrieves current value of the quality display
             *
             * @return value - current value
             */
            getValue: function() {
                const self = this;

                return self.vars.value;
            },

            /**
             * Retrieves current control value of the quality display
             *
             * @return value - current control value
             */
            getControlValue: function() {
                const self = this;

                return self.vars.valueCtrl;
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this,
                    supportedTypes = [shmi.c("TYPE_BOOL"), shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")],
                    newValue = (supportedTypes.includes(type) ? value : Number.NaN);

                if (self.vars.value === newValue) {
                    return;
                }

                self.vars.value = newValue;
                self.vars.animationBundle.start(newValue, self.config["animation-duration"], "actValue");
                queueWidgetTextUpdate(self);
            },

            onSetCtrlValue: function(value, type) {
                const self = this,
                    supportedTypes = [shmi.c("TYPE_BOOL"), shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")],
                    newValue = (supportedTypes.includes(type) ? value : Number.NaN);

                if (self.vars.valueCtrl === newValue) {
                    return;
                }

                self.vars.valueCtrl = newValue;
                self.vars.animationBundle.start(newValue, self.config["animation-duration"], "ctrlValue");
                queueWidgetTextUpdate(self);
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(...args) {
                const self = this;
                //const [min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision] = args;

                self.imports.nv.setProperties(self, args);
                self.vars.animationBundle.refresh();
                queueWidgetTextUpdate(self);
            },

            setUnitText: function(unitText) {
                const self = this;

                if (self.config['auto-unit-text']) {
                    setUnitTextImpl(self, unitText);
                }
            },

            setLabel: function(labelText) {
                const self = this;

                if (self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-radial-gauge
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-radial-gauge",
 *     "name": null,
 *     "template": "default/iq-radial-gauge"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "min": Minimum value
 * "max": Maximum value
 * "step": Step width
 * "precision": Precision
 * "type": Type is INT
 * "unit-text": Unit Text
 * "auto-label": Whether to use the auto-label (from item)
 * "auto-min": Whether to use auto-min (from item)
 * "auto-max": Whether to use auto-max (from item)
 * "auto-step": Whether to use auto-step (from item)
 * "auto-precision": Whether to use the auto-precision (from item)
 * "auto-type": Whether to use the auto-type (from item)
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "IqRadialGauge", // control name in camel-case
        uiType = "iq-radial-gauge", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-radial-gauge.variant-01",
        "label": '[Label]',
        "item": null,
        "min": Number.NEGATIVE_INFINITY,
        "max": Number.POSITIVE_INFINITY,
        "step": 1,
        "precision": 0,
        "type": shmi.c("TYPE_INT"),
        "unit-text": "[Unit]",
        "auto-label": true,
        "auto-min": true,
        "auto-max": true,
        "auto-step": true,
        "auto-precision": true,
        "auto-type": true,
        "show-icon": false,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,

        "animation-duration": 100,
        "arc-fill-reverse": false,
        "arc-fill-inverse": false,
        "default-value": "---"
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,

        /*******************/
        /** CSS MAPPINGS ***/
        /*******************/
        /* Defines a CSS to object (self.vars.styles...) mapping for a line style (background arc, foreground arc):
         *
         * propName = CSS property name, i.e. "color" means: .className { color:#c40000; }
         * attribName = The attribute name from the self.vars.styles object
         * conversion = which conversion to use (none, parseInt or special box-shadow mapping
         */
        lineStyleConfig = [
            {
                "propName": "background",
                "attribName": "color",
                "conversion": "parseBackground"
            },
            {
                "propName": "width",
                "attribName": "lineWidth",
                "conversion": "parseInt"
            },
            {
                "propName": "box-shadow",
                "attribName": ["shadowColor", "shadowOffsetX", "shadowOffsetY", "shadowBlur", null],
                "conversion": "boxShadow"
            }
        ],

        // Defines a CSS to object (self.vars.styles...) mapping for a font style (value, label, unit)
        fontStyleConfig = [
            {
                "propName": "color",
                "attribName": "color",
                "conversion": null
            },
            {
                "propName": "font-size",
                "attribName": "fontSize",
                "conversion": "parseInt"
            },
            {
                "propName": "font-family",
                "attribName": "fontFamily",
                "conversion": null
            },
            {
                "propName": "font-weight",
                "attribName": "fontWeight",
                "conversion": null
            },
            {
                "propName": "font-style",
                "attribName": "fontStyle",
                "conversion": null
            },
            {
                "propName": "text-decoration",
                "attribName": "textUnderline",
                "conversion": "parseUnderline"
            }
        ],

        orientationStyleConfig = [
            {
                "propName": "top",
                "attribName": "offsetVertical",
                "conversion": "parsePercent"
            },
            {
                "propName": "left",
                "attribName": "offsetHorizontal",
                "conversion": "parsePercent"
            },
            {
                "propName": "offset-rotate",
                "attribName": "rotation",
                "conversion": "offsetRotate"
            }
        ],

        arcSliceStyleConfig = [
            {
                "propName": "width",
                "attribName": "width",
                "conversion": "parsePercent"
            }
        ],

        percentageSizeStyle = [
            {
                "propName": "width",
                "attribName": "width",
                "conversion": "parsePercent"
            },
            {
                "propName": "height",
                "attribName": "height",
                "conversion": "parsePercent"
            }
        ],

        // Defines all available mappings (background arc, foreground arc, end marker, value, label, unit)
        cssStyleMappings = [
            {
                "data-ui": "arc-bg",
                "objName": "arcBackground",
                "styleConfig": lineStyleConfig
            },
            {
                "data-ui": "arc-fg",
                "objName": "arcForeground",
                "styleConfig": lineStyleConfig.concat(orientationStyleConfig)
            },
            {
                "data-ui": "arc-slice",
                "objName": "slice",
                "styleConfig": arcSliceStyleConfig
            },
            {
                "data-ui": "line-value",
                "objName": "valueLine",
                "styleConfig": lineStyleConfig
            },
            {
                "data-ui": "value",
                "objName": "value",
                "styleConfig": fontStyleConfig.concat(orientationStyleConfig)
            },
            {
                "data-ui": "label",
                "objName": "label",
                "styleConfig": fontStyleConfig.concat(orientationStyleConfig)
            },
            {
                "data-ui": "unit",
                "objName": "unit",
                "styleConfig": fontStyleConfig.concat(orientationStyleConfig)
            },
            {
                "data-ui": "icon-src",
                "objName": "icon",
                "styleConfig": percentageSizeStyle.concat(orientationStyleConfig)
            },
            {
                "data-ui": "icon-class",
                "objName": "iconClass",
                "styleConfig": fontStyleConfig
            }
        ];

    const styleParsers = {
        identity: (inputValue) => [inputValue],
        parseInt: (inputValue) => {
            const parsed = parseInt(inputValue);
            if (isNaN(parsed)) {
                return null;
            }

            return [parsed];
        },
        parsePercent: (inputValue) => {
            if (inputValue === "0px") {
                return [0];
            } else if (!inputValue.endsWith("%")) {
                return null;
            }

            const parsed = parseFloat(inputValue.substr(0, inputValue.length - 1));
            if (isNaN(parsed)) {
                return null;
            }

            return [parsed / 100];
        },
        boxShadow: (inputValue) => {
            if (inputValue === "none") {
                return [0, 0, 0, 0, "transparent"];
            }

            // <color> <offset-x> <offset-y> [blur-radius] [spread-radius]
            const regex = /^(rgba?\([-\d.,\s]+\))\s+(-?[0-9]+(?:\.[0-9]+)?)px\s+(-?[0-9]+(?:\.[0-9]+)?)px\s+(-?[0-9]+(?:\.[0-9]+)?)px\s+(-?[0-9]+(?:\.[0-9]+)?)px$/;
            const match = inputValue.match(regex);
            if (!match) {
                return null;
            }

            return [
                match[1], // color
                match[2], // offset-x
                match[3], // offset-y
                match[4], // blur-radius
                match[5] // spread-radius
            ];
        },
        offsetRotate: (inputValue) => {
            // [auto] <rotation>
            const regex = /^(?:auto\s+)?(-?[0-9]+(?:\.[0-9]+)?)deg$/,
                match = inputValue.match(regex);
            if (!match) {
                return null;
            }

            return [
                Math.PI * parseFloat(match[1]) / 180
            ];
        },
        parseBackground: (inputValue) => {
            const regAll = /(linear-gradient|radial-gradient)\((?:([^,]+), )?((?:rgba?\([-\d.,\s]+\)(?: [-\d.]+(%|px)?)?(?:, )?)+)\)/,
                regToken = /(rgba?\([-\d.,\s]+\))(?: ([-\d.]+)(%|px)?)?(?:, )?/g,
                regColor = /^(rgba?\([-\d.,\s]+\))/;

            const coarseGradientTokens = inputValue.match(regAll);
            if (!coarseGradientTokens) {
                // background is not a gradient.
                const colorTokens = inputValue.match(regColor);
                if (!colorTokens) {
                    return [null];
                }

                return [{
                    type: "color",
                    color: colorTokens[1]
                }];
            }

            const [, gradientType, firstValue, gradientValuesAll] = coarseGradientTokens,
                gradientValues = Array.from(gradientValuesAll.matchAll(regToken));

            switch (gradientType) {
            case "linear-gradient":
                return [{
                    type: gradientType,
                    // It looks like chrome does something wonky with gradients
                    // at 180° but this takes care of that.
                    rotation: typeof firstValue === "undefined" ? Math.PI : (parseFloat(firstValue.substr(0, firstValue.length - 3)) / 180 * Math.PI) || 0,
                    steps: gradientValues.map(([, color, step]) => ({ color, step: parseInt(step) / 100 }))
                }];
            case "radial-gradient":
                return [{
                    type: gradientType,
                    steps: gradientValues.map(([, color, step]) => ({ color, step: parseInt(step) / 100 }))
                }];
            default:
                return [null];
            }
        },
        parseUnderline: (inputValue) => [inputValue.includes("underline")]
    };

    /**
     * @param {object} self
     * @param {object} styleData
     * @param {string} propName
     * @param {string} objName
     * @param {string} attribName
     * @param {string} conversion "parseInt", "boxShadow"
     */
    function setStyleFromCss(self, styleData, propName, objName, attribName, conversion) {
        const parser = styleParsers[conversion || "identity"];
        if (!styleData || !parser) {
            return;
        }

        const styleVal = propName === "background" ? styleData.getPropertyValue("background-image") === "none" ? styleData.getPropertyValue("background-color") : styleData.getPropertyValue("background-image") : styleData.getPropertyValue(propName);
        if (!styleVal) {
            return;
        }

        const parsed = parser(styleVal);
        if (!parsed) {
            return;
        }

        if (!Array.isArray(attribName)) {
            attribName = [attribName];
        }

        parsed.forEach((value, idx) => {
            if (attribName[idx]) {
                self.vars.styles[objName][attribName[idx]] = value;
            }
        });
    }

    /**
     * @param {*} self
     * @param {number} fontSize
     * @param {string} fontFamily
     * @param {string|number} fontWeight
     * @param {string} fontStyle
     * @param {string} text
     * @returns {TextMetrics}
     */
    function calculateTextMeasurement(self, { fontSize, fontFamily, fontWeight, fontStyle }, text) {
        const ctx = self.vars.canvasObj;

        ctx.font = `${fontStyle} ${fontWeight} ${fontSize * (window.devicePixelRatio || 1)}px ${fontFamily}`;
        return ctx.measureText(text);
    }

    /**
     * @param {*} self
     * @returns {{unit: TextMetrics, label: TextMetrics, value: TextMetrics, valueLineHeight: TextMetrics}}
     */
    function calculateTextMeasurements(self) {
        return {
            value: calculateTextMeasurement(self, self.vars.styles.value, self.vars.valueFormatted),
            valueLineHeight: calculateTextMeasurement(self, self.vars.styles.value, `-0${self.config["decimal-delimiter"]}123456789`),
            unit: calculateTextMeasurement(self, self.vars.styles.unit, self.vars.unitLocalized),
            label: calculateTextMeasurement(self, self.vars.styles.label, self.vars.labelLocalized)
        };
    }

    /**
     * Computes the normalized bounding box around a rotated gauge with the
     * specified slice angle.
     *
     * @param {number} rotation Rotation of the gauge
     * @param {number} sliceAngle angle of the cut out.
     * @returns {object}
     */
    function computeNormalizedGaugeBoundingBox(rotation, sliceAngle) {
        const rotationOffset = (2 * Math.PI - sliceAngle) / 2,
            rotationAngle1 = rotation + rotationOffset,
            rotationAngle2 = rotation + rotationOffset + sliceAngle;

        // Set of points to examine:
        // - End-Points of the partial circle.
        // - Intersections with axis
        const points = [
            { x: Math.cos(rotationAngle1), y: Math.sin(rotationAngle1), theta: rotationAngle1 },
            { x: Math.cos(rotationAngle2), y: Math.sin(rotationAngle2), theta: rotationAngle2 }
        ];

        // Compute intersetctions with axis
        for (let i = Math.ceil(2 * rotationAngle1 / Math.PI); i <= Math.floor(2 * rotationAngle2 / Math.PI); ++i) {
            const theta = i * Math.PI / 2;
            points.push({ x: Math.cos(theta), y: Math.sin(theta), theta });
        }

        // Compute bounding box of points
        return points.reduce((boundingBox, point) => ({
            top: Math.min(boundingBox.top, point.y),
            left: Math.min(boundingBox.left, point.x),
            bottom: Math.max(boundingBox.bottom, point.y),
            right: Math.max(boundingBox.right, point.x),
            centerX: boundingBox.centerX,
            centerY: boundingBox.centerY,
            width: boundingBox.width,
            height: boundingBox.height
        }), {
            top: 1,
            left: 1,
            bottom: -1,
            right: -1,
            centerX: 0,
            centerY: 0,
            width: 2,
            height: 2
        });
    }

    /**
     * Takes a normalized gauge bounding box and translates and scales it such
     * that it will fit exactly within the specified dimensions and is
     * centered.
     *
     * @param {object} boundingBox Normalized gauge bounding box computed by
     * @param {number} width Width to fit the gauge bounding box in.
     * @param {number} height Height to fit the gauge bounding box in.
     * @returns {{top: number, left: number, bottom: number, right: number, centerY: number, centerX: number, height: number, width: number, radius: number}}
     */
    function translateGaugeBoundingBox(boundingBox, width, height) {
        // Compute the size of the longest side of the bounding box.
        const longestSide = (() => {
            const scalerHeight = (boundingBox.bottom - boundingBox.top) / boundingBox.height,
                scalerWidth = (boundingBox.right - boundingBox.left) / boundingBox.width,
                ratioVertical = scalerWidth / scalerHeight,
                ratioHorizontal = scalerHeight / scalerWidth;

            if (boundingBox.right - boundingBox.left > boundingBox.bottom - boundingBox.top) {
                return Math.min(ratioVertical * height, width);
            }

            return Math.min(ratioHorizontal * width, height);
        })();

        // Transformations done on the bounding box:
        // 1. Move upper left corner of bounding box to (0, 0)
        // 2. Scale bounding box such that max(width, height) = 1 while keeping
        //    the aspect ratio.
        // 3. Scale bounding box to fit the target width/height.

        const computeSize = (X, offset, scaler) => (X - offset) / Math.max(boundingBox.right - boundingBox.left, boundingBox.bottom - boundingBox.top) * scaler;

        // Compute dimensions of the translated bounding box.
        const translatedWidth = computeSize(boundingBox.right, boundingBox.left, longestSide),
            translatedHeight = computeSize(boundingBox.bottom, boundingBox.top, longestSide);

        return {
            top: (height - translatedHeight) / 2,
            left: (width - translatedWidth) / 2,
            bottom: (height + translatedHeight) / 2,
            right: (width + translatedWidth) / 2,
            centerY: computeSize(boundingBox.centerY, boundingBox.top, longestSide) + (height - translatedHeight) / 2,
            centerX: computeSize(boundingBox.centerX, boundingBox.left, longestSide) + (width - translatedWidth) / 2,
            height: translatedHeight,
            width: translatedWidth,
            radius: computeSize(1, 0, longestSide)
        };
    }

    /**
     *
     * @param {*} self
     */
    function calculateIconData(self, width, height) {
        if (!self.vars.iconEl) {
            return {
                type: "none",
                height
            };
        } else if (self.vars.iconEl.nodeName === "IMG") {
            const imageRatio = self.vars.iconEl.width / self.vars.iconEl.height;

            return {
                type: "image",
                src: self.vars.iconEl.src,
                element: self.vars.iconEl,
                ready: self.vars.iconEl.complete,
                width: Math.min(imageRatio * height, width),
                height: Math.min(width / imageRatio, height),
                drawWidth: width,
                drawHeight: height
            };
        } else if (self.vars.iconEl.nodeName !== "DIV") {
            return {
                type: "invalid",
                drawWidth: width,
                drawHeight: height
            };
        }

        const computedStyles = getComputedStyle(self.vars.iconEl, ":before");
        if (!computedStyles) {
            return {
                type: "invalid",
                drawWidth: width,
                drawHeight: height
            };
        }

        return {
            type: "icon-font",
            str: computedStyles.content,
            fontFamily: computedStyles.fontFamily,
            measurement: calculateTextMeasurement(self, { fontSize: height, fontFamily: computedStyles.fontFamily, fontWeight: 400, fontStyle: "normal" }, computedStyles.content),
            drawWidth: width,
            drawHeight: height
        };
    }

    /**
     * @param {*} self
     */
    function invalidateTextMeasurements(self) {
        if (!self.vars.cache) {
            self.vars.cache = {};
        }
        self.vars.cache.textMeasurement = null;
    }

    /**
     * Resets the style cache flag, causing all styles to be reevaluated on the
     * next draw.
     *
     * @param {object} self
     */
    function invalidateStyles(self) {
        if (!self.vars.cache) {
            self.vars.cache = {};
        }
        self.vars.cache.hasStyles = false;
    }

    /**
     * Invalidates icon data.
     *
     * @param {*} self Reference to an instance of a radial gauge.
     */
    function invalidateIconData(self) {
        if (!self.vars.cache) {
            self.vars.cache = {};
        }
        self.vars.cache.iconData = null;
    }

    /**
     * Invalidates the cached gauge bounding box.
     *
     * @param {*} self
     */
    function invalidateGaugeBoundingBox(self) {
        if (!self.vars.cache) {
            self.vars.cache = {};
        }
        self.vars.cache.boundingBox = null;
    }

    /**
     * Invalidates cached fill styles.
     *
     * @param {*} self
     */
    function invalidateFillStyles(self) {
        if (!self.vars.cache) {
            self.vars.cache = {};
        }
        self.vars.cache.fillStyles = null;
    }

    /**
     * @param {*} self
     * @returns {boolean}
     */
    function haveDisplayPropertiesChanged(self) {
        if (!self.vars.cache) {
            return true;
        } else if (!self.vars.cache.display) {
            return true;
        }

        const cache = self.vars.cache.display,
            current = {
                devicePixelRatio: window.devicePixelRatio || 1,
                widgetWidth: self.element.clientWidth,
                widgetHeight: self.element.clientHeight
            };

        return Object.keys(current).some((key) => current[key] !== cache[key]);
    }

    /**
     * Checks if styles have been evaluated and can be used.
     *
     * @param {object} self
     */
    function haveStylesChanged(self) {
        if (!self.vars.cache) {
            return true;
        }

        return !self.vars.cache.hasStyles;
    }

    /**
     * @param {*} self
     */
    function checkCache(self) {
        if (!self.vars.cache) {
            self.vars.cache = {};
        }

        if (!self.vars.cache.textMeasurement) {
            self.vars.cache.textMeasurement = calculateTextMeasurements(self);
        }
    }

    /**
     * Gets the bounding box from cache. If no bounding box is cached or a
     * a parameter is chagned, the bounding box is recomputed.
     *
     * @param {*} self
     * @param {number} width
     * @param {number} height
     * @param {number} arcRotation
     * @param {number} arcAngle
     * @returns {{width: number, height: number, arcRotation: number, arcAngle: number, boundingBox: object}}
     */
    function getBoundingBox(self, width, height, arcRotation, arcAngle) {
        if (!self.vars.cache) {
            self.vars.cache = {};
        }

        if (
            !self.vars.cache.boundingBox ||
            self.vars.cache.boundingBox.width !== width ||
            self.vars.cache.boundingBox.height !== height ||
            self.vars.cache.boundingBox.arcRotation !== arcRotation ||
            self.vars.cache.boundingBox.arcAngle !== arcAngle
        ) {
            self.vars.cache.boundingBox = {
                width,
                height,
                arcRotation,
                arcAngle,
                boundingBox: translateGaugeBoundingBox(computeNormalizedGaugeBoundingBox(arcRotation, arcAngle), width, height)
            };
        }

        return self.vars.cache.boundingBox.boundingBox;
    }

    /**
     * Gets icon data from cache. If no icon data is cached or a parameter
     * has changed, the icon data is recomputed.
     *
     * @param {*} self
     * @param {number} width
     * @param {number} height
     * @returns {object}
     */
    function getIconData(self, width, height) {
        if (!self.vars.cache) {
            self.vars.cache = {};
        }

        if (
            !self.vars.cache.iconData ||
            self.vars.cache.iconData.drawWidth !== width ||
            self.vars.cache.iconData.drawHeight !== height
        ) {
            self.vars.cache.iconData = calculateIconData(self, width, height);
        }

        return self.vars.cache.iconData;
    }

    /**
     * Computes absolute text coordinates from canvas dimensions and relative
     * coordinates.
     *
     * @param {number} width Width of the canvas
     * @param {number} height Height of the canvas
     * @param {{x: number, y:number}} position
     * @param {TextMetrics} measurement
     * @param {number} [lineHeight]
     * @return {{x: number, y:number}}
     */
    function getTextPosition(width, height, position, measurement, lineHeight) {
        return {
            x: Math.floor((width - measurement.width) * position.x + 0.5),
            y: Math.floor(height * position.y + (lineHeight || measurement.actualBoundingBoxAscent) * (1 - position.y) + 0.5)
        };
    }

    /**
     * Computes gauge offset from canvas dimensions and relative coordinates.
     *
     * @param {number} width
     * @param {number} height
     * @param {{x: number, y:number}} position
     * @param {*} boundingBox
     * @return {{x: number, y:number}}
     */
    function getGaugeOffset(width, height, position, boundingBox) {
        return {
            x: (width - boundingBox.width) * (position.x - 0.5),
            y: (height - boundingBox.height) * (position.y - 0.5)
        };
    }

    /**
     *
     * @param {object} style
     * @returns {{x: number, y: number}}
     */
    function positionFromStyle(style) {
        return {
            x: style.offsetHorizontal,
            y: style.offsetVertical
        };
    }

    /**
     * Rotates coordinates of a box.
     *
     * @param {{x0: number, y0: number, x1: number, y1: number}} param0
     * @param {number} theta angle to rotate the box by (in rad).
     */
    function rotateBox({ x0, y0, x1, y1 }, theta) {
        // Correct for starting top-left.
        theta -= (135 / 180 * Math.PI);

        const centered = {
                x0: (x0 - x1) / 2, // x0 = x0 - (x0 + x1) / 2
                y0: (y0 - y1) / 2, // y0 = y0 - (y0 + y1) / 2
                x1: (x1 - x0) / 2, // x1 = x1 - (x1 + x0) / 2
                y1: (y1 - y0) / 2 // y1 = y1 - (y1 + y0) / 2
            },
            rotated = {
                x0: centered.x0 * Math.cos(theta) - centered.y0 * Math.sin(theta),
                y0: centered.x0 * Math.sin(theta) + centered.y0 * Math.cos(theta),
                x1: centered.x1 * Math.cos(theta) - centered.y1 * Math.sin(theta),
                y1: centered.x1 * Math.sin(theta) + centered.y1 * Math.cos(theta)
            };

        return {
            x0: rotated.x0 + (x1 + x0) / 2,
            y0: rotated.y0 + (y1 + y0) / 2,
            x1: rotated.x1 + (x1 + x0) / 2,
            y1: rotated.y1 + (y1 + y0) / 2
        };
    }

    /**
     * @param {CanvasRenderingContext2D} ctx Canvas context to generate the
     *  style for.
     * @param {number} x x-Position of the object on the canvas
     * @param {number} y y-Position of the object on the canvas
     * @param {number} width Width of the object to style
     * @param {number} height Height of the object to style
     * @param {*} style
     *
     * @returns {null|string|CanvasGradient}
     */
    function makeFillStyle(ctx, x, y, width, height, style) {
        if (typeof style === "string") {
            return style;
        } else if (style === null) {
            return null;
        }

        switch (style.type) {
        case "color":
            return style.color;
        case "linear-gradient": {
            const scaler = Math.max(1, ...style.steps.map(({ step }) => step)),
                gradientBox = rotateBox({ x0: x - width * (scaler - 1), y0: y - height * (scaler - 1), x1: x + width * scaler, y1: y + height * scaler }, style.rotation),
                gradient = ctx.createLinearGradient(gradientBox.x0, gradientBox.y0, gradientBox.x1, gradientBox.y1);

            style.steps.forEach(({ color, step }) => gradient.addColorStop(step / scaler, color));
            return gradient;
        }
        case "radial-gradient": {
            const scaler = Math.max(1, ...style.steps.map(({ step }) => step)),
                gradient = ctx.createRadialGradient(
                    x + width / 2,
                    y + height / 2,
                    0,
                    x + width / 2,
                    y + height / 2,
                    Math.max(width, height) * scaler / 2
                );

            style.steps.forEach(({ color, step }) => gradient.addColorStop(step / scaler, color));
            return gradient;
        }
        default:
            return null;
        }
    }

    /**
     * Generates a fill style and stores it in cache if it doesn't already
     * exist.
     *
     * @param {*} self
     * @param {*} styleName Name of the style to generate
     * @param {CanvasRenderingContext2D} ctx Canvas context to generate the
     *  style for.
     * @param {number} x x-Position of the object on the canvas
     * @param {number} y y-Position of the object on the canvas
     * @param {number} width Width of the object to style
     * @param {number} height Height of the object to style
     */
    function cacheFillStyle(self, styleName, ctx, x, y, width, height) {
        if (!self.vars.cache) {
            self.vars.cache = {};
        }

        if (!self.vars.cache.fillStyles) {
            self.vars.cache.fillStyles = {};
        }

        if (!self.vars.cache.fillStyles[styleName]) {
            self.vars.cache.fillStyles[styleName] = makeFillStyle(ctx, x, y, width, height, self.vars.styles[styleName].color);
        }
    }

    /**
     * Draws a text
     *
     * @param {CanvasRenderingContext2D} ctx Canvas context to draw the text
     *  on.
     * @param {number} width Width of the canvas
     * @param {number} height Height of the canvas
     * @param {number} fontSize
     * @param {string} fontFamily
     * @param {string|number} fontWeight
     * @param {string} fontStyle
     * @param {number} pixelRatio
     * @param {{x: number, y: number}} position
     * @param {string} text The text to draw.
     * @param {TextMetrics} measurement
     * @param {string} color
     * @param {number} [lineHeight]
     * @param {boolean} [underline]
     */
    function drawText(ctx, width, height, fontSize, fontFamily, fontWeight, fontStyle, pixelRatio, position, text, measurement, color, lineHeight, underline) {
        const textCoords = getTextPosition(width, height, position, measurement, lineHeight),
            underlineLineWidth = Math.max(1, Math.floor(fontSize * pixelRatio / 15)),
            // Offset =
            //   1x line width for extra distance +
            // 0.5x line width to properly align the center so we always draw whole pixels.
            underlineLineOffset = underlineLineWidth * 1.5;

        ctx.beginPath();
        ctx.shadowBlur = 0;
        ctx.fillStyle = color;
        ctx.font = `${fontStyle} ${fontWeight} ${fontSize * pixelRatio}px ${fontFamily}`;
        ctx.fillText(text, textCoords.x, textCoords.y);
        ctx.stroke();

        if (underline) {
            ctx.beginPath();
            ctx.strokeStyle = ctx.fillStyle;
            ctx.lineWidth = underlineLineWidth;
            ctx.moveTo(textCoords.x, textCoords.y + underlineLineOffset);
            ctx.lineTo(textCoords.x + measurement.width, textCoords.y + underlineLineOffset);
            ctx.stroke();
        }
    }

    /**
     * Draws the widget icon
     *
     * @param {CanvasRenderingContext2D} ctx Canvas context to draw the icon
     *  on.
     * @param {object} iconData
     * @param {number} width Width of the canvas
     * @param {number} height Height of the canvas
     * @param {{x: number, y: number}} position
     * @param {number} pixelRatio
     * @param {string} color
     */
    function drawIcon(ctx, iconData, width, height, position, pixelRatio, color) {
        if (iconData.type === "image") {
            ctx.drawImage(iconData.element, (width - iconData.width) * position.x, (height - iconData.height) * position.y, iconData.width, iconData.height);
        } else if (iconData.type === "icon-font") {
            const textCoords = getTextPosition(width, height, position, iconData.measurement);
            drawText(ctx, width, textCoords.x, textCoords.y, iconData.fontFamily, "normal", "normal", pixelRatio, position, iconData.str, iconData.measurement, color);
        }
    }

    /**
     * Applies shadow styles to a canvas context.
     *
     * @param {CanvasRenderingContext2D} ctx Canvas context
     * @param {object} styles
     * @param {number} pixelRatio
     */
    function applyShadowStyles(ctx, styles, pixelRatio) {
        ctx.shadowBlur = styles.shadowBlur * pixelRatio;
        ctx.shadowColor = styles.shadowColor;
        ctx.shadowOffsetX = styles.shadowOffsetX * pixelRatio;
        ctx.shadowOffsetY = styles.shadowOffsetY * pixelRatio;
    }

    /**
     * Draws the gauge.
     *
     * @param {object} self
     * @param {CanvasRenderingContext2D} ctx Canvas context
     * @param {number} multiplier A value between 0 and 1 (0 = 0%, 1 = 100%)
     * @param {number} width Width of the canvas
     * @param {number} height Height of the canvas
     * @param {number} rotation Rotation of the gauge arc
     * @param {number} arcAngleMax
     * @param {number} pixelRatio
     * @param {boolean} inverse
     */
    function drawGauge(self, ctx, multiplier, width, height, rotation, arcAngleMax, pixelRatio, inverse) {
        const rotationOffset = (2 * Math.PI - arcAngleMax) / 2,
            lineWidthBgArc = self.vars.styles.arcBackground.lineWidth * pixelRatio,
            shadowBlurBgArc = self.vars.styles.arcBackground.shadowBlur * pixelRatio,
            lineWidthFgArc = self.vars.styles.arcForeground.lineWidth * pixelRatio,
            shadowBlurFgArc = self.vars.styles.arcForeground.shadowBlur * pixelRatio,
            lineWidthValueLine = self.vars.styles.valueLine.lineWidth * pixelRatio,
            gaugeBoundingBox = getBoundingBox(self, width, height, rotation, arcAngleMax),
            radiusBgArc = gaugeBoundingBox.radius - Math.max(lineWidthBgArc, lineWidthFgArc) / 2 - shadowBlurBgArc,
            radiusFgArc = gaugeBoundingBox.radius - Math.max(lineWidthBgArc, lineWidthFgArc) / 2 - shadowBlurFgArc;

        // Calculate angle offset for foreground arc.
        const arcForegroundBorderWidth = (lineWidthBgArc - lineWidthFgArc),
            arcForegroundRotOffset = Math.max(0, Math.atan(arcForegroundBorderWidth / radiusFgArc)),
            arcBackgroundRotOffset = Math.max(0, Math.atan(-arcForegroundBorderWidth / radiusFgArc)),
            gaugeOffset = getGaugeOffset(width, height, positionFromStyle(self.vars.styles.arcForeground), gaugeBoundingBox);

        // Do not allow an invalid radius
        if (radiusBgArc < 0 || radiusFgArc < 0) {
            return;
        }

        checkCache(self);
        cacheFillStyle(
            self, "arcBackground", ctx,
            gaugeBoundingBox.left + gaugeOffset.x,
            gaugeBoundingBox.top + gaugeOffset.y,
            gaugeBoundingBox.right - gaugeBoundingBox.left,
            gaugeBoundingBox.bottom - gaugeBoundingBox.top
        );
        cacheFillStyle(
            self, "arcForeground", ctx,
            gaugeBoundingBox.left + gaugeOffset.x,
            gaugeBoundingBox.top + gaugeOffset.y,
            gaugeBoundingBox.right - gaugeBoundingBox.left,
            gaugeBoundingBox.bottom - gaugeBoundingBox.top
        );
        ctx.clearRect(0, 0, width, height);

        // BACKGROUND LINE
        ctx.beginPath();
        ctx.lineWidth = lineWidthBgArc;
        ctx.strokeStyle = self.vars.cache.fillStyles.arcBackground;
        applyShadowStyles(ctx, self.vars.styles.arcBackground, pixelRatio);
        ctx.arc(
            gaugeBoundingBox.centerX + gaugeOffset.x,
            gaugeBoundingBox.centerY + gaugeOffset.y,
            radiusBgArc,
            rotation + rotationOffset + arcBackgroundRotOffset / 2,
            arcAngleMax + rotation + rotationOffset - arcBackgroundRotOffset / 2
        );
        ctx.stroke();

        // FILLED LINE
        ctx.beginPath();
        const arcAngleStart = !inverse ? rotation + rotationOffset + arcForegroundRotOffset / 2 : (arcAngleMax - arcForegroundRotOffset) * (1 - multiplier) + rotation + rotationOffset + arcForegroundRotOffset / 2,
            arcAngleEnd = !inverse ? (arcAngleMax - arcForegroundRotOffset) * multiplier + rotation + rotationOffset + arcForegroundRotOffset / 2 : rotation - rotationOffset + arcForegroundRotOffset / 2;
        ctx.lineWidth = lineWidthFgArc;
        ctx.strokeStyle = self.vars.cache.fillStyles.arcForeground;
        applyShadowStyles(ctx, self.vars.styles.arcForeground, pixelRatio);
        ctx.arc(
            gaugeBoundingBox.centerX + gaugeOffset.x,
            gaugeBoundingBox.centerY + gaugeOffset.y,
            radiusFgArc,
            arcAngleStart,
            arcAngleEnd
        );
        ctx.stroke();

        // ICON
        drawIcon(ctx, getIconData(self, width * self.vars.styles.icon.width, height * self.vars.styles.icon.height), width, height, positionFromStyle(self.vars.styles.icon), pixelRatio, self.vars.styles.iconClass.color);

        const valueStyles = self.vars.styles.value,
            valueTextMeasurement = self.vars.cache.textMeasurement.value,
            valueTextLineHeight = self.vars.cache.textMeasurement.valueLineHeight.actualBoundingBoxAscent;

        // VALUE LINE
        ctx.beginPath();
        const valueLineCoords = getTextPosition(width, height, positionFromStyle(self.vars.styles.value), valueTextMeasurement, valueTextLineHeight),
            valueLinePoints = {
                x0: valueLineCoords.x,
                y0: valueLineCoords.y + Math.floor(valueLineCoords.y + valueTextMeasurement.actualBoundingBoxAscent / 4) + lineWidthValueLine / 2,
                x1: valueLineCoords.x + valueTextMeasurement.width,
                y1: valueLineCoords.y + Math.floor(valueLineCoords.y + valueTextMeasurement.actualBoundingBoxAscent / 4) + lineWidthValueLine / 2
            };
        ctx.lineWidth = lineWidthValueLine;
        ctx.strokeStyle = makeFillStyle(
            ctx,
            valueLinePoints.x0,
            valueLinePoints.y0,
            valueLinePoints.x1 - valueLinePoints.x0,
            valueLinePoints.y1 - valueLinePoints.y0,
            self.vars.styles.valueLine.color
        );
        applyShadowStyles(ctx, self.vars.styles.valueLine, pixelRatio);
        ctx.moveTo(valueLinePoints.x0, valueLinePoints.y0);
        ctx.lineTo(valueLinePoints.x1, valueLinePoints.y1);
        ctx.stroke();

        // VALUE
        drawText(ctx, width, height, valueStyles.fontSize, valueStyles.fontFamily, valueStyles.fontWeight, valueStyles.fontStyle, pixelRatio, positionFromStyle(self.vars.styles.value), self.vars.valueFormatted, valueTextMeasurement, valueStyles.color, valueTextLineHeight, valueStyles.textUnderline);

        // UNIT
        if (self.vars.unitLocalized) {
            const unitStyles = self.vars.styles.unit,
                unitTextMeasurement = self.vars.cache.textMeasurement.unit;

            drawText(ctx, width, height, unitStyles.fontSize, unitStyles.fontFamily, unitStyles.fontWeight, unitStyles.fontStyle, pixelRatio, positionFromStyle(self.vars.styles.unit), self.vars.unitLocalized, unitTextMeasurement, unitStyles.color, null, unitStyles.textUnderline);
        }

        // Label
        if (self.vars.labelLocalized) {
            const labelStyles = self.vars.styles.label,
                labelTextMeasurement = self.vars.cache.textMeasurement.label;

            drawText(ctx, width, height, labelStyles.fontSize, labelStyles.fontFamily, labelStyles.fontWeight, labelStyles.fontStyle, pixelRatio, positionFromStyle(self.vars.styles.label), self.vars.labelLocalized, labelTextMeasurement, labelStyles.color, null, labelStyles.textUnderline);
        }
    }

    /**
     * Updates the canvas elements resolution to fit the widget dimensions.
     *
     * @param {*} self
     */
    function updateCanvasResolution(self) {
        const { clientWidth, clientHeight } = self.element,
            devicePixelRatio = window.devicePixelRatio || 1;

        if (self.vars.canvasEl.width !== self.vars.canvasEl.clientWidth * devicePixelRatio) {
            self.vars.canvasEl.width = self.vars.canvasEl.clientWidth * devicePixelRatio;
        }

        if (self.vars.canvasEl.height !== self.vars.canvasEl.clientHeight * devicePixelRatio) {
            self.vars.canvasEl.height = self.vars.canvasEl.clientHeight * devicePixelRatio;
        }

        self.vars.cache.display = {
            devicePixelRatio: devicePixelRatio,
            widgetWidth: clientWidth,
            widgetHeight: clientHeight
        };
    }

    /**
     * @param {*} self
     * @returns {boolean}
     */
    function isReady(self) {
        return !!self.vars.canvasObj;
    }

    /**
     * @param {*} self
     * @returns {?number}
     */
    function startDrawing(self) {
        if (self.vars.animationFrameToken) {
            return null;
        }

        return shmi.raf((timestamp) => {
            if (!isReady(self)) {
                return;
            }

            const devicePixelRatio = window.devicePixelRatio || 1,
                { min, max } = self.vars.valueSettings,
                canvas = self.vars.canvasObj;

            if (self.vars.animationTimestamp === null || self.vars.animationValueStep === null || self.vars.animationValueStep === 0) {
                self.vars.animationValue = self.vars.value;
            } else {
                const valueDiff = self.vars.animationValueStep * (timestamp - self.vars.animationTimestamp);
                self.vars.hasChanged = true;
                if (Math.abs(Math.abs(self.vars.animationValue) - Math.abs(self.vars.value)) < Math.abs(valueDiff)) {
                    self.vars.animationValue = self.vars.value;
                    self.vars.animationValueStep = null;
                } else {
                    self.vars.animationValue = self.vars.animationValue + valueDiff;
                }
            }

            self.vars.animationTimestamp = timestamp;

            if (haveDisplayPropertiesChanged(self)) {
                invalidateTextMeasurements(self);
                invalidateGaugeBoundingBox(self);
                invalidateFillStyles(self);
                if (haveStylesChanged(self)) {
                    self.setupStylesFromCss();
                }

                updateCanvasResolution(self);

                self.vars.hasChanged = true;
            } else if (haveStylesChanged(self)) {
                invalidateFillStyles(self);
                self.setupStylesFromCss();
                self.vars.hasChanged = true;
            } else if (self.vars.forceRedraw) {
                self.vars.hasChanged = true;
                self.vars.forceRedraw = false;
            }

            // Safety net: when a user sets invalid min/max values manually the circle would otherwise be overdrawn
            const multiplier = Math.min(1, Math.max((self.vars.animationValue - min) / (max - min), 0));

            if (self.vars.elementIsInViewport && self.vars.hasChanged) {
                const fillPercent = self.config["arc-fill-inverse"] ? 1 - multiplier : multiplier,
                    arcAngle = 2 * Math.PI * (1 - self.vars.styles.slice.width);

                drawGauge(self, canvas, fillPercent, self.vars.canvasEl.width, self.vars.canvasEl.height, self.vars.styles.arcForeground.rotation, arcAngle, devicePixelRatio, self.config["arc-fill-reverse"]);

                self.vars.hasChanged = false;
            }

            self.vars.animationFrameToken = null;
            self.vars.animationFrameToken = startDrawing(self);
        });
    }

    /**
     * @param {*} self
     */
    function stopDrawing(self) {
        if (self.vars.animationFrameToken) {
            shmi.caf(self.vars.animationFrameToken);
            self.vars.animationFrameToken = null;
        }
    }

    /**
     * @param {*} self
     * @param {number} value
     */
    function startAnimation(self, value, animationDuration = 250) {
        if (animationDuration > 0) {
            self.vars.animationValueStep = (value - self.vars.animationValue) / animationDuration;
        } else {
            self.vars.animationValueStep = (value - self.vars.animationValue);
        }
    }

    /**
     * Adapts a DOM event listener to the visuals listener interface.
     *
     * @param {Element} domElement Element to attach the event listener to.
     * @param {string} eventName Name of the event to listen for.
     * @param {function} eventHandler Event handler function.
     *
     * @returns {{enable: function, disable: function}}
     */
    function makeDomEventListener(domElement, eventName, eventHandler) {
        return {
            enable: domElement.addEventListener.bind(domElement, eventName, eventHandler),
            disable: domElement.removeEventListener.bind(domElement, eventName, eventHandler)
        };
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: null,
            valueFormatted: null,
            active: false,
            hasChanged: false,
            forceRedraw: false,
            cache: {},
            valueSettings: null,

            // DOM Elements
            canvasEl: null,
            styleHolderEl: null,
            iconEl: null,

            canvasObj: null,
            intersectionObserver: null,
            mutationObserver: null,
            elementIsInViewport: null,
            unit: null,
            unitLocalized: null,
            label: null,
            labelLocalized: null,
            animationFrameToken: null,
            animationValue: 0,
            animationValueStep: null,
            animationTimestamp: null,

            // Default styles if not overridden by instance config
            // Shadow blur can only be overridden via CSS (see below for details)
            styles: {
                arcBackground: {
                    lineWidth: 5,
                    color: '#dddddd',
                    shadowBlur: 0
                },
                arcForeground: {
                    lineWidth: 5,
                    offsetVertical: 0.5,
                    offsetHorizontal: 0.5,
                    rotation: Math.PI / 2,
                    shadowBlur: 0,
                    shadowColor: 'rgba(180, 200, 59, 1)',
                    color: 'rgba(180, 200, 59, 1)'
                },
                slice: {
                    width: (360 - 84) / 360
                },
                valueLine: {
                    lineWidth: 1,
                    shadowBlur: 0,
                    shadowColor: 'rgba(60, 60, 60, 0.1)',
                    color: 'rgba(60, 60, 60, 0.1)'
                },
                value: {
                    offsetVertical: 0.5,
                    offsetHorizontal: 0.5,
                    fontSize: 36,
                    fontFamily: "RobotoBold",
                    color: '#7c7c7c'
                },
                label: {
                    offsetVertical: 0.95,
                    offsetHorizontal: 0.5,
                    fontSize: 14,
                    fontFamily: "RobotoLight",
                    color: '#7c7c7c'
                },
                unit: {
                    offsetVertical: 0.32,
                    offsetHorizontal: 0.5,
                    fontSize: 18,
                    fontFamily: "RobotoLight",
                    color: '#7c7c7c'
                },
                icon: {
                    width: 1/6,
                    height: 1/6,
                    offsetVertical: 0.1,
                    offsetHorizontal: 0.5
                },
                iconClass: {
                    color: "black"
                }
            }
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.canvasEl = shmi.getUiElement('gauge-canvas', self.element);

                if (!self.vars.canvasEl) {
                    self.log('Canvas element is missing!', 3);
                    return;
                }

                // Read style config from hidden HTML element
                self.vars.styleHolderEl = shmi.getUiElement('style-holder', self.element);
                if (!self.vars.styleHolderEl) {
                    self.log('Style Holder element is missing!', 3);
                    return;
                }

                if (!self.config['show-icon']) {
                    // Nothing to do.
                } else if (self.config['icon-src']) {
                    const iconEl = document.createElement('img');
                    iconEl.src = self.config['icon-src'];
                    self.vars.iconEl = iconEl;
                    self.vars.listeners.push(makeDomEventListener(iconEl, "load", () => {
                        invalidateIconData(self);
                        self.vars.forceRedraw = true;
                    }));
                    self.vars.styleHolderEl.appendChild(iconEl);
                } else if (self.config['icon-class']) {
                    const iconEl = document.createElement('div');
                    ["icon"].concat(
                        self.config['icon-class'].split(" ")
                    ).forEach(
                        (name) => shmi.addClass(iconEl, name)
                    );
                    self.vars.iconEl = iconEl;
                    self.vars.styleHolderEl.appendChild(iconEl);
                }

                // Read styles from CSS and store for drawing.
                self.setupStylesFromCss();

                self.vars.canvasObj = self.vars.canvasEl.getContext('2d');

                self.vars.intersectionObserver = new IntersectionObserver((entries) => {
                    if (entries.length > 1) {
                        entries.sort((lhs, rhs) => rhs.time - lhs.time);
                    }
                    self.vars.elementIsInViewport = entries[0].isIntersecting;
                });

                self.vars.mutationObserver = new MutationObserver((records) => invalidateStyles(self));

                self.imports.nv.initValueSettings(self);

                // Unit
                if (typeof self.config['unit-text'] === "string") {
                    self.vars.unit = self.config['unit-text'];
                    self.vars.unitLocalized = shmi.localize(self.config['unit-text']);
                } else {
                    self.vars.unitLocalized = '';
                }

                // Label
                if (typeof self.config.label === "string") {
                    self.vars.label = self.config.label;
                    self.vars.labelLocalized = shmi.localize(self.config.label);
                } else {
                    self.vars.labelLocalized = '';
                }

                // Initialize value with default
                self.vars.valueFormatted = shmi.localize(self.config["default-value"]);

                // Resize canvas to fit widget
                updateCanvasResolution(self);

                // Paint empty gauge
                const arcAngle = 2 * Math.PI * (1 - self.vars.styles.slice.width);
                drawGauge(self, self.vars.canvasObj, 0, self.vars.canvasEl.width, self.vars.canvasEl.height, self.vars.styles.arcForeground.rotation, arcAngle, window.devicePixelRatio || 1);
            },

            /* called when control is enabled */
            onEnable: function() {
                const self = this;

                if (!isReady(self)) {
                    return;
                }

                self.vars.intersectionObserver.observe(self.vars.canvasEl);
                self.vars.mutationObserver.observe(self.element, { attributes: true, attributeFilter: ['style', 'class'] });

                // Register resize handler. Workaround for detecting style
                // changes. Clears cache on every `resize` event, causing
                // styles to be reevaluated.
                self.vars.listeners.push(makeDomEventListener(window, "resize", () => {
                    // Clearing the cache causes `haveDisplayPropertiesChanged` to
                    // return `true`, which in turn causes the CSS to be reevaluated.
                    self.vars.cache = null;
                }));

                checkCache(self);
                startDrawing(self);

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                self.vars.listeners.forEach((l) => l.enable());

                self.log('Enabled', 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                if (!isReady(self)) {
                    return;
                }

                self.vars.listeners.forEach((l) => l.disable());

                stopDrawing(self);
                self.vars.intersectionObserver.disconnect(self.vars.canvasEl);
                self.vars.mutationObserver.disconnect(self.element);

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                self.log('Disabled', 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                if (!isReady(self)) {
                    return;
                }

                shmi.addClass(this.element, 'locked');

                self.log('Locked', 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                if (!isReady(self)) {
                    return;
                }

                shmi.removeClass(self.element, 'locked');

                self.log('Unlocked', 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this;

                self.vars.value = value;
                self.vars.valueFormatted = self.imports.nv.formatOutput(self.vars.value, self);

                invalidateTextMeasurements(self);
                startAnimation(self, value, parseInt(self.config["animation-duration"]) || 0);

                self.vars.hasChanged = true;
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                const self = this;

                return self.vars.value;
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision) {
                const self = this;

                self.imports.nv.setProperties(self, arguments);

                self.vars.min = min;
                self.vars.max = max;

                invalidateTextMeasurements(self);
                self.vars.hasChanged = true;
            },

            setUnitText: function(unitText) {
                const self = this;
                if (self.config['auto-unit-text']) {
                    self.vars.unit = unitText;
                    self.vars.unitLocalized = shmi.localize(unitText);
                }
            },

            setLabel: function(labelText) {
                const self = this;
                if (self.config['auto-label']) {
                    self.vars.label = labelText;
                    self.vars.labelLocalized = shmi.localize(labelText);
                }
            },

            log: function(msg, level) {
                shmi.log('[IQ:iq-radial-gauge] ' + msg, level);
            },

            /**
             * Reads instance styles from CSS as computed style and sets them in this object in self.vars.styles...
             */
            setupStylesFromCss: function() {
                const self = this;

                cssStyleMappings.forEach(function(mapping) {
                    const readEl = shmi.getUiElement(mapping["data-ui"], self.vars.styleHolderEl);
                    if (readEl) {
                        const styleData = getComputedStyle(readEl);
                        if (styleData) {
                            mapping.styleConfig.forEach(function(styleConfig) {
                                setStyleFromCss(self, styleData, styleConfig.propName, mapping.objName, styleConfig.attribName, styleConfig.conversion);
                            });
                        }
                    }
                });

                if (!self.vars.cache) {
                    self.vars.cache = {};
                }
                self.vars.cache.hasStyles = true;
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals iq-recipe-list control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-recipe-list",
 *     "name": null,
 *     "template": "default/iq-recipe-list",
 *     "recipe-template-id": null
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * show-results {string}: When to display the results dropdown.
 *  * `never` Never show results.
 *  * `always` Always show results if there are more than 0.
 *  * `two-or-more` Show results if there are two or more.
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-recipe-list", // control name in camel-case
        uiType = "iq-recipe-list", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    const defConfig = {
        "class-name": className,
        "name": null,
        "template": "default/iq-recipe-list",
        "label": uiType,
        "recipe-template-id": null
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    function eventProxy(self, event) {
        self.fire(event.type, event.detail);
    }

    function doSetValue(val, self) {
        const selObj = {
            selRowIndex: [],
            selRows: [],
            type: -1
        };

        for (let i = 0; i < self.vars.dataGrid.getRowCount(); i++) {
            const rowData = self.vars.dataGrid.getRowData(i);
            if (rowData[0].value === parseInt(val)) {
                selObj.selRowIndex = [rowData[0].value];
                selObj.selRows = [i];
                selObj.type = 2;
            }
        }
        self.vars.ct.setSelectedRows(selObj);
    }

    function checkIfSetValue(val, self) {
        if (self.vars.dataGrid && self.vars.ct) {
            if (self.vars.dataGrid.tasksRunning > 0) {
                const tok = (shmi.listen("datagrid-ready", function() {
                    tok.unlisten();
                    doSetValue(val, self);
                }.bind(val, self)));
            } else {
                doSetValue(val, self);
            }
        }
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            domChild: null,
            dataGridId: null,
            dataGrid: null,
            dataGridSubscriptionId: null,
            ct: null,
            eventProxy: null,
            tokens: [],
            recipeTableConfig: {
                "label": "${recipe-list.title}",
                "table": "",
                "name": "",
                "class-name": "complex-table2",
                "field-datagrid-col-map": {
                    "id": 0,
                    "name": 1
                },
                "select-mode": "SINGLE",
                "default-field-control-map": {
                    "id": {
                        "ui-type": "iq-label",
                        "config": {
                            "class-name": "iq-label",
                            "template": "default/iq-label.iq-variant-01",
                            "options": [],
                            "pattern": "<%= VALUE %>",
                            "value-as-tooltip": true
                        }
                    },
                    "name": {
                        "ui-type": "iq-label",
                        "config": {
                            "class-name": "iq-label",
                            "template": "default/iq-label.iq-variant-01",
                            "options": [],
                            "pattern": null,
                            "value-as-tooltip": true
                        }
                    }
                },
                "default-field-headers": {
                    "id": "${recipe-list.table-header.id}",
                    "name": "${recipe-list.table-header.name}"
                },
                "_comment": "expr is passed to expr parameter array of shmi.visuals.core.DataGridManager.setFilter",
                "filters": [],
                "default-layout": {
                    "class-name": "layout-std",
                    "_comment": "default == no additional css layout class",
                    "column-org": {
                        "col1": {
                            "fields": ["id"],
                            "column-width": "15%"
                        },
                        "col2": {
                            "fields": ["name"],
                            "column-width": "85%"
                        }
                    }
                },
                "sortable-fields": [
                    "name",
                    "id"
                ],
                "quicksearch": {
                    "enable": true,
                    "remember": true,
                    "fields": ["name"]
                },
                "text-mode": "SINGLELINE",
                "show-nof-rows": true,
                "v-scroll-options": ["SCROLLBAR", "V_SWIPE"]
            }
        },
        /* imports added at runtime */
        imports: {
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: [
            "select"
        ],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                if (typeof (this.config["recipe-template-id"]) !== "number") {
                    this.element.textContent = "Invalid or no recipe template id given. This control will not work. Please select a valid recipe template id.";
                    fLog("Invalid or no recipe template id given:", this.config["recipe-template-id"]);
                    return;
                }

                this.vars.domChild = shmi.getUiElement("table-container", this.element);
                if (this.vars.domChild === null) {
                    this.element.textContent = "One or more required DOM elements are missing from the template. This control will not work. Please select a compatible template.";
                    fLog("Some DOM elements are missing from the template.");
                    return;
                }

                do {
                    this.vars.dataGridId = className + "-" + getRandomId();
                } while (this.imports.dgm.getGrid(this.vars.dataGridId) !== null);

                this.imports.dgm.grids[this.vars.dataGridId] = this.vars.dataGrid = new shmi.visuals.core.DataGridRecipe(this.vars.dataGridId, this.config["recipe-template-id"]);
                this.vars.dataGrid.init();
                this.vars.dataGrid.sort(1, "ASC");
            },
            rebuildTable: function() {
                const tableConfig = shmi.cloneObject(this.vars.recipeTableConfig);

                tableConfig.table = this.vars.dataGridId;
                tableConfig.name = this.vars.dataGridId;
                this.vars.dataGrid.sort(1, "ASC");

                if (this.vars.eventProxy) {
                    this.vars.eventProxy.unlisten();
                    this.vars.eventProxy = null;
                }

                if (this.vars.ct) shmi.deleteControl(this.vars.ct);

                this.vars.ct = shmi.createControl("complex-table2", this.vars.domChild, tableConfig);
                if (this.vars.ct === null) {
                    this.element.textContent = "Unable to create table control for recipes. This control will not work.";
                    fLog("Unable to create table control for recipes.");
                } else {
                    this.vars.eventProxy = this.vars.ct.listen("select", eventProxy.bind(null, this));
                }
            },
            /**
             * Returns the contents of the table selection
             *
             * @returns {object} selected recipe info
             */
            getValue: function() {
                if (this.vars.ct) {
                    const selection = this.vars.ct.getSelectionRowData(this.vars.ct.getSelectedRows());
                    if (selection.length > 0) {
                        return {
                            name: selection[0][1].value,
                            recipe_id: isNaN(selection[0][0].value) ? null : selection[0][0].value,
                            template_id: this.config["recipe-template-id"],
                            values_set: (selection[0][3].value === 1)
                        };
                    }
                }
                return null;
            },
            /**
             * Sets the value of the table selection.
             *
             * @param {string} val Value to set
             */
            setValue: function(val) {
                const self = this;
                checkIfSetValue(val, self);
            },
            refreshGrid: function() {
                if (this.vars.dataGrid) {
                    this.vars.dataGrid.refresh();
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                if (this.vars.dataGrid) {
                    this.rebuildTable();
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                if (this.vars.ct) {
                    this.vars.ct.disable();
                }

                if (this.vars.eventProxy) {
                    this.vars.eventProxy.unlisten();
                    this.vars.eventProxy = null;
                }
            },
            onDelete: function() {
                if (this.vars.ct) {
                    shmi.deleteControl(this.vars.ct);
                }

                if (this.vars.dataGrid) {
                    delete this.imports.dgm.grids[this.vars.dataGridId];
                }
                this.vars.tokens.forEach((tok) => {
                    tok.unlisten();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                if (this.vars.ct) {
                    this.vars.ct.lock();
                }
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                if (this.vars.ct) {
                    this.vars.ct.unlock();
                }
            },
            getSelectedRecipe: function() {
                if (this.vars.ct) {
                    return this.vars.ct.getSelectionRowData(this.vars.ct.getSelectedRows());
                }
                return null;
            },
            getTemplate: function() {
                return this.config["recipe-template-id"];
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * IQ Responsive Menu
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-responsive-menu",
 *     "name": null,
 *     "template": "default/iq-responsive-menu"
 * }
 *
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 0.2
 */
(function() {
    'use strict';

    //variables for reference in control definition
    const className = "IqResponsiveMenu", //control name in camel-case
        uiType = "iq-responsive-menu", //control keyword (data-ui)
        isContainer = false;

    //example - default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": `default/${uiType}`,
        "menu": [],
        "mobile-layouts": [],
        "overlay-style": {
            "background": "transparent",
            "zIndex": 1
        }
    };

    /* Example for config.menu structure */
    /**
    const exampleMenu = [
        {
            "label": "My Category",
            "items": [
                {
                    "label": "My Sub-Category",
                    "items": [
                        {
                            "label": "Menu Entry",
                            "action": [
                                {
                                    "name": "test-action",
                                    "params": [1, 2, 3]
                                }
                            ],
                            "icon-src": "pics/system/icons/debug_icon.png" // image icon
                        },
                        {
                            "label": "Menu Entry",
                            "action": [
                                {
                                    "name": "test-action",
                                    "params": [1, 2, 3]
                                }
                            ],
                            "icon-class": "icon icon-add" // icon font class
                        }
                    ]
                },
                {
                    "label": "Menu Entry",
                    "action": [
                        {
                            "name": "test-action",
                            "params": [1, 2, 3]
                        },
                        {
                            "name": "test-action",
                            "params": [4, 5, 6]
                        }
                    ]
                }
            ]
        },
        {
            "label": "Menu Entry",
            "action": [
                {
                    "name": "test-action",
                    "params": [1, 2, 3]
                }
            ]
        },
        {
            "label": "Menu Entry",
            "action": [
                {
                    "name": "test-action",
                    "params": [4, 5, 6]
                }
            ]
        }
    ];
    */

    //setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false;
    const logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    const fLog = logger.fLog,
        log = logger.log;

    const DEFAULT_ICON = "pics/system/icons/placeholder.svg";

    //declare private functions - START
    /**
     * Creates a compound listener acting both as touch- and mouse-listener.
     * Creates a touch- and mouse-listener internally and enables/disables them
     * both at the same time.
     *
     * @param {HTMLElement} element Element to attach the listener to.
     * @param {object} handlers Object specifying the corresponding handlers
     *  called by the touch- and mouse-listeners.
     * @returns {object} Object with a listener-like interface.
     */
    function createListener(element, handlers) {
        const io = shmi.requires("visuals.io"),
            ml = new io.MouseListener(element, handlers),
            tl = new io.TouchListener(element, handlers);
        return {
            enable: function() {
                ml.enable();
                tl.enable();
            },
            disable: function() {
                ml.disable();
                tl.disable();
            }
        };
    }

    /**
     * Creates a new html element with css classes and data-ui set.
     *
     * @param {string|string[]} cssClasses CSS class or classes to assign to
     *  the new element.
     * @param {?string} setUiType ui type to set.
     * @param {string} [elementType] Type of the html element. Defaults to
     *  "DIV".
     * @returns {HTMLElement}
     */
    function makeElement(cssClasses, setUiType, elementType = "DIV") {
        const elem = document.createElement(elementType || "DIV");
        if (typeof cssClasses === "string") {
            shmi.addClass(elem, cssClasses);
        } else if (Array.isArray(cssClasses)) {
            cssClasses.forEach(function(cc) {
                if (typeof cc === "string") {
                    shmi.addClass(elem, cc);
                }
            });
        }
        if (typeof setUiType === "string") {
            elem.setAttribute("data-ui", setUiType);
        }
        return elem;
    }

    /**
     * Marks the given layer as "selected". Successive layers are unmarked as
     * "selected".
     *
     * @param {*} self Reference to the widget.
     * @param {?HTMLElement} element Element to assign the "selected" CSS class
     *  to. If set to `null`, in addition to the successive layers being
     *  unmarked as "selected", the given one is unmarked as well.
     * @param {number} layer Layer index
     */
    function setSelected(self, element, layer) {
        if (self.vars.selected[layer] !== element) {
            if (self.vars.selected[layer] !== null) {
                shmi.removeClass(self.vars.selected[layer], "selected");
            }
            self.vars.selected[layer] = element;
            if (element !== null) {
                shmi.addClass(element, "selected");
            }
        }

        self.vars.selected.forEach(function(e, i) {
            if (e && (i > layer)) {
                shmi.removeClass(e, "selected");
                self.vars.selected[i] = null;
            }
        });
    }

    /**
     * Creates a menu section according to the provided configuration. Child
     * elements are created recursively.
     *
     * @param {*} self Reference to the widget.
     * @param {object} sectionConfig Configuration of the menu section.
     * @param {number} [layer] Number of the layer the section is located on.
     * @returns {object} Descriptor of the new menu section.
     */
    function makeMenuSection(self, sectionConfig, layer = 0) {
        const baseElement = makeElement("menu-group menu-element", "menu-group", "LI"),
            groupData = makeElement("group-data"),
            iconWrapper = makeElement("icon", "icon"),
            img = makeElement(null, null, "IMG"),
            label = makeElement("menu-group-label", "menu-group-label"),
            triangle = makeElement("triangle"),
            childList = makeElement(`menu-layer-${layer + 1} menu-layer-${layer}-${layer + 1} menu-layer-${layer + 1}-${layer + 2} accent-border`, `menu-layer-${layer + 1}`, "UL");

        const children = sectionConfig.items.map((child) => makeMenuElement(self, child, layer + 1)).filter((child) => !!child);

        // Piece together DOM elements
        iconWrapper.appendChild(img);
        groupData.appendChild(iconWrapper);
        groupData.appendChild(label);
        groupData.appendChild(triangle);

        children.forEach(({ elements: { base } }) => childList.appendChild(base));

        baseElement.appendChild(groupData);
        baseElement.appendChild(childList);

        const listener = createListener(groupData, {
            onClick: function onClick() {
                const crumb = self.vars.crumbs[`layer${layer}`];
                if (shmi.hasClass(baseElement, "selected")) {
                    setLayer(self, layer);
                    setSelected(self, null, layer);
                    if (layer === 0) {
                        removeOverlay(self);
                    }
                } else {
                    setLayer(self, layer + 1);
                    setSelected(self, baseElement, layer);
                    crumb.textContent = shmi.localize(sectionConfig.label);
                    if (layer === 0) {
                        createOverlay(self);
                    }
                }
            }
        });

        // Setup DOM elements
        label.textContent = shmi.localize(sectionConfig.label);

        if (sectionConfig["icon-src"]) {
            img.setAttribute("src", sectionConfig["icon-src"]);
        } else {
            img.setAttribute("src", DEFAULT_ICON);
        }

        if (sectionConfig["icon-mode"] === "no-icon") {
            baseElement.classList.add("no-icon");
        } else if (sectionConfig["icon-mode"] === "hidden") {
            baseElement.classList.add("hidden-icon");
        }

        if (layer === 0) {
            baseElement.classList.add("accent-border");
        }

        return {
            type: "section",
            config: {
                label: sectionConfig["label"] || null,
                iconSrc: sectionConfig["icon-src"] || null,
                iconMode: sectionConfig["icon-mode"] || "show",
                access: shmi.cloneObject(sectionConfig.access)
            },
            listeners: [listener],
            elements: {
                base: baseElement,
                iconImage: img,
                label: label,
                childList: childList
            },
            children: children
        };
    }

    /**
     * Creates a menu entry according to the provided configuration.
     *
     * @param {*} self Reference to the widget.
     * @param {object} entryConfig Configuration of the menu entry.
     * @param {number} [layer] Number of the layer the entry is located on.
     * @returns {object} Descriptor of the new menu entry.
     */
    function makeMenuEntry(self, entryConfig, layer = 0) {
        const baseElement = makeElement("menu-item menu-element", "menu-item", "LI"),
            iconWrapper = makeElement("icon", "icon"),
            img = makeElement(null, null, "IMG"),
            label = makeElement("menu-item-label", "menu-item-label"),
            { UiAction } = shmi.requires("visuals.core"),
            action = entryConfig.action ? new UiAction(entryConfig.action) : null;

        // Piece together DOM elements
        iconWrapper.appendChild(img);
        baseElement.appendChild(iconWrapper);
        baseElement.appendChild(label);

        // Setup DOM elements
        label.textContent = shmi.localize(entryConfig.label);
        img.setAttribute("src", entryConfig["icon-src"] || DEFAULT_ICON);

        if (entryConfig["icon-mode"] === "no-icon") {
            baseElement.classList.add("no-icon");
        } else if (entryConfig["icon-mode"] === "hidden") {
            baseElement.classList.add("hidden-icon");
        }

        if (layer === 0) {
            baseElement.classList.add("accent-border");
        }

        const listener = createListener(baseElement, {
            onClick: function onClick() {
                setSelected(self, baseElement, layer);
                setLayer(self, -1);
                removeOverlay(self);
                if (action) {
                    action.execute(self);
                }
            }
        });

        return {
            type: "entry",
            config: {
                label: entryConfig["label"] || null,
                iconSrc: entryConfig["icon-src"] || null,
                iconMode: entryConfig["icon-mode"] || "show",
                access: shmi.cloneObject(entryConfig.access),
                action: shmi.cloneObject(entryConfig.action)
            },
            listeners: [listener],
            elements: {
                base: baseElement,
                iconImage: img,
                label: label
            }
        };
    }

    /**
     * Creates a menu separator according to the provided configuration.
     *
     * @param {*} self Reference to the widget.
     * @param {object} separatorConfig Configuration of the menu separator.
     * @param {number} [layer] Number of the layer the separator is located on.
     * @returns {object} Descriptor of the new menu separator.
     */
    function makeMenuSeparator(self, separatorConfig, layer = 0) {
        const baseElement = makeElement("menu-item separator", "menu-item", "LI"),
            label = makeElement("menu-item-label", "menu-item-label");

        // Piece together DOM elements
        baseElement.appendChild(label);

        // Setup DOM elements
        label.textContent = shmi.localize(separatorConfig.label);

        if (separatorConfig["show-stroke"]) {
            baseElement.classList.add("show-stroke");
        }

        return {
            type: "separator",
            config: {
                label: separatorConfig["label"] || null,
                showStroke: separatorConfig["show-stroke"] || false
            },
            listeners: [],
            elements: {
                base: baseElement,
                label: label
            }
        };
    }

    /**
     * Creates a menu element based on the element type in the provided
     * configuration. If the configuration doesn't include a type, elements
     * with children are assumed to be sections while everything else is
     * assumed to be entries.
     *
     * @param {*} self Reference to the widget.
     * @param {object} config Configuration of the menu element.
     * @param {number} layer Number of the layer the menu element is located
     *  on.
     * @returns {object} Descriptor of the new menu element.
     */
    function makeMenuElement(self, config, layer) {
        const elementType = config.type || (Array.isArray(config.items) ? "group" : "item");

        switch (elementType) {
        case "group":
            if (layer > 1) {
                console.warn(`[${className}] Creating sections for layers >1 not supported.`);
            }

            return makeMenuSection(self, config, layer);

        case "item":
            return makeMenuEntry(self, config, layer);

        case "separator":
            return makeMenuSeparator(self, config, layer);

        default:
            console.warn(`[${className}] Unknown type for menu element ${elementType}`);
            return null;
        }
    }

    /**
     * Creates the menu root.
     *
     * @param {*} self Reference to the widget.
     * @param {object[]} menu Menu entries for layer 0
     * @returns {object} Descriptor of the new menu.
     */
    function makeMenu(self, menu) {
        const menuBase = shmi.getUiElement("menu-layer-0", self.element),
            burgerButton = shmi.getUiElement("burger-button", self.element);

        const children = menu.map((menuElement) => makeMenuElement(self, menuElement, 0)).filter((element) => !!element);

        // Piece together DOM elements
        children.forEach(({ elements: { base } }) => menuBase.appendChild(base));

        // Setup DOM elements
        const listener = createListener(burgerButton, {
            onClick: function onClick() {
                if (self.vars.activeLayer >= 0) {
                    setLayer(self, -1);
                    removeOverlay(self);
                } else {
                    setLayer(self, 0);
                    createOverlay(self);
                }
            }
        });

        return {
            type: "menu",
            config: {},
            listeners: [listener],
            children: children
        };
    }

    /**
     * Returns an array of all listeners of a menu.
     *
     * @param {object} menu Descriptor of a menu or menu element.
     * @returns {object[]} Array of listeners.
     */
    function collectListeners(menu) {
        if (!Array.isArray(menu.children)) {
            return menu.listeners || [];
        }

        return menu.children.reduce((combined, current) => combined.concat(collectListeners(current)), menu.listeners);
    }

    /**
     * Returns an array of all access condition data of a menu.
     *
     * @param {*} menu Descriptor of a menu or menu element.
     * @returns {object[]} Array of access condition data.
     */
    function collectAccessData(menu) {
        const accessData = [];

        if (menu.config.access && menu.config.access.condition && ["hide", "lock"].includes(menu.config.access.type)) {
            accessData.push({
                base: menu.elements.base,
                type: menu.config.access.type,
                condition: menu.config.access.condition,
                listeners: menu.listeners.slice(),
                menu
            });
        }

        if (!Array.isArray(menu.children)) {
            return accessData;
        }

        return menu.children.reduce((combined, current) => combined.concat(collectAccessData(current)), accessData);
    }

    /**
     * Sets the layer-active css class for the corresponding layer.
     * If `layer` is set to -1, all selections are cleared.
     *
     * @param {*} self Reference to the widget.
     * @param {number} layer Layer index
     */
    function setLayer(self, layer) {
        if (layer !== self.vars.activeLayer) {
            if (self.vars.activeLayer >= 0) {
                shmi.removeClass(self.element, `layer-${self.vars.activeLayer}-active`);
            }
            self.vars.activeLayer = layer;
            if (layer >= 0) {
                shmi.addClass(self.element, `layer-${self.vars.activeLayer}-active`);
            }
        }

        if (layer === -1) {
            self.vars.selected.forEach(function(e, idx) {
                if (e !== null) {
                    shmi.removeClass(e, "selected");
                    self.vars.selected[idx] = null;
                }
            });
        }
    }

    /**
     * Returns a condition callback for the given access data reference.
     *
     * @param {HTMLElement} param0.base HTML element to add/remove the css
     *  modifiers to/from.
     * @param {object[]} param0.listeners Listeners to enable/disable.
     * @param {string} cssClass CSS modifier to add/remove.
     * @returns {function}
     */
    function makeAccessConditionCallback({ base, listeners }, cssClass) {
        return (isActive) => {
            if (!isActive) {
                shmi.addClass(base, cssClass);
                listeners.forEach((l) => l.disable());
            } else {
                shmi.removeClass(base, cssClass);
                listeners.forEach((l) => l.enable());
            }
        };
    }

    function createOverlay(self) {
        if (self.vars.overlay) {
            removeOverlay(self);
        }

        const overlay = {
                element: document.createElement("DIV"),
                listener: null
            },
            handler = { onClick: null },
            elem = overlay.element,
            s = elem.style;
        s.position = "fixed";
        s.top = 0;
        s.bottom = 0;
        s.left = 0;
        s.right = 0;
        s.background = self.config["overlay-style"].background;
        s.zIndex = self.config["overlay-style"].zIndex;

        document.body.insertBefore(overlay.element, document.body.firstChild);

        handler.onClick = function() {
            if (self.vars.activeLayer >= 0) {
                setLayer(self, -1);
                removeOverlay(self);
            }
        };
        overlay.listener = createListener(overlay.element, handler);
        overlay.listener.enable();
        self.vars.overlay = overlay;
    }

    function removeOverlay(self) {
        if (self.vars.overlay) {
            self.vars.overlay.element.parentNode.removeChild(self.vars.overlay.element);
            self.vars.overlay.listener.disable();
            self.vars.overlay = null;
        }
    }

    //declare private functions - END

    //definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            listeners: [],
            activeLayer: -1,
            mobile: false,
            selected: [
                null, /* layer 0 */
                null, /* layer 1 */
                null /* layer 2 */
            ],
            crumbs: {
                layer0: null,
                layer1: null
            },
            tokens: [],
            overlay: null
        },
        /* imports added at runtime */
        imports: {
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                self.vars.crumbs.layer0 = shmi.getUiElement("selected-entry-layer-0", self.element);
                self.vars.crumbs.layer1 = shmi.getUiElement("selected-entry-layer-1", self.element);
                self.vars.menu = makeMenu(self, self.config.menu);

                shmi.removeClass(self.element, "mobile");
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this,
                    { ConditionObserver } = shmi.requires("visuals.tools.conditions"),
                    currentLayout = shmi.getCurrentLayout();

                if (self.config["mobile-layouts"].indexOf(currentLayout) !== -1) {
                    shmi.addClass(self.element, "mobile");
                    self.vars.mobile = true;
                } else {
                    shmi.removeClass(self.element, "mobile");
                    self.vars.mobile = false;
                }

                const layoutToken = shmi.listen("layout-change", function(evt) {
                    if (self.config["mobile-layouts"].indexOf(evt.detail.layout) !== -1) {
                        shmi.addClass(self.element, "mobile");
                        if (!self.vars.mobile) {
                            setLayer(self, -1);
                        }
                        self.vars.mobile = true;
                    } else {
                        shmi.removeClass(self.element, "mobile");
                        if (self.vars.mobile) {
                            setLayer(self, 0);
                        }
                        self.vars.mobile = false;
                    }
                    removeOverlay(self);
                });
                self.vars.tokens.push(layoutToken);

                if (!self.vars.mobile) {
                    setLayer(self, 0);
                }

                self.vars.listeners = collectListeners(self.vars.menu);
                self.vars.listeners.forEach((l) => l.enable());
                self.vars.tokens.push(
                    ...collectAccessData(self.vars.menu).map((reference) => {
                        switch (reference.type) {
                        case "hide":
                            return new ConditionObserver(reference.condition, makeAccessConditionCallback(reference, "hidden"));
                        case "lock":
                            return new ConditionObserver(reference.condition, makeAccessConditionCallback(reference, "locked"));
                        default:
                            // Should never happen
                            throw new Error(`Invalid access type ${reference.type}`);
                        }
                    })
                );
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;
                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.listeners.forEach((l) => l.disable());
                self.vars.tokens = [];
                self.vars.listeners = [];
                removeOverlay(self);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                shmi.addClass(self.element, "locked");
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                shmi.removeClass(self.element, "locked");
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals searchbar control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-searchbar",
 *     "name": null,
 *     "template": "default/iq-searchbar.iq-variant-01",
 *     "item": null,
 *     "datagrid": null,
 *     "search-column": null,
 *     "show-results": "two-or-more",
 *     "result-columns": null
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * item {string}: Item alias of an item to connect. Can be set to `null` if no
 *  item is required.
 * datagrid {string}: Name of a DataGrid to search in.
 * search-column {string}: Name of the column to search in.
 * show-results {string}: When to display the results dropdown.
 *  * `never` Never show results.
 *  * `always` Always show results if there are more than 0.
 *  * `two-or-more` Show results if there are two or more.
 * result-columns {string[]}: Array of DataGrid column names to display in
 *  the results dropdown. `null` or an empty array will cause all columns to
 *  be included.
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iqSearchbar", // control name in camel-case
        uiType = "iq-searchbar", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    var defConfig = {
        "class-name": "iq-searchbar",
        "name": null,
        "template": "default/iq-searchbar.iq-variant-01",
        "label": uiType,
        "auto-label": true,
        "item": null,
        "show-icon": false,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "datagrid": null,
        "search-column": null,
        "show-results": "two-or-more",
        "result-columns": null,
        "select-contents-on-focus": true
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START

    function getMatchData(self) {
        var gData = self.vars.dataGridData || [],
            hintIdx = self.vars.currentHintIndex || 0;

        return gData[hintIdx] || null;
    }

    /**
     * Checks if all dom elements could be found.
     *
     * @param {object} dom `this.vars.dom`
     * @returns {boolean} `true` if the object did not contain any `null`
     *  values, `false` else.
     */
    function verifyDomElements(dom) {
        var iterObj = shmi.requires("visuals.tools.iterate.iterateObject"),
            ok = true;

        iterObj(dom, function(val) {
            ok = ok && val !== null;
        });

        return ok;
    }

    /**
     * Adds an event listener to the given target and stores data required to
     * remove it again in the given category.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} category Name of the category for which to register
     *  the event listener.
     * @param {EventTarget} target Target to add the event listener to.
     * @param {string} type Name of the event.
     * @param {function} listener
     */
    function addRegisteredEventListener(self, category, target, type, listener) {
        var listeners = self.vars.registeredEventListeners[category];

        // If the category doesn't exist, create it.
        if (!listeners) {
            self.vars.registeredEventListeners[category] = [];
            listeners = self.vars.registeredEventListeners[category];
        }

        target.addEventListener(type, listener);

        listeners.push({
            type: type,
            target: target,
            listener: listener
        });
    }

    /**
     * Removes all event listeners for a given category. If no category has
     * been given, event listeners for all categories are removed.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} [category] Name of the category for which to remove all
     *  event listeners.
     */
    function removeRegisteredEventListeners(self, category) {
        var iterObj = shmi.requires("visuals.tools.iterate.iterateObject"),
            listeners = [];

        if (category) {
            listeners = self.vars.registeredEventListeners[category] || [];
            delete self.vars.registeredEventListeners[category];
        } else {
            iterObj(self.vars.registeredEventListeners, function(l) {
                listeners = listeners.concat(l || []);
            });
            self.vars.registeredEventListeners = {};
        }

        listeners.forEach(function(listener) {
            listener.target.removeEventListener(listener.type, listener.listener);
        });
    }

    /**
     * Removes the displayed hint.
     *
     * @param {*} self Reference to a searchbar control.
     */
    function clearHint(self) {
        self.vars.dom.suggestion.textContent = "";
        self.vars.currentHint = null;
    }

    /**
     * Sets a hint.
     *
     * @param {*} self Reference to a searchbar control
     * @param {?string} hint The hint
     * @param {boolean} [force] Show the hint text even if the user input field
     *  is empty.
     */
    function setHint(self, hint, force) {
        var val;

        if (!hint) {
            clearHint(self);
        } else {
            val = self.getValue();

            if (val.length === 0 && !force) {
                clearHint(self);
            } else {
                self.vars.dom.suggestion.textContent = hint.substr(val.length);
                self.vars.currentHint = hint;
            }
        }
    }

    /**
     * Set a hint at the given index in the current data grid data buffer.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {number} idx Index of the hint to set.
     * @param {boolean} [force] Show the hint text even if the user input field
     *  is empty.
     */
    function setHintByIdx(self, idx, force) {
        if (self.vars.dataGridData.length === 0) {
            clearHint(self);

            return;
        }

        // Make sure the index is in [0, self.vars.dataGridData)
        while (idx < 0) {
            idx += self.vars.dataGridData.length;
        }
        idx = idx % self.vars.dataGridData.length;

        if (self.vars.currentHintIndex !== null) {
            shmi.removeClass(self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex], "hint-current");
        }
        shmi.addClass(self.vars.dom.resultBox.firstChild.children[idx], "hint-current");

        setHint(self, self.vars.dataGridData[idx][self.config["search-column"]], force);
        self.vars.currentHintIndex = idx;
    }

    /**
     * Update the displayed hint. If new hints need to be fetched, this is done
     * asynchronously and the hint is cleared in the meantime.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} [inputText] String to look (and display) up hints for.
     *  If no string is given, the current string of the input field is used
     *  instead.
     */
    function updateHint(self, inputText) {
        fetchHint(self, inputText || self.getValue());
    }

    /**
     * Callback called when a finished processing a new hint.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {?string} inputText String the looked up hint is meant for. If no
     *  string is given, the current string of the input field is used
     *  instead
     * @param {?string} hint The fetched hint. May be `null` if there is no
     *  hint, in which case `inputText` also isn't a valid input.
     */
    function onHint(self, inputText, hint) {
        inputText = inputText || self.getValue();

        if (!hint) {
            if (inputText.length > 0) {
                shmi.addClass(self.element, "notfound");
            }
            clearHint(self);
            self.vars.lastHintSearchResult = null;
        } else {
            shmi.removeClass(self.element, "notfound");
            setHint(self, hint);
            self.vars.lastHintSearchResult = hint;
        }
    }

    /**
     * Fetches a new hint from the connected datagrid. If the given string is
     * guaranteed to yield in the same hint being found, the last search hit is
     * being used instead of actually querying the datagrid.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} inputText String to look up a hint for.
     */
    function fetchHint(self, inputText) {
        var newValueIsMoreSpecific = self.vars.lastHintSearchToken && (inputText.indexOf(self.vars.lastHintSearchToken) === 0);
        var hintMatchesNewValue = self.vars.lastHintSearchResult && (self.vars.lastHintSearchResult.indexOf(inputText) === 0);

        // No need to update the hint
        if (newValueIsMoreSpecific && hintMatchesNewValue) {
            onHint(self, inputText, self.vars.lastHintSearchResult);
            updateResultBox(self, self.vars.dataGridData.filter(function(val) {
                return val[self.config["search-column"]] && val[self.config["search-column"]].indexOf(inputText) !== -1;
            }));

            return;
        }

        clearHint(self);
        self.vars.dataGrid.setFilter(self.vars.dataGridSearchColumnIdx, [ inputText + "%" ]);
        self.vars.lastHintSearchToken = inputText;
    }

    /**
     * Removes all nodes from the result box.
     *
     * @param {*} self Reference to a searchbar control.
     */
    function clearResultBox(self) {
        removeRegisteredEventListeners(self, "result-box");
        while (self.vars.dom.resultBox.firstChild) {
            self.vars.dom.resultBox.removeChild(self.vars.dom.resultBox.firstChild);
        }
    }

    /**
     * Creates a new result table and attaches it to the result box.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {object[]} data DataGrid data to display.
     */
    function updateResultBox(self, data) {
        var table = document.createElement("table"),
            resultBoxVisible = false,
            columns,
            mainColumnIdx;

        clearResultBox(self);

        if (self.config["show-results"] === "never") {
            // Control is set up to never show results.
            resultBoxVisible = false;
        } else if (self.config["show-results"] === "two-or-more" && data.length <= 1) {
            // Control is set up to only show the results if there are two or
            // more. We don't have enough so stop here.
            resultBoxVisible = false;
        } else if (data.length !== 0) {
            resultBoxVisible = true;
        }

        if (resultBoxVisible) {
            self.vars.dom.resultBox.style.display = "";
        } else {
            self.vars.dom.resultBox.style.display = "none";
            return; //nothing left to do here
        }

        columns = Object.keys(data[0]);
        if ((self.config["result-columns"] || []).length > 0) {
            columns = columns.filter(function(columnName) {
                return self.config["result-columns"].indexOf(columnName) !== -1;
            });
        }

        // See if there are columns left we can display. If not, stop here.
        if (columns.length === 0) {
            self.vars.dom.resultBox.innerHTML = "No columns to display. Please check <span style=\"font-weight: bold\">result-columns</span> in your control configuration.";
            fLog("No columns to display.", "dataGrid columns =", Object.keys(data[0]), "columns to display =", self.config["result-columns"]);
            return;
        }

        // Make sure to `search-column` is at position 0 in the columns array
        // (= it is displayed first).
        mainColumnIdx = columns.indexOf(self.config["search-column"]);
        if (mainColumnIdx > 0) {
            columns.unshift(columns[mainColumnIdx]);
            columns.splice(mainColumnIdx + 1, 1);
        }

        // Fill the table.
        data.forEach(function(row, idx) {
            var tr = document.createElement("tr");
            columns.forEach(function(columnName) {
                var cell = document.createElement("td");
                cell.textContent = row[columnName] || "";

                tr.appendChild(cell);
            });

            addRegisteredEventListener(self, "result-box", tr, "touchstart", (evt) => {
                evt.stopPropagation();
            });
            addRegisteredEventListener(self, "result-box", tr, "mousedown", function(ev) {
                self.setValue(row[self.config["search-column"]]);
            });

            addRegisteredEventListener(self, "result-box", tr, "mouseover", setHintByIdx.bind(null, self, idx, true));
            addRegisteredEventListener(self, "result-box", tr, "mouseleave", shmi.removeClass.bind(shmi, tr, "hint-current"));

            table.appendChild(tr);
        });

        self.vars.dom.resultBox.appendChild(table);
    }

    /**
     * Scrolls the result box to the given element. Will not scroll, if the
     * element is already visible. After a scroll, the element will be top
     * aligned if the result box was scrolled up and down aligned if the result
     * box was scrolled down.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {DOMElement} element Element to scroll the result box to.
     */
    function doResultBoxScroll(self, element) {
        var scrollTop = self.vars.dom.resultBox.scrollTop,
            scrollBottom = scrollTop + self.vars.dom.resultBox.offsetHeight;

        if (element.offsetTop < scrollTop) {
            element.scrollIntoView(true);
        } else if (element.offsetTop + element.offsetHeight > scrollBottom) {
            element.scrollIntoView(false);
        }
    }

    /**
     * Returns a datagrids data in a more convenient format.
     *
     * @param {DataGrid} dg Data grid to get data from.
     * @param {number} subID Subscription id.
     * @returns {object[]} Array of rows where each row is key-, value store.
     */
    function getDataGridDataHelper(dg, subID) {
        var fieldNames = dg.getFields();

        return dg.getCurrentIDs(subID).map(function(id) {
            var row = dg.getRowData(id),
                rowdata = {};

            if (!row) {
                return null;
            }

            row.forEach(function(cell, cellIdx) {
                rowdata[fieldNames[cellIdx]] = String(cell.value);
            });

            return rowdata;
        });
    }

    /**
     * Subscriber function attached to the searchbars datagrid.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {object} dgData DataGrid update notification data.
     */
    function hintFetchCompleteCallback(self, dgData) {
        var inputText = self.getValue(),
            rowData = getDataGridDataHelper(self.vars.dataGrid, self.vars.dataGridSubscriptionId.id),
            hint;

        // No hint found?
        if (rowData.length > 0) {
            hint = (rowData[0] || {})[self.config["search-column"]];
        }

        self.vars.currentHintIndex = null;
        self.vars.dataGridData = rowData;
        onHint(self, inputText, hint || null);
        updateResultBox(self, rowData);
        self.fire("change", { value: self.getValue() });
    }

    /**
     * Set the current selection to the end of the given elements content.
     *
     * @param {DOMElement} element
     * @param {boolean} selectContent Selects the entire content instead of
     *  just the end.
     */
    function setSelected(element, selectContent) {
        // Setting textContent reset our cursor position. Set the
        // cursor to the end of the content.
        var range = document.createRange();
        range.selectNodeContents(element);
        if (!selectContent) {
            range.setStart(element, range.endOffset);
        }

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }

    /**
     * Event listener callback attached to the user input field.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {KeyboardEvent} ev
     */
    function userInputKeyDownHandler(self, ev) {
        if (ev.key === "Tab") {
            // On "Tab" use autocompletion if appropriate. Prevent the event
            // from propagating if autocompletion was used but don't otherwise.
            if (self.vars.currentHint && self.getValue() !== self.vars.currentHint) {
                ev.preventDefault();

                self.setValue(self.vars.currentHint);
                clearHint(self);

                // Setting textContent reset our cursor position. Set the
                // cursor to the end of the content.
                setSelected(self.vars.dom.input);
            }
        } else if (ev.key === "Escape") {
            // On "Escape" toggle the result box.
            if (shmi.hasClass(self.element, "show-results")) {
                shmi.removeClass(self.element, "show-results");
            } else {
                shmi.addClass(self.element, "show-results");
            }
        } else if (ev.key === "ArrowUp") {
            // On "ArrowUp" scroll up by 1.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, (self.vars.currentHintIndex || 0) - 1, true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.key === "PageUp") {
            // On "PageUp" scroll up by 10.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, Math.max((self.vars.currentHintIndex || 0) - 10, 0), true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.key === "ArrowDown") {
            // On "ArrowDown" scroll down by 1.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, (self.vars.currentHintIndex || 0) + 1, true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.key === "PageDown") {
            // On "PageDown" scroll down by 10.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, Math.min((self.vars.currentHintIndex || 0) + 10, self.vars.dataGridData.length - 1), true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.keyCode === 13 || event.keyCode === 9) {
            // On "Return" or "Enter" remove focus and autocomplete.
            ev.preventDefault();
            if (self.vars.currentHint) {
                self.setValue(self.vars.currentHint);
            }

            ev.target.blur();
        }
    }

    /**
     * Attaches event listeners to the user input field.
     *
     * @param {*} self Reference to a searchbar control.
     */
    function setupDomUserInput(self) {
        // Update the hint whenever the content of the user input field
        // changes.
        addRegisteredEventListener(self, "input", self.vars.dom.input, "input", function onInput() {
            var inputText = self.getValue();

            self.vars.dom.mirror.textContent = inputText;

            updateHint(self, inputText);
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "keydown", userInputKeyDownHandler.bind(null, self));

        addRegisteredEventListener(self, "input", self.vars.dom.input, "focus", function onFocus() {
            self.fire("enter", { value: self.getValue() });
            self.vars.userInputActive = true;
            shmi.addClass(self.element, "focused");
            shmi.addClass(self.element, "show-results");
            updateHint(self);

            if (self.config["select-contents-on-focus"]) {
                setSelected(self.vars.dom.input, true);
            }
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "blur", function onBlur() {
            self.vars.userInputActive = false;
            shmi.removeClass(self.element, "focused");
            window.getSelection().removeAllRanges();
            clearHint(self);

            if (self.vars.itemSubscription && self.config.item) {
                self.imports.im.writeValue(self.config.item, self.getValue());
            }

            self.fire("change", { value: self.getValue() });
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "touchstart", function onDown(evt) {
            evt.stopPropagation();
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "click", function onClick() {
            shmi.addClass(self.element, "show-results");
        });
    }

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.dom.label) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.dom.label.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.dom.label.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            dom: {
                icon: null,
                label: null,
                input: null,
                mirror: null,
                suggestion: null,
                resultBox: null,
                clear: null
            },
            label: null,
            initDone: false,
            registeredEventListeners: {},
            dataGrid: null,
            dataGridSubscriptionId: null,
            dataGridSearchColumnIdx: -1,
            dataGridData: [],
            currentHint: null,
            currentValue: null,
            lastHintSearchToken: null,
            lastHintSearchResult: null,
            userInputActive: false,
            currentHintIndex: null,
            itemSubscription: null,
            listeners: []
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: [
            "change", "enter"
        ],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this,
                    io = shmi.requires("visuals.io");

                this.vars.dom.icon = shmi.getUiElement("icon", this.element);
                this.vars.dom.label = shmi.getUiElement("label", this.element);
                this.vars.dom.input = shmi.getUiElement("searchbar-user-input", this.element);
                this.vars.dom.mirror = shmi.getUiElement("searchbar-mirror", this.element);
                this.vars.dom.suggestion = shmi.getUiElement("searchbar-suggestion", this.element);
                this.vars.dom.resultBox = shmi.getUiElement("searchbar-result-box", this.element);
                this.vars.dom.clear = shmi.getUiElement("clear-input", this.element);
                this.vars.dataGrid = this.imports.dgm.getGrid(this.config.datagrid);

                if (!verifyDomElements(this.vars.dom)) {
                    this.element.textContent = "One or more required DOM elements are missing from the template. This control will not work. Please select a compatible template.";
                    fLog("Some DOM elements are missing from the template.");
                    return;
                }

                // Icon
                if (!this.vars.dom.icon) {
                    shmi.log('[IQ:iq-input-field] no button-icon element provided', 1);
                } else if (this.config['icon-src'] && this.config['show-icon']) {
                    this.vars.dom.icon.style.backgroundImage = `url(${this.config['icon-src']})`;
                } else if (this.config['icon-class'] && this.config['show-icon']) {
                    const iconClasses = this.config['icon-class'].trim().split(" ");
                    iconClasses.forEach(shmi.addClass.bind(shmi, this.vars.dom.icon));
                } else {
                    // No icon configured - hide
                    shmi.addClass(this.element, "no-icon");
                }

                // Label
                setLabelImpl(this, this.config.label);

                var domLabel = shmi.getUiElement("searchbar-label", this.element);
                if (domLabel) {
                    if (this.config.label && String(this.config.label).length > 0) {
                        domLabel.textContent = shmi.localize(this.config.label);
                    } else {
                        domLabel.style.display = "none";
                    }
                }

                var clearHandler = {
                    onClick: function() {
                        if (self.isActive() && !self.locked) {
                            self.setValue("");
                        }
                    }
                };
                self.vars.listeners.push(new io.MouseListener(self.vars.dom.clear, clearHandler));
                self.vars.listeners.push(new io.TouchListener(self.vars.dom.clear, clearHandler));

                if (this.vars.dataGrid === null) {
                    this.element.textContent = "Unable to find datagrid or no datagrid configured. This control will not work. Please select a valid datagrid.";
                    if (this.config.datagrid) {
                        fLog("Datagrid not found", this.config.datagrid);
                    } else {
                        fLog("No datagrid configured");
                    }

                    return;
                }

                this.vars.initDone = true;

                this.vars.dataGridSearchColumnIdx = this.vars.dataGrid.getFields().indexOf(this.config["search-column"]);
                if (this.vars.dataGridSearchColumnIdx === -1) {
                    this.element.textContent = "Datagrid does not have the configured field to search in. This control will not work. Please select a valid search field.";
                    fLog("Datagrid does not have the configured search field.");
                }

                this.vars.dataGrid.sort(this.vars.dataGridSearchColumnIdx, "ASC");
            },
            /**
             * Returns the contents of the user input field. Since some
             * browsers add non-breaking-spaces, those spaces are converted to
             * normal ones.
             *
             * @returns {string} Value of the input field.
             */
            getValue: function() {
                if (!this.vars.initDone) {
                    return null;
                }

                const value = this.vars.dom.input.value;

                // Replace non breaking spaces with normal ones
                return value.replace(String.fromCharCode(0xA0), " ");
            },
            getMatchData: function() {
                return getMatchData(this);
            },
            /**
             * Sets the value of the user input field.
             *
             * @param {string} val Value to set
             */
            setValue: function(val) {
                if (!this.vars.initDone) {
                    return;
                }

                this.vars.dom.input.value = val;
                this.vars.dom.mirror.textContent = val;
                updateHint(this, val);
            },
            /* called when control is enabled */
            onEnable: function() {
                if (!this.vars.initDone) {
                    return;
                }

                setupDomUserInput(this);

                this.vars.dataGridSubscriptionId = this.vars.dataGrid.subscribePage(0, 100, hintFetchCompleteCallback.bind(null, this));
                if (this.config.item) {
                    this.vars.itemSubscription = this.vars.im.subscribeItem(this.config.item, this);
                }
                this.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                if (!this.vars.initDone) {
                    return;
                }

                this.vars.dataGrid.unsubscribe(this.vars.dataGridSubscriptionId.id);
                this.vars.dataGridSubscriptionId = null;

                if (this.vars.itemSubscription) {
                    this.vars.itemSubscription.unlisten();
                    this.vars.itemSubscription = null;
                }

                clearResultBox(this);
                removeRegisteredEventListeners(this);
                this.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                this.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                shmi.addClass(this.element, "locked");
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                this.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                shmi.removeClass(this.element, "locked");
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                if (this.vars.initDone && !this.vars.userInputActive) {
                    this.setValue(value);
                }
            },
            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step) {

            },
            setLabel: function(labelText) {
                if (this.vars.dom.label && this.config['auto-label']) {
                    setLabelImpl(this, labelText, false);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-select-box
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-select-box",
 *     "name": null,
 *     "template": "default/iq-select-box.iq-variant-01"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "auto-label": Whether to use the auto-label (from item)
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 * "no-selection-label": The default "no selection" label
 * "selected": Which option is selected
 * "options": The options
 * "item": The item
 * "show-icon": Whether to show the icon
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-select-box", // control name in camel-case
        uiType = "iq-select-box", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-select-box",
        "name": null,
        "template": "default/iq-select-box.iq-variant-01",
        "label": '[Label]',
        "item": null,
        "auto-label": true,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "no-selection-label": "---",
        "selected": -1,
        "options": [],
        "show-text": true,
        "show-icon": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            optionListeners: [],
            domListeners: {},
            value: 0,
            active: false,

            // DOM Elements
            templateEl: null,
            labelEl: null,
            iconEl: null,
            containerEl: null,
            anchorEl: null,
            optionEl: null,
            selectedEl: null,
            selectedValEl: null,
            optionEls: [],

            selected: -1,
            clickedInside: false,
            isOpen: false,
            isLocked: false,
            highlightIndex: null,
            subscriptionTargetId: null,

            // Dynamic CSS classes
            iconClass: 'iq-icon',
            showTextAndIconClass: 'iq-icon-and-text',
            showIconOnlyClass: 'iq-icon-only',
            labelAreaClass: 'iq-label-area',
            highlightClass: 'highlighted', // WebIQ default
            selectBoxSelectedClass: 'iq-select-box-selected',
            optionIconAndTextClass: 'iq-option-icon-and-text',
            optionIconOnlyClass: 'iq-option-icon-only',
            optionIconClass: 'iq-option-icon'
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            iter: "visuals.tools.iterate.iterateObject"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.templateEl = self.element.cloneNode(true);

                self.vars.iqWidgetContent = shmi.getUiElement('iq-widget-content', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);

                self.vars.containerEl = shmi.getUiElement('options', self.element);
                self.vars.selectedEl = shmi.getUiElement('selected', self.element);
                self.vars.selectedValEl = shmi.getUiElement('value', self.vars.selectedEl);
                self.vars.optionEl = shmi.getUiElement('option', self.element);
                self.vars.anchorEl = shmi.getUiElement('anchor', self.vars.containerEl);

                if (!self.vars.containerEl || !self.vars.anchorEl || !self.vars.optionEl || !self.vars.selectedEl || !self.vars.selectedValEl) {
                    shmi.log('[IQ:iq-select-box] At least one element is missing: containerEl, anchorEl, optionEl, selectedEl, selectedValEl', 3);
                    return;
                }

                // remove template DOM nodes and remap option elements to template
                self.vars.iqWidgetContent.removeChild(self.vars.selectedEl);
                self.vars.iqWidgetContent.removeChild(self.vars.containerEl);
                self.vars.iqWidgetContent.removeChild(self.vars.optionEl);

                self.vars.containerEl = shmi.getUiElement('options', self.vars.templateEl);
                self.vars.anchorEl = shmi.getUiElement('anchor', self.vars.containerEl);
                self.vars.optionEl = shmi.getUiElement('option', self.vars.templateEl);
                self.vars.selectedEl = shmi.getUiElement('selected', self.vars.templateEl);
                self.vars.selectedValEl = shmi.getUiElement('value', self.vars.selectedEl);

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-select-box] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    self.vars.iconEl.style.backgroundImage = `url(${self.config['icon-src']})`;
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    var iconClasses = self.config['icon-class'].trim().split(" ");
                    iconClasses.forEach(function(cls) {
                        shmi.addClass(self.vars.iconEl, cls);
                    });
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, "no-icon");
                }

                // Label
                if (self.config['show-text']) {
                    setLabelImpl(self, self.config.label);
                } else {
                    setLabelImpl(self, null);
                }

                var popupContainerEl = document.createElement("DIV");
                shmi.addClass(popupContainerEl, 'iq-popup-container');
                self.vars.iqWidgetContent.appendChild(popupContainerEl);
                popupContainerEl.appendChild(self.vars.containerEl);
                popupContainerEl.appendChild(self.vars.selectedEl);

                self.rebuildOptions(false);

                self.vars.selected = self.config.selected;
                self.vars.containerEl.setAttribute('tabindex', '-1');

                /*****************/
                /*** LISTENERS ***/
                /*****************/
                self.vars.domListeners = self.createDomListenerObject();
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.element, self.createElementListenerObject()));
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.element, self.createElementListenerObject()));
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.containerEl, self.createContainerListenerObject()));

                // Defaults
                shmi.addClass(self.vars.containerEl, 'hidden');
                if (self.vars.selected === -1) {
                    self.setSelected(null);
                } else {
                    self.setSelected(self.vars.optionEls[self.config.selected]);
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.element.setAttribute('tabindex', 0);

                self.lockUnlockListeners(false);

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-select-box] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.lockUnlockListeners(true);

                if (self.vars.subscriptionTargetId) {
                    self.vars.subscriptionTargetId.unlisten();
                    self.vars.subscriptionTargetId = null;
                }

                shmi.log("[IQ:iq-select-box] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                self.vars.isLocked = true;
                shmi.addClass(self.vars.containerEl, 'hidden');
                self.vars.highlightIndex = -1;

                self.lockUnlockListeners(true);

                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-select-box] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                self.lockUnlockListeners(false);

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-select-box] unlocked", 1);
            },

            setOptions: function(options) {
                const self = this;

                self.config.options = options;
                if (self.initialized) {
                    self.rebuildOptions(self.isActive());
                    self.setValue(self.getValue());
                }
            },

            getOptions: function() {
                var self = this;
                return shmi.cloneObject(self.config.options);
            },

            /**
             * Sets the selected element
             *
             * @param element - element to select
             */
            setSelected: function(element) {
                const self = this,
                    selectedIdx = self.vars.optionEls.findIndex((el) => el === element);

                // Unselect all
                self.vars.optionEls.forEach(function(val, idx) {
                    shmi.removeClass(val, self.vars.selectBoxSelectedClass);
                });

                if (selectedIdx === -1) {
                    self.vars.selectedValEl.textContent = shmi.localize(self.config['no-selection-label']);
                    self.vars.value = null;
                    shmi.removeClass(self.vars.selectedEl, self.vars.showTextAndIconClass);
                    shmi.removeClass(self.vars.selectedEl, self.vars.showIconOnlyClass);
                } else {
                    // Get selected label
                    const selectedLabel = self.config.options[selectedIdx].label;

                    shmi.addClass(element, self.vars.selectBoxSelectedClass);
                    self.vars.selectedValEl.textContent = shmi.localize(selectedLabel);
                    self.vars.value = self.config.options[selectedIdx].value;

                    var selectedOption = self.config.options[selectedIdx],
                        iconEl = shmi.getUiElement("option-icon", self.vars.selectedEl),
                        hasIcon = false;

                    if (selectedOption && iconEl) {
                        hasIcon = self.setIcon(iconEl, selectedOption, true);
                        if (hasIcon && selectedOption.label) {
                            shmi.addClass(self.vars.selectedEl, self.vars.optionIconAndTextClass);
                        } else if (hasIcon) {
                            shmi.addClass(self.vars.selectedEl, self.vars.optionIconOnlyClass);
                        } else {
                            shmi.removeClass(self.vars.selectedEl, self.vars.optionIconAndTextClass);
                            shmi.removeClass(self.vars.selectedEl, self.vars.optionIconOnlyClass);
                        }
                    } else if (iconEl) {
                        shmi.removeClass(self.vars.selectedEl, self.vars.optionIconAndTextClass);
                        shmi.removeClass(self.vars.selectedEl, self.vars.optionIconOnlyClass);
                    }
                }
                self.vars.selected = selectedIdx;
            },

            getCurrentIndex: function() {
                var self = this;

                return self.vars.selected;
            },

            /**
             * Writes the current value to a connected data-source item
             *
             */
            updateValue: function() {
                var self = this;
                if (self.config.item) {
                    self.imports.im.writeValue(self.config.item, self.getValue());
                }
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                const self = this,
                    oldValue = self.getValue();

                shmi.log("[IQ:iq-select-box] Setting value...: " + value, 0);

                const selectedIndex = self.config.options.findIndex((option) => option.value === value);
                self.setSelected(selectedIndex !== -1 ? self.vars.optionEls[selectedIndex] : null);

                if (self.getValue() !== oldValue) {
                    self.fire("change", {
                        value: self.getValue()
                    });
                }
                shmi.log("[IQ:iq-select-box] Value set: " + value, 1);
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                var self = this;

                return self.vars.value;
            },

            /**
             * Called when the label is received
             *
             * @param labelText
             */
            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label'] && self.config['show-text']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Helper function for locking/unlocking
             *
             * @param {boolean} lock
             */
            lockUnlockListeners: function(lock) {
                var self = this;

                // SHMI listeners
                self.vars.listeners.forEach(function(l) {
                    if (lock) {
                        l.disable();
                    } else {
                        l.enable();
                    }
                });

                self.vars.optionListeners.forEach(function(l) {
                    if (lock) {
                        l.disable();
                    } else {
                        l.enable();
                    }
                });

                // DOM Listeners
                self.imports.iter(self.vars.domListeners, function(val, prop) {
                    if (val.element) {
                        if (lock) {
                            val.element.removeEventListener(val.event, val.callback);
                        } else {
                            val.element.addEventListener(val.event, val.callback);
                        }
                    }
                });
            },

            /**
             * Rebuilds options
             *
             * @param enableListeners
             */
            rebuildOptions: function(enableListeners) {
                var self = this;

                // Remove listeners
                self.vars.optionListeners.forEach(function(listener) {
                    listener.disable();
                });
                self.vars.optionListeners = [];

                // Reset option-elements in case of call to 'setOptions(..)'
                self.vars.optionEls = [];

                // Clear old option elements
                while (self.vars.anchorEl.firstChild) {
                    self.vars.anchorEl.removeChild(self.vars.anchorEl.firstChild);
                }

                self.config.options.forEach(function(option, index) {
                    var label = shmi.localize(option.label),
                        optionEl = self.vars.optionEl.cloneNode(true),
                        valueEl = shmi.getUiElement('value', optionEl),
                        iconEl = shmi.getUiElement('icon', optionEl),
                        hasIcon = false;

                    if (valueEl) {
                        valueEl.textContent = label;
                    } else {
                        optionEl.textContent = label;
                    }

                    if (option["tooltip"] || option["icon-title"]) {
                        optionEl.setAttribute("title", shmi.localize(option.tooltip || option["icon-title"]));
                    }

                    if (iconEl) {
                        hasIcon = self.setIcon(iconEl, option, false);
                        if (hasIcon && label) {
                            shmi.addClass(optionEl, self.vars.optionIconAndTextClass);
                        } else if (hasIcon) {
                            shmi.addClass(optionEl, self.vars.optionIconOnlyClass);
                        }
                    }

                    self.vars.anchorEl.appendChild(optionEl);
                    self.vars.optionEls.push(optionEl);
                });

                var mouseHandler = self.makeOptionMouseHandler(),
                    touchHandler = self.makeOptionTouchHandler();

                self.vars.optionEls.forEach(function(el) {
                    self.vars.optionListeners.push(new shmi.visuals.io.TouchListener(el, touchHandler));
                    self.vars.optionListeners.push(new shmi.visuals.io.MouseListener(el, mouseHandler));
                });

                if (enableListeners) {
                    self.vars.optionListeners.forEach(function(listener) {
                        listener.enable();
                    });
                }
            },

            /**
             * Sets the icon
             *
             * @param {object} element
             * @param {object} option
             * @param {boolean} isSelected
             *
             * @returns {boolean}
             */
            setIcon: function(element, option, isSelected) {
                var self = this,
                    img = null,
                    hasIcon = false;

                if (option["icon-src"]) {
                    if (element.tagName !== "IMG") {
                        img = document.createElement("IMG");
                        element.parentNode.insertBefore(img, element);
                        element.parentNode.removeChild(element);
                    } else {
                        img = element;
                    }

                    shmi.addClass(img, self.vars.optionIconClass);
                    img.setAttribute("src", option["icon-src"]);
                    hasIcon = true;
                } else if (option["icon-class"]) {
                    if (element.tagName !== "DIV") {
                        img = document.createElement("DIV");
                        element.parentNode.insertBefore(img, element);
                        element.parentNode.removeChild(element);
                    } else {
                        img = element;
                    }

                    shmi.addClass(img, self.vars.optionIconClass);
                    shmi.addClass(img, option["icon-class"]);
                    hasIcon = true;
                } else {
                    shmi.addClass(element, self.vars.optionIconClass);
                }

                if (img && hasIcon) {
                    img.dataset.ui = "option-icon";
                } else {
                    element.dataset.ui = "option-icon";
                }

                if (!hasIcon) {
                    shmi.addClass(element, "hidden");
                } else {
                    shmi.removeClass(element, "hidden");
                }

                return hasIcon;
            },

            /**
             * Creates mouse listener functions to be attached to each option element.
             */
            makeOptionMouseHandler: function() {
                var self = this;
                return {
                    onClick: function onClick(x, y, e) {
                        shmi.log("[IQ:iq-select-box] Click on element", 0);
                        const oldValue = self.getValue();

                        self.setSelected(e.currentTarget);
                        self.updateValue();

                        shmi.addClass(self.vars.containerEl, 'hidden');

                        self.vars.isOpen = false;
                        self.vars.highlightIndex = -1;
                        if (self.getValue() !== oldValue) {
                            self.fire("change", {
                                value: self.getValue()
                            });
                        }
                    },

                    onPress: function onPress(x, y, e) {
                        shmi.log("[IQ:iq-select-box] Press", 0);
                        e.preventDefault();
                    },

                    onRelease: function onRelease() {
                        shmi.log("[IQ:iq-select-box] Release", 0);
                    }
                };
            },

            /**
             * Creates touch listener functions to be attached to each option element.
             */
            makeOptionTouchHandler: function() {
                var self = this;
                return {
                    onPress: function onPress() {
                    },
                    onClick: function onClick(x, y, e) {
                        const oldValue = self.getValue();

                        shmi.log("[IQ:iq-select-box] Click on element", 0);

                        self.setSelected(e.currentTarget);
                        self.updateValue();

                        shmi.addClass(self.vars.containerEl, 'hidden');
                        self.vars.isOpen = false;
                        self.vars.highlightIndex = -1;

                        if (self.getValue() !== oldValue) {
                            self.fire("change", {
                                value: self.getValue()
                            });
                        }
                    }
                };
            },

            /**
             * Generates the listener object
             *
             * @returns {object}
             */
            createDomListenerObject: function() {
                var self = this;
                return {
                    containerBlur: {
                        "event": 'blur',
                        "element": self.vars.containerEl,
                        "callback": function(e) {
                            if (self.vars.clickedInside) {
                                setTimeout(function() {
                                    self.vars.containerEl.focus();
                                }, shmi.c("DECOUPLE_TIMEOUT"));
                            } else {
                                shmi.addClass(self.vars.containerEl, 'hidden');
                                setTimeout(function() {
                                    self.vars.isOpen = false;
                                }, shmi.c("DECOUPLE_TIMEOUT"));
                                self.vars.highlightIndex = -1;
                            }
                        }
                    },
                    elementKeydown: {
                        "event": 'keydown',
                        "element": self.element,
                        "callback": function(e) { // Add keyboard listener to select box to be opened on ENTER
                            var key = e.key ? e.key : e.code,
                                focusedNotLocked = (this.element === document.activeElement) && !this.locked;
                            if (focusedNotLocked && (key === 'Enter' || key === 'NumpadEnter')) {
                                shmi.log("[IQ:iq-select-box] click on container", 1);
                                if (self.vars.isOpen) {
                                    self.vars.containerEl.blur();
                                } else {
                                    shmi.removeClass(self.vars.containerEl, 'hidden');
                                    self.vars.isOpen = true;
                                    setTimeout(function() {
                                        self.vars.containerEl.focus();
                                        shmi.visuals.session.FocusElement = self.vars.containerEl;
                                    }, shmi.c("DECOUPLE_TIMEOUT"));
                                }
                            }
                        }
                    },
                    containerKeydown: {
                        "event": "keydown",
                        "element": self.vars.containerEl,
                        "callback": function(e) {
                            var key = e.key ? e.key : e.code;
                            shmi.log("[IQ:iq-select-box] Keydown event: " + key, 1);
                            if (self.vars.highlightIndex < 0) {
                                self.vars.highlightIndex = self.getCurrentIndex();
                            }
                            shmi.log("[IQ:iq-select-box] Highlight index " + self.vars.highlightIndex, 1);
                            if (key === "Escape") {
                                self.vars.optionEls.forEach(function(optionEl) {
                                    shmi.removeClass(optionEl, 'highlighted');
                                });
                                shmi.addClass(self.vars.containerEl, 'hidden');
                                self.vars.isOpen = false;
                                self.vars.highlightIndex = -1;
                            } else if (key === "Enter" || key === "NumpadEnter") {
                                if (self.vars.highlightIndex !== -1) {
                                    shmi.log("[IQ:iq-select-box] Click on element", 0);
                                    var element = self.vars.optionEls[self.vars.highlightIndex],
                                        oldValue = self.getValue();

                                    self.setSelected(element);
                                    self.updateValue();
                                    shmi.addClass(self.vars.containerEl, 'hidden');
                                    self.vars.isOpen = false;
                                    self.vars.highlightIndex = -1;
                                    if (self.getValue() !== oldValue) {
                                        self.fire("change", { value: self.getValue() });
                                    }
                                    self.vars.optionEls.forEach(function(optionEl) {
                                        shmi.removeClass(optionEl, 'highlighted');
                                    });
                                    self.vars.highlightIndex = -1;
                                }
                            } else if (key === "ArrowUp") {
                                if (self.vars.optionEls[self.vars.highlightIndex]) {
                                    shmi.removeClass(self.vars.optionEls[self.vars.highlightIndex], 'highlighted');
                                }
                                if (self.vars.highlightIndex > 0) {
                                    self.vars.highlightIndex--;
                                } else {
                                    self.vars.highlightIndex = self.vars.optionEls.length - 1;
                                }
                                if (self.vars.optionEls[self.vars.highlightIndex]) {
                                    shmi.addClass(self.vars.optionEls[self.vars.highlightIndex], 'highlighted');
                                }
                            } else if (key === "ArrowDown") {
                                if (self.vars.optionEls[self.vars.highlightIndex]) {
                                    shmi.removeClass(self.vars.optionEls[self.vars.highlightIndex], 'highlighted');
                                }
                                if (self.vars.highlightIndex < (self.vars.optionEls.length - 1)) {
                                    self.vars.highlightIndex++;
                                } else {
                                    self.vars.highlightIndex = 0;
                                }
                                if (self.vars.optionEls[self.vars.highlightIndex]) {
                                    shmi.addClass(self.vars.optionEls[self.vars.highlightIndex], 'highlighted');
                                }
                            }
                            return false;
                        }
                    }
                };
            },

            /**
             * Generates SHMI listener object
             *
             * @returns {{onPress: onPress, onClick: onClick}}
             */
            createElementListenerObject: function() {
                var self = this;
                return {
                    onPress: function() {
                        // Left empty
                    },
                    onClick: function(x, y, e) {
                        if (e.target !== self.vars.anchorEl) {
                            shmi.log("[IQ:is-select-box] Click on container", 1);
                            if (self.vars.isOpen) {
                                self.vars.containerEl.blur();
                            } else {
                                shmi.removeClass(self.vars.containerEl, 'hidden');
                                self.vars.isOpen = true;
                                setTimeout(function() {
                                    self.vars.containerEl.focus();
                                    shmi.visuals.session.FocusElement = self.vars.containerEl;
                                }, shmi.c("DECOUPLE_TIMEOUT"));
                            }
                        }
                    }
                };
            },

            /**
             * Creates SHMI listener object
             *
             * @returns {{onPress: onPress, onRelease: onRelease, onLeave: onLeave}}
             */
            createContainerListenerObject: function() {
                var self = this;
                return {
                    onPress: function(x, y, e) {
                        if (e.currentTarget === self.vars.containerEl) {
                            self.vars.clickedInside = true;
                        }
                    },
                    onRelease: function(x, y, e) {
                        if (e.currentTarget === self.vars.containerEl) {
                            self.vars.clickedInside = false;
                        }
                    },
                    onLeave: function() {
                        shmi.log("[IQ:iq-select-box] onLeave container element", 0);
                        self.vars.containerEl.blur();
                        self.vars.highlightIndex = -1;
                    }
                };
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-select-date
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-select-date",
 *     "name": null,
 *     "template": "default/iq-select-date"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "auto-label": Whether to use the auto-label (from item)
 * "tooltip": Tooltip
 * "dateformat": The date format
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-select-date", // control name in camel-case
        uiType = "iq-select-date", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-select-date",
        "name": null,
        "template": "default/iq-select-date.variant-01",
        "label": '[Label]',
        "item": null,
        "auto-label": true,
        "tooltip": null,
        "dateformat": "${V_DATEFORMAT}",
        "daynames": [
            '${V_SUN_SHORT}',
            '${V_MON_SHORT}',
            '${V_TUE_SHORT}',
            '${V_WED_SHORT}',
            '${V_THU_SHORT}',
            '${V_FRI_SHORT}',
            '${V_SAT_SHORT}'
        ],
        "daynames-long": [
            '${V_SUN}',
            '${V_MON}',
            '${V_TUE}',
            '${V_WED}',
            '${V_THU}',
            '${V_FRI}',
            '${V_SAT}'
        ],
        "monthnames": [
            '${V_JAN}',
            '${V_FEB}',
            '${V_MAR}',
            '${V_APR}',
            '${V_MAY}',
            '${V_JUN}',
            '${V_JUL}',
            '${V_AUG}',
            '${V_SEP}',
            '${V_OCT}',
            '${V_NOV}',
            '${V_DEC}'
        ],
        "show-icon": false,
        "show-text": true,
        "icon-src": null,
        "icon-class": null,
        "icon-title": null
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,
            isOpen: true,

            // DOM Elements
            labelEl: null,
            calendarEl: null,
            daySelectEl: null,
            headerEl: null,
            dayNameEls: null,
            selectedEl: null,
            dayEls: [],

            monthSelectEl: null,
            previousMonthEl: null,
            currentMonthEl: null,
            nextMonthEl: null,

            yearSelectEl: null,
            previousYearEl: null,
            currentYearEl: null,
            nextYearEl: null,

            pressedEl: null,

            // Event Handler
            blurHandler: null,
            subscriptionTargetId: null,

            // Vars
            date: null,
            selectedDate: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues",
            dt: "visuals.tools.date"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                self.initDate();

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.iconEl = shmi.getUiElement('icon', self.element);
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.calendarEl = shmi.getUiElement('calendar', self.element);
                self.vars.daySelectEl = shmi.getUiElement('day-select', self.element);
                self.vars.headerEl = shmi.getUiElement('day-header', self.element);
                self.vars.yearSelectEl = shmi.getUiElement('year-select', self.element);

                self.vars.dayNameEls = shmi.getUiElements('day-name', self.vars.headerEl);
                self.vars.dayEls = shmi.getUiElements('day', self.element);
                self.vars.valueEl = shmi.getUiElement('value', self.element);

                self.vars.selectedEl = shmi.getUiElement('selected-date', self.element);
                if (self.vars.valueEl) {
                    self.vars.selectedEl = self.vars.valueEl;
                }

                self.vars.monthSelectEl = shmi.getUiElement('month-select', self.element);
                if (self.vars.monthSelectEl) {
                    self.vars.previousMonthEl = shmi.getUiElement('previous', self.vars.monthSelectEl);
                    self.vars.currentMonthEl = shmi.getUiElement('current', self.vars.monthSelectEl);
                    self.vars.nextMonthEl = shmi.getUiElement('next', self.vars.monthSelectEl);
                }

                if (self.vars.yearSelectEl) {
                    self.vars.previousYearEl = shmi.getUiElement('previous', self.vars.yearSelectEl);
                    self.vars.currentYearEl = shmi.getUiElement('current', self.vars.yearSelectEl);
                    self.vars.nextYearEl = shmi.getUiElement('next', self.vars.yearSelectEl);
                }

                if (!self.vars.calendarEl || !self.vars.daySelectEl || !self.vars.selectedEl || !self.vars.headerEl || !self.vars.monthSelectEl || !self.vars.yearSelectEl) {
                    shmi.log('[IQ:iq-select-date] No calendar/day-select/selected-date/value/day-header/month-select/year-select element provided', 3);
                    return;
                }

                if (!self.vars.dayNameEls || (self.vars.dayNameEls && self.vars.dayNameEls.length !== 7)) {
                    shmi.log('[IQ:iq-select-date] No day-name elements, or not the right count (7) provided', 3);
                    return;
                }

                if (!(self.vars.previousMonthEl && self.vars.currentMonthEl && self.vars.nextMonthEl && self.vars.previousYearEl && self.vars.currentYearEl && self.vars.nextYearEl)) {
                    shmi.log('[IQ:iq-select-date] Not all month & year select elements provided', 3);
                    return;
                }

                self.vars.dayNameEls.forEach(function(el, idx) {
                    el.textContent = shmi.localize(self.config.daynames[(idx === 6) ? 0 : idx + 1]);
                });

                self.vars.calendarEl.setAttribute('tabindex', "0");

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-select-date] no button-icon element provided', 1);
                } else if (self.config['icon-src'] && self.config['show-icon']) {
                    self.vars.iconEl.style.backgroundImage = `url(${self.config['icon-src']})`;
                } else if (self.config['icon-class'] && self.config['show-icon']) {
                    const iconClasses = self.config['icon-class'].trim().split(" ");
                    iconClasses.forEach(function(cls) {
                        shmi.addClass(self.vars.iconEl, cls);
                    });
                } else {
                    // No icon configured - hide
                    shmi.addClass(self.element, 'no-icon');
                }

                // LISTENERS
                self.setupListeners();

                // INTITIAL UPDATE
                self.updateCalendar();

                // Label
                setLabelImpl(self, self.config['show-text'] ? self.config.label : null);
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                if (self.vars.calendarEl) {
                    self.vars.calendarEl.addEventListener("blur", self.vars.blurHandler);
                }

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-select-date] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                if (self.vars.calendarEl) {
                    self.vars.calendarEl.removeEventListener("blur", self.vars.blurHandler);
                }

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-select-date] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                shmi.addClass(self.vars.calendarEl, 'hidden');
                self.isOpen = false;

                if (self.vars.calendarEl) {
                    self.vars.calendarEl.removeEventListener("blur", self.vars.blurHandler);
                }
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(self.element, 'locked');

                shmi.log("[IQ:iq-select-date] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                if (self.vars.calendarEl) {
                    self.vars.calendarEl.addEventListener("blur", self.vars.blurHandler);
                }

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-select-date] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this,
                    date = new Date(value * 1000),
                    oldValue = self.vars.value;

                value = parseInt(value);

                self.vars.value = value;
                self.vars.date.setFullYear(date.getYear() + 1900, date.getMonth());
                self.vars.selectedDate.setFullYear(date.getYear() + 1900, date.getMonth(), date.getDate());
                self.updateCalendar();

                if (oldValue !== self.vars.value) {
                    self.fire("change", {
                        value: self.vars.value
                    });
                }
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                var self = this;

                return self.vars.value;
            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label'] && self.config['show-text']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * Updates the date with the given month
             *
             * @param {int} newMonth
             * @param {object} el
             */
            updateDateMonth: function(newMonth, el) {
                var self = this;

                self.vars.date.setFullYear(self.vars.date.getYear() + 1900, newMonth);
                self.vars.selectedDate.setFullYear(self.vars.date.getYear() + 1900, newMonth, el.textContent);
            },

            /**
             * Sets up the listeners, moved here to make onInit shorter
             */
            setupListeners: function() {
                var self = this;

                // MONTH: PREVIOUS
                var previousMonthFunctions = {
                    onPress: function() {
                    },
                    onClick: function(x, y, event) {
                        self.vars.date.setFullYear(self.vars.date.getYear() + 1900, self.vars.date.getMonth() - 1);
                        self.updateCalendar();
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.previousMonthEl, previousMonthFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.previousMonthEl, previousMonthFunctions));

                // MONTH: NEXT
                var nextMonthFunctions = {
                    onPress: function() {
                    },
                    onClick: function(x, y, e) {
                        self.vars.date.setFullYear(self.vars.date.getYear() + 1900, self.vars.date.getMonth() + 1);
                        self.updateCalendar();
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.nextMonthEl, nextMonthFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.nextMonthEl, nextMonthFunctions));

                // YEAR: PREVIOUS
                var previousYearFunctions = {
                    onPress: function() {
                    },
                    onClick: function(x, y, e) {
                        self.vars.date.setFullYear(self.vars.date.getYear() + 1900 - 1, self.vars.date.getMonth());
                        self.updateCalendar();
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.previousYearEl, previousYearFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.previousYearEl, previousYearFunctions));

                // YEAR: NEXT
                var nextYearFunctions = {
                    onPress: function() {
                    },
                    onClick: function(x, y, e) {
                        self.vars.date.setFullYear(self.vars.date.getYear() + 1900 + 1, self.vars.date.getMonth());
                        self.updateCalendar();
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.nextYearEl, nextYearFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.nextYearEl, nextYearFunctions));

                // DAY
                self.vars.pressedEl = null;
                var dayFunctions = {
                    onPress: function(x, y, e) {
                        shmi.addClass(e.target, 'pressed');
                        self.vars.pressedEl = e.target;
                    },
                    onRelease: function(x, y, e) {
                        shmi.removeClass(self.vars.pressedEl, 'pressed');
                    },
                    onClick: function(x, y, e) {
                        var dayEl = e.target;
                        if (shmi.hasClass(dayEl, 'iq-previous-month-day')) {
                            self.updateDateMonth(self.vars.date.getMonth() - 1, dayEl);
                        } else if (shmi.hasClass(dayEl, 'iq-current-month-day')) {
                            self.updateDateMonth(self.vars.date.getMonth(), dayEl);
                        } else if (shmi.hasClass(dayEl, 'iq-next-month-day')) {
                            self.updateDateMonth(self.vars.date.getMonth() + 1, dayEl);
                        }

                        self.updateCalendar();

                        if (self.config.item) {
                            self.imports.im.writeValue(self.config.item, self.vars.value);
                        }

                        shmi.addClass(self.vars.calendarEl, 'hidden');

                        self.vars.isOpen = false;
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.daySelectEl, dayFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.daySelectEl, dayFunctions));

                // CALENDAR SELECTOR
                shmi.addClass(self.vars.calendarEl, 'hidden');
                self.vars.isOpen = false;
                var selectedFunctions = {
                    onClick: function() {
                        if (!self.vars.isOpen) {
                            shmi.removeClass(self.vars.calendarEl, 'hidden');
                            self.isOpen = true;
                            shmi.decouple(() => {
                                self.vars.calendarEl.focus();
                                shmi.visuals.session.FocusElement = self.vars.calendarEl;
                            });
                        } else {
                            self.vars.calendarEl.blur();
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.element, selectedFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.element, selectedFunctions));

                self.vars.blurHandler = function() {
                    shmi.addClass(self.vars.calendarEl, 'hidden');
                    shmi.decouple(() => {
                        self.vars.isOpen = false;
                    });
                };
            },
            /**
             * Updates the calendar element
             *
             */
            updateCalendar: function() {
                var self = this,
                    oldValue = self.vars.value,
                    date = new Date(self.vars.value * 1000);

                self.initDate();

                date.setFullYear(self.vars.selectedDate.getYear() + 1900, self.vars.selectedDate.getMonth(), self.vars.selectedDate.getDate());
                self.vars.value = date.getTime() / 1000;

                shmi.log("[IQ:iq-select-date] selected time: " + self.vars.value, 0);

                var currentMonth = self.vars.date.getMonth(),
                    currentYear = self.vars.date.getYear() + 1900,
                    firstDay = new Date(currentYear, currentMonth, 1).getDay(),
                    start = firstDay - 1;

                self.vars.currentMonthEl.textContent = shmi.localize(self.config.monthnames[currentMonth]);
                self.vars.currentYearEl.textContent = currentYear;

                if (firstDay === 1) {
                    start = 7;
                } else if (firstDay === 0) {
                    start = 6;
                }

                var prevDays = self.getDaysOfMonth(currentMonth, currentYear),
                    curDays = self.getDaysOfMonth(currentMonth + 1, currentYear),
                    tempDayEl = shmi.getUiElements('day', self.element),
                    tempDay = prevDays - start + 1;

                for (var i = 0; i < start; i++) {
                    tempDayEl[i].className = 'iq-previous-month-day';
                    tempDayEl[i].textContent = tempDay;
                    if (new Date(currentYear, currentMonth - 1, tempDay).getTime() === self.vars.selectedDate.getTime()) {
                        shmi.addClass(tempDayEl[i], 'iq-selected');
                    } else {
                        shmi.removeClass(tempDayEl[i], 'iq-selected');
                    }
                    tempDay++;
                }

                tempDay = 1;
                for (i = start; i < curDays + start; i++) {
                    tempDayEl[i].className = 'iq-current-month-day';
                    tempDayEl[i].textContent = tempDay;
                    if (new Date(currentYear, currentMonth, tempDay).getTime() === self.vars.selectedDate.getTime()) {
                        shmi.addClass(tempDayEl[i], 'iq-selected');
                    } else {
                        shmi.removeClass(tempDayEl[i], 'iq-selected');
                    }
                    tempDay++;
                }

                tempDay = 1;
                for (i = curDays + start; i < tempDayEl.length; i++) {
                    tempDayEl[i].className = 'iq-next-month-day';
                    tempDayEl[i].textContent = tempDay;
                    if (new Date(currentYear, currentMonth + 1, tempDay).getTime() === self.vars.selectedDate.getTime()) {
                        shmi.addClass(tempDayEl[i], 'iq-selected');
                    } else {
                        shmi.removeClass(tempDayEl[i], 'iq-selected');
                    }
                    tempDay++;
                }

                var currentDate = shmi.localize(self.config.dateformat);
                if (typeof (currentDate) === 'string') {
                    self.vars.selectedEl.textContent = self.imports.dt.formatDateTime(self.vars.selectedDate, { datestring: currentDate });
                } else {
                    self.vars.selectedEl.innerHTML = '&nbsp;';
                }

                if (oldValue !== self.vars.value) {
                    self.fire("change", {
                        value: self.vars.value
                    });
                }
            },

            /**
             * Returns number of days for month of year
             *
             * @param month - month of year
             * @param year - year
             * @return days days in month
             */
            getDaysOfMonth: function(month, year) {
                var d = new Date(year, month, 0);
                return d.getDate();
            },

            /**
             * Initializes the date if not set. As setValue might be called before onInit has finished we need to do it like this
             */
            initDate: function() {
                var self = this;

                // Initialize date
                if (self.vars.date === null) {
                    self.vars.date = new Date();
                }

                if (self.vars.selectedDate === null) {
                    self.vars.selectedDate = new Date(new Date().setHours(0, 0, 0, 0));
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-select-radio
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-select-radio",
 *     "name": null,
 *     "template": "default/iq-select-radio.iq-variant-01"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "auto-label": Whether to use the auto-label (from item)
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-select-radio", // control name in camel-case
        uiType = "iq-select-radio", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-select-radio",
        "name": null,
        "template": "default/iq-select-radio.variant-01",
        "label": '[Label]',
        "item": null,
        "auto-label": true,
        "show-icon": false,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "options": [],
        "selected": -1
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: -1,
            active: false,

            // DOM Elements
            optionContainerEl: null,
            optionEls: [],
            labelEl: null,
            iconEl: null,
            iconClassEl: null,
            label: null,
            selectedEl: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: [
            "change"
        ],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            // Note: onTemplate has not been implemented in this version anymore as variants should be used instead

            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);
                self.vars.optionEl = shmi.getUiElement('option', self.element); // To be duplicated!
                self.vars.optionContainerEl = shmi.getUiElement('option-container', self.element);

                // Verify options set and available in HTML template
                if (!self.config['options']) {
                    shmi.log("[IQ:iq-select-radio] No options defined in config", 1);
                    return;
                } else {
                    shmi.log("[IQ:iq-select-radio] Options newly defined", 1);
                }
                if (!self.vars.optionEl || !self.vars.optionContainerEl) {
                    shmi.log("[IQ:iq-select-radio] Missing option and/or option container element in template!", 1);
                    return;
                }

                // Remove initial option from DOM after cloning it for reuse
                var clone = self.vars.optionEl.cloneNode(true);
                self.vars.optionContainerEl.removeChild(self.vars.optionEl);
                self.vars.optionEl = clone;

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-select-radio] no button-icon element provided', 1);
                } else if (!self.config['show-icon']) {
                    // Icon disabled
                    self.vars.iconEl.style.display = 'none';
                } else if (self.config['icon-src']) {
                    if (self.vars.iconEl) {
                        self.vars.iconEl.style.backgroundImage = `url(${self.config['icon-src']})`;
                    }
                } else if (self.config['icon-class']) {
                    if (self.vars.iconEl) {
                        shmi.removeClass(self.vars.iconEl, "iq-icon");
                        shmi.addClass(self.vars.iconEl, "iq-icon-class");
                        var iconClasses = self.config['icon-class'].trim().split(" ");
                        iconClasses.forEach(function(cls) {
                            shmi.addClass(self.vars.iconEl, cls);
                        });
                    }
                }

                // Label
                setLabelImpl(self, self.config.label);

                self.rebuildOptions();
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-select-radio] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-select-radio] disabled", 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                //self.vars.currentValueEl.blur();
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(this.element, 'locked');

                shmi.log("[IQ:iq-select-radio] Locked", 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                shmi.log("[IQ:iq-select-radio] unlocked", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this,
                    changed = (self.vars.value !== value);

                self.vars.value = value;

                const optionIdx = self.config.options.findIndex((option) => option.value === value);
                if (optionIdx !== -1) {
                    self.setSelected(self.vars.optionEls[optionIdx]);
                } else {
                    self.setSelected(null);
                }

                shmi.log("[IQ:iq-select-radio] value set: " + value, 1);
                if (changed) {
                    this.fire('change', { value: self.vars.value });
                }
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                return this.vars.value;
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step) {
                var self = this;
                self.imports.nv.setProperties(self, arguments);
            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            },

            /**
             * (Re-)Creates the widgets options elements.
             *
             * @param {boolean} enableListeners Whether or not to enable listeners attached to the option elements.
             */
            rebuildOptions: function(enableListeners) {
                var self = this;

                // Remove container listeners
                self.vars.listeners.forEach(function(listener) {
                    listener.disable();
                });
                self.vars.listeners = [];

                // We're going to build the dynamic option list by cloning the option element in the template which we have removed from the DOM before
                const containerEl = self.vars.optionContainerEl,
                    optionTemplate = self.vars.optionEl;

                // Clear content
                self.vars.optionEls = [];
                containerEl.innerHTML = '';

                // Add options one-by-one to the container
                self.config.options.forEach(function(option, index) {
                    const newOption = optionTemplate.cloneNode(true);
                    newOption.dataset.value = option.value;
                    newOption.dataset.no = index;

                    const labelEl = shmi.getUiElement('option-label', newOption);
                    if (option.label && labelEl) {
                        labelEl.textContent = shmi.localize(option.label);
                    } else {
                        shmi.addClass(newOption, "no-label");
                    }

                    // Prefer icon over icon class
                    const iconEl = shmi.getUiElement('option-icon', newOption);
                    if (option['icon-src'] && iconEl) {
                        iconEl.style.backgroundImage = `url(${option['icon-src']})`;
                    } else if (option['icon-class'] && iconEl) {
                        shmi.addClass(iconEl, option['icon-class']);
                    } else {
                        shmi.addClass(newOption, "no-icon");
                    }

                    if (option['tooltip'] && iconEl) {
                        iconEl.setAttribute("title", shmi.localize(option['tooltip']));
                    }

                    containerEl.appendChild(newOption);

                    self.vars.optionEls.push(newOption);
                });

                // Event listeners
                var eventFunctions = {
                    onPress: function onPress(x, y, e) {
                        shmi.addClass(e.currentTarget, 'pressed');
                    },

                    onRelease: function onRelease(x, y, e) {
                        self.vars.optionEls.forEach((element) => {
                            shmi.removeClass(element, 'pressed');
                        });
                    },

                    onClick: function onClick(x, y, e) {
                        var targetEl = e.target,
                            useTargetEl = null,
                            changed = false,
                            value = null;

                        if (typeof targetEl.dataset.ui !== 'undefined' && targetEl.dataset.ui === 'option') {
                            // We already have the correct option element!
                            useTargetEl = targetEl;
                        } else {
                            // We have to traverse the tree up until we find it
                            useTargetEl = targetEl.closest('[data-ui="option"]');
                        }
                        if (!useTargetEl) {
                            console.error(self.uiType, "Could not match clicked option");
                        } else if (typeof useTargetEl.dataset.no === 'undefined') {
                            // Should not happen as we add it ourselves!
                            console.error(self.uiType, "Could not match clicked option (data-no missing!?)");
                            console.log(useTargetEl);
                        } else {
                            value = self.config.options[parseInt(useTargetEl.dataset.no)];
                            changed = (value.value !== self.vars.value);
                            self.vars.value = value.value;
                            self.setSelected(useTargetEl);
                            self.updateValue();
                            if (changed) {
                                self.fire('change', { value: self.vars.value });
                            }
                        }
                    }
                };
                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.vars.optionContainerEl, eventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.vars.optionContainerEl, eventFunctions));
                if (enableListeners) {
                    self.listeners.forEach(function(listener) {
                        listener.enable();
                    });
                }
            },

            /**
             * Sets the selected element, not called by VISUALS
             *
             * @param element - element to select
             */
            setSelected: function(element) {
                const self = this;

                self.vars.optionEls.forEach((el) => {
                    if (element !== el) {
                        shmi.removeClass(el, 'selected');
                    } else {
                        shmi.addClass(el, 'selected');
                    }
                });

                self.vars.selectedEl = element;
            },

            /**
             * @returns {number}
             */
            getCurrentIndex: function() {
                var self = this,
                    selectedEl = self.vars.selectedEl;

                return self.vars.optionEls.findIndex((element) => selectedEl === element);
            },

            /**
             * Can be called externally, not called by VISUALS
             * @param options
             */
            setOptions: function(options) {
                var self = this;

                self.config.options = options;

                // reset properties
                self.vars.value = -1;
                self.vars.selectedEl = null;

                self.rebuildOptions(self.active);
            },

            /**
             * Writes current value to connected data source
             */
            updateValue: function() {
                var self = this;
                if (self.config.item) {
                    shmi.visuals.session.ItemManager.writeValue(this.config.item, self.vars.value);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-select-time
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-select-time",
 *     "name": null,
 *     "template": "custom/controls/iq-select-time"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "type": Type is INT
 * "auto-label": Whether to use the auto-label (from item)
 * "tooltip": Tooltip
 * "isUTC": Whether the timestamp given is UTC time (and not local time)
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-select-time", // control name in camel-case
        uiType = "iq-select-time", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-select-time",
        "name": null,
        "template": "default/iq-select-time.iq-variant-01",
        "label": '[Label]',
        "item": null,
        "auto-label": true,
        "numpad-enabled": false,
        "tooltip": null,
        "isUTC": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Sets the label text and handles toggling the `no-label` style.
     *
     * @param {*} self Reference to the widget
     * @param {?string} labelText Label text to set
     */
    function setLabelImpl(self, labelText) {
        if (!self.vars.labelEl) {
            // Nothing to do.
        } else if (labelText === "" || labelText === null || typeof labelText === "undefined") {
            self.vars.label = "";
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        } else {
            self.vars.label = labelText;
            self.vars.labelEl.textContent = shmi.localize(labelText);
            shmi.removeClass(self.element, "no-label");
        }
    }

    function setInputDisabledState(self, disabled) {
        if (self.vars.hourEl) {
            self.vars.hourEl.disabled = disabled;
        }

        if (self.vars.minuteEl) {
            self.vars.minuteEl.disabled = disabled;
        }

        if (self.vars.secondEl) {
            self.vars.secondEl.disabled = disabled;
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,
            label: null,
            subscriptionTargetId: null,

            // DOM Elements
            labelEl: null,
            hourEl: null,
            minuteEl: null,
            secondEl: null,

            hours: 0,
            minutes: 0,
            seconds: 0,
            evListeners: [],

            itemNeedsString: false
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.labelEl = shmi.getUiElement('label', self.element);

                self.setupRockerComponent({
                    varName: "hourEl",
                    uiElementName: 'hour-select',
                    inputValidator: self.validateGeneric.bind(this, /^[0-1]?[0-9]$|^2[0-3]$/, 'hours'),
                    addHandler: self.addHours.bind(self),
                    max: 23,
                    label: '${V_HOURS}'
                });

                self.setupRockerComponent({
                    varName: "minuteEl",
                    uiElementName: 'minute-select',
                    inputValidator: self.validateGeneric.bind(this, /^[0-5]?[0-9]$/, 'minutes'),
                    addHandler: self.addMinutes.bind(self),
                    max: 59,
                    label: '${V_MINUTES}'
                });

                self.setupRockerComponent({
                    varName: "secondEl",
                    uiElementName: 'second-select',
                    inputValidator: self.validateGeneric.bind(this, /^[0-5]?[0-9]$/, 'seconds'),
                    addHandler: self.addSeconds.bind(self),
                    max: 59,
                    label: '${V_SECONDS}'
                });

                self.updateInputFields(self.vars);

                // Label
                setLabelImpl(self, self.config.label);
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                setInputDisabledState(self, false);

                self.log('Enabled', 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                setInputDisabledState(self, true);

                self.log('Disabled', 1);
            },

            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;

                setInputDisabledState(self, true);

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                shmi.addClass(self.element, 'locked');

                self.log('Locked', 1);
            },

            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;

                setInputDisabledState(self, false);

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                shmi.removeClass(self.element, 'locked');

                self.log('Unlocked', 1);
            },

            onDelete: function() {
                var self = this;

                self.vars.evListeners.forEach(function(info) {
                    info.element.removeEventListener(info.eventType, info.listener);
                });
                self.vars.evListeners = [];
                self.vars.listeners = [];
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this;

                value = parseInt(value);
                var date = new Date(value * 1000);

                self.vars.itemNeedsString = (typeof (value) === 'string');

                if (self.config.isUTC) {
                    self.vars.hours = date.getUTCHours();
                    self.vars.minutes = date.getUTCMinutes();
                    self.vars.seconds = date.getUTCSeconds();
                } else {
                    self.vars.hours = date.getHours();
                    self.vars.minutes = date.getMinutes();
                    self.vars.seconds = date.getSeconds();
                }

                self.updateInputFields(self.vars);

                if (self.vars.value !== value) {
                    self.vars.value = value;
                    self.fire('change', { value: value });
                }
            },

            /**
             * Retrieves current value of the Rocker Button
             *
             * @return value - current value
             */
            getValue: function() {
                var self = this;

                return self.vars.value;
            },

            /** Returns the number of hours. */
            getHours: function() {
                var self = this;

                return self.vars.hours;
            },

            /** Returns the number of minutes. */
            getMinutes: function() {
                var self = this;

                return self.vars.minutes;
            },

            /** Returns the number of seconds. */
            getSeconds: function() {
                var self = this;

                return self.vars.seconds;
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step, name, type, warnmin, warnmax, prewarnmin, prewarnmax, precision) {

            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label']) {
                    setLabelImpl(self, labelText);
                }
            },

            log: function(msg, level) {
                shmi.log('[IQ:iq-select-time] '+msg, level);
            },

            getType: function(v) {
                if (Array.isArray(v)) {
                    return 'array';
                } else if (v === null) {
                    return 'null';
                } else {
                    return typeof (v);
                }
            },

            /**
             * Checks whether or not the given value is of one of the expected types.
             *
             * @param {*} value Value to check
             * @param {string|string[]} expected Expected type or types of the provided value.
             */
            checkType: function(value, expected) {
                var self = this;

                if (Array.isArray(expected)) {
                    if (!(value in expected)) {
                        return false;
                    }
                } else if (expected !== self.getType(value)) {
                    return false;
                }

                return true;
            },

            /**
             * Throws an exception if the given value is not of the provided type.
             *
             * @param {*} value Value to check
             * @param {string|string[]} expected Expected type or types of the provided value.
             * @param {string} [message] Error message
             *
             * @throws {TypeError}
             */
            assertType: function(value, expected, message) {
                var self = this;

                if (!self.checkType(value, expected)) {
                    throw TypeError(message);
                }
            },

            /**
             * Compute new control value from the controls current state.
             */
            computeNewValue: function() {
                var self = this,
                    vars = self.vars,
                    date = new Date(vars.value * 1000);

                if (self.config.isUTC) {
                    date.setUTCHours(vars.hours);
                    date.setUTCMinutes(vars.minutes);
                    date.setUTCSeconds(vars.seconds);
                } else {
                    date.setHours(vars.hours);
                    date.setMinutes(vars.minutes);
                    date.setSeconds(vars.seconds);
                }

                return date.getTime() / 1000;
            },

            /**
             * Compute the controls new value, update its items' value and fire a
             * `change` event if the value actually changed.
             */
            updateValue: function() {
                var self = this;

                var newValue = self.computeNewValue();

                if (self.config.item) {
                    // Make sure we write a string to the item if it was a string before
                    if (self.vars.itemNeedsString) {
                        self.imports.im.writeValue(self.config.item, String(newValue));
                    } else {
                        self.imports.im.writeValue(self.config.item, newValue);
                    }
                }

                // Only fire a 'changed' event if the value actually changed.
                if (self.vars.value !== newValue) {
                    self.vars.value = newValue;
                    self.fire('change', { value: newValue });
                }

                self.updateInputFields(self.vars);
            },

            /**
             * @param {int} val Integer to convert to a number within the group
             * @param {int} N Modulo of the group
             */
            integerToGroup: function(val, N) {
                return (N + (val % N)) % N;
            },

            /**
             * Adds seconds to the current time. Does overflow around into minutes.
             *
             * @param {int} x Number of seconds to add
             */
            addSeconds: function(x) {
                var self = this;

                self.assertType(x, 'number');

                if (self.vars.secondEl) {
                    self.vars.secondEl.blur();
                }
                self.vars.seconds = self.integerToGroup(self.vars.seconds + x, 60);

                self.updateValue();
            },

            /**
             * Adds minutes to the current time. Does overflow around into hours.
             *
             * @param {int} x Number of minutes to add
             */
            addMinutes: function(x) {
                var self = this;

                self.assertType(x, 'number');

                if (self.vars.minuteEl) {
                    self.vars.minuteEl.blur();
                }

                self.vars.minutes = self.integerToGroup(self.vars.minutes + x, 60);
                self.updateValue();
            },

            /**
             * Adds hours to the current time. Does overflow around into days.
             *
             * @param {int} x Number of hours to add
             */
            addHours: function(x) {
                var self = this;

                self.assertType(x, 'number');

                if (self.vars.hourEl) {
                    self.vars.hourEl.blur();
                }

                self.vars.hours = self.integerToGroup(self.vars.hours + x, 24);

                self.updateValue();
            },

            /**
             * Validates and stores the nodes value. If the nodes value is invalid, it
             * is overwritten with the last known-to-be-good value.
             *
             * @param {RegExp} regex A regex that has to match the elements textContent
             * @param {string} timecomponent Component to verify. Can be 'seconds', 'minutes' or 'hours'
             * @param {Node} element Node of the input field to validate
             */
            validateGeneric: function(regex, timecomponent, element) {
                var self = this;

                if (RegExp(regex).test(element.value)) {
                    self.vars[timecomponent] = parseInt(element.value);
                    self.updateValue(self);
                    if (self.vars[timecomponent] < 10 && element.value.length === 1) {
                        element.value = '0' + element.value;
                    }
                } else if (self.vars[timecomponent] < 10) {
                    element.value = '0' + self.vars[timecomponent];
                } else {
                    element.value = self.vars[timecomponent];
                }
            },

            /**
             * Keypress listener for h/m/s input fields.
             *
             * @param {Node} element Node the listener is attached to
             * @param {Event} event The event
             */
            inputKeypressListener: function(element, event) {
                if (Number(event.keyCode) === 13) {
                    event.preventDefault();
                    window.getSelection().removeAllRanges();
                    element.blur();
                }
            },

            /**
             * Keyup listener for h/m/s input fields.
             *
             * @param {Node} element Node the listener is attached to
             * @param {Event} event The event
             */
            inputChangeListener: function(element, event) {
                if (Number(event.keyCode) === 13) {
                    // Don't handle enter/return key
                } else if (element.value.length > 1) {
                    window.getSelection().removeAllRanges();
                    element.blur();
                }
            },

            /**
             * Blur listener for h/m/s input fields.
             *
             * @param {Node} element Node the listener is attached to
             * @param {function} validateFunc
             * @param {Event} event The event
             */
            inputBlurListener: function(element, validateFunc, event) {
                window.getSelection().removeAllRanges();
                shmi.removeClass(element, 'selectableText');
                validateFunc(element);
            },

            /**
             * Attaches mouse and touch listeners to a given node. The listeners
             * references are stored in the controls variable store and are enabled
             * or disabled by the control.
             *
             * @param {object} vars Reference to the controls variable storage
             * @param {Node} element Element to attach the listeners to
             * @param {Function} functions Listener callback
             */
            addClickTouchListener: function(vars, element, functions) {
                vars.listeners.push(new shmi.visuals.io.MouseListener(element, functions));
                vars.listeners.push(new shmi.visuals.io.TouchListener(element, functions));
            },

            /**
             * Attach an event listener to a given node. The listeners are detached
             * when the control is destroyed.
             *
             * @param {object} vars Reference to the controls variable storage
             * @param {Node} element Element to attach the event listener to
             * @param {string} eventType Type of the event to attach the listener to.
             * @param {Function} listener Listener callback
             */
            addEventListener: function(vars, element, eventType, listener) {
                element.addEventListener(eventType, listener);

                vars.evListeners.push({
                    element: element,
                    eventType: eventType,
                    listener: listener
                });
            },

            /**
             * Creates input field on specified elemnt
             *
             * @param {object} vars Reference to the controls variable storage
             * @param {Node} element Element for input field
             * @param {Function} validateFunc Validation function
             * @param {object} numpadConfig Numpad configuration
             */
            createInputField: function(vars, element, validateFunc, numpadConfig) {
                var self = this;

                if (!element) {
                    self.log('no base element provided', 1);
                    return;
                }

                var appConfig = shmi.requires("visuals.session.config"),
                    keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

                numpadConfig = shmi.cloneObject(numpadConfig);
                numpadConfig.callback = function(res) {
                    element.value = res;
                    validateFunc(element);
                };

                self.addClickTouchListener(vars, element, {
                    onPress: (x, y, event) => {
                        if (numpadConfig.numpadEnabled || keyboardEnabled) {
                            event.preventDefault();
                            event.stopPropagation();
                        }
                    },
                    onClick: function onClick(x, y, event) {
                        if (numpadConfig.numpadEnabled || keyboardEnabled) {
                            numpadConfig.value = element.value;
                            shmi.numpad(numpadConfig);
                        }
                    }
                });

                self.addEventListener(vars, element, 'keypress', self.inputKeypressListener.bind(null, element));
                self.addEventListener(vars, element, 'input', self.inputChangeListener.bind(null, element));
                self.addEventListener(vars, element, 'blur', self.inputBlurListener.bind(null, element, validateFunc));
                self.addEventListener(vars, element, 'focus', (event) => {
                    if (numpadConfig.numpadEnabled || keyboardEnabled) {
                        numpadConfig.value = element.value;
                        shmi.numpad(numpadConfig);
                    } else {
                        element.select();
                    }
                });
            },

            /**
             * Adds listeners to a "rocker component" (+/- buttons and input field)
             *
             * @param {object} configuration Rocker component configuration
             */
            setupRockerComponent: function(configuration) {
                var self = this;

                var elementComponent = shmi.getUiElement(configuration.uiElementName, self.element);
                if (!elementComponent) {
                    self.log('Element not found: ' + configuration.uiElementName, 1);
                    return false;
                }

                var elementPrev = shmi.getUiElement('previous', elementComponent);
                if (!elementPrev) {
                    self.log('No UI element found for ' + configuration.uiElementName + '.previous', 1);
                    return false;
                }

                var elementCurrent = shmi.getUiElement('current', elementComponent);
                if (!elementCurrent) {
                    self.log('No UI element found for ' + configuration.uiElementName + '.current', 1);
                    return false;
                }

                var elementNext = shmi.getUiElement('next', elementComponent);
                if (!elementNext) {
                    self.log('No UI element found for ' + configuration.uiElementName + '.next', 1);
                    return false;
                }

                self.createInputField(
                    self.vars,
                    elementCurrent,
                    configuration.inputValidator.bind(self),
                    {
                        min: 0,
                        max: configuration.max,
                        label: configuration.label,
                        numpadEnabled: self.config["numpad-enabled"] || false
                    }
                );

                self.addClickTouchListener(self.vars, elementPrev, {
                    onPress: shmi.addClass.bind(null, elementPrev, 'pressed'),
                    onRelease: shmi.removeClass.bind(null, elementPrev, 'pressed'),
                    onClick: configuration.addHandler.bind(null, -1)
                });

                self.addClickTouchListener(self.vars, elementNext, {
                    onPress: shmi.addClass.bind(null, elementNext, 'pressed'),
                    onRelease: shmi.removeClass.bind(null, elementNext, 'pressed'),
                    onClick: configuration.addHandler.bind(null, 1)
                });

                self.vars[configuration.varName] = elementCurrent;

                return true;
            },

            updateInputFields: function(vars) {
                var self = this;

                if (self.vars.hourEl) {
                    self.vars.hourEl.value = (vars.hours > 9) ? vars.hours : ('0' + vars.hours);
                }
                if (self.vars.minuteEl) {
                    self.vars.minuteEl.value = (vars.minutes > 9) ? vars.minutes : ('0' + vars.minutes);
                }
                if (self.vars.secondEl) {
                    self.vars.secondEl.value = (vars.seconds > 9) ? vars.seconds : ('0' + vars.seconds);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals shape widget.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-shape",
 *     "name": null,
 *     "template": "default/iq-shape",
 *     "polygons": [],
 *     "rotation": 0,
 *     "scale": 1
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * polygons {number[][]}: Array of 2d vectors used as polygons. Must contain at least 3 vectors in polygon mode.
 * rotation {number}: Rotation in degrees to apply to the shape.
 * scale {number}: Scaling factor to apply to the shape.
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqShape", // control name in camel-case
        uiType = "iq-shape", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    const defConfig = {
        "class-name": "iq-shape",
        "name": null,
        "template": "default/iq-shape",
        "polygons": [],
        "rotation": 0,
        "scale": 1
    };

    // declare private functions - START

    /**
     * Calculates a rotation matrix for a given angle.
     *
     * @param {number} rotation Rotation angle in radiants
     * @returns {number[][]} 2D rotation matrix for the given angle as array of arrays
     */
    function calculateRotationMatrix2D(rotation) {
        return [
            [Math.cos(rotation), -Math.sin(rotation)],
            [Math.sin(rotation), Math.cos(rotation)]
        ];
    }

    /**
     * Returns the sum of two 2D vectors.
     *
     * @param {number[]} vector
     * @param {number[]} translate
     * @returns {number[][]}
     */
    function translateVector2D(vector, translate) {
        return [vector[0] + translate[0], vector[1] + translate[1]];
    }

    /**
     * Returns the result of the matrix multiplication of M x V.
     *
     * @param {number[]} vector
     * @param {number[][]} matrix
     * @returns {number[]}
     */
    function rotateVector2D(vector, matrix) {
        return [
            matrix[0][0] * vector[0] + matrix[0][1] * vector[1],
            matrix[1][0] * vector[0] + matrix[1][1] * vector[1]
        ];
    }

    /**
     * Rotates a vector using [0.5, 0.5] as origin.
     *
     * @param {number[]} vector
     * @param {number[][]} matrix
     * @returns {number[]}
     */
    function rotateVector2DCenter(vector, matrix) {
        return translateVector2D(
            rotateVector2D(
                translateVector2D(vector, [-0.5, -0.5]),
                matrix
            ),
            [0.5, 0.5]
        );
    }

    function scaleVector2D(vector, scale) {
        return [vector[0] * scale, vector[1] * scale];
    }

    /**
     * Scales a vector using [0.5, 0.5] as origin.
     *
     * @param {number[]} vector
     * @param {number[][]} matrix
     * @returns {number[]}
     */
    function scaleVector2DCentered(vector, scale) {
        return translateVector2D(
            scaleVector2D(
                translateVector2D(vector, [-0.5, -0.5]),
                scale
            ),
            [0.5, 0.5]
        );
    }

    /**
     * Creates a point-string as understood by the <polygon>-tag from the given
     * polygons.
     *
     * @param {number} width Width of the SVG
     * @param {number} height Height of the SVG
     * @param {number[][]} polygons Polygons
     * @param {number} strokeWidth
     * @returns {string}
     */
    function makePolygonPoints(width, height, polygons) {
        return polygons.map(([x, y]) => `${x * width},${y * height}`).join(" ");
    }

    /**
     * Returns an object with elements.
     *
     * @param {string[]} elements UI Elements to get.
     * @param {Element} baseElement Root node for the search.
     * @returns {Object}
     */
    function getUiElements(elements, baseElement) {
        const obj = {};
        const allElementsPresent = elements.every((elementName) => {
            const element = shmi.getUiElement(elementName, baseElement);
            if (!element) {
                console.error(`[${className}] "${elementName}" element is missing in template.`);
                return false;
            }

            obj[elementName] = element;
            return true;
        });

        return allElementsPresent ? obj : null;
    }
    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {},
        /* imports added at runtime */
        imports: {},

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function onInit() {
                const svgElement = shmi.getUiElement("shape", this.element),
                    rotMatrix = calculateRotationMatrix2D((this.config.rotation || 0) / 360 * 2 * Math.PI);

                if (!svgElement) {
                    console.error(`[${className}] "shape" element is missing in template.`);
                    return;
                }

                const elements = getUiElements(["shape-polygon", "clip-path", "clip-path-polygon"], svgElement);
                if (!elements) {
                    return;
                }

                // Setup "unique" IDs for the svg.
                const randomId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
                Object.entries(elements).forEach(([key, el]) => el.setAttribute("id", `${key}-${randomId}`));
                elements["shape-polygon"].setAttribute("clip-path", `url(#clip-path-${randomId})`);

                // Compute polygons.
                const polygons = (this.config.polygons || []).map((poly) => scaleVector2DCentered(rotateVector2DCenter(poly, rotMatrix), this.config.scale / 100)),
                    points = makePolygonPoints(svgElement.viewBox.baseVal.width, svgElement.viewBox.baseVal.height, polygons);

                // Set attributes.
                switch (elements["shape-polygon"].tagName) {
                case "polygon":
                    elements["shape-polygon"].setAttribute("points", points);
                    elements["clip-path-polygon"].setAttribute("points", points);
                    break;
                default:
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals iq-slider control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-slider",
 *     "name": null,
 *     "template": "default/iq-slider.iq-variant-01"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqSlider", // control name in camel-case
        uiType = "iq-slider", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    const defConfig = {
            "class-name": uiType,
            "name": null,
            "template": "default/iq-slider.iq-variant-01",
            "send-interval": 100,
            "animation-duration": 100,
            "continuous": true,
            "min": 0,
            "max": 100,
            "step": 0,
            "precision": -1,
            "type": shmi.c("TYPE_FLOAT"),
            "label": "[Label]",
            "unit-text": "[Unit]",
            "decimal-delimiter": ".",
            "auto-min": true,
            "auto-max": true,
            "auto-step": true,
            "auto-label": true,
            "auto-unit-text": true,
            "auto-type": true,
            "auto-precision": true,
            "initial-text": "-",
            "show-text": true,
            "show-icon": false,
            "icon-src": null,
            "icon-class": null,
            "inverted": false
        },
        cssClasses = {
            NO_LABEL: "no-label",
            NO_ICON: "no-icon",
            NO_UNIT: "no-unit",
            LOCKED: "locked",
            PRESSED: "pressed",
            INVERTED: "invert-slider"
        },
        orientations = {
            VERTICAL: "vertical",
            HORIZONTAL: "horizontal"
        };

    // declare private functions - START

    /**
     * setLabel - set label text for widget. applies "no-label" CSS class when no text is specified
     *
     * @param {object} self control instance
     * @param {string} [value] text value
     */
    function setLabel(self, value) {
        const { elements } = self.vars;

        if (typeof value === "string" && value.length) {
            elements.label.textContent = shmi.localize(value);
            shmi.removeClass(self.element, cssClasses.NO_LABEL);
        } else {
            elements.label.textContent = "";
            shmi.addClass(self.element, cssClasses.NO_LABEL);
        }
    }

    /**
     * setUnit - set unit text for widget. applies "no-unit" CSS class when no text is specified
     *
     * @param {object} self control instance
     * @param {string} [value] text value
     */
    function setUnit(self, value) {
        const { elements } = self.vars;

        if (typeof value === "string" && value.length) {
            elements.unit.textContent = shmi.localize(value);
            shmi.removeClass(self.element, cssClasses.NO_UNIT);
        } else {
            elements.unit.textContent = "";
            shmi.addClass(self.element, cssClasses.NO_UNIT);
        }
    }

    /**
     * initIcon - initialize icon element according to widget configuration
     *
     * @param {object} self control instance
     */
    function initIcon(self) {
        const { elements } = self.vars,
            config = self.getConfig();

        if (!elements.icon) {
            shmi.addClass(self.element, cssClasses.NO_ICON);
        } else if (config["show-icon"] && config["icon-src"]) {
            elements.icon.style.backgroundImage = `url(${self.config['icon-src']})`;
        } else if (config["show-icon"] && config["icon-class"]) {
            shmi.addClass(elements.icon, config["icon-class"]);
        } else {
            shmi.addClass(self.element, cssClasses.NO_ICON);
        }
    }

    /**
     * updateDimensions - recalculate widget dimensions
     *
     * @param {object} self control instance
     */
    function updateDimensions(self) {
        const { dims, elements, orientation } = self.vars;

        switch (orientation) {
        case orientations.HORIZONTAL:
            dims.track = elements.track.clientWidth;
            dims.handle = elements.handle.offsetWidth;
            dims.touchzone = elements.touchzone.offsetWidth;
            break;
        case orientations.VERTICAL:
            dims.track = elements.track.clientHeight;
            dims.handle = elements.handle.offsetHeight;
            dims.touchzone = elements.touchzone.offsetHeight;
            break;
        default:
        }

        updateHandle(self, getTranslation(self, self.getValue()));
    }

    /**
     * getInputHandler - create handler for Mouse-/Touch-Listeners
     *
     * @param {object} self control instance
     * @returns {object} input handler
     */
    function getInputHandler(self) {
        return {
            onPress: function() {
                self.vars.dragging = false;
                shmi.addClass(self.vars.elements.handle, cssClasses.PRESSED);
            },
            onDrag: function(dx, dy, event) {
                event.preventDefault();

                const { movable, dims } = self.vars;

                if (!self.vars.dragging) {
                    self.vars.dragging = true;
                    if (self.config.continuous !== false) {
                        startSend(self);
                    }
                }
                if (self.vars.orientation === orientations.VERTICAL) {
                    if ((movable.ty + dy) > 0) {
                        movable.ty = 0;
                        movable.update();
                    } else if ((movable.ty + dy) < Math.min(0, dims.handle - dims.track)) {
                        movable.ty = Math.min(0, dims.handle - dims.track);
                        movable.update();
                    } else {
                        movable.translate(0, dy);
                    }
                    updateScale(self, movable.ty);
                    updateTouchZone(self, movable.ty);
                } else {
                    if ((movable.tx + dx) < 0) {
                        movable.tx = 0;
                        movable.update();
                    } else if ((movable.tx + dx + dims.handle) > dims.track) {
                        movable.tx = Math.max(0, dims.track - dims.handle);
                        movable.update();
                    } else {
                        movable.translate(dx, 0);
                    }
                    updateScale(self, movable.tx);
                    updateTouchZone(self, movable.tx);
                }
            },
            onRelease: function() {
                self.vars.dragging = false;
                stopSend(self);
                updateValue(self);
                shmi.removeClass(self.vars.elements.handle, cssClasses.PRESSED);
            }
        };
    }

    /**
     * getInputHandler - create handler for Mouse-/Touch-Listeners for the slider track.
     *
     * @param {object} self control instance
     * @returns {object} input handler
     */
    function getSliderTrackInputHandler(self) {
        const handler = getInputHandler(self),
            originalOnPress = handler.onPress,
            originalOnRelease = handler.onRelease;

        return Object.assign(handler, {
            onPress: function onPress(startX, startY, event) {
                const { movable, dims } = self.vars,
                    { min, max } = self.vars.valueSettings;

                // Prevent animations
                self.vars.dragging = true;

                // Prevents pointer events for the actual handle and applies
                // its hover style.
                shmi.addClass(self.vars.elements.track, cssClasses.PRESSED);

                // Set handle position
                const boundingBox = self.vars.elements.track.getBoundingClientRect();
                if (self.vars.orientation === orientations.VERTICAL) {
                    const posY = startY - boundingBox.y;
                    if (self.config.inverted) {
                        movable.ty = getTranslation(self, posY / dims.track * (max - min) + min);
                    } else {
                        movable.ty = getTranslation(self, (1 - posY / dims.track) * (max - min) + min);
                    }
                    updateScale(self, movable.ty);
                    updateTouchZone(self, movable.ty);
                } else {
                    const posX = startX - boundingBox.x;
                    if (self.config.inverted) {
                        movable.tx = getTranslation(self, (1 - posX / dims.track) * (max - min) + min);
                    } else {
                        movable.tx = getTranslation(self, posX / dims.track * (max - min) + min);
                    }
                    updateScale(self, movable.tx);
                    updateTouchZone(self, movable.tx);
                }
                movable.update();

                // Update widget
                updateValue(self);

                // Call original onPress
                originalOnPress.call(handler, startX, startY, event);
            },

            onRelease: function onRelease(...args) {
                // Call original onRelease
                originalOnRelease.call(handler, ...args);

                // Remove fake hover style and enable pointer events on the
                // handle again.
                shmi.removeClass(self.vars.elements.track, cssClasses.PRESSED);

                // Focus handle
                self.vars.elements.handle.focus();
            }
        });
    }

    /**
     * startSend - start sending value updates while slider handle is dragging
     *
     * @param {object} self control instance
     */
    function startSend(self) {
        if (self.vars.sendInterval === 0) {
            self.vars.sendInterval = setInterval(updateValue.bind(null, self), self.getConfig()["send-interval"]);
        }
    }

    /**
     * stopSend - stop sending value updates when slider handle stops dragging
     *
     * @param {object} self control instance
     */
    function stopSend(self) {
        if (self.vars.sendInterval !== 0) {
            clearInterval(self.vars.sendInterval);
            self.vars.sendInterval = 0;
        }
    }

    /**
     * updateValue - update widget value after handle is moved
     *
     * @param {object} self control instance
     */
    function updateValue(self) {
        const { nv, im } = self.imports,
            { valueSettings, orientation, movable, dims, elements } = self.vars,
            { min, max } = valueSettings,
            config = self.getConfig(),
            lastValue = self.getValue(),
            ds = (orientation === orientations.VERTICAL) ? -movable.ty : movable.tx,
            maxTranslation = dims.track - dims.handle;

        let value = null;

        if (maxTranslation <= 0) {
            return; /* value cannot be calculated when handle is not movable */
        } else if (max === min) {
            value = max;
        } else if (self.config.inverted) {
            value = (1 - ds / maxTranslation) * (max - min) + min;
        } else {
            value = (ds * (max - min) / maxTranslation) + min;
        }

        value = nv.applyInputSettings(value, self);

        if (value !== lastValue) {
            if (elements.value) {
                elements.value.textContent = nv.formatOutput(value, self);
            }
            self.vars.value = value;
            if (config.item) {
                im.writeValue(config.item, value);
            } else {
                self.fire("change", {
                    value: value
                });
            }
        }
    }

    /**
     * updateHandle - update handle position to match current value
     *
     * @param {object} self control instance
     * @param {number} translation handle translation
     * @returns
     */
    function updateHandle(self, translation) {
        if (isNaN(translation)) {
            return;
        }

        if (self.vars.orientation === orientations.VERTICAL) {
            self.vars.movable.ty = translation;
        } else {
            self.vars.movable.tx = translation;
        }
        self.vars.movable.update();
        updateScale(self, translation);
        updateTouchZone(self, translation);
    }

    /**
     * updateScale - update value scale to match current value
     *
     * @param {object} self control instance
     * @param {number} translation handle translation
     */
    function updateScale(self, translation) {
        let trackLength = self.vars.dims.track - self.vars.dims.handle,
            ratio = (trackLength > 0) ? Math.abs(translation) / trackLength : 1;

        const { elements: { scale } } = self.vars;

        scale.style.setProperty("--internal-fill-level", `${ratio * 100}%`);
    }

    /**
     * updateTouchZone - update touch zone element to remain within widget container boundaries
     *
     * @param {object} self
     * @param {number} translation handle translation
     */
    function updateTouchZone(self, translation) {
        const { MIN_MOVED_PX } = shmi.Constants,
            { dims, orientation, elements } = self.vars,
            maxTranslation = Math.max(0, dims.track - dims.handle);

        shmi.caf(self.vars.touchzoneRafId);
        self.vars.touchzoneRafId = shmi.raf(() => {
            translation = Math.abs(translation);

            if (orientation === orientations.VERTICAL) {
                if (translation < MIN_MOVED_PX) {
                    elements.touchzone.style.height = `calc(100% + ${Math.floor(translation) + MIN_MOVED_PX}px)`;
                    if (self.vars.touchzoneState.offsetModified) {
                        elements.touchzone.style.top = "";
                        self.vars.touchzoneState.offsetModified = false;
                    }
                    self.vars.touchzoneState.lengthModified = true;
                } else if (translation > maxTranslation - MIN_MOVED_PX) {
                    elements.touchzone.style.height = `calc(100% + ${Math.floor(maxTranslation - translation) + MIN_MOVED_PX}px)`;
                    elements.touchzone.style.top = `-${Math.floor(maxTranslation - translation)}px`;
                    self.vars.touchzoneState.lengthModified = true;
                    self.vars.touchzoneState.offsetModified = true;
                } else {
                    if (self.vars.touchzoneState.lengthModified) {
                        elements.touchzone.style.height = "";
                        self.vars.touchzoneState.lengthModified = false;
                    }
                    if (self.vars.touchzoneState.offsetModified) {
                        elements.touchzone.style.top = "";
                        self.vars.touchzoneState.offsetModified = false;
                    }
                }
            } else if (translation < MIN_MOVED_PX) {
                elements.touchzone.style.width = `calc(100% + ${Math.floor(translation) + MIN_MOVED_PX}px)`;
                elements.touchzone.style.left = `-${Math.floor(translation)}px`;
                self.vars.touchzoneState.lengthModified = true;
                self.vars.touchzoneState.offsetModified = true;
            } else if (translation > maxTranslation - MIN_MOVED_PX) {
                elements.touchzone.style.width = `calc(100% + ${Math.floor(maxTranslation - translation) + MIN_MOVED_PX}px)`;
                if (self.vars.touchzoneState.offsetModified) {
                    elements.touchzone.style.left = "";
                    self.vars.touchzoneState.offsetModified = false;
                }
                self.vars.touchzoneState.lengthModified = true;
            } else {
                if (self.vars.touchzoneState.lengthModified) {
                    elements.touchzone.style.width = "";
                    self.vars.touchzoneState.lengthModified = false;
                }
                if (self.vars.touchzoneState.offsetModified) {
                    elements.touchzone.style.left = "";
                    self.vars.touchzoneState.offsetModified = false;
                }
            }
        });
    }

    /**
     * getTranslation - calculate handle translation for given value
     *
     * @param {object} self control instance
     * @param {number} value widget value
     * @returns {number} handle translation
     */
    function getTranslation(self, value) {
        const { min, max } = self.vars.valueSettings,
            { dims, orientation } = self.vars;

        let tx = null;

        if (max === min) {
            tx = Math.max(0, dims.track - dims.handle);
        } else if (self.config.inverted) {
            tx = (1 - ((value - min) / (max - min))) *
                Math.max(0, dims.track - dims.handle);
        } else {
            tx = ((value - min) / (max - min)) *
                Math.max(0, dims.track - dims.handle);
        }

        if (orientation === orientations.VERTICAL) {
            tx *= -1;
        }

        return tx;
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            elements: {
                label: null,
                icon: null,
                value: null,
                unit: null,
                track: null,
                scale: null,
                handle: null,
                touchzone: null
            },
            resizeObserver: null,
            listeners: [],
            tokens: [],
            valueSettings: null,
            value: 0,
            orientation: orientations.HORIZONTAL,
            dims: {
                track: null,
                handle: null,
                touchzone: null
            },
            dragging: false,
            movable: null,
            anim: null,
            sendInterval: 0,
            touchzoneState: {
                lengthModified: false,
                offsetModified: false
            },
            touchzoneRafId: 0,
            firstDraw: true
        },
        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            iter: "visuals.tools.iterate.iterateObject",
            nv: "visuals.tools.numericValues",
            io: "visuals.io",
            gfx: "visuals.gfx"
        },
        /* array of custom event types fired by this control */
        events: [ "change" ],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { elements, listeners } = self.vars,
                    { iter, nv, io, gfx } = self.imports;

                iter(elements, (value, name) => {
                    elements[name] = shmi.getUiElement(name, self.element);
                });

                if (!elements.track || !elements.scale || !elements.handle) {
                    console.error(`[${className}] template is missing required element ("track", "scale", "handle")`);
                    return;
                }

                nv.initValueSettings(self);

                if (elements.value && typeof self.config["initial-text"] === "string") {
                    elements.value.textContent = shmi.localize(self.config["initial-text"]);
                }

                if (elements.label) {
                    setLabel(self, self.config["show-text"] ? self.config.label : null);
                }

                if (elements.unit) {
                    setUnit(self, self.config["unit-text"]);
                }

                if (elements.track.dataset.orientation === orientations.HORIZONTAL || elements.track.dataset.orientation === orientations.VERTICAL) {
                    self.vars.orientation = elements.track.dataset.orientation;
                } else {
                    console.warn(`[${className}] invalid orientation setting '${elements.track.dataset.orientation}' in template '${self.getConfig()['template']}', defaulting to horizontal.`);
                }

                if (self.vars.orientation === orientations.VERTICAL) {
                    elements.scale.style.transformOrigin = "bottom";
                } else {
                    elements.scale.style.transformOrigin = "left";
                }

                if (self.config.inverted) {
                    shmi.addClass(self.element, cssClasses.INVERTED);
                }

                initIcon(self);

                self.vars.movable = new gfx.Movable(elements.handle);
                self.vars.anim = new gfx.Animation(updateHandle.bind(null, self));

                self.vars.resizeObserver = new ResizeObserver(updateDimensions.bind(null, self));
                listeners.push(new io.MouseListener(elements.handle, getInputHandler(self)));
                listeners.push(new io.TouchListener(elements.handle, getInputHandler(self)));
                listeners.push(new io.MouseListener(elements.track, getSliderTrackInputHandler(self)));
                listeners.push(new io.TouchListener(elements.track, getSliderTrackInputHandler(self)));
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this,
                    { tokens, listeners } = self.vars,
                    { im } = self.imports;

                listeners.forEach((l) => l.enable());
                updateDimensions(self);
                self.vars.resizeObserver.observe(self.element);
                self.vars.firstDraw = true;
                if (self.config.item) {
                    tokens.push(im.subscribeItem(self.config.item, self));
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                const self = this;

                self.vars.resizeObserver.unobserve(self.element);
                self.vars.listeners.forEach((l) => l.disable());
                self.vars.tokens.forEach((t) => t.unlisten());
                self.vars.tokens = [];
                self.vars.anim.stop();
            },
            onDelete: function() {

            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach((l) => {
                    if (!l.keepUnlocked) {
                        l.disable();
                    }
                });
                shmi.addClass(self.element, cssClasses.LOCKED);
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach((l) => {
                    if (!l.keepUnlocked) {
                        l.enable();
                    }
                });
                shmi.removeClass(self.element, cssClasses.LOCKED);
            },
            /**
             * Sets properties for subscribed item
             */
            onSetProperties: function() {
                const self = this,
                    { nv } = self.imports;

                nv.setProperties(self, arguments);
            },
            /**
             * Set widget value
             *
             * @param {number} value value to set
             */
            onSetValue: function(value) {
                const self = this,
                    nv = shmi.requires("visuals.tools.numericValues"),
                    { elements, orientation, anim, movable } = self.vars,
                    config = self.getConfig(),
                    oldValue = self.getValue();

                self.vars.value = nv.applyInputSettings(value, self);

                if (elements.value) {
                    elements.value.textContent = nv.formatOutput(self.vars.value, self);
                }

                if (!self.vars.dragging) {
                    anim.setCurrentValue(orientation === orientations.VERTICAL ? movable.ty : movable.tx);
                    if (self.vars.firstDraw) {
                        self.vars.firstDraw = false;
                        anim.start(getTranslation(self, self.vars.value), 0);
                    } else {
                        anim.start(getTranslation(self, self.vars.value), config["animation-duration"]);
                    }
                }

                if (self.vars.value !== oldValue) {
                    self.fire("change", { value: self.vars.value });
                }
            },
            /**
             * Get widget value
             *
             * @returns {number} widget value
             */
            getValue: function() {
                return this.vars.value;
            },
            /**
             * Set widget label (when "show-text" & "auto-label" are enabled)
             *
             * @param {stgring} labelText label text
             */
            setLabel: function(labelText) {
                const self = this,
                    { elements } = self.vars;

                if (self.config["show-text"] && self.config['auto-label'] && elements.label) {
                    setLabel(self, labelText);
                    updateDimensions(self);
                }
            },
            /**
             * Set unit text (when "auto-unit-text" is enabled)
             *
             * @param {string} unitText unit text
             */
            setUnitText: function(unitText) {
                const self = this,
                    { elements } = self.vars;

                if (self.config['auto-unit-text'] && elements.unit) {
                    setUnit(self, unitText);
                    updateDimensions(self);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * iq-text-display
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-text-display",
 *     "name": null,
 *     "template": "custom/controls/iq-text-display"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "label": Label of the widget
 * "item": The item
 * "min": Minimum value
 * "max": Maximum value
 * "precision": Precision
 * "decimal-delimiter": Decimal delimiter
 * "type": Type is INT
 * "unit-text": Unit Text
 * "auto-label": Whether to use the auto-label (from item)
 * "auto-unit-text": Whether to use the auto-unit (from item)
 * "auto-min": Whether to use auto-min (from item)
 * "auto-max": Whether to use auto-max (from item)
 * "auto-precision": Whether to use the auto-precision (from item)
 * "auto-type": Whether to use the auto-type (from item)
 * "icon-src": Default icon source
 * "icon-class": Default icon class
 * "tooltip": Tooltip
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-text-display", // control name in camel-case
        uiType = "iq-text-display", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-text-display",
        "name": null,
        "template": "default/iq-text-display.variant-01",
        "label": '[Label]',
        "item": null,
        "min": Number.NEGATIVE_INFINITY,
        "max": Number.POSITIVE_INFINITY,
        "precision": -1,
        "decimal-delimiter": ".",
        "type": shmi.c("TYPE_INT"),
        "unit-text": "[Unit]",
        "auto-label": true,
        "auto-unit-text": true,
        "auto-min": true,
        "auto-max": true,
        "auto-precision": true,
        "auto-type": true,
        "icon-src": null,
        "icon-class": null,
        "icon-title": null,
        "tooltip": null,
        "initial-text": "-"
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * setLabel - set label text for widget. applies "no-label" CSS class when no text is specified
     *
     * @param {object} self control instance
     * @param {string} [value] text value
     */
    function setLabel(self, value) {
        if (typeof value === "string" && value.length) {
            self.vars.labelEl.textContent = shmi.localize(value);
            shmi.removeClass(self.element, "no-label");
        } else {
            self.vars.labelEl.textContent = "";
            shmi.addClass(self.element, "no-label");
        }
    }

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        tooltipProperties: ['icon-title'],

        /* instance variables */
        vars: {
            listeners: [],
            value: 0,
            active: false,

            // DOM Elements
            currentValueEl: null,
            labelEl: null,
            unitEl: null,
            iconEl: null,

            initialized: false,
            subscriptionTargetId: null,

            requestId: null
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                self.imports.nv.initValueSettings(self);

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.labelEl = shmi.getUiElement('label', self.element);
                self.vars.unitEl = shmi.getUiElement('unit', self.element);
                self.vars.iconEl = shmi.getUiElement('icon', self.element);
                self.vars.currentValueEl = shmi.getUiElement('value', self.element);

                // Check for required elements
                if (!self.vars.currentValueEl) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-text-display] Current value element is missing)", 3);
                    self.vars.labelEl = null;
                    self.vars.unitEl = null;
                    return;
                }

                // Unit
                if (self.vars.unitEl) {
                    if (self.config['unit-text']) { //} && !self.config['auto-unit-text']) {
                        self.vars.unit = self.config['unit-text'];
                        self.vars.unitEl.textContent = shmi.localize(self.config['unit-text']);
                    } else {
                        self.vars.unitEl.textContent = '';
                    }
                }

                if (this.config['unit-scale']) {
                    this.config['unit-scale'] = parseFloat(shmi.localize(this.config['unit-scale']));
                }

                // Label
                if (self.vars.labelEl && self.config["show-text"]) {
                    setLabel(self, self.config.label);
                } else {
                    setLabel(self, null);
                }

                /************/
                /*** ICON ***/
                /************/
                if (!self.vars.iconEl) {
                    shmi.log('[IQ:iq-button] no button-icon element provided', 1);
                    shmi.addClass(self.element, "no-icon");
                } else if (self.config["show-icon"] && self.config['icon-src']) {
                    self.vars.iconEl.style.backgroundImage = `url(${self.config['icon-src']})`;
                } else if (self.config["show-icon"] && self.config['icon-class']) {
                    var iconClasses = self.config['icon-class'].trim().split(" ");
                    iconClasses.forEach(function(cls) {
                        shmi.addClass(self.vars.iconEl, cls);
                    });
                } else {
                    shmi.addClass(self.element, "no-icon");
                }

                if (typeof self.config["initial-text"] === "string") {
                    self.vars.currentValueEl.textContent = shmi.localize(self.config["initial-text"]);
                }
            },

            /* called when control is enabled */
            onEnable: function() {
                var self = this;

                if (self.config.item) {
                    self.vars.subscriptionTargetId = self.imports.im.subscribeItem(self.config.item, self);
                }

                shmi.log("[IQ:iq-text-display] Enabled", 1);
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                if (self.config.item) {
                    self.imports.im.unsubscribeItem(self.config.item, self.vars.subscriptionTargetId);
                }

                shmi.log("[IQ:iq-text-display] disabled", 1);
            },

            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                var self = this;

                if (type === shmi.c("TYPE_STRING")) {
                    self.vars.value = value;
                } else if (isNaN(parseFloat(value))) {
                    self.vars.value = value;
                } else {
                    self.vars.value = parseFloat(value);
                }

                shmi.caf(self.vars.requestId);
                self.vars.requestId = shmi.raf(function() {
                    self.asyncSet(type);
                });
            },

            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function() {
                const self = this,
                    { nv } = self.imports;

                nv.setProperties(self, arguments);
            },

            setUnitText: function(unitText) {
                var self = this;
                if (self.vars.unitEl && self.config['auto-unit-text']) {
                    self.vars.unit = unitText;
                    self.vars.unitEl.textContent = shmi.localize(unitText);
                }
            },

            setLabel: function(labelText) {
                var self = this;

                if (self.vars.labelEl && self.config['auto-label'] && self.config['show-text']) {
                    setLabel(self, labelText);
                }
            },

            /**
             * used to set the current value asynchronously
             */
            asyncSet: function(type) {
                var self = this;

                if (shmi.visuals.session.deactivateControls) {
                    setTimeout(self.asyncSet.bind(self, type), shmi.c("ACTION_RETRY_TIMEOUT"));
                } else {
                    self.vars.currentValueEl.textContent = self.imports.nv.formatOutput(self.vars.value, self);
                }
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();
/**
 * iq-text
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-text",
 *     "name": null,
 *     "template": "custom/controls/iq-text"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "tooltip": Tooltip
 * "text": The text
 * "items": Items to be used inside
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "iq-text", // control name in camel-case
        uiType = "iq-text", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": "iq-text",
        "name": null,
        "template": "default/iq-text.variant-01",
        "text": '[Text]',
        "items": [],
        "tooltip": null
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // Definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            tokens: [],
            textEl: null,
            itemValues: [],
            rafId: 0
        },

        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager",
            nv: "visuals.tools.numericValues"
        },

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                /***********/
                /*** DOM ***/
                /***********/
                self.vars.textEl = shmi.getUiElement('text', self.element);

                if (!self.vars.textEl) {
                    // As this should never happen we do a bit more work here in cleaning up than checking above
                    shmi.log("[IQ:iq-text] Missing main element", 3);
                    return;
                }

                if (self.config.text) {
                    self.vars.textEl.textContent = shmi.localize(self.config.text);
                }
            },

            /* Called when control is enabled */
            onEnable: function() {
                var self = this;

                if (Array.isArray(self.config.items)) {
                    self.config.items.forEach(function(item, idx) {
                        var handler = self.imports.im.getItemHandler();

                        self.vars.itemValues.push({
                            value: null,
                            unit: null,
                            label: null
                        });

                        handler.setValue = function setValue(value, type, name) {
                            const handlerItem = self.imports.im.getItem(name);
                            let unit = null;

                            if (handlerItem) {
                                if (handlerItem.adapter && handlerItem.adapter.unitText) {
                                    unit = handlerItem.adapter.unitText;
                                } else if (typeof handlerItem.unit === "string") {
                                    unit = handlerItem.unit;
                                }
                                self.vars.itemValues[idx] = {
                                    value: (type === shmi.Constants.TYPE_STRING) ? handlerItem.readValue() : self.imports.nv.formatNumber(handlerItem.readValue(true), {
                                        "precision": handlerItem.digits,
                                        "unit": handlerItem.unit,
                                        "show-unit": false
                                    }),
                                    unit: shmi.localize(unit),
                                    label: shmi.localize(handlerItem.labelToken)
                                };
                                self.updateText();
                            }
                        };
                        self.vars.tokens.push(self.imports.im.subscribeItem(item, handler));
                    });
                }

                shmi.log("[IQ:iq-text] Enabled", 1);
            },

            /* Called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];

                shmi.log("[IQ:iq-text] disabled", 1);
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
                shmi.log("[IQ:iq-text] Locked", 1);
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                shmi.removeClass(self.element, 'locked');
                shmi.log("[IQ:iq-text] unlocked", 1);
            },

            /** Updates the text **/
            updateText: function() {
                var self = this;

                shmi.caf(self.vars.rafId);
                self.vars.rafId = shmi.raf(function() {
                    if (!self.isActive()) {
                        return;
                    }
                    self.vars.textEl.textContent = shmi.evalString(shmi.localize(self.config.text), {
                        items: self.vars.itemValues
                    });
                });
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();
/**
 * iq-trend-legend
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-legend",
 *     "name": null,
 *     "template": "custom/controls/iq-trend-legend"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-trend-legend", // control name in camel-case
        uiType = "iq-trend-legend", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-trend-legend",
        "name": null,
        "template": "default/iq-trend-legend",
        "trend-widget": null,
        "show-unit": true
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * update legend display when graph settings change
     *
     * @param {object} self control instance
     */
    function handleGraphChange(self) {
        clearValueInfo(self);
        generateValueInfo(self);
    }

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        self.vars.styleImages = loadStyleImages(self, control.getValueStyles());
        self.vars.defaultStyleImage = loadStyleImages(self, [control.getDefaultStyle()])[0];
        self.vars.inactiveImage = loadStyleImages(self, [control.getInactiveStyle()])[0];
        generateValueInfo(self);
        self.vars.infoToken = control.listen("value-info", handleGraphChange.bind(null, self));
        self.vars.optionsToken = control.listen("graph-options", handleGraphChange.bind(null, self));
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        clearValueInfo(self);
        self.vars.trendWidget = null;
        if (self.vars.infoToken) {
            self.vars.infoToken.unlisten();
            self.vars.infoToken = null;
        }

        if (self.vars.optionsToken) {
            self.vars.optionsToken.unlisten();
            self.vars.optionsToken = null;
        }
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    /**
     * clear generated value info elements
     *
     * @param {object} self control instance
     */
    function clearValueInfo(self) {
        self.vars.listeners.forEach((l) => {
            l.disable();
        });
        self.vars.listeners = [];
        self.vars.valueInfos.forEach((vi) => {
            vi.base.parentNode.removeChild(vi.base);
        });
        self.vars.valueInfos = [];
    }

    /**
     * check if unit is a unit-class
     *
     * @param {string|number} unitText configured unit
     * @return {boolean} `true` if unit is unit-class, `false` else
     */
    function isUnitClass(unitText) {
        return String(parseInt(unitText)) === String(unitText);
    }

    /**
     * generate value info
     *
     * @param {object} self control instance
     */
    function generateValueInfo(self) {
        const { trendWidget } = self.vars;
        if (!trendWidget) {
            return;
        }

        const valueInfo = self.vars.trendWidget.getValueInfo(),
            selectedInfo = self.vars.trendWidget.getSelectedValues(),
            uc = shmi.requires("visuals.tools.unitClasses"),
            highlightActive = selectedInfo.some((vi) => !!(vi.options && vi.options.highlight));

        if (!highlightActive) {
            if (self.vars.highlighted) {
                shmi.removeClass(self.vars.highlighted.base, "active");
            }
            self.vars.highlighted = null;
            shmi.removeClass(self.element, "highlighted");
        } else {
            shmi.addClass(self.element, "highlighted");
        }

        self.vars.valueInfos = selectedInfo.filter((info) => valueInfo[info.name] && typeof valueInfo[info.name].data_type === "string" && valueInfo[info.name].data_type !== "string").map((info) => {
            const template = self.vars.elements.valueInfoTemplate.content.cloneNode(true),
                valueElement = shmi.getUiElement("value-info", template),
                base = valueElement ? valueElement : null,
                label = base ? shmi.getUiElement("value-label", base) : null,
                icon = base ? shmi.getUiElement("value-icon", base) : null,
                unit = base ? shmi.getUiElement("value-unit", base) : null;

            if (label) {
                label.textContent = valueInfo[info.name] && valueInfo[info.name].label ? shmi.localize(valueInfo[info.name].label) : info.name;
            }

            if (icon) {
                if (highlightActive && !info.options.highlight) {
                    icon.setAttribute("src", self.vars.inactiveImage);
                } else {
                    const styleImage = self.vars.styleImages[info.options.style] ? self.vars.styleImages[info.options.style] : self.vars.defaultStyleImage;
                    icon.setAttribute("src", styleImage);
                }
            }

            let unitText = "";
            if (base && unit) {
                if (self.config["show-unit"] && valueInfo[info.name] && valueInfo[info.name].unit !== null) {
                    if (isUnitClass(valueInfo[info.name].unit)) {
                        const ucAdapter = uc.getSelectedAdapter(valueInfo[info.name].unit);
                        if (ucAdapter) {
                            unitText = ucAdapter.unitText;
                        }
                    } else {
                        unitText = valueInfo[info.name].unit;
                    }
                }

                if (typeof unitText === "string" && unitText !== "") {
                    unit.textContent = shmi.localize(unitText);
                } else {
                    shmi.addClass(base, "no-unit");
                }
            }

            const infoRef = {
                base,
                label,
                icon,
                unit,
                info
            };

            if (base) {
                if ((info.options && info.options.highlight) || (self.vars.highlighted && self.vars.highlighted.info.name === info.name)) {
                    if (self.vars.highlighted && self.vars.highlighted !== infoRef) {
                        shmi.removeClass(self.vars.highlighted.base, "active");
                    }
                    self.vars.highlighted = infoRef;
                    shmi.addClass(base, "active");
                }
                const { MouseListener, TouchListener } = shmi.requires("visuals.io"),
                    handler = {
                        onPress: () => { /* implementation of onPress handler stops event bubbling */ },
                        onClick: () => {
                            const highlighted = self.vars.highlighted;
                            trendWidget.setGraphOptions(info.name, { highlight: (highlighted === null) || (highlighted !== infoRef) });
                        }
                    };

                self.vars.listeners.push(new MouseListener(base, handler), new TouchListener(base, handler));
            }

            return infoRef;
        });

        self.vars.valueInfos.forEach((vi) => {
            if (vi.base) {
                self.vars.elements.valueAnchor.appendChild(vi.base);
            }
        });

        if (!self.isLocked()) {
            self.vars.listeners.forEach((l) => {
                l.enable();
            });
        }
    }

    /**
     * load images to use for style preview
     *
     * @param {object} self control instance
     * @param {object[]} styles curve style configuration
     * @return {string[]} image urls for style preview
     */
    function loadStyleImages(self, styles) {
        if (!self.vars.elements.styleIconTemplate) {
            return [];
        }
        const dataPrefix = "data:image/svg+xml;base64,",
            options = styles.map((style) => {
                const template = self.vars.elements.styleIconTemplate.content.cloneNode(true),
                    svg = template.querySelector("svg");
                if (svg) {
                    const line = svg.querySelector(".curve-line");
                    if (line) {
                    //set style...
                        line.style.setProperty("fill", style.value.strokeStyle);
                    }
                    const fill = svg.querySelector(".curve-fill");
                    if (fill) {
                    //set style...
                        fill.style.setProperty("fill", style.value.fillStyle);
                    }
                    return `${dataPrefix}${btoa(svg.outerHTML)}`;
                }

                return null;
            }).filter((imgData) => imgData !== null);

        return options;
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            trendWidget: null,
            elements: {
                valueAnchor: null,
                valueInfoTemplate: null,
                styleIconTemplate: null
            },
            styleImages: [],
            defaultStyleImage: null,
            inactiveImage: null,
            valueInfos: [],
            infoToken: null,
            optionsToken: null,
            tokens: [],
            listeners: [],
            highlighted: null
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                self.vars.elements.valueAnchor = shmi.getUiElement("value-anchor", self.element);
                self.vars.elements.valueInfoTemplate = shmi.getUiElement("value-info-template", self.element);
                self.vars.elements.styleIconTemplate = shmi.getUiElement("style-icon-template", self.element);

                if (self.vars.elements.valueAnchor && self.config.orientation === "vertical") {
                    shmi.addClass(self.vars.elements.valueAnchor, "vertical");
                }
            },

            /* Called when control is enabled */
            onEnable: function() {
                const { elements } = this.vars;
                if (!(elements.valueAnchor && elements.valueInfoTemplate && elements.styleIconTemplate)) {
                    console.error(`[${uiType}] template is missing required elements.`);
                    return;
                }
                if (this.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(this.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(this, trendWidget);
                    }

                    attachTrendListeners(this);
                }
            },

            /* Called when control is disabled */
            onDisable: function() {
                onTrendDisable(this);
                this.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.tokens = [];
            },

            /* Called when control is deleted */
            onDelete: function() {

            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * iq-trend-live
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-live",
 *     "name": null,
 *     "template": "custom/controls/iq-trend-live"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-trend-live", // control name in camel-case
        uiType = "iq-trend-live", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-trend-live",
        "name": null,
        "template": "default/iq-trend-live",
        "trend-widget": null,
        "orientation": "horizontal"
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * update active state of button elements
     *
     * @param {object} self control instance
     * @param {boolean} isLive state of live mode
     */
    function updateButtonState(self, isLive) {
        if (isLive) {
            shmi.removeClass(self.vars.elements.pauseButton, "active");
            shmi.addClass(self.vars.elements.playButton, "active");
        } else {
            shmi.removeClass(self.vars.elements.playButton, "active");
            shmi.addClass(self.vars.elements.pauseButton, "active");
        }
    }

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        if (!self.isLocked()) {
            shmi.removeClass(self.element, "locked");
        }
        updateButtonState(self, control.isLive());
        self.vars.tokens.push(control.listen("live", (evt) => {
            updateButtonState(self, evt.detail.isLive);
        }));
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        self.vars.trendWidget = null;
        if (self.isActive() && !self.isLocked()) {
            shmi.addClass(self.element, "locked");
        }
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            trendWidget: null,
            elements: {
                playButton: null,
                pauseButton: null
            },
            tokens: [],
            listeners: []
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { MouseListener, TouchListener } = shmi.requires("visuals.io");

                self.vars.elements.playButton = shmi.getUiElement("play-button", self.element);
                self.vars.elements.pauseButton = shmi.getUiElement("pause-button", self.element);

                if (self.config.orientation === "vertical") {
                    shmi.addClass(self.element, "vertical");
                }

                if (self.vars.elements.playButton && self.vars.elements.pauseButton) {
                    const playHandler = {
                        onClick: () => {
                            if (self.vars.trendWidget && !self.vars.trendWidget.isLive()) {
                                self.vars.trendWidget.setLive(true);
                            }
                        }
                    };
                    self.vars.listeners.push(
                        new MouseListener(self.vars.elements.playButton, playHandler),
                        new TouchListener(self.vars.elements.playButton, playHandler)
                    );

                    const pauseHandler = {
                        onClick: () => {
                            if (self.vars.trendWidget && self.vars.trendWidget.isLive()) {
                                self.vars.trendWidget.setLive(false);
                            }
                        }
                    };
                    self.vars.listeners.push(
                        new MouseListener(self.vars.elements.pauseButton, pauseHandler),
                        new TouchListener(self.vars.elements.pauseButton, pauseHandler)
                    );
                }
            },

            /* Called when control is enabled */
            onEnable: function() {
                const { elements } = this.vars;
                if (!(elements.playButton && elements.pauseButton)) {
                    console.error(`[${uiType}] template is missing required elements.`);
                    return;
                }
                if (this.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(this.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(this, trendWidget);
                    }

                    attachTrendListeners(this);
                }
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            },

            /* Called when control is disabled */
            onDisable: function() {
                onTrendDisable(this);
                this.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.tokens = [];
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
            },

            /* Called when control is deleted */
            onDelete: function() {

            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * iq-trend-select
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-select",
 *     "name": null,
 *     "template": "custom/controls/iq-trend-select"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * "tooltip": Tooltip
 * "text": The text
 * "items": Items to be used inside
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-trend-select", // control name in camel-case
        uiType = "iq-trend-select", // control keyword (data-ui)
        isContainer = true;

    // default configuration
    const defConfig = {
        "class-name": "iq-trend-select",
        "name": null,
        "template": "default/iq-trend-select",
        "trend-widget": null,
        "label-mode": "label",
        "settingsDialogOptions": {}
    };

    /**
     * get configuration for complex-table2 instance
     *
     * @param {string} gridName datagrid name
     * @param {string} [labelMode="label"] label-mode to use, either `"label"` or `"item"`
     * @param {string} widgetHandle iq-trend-select instance node handle
     */
    const getTableConfig = (gridName, labelMode = "label", widgetHandle) => ({
        "label": "Trend Select Table",
        "table": gridName,
        "name": "trend-select-table",
        "class-name": "complex-table2 trend-item-selection",
        "field-datagrid-col-map": {
            "selected": 3,
            "name": 0,
            "label": 1,
            "options": 2,
            "image-url": 4
        },
        "default-field-control-map": {
            "selected": {
                "ui-type": "iq-checkbox",
                "config": {
                    "class-name": "iq-checkbox iq-variable-01",
                    "template": "default/iq-checkbox.iq-variant-01",
                    "label": ""
                }
            },
            "name": {
                "ui-type": "iq-label",
                "config": {
                    "class-name": "iq-label iq-variant-01",
                    "template": "default/iq-label.iq-variant-01"
                }
            },
            "label": {
                "ui-type": "iq-label",
                "config": {
                    "class-name": "iq-label variant-01",
                    "template": "default/iq-label.iq-variant-01"
                }
            },
            "options": {
                "ui-type": "local-script",
                "config": {
                    "module": "visuals.controls.iqtrendselect.ls.settings",
                    "handle": widgetHandle
                }
            },
            "image-url": {
                "ui-type": "local-script",
                "config": {
                    "module": "visuals.controls.iqtrendselect.ls.style",
                    "handle": widgetHandle
                }
            }
        },
        "default-field-headers": {
            "selected": " ",
            "name": "${iqtrendselect.table.value.header}",
            "label": "${iqtrendselect.table.value.header}",
            "options": " ",
            "image-url": " "
        },
        "_comment": "expr is passed to expr parameter array of shmi.visuals.core.DataGridManager.setFilter",
        "filters": [],
        "default-layout": {
            "class-name": "layout-std",
            "_comment": "default == no additional css layout class",
            "column-org": {
                "col1": {
                    "fields": ["selected"],
                    "column-width": "10%"
                },
                "col2": {
                    "fields": [ labelMode === "label" ? "label" : "name"],
                    "column-width": "40%"
                },
                "col3": {
                    "fields": ["image-url"],
                    "column-width": "10%"
                },
                "col4": {
                    "fields": ["options"],
                    "column-width": "10%"
                }
            },
            "line-height": "var(--table-row-height)"
        },
        "sortable-fields": [
            "selected",
            "name",
            "label"
        ],
        "delete-selected-rows": false,
        "show-nof-rows": true,
        "show-buttons-table-min-width-px": 400,
        "text-mode": "SINGLELINE",
        "responsive-layouts": [],
        "default-nof-buffered-rows": 60,
        "buffer-size": 500,
        "quicksearch": {
            "enable": true,
            "remember": true,
            "fields": [
                "name",
                "label"
            ]
        },
        "select-mode": "SINGLE",
        "show-select-boxes": false
    });

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * get random id for datagrid naming
     *
     * @return {string}
     */
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        self.vars.styleImages = loadStyleImages(self, control.getValueStyles());
        self.vars.infoToken = control.listen("value-info", (evt) => {
            self.vars.grid.refresh();
        });
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        self.vars.trendWidget = null;
        self.vars.infoToken.unlisten();
        self.vars.infoToken = null;
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    /**
     * load images to use for style preview
     *
     * @param {object} self control instance
     * @param {object[]} styles curve style configuration
     * @return {string[]} image urls for style preview
     */
    function loadStyleImages(self, styles) {
        if (!self.vars.elements.styleIconTemplate) {
            return [];
        }
        const dataPrefix = "data:image/svg+xml;base64,",
            options = styles.map((style) => {
                const div = document.createElement("div");
                div.innerHTML = self.vars.elements.styleIconTemplate.innerHTML;
                const svg = div.querySelector("svg");
                if (svg) {
                    const line = svg.querySelector(".curve-line");
                    if (line) {
                    //set style...
                        line.style.setProperty("fill", style.value.strokeStyle);
                    }
                    const fill = svg.querySelector(".curve-fill");
                    if (fill) {
                    //set style...
                        fill.style.setProperty("fill", style.value.fillStyle);
                    }
                    return `${dataPrefix}${btoa(div.innerHTML)}`;
                }

                return null;
            }).filter((imgData) => imgData !== null);

        return options;
    }

    /**
     * store value settings of specified value name
     *
     * @param {object} self control instance
     * @param {string} valueName name of value
     */
    function storeValueSettings(self, valueName) {
        if (self.vars.trendWidget) {
            const selectedValues = self.vars.trendWidget.getSelectedValues(),
                valueSettings = selectedValues.find(({ name }) => name === valueName);

            if (valueSettings) {
                self.vars.storedSettings[valueName] = valueSettings;
            }
        }
    }

    /**
     * get value settings for newly selected value
     *
     * @param {object} self control instance
     * @param {string} valueName name of value
     * @param {object[]} rowData datagrid row data
     * @return {object} value settings
     */
    function getValueSettings(self, valueName, rowData) {
        const storedSettings = self.vars.storedSettings[valueName] ?? null,
            selectedValues = self.vars.trendWidget.getSelectedValues(),
            valueStyles = self.vars.trendWidget.getValueStyles(),
            usedStyles = selectedValues.map(
                (value) => (value.options && typeof value.options.style === "number" ? value.options.style : null)
            ).filter((idx) => idx !== null),
            storedStyles = Object.values(self.vars.storedSettings).map((value) => value.options.style),
            preferredStyle = valueStyles.findIndex((style, idx) => !usedStyles.includes(idx) && !storedStyles.includes(idx)),
            newStyle = preferredStyle > -1 ? preferredStyle : valueStyles.findIndex((style, idx) => !usedStyles.includes(idx)),
            options = JSON.parse(rowData[2].value);

        const valueSettings = storedSettings ? storedSettings.options : {
            fillCurve: false,
            drawTolerance: false,
            style: typeof newStyle === "number" ? newStyle : undefined,
            scale: {
                location: "left",
                hidden: options.item.data_type === "string"
            },
            limits: {
                mode: "config"
            },
            scaleGroup: null
        };

        return valueSettings;
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            trendWidget: null,
            elements: {
                tableAnchor: null,
                styleIconTemplate: null
            },
            styleImages: [],
            defaultStyleImage: null,
            grid: null,
            infoToken: null,
            tokens: [],
            storedSettings: {}
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    gridUtil = shmi.requires("visuals.controls.iqtrendselect.grid"),
                    dgm = shmi.requires("visuals.session.DataGridManager"),
                    tm = shmi.requires("visuals.core.TrendManager2");
                let gridId = null;
                do {
                    gridId = className + "-" + getRandomId();
                } while (dgm.getGrid(gridId) !== null);

                self.vars.elements.styleIconTemplate = shmi.getUiElement("style-icon-template", self.element);

                //create datagrid for item selection
                self.vars.grid = gridUtil.create({
                    name: gridId,
                    fields: ["name", "label", "options", "selected", "image-url"],
                    index: ["name"],
                    mode: gridUtil.FILTER_MODE_OR
                }, (callback) => {
                    if (self.vars.trendWidget) {
                        const trendInfo = self.vars.trendWidget.getTrendInfo(),
                            valueInfo = self.vars.trendWidget.getValueInfo(),
                            styles = self.vars.trendWidget.getValueStyles(),
                            selectedInfo = self.vars.trendWidget.getSelectedValues(),
                            scaleGroups = [];

                        if (!trendInfo) {
                            callback([]);
                        }
                        self.vars.styleImages = loadStyleImages(self, styles);
                        self.vars.defaultStyleImage = loadStyleImages(self, [self.vars.trendWidget.getDefaultStyle()]);

                        selectedInfo.forEach((variable) => {
                            if (variable.options && typeof variable.options.scaleGroup === "number" && !scaleGroups.includes(variable.options.scaleGroup)) {
                                scaleGroups.push(variable.options.scaleGroup);
                            }
                        });

                        tm.getInfo(trendInfo.name).then((response) => {
                            if (response && Array.isArray(response.items)) {
                                const rows = response.items.map((item, idx) => {
                                    const selected = selectedInfo.find((variable) => variable.name === item.name);

                                    return [

                                        {
                                            type: shmi.c("TYPE_STRING"),
                                            value: item.name,
                                            min: null,
                                            max: null
                                        },
                                        {
                                            type: shmi.c("TYPE_STRING"),
                                            value: item.label ? shmi.localize(item.label) : item.name,
                                            min: null,
                                            max: null
                                        },
                                        {
                                            type: shmi.c("TYPE_STRING"),
                                            value: JSON.stringify({
                                                data: selected ? selected : null,
                                                scaleGroups: scaleGroups,
                                                styles: styles,
                                                item
                                            }),
                                            min: null,
                                            max: null
                                        },
                                        {
                                            type: shmi.c("TYPE_BOOL"),
                                            value: valueInfo && valueInfo[item.name] ? 1 : 0,
                                            min: null,
                                            max: null
                                        },
                                        {
                                            type: shmi.c("TYPE_STRING"),
                                            value: selected && selected.options && typeof selected.options.style === "number" ? self.vars.styleImages[selected.options.style] : "",
                                            min: null,
                                            max: null
                                        }
                                    ];
                                });
                                callback(rows);
                            } else {
                                callback([]);
                            }
                        }).catch((exc) => {
                            console.error("Error retrieving trend info:", exc);
                            callback([]);
                        });
                    } else {
                        callback([]);
                    }
                });
                self.vars.elements.tableAnchor = shmi.getUiElement("table-anchor", self.element);
                if (self.vars.elements.tableAnchor) {
                    shmi.createControl("complex-table2", self.vars.elements.tableAnchor, getTableConfig(gridId, self.config["label-mode"], shmi.requires("visuals.tools.nodes").getNodeHandle(self)), "DIV");
                }
            },

            /* Called when control is enabled */
            onEnable: function() {
                if (this.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(this.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(this, trendWidget);
                    }

                    attachTrendListeners(this);
                }

                this.controls.forEach((c) => {
                    c.enable();
                });

                //listen for selection change on datagrid
                this.vars.tokens.push(shmi.listen("grid-changed", (evt) => {
                    if (!this.vars.trendWidget) {
                        return;
                    }
                    const rowData = evt.source.getRowData(evt.detail.row);

                    if (rowData) {
                        if (evt.detail.value === 1) {
                            const valueSettings = getValueSettings(this, rowData[0].value, rowData);

                            this.vars.trendWidget.addGraphValue(rowData[0].value, valueSettings);
                            delete this.vars.storedSettings[rowData[0].value];
                        } else {
                            storeValueSettings(this, rowData[0].value);
                            this.vars.trendWidget.removeGraph(rowData[0].value);
                        }
                    }
                }, { "detail.grid.name": this.vars.grid.name, "detail.field": "selected" }));

                //listen for changes to display options on datagrid
                this.vars.tokens.push(shmi.listen("grid-changed", (evt) => {
                    if (this.vars.trendWidget) {
                        const config = JSON.parse(evt.detail.value);
                        this.vars.trendWidget.setGraphOptions(config.data.name, config.data.options);
                        this.vars.grid.refresh();
                    }
                }, { "detail.grid.name": this.vars.grid.name, "detail.field": "options" }));
            },
            /**
             * get images to preview curve styles
             *
             * @returns {string[]} curve style images
             */
            getStyleImages() {
                return shmi.cloneObject(this.vars.styleImages);
            },
            /**
             * get image to preview default curve style
             *
             * @return {string} curve style image
             */
            getDefaultStyleImage() {
                return this.vars.defaultStyleImage;
            },
            /* Called when control is disabled */
            onDisable: function() {
                this.controls.forEach((c) => {
                    c.disable();
                });
                this.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.tokens = [];
            },

            /* Called when control is deleted */
            onDelete: function() {
                const gridUtil = shmi.requires("visuals.controls.iqtrendselect.grid");
                if (this.vars.grid) {
                    gridUtil.remove(this.vars.grid.name);
                    this.vars.grid = null;
                }
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * Module to implement dialog used for trend curve settings
 *
 * @module visuals/controls/iqtrendselect/dialog
 */
(function() {
    const MODULE_NAME = "visuals.controls.iqtrendselect.dialog",
        /** @lends module:visuals/controls/iqtrendselect/dialog */
        module = shmi.pkg(MODULE_NAME);

    const CONTROLLER_NAME = "trend-select-config-dialog";

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    /**
     * initialize style selection options
     *
     * @param {object} state controller state
     * @param {string[]} styleImages style preview image urls
     */
    function initStyles(state, styleImages) {
        const selectStyle = state.getInstance("selectStyle");

        selectStyle.setOptions(styleImages.map((imgData, idx) => ({
            "label": `\${iqtrendselect.settings.style.option} ${idx + 1}`,
            "value": idx,
            "icon-src": imgData
        })));
    }

    /**
     * check if required widgets are ready
     *
     * @param {object} state controller state
     * @return {boolean} `true` if widgets are ready, `false` else
     */
    function widgetsReady(state) {
        return [
            "selectStyle",
            "checkTolerance",
            "checkFill",
            "selectLimitsMode",
            "inputLimitsStep",
            "inputLimitsMin",
            "inputLimitsMax",
            "selectScaleLocation",
            "checkScaleHide",
            "selectScaleGroup",
            "buttonScaleAddGroup",
            "buttonApply"
        ].every((slotName) => !!state.getInstance(slotName));
    }

    /**
     * set controller slot values
     *
     * @param {object} state controller state
     * @param {object} slotData slot data
     */
    function setSlotValues(state, slotData) {
        const { cellInfo } = state.data,
            scaleGroupOptions = [
                {
                    label: "${iqtrendselect.settings.scalegroup.none}",
                    value: null
                }
            ];

        state.data.ignoreEvents = true;

        cellInfo.scaleGroups.forEach((groupId) => {
            scaleGroupOptions.push({
                label: `${groupId}`,
                value: groupId
            });
        });
        state.getInstance("selectScaleGroup").setOptions(scaleGroupOptions);

        state.getInstance("selectStyle").setValue(slotData.style);
        state.getInstance("checkTolerance").setValue(!!slotData.drawTolerance);
        state.getInstance("checkFill").setValue(!!slotData.fillCurve);
        state.getInstance("selectLimitsMode").setValue(slotData.limits && slotData.limits.mode ? slotData.limits.mode : "config");
        state.getInstance("inputLimitsStep").setValue(slotData.limits && typeof slotData.limits.step === "number" ? slotData.limits.step : 0);
        state.getInstance("inputLimitsMin").setValue(slotData.limits && typeof slotData.limits.min === "number" ? slotData.limits.min : 0);
        state.getInstance("inputLimitsMax").setValue(slotData.limits && typeof slotData.limits.max === "number" ? slotData.limits.max : 100);
        state.getInstance("selectScaleLocation").setValue(slotData.scale && typeof slotData.scale.location === "string" ? slotData.scale.location : "left");
        state.getInstance("checkScaleHide").setValue(slotData.scale && typeof slotData.scale.hidden === "boolean" ? slotData.scale.hidden : false);
        state.getInstance("selectScaleGroup").setValue(typeof slotData.scaleGroup === "number" ? slotData.scaleGroup : null);

        state.data.ignoreEvents = false;

        updateState(state);
    }

    /**
     * get controller slot values
     *
     * @param {object} state controller state
     * @return {object} slot data
     */
    function getSlotValues(state) {
        const options = {
            style: state.getInstance("selectStyle").getValue(),
            drawTolerance: state.getInstance("checkTolerance").getValue(),
            fillCurve: state.getInstance("checkFill").getValue(),
            limits: {
                mode: state.getInstance("selectLimitsMode").getValue(),
                step: state.getInstance("inputLimitsStep").getValue(),
                min: state.getInstance("inputLimitsMin").getValue(),
                max: state.getInstance("inputLimitsMax").getValue()
            },
            scale: {
                location: state.getInstance("selectScaleLocation").getValue(),
                hidden: state.getInstance("checkScaleHide").getValue()
            },
            scaleGroup: state.getInstance("selectScaleGroup").getValue()
        };

        if (options.limits.mode === "config") {
            delete options.limits.step;
            delete options.limits.min;
            delete options.limits.max;
        } else if (options.limits.mode === "auto") {
            if (options.limits.step === 0) {
                //disable stepping
                options.limits.step = null;
            }
            delete options.limits.min;
            delete options.limits.max;
        } else {
            delete options.limits.step;
            if (options.limits.min > options.limits.max) {
                //switch min & max
                const tmpMin = options.limits.min;
                options.limits.min = options.limits.max;
                options.limits.max = tmpMin;
            }
        }

        return options;
    }

    /**
     * handle click on add scalegroup button
     *
     * @param {object} state controller state
     */
    function handleAddScalegroup(state) {
        state.data.ignoreEvents = true;
        const selectGroup = state.getInstance("selectScaleGroup");
        const options = selectGroup.getOptions();
        const groups = options.map((option) => option.value).filter((value) => value !== null);
        let newGroup = 1;
        while (groups.includes(newGroup)) {
            newGroup += 1;
        }
        options.push({
            label: `${newGroup}`,
            value: newGroup
        });
        selectGroup.setOptions(options);
        selectGroup.setValue(newGroup);
        state.getInstance("checkScaleHide").setValue(false);
        state.data.ignoreEvents = false;
    }

    /**
     * handle controller slot events
     *
     * @param {object} state controller state
     * @param {string} slot slot name
     * @param {string} type event type
     * @param {object} event event reference
     */
    function handleSlotEvent(state, slot, type, event) {
        if (state.data.ignoreEvents) {
            return;
        }
        switch (slot) {
        case "dialog":
            if (!state.data.resolved) {
                state.data.resolved = true;
                state.data.resolve(null);
            }
            shmi.deleteControl(state.getInstance("dialog"));
            break;
        case "selectLimitsMode":
            updateState(state);
            break;
        case "buttonApply":
            state.data.resolved = true;
            state.data.resolve(getSlotValues(state));
            state.getInstance("dialog").hide();
            break;
        case "buttonScaleAddGroup":
            handleAddScalegroup(state);
            break;
        default:
        }
    }

    /**
     * get controller configuration for dialog logic controller
     *
     * @param {object} options curve options
     * @param {object} options.cellInfo curve information of current table cell
     * @param {string[]} styleImages style preview image urls
     * @param {function} resolve function to resolve created promise for selection dialog
     * @return {object} dialog controller configuration
     */
    function getDialogController(options, styleImages, resolve) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                dialog: {
                    ui: "dialog-box",
                    events: ["close"]
                },
                selectStyle: {
                    ui: "iq-select-box",
                    optional: true,
                    events: ["change"]
                },
                checkTolerance: {
                    ui: "iq-checkbox",
                    optional: true,
                    events: ["change"]
                },
                checkFill: {
                    ui: "iq-checkbox",
                    optional: true,
                    events: ["change"]
                },
                selectLimitsMode: {
                    ui: "iq-select-box",
                    optional: true,
                    events: ["change"]
                },
                inputLimitsStep: {
                    ui: "iq-input-field",
                    optional: true,
                    events: ["change"]
                },
                inputLimitsMin: {
                    ui: "iq-input-field",
                    optional: true,
                    events: ["change"]
                },
                inputLimitsMax: {
                    ui: "iq-input-field",
                    optional: true,
                    events: ["change"]
                },
                selectScaleLocation: {
                    ui: "iq-select-box",
                    optional: true,
                    events: ["change"]
                },
                checkScaleHide: {
                    ui: "iq-checkbox",
                    optional: true,
                    events: ["change"]
                },
                selectScaleGroup: {
                    ui: "iq-select-box",
                    optional: true,
                    events: ["change"]
                },
                buttonScaleAddGroup: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                },
                buttonApply: {
                    ui: "iq-button",
                    optional: true,
                    events: ["click"]
                }
            },
            onChange: function(state, detail) {
                if (widgetsReady(state) && !state.data.contentSet) {
                    initStyles(state, styleImages);
                    //all widgets ready -> set data & update state
                    setSlotValues(state, (options.cellInfo && options.cellInfo.data) ? options.cellInfo.data.options : {});
                    updateState(state);
                    state.data.contentSet = true;
                }
            },
            onEnable: function(state) {
                state.data.cellInfo = options.cellInfo;
                state.data.resolve = resolve;
            },
            onDisable: function(state) {

            },
            onEvent: handleSlotEvent,
            data: {
                cellInfo: null,
                ignoreEvents: false,
                resolve: null,
                resolved: false,
                contentSet: false
            }
        };
    }

    /**
     * update controller display state
     *
     * @param {object} state controller state
     */
    function updateState(state) {
        if (!widgetsReady(state)) {
            return;
        }
        const selectLimitsMode = state.getInstance("selectLimitsMode"),
            inputLimitsStep = state.getInstance("inputLimitsStep"),
            inputLimitsMin = state.getInstance("inputLimitsMin"),
            inputLimitsMax = state.getInstance("inputLimitsMax");

        const mode = selectLimitsMode.getValue();
        switch (mode) {
        case "config":
            shmi.addClass(inputLimitsStep.element, "hidden");
            shmi.addClass(inputLimitsMin.element, "hidden");
            shmi.addClass(inputLimitsMax.element, "hidden");
            break;
        case "auto":
            shmi.removeClass(inputLimitsStep.element, "hidden");
            shmi.addClass(inputLimitsMin.element, "hidden");
            shmi.addClass(inputLimitsMax.element, "hidden");
            break;
        case "manual":
            shmi.addClass(inputLimitsStep.element, "hidden");
            shmi.removeClass(inputLimitsMin.element, "hidden");
            shmi.removeClass(inputLimitsMax.element, "hidden");
            break;
        default:
        }
    }

    /**
     * get configured ui layout for curve configuration dialog
     *
     * @param {object} options dialog options
     * @param {string} [options.dialogClassName] class-name to apply to dialog
     * @param {string} [options.dialogTemplate] template to use for dialog
     * @param {string} [options.selectStyleClassName] class-name to apply to style select iq-selectbox
     * @param {string} [options.selectStyleTemplate] template to use for style select iq-selectbox
     * @param {string} [options.checkFillClassName] class-name to apply to fill curve iq-checkbox
     * @param {string} [options.checkFillTemplate] template to use for fill curve iq-checkbox
     * @param {string} [options.checkToleranceClassName] class-name to apply to draw tolerances iq-checkbox
     * @param {string} [options.checkToleranceTemplate] template to use for draw tolerances iq-checkbox
     * @param {string} [options.labelLimitsClassName] class-name to apply to limits section iq-label
     * @param {string} [options.labelLimitsTemplate] template to use for limits section iq-label
     * @param {string} [options.selectLimitsModeClassName] class-name to apply to limit mode iq-selectbox
     * @param {string} [options.selectLimitsModeTemplate] template to use for limit mode iq-selectbox
     * @param {string} [options.inputLimitsStepClassName] class-name to apply to limit step iq-input-field
     * @param {string} [options.inputLimitsStepTemplate] template to use for limit step iq-input-field
     * @param {string} [options.inputLimitsMinClassName] class-name to apply to limit min iq-input-field
     * @param {string} [options.inputLimitsMinTemplate] template to use for limit min iq-input-field
     * @param {string} [options.inputLimitsMaxClassName] class-name to apply to limit max iq-input-field
     * @param {string} [options.inputLimitsMaxTemplate] template to use for limit max iq-input-field
     * @param {string} [options.labelScaleClassName] class-name to apply to scale section iq-label
     * @param {string} [options.labelScaleTemplate] template to use for scale section iq-label
     * @param {string} [options.selectScaleLocationClassName] class-name to apply to scale location iq-selectbox
     * @param {string} [options.selectScaleLocationTemplate] template to use for scale location iq-selectbox
     * @param {string} [options.checkScaleHideClassName] class-name to apply to hide scale iq-checkbox
     * @param {string} [options.checkScaleHideTemplate] template to use for hide scale iq-checkbox
     * @param {string} [options.selectScaleGroupClassName] class-name to apply to scale group iq-selectbox
     * @param {string} [options.selectScaleGroupTemplate] template to use for scale group iq-selectbox
     * @param {string} [options.addGroupClassName] class-name to apply to add scale group iq-button
     * @param {string} [options.addGroupTemplate] template to use for add scale group iq-button
     * @param {string} [options.applyClassName] class-name to apply to apply settings iq-button
     * @param {string} [options.applyTemplate] template to use for apply settings iq-button
     * @param {string[]} styleImages style preview image urls
     * @param {function} resolve dialog resolve function
     * @param {function} reject dialog reject function
     * @return {object} ui layout for selection dialog
     */
    function getDialogLayout(options, styleImages, resolve, reject) {
        return {
            "ui": "dialog-box",
            "config": {
                "name": "curveSettingsDialog",
                "class-name": options.dialogClassName || "dialog-box iq-trend-select-dialog",
                "template": options.dialogTemplate || "default/dialog-box",
                "initial-state": "hidden",
                "top-level": true,
                "title": "${iqtrendselect.settings.dialogTitle}",
                "content-template": null,
                "tab-limit": false,
                "_controllers_": getControllerConfig("dialog")
            },
            "controller": getDialogController(options, styleImages, resolve),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "ui": "container",
                        "class-name": "iq-container settings-content",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content",
                        "template": null
                    },
                    "children": [
                        {
                            "ui": "iq-select-box",
                            "config": {
                                "class-name": options.selectStyleClassName || "iq-select-box iq-variant-01 style-select",
                                "template": options.selectStyleTemplate || "default/iq-select-box.iq-variant-01",
                                "name": "styleSelect",
                                "ui": "iq-select-box",
                                "label": "${iqtrendselect.settings.style.label}",
                                "item": null,
                                "auto-label": true,
                                "icon-src": null,
                                "icon-title": null,
                                "icon-class": null,
                                "show-icon": false,
                                "show-text": true,
                                "no-selection-label": "---",
                                "selected": -1,
                                "options": [],
                                "_controllers_": getControllerConfig("selectStyle")
                            },
                            "children": null
                        },
                        {
                            "ui": "iq-checkbox",
                            "config": {
                                "class-name": options.checkFillClassName || "iq-checkbox iq-variant-01 fill-curve",
                                "template": options.checkFillTemplate || "default/iq-checkbox.iq-variant-01",
                                "name": "fillBox",
                                "ui": "iq-checkbox",
                                "label": "${iqtrendselect.settings.fill.label}",
                                "item": null,
                                "auto-label": true,
                                "type": 2,
                                "show-text": true,
                                "show-icon": false,
                                "icon-src": null,
                                "tooltip": null,
                                "icon-class": null,
                                "on-value": true,
                                "off-value": false,
                                "pressed-class": "pressed",
                                "confirm-on": false,
                                "confirm-off": false,
                                "confirm-on-text": "${V_CONFIRM_ON}",
                                "confirm-off-text": "${V_CONFIRM_OFF}",
                                "_controllers_": getControllerConfig("checkFill")
                            },
                            "children": null
                        },
                        {
                            "ui": "iq-checkbox",
                            "config": {
                                "class-name": options.checkToleranceClassName || "iq-checkbox iq-variant-01 draw-tolerance",
                                "template": options.checkToleranceTemplate || "default/iq-checkbox.iq-variant-01",
                                "name": "toleranceBox",
                                "ui": "iq-checkbox",
                                "label": "${iqtrendselect.settings.tolerance.label}",
                                "item": null,
                                "auto-label": true,
                                "type": 2,
                                "show-text": true,
                                "show-icon": false,
                                "icon-src": null,
                                "tooltip": null,
                                "icon-class": null,
                                "on-value": true,
                                "off-value": false,
                                "pressed-class": "pressed",
                                "confirm-on": false,
                                "confirm-off": false,
                                "confirm-on-text": "${V_CONFIRM_ON}",
                                "confirm-off-text": "${V_CONFIRM_OFF}",
                                "_controllers_": getControllerConfig("checkTolerance")
                            },
                            "children": null
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container",
                                "type": "iqflex",
                                "flex-orientation": "column",
                                "flex-none": true,
                                "flex-wrap": false,
                                "flex-primary-align": "start",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "limitsContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-label",
                                    "config": {
                                        "class-name": options.labelLimitsClassName || "iq-label iq-variant-01 limits-header",
                                        "template": options.labelLimitsTemplate || "default/iq-label.iq-variant-01",
                                        "name": "limitsLabel",
                                        "ui": "iq-label",
                                        "item": null,
                                        "type": null,
                                        "auto-type": true,
                                        "decimal-delimiter": ".",
                                        "precision": 2,
                                        "auto-precision": true,
                                        "options": [],
                                        "pattern": null,
                                        "tooltip": null,
                                        "value-as-tooltip": false,
                                        "text": "${iqtrendselect.settings.limits.header}"
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-select-box",
                                    "config": {
                                        "class-name": options.selectLimitsModeClassName || "iq-select-box iq-variant-01 limits-select",
                                        "template": options.selectLimitsModeTemplate || "default/iq-select-box.iq-variant-01",
                                        "name": "limitModeSelect",
                                        "ui": "iq-select-box",
                                        "label": "${iqtrendselect.settings.limits.mode.label}",
                                        "item": null,
                                        "auto-label": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "show-icon": false,
                                        "show-text": true,
                                        "no-selection-label": "---",
                                        "selected": -1,
                                        "options": [
                                            {
                                                "label": "${iqtrendselect.settings.limits.mode.config}",
                                                "value": "config",
                                                "mask": null,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null
                                            },
                                            {
                                                "label": "${iqtrendselect.settings.limits.mode.auto}",
                                                "value": "auto",
                                                "mask": null,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null
                                            },
                                            {
                                                "label": "${iqtrendselect.settings.limits.mode.manual}",
                                                "value": "manual",
                                                "mask": null,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null
                                            }
                                        ],
                                        "_controllers_": getControllerConfig("selectLimitsMode")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-input-field",
                                    "config": {
                                        "class-name": options.inputLimitsStepClassName || "iq-input-field iq-variant-01 step-input",
                                        "template": options.inputLimitsStepTemplate || "default/iq-input-field.iq-variant-01",
                                        "name": "iq-input-field",
                                        "ui": "iq-input-field",
                                        "label": "${iqtrendselect.settings.limits.step.label}",
                                        "item": null,
                                        "min": 0,
                                        "max": null,
                                        "value-alignment": "left",
                                        "numeric-class": "numeric",
                                        "unit": "[Unit]",
                                        "unit-text": "",
                                        "auto-label": false,
                                        "auto-unit-text": false,
                                        "unit-scale": 1,
                                        "auto-min": true,
                                        "auto-max": true,
                                        "type": 3,
                                        "auto-type": false,
                                        "decimal-delimiter": ".",
                                        "numpad-enabled": false,
                                        "precision": -1,
                                        "auto-precision": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "multiline": false,
                                        "show-icon": false,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("inputLimitsStep")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-input-field",
                                    "config": {
                                        "class-name": options.inputLimitsMinClassName || "iq-input-field iq-variant-01 min-input",
                                        "template": options.inputLimitsMinTemplate || "default/iq-input-field.iq-variant-01",
                                        "name": "iq-input-field_1",
                                        "ui": "iq-input-field",
                                        "label": "${iqtrendselect.settings.limits.min.label}",
                                        "item": null,
                                        "min": null,
                                        "max": null,
                                        "value-alignment": "left",
                                        "numeric-class": "numeric",
                                        "unit": "[Unit]",
                                        "unit-text": "",
                                        "auto-label": false,
                                        "auto-unit-text": false,
                                        "unit-scale": 1,
                                        "auto-min": true,
                                        "auto-max": true,
                                        "type": 3,
                                        "auto-type": false,
                                        "decimal-delimiter": ".",
                                        "numpad-enabled": false,
                                        "precision": -1,
                                        "auto-precision": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "multiline": false,
                                        "show-icon": false,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("inputLimitsMin")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-input-field",
                                    "config": {
                                        "class-name": options.inputLimitsMaxClassName || "iq-input-field iq-variant-01 max-input",
                                        "template": options.inputLimitsMaxTemplate || "default/iq-input-field.iq-variant-01",
                                        "name": "iq-input-field_2",
                                        "ui": "iq-input-field",
                                        "label": "${iqtrendselect.settings.limits.max.label}",
                                        "item": null,
                                        "min": null,
                                        "max": null,
                                        "value-alignment": "left",
                                        "numeric-class": "numeric",
                                        "unit": "[Unit]",
                                        "unit-text": "",
                                        "auto-label": false,
                                        "auto-unit-text": false,
                                        "unit-scale": 1,
                                        "auto-min": true,
                                        "auto-max": true,
                                        "type": 3,
                                        "auto-type": false,
                                        "decimal-delimiter": ".",
                                        "numpad-enabled": false,
                                        "precision": -1,
                                        "auto-precision": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "multiline": false,
                                        "show-icon": false,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("inputLimitsMax")
                                    },
                                    "children": null
                                }
                            ]
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container",
                                "type": "iqflex",
                                "flex-orientation": "column",
                                "flex-none": true,
                                "flex-wrap": false,
                                "flex-primary-align": "start",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "scaleContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-label",
                                    "config": {
                                        "class-name": options.labelScaleClassName || "iq-label iq-variant-01 scale-header",
                                        "template": options.labelScaleTemplate || "default/iq-label.iq-variant-01",
                                        "name": "scaleLabel",
                                        "ui": "iq-label",
                                        "item": null,
                                        "type": null,
                                        "auto-type": true,
                                        "decimal-delimiter": ".",
                                        "precision": 2,
                                        "auto-precision": true,
                                        "options": [],
                                        "pattern": null,
                                        "tooltip": null,
                                        "value-as-tooltip": false,
                                        "text": "${iqtrendselect.settings.scale.header}"
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-select-box",
                                    "config": {
                                        "class-name": options.selectScaleLocationClassName || "iq-select-box iq-variant-01 location-select list-on-top",
                                        "template": options.selectScaleLocationTemplate || "default/iq-select-box.iq-variant-01",
                                        "name": "locationSelect",
                                        "ui": "iq-select-box",
                                        "label": "${iqtrendselect.settings.scale.location.label}",
                                        "item": null,
                                        "auto-label": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "show-icon": false,
                                        "show-text": true,
                                        "no-selection-label": "---",
                                        "selected": -1,
                                        "options": [
                                            {
                                                "label": "${iqtrendselect.settings.scale.location.left}",
                                                "value": "left",
                                                "mask": null,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null
                                            },
                                            {
                                                "label": "${iqtrendselect.settings.scale.location.right}",
                                                "value": "right",
                                                "mask": null,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null
                                            }
                                        ],
                                        "_controllers_": getControllerConfig("selectScaleLocation")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-checkbox",
                                    "config": {
                                        "class-name": options.checkScaleHideClassName || "iq-checkbox iq-variant-01 hide-scale",
                                        "template": options.checkScaleHideTemplate || "default/iq-checkbox.iq-variant-01",
                                        "name": "hiddenBox",
                                        "ui": "iq-checkbox",
                                        "label": "${iqtrendselect.settings.scale.hide.label}",
                                        "item": null,
                                        "auto-label": true,
                                        "type": 2,
                                        "show-text": true,
                                        "show-icon": false,
                                        "icon-src": null,
                                        "tooltip": null,
                                        "icon-class": null,
                                        "on-value": true,
                                        "off-value": false,
                                        "pressed-class": "pressed",
                                        "confirm-on": false,
                                        "confirm-off": false,
                                        "confirm-on-text": "${V_CONFIRM_ON}",
                                        "confirm-off-text": "${V_CONFIRM_OFF}",
                                        "_controllers_": getControllerConfig("checkScaleHide")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "container",
                                    "config": {
                                        "class-name": "iq-container",
                                        "type": "iqflex",
                                        "flex-orientation": "row",
                                        "flex-none": true,
                                        "flex-wrap": true,
                                        "flex-primary-align": "start",
                                        "flex-secondary-align": "stretch",
                                        "flex-line-align": "start",
                                        "name": "buttonContainer",
                                        "template": null
                                    },
                                    "children": [
                                        {
                                            "ui": "iq-select-box",
                                            "config": {
                                                "class-name": options.selectScaleGroupClassName || "iq-select-box iq-variant-01 scalegroup-select list-on-top",
                                                "template": options.selectScaleGroupTemplate || "default/iq-select-box.iq-variant-01",
                                                "name": "scaleGroupSelect",
                                                "ui": "iq-select-box",
                                                "label": "${iqtrendselect.settings.scalegroup.label}",
                                                "item": null,
                                                "auto-label": true,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null,
                                                "show-icon": false,
                                                "show-text": true,
                                                "no-selection-label": "---",
                                                "selected": -1,
                                                "options": [],
                                                "_controllers_": getControllerConfig("selectScaleGroup")
                                            },
                                            "children": null
                                        },
                                        {
                                            "ui": "iq-button",
                                            "config": {
                                                "class-name": options.addGroupClassName || "iq-button iq-variant-01 add-scale-group",
                                                "template": options.addGroupTemplate || "default/iq-button.iq-variant-01",
                                                "action": null,
                                                "show-text": true,
                                                "disable-item-lock": false,
                                                "monoflop": false,
                                                "monoflop-value": 1,
                                                "monoflop-interval": 100,
                                                "name": "scaleGroupAdd",
                                                "ui": "iq-button",
                                                "label": "${iqtrendselect.settings.scalegroup.add}",
                                                "item": null,
                                                "auto-label": false,
                                                "icon-src": null,
                                                "icon-title": null,
                                                "icon-class": null,
                                                "label-from-item": false,
                                                "write-bool": false,
                                                "action-release": null,
                                                "action-while-pressed": null,
                                                "interval-while-pressed": null,
                                                "action-press": null,
                                                "disable-alarms": false,
                                                "show-icon": false,
                                                "on-value": true,
                                                "off-value": false,
                                                "tooltip": null,
                                                "_controllers_": getControllerConfig("buttonScaleAddGroup")
                                            },
                                            "children": null
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                },
                {
                    "ui": "container",
                    "config": {
                        "class-name": "iq-container button-container",
                        "type": "iqflex",
                        "flex-orientation": "row",
                        "flex-none": true,
                        "flex-wrap": true,
                        "flex-primary-align": "end",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "buttonContainer",
                        "template": null
                    },
                    "children": [
                        {
                            "ui": "iq-button",
                            "config": {
                                "class-name": options.applyClassName || "iq-button iq-variant-01 apply",
                                "template": options.applyTemplate || "default/iq-button.iq-variant-01",
                                "action": null,
                                "show-text": true,
                                "disable-item-lock": false,
                                "monoflop": false,
                                "monoflop-value": 1,
                                "monoflop-interval": 100,
                                "name": "commentApply",
                                "ui": "iq-button",
                                "label": "${iqtrendselect.settings.apply}",
                                "item": null,
                                "auto-label": false,
                                "icon-src": null,
                                "icon-title": null,
                                "icon-class": null,
                                "label-from-item": false,
                                "write-bool": false,
                                "action-release": null,
                                "action-while-pressed": null,
                                "interval-while-pressed": null,
                                "action-press": null,
                                "disable-alarms": false,
                                "show-icon": false,
                                "on-value": true,
                                "off-value": false,
                                "tooltip": null,
                                "_controllers_": getControllerConfig("buttonApply")
                            },
                            "children": null
                        }
                    ]
                }
            ]
        };
    }

    /**
     * create curve configuration dialog
     *
     * @param {object} options curve config dialog options
     * @param {object} options.cellInfo curve information of current table cell
     * @param {string} [options.dialogClassName] class-name to apply to dialog
     * @param {string} [options.dialogTemplate] template to use for dialog
     * @param {string} [options.selectStyleClassName] class-name to apply to style select iq-selectbox
     * @param {string} [options.selectStyleTemplate] template to use for style select iq-selectbox
     * @param {string} [options.checkFillClassName] class-name to apply to fill curve iq-checkbox
     * @param {string} [options.checkFillTemplate] template to use for fill curve iq-checkbox
     * @param {string} [options.checkToleranceClassName] class-name to apply to draw tolerances iq-checkbox
     * @param {string} [options.checkToleranceTemplate] template to use for draw tolerances iq-checkbox
     * @param {string} [options.labelLimitsClassName] class-name to apply to limits section iq-label
     * @param {string} [options.labelLimitsTemplate] template to use for limits section iq-label
     * @param {string} [options.selectLimitsModeClassName] class-name to apply to limit mode iq-selectbox
     * @param {string} [options.selectLimitsModeTemplate] template to use for limit mode iq-selectbox
     * @param {string} [options.inputLimitsStepClassName] class-name to apply to limit step iq-input-field
     * @param {string} [options.inputLimitsStepTemplate] template to use for limit step iq-input-field
     * @param {string} [options.inputLimitsMinClassName] class-name to apply to limit min iq-input-field
     * @param {string} [options.inputLimitsMinTemplate] template to use for limit min iq-input-field
     * @param {string} [options.inputLimitsMaxClassName] class-name to apply to limit max iq-input-field
     * @param {string} [options.inputLimitsMaxTemplate] template to use for limit max iq-input-field
     * @param {string} [options.labelScaleClassName] class-name to apply to scale section iq-label
     * @param {string} [options.labelScaleTemplate] template to use for scale section iq-label
     * @param {string} [options.selectScaleLocationClassName] class-name to apply to scale location iq-selectbox
     * @param {string} [options.selectScaleLocationTemplate] template to use for scale location iq-selectbox
     * @param {string} [options.checkScaleHideClassName] class-name to apply to hide scale iq-checkbox
     * @param {string} [options.checkScaleHideTemplate] template to use for hide scale iq-checkbox
     * @param {string} [options.selectScaleGroupClassName] class-name to apply to scale group iq-selectbox
     * @param {string} [options.selectScaleGroupTemplate] template to use for scale group iq-selectbox
     * @param {string} [options.addGroupClassName] class-name to apply to add scale group iq-button
     * @param {string} [options.addGroupTemplate] template to use for add scale group iq-button
     * @param {string} [options.applyClassName] class-name to apply to apply settings iq-button
     * @param {string} [options.applyTemplate] template to use for apply settings iq-button
     * @param {string[]} styleImages style preview image urls
     * @return {Promise<object|null>} promise resolving to curve configuration or `null` if closed without selection
     */
    module.getDialog = (options, styleImages = []) => new Promise((resolve, reject) => {
        const baseElement = document.querySelector("[data-ui=container]"),
            baseContainer = baseElement ? shmi.getControlByElement(baseElement) : null;

        if (!baseContainer) {
            reject(new Error("Base container element not found."));
        } else {
            const dialogLayout = getDialogLayout(options, styleImages, resolve, reject);
            if (dialogLayout) {
                baseContainer.addControl(dialogLayout, (err, controls) => {
                    if (err) {
                        reject(err);
                    } else {
                        controls[0].show();
                    }
                });
            }
        }
    });
}());
/**
 * Module to create simple, dynamic datagrids to work with small to medium size
 * datasets. Provides an easy way to create custom datagrids by specifying a loader
 * function for row data.
 *
 * @module designer/tools/grid
 */
(function() {
    var MODULE_NAME = "visuals.controls.iqtrendselect.grid",
        /** @lends module:designer/tools/grid */
        module = shmi.pkg(MODULE_NAME);

    /**
     * conjunctive filter mode: all filters must match
     *
     * @constant
     * @type {number}
     * @default
     */
    module.FILTER_MODE_AND = 1;
    /**
     * disjunctive filter mode: at least one filter must match
     *
     * @constant
     * @type {number}
     * @default
     */
    module.FILTER_MODE_OR = 2;

    /**
     * ASCending sort order
     *
     * @constant
     * @type {number}
     * @default
     */
    module.SORT_ORDER_ASC = 1;

    /**
     * DESCending sort order
     *
     * @constant
     * @type {number}
     * @default
     */
    module.SORT_ORDER_DESC = 2;

    /**
     * getColumnSorter - create sorter function for specified column
     *
     * @param {number} rowIndex column index
     *
     * @returns {function} sorter function
     */
    function getColumnSorter(rowIndex) {
        return function columnSorter(a, b) {
            if (!(a[rowIndex] && b[rowIndex])) {
                throw new Error(MODULE_NAME + " invalid row index: " + rowIndex);
            }
            a = String(a[rowIndex].value).toUpperCase();
            b = String(b[rowIndex].value).toUpperCase();

            if (a < b) {
                return -1;
            } else if (a > b) {
                return 1;
            }
            return 0;
        };
    }

    /**
     * sortGridData - apply sorting to grid data
     *
     * the specified array will be modified by the operation and contains the
     * sorted data after execution.
     *
     * @param  {object[]} rows array of row data
     * @param  {number} colIdx column index
     * @param  {number} order sort order, either `module.SORT_ORDER_ASC` or `module.SORT_ORDER_DESC`
     * @return {undefined}
     */
    function sortGridData(rows, colIdx, order) {
        rows.sort(getColumnSorter(colIdx));
        if (order === module.SORT_ORDER_DESC) {
            rows.reverse();
        }
    }

    /**
     * convertFilterToRegExp - create regular expression from filter string
     *
     * @param  {string} filterString input filter string
     * @return {RegExp} created regular expression
     */
    function convertFilterToRegExp(filterString) {
        //convert to lower case since comparison is done against lower cased field value
        filterString = filterString.toLowerCase();
        // Escape any command characters used in regular expressions.
        filterString = filterString.replace(/([.?*{}[\]():^$!])/g, '\\$1');
        // Have any '%' match any substring and '_' match exactly one character.
        filterString = filterString.replace(/%/g, '.*');
        filterString = filterString.replace(/_/g, '.');

        return RegExp('^' + filterString + '$');
    }

    /**
     * filterGridData - filter row data based on specified filter settings
     *
     * does not modify provided array data and instead returns filtered result.
     *
     * @param  {object[]} rows array of row data
     * @param  {string[]} fields field names
     * @param  {object} filter filter settings
     * @param  {number} mode filter mode, either `module.FILTER_MODE_OR` or `module.FILTER_MODE_AND`
     * @return {object[]} filtered row data
     */
    function filterGridData(rows, fields, filter, mode) {
        var iter = shmi.requires("visuals.tools.iterate").iterateObject,
            filtered = [],
            re = null,
            noFilterSet = true,
            filterMatches = {};

        iter(filter, function(val) {
            if (val !== null) {
                noFilterSet = false;
            }
        });

        rows.forEach(function(r) {
            iter(filter, function(val, prop) {
                const fieldIndex = fields.indexOf(prop);

                filterMatches[prop] = filterMatches[prop] || [];

                if (noFilterSet) {
                    filterMatches[prop].push(r);
                } else if (val !== null) {
                    re = convertFilterToRegExp(val);
                    if (re && String(r[fieldIndex].value).toLowerCase().match(re)) {
                        filterMatches[prop].push(r);
                    }
                } else if (mode === module.FILTER_MODE_AND) {
                    filterMatches[prop].push(r);
                }
            });
        });

        filtered = rows.filter(function(r) {
            let includeRow = false;

            if (mode === module.FILTER_MODE_OR) {
                includeRow = Object.keys(filter).some(function(fieldName) {
                    return (filterMatches[fieldName].indexOf(r) !== -1);
                });
            } else if (mode === module.FILTER_MODE_AND) {
                includeRow = Object.keys(filter).every(function(fieldName) {
                    return (filterMatches[fieldName].indexOf(r) !== -1);
                });
            }

            return includeRow;
        });

        return filtered;
    }

    /**
     * throwError - shortcut to throw error including module name in text output
     *
     * @param  {string} msg error message
     * @return {undefined}
     */
    function throwError(msg) {
        throw new Error(`[${MODULE_NAME}] ${msg}`);
    }

    /**
     * checkOptions - validate types of grid options
     *
     * @param  {object} gridOptions grid options
     * @return {undefined}
     */
    function checkOptions(gridOptions) {
        const dgm = shmi.requires("visuals.session.DataGridManager");
        if (gridOptions === null || typeof gridOptions !== "object") {
            throwError("parameter 'options' must be an object");
        }

        if (typeof gridOptions.name !== "string" || gridOptions.name.trim().length === 0) {
            throwError("option `name` must be a non-empty string");
        }

        if (dgm.getGrid(gridOptions.name)) {
            throwError(`grid '${gridOptions.name}' already exists`);
        }

        if (!Array.isArray(gridOptions.fields)) {
            throwError("option 'fields' must be an array of field names");
        }

        if (!Array.isArray(gridOptions.index)) {
            throwError("option 'index' must be an array of field names");
        }

        if (typeof gridOptions.mode !== "number" || [module.FILTER_MODE_OR, module.FILTER_MODE_AND].indexOf(gridOptions.mode) === -1) {
            throwError(`option 'mode' must either be '${module.FILTER_MODE_OR}' (OR) or '${module.FILTER_MODE_AND}' (AND)`);
        }
    }

    /**
     * setGridData - insert specified row data into datagrid
     *
     * @param  {object} grid datagrid reference
     * @param  {object[]} rowData array of row data
     * @return {undefined}
     */
    function setGridData(grid, rowData) {
        grid.nextRowId = 0;
        grid.indexMap = {};

        if (grid.totalRows > rowData.length) {
            const diff = grid.totalRows - rowData.length;
            for (let i = 0; i < diff; i++) {
                delete grid.data[rowData.length + i];
            }
        }

        rowData.forEach(function(r, idx) {
            grid.insertRow(r, idx, true);
        });

        grid.totalRows = rowData.length;
        grid._updateSubscriber();
    }

    const loadQueue = [];
    let loadRunning = false;

    function checkQueue() {
        if (loadQueue.length && !loadRunning) {
            loadRunning = true;
            loadQueue[0][0](loadQueue[0][1]);
        }
    }

    /**
     * create - creates a new dynamic datagrid.
     *
     * @param {object} options datargrid options
     * @param {string} options.name name of new datagrid
     * @param {string[]} options.fields datagrid field names
     * @param {string[]} options.index selector field names, must be subset of `options.fields`
     * @param {number} options.mode filter mode, either `module.FILTER_MODE_OR` or `module.FILTER_MODE_AND`
     * @param {function} loadFunction data retrieval function
     *
     * @returns {object} DataGrid reference
     */
    module.create = function create(options, loadFunction) {
        var dgm = shmi.requires("visuals.session.DataGridManager"),
            iter = shmi.requires("visuals.tools.iterate.iterateObject"),
            core = shmi.requires("visuals.core"),
            filter = {},
            sort = null,
            grid = null,
            data = [],
            activeData = [];

        checkOptions(options);
        if (typeof loadFunction !== "function") {
            throwError("parameter 'loadFunction' must be a function accepting a callback parameter to return an array of row data");
        }

        /**
         * onLoaded - callback executed when row data is loaded
         *
         * @param {function} callback callback to run when execution has completed
         * @param {object[]} response row data
         * @return {undefined}
         */
        function onLoaded(callback, response) {
            loadRunning = false;
            loadQueue.splice(0, 1);
            if (!Array.isArray(response)) {
                throwError("callback parameter 'response' must be an array of row data");
            }
            data = response;
            if (sort) {
                sortGridData(data, options.fields.indexOf(sort.column), sort.order);
            }
            activeData = filterGridData(data, options.fields, filter, options.mode);
            setGridData(grid, activeData);
            callback();
            shmi.decouple(() => {
                checkQueue();
            });
        }

        options.fields.forEach(function(fieldName) {
            filter[fieldName] = null;
        });

        grid = new core.DataGridDynamic(options.name, options.fields, false, null, 0, options.index);

        //called on new subscriptions
        grid.onSubscribe = function onSubscribe(offset, size, callback) {
            loadQueue.push([loadFunction, onLoaded.bind(null, callback)]);
            checkQueue();
        };

        //called when subscriber changes displayed offset
        grid.onSetOffset = function onSetOffset(offset, subId, callback) {
            shmi.raf(callback);
        };

        //called when active filter is changed
        grid.onSetFilter = function onSetFilter(col, expression, callback) {
            iter(grid.subscribers, function(sub, id) {
                sub.offset = 0;
            });
            filter[options.fields[col]] = expression[0];
            activeData = filterGridData(data, options.fields, filter, options.mode);
            setGridData(grid, activeData);
            callback();
        };

        //called when active filter is cleared
        grid.onClearFilter = function onClearFilter(col, callback) {
            iter(grid.subscribers, function(sub, id) {
                sub.offset = 0;
            });

            if (col === -1) {
                iter(filter, function(val, prop) {
                    filter[prop] = null;
                });
            } else if (options.fields[col]) {
                filter[options.fields[col]] = null;
            }
            activeData = filterGridData(data, options.fields, filter, options.mode);
            setGridData(grid, activeData);
            callback();
        };

        //called to refresh grid data
        grid.onRefresh = function onRefresh(callback) {
            iter(grid.subscribers, function(sub, id) {
                sub.offset = 0;
            });
            loadQueue.push([loadFunction, onLoaded.bind(null, callback)]);
            checkQueue();
        };

        //called when sort settings are changed
        grid.onSort = function onSort(col, order, callback) {
            iter(grid.subscribers, function(sub, id) {
                sub.offset = 0;
            });
            if (col === -1) {
                sort = null;
            } else {
                sort = {
                    column: options.fields[col],
                    order: (order === "DESC") ? module.SORT_ORDER_DESC : module.SORT_ORDER_ASC
                };
            }

            if (sort) {
                sortGridData(data, options.fields.indexOf(sort.column), sort.order);
            }
            activeData = filterGridData(data, options.fields, filter, options.mode);
            setGridData(grid, activeData);
            callback();
        };

        //called when grid row is inserted
        grid.onInsert = function onInsert(rowData, rId, noNotify, callback) {
            shmi.raf(callback);
        };

        //called when grid row is deleted
        grid.onDelete = function onDelete(rId, callback) {
            shmi.raf(callback);
        };

        //called when all grid rows are deleted
        grid.onDeleteAll = function onDeleteAll(callback) {
            shmi.raf(callback);
        };

        //called to update subscriber state
        grid._calcState = function _calcState(subId) {
            var self = this;
            var sub = self.subscribers[subId];
            var state = {};
            state.status = "OK";
            state.offset = sub.offset;
            state.totalRows = self.totalRows;

            return state;
        };

        grid.getGridData = () => shmi.cloneObject(activeData);

        dgm.grids[options.name] = grid;

        console.log(MODULE_NAME, "created datagrid:", options.name);

        return grid;
    };

    module.remove = function remove(name) {
        const dgm = shmi.requires("visuals.session.DataGridManager");

        if (shmi.objectHasOwnProperty(dgm.grids, name)) {
            dgm.grids[name].subscribers = {};
            delete dgm.grids[name];
        }
    };
}());

(function() {
    /**
     * module to configure curve settings in iq-trend-select table
     *
     * @module visuals/controls/iqtrendselect/ls/settings.
     *
     */

    const MODULE_NAME = "visuals.controls.iqtrendselect.ls.settings",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        /** @lends module:visuals/controls/iqtrendselect/ls/settings */
        module = shmi.pkg(MODULE_NAME);

    // MODULE CODE - START

    /* private variables */

    /* private functions */

    /**
     * Implements local-script run function.
     *
     * This function will be called each time a local-script will be enabled.
     *
     * @param {LocalScript} self instance reference of local-script control
     */
    module.run = function(self) {
        const tokens = [],
            im = shmi.requires("visuals.session.ItemManager"),
            itemHandler = im.getItemHandler();
        let detailsButton = null,
            cellInfo = null;

        self.vars = self.vars || {};

        detailsButton = shmi.createControl("iq-button", self.element.parentNode, {
            "class-name": "iq-button iq-variant-01 curve-settings",
            "template": "default/iq-button.iq-variant-01",
            "label": null,
            "show-icon": true,
            "show-text": false,
            "icon-src": "pics/system/controls/iq-trend-select/edit-dark.svg"
        }, "DIV");

        itemHandler.setValue = function(value) {
            try {
                cellInfo = JSON.parse(value);
                if (cellInfo && cellInfo.data) {
                    detailsButton.unlock();
                } else {
                    detailsButton.lock();
                    cellInfo = null;
                }
            } catch (exc) {
                cellInfo = null;
            }
        };

        tokens.push(detailsButton.listen("click", function() {
            const { HANDLE_PREFIX } = shmi.requires("visuals.tools.nodes");
            let styleImages = [],
                settingsDialogOptions = {
                    cellInfo
                };
            const selectWidget = self.config.handle ? shmi.ctrl(`${HANDLE_PREFIX}${self.config.handle}`) : null;
            if (selectWidget) {
                styleImages = selectWidget.getStyleImages();
                settingsDialogOptions = Object.assign(selectWidget.config.settingsDialogOptions || {}, settingsDialogOptions);
            }
            shmi.requires("visuals.controls.iqtrendselect.dialog").getDialog(settingsDialogOptions, styleImages || []).then((response) => {
                if (response !== null) {
                    cellInfo.data.options = response;
                    im.writeValue(self.config.item, JSON.stringify(cellInfo));
                }
            });
        }));

        if (self.config.item) {
            tokens.push(im.subscribeItem(self.config.item, itemHandler));
        }

        /* called when this local-script is disabled */
        self.onDisable = function() {
            self.run = false; // from original .onDisable function of LocalScript control
            tokens.forEach(function(t) {
                t.unlisten();
            });
            shmi.deleteControl(detailsButton);
        };
    };
    // MODULE CODE - END

    fLog("module loaded");
})();
(function() {
    /**
     * module to preview style for iq-trend-select
     *
     * @module visuals/controls/iqtrendselect/ls/style.
     *
     */

    const MODULE_NAME = "visuals.controls.iqtrendselect.ls.style",
        ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(MODULE_NAME, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log,
        /** @lends module:visuals/controls/iqtrendselect/ls/style */
        module = shmi.pkg(MODULE_NAME);

    // MODULE CODE - START

    /* private variables */

    /* private functions */

    /**
     * Implements local-script run function.
     *
     * This function will be called each time a local-script will be enabled.
     *
     * @param {LocalScript} self instance reference of local-script control
     */
    module.run = function(self) {
        const tokens = [],
            im = shmi.requires("visuals.session.ItemManager"),
            itemHandler = im.getItemHandler();

        self.vars = self.vars || {};

        let imageElement = null;

        itemHandler.setValue = function(value) {
            if (value) {
                if (!imageElement) {
                    imageElement = document.createElement("img");
                }
                imageElement.classList.add("style-image");
                imageElement.setAttribute("src", value);
                self.element.parentNode.appendChild(imageElement);
            } else if (imageElement) {
                imageElement.parentNode.removeChild(imageElement);
                imageElement = null;
            }
        };

        if (self.config.item) {
            tokens.push(im.subscribeItem(self.config.item, itemHandler));
        }

        /* called when this local-script is disabled */
        self.onDisable = function() {
            self.run = false; // from original .onDisable function of LocalScript control
            tokens.forEach(function(t) {
                t.unlisten();
            });
            if (imageElement) {
                imageElement.parentNode.removeChild(imageElement);
                imageElement = null;
            }
        };
    };
    // MODULE CODE - END

    fLog("module loaded");
})();
/**
 * WebIQ Visuals widget to select sets of trend values displayed in IQ Trend
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-set-select",
 *     "name": null,
 *     "template": "default/iq-trend-set-select"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default CSS class applied on widget root element
 * name {string}: Name of widget set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.2
 */
(function() {
    'use strict';

    // variables for reference in widget definition
    const className = "IqTrendSetSelect", // widget name in camel case
        uiType = "iq-trend-set-select", // widget keyword (data-ui)
        isContainer = true,
        storageIdentifierSets = "webiq-trend-sets",
        storageIdentifierSettings = "webiq-widget-settings";

    // example - default configuration
    const defaultConfig = {
        "class-name": "iq-trend-set-select",
        "name": null,
        "template": "default/iq-trend-set-select",
        "label": uiType,
        "trend-widget": null,
        "trend-sets-identifier": "trend-sets-1",
        "storage-location": "localStorage",
        "trend-sets": [],
        "selected": null,
        "popup-location": "bottom",
        "modified-marker": "(*)",
        "remember-last-set": false,
        "layoutOptions": {
            "selectBoxClassName": "iq-select-box iq-variant-01 set-select",
            "selectBoxTemplate": "default/iq-select-box.iq-variant-01"
        },
        "fixed-set-icon": "pics/system/controls/iq-trend-set-select/fixed-set.svg"
    };

    // setup module-logger
    const ENABLE_LOGGING = true, //enable logging with 'log' function
        RECORD_LOG = false, //store logged messages in logger memory
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog, //force log - logs message even if logger is disabled
        log = logger.log; //log message if logger is enabled

    // declare private functions - START

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        //trend active
        updateTrendWidget(self);
        self.vars.trendWidgetTokens.push(control.listen("value-info", (evt) => {
            checkModified(self);
        }));
        self.vars.trendWidgetTokens.push(control.listen("graph-options", (evt) => {
            checkModified(self);
        }));
    }

    /**
     * check if selected set is modified
     *
     * @param {object} self control instance
     */
    function checkModified(self) {
        if (self.vars.selected) {
            const trendSet = self.vars.trendSets.find(({ name }) => name === self.vars.selected);
            if (trendSet) {
                const { isEqualDeep } = shmi.requires("visuals.tools.compare");
                if (trendSet.fixed || isEqualDeep(self.vars.trendWidget.getSelectedValues(), trendSet.values)) {
                    setModified(self, false);
                } else {
                    setModified(self, true);
                }
            }
        }
    }

    /**
     * set modified state
     *
     * @param {object} self control instance
     * @param {boolean} modified modified state
     */
    function setModified(self, modified) {
        self.vars.modified = modified;
        if (modified) {
            shmi.addClass(self.element, "modified");
        } else {
            shmi.removeClass(self.element, "modified");
        }
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        self.vars.trendWidget = null;
        //trend inactive
        self.vars.trendWidgetTokens.forEach((t) => t.unlisten());
        self.vars.trendWidgetTokens = [];
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    /**
     * initialize fixed (pre-configured) trend sets
     *
     * @param {object} self control instance
     */
    function initFixedTrendSets(self) {
        self.vars.trendSets = shmi.cloneObject(self.config["trend-sets"]);
        self.vars.trendSets.forEach((trendSet) => {
            trendSet.fixed = true;
        });
    }

    /**
     * save trend sets to localStorage
     *
     * @param {object} self control instance
     */
    function saveTrendSetsLocalStorage(self) {
        const settingsText = localStorage.getItem(storageIdentifierSets),
            username = shmi.requires("visuals.session.UserManager").currentUser.name;
        let trendSetData = {};

        if (settingsText) {
            try {
                trendSetData = JSON.parse(settingsText);
            } catch (exc) {
                console.error(`[${uiType}] could not parse saved selection:`, exc);
            }
        }

        trendSetData[username] = trendSetData[username] || {};
        trendSetData[username][self.vars.trendSetsIdentifier] = self.vars.trendSets.filter((trendSet) => !trendSet.fixed);

        localStorage.setItem(storageIdentifierSets, JSON.stringify(trendSetData, null, 4));
    }

    /**
     * save trend sets to user data
     *
     * @param {object} self control instance
     * @return {Promise} promise resolving when data has been set
     */
    function saveTrendSetsUserData(self) {
        const trendSets = self.vars.trendSets.filter((trendSet) => !trendSet.fixed);
        return new Promise((resolve, reject) => {
            shmi.requires("visuals.session.UserManager").setUserData({
                [`${storageIdentifierSets}:${self.vars.trendSetsIdentifier}`]: JSON.stringify(trendSets, null, 4)
            }, (response, error) => {
                if (error) {
                    reject(error);
                } else {
                    resolve(response);
                }
            });
        });
    }

    /**
     * load trend sets from localStorage
     *
     * @param {object} self control instance
     */
    function loadTrendSetsLocalStorage(self) {
        const settingsText = localStorage.getItem(storageIdentifierSets),
            username = shmi.requires("visuals.session.UserManager").currentUser.name;
        let trendSetData = {};

        if (settingsText) {
            try {
                trendSetData = JSON.parse(settingsText);
            } catch (exc) {
                console.error(`[${uiType}] could not parse saved selection:`, exc);
            }
        }

        trendSetData[username] = trendSetData[username] || {};
        trendSetData[username][self.vars.trendSetsIdentifier] = trendSetData[username][self.vars.trendSetsIdentifier] || [];

        self.vars.trendSets.push(...trendSetData[username][self.vars.trendSetsIdentifier]);
    }

    /**
     * load trend sets from user data
     *
     * @param {object} self control instance
     */
    function loadTrendSetsUserData(self) {
        const settingsText = shmi.requires("visuals.session.UserManager").getUserData(`${storageIdentifierSets}:${self.vars.trendSetsIdentifier}`);
        let trendSetData = [];

        if (settingsText) {
            try {
                trendSetData = JSON.parse(settingsText);
            } catch (exc) {
                console.error(`[${uiType}] could not parse saved selection:`, exc);
            }
        }
        self.vars.trendSets.push(...trendSetData);
    }

    /**
     * load custom trend sets
     *
     * @param {object} self control instance
     */
    function loadTrendSets(self) {
        switch (self.vars.storageLocation) {
        case "localStorage":
            loadTrendSetsLocalStorage(self);
            break;
        case "userData":
            loadTrendSetsUserData(self);
            break;
        default:
            throw new Error(`Invalid storage location configured: ${self.vars.storageLocation}`);
        }
        self.vars.setsLoaded = true;
    }

    /**
     * save custom trend sets
     *
     * @param {object} self control instance
     */
    async function saveTrendSets(self) {
        switch (self.vars.storageLocation) {
        case "localStorage":
            saveTrendSetsLocalStorage(self);
            break;
        case "userData":
            await saveTrendSetsUserData(self);
            break;
        default:
            throw new Error(`Invalid storage location configured: ${self.vars.storageLocation}`);
        }
    }

    /**
     * update values displayed in connected trend widget
     *
     * @param {object} self control instance
     */
    async function updateTrendWidget(self) {
        if (self.vars.trendWidget && self.vars.selected) {
            const selected = self.vars.selected;
            const trendSet = self.vars.trendSets.find(({ name }) => name === self.vars.selected);
            if (trendSet) {
                const widgetInfo = self.vars.trendWidget.getTrendInfo();
                const setItems = trendSet.values.map((value) => value.name);
                if (widgetInfo.type === "trend" && widgetInfo.name) {
                    const tm2 = shmi.requires("visuals.core.TrendManager2");
                    let info = null;
                    try {
                        info = await tm2.getInfo(widgetInfo.name, setItems);
                    } catch (exc) {
                        console.error(`[trend2.info] error retrieving trend info for trend: ${self._name}:`, exc);
                    }
                    const infoItems = info === null ? [] : info.items.map((item) => item.name);
                    trendSet.values = trendSet.values.filter((value) => {
                        const isInTrend = infoItems.includes(value.name);
                        if (!isInTrend) {
                            console.warn(`[${className}] item included in set is not present in trend '${widgetInfo.name}': ${value.name}`);
                        }
                        return isInTrend;
                    });
                }
                //check again if trend widget and selection are still present
                if (self.vars.trendWidget && (selected === self.vars.selected)) {
                    self.vars.trendWidget.setSelectedValues(trendSet.values);
                }
            }
        }
    }

    /**
     * restore last set from local-storage
     *
     * @param {object} self control instance
     */
    function restoreLastSet(self) {
        const settingsText = localStorage.getItem(storageIdentifierSettings);
        if (settingsText) {
            try {
                const username = shmi.requires("visuals.session.UserManager").currentUser.name,
                    widgetSettings = JSON.parse(settingsText);

                let handle = shmi.requires("visuals.tools.nodes").getNodeHandle(self);
                if (!handle) {
                    handle = self.getName(); //use instance name if node handle not available
                }

                if (widgetSettings?.[username]?.[uiType]?.[handle]?.selectedSet) {
                    const trendSet = self.vars.trendSets.find(({ name }) => name === widgetSettings[username][uiType][handle].selectedSet);
                    if (trendSet) {
                        self.vars.selected = trendSet.name;
                    }
                }
            } catch (exc) {
                console.error(`[${uiType}] could not parse saved widget settings:`, exc);
            }
        }
    }

    /**
     * save last set in local-storage
     *
     * @param {object} self control instance
     */
    function saveLastSet(self) {
        if (self.config["remember-last-set"]) {
            const username = shmi.requires("visuals.session.UserManager").currentUser.name,
                settingsText = localStorage.getItem(storageIdentifierSettings);
            let widgetSettings = {};

            let handle = shmi.requires("visuals.tools.nodes").getNodeHandle(self);
            if (!handle) {
                handle = self.getName(); //use instance name if node handle not available
            }

            if (settingsText) {
                try {
                    widgetSettings = JSON.parse(settingsText);
                } catch (exc) {
                    console.error(`[${uiType}] could not parse saved widget settings:`, exc);
                }
            }

            widgetSettings[username] = widgetSettings[username] || {};
            widgetSettings[username][uiType] = widgetSettings[username][uiType] || {};
            widgetSettings[username][uiType][handle] = widgetSettings[username][uiType][handle] || {};
            widgetSettings[username][uiType][handle].selectedSet = shmi.cloneObject(self.vars.selected);

            localStorage.setItem(storageIdentifierSettings, JSON.stringify(widgetSettings));
        }
    }

    /**
     * update options available in set selection box
     *
     * @param {object} control instance
     */
    function updateSetOptions(self) {
        if (self.vars.selectBox) {
            self.vars.ignoreChange = true;
            self.vars.selectBox.setOptions(self.vars.trendSets.map(({ name, fixed }) => ({
                "label": name,
                "tooltip": name,
                "value": name,
                "icon-src": fixed && self.config["fixed-set-icon"] ? self.config["fixed-set-icon"] : null
            })));
            if (!self.vars.selectBox.initialized) {
                const selectedIdx = self.vars.trendSets.findIndex(({ name }) => name === self.vars.selected);
                self.vars.selectBox.config.selected = selectedIdx;
            } else {
                self.vars.selectBox.setValue(self.vars.selected);
            }
            self.vars.ignoreChange = false;
        }
    }

    // declare private functions - END

    // definition of new widget extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defaultConfig,
        /* instance variables */
        vars: {
            elements: {
                selectionAnchor: null
            },
            selectBox: null,
            trendWidget: null,
            trendSetsIdentifier: null,
            selected: null,
            trendSets: [],
            tokens: [],
            trendWidgetTokens: [],
            storageLocation: null,
            setsLoaded: false,
            ignoreChange: false,
            modified: false
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager"
        },

        /* array of custom event types fired by this widget */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;

                self.vars.trendSetsIdentifier = self.config["trend-sets-identifier"];
                self.vars.storageLocation = self.config["storage-location"];
                self.vars.selected = self.config.selected ? self.config.selected : null;

                if (self.config["modified-marker"]) {
                    self.element.style.setProperty("--modified-marker-content", `"${self.config["modified-marker"]} "`);
                }

                self.vars.elements.selectionAnchor = shmi.getUiElement("selection-anchor", self.element);
                if (self.vars.elements.selectionAnchor) {
                    self.vars.selectBox = shmi.createControl("iq-select-box", self.vars.elements.selectionAnchor, {
                        "class-name": (self.config.layoutOptions?.selectBoxClassName ?? "iq-select-box iq-variant-01 set-select") + (self.config["popup-location"] === "top" ? " list-on-top" : ""),
                        "template": (self.config.layoutOptions?.selectBoxTemplate ?? "default/iq-select-box.iq-variant-01"),
                        "label": self.config.label,
                        "name": "trendSetSelectBox"
                    }, "DIV");
                } else {
                    console.error(`[${className}] template is missing required element ("selection-anchor")`);
                }
            },
            /* called when widget is enabled */
            onEnable: function() {
                const self = this;

                self.controls.forEach((c) => {
                    c.enable();
                });

                initFixedTrendSets(self);
                loadTrendSets(self);

                if (self.config["remember-last-set"]) {
                    restoreLastSet(self);
                }

                if (self.vars.selectBox) {
                    updateSetOptions(self);

                    self.vars.tokens.push(self.vars.selectBox.listen("change", (evt) => {
                        if (self.vars.ignoreChange) {
                            return;
                        }
                        self.vars.selected = evt.detail.value;
                        updateTrendWidget(self);
                        setModified(self, false);
                        saveLastSet(self);
                    }));
                }

                if (self.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(self.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(self, trendWidget);
                    }

                    attachTrendListeners(self);
                }
            },
            /* called when widget is disabled */
            onDisable: function() {
                const self = this;
                self.controls.forEach((c) => {
                    c.disable();
                });
                self.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                self.vars.tokens = [];
                self.vars.trendWidgetTokens.forEach((t) => t.unlisten());
                self.vars.trendWidgetTokens = [];
                self.vars.trendWidget = null;
            },
            /* called when widget is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                if (self.vars.selectBox) {
                    self.vars.selectBox.lock();
                }
            },
            /* called when widget is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                if (self.vars.selectBox) {
                    self.vars.selectBox.unlock();
                }
            },
            /* called by ItemManager when value of subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /* called by ItemManager to provide properties (min & max values etc.) of subscribed item */
            onSetProperties: function(min, max, step, name, type, warnMin, warnMax, prewarnMin, prewarnMax, digits) {

            },
            /* called when widget is deleted - used for instance clean-up */
            onDelete: function() {

            },
            /**
             * get connected trend widget instance
             *
             * @returns {object|null} trend widget instance
            */
            getTrendWidget: function() {
                return this.vars.trendWidget;
            },
            /**
             * get identifier used to store trend sets
             *
             * @returns {string} trend sets identifier
            */
            getTrendSetsIdentifier: function() {
                return this.vars.trendSetsIdentifier;
            },
            /**
             * get list of available trend sets
             *
             * @returns {object[]} trend sets
            */
            getTrendSets: function() {
                return this.vars.setsLoaded ? this.vars.trendSets : null;
            },
            /**
             * get currently selected trend set
             *
             * @returns {object|null} trend set or `null` if none is selected
            */
            getSelected: function() {
                const self = this,
                    selectedSet = self.vars.trendSets.find(({ name }) => name === self.vars.selected);
                return selectedSet ? selectedSet : null;
            },
            /**
             * create new trend set
             *
             * @param {string} setName trend set name
             * @returns {boolean} `true` if trend set was created, `false` else
            */
            createTrendSet: async function(setName) {
                const self = this;
                const trendSet = self.vars.trendSets.find(({ name }) => name === setName);
                if (!trendSet && self.vars.trendWidget) {
                    self.vars.trendSets.push({
                        name: setName,
                        values: self.vars.trendWidget.getSelectedValues()
                    });
                    await saveTrendSets(self);

                    self.vars.selected = setName;
                    setModified(self, false);
                    updateSetOptions(self);
                    saveLastSet(self);

                    return true;
                }
                return false;
            },
            /**
             * rename selected trend set
             *
             * @param {string} setName new trend set name
             * @returns {boolean} `true` if trend set has been renamed, `false` else
             */
            renameTrendSet: async function(setName) {
                const self = this,
                    exists = !self.vars.trendSets.every(({ name }) => name !== setName),
                    trendSet = self.getSelected();

                if (!exists && trendSet && self.vars.trendWidget) {
                    trendSet.name = setName;
                    await saveTrendSets(self);

                    self.vars.selected = setName;
                    checkModified(self);
                    updateSetOptions(self);
                    saveLastSet(self);

                    return true;
                }
                return false;
            },
            /**
             * save currently selected trend set
             *
             * @returns {boolean} `true` if trend set has been saved, `false` else
             */
            saveTrendSet: async function() {
                const self = this;
                const trendSet = self.vars.trendSets.find(({ name, fixed }) => name === self.vars.selected && !fixed);
                if (trendSet && self.vars.trendWidget) {
                    trendSet.values = self.vars.trendWidget.getSelectedValues();
                    await saveTrendSets(self);
                    setModified(self, false);
                    return true;
                }
                return false;
            },
            /**
             * delete currently selected trend set
             *
             * @returns {boolean} `true` if trend set has been deleted, `false` else
             */
            deleteTrendSet: async function() {
                const self = this;

                if (self.vars.selected) {
                    const oldCount = self.vars.trendSets.length;
                    self.vars.trendSets = self.vars.trendSets.filter((trendSet) => (trendSet.name !== self.vars.selected) || trendSet.fixed);
                    if (self.vars.trendSets.length !== oldCount) {
                        self.vars.selected = null;
                        setModified(self, false);
                        updateSetOptions(self);

                        await saveTrendSets(self);
                        saveLastSet(self);

                        return true;
                    }
                }

                return false;
            },
            /**
             * check if currently selected trend set is modified
             *
             * @returns {boolean} `true` if trend set is modified, `false` else
             */
            isTrendSetModified: function() {
                return this.vars.modified;
            }
        }
    };

    // definition of new widget extending BaseControl - END

    // generate widget constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();

/**
 * iq-trend-time
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-time",
 *     "name": null,
 *     "template": "default/iq-trend-time"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-trend-time", // control name in camel-case
        uiType = "iq-trend-time", // control keyword (data-ui)
        isContainer = true;

    // default configuration
    const defConfig = {
        "class-name": "iq-trend-time",
        "name": null,
        "template": "default/iq-trend-time",
        "trend-widget": null,
        "mode": "start-end", //either "start-end", or "start-duration"
        "date-time-format": "$DD.$MM.$YYYY, $HH:$mm:$ss",
        "popup-orientation": "bottom-left",
        "startLabel": "Start",
        "endLabel": "End",
        "durationLabel": "Duration",
        "layoutOptions": {
            "dateLabel": "Select Date",
            "timeLabel": "Select Time",
            "okLabel": "OK",
            "cancelLabel": "Cancel"
        }
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * update display start & end time
     *
     * @param {object} self control instance
     */
    function updateDisplayTime(self) {
        const { formatDateTime } = shmi.requires("visuals.tools.date"),
            { startValue, endValue } = self.vars.elements;
        if (self.vars.trendWidget) {
            const dim = self.vars.trendWidget.getDimensionsX();
            if (self.config.mode === "start-end") {
                if (startValue) {
                    startValue.textContent = formatDateTime(Math.floor(dim.min / 1000), shmi.localize(self.config["date-time-format"]));
                }
                if (endValue) {
                    endValue.textContent = formatDateTime(Math.floor(dim.max / 1000), shmi.localize(self.config["date-time-format"]));
                }
            } else if (self.config.mode === "start-duration") {
                if (startValue) {
                    startValue.textContent = formatDateTime(dim.min / 1000, shmi.localize(self.config["date-time-format"]));
                }
            }
        }
    }

    /**
     * update inactive-mode state of trend widget
     *
     * @param {object} self control instance
     */
    function updateInactiveState(self) {
        self.vars.isInactive = !self.vars.trendWidget;
        if (self.vars.isInactive) {
            shmi.addClass(self.element, "inactive-mode");
            if (self.vars.durationSelect) {
                self.vars.durationSelect.lock();
            }
        } else {
            shmi.removeClass(self.element, "inactive-mode");
            if (self.vars.durationSelect) {
                self.vars.durationSelect.unlock();
            }
        }
    }

    /**
     * update popup-anchor position based on selected orientation
     *
     * @param {object} self control instance
     * @param {boolean} isStart `true` to positon for start-time, `false` for end-time
     */
    function updatePopupAnchor(self, isStart = true) {
        const contentElement = self.element.querySelector(".widget-content"),
            outerBb = contentElement ? contentElement.getBoundingClientRect() : self.element.getBoundingClientRect(),
            { popup, startTime, endTime } = self.vars.elements;
        let innerBb = null;
        let offset = 0;

        switch (self.config["popup-orientation"]) {
        case "bottom-right":
        case "top-right":
            innerBb = isStart ? startTime.getBoundingClientRect() : endTime.getBoundingClientRect();
            offset = (outerBb.x + outerBb.width) - (innerBb.x + innerBb.width);
            popup.style.right = `${offset}px`;
            break;
        case "bottom-left":
        case "top-left":
            innerBb = isStart ? startTime.getBoundingClientRect() : endTime.getBoundingClientRect();
            offset = innerBb.x - outerBb.x;
            popup.style.left = `${offset}px`;
            break;
        default:
        }
    }

    /**
     * wait for control to initialize
     *
     * @returns {Promise} promise resolving when control is initialized
    */
    function onInitControl(control) {
        return new Promise((resolve) => {
            shmi.waitOnInit(control, resolve);
        });
    }

    /**
     * update selectable durations
     *
     * @param {object} self control instance
     */
    async function updateDurationOptions(self) {
        if (self.vars.trendWidget && self.vars.durationSelect) {
            await onInitControl(self.vars.durationSelect);
            const { formatDuration } = shmi.requires("visuals.tools.date");
            const options = self.vars.trendWidget.getDisplayDurations().reverse().map((duration) => ({
                label: formatDuration(duration, "compact"),
                value: duration
            }));
            self.vars.ignoreChange = true;
            self.vars.durationSelect.setOptions(options);
            const currentDuration = self.vars.trendWidget.getDisplayDuration();
            self.vars.durationSelect.setValue(currentDuration);
            self.vars.ignoreChange = false;
        }
    }

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        updateDisplayTime(self);
        updateInactiveState(self);
        updateDurationOptions(self);

        self.vars.tokens.push(control.listen("live", (evt) => {
            if (!evt.detail.isLive) {
                updateDisplayTime(self);
            }
        }));

        self.vars.tokens.push(control.listen("display-duration", (evt) => {
            updateDisplayTime(self);
            if (self.vars.durationSelect) {
                self.vars.ignoreChange = true;
                self.vars.durationSelect.setValue(evt.detail.duration);
                self.vars.ignoreChange = false;
            }
        }));

        self.vars.tokens.push(control.listen("display-time", updateDisplayTime.bind(null, self)));

        if (!self.isLocked()) {
            shmi.removeClass(self.element, "locked");
        }
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        self.vars.trendWidget = null;

        updateInactiveState(self);
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            trendWidget: null,
            elements: {
                startTime: null,
                startValue: null,
                endTime: null,
                endValue: null,
                duration: null,
                popup: null
            },
            durationSelect: null,
            ignoreChange: false,
            tokens: [],
            listeners: [],
            isInactive: false,
            templateElementsFound: false
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { getPopup } = shmi.requires("visuals.controls.iqtrendtime.popup"),
                    { MouseListener, TouchListener } = shmi.requires("visuals.io"),
                    { elements } = self.vars;

                elements.startTime = shmi.getUiElement("start-time", self.element);
                elements.startLabel = shmi.getUiElement("start-label", self.element);
                elements.startValue = elements.startTime ? shmi.getUiElement("label", elements.startTime) : null;
                elements.endTime = shmi.getUiElement("end-time", self.element);
                elements.endLabel = shmi.getUiElement("end-label", self.element);
                elements.endValue = elements.endTime ? shmi.getUiElement("label", elements.endTime) : null;
                elements.duration = shmi.getUiElement("duration", self.element);
                elements.popup = shmi.getUiElement("popup-anchor", self.element);

                if (elements.startTime && elements.startValue && elements.startLabel && elements.endTime && elements.endValue && elements.endLabel && elements.duration && elements.popup) {
                    self.vars.templateElementsFound = true;
                } else {
                    console.error(`[${uiType}] template is missing required elements: ${self.config.template}`);
                    return;
                }

                if (["top-right", "top-left"].includes(self.config["popup-orientation"])) {
                    shmi.addClass(elements.popup, "popup-on-top");
                }

                if (["top-right", "bottom-right"].includes(self.config["popup-orientation"])) {
                    shmi.addClass(elements.popup, "right-orientation");
                }

                elements.startLabel.textContent = shmi.localize(self.config.startLabel);
                elements.endLabel.textContent = shmi.localize(self.config.endLabel);

                if (elements.startTime) {
                    const handler = {
                        onClick: async () => {
                            if (self.vars.trendWidget) {
                                updatePopupAnchor(self, true);
                                const dim = self.vars.trendWidget.getDimensionsX(),
                                    result = await getPopup(self, Object.assign(self.config.layoutOptions || {}, {
                                        timestamp: dim.min
                                    }));
                                if (result !== null) {
                                    self.vars.trendWidget.setOptions({
                                        tStart: result
                                    });
                                    if (self.config.mode === "start-end") {
                                        const duration = Math.floor(dim.max - result);
                                        self.vars.trendWidget.setDisplayDuration(duration >= 1000 ? duration : 1000);
                                    }
                                }
                            }
                        }
                    };
                    self.vars.listeners.push(
                        new MouseListener(elements.startTime, handler),
                        new TouchListener(elements.startTime, handler)
                    );
                }

                if (self.config.mode === "start-end") {
                    shmi.addClass(self.element, "mode-start-end");
                    if (elements.endTime) {
                        const handler = {
                            onClick: async () => {
                                if (self.vars.trendWidget) {
                                    updatePopupAnchor(self, false);
                                    const dim = self.vars.trendWidget.getDimensionsX(),
                                        result = await getPopup(self, Object.assign(self.config.layoutOptions || {}, {
                                            timestamp: dim.max
                                        }));
                                    if (result !== null) {
                                        const duration = Math.floor(result - dim.min);
                                        if (duration < 1000) {
                                            self.vars.trendWidget.setOptions({
                                                tStart: result - 1000
                                            });
                                        } else {
                                            self.vars.trendWidget.setOptions({
                                                tStart: dim.min
                                            });
                                        }
                                        self.vars.trendWidget.setDisplayDuration(duration >= 1000 ? duration : 1000);
                                    }
                                }
                            }
                        };
                        self.vars.listeners.push(
                            new MouseListener(elements.endTime, handler),
                            new TouchListener(elements.endTime, handler)
                        );
                    }
                } else if (self.config.mode === "start-duration") {
                    shmi.addClass(self.element, "mode-start-duration");
                    if (elements.duration) {
                        const { layoutOptions } = self.config,
                            durationClassName = layoutOptions && layoutOptions.selectDurationClassName ? layoutOptions.selectDurationClassName : `iq-select-box iq-variant-01${["top-right", "top-left"].includes(self.config["popup-orientation"]) ? " list-on-top": ""}`,
                            durationTemplate = layoutOptions && layoutOptions.selectDurationTemplate ? layoutOptions.selectDurationTemplate : "default/iq-select-box.iq-variant-01";
                        self.vars.durationSelect = shmi.createControl("iq-select-box", elements.duration, {
                            "label": self.config.durationLabel,
                            "class-name": durationClassName,
                            "template": durationTemplate
                        });
                        self.vars.tokens.push(self.vars.durationSelect.listen("change", (evt) => {
                            if (self.vars.trendWidget && !self.vars.ignoreChange) {
                                self.vars.trendWidget.setDisplayDuration(evt.detail.value);
                            }
                        }));
                    }
                }
            },
            /* Called when control is enabled */
            onEnable: function() {
                if (!this.vars.templateElementsFound) {
                    return;
                }

                if (this.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(this.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(this, trendWidget);
                    }

                    attachTrendListeners(this);
                }
                updateInactiveState(this);
                this.controls.forEach((c) => {
                    c.enable();
                });
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            },
            /* Called when control is disabled */
            onDisable: function() {
                if (!this.vars.templateElementsFound) {
                    return;
                }
                onTrendDisable(this);
                this.vars.tokens.forEach((t) => {
                    if (t.name !== "change") { //keep duration change listener token
                        t.unlisten();
                    }
                });
                this.vars.tokens = this.vars.tokens.filter((t) => t.name === "change");
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
                this.controls.forEach((c) => {
                    c.disable();
                });
            },
            /* Called when control is deleted */
            onDelete: function() {
                this.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.tokens = [];
            },
            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
                if (!this.vars.templateElementsFound) {
                    return;
                }
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
                if (this.vars.durationSelect) {
                    this.vars.durationSelect.lock();
                }
            },
            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                if (this.vars.trendWidget) {
                    shmi.removeClass(this.element, 'locked');
                }
                if (!this.vars.templateElementsFound) {
                    return;
                }
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
                if (this.vars.durationSelect) {
                    this.vars.durationSelect.unlock();
                }
            },
            /**
             * add child controls to popup-anchor element of this control
             *
             * @param {shmi.visuals.core.BaseControl~ControlOptions} options nested control hierarchy to add with options
             * @param {function} callback function to call when controls have been created & initialized
             */
            onAddControl: function(options, callback) {
                const self = this,
                    cm = shmi.requires("visuals.tools.controller"),
                    controls = [];
                let initToken = null;

                if (!self.initialized) {
                    throw new Error("Control not initialized yet.");
                }

                if (!self.vars.elements.popup) {
                    throw new Error("Element missing in template: 'popup-anchor'");
                }

                if (!Array.isArray(options)) {
                    options = [options];
                }

                options.forEach(function(option) {
                    const control = shmi.createControl(option.ui, self.vars.elements.popup, option.config, "DIV", null, false);
                    if (control !== null) {
                        if (option.style && typeof option.style === "object") {
                            let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                            iter(option.style, (value, name) => {
                                control.element.style[name] = value;
                            });
                        }
                        controls.push(control);
                    }
                });

                initToken = shmi.waitOnInit(controls, function() {
                    const
                        errors = [],
                        tm = shmi.requires("visuals.task"),
                        tasks = [];
                    let idx = -1,
                        tl = null;

                    if (initToken) {
                        idx = self._init_.tokens.indexOf(initToken);
                        if (idx !== -1) {
                            self._init_.tokens.splice(idx, 1);
                        }
                        initToken.unlisten();
                        initToken = null;
                    }

                    options.forEach(function(opt, jdx) {
                        const control = controls[jdx];
                        let t = null;

                        if (opt.controller) {
                            cm.create(opt.controller.name, opt.controller, control.getName());
                        }

                        if (self.isActive()) {
                            control.enable();
                        }

                        if (Array.isArray(opt.children) && opt.children.length) {
                            t = tm.createTask("child control");
                            t.run = function() {
                                control.addControl(opt.children, function(childErr, childOptions) {
                                    if (childErr) {
                                        errors.push(childErr);
                                    }
                                    t.complete();
                                });
                            };
                            tasks.push(t);
                        }
                    });

                    if (tasks.length) {
                        tl = tm.createTaskList(tasks, false);
                        tl.onComplete = function() {
                            callback(errors.length ? errors[0] : null, controls);
                        };
                        tl.run();
                    } else {
                        callback(errors.length ? errors[0] : null, controls);
                    }
                });
                if (initToken) {
                    self._init_.tokens.push(initToken);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * Module to implement popup to select date & time
 *
 * @module visuals/controls/iqtrendtime/popup
 */
(function() {
    const MODULE_NAME = "visuals.controls.iqtrendtime.popup",
        /** @lends module:visuals/controls/iqtrendtime/popup */
        module = shmi.pkg(MODULE_NAME);

    const CONTROLLER_NAME = "trend-time-popup";

    /**
     * build '_controllers_' option for widget configuration
     *
     * @param {string} slot slot name
     * @return {object[]} widget '_controller_' option value
     */
    function getControllerConfig(slot) {
        return [
            {
                name: CONTROLLER_NAME,
                slot: slot
            }
        ];
    }

    /**
     * check if required widgets are ready
     *
     * @param {object} state controller state
     * @return {boolean} `true` if widgets are ready, `false` else
     */
    function widgetsReady(state) {
        return [
            "selectDate",
            "selectTime",
            "buttonOk",
            "buttonCancel"
        ].every((slotName) => !!state.getInstance(slotName));
    }

    /**
     * set controller slot values
     *
     * @param {object} state controller state
     * @param {object} timestamp slot data
     */
    function setTimestamp(state, timestamp) {
        const secondsTimestamp = Math.round(timestamp / 1000);
        state.data.ignoreEvents = true;
        state.getInstance("selectDate").setValue(secondsTimestamp);
        state.getInstance("selectTime").setValue(secondsTimestamp);
        state.data.ignoreEvents = false;
    }

    /**
     * get controller slot values
     *
     * @param {object} state controller state
     * @return {object} slot data
     */
    function getTimestamp(state) {
        return state.getInstance("selectTime").getValue() * 1000;
    }

    /**
     * handle controller slot events
     *
     * @param {object} state controller state
     * @param {string} slot slot name
     * @param {string} type event type
     * @param {object} event event reference
     */
    function handleSlotEvent(state, slot, type, event) {
        if (state.data.ignoreEvents) {
            return;
        }
        switch (slot) {
        case "popup":
            if (!state.data.resolved) {
                state.data.resolved = true;
                state.data.resolve(null);
            }
            shmi.deleteControl(state.getInstance("popup"));
            break;
        case "selectDate":
            state.data.ignoreEvents = true;
            state.getInstance("selectTime").setValue(state.getInstance("selectDate").getValue());
            state.data.ignoreEvents = false;
            break;
        case "selectTime":
            state.data.ignoreEvents = true;
            state.getInstance("selectDate").setValue(state.getInstance("selectTime").getValue());
            state.data.ignoreEvents = false;
            break;
        case "buttonOk":
            state.data.resolved = true;
            state.data.resolve(getTimestamp(state));
            state.getInstance("popup").close();
            break;
        case "buttonCancel":
            if (!state.data.resolved) {
                state.data.resolved = true;
                state.data.resolve(null);
            }
            state.getInstance("popup").close();
            break;
        default:
        }
    }

    /**
     * get controller configuration for popup logic controller
     *
     * @param {object} options popup options
     * @param {function} resolve function to resolve created promise for selection popup
     * @return {object} popup controller configuration
     */
    function getPopupController(options, resolve) {
        return {
            name: CONTROLLER_NAME,
            slots: {
                popup: {
                    ui: "popup-menu",
                    events: ["close"]
                },
                selectDate: {
                    ui: "iq-select-date",
                    events: ["change"],
                    optional: true
                },
                selectTime: {
                    ui: "iq-select-time",
                    events: ["change"],
                    optional: true
                },
                buttonOk: {
                    ui: "iq-button",
                    events: ["click"],
                    optional: true
                },
                buttonCancel: {
                    ui: "iq-button",
                    events: ["click"],
                    optional: true
                }
            },
            onChange: function(state, detail) {
                if (widgetsReady(state) && !state.data.contentSet) {
                    //all widgets ready -> set data
                    setTimestamp(state, options.timestamp);
                    state.data.contentSet = true;
                }
            },
            onEnable: function(state) {
                state.data.resolve = resolve;
            },
            onDisable: function(state) {

            },
            onEvent: handleSlotEvent,
            data: {
                ignoreEvents: false,
                resolve: null,
                resolved: false,
                contentSet: false
            }
        };
    }

    /**
     * get configured ui layout for date & time selection popup
     *
     * @param {object} options popup options
     * @param {number} options.timestamp initial timestamp
     * @param {string} options.dateLabel date select label
     * @param {string} options.timeLabel time select label
     * @param {string} options.popupClassName class-name to apply to popup
     * @param {string} options.dateLabelClassName class-name to apply to date label
     * @param {string} options.dateLabelTemplate template to use for date label
     * @param {string} options.selectDateClassName class-name to apply to select-date
     * @param {string} options.selectDateTemplate template to use for select-date
     * @param {string} options.timeLabelClassName class-name to apply to time label
     * @param {string} options.timeLabelTemplate template to use for time label
     * @param {string} options.selectTimeClassName class-name to apply to select-time
     * @param {string} options.selectTimeTemplate template to use for select-time
     * @param {string} options.cancelButtonClassName class-name to apply to cancel button
     * @param {string} options.cancelButtonTemplate template to use for cancel button
     * @param {string} options.okButtonClassName class-name to apply to ok button
     * @param {string} options.okButtonTemplate template to use for ok button
     * @param {function} resolve dialog resolve function
     * @param {function} reject dialog reject function
     * @return {object} ui layout for selection dialog
     */
    function getPopupLayout(options, resolve, reject) {
        return {
            "ui": "popup-menu",
            "config": {
                "name": "trendTimePopup",
                "class-name": options.popupClassName || "popup-menu iq-flex trend-time",
                "template": "default/popup-menu",
                "auto-close": false,
                "content-template": null,
                "tab-limit": false,
                "_controllers_": getControllerConfig("popup")
            },
            "controller": getPopupController(options, resolve),
            "children": [
                {
                    "ui": "container",
                    "config": {
                        "ui": "container",
                        "class-name": "iq-container",
                        "type": "iqflex",
                        "flex-orientation": "column",
                        "flex-none": true,
                        "flex-wrap": false,
                        "flex-primary-align": "start",
                        "flex-secondary-align": "stretch",
                        "flex-line-align": "start",
                        "name": "content",
                        "template": null
                    },
                    "children": [
                        {
                            "ui": "iq-label",
                            "config": {
                                "class-name": options.dateLabelClassName || "iq-label iq-variant-01 date",
                                "template": options.dateLabelTemplate || "default/iq-label.iq-variant-01",
                                "name": "dateLabel",
                                "ui": "iq-label",
                                "item": null,
                                "type": null,
                                "auto-type": true,
                                "decimal-delimiter": ".",
                                "precision": 2,
                                "auto-precision": true,
                                "options": [],
                                "pattern": null,
                                "tooltip": null,
                                "value-as-tooltip": false,
                                "text": options.dateLabel || "Select Date"
                            },
                            "children": null
                        },
                        {
                            "ui": "iq-select-date",
                            "config": {
                                "class-name": options.selectDateClassName || "iq-select-date iq-variant-01 always-open",
                                "template": options.selectDateTemplate || "default/iq-select-date.iq-variant-01",
                                "name": "selectDate",
                                "ui": "iq-select-date",
                                "label": "",
                                "item": null,
                                "auto-label": false,
                                "dateformat": "${V_DATEFORMAT}",
                                "show-icon": false,
                                "icon-src": null,
                                "icon-class": null,
                                "icon-title": null,
                                "_controllers_": getControllerConfig("selectDate")
                            },
                            "children": null
                        },
                        {
                            "ui": "iq-label",
                            "config": {
                                "class-name": options.timeLabelClassName || "iq-label iq-variant-01 time",
                                "template": options.timeLabelTemplate || "default/iq-label.iq-variant-01",
                                "name": "timeLabel",
                                "ui": "iq-label",
                                "item": null,
                                "type": null,
                                "auto-type": true,
                                "decimal-delimiter": ".",
                                "precision": 2,
                                "auto-precision": true,
                                "options": [],
                                "pattern": null,
                                "tooltip": null,
                                "value-as-tooltip": false,
                                "text": options.timeLabel || "Select Time"
                            },
                            "children": null
                        },
                        {
                            "ui": "iq-select-time",
                            "config": {
                                "class-name": options.selectTimeClassName || "iq-select-time iq-variant-01 iq-icon-variant-01",
                                "template": options.selectTimeTemplate || "default/iq-select-time.iq-variant-01",
                                "name": "selectTime",
                                "ui": "iq-select-time",
                                "label": "",
                                "item": null,
                                "auto-label": false,
                                "numpad-enabled": false,
                                "isUTC": false,
                                "_controllers_": getControllerConfig("selectTime")
                            },
                            "children": null
                        },
                        {
                            "ui": "container",
                            "config": {
                                "class-name": "iq-container button-container",
                                "type": "iqflex",
                                "flex-orientation": "row",
                                "flex-none": true,
                                "flex-wrap": false,
                                "flex-primary-align": "space-between",
                                "flex-secondary-align": "stretch",
                                "flex-line-align": "start",
                                "name": "buttonContainer",
                                "template": null
                            },
                            "children": [
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": options.cancelButtonClassName || "iq-button iq-variant-01",
                                        "template": options.cancelButtonTemplate || "default/iq-button.iq-variant-01",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "cancelButton",
                                        "ui": "iq-button",
                                        "label": options.cancelLabel || "Cancel",
                                        "item": null,
                                        "auto-label": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("buttonCancel")
                                    },
                                    "children": null
                                },
                                {
                                    "ui": "iq-button",
                                    "config": {
                                        "class-name": options.okButtonClassName || "iq-button iq-variant-01",
                                        "template": options.okButtonTemplate || "default/iq-button.iq-variant-01",
                                        "action": null,
                                        "show-text": true,
                                        "disable-item-lock": false,
                                        "monoflop": false,
                                        "monoflop-value": 1,
                                        "monoflop-interval": 100,
                                        "name": "okButton",
                                        "ui": "iq-button",
                                        "label": options.okLabel || "OK",
                                        "item": null,
                                        "auto-label": true,
                                        "icon-src": null,
                                        "icon-title": null,
                                        "icon-class": null,
                                        "label-from-item": false,
                                        "write-bool": false,
                                        "action-release": null,
                                        "action-while-pressed": null,
                                        "interval-while-pressed": null,
                                        "action-press": null,
                                        "disable-alarms": false,
                                        "show-icon": false,
                                        "on-value": 1,
                                        "off-value": 0,
                                        "tooltip": null,
                                        "_controllers_": getControllerConfig("buttonOk")
                                    },
                                    "children": null
                                }
                            ]
                        }
                    ]
                }
            ]
        };
    }

    /**
     * create date and time selection popup
     *
     * @param {object} options selection popup config
     * @param {number} options.timestamp initial timestamp
     * @param {string} options.dateLabel date select label
     * @param {string} options.timeLabel time select label
     * @param {string} options.popupClassName class-name to apply to popup
     * @param {string} options.dateLabelClassName class-name to apply to date label
     * @param {string} options.dateLabelTemplate template to use for date label
     * @param {string} options.selectDateClassName class-name to apply to select-date
     * @param {string} options.selectDateTemplate template to use for select-date
     * @param {string} options.timeLabelClassName class-name to apply to time label
     * @param {string} options.timeLabelTemplate template to use for time label
     * @param {string} options.selectTimeClassName class-name to apply to select-time
     * @param {string} options.selectTimeTemplate template to use for select-time
     * @param {string} options.cancelButtonClassName class-name to apply to cancel button
     * @param {string} options.cancelButtonTemplate template to use for cancel button
     * @param {string} options.okButtonClassName class-name to apply to ok button
     * @param {string} options.okButtonTemplate template to use for ok button
     * @return {Promise<object|null>} promise resolving to configured timestamp or `null` if closed without selection
     */
    module.getPopup = (parent, options) => new Promise((resolve, reject) => {
        if (!parent) {
            reject(new Error("Parent widget not specified."));
        } else {
            const popup = getPopupLayout(options, resolve, reject);
            if (popup) {
                parent.addControl(popup, (err, controls) => {
                    if (err) {
                        reject(err);
                    } else {
                        controls[0].open();
                    }
                });
            }
        }
    });
}());
/**
 * iq-trend-zoom
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-trend-zoom",
 *     "name": null,
 *     "template": "custom/controls/iq-trend-zoom"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "iq-trend-zoom", // control name in camel-case
        uiType = "iq-trend-zoom", // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        "class-name": "iq-trend-zoom",
        "name": null,
        "template": "default/iq-trend-zoom",
        "trend-widget": null,
        "orientation": "horizontal"
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    /**
     * initialize binding to iq-trend instance
     *
     * @param {object} self control instance
     * @param {object} control iq-trend instance reference
     */
    function onTrendEnable(self, control) {
        self.vars.trendWidget = control;
        if (!self.isLocked()) {
            shmi.removeClass(self.element, "locked");
        }
    }

    /**
     * remove binding to iq-trend instance
     *
     * @param {object} self control instance
     */
    function onTrendDisable(self) {
        self.vars.trendWidget = null;
        if (self.isActive() && !self.isLocked()) {
            shmi.addClass(self.element, "locked");
        }
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    /**
     * attach listeners for iq-trend instance events
     *
     * @param {object} self control instance
     */
    function attachTrendListeners(self) {
        function onTrendEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on enable
                onTrendEnable(self, evt.source);
            }
        }

        function onTrendDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["trend-widget"], evt.source)) {
                //on disable
                onTrendDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onTrendEnableHandler, { "source.uiType": "iq-trend" }));
        self.vars.tokens.push(shmi.listen("disable", onTrendDisableHandler, { "source.uiType": "iq-trend" }));
    }

    // Definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            trendWidget: null,
            elements: {
                inButton: null,
                outButton: null
            },
            tokens: [],
            listeners: []
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: [],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this,
                    { MouseListener, TouchListener } = shmi.requires("visuals.io");

                self.vars.elements.inButton = shmi.getUiElement("in-button", self.element);
                self.vars.elements.outButton = shmi.getUiElement("out-button", self.element);

                if (self.config.orientation === "vertical") {
                    shmi.addClass(self.element, "vertical");
                }

                if (self.vars.elements.inButton && self.vars.elements.outButton) {
                    const inHandler = {
                        onClick: () => {
                            if (self.vars.trendWidget) {
                                if (self.config.axis === "y-axis") {
                                    self.vars.trendWidget.zoomInY();
                                } else if (self.config.axis === "x-axis") {
                                    self.vars.trendWidget.zoomInX();
                                }
                            }
                        }
                    };
                    self.vars.listeners.push(
                        new MouseListener(self.vars.elements.inButton, inHandler),
                        new TouchListener(self.vars.elements.inButton, inHandler)
                    );

                    const outHandler = {
                        onClick: () => {
                            if (self.vars.trendWidget) {
                                if (self.config.axis === "y-axis") {
                                    self.vars.trendWidget.zoomOutY();
                                } else if (self.config.axis === "x-axis") {
                                    self.vars.trendWidget.zoomOutX();
                                }
                            }
                        }
                    };
                    self.vars.listeners.push(
                        new MouseListener(self.vars.elements.outButton, outHandler),
                        new TouchListener(self.vars.elements.outButton, outHandler)
                    );
                }
            },

            /* Called when control is enabled */
            onEnable: function() {
                const { elements } = this.vars;
                if (!(elements.inButton && elements.outButton)) {
                    console.error(`[${uiType}] template is missing required elements.`);
                    return;
                }
                if (this.config["trend-widget"] !== null) {
                    const trendWidget = shmi.ctrl(this.config["trend-widget"]);
                    if (trendWidget && trendWidget.isActive()) {
                        onTrendEnable(this, trendWidget);
                    }

                    attachTrendListeners(this);
                }
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            },

            /* Called when control is disabled */
            onDisable: function() {
                onTrendDisable(this);
                this.vars.tokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.tokens = [];
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
            },

            /* Called when control is deleted */
            onDelete: function() {

            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                shmi.addClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.disable();
                });
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                shmi.removeClass(this.element, 'locked');
                this.vars.listeners.forEach((l) => {
                    l.enable();
                });
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * iq-trend
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqTrend", // control name in camel-case
        uiType = "iq-trend", // control keyword (data-ui)
        isContainer = true;

    /* default graph value style */
    const defaultValueStyle = {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(0, 0, 0)",
                fillStyle: "rgba(0, 0, 0, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(0, 0, 0)",
                lineDash: [5, 5]
            }
        },
        /* default inactive graph value style (not highlighted) */
        defaultInactiveStyle = {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(163, 163, 163)",
                fillStyle: "rgba(163, 163, 163, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(163, 163, 163)",
                lineDash: [5, 5]
            }
        };

    const storageIdentifier = "webiq-widget-settings";

    /* default list of graph value styles */
    const valueStyles = [
        {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(141, 77, 163)",
                fillStyle: "rgba(141, 77, 163, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(141, 77, 163)",
                lineDash: [5, 5]
            }
        },
        {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(227, 67, 147)",
                fillStyle: "rgba(227, 67, 147, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(227, 67, 147)",
                lineDash: [5, 5]
            }
        },
        {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(211, 229, 116)",
                fillStyle: "rgba(211, 229, 116, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(211, 229, 116)",
                lineDash: [5, 5]
            }
        },
        {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(71, 178, 178)",
                fillStyle: "rgba(71, 178, 178, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(71, 178, 178)",
                lineDash: [5, 5]
            }
        },
        {
            value: {
                lineWidth: 1,
                strokeStyle: "rgb(186, 74, 130)",
                fillStyle: "rgba(186, 74, 130, 0.1)",
                lineDash: []
            },
            tolerance: {
                lineWidth: 1,
                strokeStyle: "rgb(186, 74, 130)",
                lineDash: [5, 5]
            }
        }
    ];

    // default configuration
    const defConfig = {
        "class-name": "iq-trend",
        "name": null,
        "template": "default/iq-trend",
        "tooltip": null,
        "datasource": {
            "type": "trend",
            "name": "Trend"
        },
        "valueStyles": valueStyles,
        "defaultValueStyle": defaultValueStyle,
        "inactiveStyle": defaultInactiveStyle,
        "selectedValues": [],
        "displayDuration": 60000,
        "availableDurations": null,
        "availableZoomStages": null,
        "timelineMarkerFormats": null,
        "timelineStartMarkerFormat": null,
        "handleDateTimeFormat": {
            "date": "$DD.$MM.$YYYY",
            "time": "$HH:$mm:$ss",
            "time_sub_second": "$HH:$mm:$ss .${SSS}"
        },
        "handle": {
            "enabled": true,
            "secondary": true
        },
        "showComments": true,
        "startLive": false,
        "rememberSelectedValues": false,
        "noValueText": "<no value>",
        "commentTimestampFormat": "$DD.$MM.$YYYY, $HH:$mm:$ss",
        "commentButtonConfig": {
            "class-name": "iq-button iq-variant-01",
            "template": "default/iq-button.iq-variant-01",
            "action": null,
            "show-text": true,
            "disable-item-lock": false,
            "monoflop": false,
            "monoflop-value": 1,
            "monoflop-interval": 100,
            "name": "iq-button",
            "ui": "iq-button",
            "label": "Comment",
            "item": null,
            "auto-label": true,
            "icon-src": null,
            "icon-title": null,
            "icon-class": null,
            "label-from-item": false,
            "write-bool": false,
            "action-release": null,
            "action-while-pressed": null,
            "interval-while-pressed": null,
            "action-press": null,
            "disable-alarms": false,
            "show-icon": false,
            "on-value": 1,
            "off-value": 0,
            "tooltip": null
        }
    };

    /* default zoom stages (y-axis) */
    const zoomStages = [
        /* [0] */ 100,
        /* [1] */ 110,
        /* [2] */ 125,
        /* [3] */ 150,
        /* [4] */ 175,
        /* [5] */ 200,
        /* [6] */ 250,
        /* [7] */ 300,
        /* [8] */ 400,
        /* [9] */ 500,
        /* [10] */ 600,
        /* [11] */ 700,
        /* [12] */ 800,
        /* [13] */ 900,
        /* [14] */ 1000
    ];

    /* definition of time units */
    const time = {
        SECOND: 1000,
        MINUTE: 60000,
        HOUR: 3600000,
        DAY: 86400000,
        WEEK: 604800000,
        MONTH: 2592000000,
        YEAR: 31536000000
    };

    /* default display durations (x-axis) */
    const displayDurations = [
        time.SECOND,
        2 * time.SECOND,
        3 * time.SECOND,
        4 * time.SECOND,
        5 * time.SECOND,
        10 * time.SECOND,
        30 * time.SECOND,
        time.MINUTE,
        2 * time.MINUTE,
        3 * time.MINUTE,
        5 * time.MINUTE,
        10 * time.MINUTE,
        15 * time.MINUTE,
        30 * time.MINUTE,
        time.HOUR,
        2 * time.HOUR,
        4* time.HOUR,
        8 * time.HOUR,
        12 * time.HOUR,
        time.DAY,
        2 * time.DAY,
        time.WEEK,
        2 * time.WEEK,
        time.MONTH,
        3 * time.MONTH,
        6 * time.MONTH,
        time.YEAR
    ];

    /* pixel/value density */
    const pixelsPerValue = 2;

    /**
     * extract values from datasource data
     *
     * @param {object} data datasource data
     * @param {string} itemName item name
     * @return {any[][]} array of timestamp, value, property tuples
     */
    function getSourceDataValues(data, itemName) {
        const values = [],
            itemData = data.values[itemName];

        Object.keys(itemData).forEach((property) => {
            values.push(...itemData[property].map(([timestamp, value]) => (property === "item" ? [timestamp, value, null] : [timestamp, value, property])));
        });

        return values;
    }

    /**
     * update highlighting state of datasets
     *
     * @param {object} self instance reference
     */
    function updateHighlightState(self) {
        const { selectedValues, datasets } = self.vars,
            styles = self.getValueStyles(),
            highlightOption = selectedValues.find((selected) => selected.options && selected.options.highlight);

        datasets.forEach((ds, idx) => {
            if (!highlightOption || highlightOption.name === ds.name) {
                ds.setOptions({
                    style: styles[selectedValues[idx].options.style] ? styles[selectedValues[idx].options.style] : self.config.defaultValueStyle
                });
            } else {
                ds.setOptions({
                    style: self.config.inactiveStyle
                });
            }
            ds.update();
        });
    }

    /**
     * add value to graph display
     *
     * @param {object} self instance reference
     * @param {string} itemName item name
     * @param {object} [options={
     *         drawTolerance: false,
     *         externalDraw: true,
     *         fillCurve: true,
     *         style: 0,
     *         scaleGroup: null,
     *         limits: null
     *     }] value options
     * @param {boolean} [options.drawTolerance] set to `true` to display tolerances
     * @param {boolean} [options.externalDraw] set to `true` to manually trigger graph updates
     * @param {boolean} [options.fillCurve] set to `true` to fill displayed graph curve
     * @param {number} [options.style] configured style index
     * @param {number|null} [options.scaleGroup] scale group ID or `null` if not bound to scale group
     * @param {object|null} [options.limits] limit options
     * @return {object} graph value handling reference
     */
    function addGraphValue(self, itemName, options = {
        drawTolerance: false,
        externalDraw: true,
        fillCurve: true,
        style: 0,
        scaleGroup: null,
        limits: null
    }) {
        options = shmi.cloneObject(options); //create copy to not modify original input parameter
        const { DataSet } = shmi.requires("visuals.controls.iqtrend.dataset");
        const styleConfig = self.config.valueStyles[options.style] || self.config.defaultValueStyle;
        options.scaleGroup = typeof options.scaleGroup === "undefined" ? null : options.scaleGroup;
        options.scale = options.scale || {
            location: options.scale && options.scale.location && ["left", "right"].includes(options.scale.location) ? options.scale.location : self.vars.elements.scalesLeft.childElementCount > self.vars.elements.scalesRight.childElementCount ? "right": "left"
        };
        options.style = styleConfig;
        options.name = itemName;
        const dataset = new DataSet(self.vars.elements, options, self);

        const xDims = self.vars.xAxis.getDimensions();
        const tsStart = xDims.min;
        const tsEnd = xDims.max;
        const duration = (tsEnd - tsStart);
        const resolution = Math.ceil((duration / dataset.getDimensions().width) * pixelsPerValue);

        dataset.setOptions({
            x: {
                min: tsStart,
                max: tsEnd
            },
            y: {
                min: 0,
                max: 1
            },
            interval: resolution
        });

        updateZoomStage(self, self.vars.focusY, dataset);
        let limitsOption = null;
        if (options.limits) {
            limitsOption = shmi.cloneObject(options.limits);
            limitsOption.name = options.name;
        }

        let toleranceOption = null;
        if (typeof options.drawTolerance === "boolean") {
            toleranceOption = {
                name: options.name,
                drawTolerance: options.drawTolerance
            };
        }

        self.vars.datasource.source.setOptions({
            tStart: tsStart,
            duration,
            interval: resolution,
            limits: limitsOption ? limitsOption : undefined,
            tolerance: toleranceOption ? toleranceOption : undefined,
            loadComments: self.config.showComments
        }, true);
        if (self.vars.markerLayer) {
            self.vars.markerLayer.setOptions({
                x: {
                    min: tsStart,
                    max: tsStart + duration
                }
            });
        }
        let setUpdateTimeout = 0;
        const setToken = dataset.watch(({ width, dimensions }) => {
            clearTimeout(setUpdateTimeout);
            setUpdateTimeout = setTimeout(() => {
                if (self.vars.handleInfo.primary.active) {
                    if (self.vars.handleInfo.primary.graphWidth !== width) {
                        const oldWidth = self.vars.handleInfo.primary.graphWidth,
                            scaleFactor = (oldWidth + 1) / (width + 1);
                        self.vars.handleInfo.primary.graphWidth = width;
                        self.vars.handleInfo.primary.offset /= scaleFactor;
                        self.vars.elements.handle.primary.base.style.left = `${self.vars.handleInfo.primary.offset}px`;
                        setHandlePosition(self, true, self.vars.handleInfo.primary.offset, false);
                    }
                }
                if (self.vars.handleInfo.secondary.active) {
                    if (self.vars.handleInfo.secondary.graphWidth !== width) {
                        const oldWidth = self.vars.handleInfo.secondary.graphWidth,
                            scaleFactor = (oldWidth + 1) / (width + 1);
                        self.vars.handleInfo.secondary.graphWidth = width;
                        self.vars.handleInfo.secondary.offset /= scaleFactor;
                        self.vars.elements.handle.secondary.base.style.left = `${self.vars.handleInfo.secondary.offset}px`;
                        setHandlePosition(self, false, self.vars.handleInfo.secondary.offset, false);
                    }
                }
                self.vars.datasource.source.setOptions({
                    interval: Math.ceil(((dimensions.x.max - dimensions.x.min) / width) * pixelsPerValue)
                });

                if (self.vars.markerLayer) {
                    self.vars.markerLayer.updateSize();
                }
            }, 100);
        });

        return {
            setToken,
            dataset
        };
    }

    /**
     * update zoomstage for dataset
     *
     * @param {object} self instance reference
     * @param {number} focusY focus y-coordinate
     * @param {object} ds dataset reference
     */
    function updateDatasetZoomStage(self, focusY, ds) {
        if (!self.vars.datasource.source.getValueSettings()[ds.name]) {
            return;
        }
        if (ds.scaleGroup && ds.scaleGroup.master.dataset !== ds) {
            return;
        }
        const { min, max } = ds.scaleGroup ? ds.scaleGroup.getValueLimits() : self.vars.datasource.source.getValueSettings()[ds.name];
        if (self.vars.zoomStage === 0) {
            if (ds.scaleGroup) {
                ds.scaleGroup.setOptions({
                    y: {
                        min,
                        max
                    }
                }, null);
            } else {
                ds.setOptions({
                    y: {
                        min,
                        max
                    }
                });
            }
            return;
        }
        const range = max - min;
        const newRange = range / (self.vars.zoomStages[self.vars.zoomStage] / 100);
        const focusValue = ds.getValueAtY(focusY),
            currentDim = ds.getDimensions().y,
            currentRange = currentDim.max - currentDim.min,
            valueScale = (focusValue - currentDim.min) / currentRange;
        let newMin = focusValue - (valueScale * newRange),
            newMax = newMin + newRange;

        if (newMin < min) {
            const diff = min - newMin;
            newMin = newMin + diff;
            newMax = newMin + newRange;
        } else if (newMax > max) {
            const diff = newMax - max;
            newMax = newMax - diff;
            newMin = newMax - newRange;
        }

        if (ds.scaleGroup) {
            ds.scaleGroup.setOptions({
                y: {
                    min: newMin,
                    max: newMax
                }
            }, null);
        } else {
            ds.setOptions({
                y: {
                    min: newMin,
                    max: newMax
                }
            });
        }
    }

    /**
     * update zoomstage for all or specified dataset
     *
     * @param {object} self instance reference
     * @param {number} focusY focus y-coordinate
     * @param {object|null} [dataset=null] dataset reference, if `null` all datasets are updated
     */
    function updateZoomStage(self, focusY, dataset = null) {
        if (dataset !== null) {
            updateDatasetZoomStage(self, focusY, dataset);
        } else {
            self.vars.datasets.forEach((ds) => {
                updateDatasetZoomStage(self, focusY, ds);
            });
        }
    }

    /**
     * get dataset limits
     *
     * @param {object} self instance reference
     * @param {object} dataset dataset reference
     * @return {object} dataset limits
     */
    function getMinMax(self, dataset) {
        if (dataset.scaleGroup) {
            const minValues = dataset.scaleGroup.getValueNames().map((name) => self.vars.datasource.source.getValueSettings()[name].min);
            const maxValues = dataset.scaleGroup.getValueNames().map((name) => self.vars.datasource.source.getValueSettings()[name].max);
            return {
                min: Math.min(...minValues),
                max: Math.max(...maxValues)
            };
        } else {
            const valueSettings = self.vars.datasource.source.getValueSettings()[dataset.name];
            return {
                min: valueSettings.min,
                max: valueSettings.max
            };
        }
    }

    /**
     * update drag position
     *
     * @param {object} self instance reference
     * @param {number} dx change of x-coordinate
     * @param {number} dy change of y-coordinate
     */
    function updateDrag(self, dx, dy) {
        if (dy !== 0) {
            self.vars.datasets.forEach((ds) => {
                let setScaleGroup = false;
                if (ds.scaleGroup && ds.scaleGroup.master && ds.scaleGroup.master.dataset !== ds) {
                    return;
                } else if (ds.scaleGroup && ds.scaleGroup.master) {
                    setScaleGroup = true;
                }
                const { min, max } = getMinMax(self, ds);
                const { scaleY } = ds.graph;
                const currentDim = ds.getDimensions().y,
                    range = currentDim.max - currentDim.min;
                const diff = dy / scaleY;
                if (dy > 0 && currentDim.max !== max) { //drag up
                    let newMax = currentDim.max + diff;
                    if (newMax > max) {
                        newMax = max;
                    }
                    if (setScaleGroup) {
                        ds.scaleGroup.setOptions({
                            y: {
                                min: newMax - range,
                                max: newMax
                            }
                        }, null);
                    } else {
                        ds.setOptions({
                            y: {
                                min: newMax - range,
                                max: newMax
                            }
                        });
                    }
                } else if (dy < 0 && currentDim.min !== min) {
                    let newMin = currentDim.min + diff;
                    if (newMin < min) {
                        newMin = min;
                    }
                    if (setScaleGroup) {
                        ds.scaleGroup.setOptions({
                            y: {
                                min: newMin,
                                max: newMin + range
                            }
                        }, null);
                    } else {
                        ds.setOptions({
                            y: {
                                min: newMin,
                                max: newMin + range
                            }
                        });
                    }
                }
            });
        }

        if (dx !== 0) {
            if (self.vars.live) {
                self.setLive(false);
            }
            const min = 0,
                max = Date.now(),
                { xAxis } = self.vars,
                scaleX = xAxis.getScale(),
                currentDim = xAxis.getDimensions(),
                range = currentDim.max - currentDim.min,
                diff = dx / scaleX;
            let newMax = currentDim.max,
                newMin = currentDim.min;

            if (dx < 0 && currentDim.max !== max) {
                newMax = currentDim.max - diff;
                if (newMax > max) {
                    newMax = max;
                }
                newMin = newMax - range;
            } else if (dx > 0 && currentDim.min !== min) {
                newMin = currentDim.min - diff;
                if (newMin < min) {
                    newMin = min;
                }
                newMax = newMin + range;
            }

            if ((newMin !== currentDim.min) || (newMax !== currentDim.max)) {
                xAxis.setOptions({
                    x: {
                        min: newMin,
                        max: newMax
                    }
                });
                self.vars.datasets.forEach((ds) => {
                    ds.setOptions({
                        x: {
                            min: newMin,
                            max: newMax
                        }
                    },
                    true);
                });
                if (self.vars.markerLayer) {
                    self.vars.markerLayer.setOptions({
                        x: {
                            min: newMin,
                            max: newMax
                        }
                    });
                }
                self.vars.datasource.source.setOptions({
                    tStart: newMin
                });
            }
        }
    }

    /**
     * update duration displayed on graph
     *
     * @param {object} self instance reference
     * @param {number} displayDuration displayed duration in ms
     * @param {number} focusX focus x-coordinate
     */
    function updateDisplayDuration(self, displayDuration, focusX) {
        const ds0 = self.vars.datasets[0],
            max = Date.now(),
            min = 0,
            focusTime = Math.round(ds0.getTimeAt(focusX)),
            currentDim = ds0.getDimensions().x,
            currentRange = currentDim.max - currentDim.min,
            valueScale = (focusTime - currentDim.min) / currentRange;
        let newMin = focusTime - (valueScale * displayDuration),
            newMax = newMin + displayDuration;

        if (newMin < min) {
            let diff = min - newMin;
            newMin = newMin + diff;
            newMax = newMin + displayDuration;
        } else if (newMax > max) {
            let diff = newMax - max;
            newMax = newMax - diff;
            newMin = newMax - displayDuration;
        }
        self.vars.xAxis.setOptions({
            x: {
                min: newMin,
                max: newMax
            }
        });
        self.vars.datasets.forEach((g) => {
            g.setOptions({
                x: {
                    min: newMin,
                    max: newMax
                }
            },
            true);
        });
        if (self.vars.markerLayer) {
            self.vars.markerLayer.setOptions({
                x: {
                    min: newMin,
                    max: newMax
                }
            });
        }
        self.vars.xAxis.draw();
        self.vars.datasets.forEach((ds) => {
            ds.update();
        });
        const dimensions = ds0.getDimensions();
        self.vars.datasource.source.setOptions({
            tStart: newMin,
            duration: newMax - newMin,
            interval: Math.ceil(((dimensions.x.max - dimensions.x.min) / dimensions.width) * pixelsPerValue)
        });
    }

    /**
     * update time displayed on trend handle
     *
     * @param {object} self instance reference
     * @param {boolean} primary `true` for primary handle, `false` for secondary
     */
    function updateHandleTime(self, primary) {
        const { formatDateTime } = shmi.requires("visuals.tools.date"),
            handleRef = primary ? self.vars.elements.handle.primary : self.vars.elements.handle.secondary,
            handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary;

        if (self.vars.datasets.length) {
            const timestamp = self.vars.datasets[0].getTimeAt(handleInfo.offset);
            handleInfo.timestamp = timestamp;
        }

        const timeFormat = self.vars.datasets.length && self.vars.datasets[0].graph.interval < 1000 ? shmi.localize(self.config.handleDateTimeFormat.time_sub_second) : shmi.localize(self.config.handleDateTimeFormat.time);
        handleRef.date.textContent = formatDateTime(handleInfo.timestamp / 1000, shmi.localize(self.config.handleDateTimeFormat.date));
        handleRef.time.textContent = formatDateTime(handleInfo.timestamp / 1000, timeFormat);
    }

    /**
     * check if unit is a unit-class
     *
     * @param {string|number} unitText configured unit
     * @return {boolean} `true` if unit is unit-class, `false` else
     */
    function isUnitClass(unitText) {
        return String(parseInt(unitText)) === String(unitText);
    }

    /**
     * update values displayed on trend-handle
     *
     * @param {object} self instance reference
     * @param {boolean} primary `true` for primary handle, `false` for secondary
     */
    function updateHandleValues(self, primary) {
        const { formatNumber } = shmi.requires("visuals.tools.numericValues"),
            valueSettings = self.vars.datasource.source.getValueSettings(),
            handleRef = primary ? self.vars.elements.handle.primary : self.vars.elements.handle.secondary,
            handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary,
            valueInfo = getHandleValues(self, handleInfo.offset),
            uc = shmi.requires("visuals.tools.unitClasses");

        Object.keys(valueInfo).forEach((name) => {
            const dataset = self.vars.datasets.find((ds) => ds.name === name);
            if (!handleInfo.values[name]) {
                const ref = {
                    base: self.vars.handleValuesTemplate.cloneNode(true),
                    label: null,
                    value: null,
                    unit: null
                };

                ref.label = shmi.getUiElement("label", ref.base);
                ref.value = shmi.getUiElement("value", ref.base);
                ref.unit = shmi.getUiElement("unit", ref.base);
                handleInfo.values[name] = {
                    ref,
                    info: valueInfo[name],
                    strokeStyle: null
                };
                ref.label.textContent = ref.label.title = valueSettings[name] && valueSettings[name].label ? shmi.localize(valueSettings[name].label) : name;
                handleRef.values.appendChild(ref.base);

                if (dataset) {
                    handleInfo.values[name].strokeStyle = dataset.graph.style.value.strokeStyle;
                    ref.base.style.setProperty("--line-color", dataset.graph.style.value.strokeStyle);
                }
            } else if (dataset && handleInfo.values[name].strokeStyle !== dataset.graph.style.value.strokeStyle) {
                handleInfo.values[name].strokeStyle = dataset.graph.style.value.strokeStyle;
                handleInfo.values[name].ref.base.style.setProperty("--line-color", dataset.graph.style.value.strokeStyle);
            }

            let unitText = "";
            if (valueSettings[name]) {
                if (isUnitClass(valueSettings[name].unit)) {
                    const ucAdapter = uc.getSelectedAdapter(valueSettings[name].unit);
                    if (ucAdapter) {
                        unitText = ucAdapter.unitText;
                    }
                } else {
                    unitText = valueSettings[name].unit;
                }
            }

            if (valueInfo[name].value === null) {
                handleInfo.values[name].ref.value.textContent = self.config.noValueText ? shmi.localize(self.config.noValueText) : "<no value>";
            } else if (typeof valueInfo[name].value === "number" && valueSettings[name]) {
                handleInfo.values[name].ref.value.textContent = formatNumber(valueInfo[name].value, {
                    "unit": unitText,
                    "precision": valueSettings[name].digits,
                    "show-unit": false
                });
            } else {
                handleInfo.values[name].ref.value.textContent = valueInfo[name].value;
            }

            handleInfo.values[name].ref.unit.textContent = unitText === null ? "" : shmi.localize(unitText);
        });

        Object.keys(handleInfo.values).forEach((name) => {
            if (!valueInfo[name]) {
                handleInfo.values[name].ref.base.parentNode.removeChild(handleInfo.values[name].ref.base);
                delete handleInfo.values[name];
            }
        });
    }

    function checkHandleFocus(self, primary, activeSet) {
        const handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary,
            handleRef = primary ? self.vars.elements.handle.primary : self.vars.elements.handle.secondary,
            otherHandle = primary ? self.vars.handleInfo.secondary : self.vars.handleInfo.primary,
            otherRef = primary ? self.vars.elements.handle.secondary : self.vars.elements.handle.primary;

        if (activeSet) {
            handleInfo.recent = true;
            otherHandle.recent = false;
        }

        if (handleInfo.recent && otherHandle.active) {
            const parent = handleRef.base.parentNode,
                handleRect = handleRef.base.getBoundingClientRect(),
                otherRect = otherRef.base.getBoundingClientRect();
            if (!(otherRect.left >= handleRect.right ||
                otherRect.right <= handleRect.left ||
                otherRect.top >= handleRect.bottom ||
                otherRect.bottom <= handleRect.top)) {
                parent.insertBefore(otherRef.base, handleRef.base);
                handleRef.base.classList.add("focused");
                otherRef.base.classList.remove("focused");
            } else {
                handleRef.base.classList.remove("focused");
                otherRef.base.classList.remove("focused");
            }
        } else if (!otherHandle.active) {
            handleRef.base.classList.remove("focused");
            otherRef.base.classList.remove("focused");
        }
    }

    /**
     * set position of trend-handle
     *
     * @param {object} self instance reference
     * @param {boolean} primary `true` for primary handle, `false` for secondary
     * @param {number} pos x-coordinate of handle
     */
    function setHandlePosition(self, primary, pos, activeSet = true) {
        if (!self.vars.datasets.length) {
            return;
        }
        const handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary,
            handleRef = primary ? self.vars.elements.handle.primary : self.vars.elements.handle.secondary,
            dims = self.vars.datasets[0].getDimensions(),
            { width } = dims,
            baseWidth = handleRef.base.offsetWidth;

        let offset = pos;
        if (offset < 1) {
            offset = 1;
        } else if (offset > width + 1) {
            offset = width + 1;
        }

        let reversed = false;
        if (offset > width + 1 - baseWidth) {
            handleRef.base.classList.add("reversed");
            reversed = true;
        } else {
            handleRef.base.classList.remove("reversed");
        }

        let styleOffset = reversed ? offset - baseWidth : offset;

        handleInfo.offset = offset;
        handleInfo.graphWidth = dims.width;
        handleRef.base.style.left = `${styleOffset}px`;
        updateHandleTime(self, primary);
        updateHandleValues(self, primary);

        if (!handleInfo.active) {
            handleInfo.active = true;
            handleRef.base.classList.add("active");
        }

        checkHandleFocus(self, primary, activeSet);

        clearTimeout(self.vars.handleUpdateTimeout);
        self.vars.handleUpdateTimeout = setTimeout(() => {
            if (self.isActive()) {
                self.fire("handle-info", self.getHandleInfo());
            }
        }, 100);
    }

    /**
     * get values to display at trend-handle
     *
     * @param {object} self instance reference
     * @param {number} posX x-coordinate of values
     * @return {object} display values
     */
    function getHandleValues(self, posX) {
        const values = {};

        self.vars.datasets.forEach((ds) => {
            const timestamp = ds.getTimeAt(posX),
                valueInfo = ds.getValue(timestamp);
            values[ds.name] = valueInfo;
        });

        return values;
    }

    /**
     * create input handler for graphs area
     *
     * @param {object} self instance reference
     * @returns {object} input handler
     */
    function getInputHandler(self) {
        let isScaling = false,
            scaleXFactor = 1,
            scaleYFactor = 1;
        return {
            onPress: (x, y, event) => {
                if (event.touches && event.touches.length > 1) {
                    isScaling = true;
                }
            },
            onRelease: (x, y, event) => {
                if (isScaling && event.touches && event.touches.length === 0) {
                    shmi.decouple(() => {
                        isScaling = false;
                        if (scaleXFactor === 1 && scaleYFactor === 1) {
                            self.setLive(!self.vars.live);
                        }
                        scaleXFactor = 1;
                        scaleYFactor = 1;
                    });
                }
            },
            onClick: (x, y, event) => {
                if (isScaling) {
                    return;
                }

                if (event.shiftKey) {
                    self.setLive(!self.vars.live);
                }

                const relPos = getRelativePosition(self.vars.elements.graphs, x, y);
                if (!event.shiftKey && self.config.handle.enabled) {
                    const handleInfo = self.vars.handleInfo;
                    if (!handleInfo.primary.active) {
                        setHandlePosition(self, true, relPos.x);
                    } else if (self.config.handle.secondary && !handleInfo.secondary.active) {
                        setHandlePosition(self, false, relPos.x);
                    } else if (!self.config.handle.secondary) {
                        setHandlePosition(self, true, relPos.x);
                    } else { //both active
                        const distPrimary = Math.abs(relPos.x - handleInfo.primary.offset),
                            distSecondary = Math.abs(relPos.x - handleInfo.secondary.offset);
                        if (distPrimary < distSecondary) {
                            setHandlePosition(self, true, relPos.x);
                        } else {
                            setHandlePosition(self, false, relPos.x);
                        }
                    }
                }
            },
            onDrag: (dx, dy, event) => {
                event.preventDefault();
                event.stopPropagation();
                if (dx === 0 && self.vars.zoomStage === 0) {
                    return;
                }
                updateDrag(self, dx, dy);
            },
            onWheel: (delta, event) => {
                event.preventDefault();
                event.stopPropagation();
                const relPos = getRelativePosition(self.vars.elements.graphs, event.clientX, event.clientY);
                if (!event.shiftKey) {
                    //Y axis zoom
                    if (delta > 0 && self.vars.zoomStage < self.vars.zoomStages.length - 1) { //zoom in (-> smaller value range)
                        self.vars.zoomStage = self.vars.zoomStage + 1;
                        self.vars.focusY = relPos.y;
                        updateZoomStage(self, relPos.y);
                    } else if (delta < 0 && self.vars.zoomStage > 0) { //zoom out (-> bigger value range)
                        self.vars.zoomStage = self.vars.zoomStage - 1;
                        self.vars.focusY = relPos.y;
                        updateZoomStage(self, relPos.y);
                    }
                } else {
                    //X axis zoom
                    const displayDuration = self.vars.displayDurationUpdate === null ? self.vars.displayDuration : self.vars.displayDurationUpdate;
                    let newDuration = displayDuration;

                    if (delta < 0 && displayDuration < self.vars.displayDurations[self.vars.displayDurations.length - 1]) { //zoom out (-> bigger duration)
                        newDuration = self.vars.displayDurations.find((duration) => duration > displayDuration);
                    } else if (delta > 0 && displayDuration > self.vars.displayDurations[0]) { //zoom in (-> smaller duration)
                        newDuration = self.vars.displayDurations.slice().reverse().find((duration) => duration < displayDuration);
                    }
                    if (newDuration !== displayDuration) {
                        if (self.vars.live) {
                            self.setLive(false);
                        }
                        clearTimeout(self.vars.durationUpdateTimeout);
                        self.vars.displayDurationUpdate = newDuration;
                        self.vars.durationUpdateTimeout = setTimeout(() => {
                            self.vars.displayDurationUpdate = null;
                            self.setDisplayDuration(newDuration, relPos.x);
                        }, 100);
                    }
                }
            },
            onScale: (scale, posX, posY, scaleX, scaleY, points) => {
                if ((scaleY >= 1.25 * scaleYFactor) || (scaleY <= 0.75 * scaleYFactor)) {
                    //zoom-y
                    if (scaleY > scaleYFactor && self.vars.zoomStage < self.vars.zoomStages.length - 1) { //zoom in (-> smaller value range)
                        self.vars.zoomStage = self.vars.zoomStage + 1;
                        self.vars.focusY = posY;
                        updateZoomStage(self, posY);
                        scaleYFactor *= 1.25;
                    } else if (scaleY < scaleYFactor && self.vars.zoomStage > 0) { //zoom out (-> bigger value range)
                        self.vars.zoomStage = self.vars.zoomStage - 1;
                        self.vars.focusY = posY;
                        updateZoomStage(self, posY);
                        scaleYFactor *= 0.75;
                    }
                }

                if ((scaleX >= 1.25 * scaleXFactor) || (scaleX <= 0.75 * scaleXFactor)) {
                    //zoom-x
                    const displayDuration = self.vars.displayDurationUpdate === null ? self.vars.displayDuration : self.vars.displayDurationUpdate;
                    let newDuration = displayDuration;

                    if (scaleX < scaleXFactor && displayDuration < self.vars.displayDurations[self.vars.displayDurations.length - 1]) { //zoom out (-> bigger duration)
                        newDuration = self.vars.displayDurations.find((duration) => duration > displayDuration);
                        scaleXFactor *= 0.75;
                    } else if (scaleX > scaleXFactor && displayDuration > self.vars.displayDurations[0]) { //zoom in (-> smaller duration)
                        newDuration = self.vars.displayDurations.slice().reverse().find((duration) => duration < displayDuration);
                        scaleXFactor *= 1.25;
                    }
                    if (newDuration !== displayDuration) {
                        if (self.vars.live) {
                            self.setLive(false);
                        }
                        clearTimeout(self.vars.durationUpdateTimeout);
                        self.vars.displayDurationUpdate = newDuration;
                        self.vars.durationUpdateTimeout = setTimeout(() => {
                            self.vars.displayDurationUpdate = null;
                            self.setDisplayDuration(newDuration, posX);
                        }, 100);
                    }
                }
            }
        };
    }

    /**
     * get position relative to specified element
     *
     * @param {HTMLElement} element base element
     * @param {number} clientX client x-coordinate
     * @param {number} clientY client y-coordinate
     * @return {object}  relative position
     */
    function getRelativePosition(element, clientX, clientY) {
        const bounds = element.getBoundingClientRect();
        return {
            x: clientX - bounds.left,
            y: clientY - bounds.top,
            bounds: bounds
        };
    }

    /**
     * initialize trend-handle
     *
     * @param {object} self instance reference
     * @param {boolean} primary `true` for primary handle, `false` for secondary
     */
    function initHandle(self, primary) {
        const parent = self.vars.elements.graphs,
            base = self.vars.handleTemplate.cloneNode(true);

        const ref = primary ? self.vars.elements.handle.primary : self.vars.elements.handle.secondary;
        ref.base = base;
        ref.date = shmi.getUiElement("handle-date", base);
        ref.time = shmi.getUiElement("handle-time", base);
        ref.button = shmi.getUiElement("button", base);
        ref.values = shmi.getUiElement("data-values", base);
        ref.handle = shmi.getUiElement("drag-handle", base);
        ref.comment = shmi.getUiElement("comment-anchor", base);

        if (self.config.showComments) {
            const commentButton = shmi.createControl("iq-button", ref.comment, shmi.cloneObject(self.config.commentButtonConfig), "DIV");
            self.vars.eventTokens.push(commentButton.listen("click", async (evt) => {
                const trendInfo = self.getTrendInfo();
                const threads = shmi.requires("visuals.tools.threads");
                const handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary;
                if (trendInfo) {
                    const threadId = await threads.create("trend", trendInfo.trend_id, handleInfo.timestamp, {
                        dialogTitle: "${iqtrend_create_thread}",
                        titleLabel: "${iqtrend_threads_title}",
                        messageLabel: "${iqtrend_threads_message}",
                        applyLabel: "${iqtrend_threads_create_btn}",
                        enterMessageNotification: "${iqtrend_threads_enter_message_notification}"
                    });
                    if (threadId !== null) {
                        if (self.vars.datasource.source && typeof self.vars.datasource.source.reloadComments === "function") {
                            self.vars.datasource.source.reloadComments();
                        }
                    }
                }
            }));
        }

        const { MouseListener, TouchListener } = shmi.requires("visuals.io");
        //trend handle drag handler
        const inputHandlerHandle = {
            onPress: () => { /* implementation of handler stops event bubbling */ },
            onRelease: () => { /* implementation of handler stops event bubbling */ },
            onDrag: (dx, dy, event) => {
                const handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary;
                setHandlePosition(self, primary, handleInfo.offset + dx);
            }
        };
        const mhHandle = new MouseListener(ref.handle, inputHandlerHandle, { dragThreshold: 0 });
        const thHandle = new TouchListener(ref.handle, inputHandlerHandle, false, { dragThreshold: 0 });

        //prevent events on handle from propagating to graph area
        const inputHandlerBase = {
            onPress: () => { /* implementation of handler stops event bubbling */ },
            onRelease: () => { /* implementation of handler stops event bubbling */ },
            onClick: (x, y, event) => {
                checkHandleFocus(self, primary, true);
            },
            onWheel: (delta, event) => {
                event.stopPropagation();
            }
        };
        const mhBase = new MouseListener(ref.base, inputHandlerBase);
        const thBase = new TouchListener(ref.base, inputHandlerBase);

        //handle close button input
        const inputHandlerClose = {
            onPress: () => { /* implementation of handler stops event bubbling */ },
            onRelease: () => { /* implementation of handler stops event bubbling */ },
            onClick: () => {
                if (!self.isLocked()) {
                    ref.base.classList.remove("active");
                    const handleInfo = primary ? self.vars.handleInfo.primary : self.vars.handleInfo.secondary;
                    handleInfo.active = false;
                    self.fire("handle-info", self.getHandleInfo());
                }
            }
        };
        const mhClose = new MouseListener(ref.button, inputHandlerClose);
        const thClose = new TouchListener(ref.button, inputHandlerClose);

        self.vars.inputListeners.push(mhHandle, thHandle, mhBase, thBase, mhClose, thClose);
        parent.appendChild(ref.base);
    }

    /**
     * update widget settings stored in local-storage
     *
     * @param {object} self control instance reference
     */
    function updateWidgetSettings(self) {
        if (self.config.rememberSelectedValues) {
            const handle = shmi.requires("visuals.tools.nodes").getNodeHandle(self),
                username = shmi.requires("visuals.session.UserManager").currentUser.name,
                settingsText = localStorage.getItem(storageIdentifier);
            let widgetSettings = {};

            if (settingsText) {
                try {
                    widgetSettings = JSON.parse(settingsText);
                } catch (exc) {
                    console.error(`[${uiType}] could not parse saved selection:`, exc);
                }
            }

            widgetSettings[username] = widgetSettings[username] || {};
            widgetSettings[username][uiType] = widgetSettings[username][uiType] || {};
            widgetSettings[username][uiType][handle] = widgetSettings[username][uiType][handle] || {};
            widgetSettings[username][uiType][handle].selectedValues = shmi.cloneObject(self.vars.selectedValues);

            localStorage.setItem(storageIdentifier, JSON.stringify(widgetSettings));
        }
    }

    /**
     * fire 'display-time' event with current x-axis dimensions
     */
    function fireDisplayTimeUpdate(self, dims) {
        if (!self.isLive()) {
            shmi.caf(self.vars.displayTimeRafId);
            self.vars.displayTimeRafId = shmi.raf(() => {
                self.updateHandles();
                self.fire("display-time", dims);
                self.vars.lastDisplayTimeUpdate = Date.now();
            });
        } else if (Date.now() - self.vars.lastDisplayTimeUpdate > 1000) {
            self.fire("display-time", dims);
            self.vars.lastDisplayTimeUpdate = Date.now();
        }
    }

    // Definition of new control extending BaseControl - START
    /** control definition @type {object} */
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,

        /* instance variables */
        vars: {
            datasets: [],
            xAxis: null,
            tokens: [],
            valueTokens: [],
            handleToken: null,
            rafId: 0,
            displayDuration: 60000,
            displayDurationUpdate: null,
            values: {},
            collectValues: false,
            inputListeners: [],
            zoomStage: 0,
            focusY: 50,
            elements: {
                scalesLeft: null,
                scalesRight: null,
                graphs: null,
                timeline: null,
                loadingIndicator: null,
                markerLayer: null,
                handle: {
                    primary: {
                        base: null,
                        time: null,
                        date: null,
                        button: null,
                        values: null,
                        handle: null
                    },
                    secondary: {
                        base: null,
                        time: null,
                        date: null,
                        button: null,
                        values: null,
                        handle: null
                    }
                }
            },
            handleInfo: {
                primary: {
                    active: false,
                    offset: 0,
                    timestamp: null,
                    graphWidth: null,
                    values: {},
                    recent: false
                },
                secondary: {
                    active: false,
                    offset: 0,
                    timestamp: null,
                    graphWidth: null,
                    values: {},
                    recent: false
                }
            },
            handleTemplate: null,
            handleValuesTemplate: null,
            markerTemplates: {
                comment: null,
                alarm: null
            },
            markerLayer: null,
            valueSettings: {},
            live: false,
            xAxisMode: "timeline",
            datasource: {
                type: "trend",
                name: "Trend",
                source: null
            },
            selectedValues: [],
            durationUpdateTimeout: 0,
            handleUpdateTimeout: 0,
            scaleGroups: {},
            loadingToken: null,
            markerToken: null,
            eventTokens: [],
            lastValueInfo: [],
            zoomStages: zoomStages,
            displayDurations: displayDurations,
            displayTimeRafId: 0,
            lastDisplayTimeUpdate: 0,
            restoreLiveToken: null
        },

        /* imports added at runtime */
        imports: {},

        /* Array of custom event types fired by this control */
        events: ["display-duration", "handle-info", "value-info", "graph-options", "live", "display-time"],

        /* Functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const self = this;
                self.element.style.overflow = "hidden"; //prevents observer errors in case stylesheet has not been loaded / overflow style is missing
                self.vars.elements.scalesLeft = shmi.getUiElement("scales-left", self.element);
                self.vars.elements.scalesRight = shmi.getUiElement("scales-right", self.element);
                self.vars.elements.graphs = shmi.getUiElement("graphs", self.element);
                self.vars.elements.xAxis = shmi.getUiElement("x-axis", self.element);
                self.vars.elements.loadingIndicator = shmi.getUiElement("loading-indicator", self.element);

                if (self.config.rememberSelectedValues) {
                    const settingsText = localStorage.getItem(storageIdentifier);
                    if (settingsText) {
                        try {
                            const handle = shmi.requires("visuals.tools.nodes").getNodeHandle(self),
                                username = shmi.requires("visuals.session.UserManager").currentUser.name,
                                widgetSettings = JSON.parse(settingsText);

                            if (widgetSettings && widgetSettings[username] && widgetSettings[username][uiType] && widgetSettings[username][uiType][handle] && Array.isArray(widgetSettings[username][uiType][handle].selectedValues)) {
                                self.config.selectedValues = widgetSettings[username][uiType][handle].selectedValues;
                            }
                        } catch (exc) {
                            console.error(`[${uiType}] could not parse saved selection:`, exc);
                        }
                    }
                }

                self.vars.elements.markerLayer = shmi.getUiElement("marker-layer", self.element);
                if (!self.vars.elements.markerLayer) {
                    console.error(`[${uiType}] could not find required 'marker-layer' element in template.`);
                    return;
                }
                self.vars.markerTemplates.comment = shmi.getUiElement("trend-marker-comment", self.element);
                if (!self.vars.markerTemplates.comment) {
                    console.error(`[${uiType}] could not find required 'trend-marker-comment' element in template.`);
                    return;
                }
                const commentMarkerCopy = self.vars.markerTemplates.comment.cloneNode(true);
                self.vars.markerTemplates.comment.parentNode.removeChild(self.vars.markerTemplates.comment);
                self.vars.markerTemplates.comment = commentMarkerCopy;

                self.vars.markerTemplates.alarm = shmi.getUiElement("trend-marker-alarm", self.element);
                if (!self.vars.markerTemplates.alarm) {
                    console.error(`[${uiType}] could not find required 'trend-marker-alarm' element in template.`);
                    return;
                }
                const alarmMarkerCopy = self.vars.markerTemplates.alarm.cloneNode(true);
                self.vars.markerTemplates.alarm.parentNode.removeChild(self.vars.markerTemplates.alarm);
                self.vars.markerTemplates.alarm = alarmMarkerCopy;

                self.vars.handleTemplate = shmi.getUiElement("trend-handle", self.element);
                if (!self.vars.handleTemplate) {
                    console.error(`[${uiType}] could not find required 'trend-handle' element in template.`);
                    return;
                }
                const handleTemplateCopy = self.vars.handleTemplate.cloneNode(true);
                self.vars.handleTemplate.parentNode.removeChild(self.vars.handleTemplate);
                self.vars.handleTemplate = handleTemplateCopy;

                self.vars.handleValuesTemplate = shmi.getUiElement("data-box", self.vars.handleTemplate);
                if (!self.vars.handleValuesTemplate) {
                    console.error(`[${uiType}] could not find required 'data-box' element in template.`);
                    return;
                }
                const valuesTemplateCopy = self.vars.handleValuesTemplate.cloneNode(true);
                self.vars.handleValuesTemplate.parentNode.removeChild(self.vars.handleValuesTemplate);
                self.vars.handleValuesTemplate = valuesTemplateCopy;

                if (Array.isArray(self.config.availableDurations)) {
                    self.vars.displayDurations = self.config.availableDurations;
                    if (!self.vars.displayDurations.length) {
                        console.warn(`[IqTrend] no display durations configured, adding 60000ms`);
                        self.vars.displayDurations.push(60000);
                    }
                    self.vars.displayDurations.sort((a, b) => a - b);
                }

                if (Array.isArray(self.config.availableZoomStages)) {
                    self.vars.zoomStages = self.config.availableZoomStages.filter((zoomStage) => {
                        if (zoomStage < 100) {
                            console.warn(`[IqTrend] configured zoom-stage is below 100 and will not be available: ${zoomStage}`);
                            return false;
                        }
                        return true;
                    });
                    if (!self.vars.zoomStages.includes(100)) {
                        console.warn(`[IqTrend] configured zoom-stages do not include 100% - original zoom-stage added`);
                        self.vars.zoomStages.push(100);
                    }
                    self.vars.zoomStages.sort((a, b) => a - b);
                }

                initHandle(self, true);
                initHandle(self, false);

                const { MouseListener, TouchListener } = shmi.requires("visuals.io");
                self.vars.displayDuration = self.config.displayDuration;
                const inputHandler = getInputHandler(this);
                this.vars.inputListeners.push(
                    new MouseListener(self.vars.elements.graphs, inputHandler, { dragThreshold: 10 })
                );

                this.vars.inputListeners.push(
                    new TouchListener(self.vars.elements.graphs, inputHandler, false, { dragThreshold: 10 })
                );

                //prevent context menu from opening when pausing / unpausing live trend
                self.vars.elements.graphs.oncontextmenu = (e) => {
                    e.preventDefault();
                };

                if (self.vars.xAxisMode === "timeline") {
                    const { Timeline } = shmi.requires("visuals.controls.iqtrend.timeline");
                    const now = Date.now();
                    this.vars.xAxis = new Timeline(self.vars.elements.xAxis, {
                        min: now - self.vars.displayDuration,
                        max: now,
                        numSteps: 21,
                        mirrored: false,
                        markerFormats: self.config.timelineMarkerFormats,
                        startMarkerFormat: self.config.timelineStartMarkerFormat
                    }, fireDisplayTimeUpdate.bind(null, self));
                } else {
                    const { XAxis } = shmi.requires("visuals.controls.iqtrend.xaxis");
                    this.vars.xAxis = new XAxis(self.vars.elements.xAxis, {
                        min: 0,
                        max: 100,
                        numSteps: 21,
                        mirrored: false
                    }, fireDisplayTimeUpdate.bind(null, self));
                }

                if (self.config.showComments) {
                    const { MarkerLayer } = shmi.requires("visuals.controls.iqtrend.markerlayer");
                    this.vars.markerLayer = new MarkerLayer(this.vars.elements.markerLayer, this);
                }

                if (this.config.datasource && typeof this.config.datasource.type === "string" && typeof this.config.datasource.name === "string") {
                    this.vars.datasource.type = this.config.datasource.type;
                    this.vars.datasource.name = this.config.datasource.name;
                }

                if (this.vars.datasource.type === "trend") {
                    const { DataSourceTrend } = shmi.requires("visuals.controls.iqtrend.datasourceTrend");
                    this.vars.datasource.source = new DataSourceTrend(this.vars.datasource.name);
                    this.vars.loadingToken = this.vars.datasource.source.watch(({
                        loading
                    }) => {
                        if (typeof loading === "boolean") {
                            if (loading) {
                                shmi.addClass(self.vars.elements.loadingIndicator, "loading");
                            } else {
                                shmi.removeClass(self.vars.elements.loadingIndicator, "loading");
                            }
                        }
                    });
                }
            },
            /**
             * add graph value
             *
             * @param {string} itemName name of value
             * @param {object} options value options
            */
            addGraphValue: function(itemName, options) {
                const self = this;
                const valueConfig = shmi.cloneObject(options);

                self.vars.selectedValues.push({
                    name: itemName,
                    options: valueConfig
                });
                self.vars.valueTokens.push(addGraphValue(self, itemName, valueConfig));

                updateHighlightState(self);
                self.vars.datasource.source.select(itemName);
                updateWidgetSettings(self);
            },
            /**
             * remove graph value
             *
             * @param {string|number} itemNameOrIndex name of value or index to remove
             */
            removeGraph: function(itemNameOrIndex) {
                const self = this;
                let idx = -1,
                    name = null;

                if (typeof itemNameOrIndex === "number") {
                    if (self.vars.selectedValues[itemNameOrIndex]) {
                        idx = itemNameOrIndex;
                    }
                } else if (typeof itemNameOrIndex === "string") {
                    idx = self.vars.selectedValues.findIndex((valueConfig) => valueConfig.name === itemNameOrIndex);
                }

                if (idx >= 0) {
                    if (self.vars.selectedValues.length === 1 && self.isLive()) {
                        self.setLive(false);
                    }
                    name = self.vars.selectedValues[idx].name;
                    if (self.vars.selectedValues[idx].options && self.vars.selectedValues[idx].options.highlight) {
                        self.setGraphOptions(name, { highlight: false });
                    }
                    self.vars.selectedValues.splice(idx, 1);
                    self.vars.datasource.source.deselect(name);
                    self.vars.valueTokens[idx].setToken.unwatch();
                    self.vars.valueTokens.splice(idx, 1);

                    self.vars.datasets[idx].remove();
                    self.vars.datasets.splice(idx, 1);

                    updateWidgetSettings(self);
                    self.updateHandles();
                }
            },
            /**
             * set graph options at runtime
             *
             * @param {string} name name of value
             * @param {object} options graph options
             * @param {number} [options.style] value style index
             * @param {boolean} [options.drawTolerance] enable or disable display of tolerances (if available)
             * @param {boolean} [options.fillCurve] fill area below curve if enabled
             * @param {boolean} [options.highlight] highlight curve if enabled
             * @param {object} [options.limits] scale limit settings
             * @param {string} [options.limits.mode] either `"config"`, `"auto"` or `"manual"`
             * @param {number} [options.limits.step] scale limit stepping when mode is `"auto"`
             * @param {number} [options.limits.min] minimum scale value when mode is `"manual"`
             * @param {number} [options.limits.max] maximum scale value when mode is `"manual"`
             * @param {number|null} [options.scaleGroup] scale group id, `null` to remove
             * @param {object} [options.scale] scale appearance settings
             * @param {string} [options.scale.location] scale location, either `"left"` or `"right"`
             * @param {boolean} [options.scale.hidden] hide scale if enabled
             */
            setGraphOptions(name, options) {
                const self = this,
                    dataset = self.vars.datasets.find((ds) => ds.name === name),
                    selectedValue = self.vars.selectedValues.find((value) => value.name === name),
                    datasetOptions = {},
                    datasourceOptions = {};
                let updateDataset = false,
                    updateHandles = false,
                    updateDatasource = false;

                if (!(dataset && selectedValue)) {
                    console.warn(`[IqTrend] cannot set options for unknown graph: ${name}`);
                    return;
                }
                if (typeof options.style === "number") {
                    const styles = self.getValueStyles();
                    datasetOptions.style = styles[options.style] ? styles[options.style] : self.config.defaultValueStyle;
                    selectedValue.options.style = options.style;
                    updateDataset = true;
                    updateHandles = true;
                }

                if (typeof options.drawTolerance === "boolean") {
                    datasetOptions.drawTolerance = options.drawTolerance;
                    selectedValue.options.drawTolerance = options.drawTolerance;
                    datasourceOptions.tolerance = {
                        name,
                        drawTolerance: options.drawTolerance
                    };
                    updateDataset = true;
                    updateDatasource = true;
                }

                if (typeof options.fillCurve === "boolean") {
                    datasetOptions.fillCurve = options.fillCurve;
                    selectedValue.options.fillCurve = options.fillCurve;
                    updateDataset = true;
                }

                if (options.limits && typeof options.limits.mode === "string") {
                    selectedValue.options.limits = selectedValue.options.limits || {};
                    selectedValue.options.limits = options.limits;
                    const limitsOpt = shmi.cloneObject(options.limits);
                    limitsOpt.name = name;
                    datasourceOptions.limits = limitsOpt;
                    updateDatasource = true;
                }

                if (options.scaleGroup === null || typeof options.scaleGroup === "number") {
                    if (dataset.groupId !== options.scaleGroup) {
                        selectedValue.options.scaleGroup = options.scaleGroup;
                        if (dataset.scaleGroup && typeof dataset.groupId === "number") {
                            dataset.scaleGroup.removeValue(name);
                        }
                        dataset.groupId = options.scaleGroup;

                        if (options.scaleGroup === null) {
                            if (dataset.scaleGroup && dataset.scaleGroup.getValueNames().length === 0) {
                                delete self.vars.scaleGroups[dataset.scaleGroup.groupId];
                            }
                            dataset.scaleGroup = null;
                        } else if (!self.vars.scaleGroups[options.scaleGroup]) {
                            const { ScaleGroup } = shmi.requires("visuals.controls.iqtrend.scalegroup");
                            self.vars.scaleGroups[options.scaleGroup] = dataset.scaleGroup = new ScaleGroup(options.scaleGroup, this, dataset);
                        } else {
                            dataset.scaleGroup = self.vars.scaleGroups[options.scaleGroup];
                            dataset.scaleGroup.addValue(name, dataset);
                        }
                    }
                }

                if (options.scale) {
                    if (typeof options.scale.location === "string") {
                        selectedValue.options.scale = selectedValue.options.scale || {};
                        selectedValue.options.scale.location = options.scale.location;
                        dataset.scale.setOptions({
                            location: options.scale.location
                        });

                        if (dataset.scaleGroup && dataset.scaleGroup.master.dataset !== dataset) {
                            dataset.scaleGroup.master.dataset.scale.setOptions({
                                location: options.scale.location
                            });
                        }
                    }

                    if (typeof options.scale.hidden === "boolean") {
                        selectedValue.options.scale = selectedValue.options.scale || {};
                        selectedValue.options.scale.hidden = options.scale.hidden;
                        if (dataset.scaleGroup && dataset.scaleGroup.master.dataset !== dataset) {
                            dataset.scaleGroup.master.dataset.scale.setOptions({
                                hidden: options.scale.hidden
                            });
                        } else if (options.scale.hidden !== dataset.scale.hidden) {
                            dataset.scale.setOptions({
                                hidden: options.scale.hidden
                            });
                        }
                    }
                }

                if (typeof options.highlight === "boolean") {
                    self.vars.datasets.forEach((ds, idx) => {
                        if (options.highlight === false || ds.name !== name) {
                            self.vars.selectedValues[idx].options.highlight = false;
                        } else {
                            self.vars.selectedValues[idx].options.highlight = true;
                        }
                    });
                    updateHighlightState(self);
                    updateHandles = true;
                }

                if (updateDataset) {
                    dataset.setOptions(datasetOptions);
                }

                if (updateDatasource) {
                    self.vars.datasource.source.setOptions(datasourceOptions);
                }

                if (updateHandles) {
                    self.updateHandles();
                }

                self.fire("graph-options", shmi.cloneObject(selectedValue));
                updateWidgetSettings(self);
            },
            /**
             * get currently selected values
             *
             * @returns {TrendValueConfig[]} selected value info
             */
            getSelectedValues: function() {
                const self = this,
                    selected = shmi.cloneObject(self.vars.selectedValues);

                selected.forEach((selection) => {
                    const dataset = self.vars.datasets.find((ds) => ds.name === selection.name);
                    if (dataset && dataset.scaleGroup) {
                        selection.options.scale = selection.options.scale || {};
                        selection.options.scale.location = dataset.scaleGroup.master.dataset.scale.location;
                        selection.options.scale.hidden = dataset.scaleGroup.master.dataset.scale.hidden;
                    }
                });

                return selected;
            },
            /**
             * set selected values
             *
             * @param {TrendValueConfig[]} selectedValues selected value info
             */
            setSelectedValues: function(selectedValues) {
                if (!this.isActive()) {
                    console.error("Cannot set selected values while widget is inactive.");
                    return;
                }
                if (!Array.isArray(selectedValues) || !selectedValues.every((value) => typeof value.name === "string" && value.options && typeof value.options === "object")) {
                    console.error("Parameter contains invalid value definitions:", selectedValues);
                    throw new Error("Parameter contains invalid value definitions.");
                }
                const self = this,
                    selectedNames = self.vars.selectedValues.map((selected) => selected.name),
                    wasLive = self.isLive() || (selectedNames.length === 0 && self.config.startLive);

                if (self.vars.restoreLiveToken) {
                    self.vars.restoreLiveToken.unwatch();
                    self.vars.restoreLiveToken = null;
                }

                self.vars.selectedValues = shmi.cloneObject(selectedValues);
                self.vars.datasource.source.deselect(...selectedNames);
                self.vars.valueTokens.forEach((t) => {
                    t.setToken.unwatch();
                });
                self.vars.valueTokens = [];

                self.vars.datasets.forEach((d) => {
                    d.remove();
                });
                self.vars.datasets = [];

                self.vars.selectedValues.forEach((selected) => {
                    self.vars.valueTokens.push(addGraphValue(self, selected.name, selected.options));
                });
                self.vars.datasource.source.select(...self.vars.selectedValues.map((selected) => selected.name));

                if (wasLive) {
                    if (!self.vars.datasource.source._busy) {
                        self.setLive(true);
                    } else {
                        self.vars.restoreLiveToken = self.vars.datasource.source.watch(() => {
                            if (!self.vars.datasource.source._busy) {
                                self.vars.restoreLiveToken.unwatch();
                                self.vars.restoreLiveToken = null;
                                self.setLive(true);
                            }
                        });
                    }
                }

                updateHighlightState(self);
                updateWidgetSettings(self);
            },
            /**
             * set displayed duration
             *
             * @param {number} displayDuration displayed duration
             * @param {number} [focusX] optional focus coordinate for duration change
             */
            setDisplayDuration: function(displayDuration, focusX = 0) {
                if (this.vars.live) {
                    this.setLive(false);
                }
                this.vars.displayDuration = displayDuration;
                this.fire("display-duration", { duration: this.vars.displayDuration });
                updateDisplayDuration(this, displayDuration, focusX);
            },
            /**
             * get currently displayed duration
             * @returns {number} displayed duration in ms
             */
            getDisplayDuration: function() {
                return this.vars.displayDuration;
            },
            /**
             * get configured display durations
             *
             * @returns {number[]} configured display durations
             */
            getDisplayDurations: function() {
                return shmi.cloneObject(this.vars.displayDurations);
            },
            /* Called when control is enabled */
            onEnable: function() {
                const self = this;

                self.vars.live = (self.config.startLive === true);

                if (Array.isArray(self.config.selectedValues)) {
                    self.vars.selectedValues = shmi.cloneObject(self.config.selectedValues);
                }

                self.vars.selectedValues.forEach((selected) => {
                    self.vars.valueTokens.push(addGraphValue(self, selected.name, selected.options));
                });
                updateHighlightState(self);

                self.vars.handleToken = self.vars.datasource.source.watch(({
                    info,
                    data
                }) => {
                    if (info && data) {
                        if (info.values) {
                            self.vars.datasets.forEach((dataset) => {
                                const itemName = dataset.name;
                                if (info.values[itemName]) {
                                    dataset.setOptions({
                                        interval: info.values[itemName].interval,
                                        unit: info.values[itemName].unit,
                                        discrete: info.values[itemName].data_type === "boolean" ? true : info.values[itemName].discrete
                                    });
                                    if (info.values[itemName].data_type === "string" && !dataset.scale.hidden) {
                                        dataset.scale.setOptions({ hidden: true });
                                    }
                                    updateZoomStage(self, self.vars.focusY, dataset);
                                }
                                if (data.values[itemName]) {
                                    dataset.clearValues();
                                    dataset.addValues(getSourceDataValues(data, itemName));
                                    dataset.update();
                                }
                            });
                            self.updateHandles();

                            const values = shmi.cloneObject(info.values),
                                valueNames = [];
                            Object.keys(values).forEach((name) => {
                                if (typeof values[name].interval === "number" && typeof values[name].data_type === "string") {
                                    valueNames.push(name);
                                } else {
                                    delete values[name];
                                }
                            });
                            valueNames.sort();
                            if (valueNames.length !== self.vars.lastValueInfo.length || !self.vars.lastValueInfo.every((valueName, idx) => valueName === valueNames[idx])) {
                                self.fire("value-info", shmi.cloneObject(values));
                                self.vars.lastValueInfo = valueNames;
                            }
                        }
                    }
                });

                const selectedNames = self.vars.selectedValues.map((selected) => selected.name);
                self.vars.datasource.source.select(...selectedNames);

                self.vars.inputListeners.forEach((l) => {
                    l.enable();
                });

                if (self.config.showComments) {
                    self.vars.markerToken = self.vars.datasource.source.watch(({
                        comments,
                        alarms
                    }) => {
                        if (comments) {
                            self.vars.markerLayer.clearMarkers("comment");
                            self.vars.markerLayer.addMarkers(comments);
                        }

                        if (alarms) {
                            self.vars.markerLayer.clearMarkers("alarm");
                            self.vars.markerLayer.addMarkers(alarms);
                        }
                    });
                }
                self.controls.forEach((c) => {
                    c.enable();
                });

                self.setLive(self.vars.live);
            },
            /**
             * check if live mode is active
             *
             * @returns {boolean} `true` if live mode is active, `false` else
            */
            isLive: function() {
                return this.vars.live;
            },
            /**
             * set state of live mode
             * @param {boolean} isLive `true` to activate live mode, `false` to deativate
             */
            setLive: function(isLive) {
                const self = this,
                    { source } = self.vars.datasource;
                if (!source.hasLive) {
                    console.log(`[${uiType}] live mode not available with current datatsource.`);
                    return;
                }

                if (isLive === source.isLive) {
                    return;
                }
                self.vars.live = source.setLive(isLive);
                self.fire("live", {
                    isLive: self.vars.live
                });

                function updateGraph() {
                    if (self.isActive() && self.vars.live) {
                        const newMax = Date.now(),
                            newMin = newMax - self.vars.displayDuration;

                        self.vars.xAxis.setOptions({
                            x: {
                                min: newMin,
                                max: newMax
                            }
                        });
                        self.vars.datasets.forEach((g) => {
                            g.setOptions({
                                x: {
                                    min: newMin,
                                    max: newMax
                                }
                            },
                            true);
                        });
                        if (self.vars.markerLayer) {
                            self.vars.markerLayer.setOptions({
                                x: {
                                    min: newMin,
                                    max: newMax
                                }
                            });
                        }
                        self.vars.datasource.source.setOptions({
                            tStart: newMin
                        }, true);

                        self.updateHandles();

                        self.vars.rafId = shmi.raf(updateGraph);
                    }
                }

                if (self.vars.live) {
                    updateGraph();
                } else {
                    shmi.caf(self.vars.rafId);
                    self.vars.rafId = 0;
                }
            },

            /* Called when control is disabled */
            onDisable: function() {
                const self = this;

                shmi.caf(self.vars.rafId);
                self.vars.rafId = 0;

                if (self.vars.live) {
                    self.setLive(false);
                }

                if (self.vars.handleToken) {
                    self.vars.handleToken.unwatch();
                    self.vars.handleToken = null;
                }
                const selectedNames = self.vars.selectedValues.map((selected) => selected.name);
                self.vars.datasource.source.deselect(...selectedNames);
                self.vars.valueTokens.forEach((t) => {
                    t.setToken.unwatch();
                });
                self.vars.valueTokens = [];

                self.vars.datasets.forEach((d) => {
                    d.remove();
                });
                self.vars.datasets = [];

                self.vars.inputListeners.forEach((l) => {
                    l.disable();
                });

                self.controls.forEach((c) => {
                    c.disable();
                });
                self.vars.lastValueInfo = [];
            },

            /* Called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;
                self.vars.inputListeners.forEach((l) => {
                    l.disable();
                });

                self.controls.forEach((c) => {
                    c.lock();
                });
            },

            /* Called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;
                self.vars.inputListeners.forEach((l) => {
                    l.enable();
                });

                self.controls.forEach((c) => {
                    c.unlock();
                });
            },
            /* called when control is deleted - instance cleanup */
            onDelete: function() {
                if (this.vars.xAxis) {
                    this.vars.xAxis.remove();
                    this.vars.xAxis = null;
                }

                if (this.vars.loadingToken) {
                    this.vars.loadingToken.unwatch();
                    this.vars.loadingToken = null;
                }

                this.vars.eventTokens.forEach((t) => {
                    t.unlisten();
                });
                this.vars.eventTokens = [];
            },
            /**
             * set display options
             * @param {object} options display options
             */
            setOptions: function(options) {
                const self = this,
                    { xAxis } = self.vars;

                if (typeof options.tStart === "number") {
                    if (self.vars.live) {
                        self.setLive(false);
                    }
                    xAxis.setOptions({
                        x: {
                            min: options.tStart,
                            max: options.tStart + self.vars.displayDuration
                        }
                    });
                    self.vars.datasets.forEach((ds) => {
                        ds.setOptions({
                            x: {
                                min: options.tStart,
                                max: options.tStart + self.vars.displayDuration
                            }
                        },
                        true);
                    });
                    if (self.vars.markerLayer) {
                        self.vars.markerLayer.setOptions({
                            x: {
                                min: options.tStart,
                                max: options.tStart + self.vars.displayDuration
                            }
                        });
                    }
                    self.vars.datasource.source.setOptions({
                        tStart: options.tStart
                    });
                }
            },
            /**
             * update trend handles
             */
            updateHandles() {
                if (this.vars.handleInfo.primary.active) {
                    updateHandleTime(this, true);
                    updateHandleValues(this, true);
                }
                if (this.vars.handleInfo.secondary.active) {
                    updateHandleTime(this, false);
                    updateHandleValues(this, false);
                }
            },
            /**
             * get trend handle information
             * @returns {object} handle info
             */
            getHandleInfo() {
                const handleInfo = shmi.cloneObject(this.vars.handleInfo);
                if (handleInfo.primary.active) {
                    delete handleInfo.primary.active;
                    delete handleInfo.primary.graphWidth;
                    delete handleInfo.primary.offset;
                    Object.keys(handleInfo.primary.values).forEach((valueName) => {
                        handleInfo.primary.values[valueName] = handleInfo.primary.values[valueName].info;
                    });
                } else {
                    handleInfo.primary = null;
                }

                if (handleInfo.secondary.active) {
                    delete handleInfo.secondary.active;
                    delete handleInfo.secondary.graphWidth;
                    delete handleInfo.secondary.offset;
                    Object.keys(handleInfo.secondary.values).forEach((valueName) => {
                        handleInfo.secondary.values[valueName] = handleInfo.secondary.values[valueName].info;
                    });
                } else {
                    handleInfo.secondary = null;
                }

                return handleInfo;
            },
            /**
             * get information on displayed values
             * @returns {object} value info
             */
            getValueInfo() {
                if (this.vars.datasource.source) {
                    return this.vars.datasource.source.getValueSettings();
                }
                return null;
            },
            /**
             * get information on currently configured trend / datasource
             * @returns {object} trend info
             */
            getTrendInfo() {
                if (this.vars.datasource.source) {
                    const trendInfo = this.vars.datasource.source.getTrendSettings();
                    trendInfo.type = this.vars.datasource.type;
                    trendInfo.name = this.vars.datasource.name;
                    return trendInfo;
                }
                return null;
            },
            /**
             * get inactive graph style
             * @returns {object} inactive graph style
             */
            getInactiveStyle() {
                return shmi.cloneObject(this.config.inactiveStyle ? this.config.inactiveStyle : defaultInactiveStyle);
            },
            /**
             * get default graph style
             * @returns {object} default graph style
             */
            getDefaultStyle() {
                return shmi.cloneObject(this.config.defaultValueStyle ? this.config.defaultValueStyle : defaultValueStyle);
            },
            /**
             * get configured value styles
             *
             * @returns {object[]} value styles
             */
            getValueStyles() {
                return shmi.cloneObject(this.config.valueStyles);
            },
            setHandlePosition(primary = true, pos, activeSet = true) {
                setHandlePosition(this, primary, pos, activeSet);
            },
            /**
             * Zoom in Y-axis
             */
            zoomInY() {
                const self = this;

                if (self.vars.zoomStage < self.vars.zoomStages.length - 1) {
                    const rect = self.vars.elements.graphs.getBoundingClientRect(),
                        posY = Math.floor(rect.height / 2);

                    self.vars.zoomStage = self.vars.zoomStage + 1;
                    self.vars.focusY = posY;
                    updateZoomStage(self, posY);
                }
            },
            /**
             * Zoom out Y-axis
             */
            zoomOutY() {
                const self = this;

                if (self.vars.zoomStage > 0) {
                    const rect = self.vars.elements.graphs.getBoundingClientRect(),
                        posY = Math.floor(rect.height / 2);

                    self.vars.zoomStage = self.vars.zoomStage - 1;
                    self.vars.focusY = posY;
                    updateZoomStage(self, posY);
                }
            },
            /**
             * get current zoom stage
             * @returns {number} current zoom stage
            */
            getZoomStage() {
                return this.vars.zoomStages[this.vars.zoomStage];
            },
            /**
             * get configured zoom stages
             * @returns {number[]} configured zoom stages
             */
            getZoomStages() {
                return shmi.cloneObject(this.vars.zoomStages);
            },
            /**
             * Zoom in X-axis
             */
            zoomInX() {
                const self = this,
                    { displayDuration } = self.vars;
                let newDuration = displayDuration;

                if (displayDuration > self.vars.displayDurations[0]) { //zoom in (-> smaller duration)
                    const rect = self.vars.elements.graphs.getBoundingClientRect(),
                        posX = Math.floor(rect.width / 2);
                    newDuration = self.vars.displayDurations.slice().reverse().find((duration) => duration < displayDuration);
                    if (self.vars.live) {
                        self.setLive(false);
                    }
                    self.setDisplayDuration(newDuration, posX);
                }
            },
            /**
             * Zoom out X-axis
             */
            zoomOutX() {
                const self = this,
                    { displayDuration } = self.vars;
                let newDuration = displayDuration;

                if (displayDuration < self.vars.displayDurations[self.vars.displayDurations.length - 1]) { //zoom out (-> bigger duration)
                    const rect = self.vars.elements.graphs.getBoundingClientRect(),
                        posX = Math.floor(rect.width / 2);
                    newDuration = self.vars.displayDurations.find((duration) => duration > displayDuration);
                    if (self.vars.live) {
                        self.setLive(false);
                    }
                    self.setDisplayDuration(newDuration, posX);
                }
            },
            /**
             * get x-axis dimensions
             *
             * @returns {object} x-axis dimensions
             */
            getDimensionsX() {
                return this.vars.xAxis.getDimensions();
            }
        }
    };

    /**
     * @typedef {object} TrendValueConfig
     * @property {string} name name of trend value
     * @property {object} options value options
     * @property {boolean} [options.drawTolerance] enable or disable display of tolerances (if available)
     * @property {boolean} [options.fillCurve] fill area below curve if enabled
     * @property {boolean} [options.highlight] highlight curve if enabled
     * @property {object} [options.limits] scale limit settings
     * @property {string} [options.limits.mode] either `"config"`, `"auto"` or `"manual"`
     * @property {number} [options.limits.step] scale limit stepping when mode is `"auto"`
     * @property {number} [options.limits.min] minimum scale value when mode is `"manual"`
     * @property {number} [options.limits.max] maximum scale value when mode is `"manual"`
     * @property {number|null} [options.scaleGroup] scale group id, `null` to remove
     * @property {object} [options.scale] scale appearance settings
     * @property {string} [options.scale.location] scale location, either `"left"` or `"right"`
     * @property {boolean} [options.scale.hidden] hide scale if enabled
     */

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    shmi.requires("visuals.tools.control-generator").generate(definition);
})();
/**
 * Module to implement commonly used features for iq-trend
 *
 * @module visuals/controls/iqtrend/common
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.common",
        /** @lends module:visuals/controls/iqtrend/common */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    let canvas = null,
        context = null;

    /**
     * states of trend-tasks
     * @const
     */
    module.taskStates = {
        CREATED: 1,
        RUNNING: 2,
        DONE: 3,
        ERROR: 4,
        CANCELED: 5
    };

    /**
     * calculate dimensions of specified text
     *
     * @param {string} text input text
     * @param {string} fontStyle font style to use for caclulation
     * @returns {object} text dimension info
    */
    module.getTextDimensions = function(text, fontStyle = "400 12px RobotoLight") {
        if (!canvas) {
            canvas = document.createElement("canvas");
        }
        if (!context) {
            context = canvas.getContext("2d");
        }
        context.font = fontStyle;
        const dimensions = context.measureText(text);
        dimensions.fontHeight = dimensions.fontBoundingBoxAscent + dimensions.fontBoundingBoxDescent;
        dimensions.textHeight = dimensions.actualBoundingBoxAscent + dimensions.actualBoundingBoxDescent;
        return dimensions;
    };

    /**
     * get font style of specified element
     *
     * @param {HTMLElement} element element to retrieve font style from
     * @returns {string} font style
    */
    module.getFontStyle = function(element) {
        const style = getComputedStyle(element);
        return `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
    };
}());

/**
 * Module to implement dataset (graph + scale) structure for use in iq-trend
 *
 * @module visuals/controls/iqtrend/dataset
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.dataset",
        /** @lends module:visuals/controls/iqtrend/dataset */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * check if dataset if master dataset of configured scale group
     *
     * @param {object} self dataset reference
     * @return {boolean} `true` if dataset is scale group master, `false` else
     */
    function isGroupMaster(self) {
        return self.scaleGroup && self.scaleGroup.master.dataset === self;
    }

    /**
     * DataSet definition
     */
    module.DataSet = class DataSet {
        /**
         * Creates an instance of DataSet.
         *
         * @param {object} elements base elements reference
         * @param {HTMLElement} elements.graphs base element for graphs
         * @param {HTMLElement} elements.scalesLeft base element for left-side scales
         * @param {HTMLElement} elements.scalesRight base element for right-side scales
         * @param {object} options dataset options
         * @param {object} trendWidget iq-trend reference
         */
        constructor(elements, options, trendWidget) {
            const { Graph } = shmi.requires("visuals.controls.iqtrend.graph"),
                { Scale } = shmi.requires("visuals.controls.iqtrend.scale"),
                { ScaleGroup } = shmi.requires("visuals.controls.iqtrend.scalegroup");

            this.name = options.name;
            this.trendWidget = trendWidget;
            this.groupId = options.scaleGroup;
            options.dataset = this;

            const { scaleGroups } = this.trendWidget.vars;
            if (this.groupId !== null) {
                if (scaleGroups[this.groupId]) {
                    this.scaleGroup = scaleGroups[this.groupId];
                    this.scaleGroup.addValue(this.name, this);
                } else {
                    this.scaleGroup = scaleGroups[this.groupId] = new ScaleGroup(this.groupId, this.trendWidget, this);
                }
            }

            this.graph = new Graph(elements.graphs, options);

            if (options.scale && options.scale.location === "right") {
                this.scale = new Scale(elements.scalesRight, {
                    mirrored: options.scale.align !== "left",
                    align: options.scale.align ? options.scale.align : "right",
                    location: "right",
                    hidden: this.scaleGroup && !isGroupMaster(this) ? true : options.scale && typeof options.scale.hidden === "boolean" ? options.scale.hidden : false,
                    valueName: options.name
                }, this);
            } else {
                this.scale = new Scale(elements.scalesLeft, {
                    mirrored: options.scale && options.scale.align === "right",
                    align: options.scale && options.scale.align ? options.scale.align : "left",
                    location: "left",
                    hidden: this.scaleGroup && !isGroupMaster(this) ? true : options.scale && typeof options.scale.hidden === "boolean" ? options.scale.hidden : false,
                    valueName: options.name
                }, this);
            }

            this.trendWidget.vars.datasets.push(this);

            if (this.scaleGroup && !isGroupMaster(this)) {
                this.scaleGroup.master.dataset.scale.setOptions({
                    addValue: this.name
                });
            }
        }
        /**
         * retrieve graph dimentsions
         *
         * @return {object} graph dimensions
         */
        getDimensions() {
            return this.graph.getDimensions();
        }
        /**
         * set dataset options
         * @param {object} options dataset options
         */
        setOptions(options) {
            if (options.y && this.scaleGroup) {
                this.scaleGroup.setOptions({
                    y: options.y
                }, this.name);
                options = shmi.cloneObject(options);
                delete options.y;
            }
            this.graph.setOptions(options);
            this.scale.setOptions(options);
            if (options.style && this.scaleGroup && !isGroupMaster(this)) {
                this.scaleGroup.master.dataset.scale.setOptions({
                    style: options.style
                });
            }
        }
        /**
         * add value to graph
         */
        addValue() {
            this.graph.addValue.apply(this.graph, arguments);
        }
        /**
         * add array of values to graph
         */
        addValues() {
            this.graph.addValues.apply(this.graph, arguments);
        }
        /**
         * clear graph values
         */
        clearValues() {
            this.graph.clearValues();
        }
        /**
         * get graph value at specified x-coordinate
         */
        getValue() {
            return this.graph.getValue.apply(this.graph, arguments);
        }
        /**
         * get graph value at specified y-coordinate
         */
        getValueAtY() {
            return this.graph.getValueAtY.apply(this.graph, arguments);
        }
        /**
         * get timestamp at specified x-coordinate
         */
        getTimeAt() {
            return this.graph.getTimeAt.apply(this.graph, arguments);
        }
        /**
         * watch for changes of graph dimensions
         */
        watch() {
            return this.graph.watch.apply(this.graph, arguments);
        }
        /**
         * update graph & scale
         */
        update() {
            this.graph.draw();
            this.scale.draw();
        }
        /**
         * remove this dataset
         */
        remove() {
            this.graph.remove();
            this.scale.remove();
            if (this.scaleGroup) {
                this.scaleGroup.removeValue(this.name);
                if (this.scaleGroup.getValueNames().length === 0) {
                    delete this.trendWidget.vars.scaleGroups[this.scaleGroup.groupId];
                }
                this.scaleGroup = null;
            }
        }
    };
}());

/**
 * Module to implement abstract DataSource template class
 *
 * @module visuals/controls/iqtrend/datasource
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.datasource",
        /** @lends module:visuals/controls/iqtrend/datasource */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    const msgNotImplemented = "Abstract method has no implementation:";

    module.DataSource = class DataSource {
        /**
         * Creates an instance of DataSource.
         */
        constructor() {
            this._name = "AbstractDataSource";
            this._type = "abstract";
            this._hasLive = false;
            this._hasArchived = false;
            this._hasComments = false;
            this._hasAlarms = false;
            this._isLive = false;
            this._tStart = 0;
            this._tEnd = 0;
            this._interval = 0;
        }
        /**
         * read datasource name
         *
         * @readonly
         * @type {string}
         */
        get name() {
            return this._name;
        }
        /**
         * read datasource type
         *
         * @readonly
         * @type {string}
         */
        get type() {
            return this._type;
        }
        /**
         * check if datasource has live values
         *
         * @readonly
         * @type {boolean}
         */
        get hasLive() {
            return this._hasLive;
        }
        /**
         * check if datasource has archived values
         *
         * @readonly
         * @type {boolean}
         */
        get hasArchived() {
            return this._hasArchived;
        }
        /**
         * check if datasource has comments
         *
         * @readonly
         * @type {boolean}
         */
        get hasComments() {
            return this._hasComments;
        }

        /**
         * check if
         *
         * @readonly
         * @type {boolean}
         */
        get hasAlarms() {
            return this._hasAlarms;
        }
        /**
         * check if datasource is retrieving live values
         *
         * @readonly
         * @type {boolean}
         */
        get isLive() {
            return this._isLive;
        }
        /**
         * check current datasource step interval in ms
         *
         * @readonly
         * @type {number}
         */
        get interval() {
            return this._interval;
        }
        /**
         * enable disable live data if supported
         *
         * @param {boolean} live enable `true` or disable `false` live mode
         *
         */
        setLive(live) {
            if (!this._hasLive) {
                throw new Error("Data source does not support live mode");
            }
        }
        /**
         * retrieve datasource value settings
         *
         */
        getValueSettings() {
            throw new Error(`[${this.name}] ${msgNotImplemented} getValueSettings`);
        }
        /**
         * retrieve datasource trend settings
         */
        getTrendSettings() {
            throw new Error(`[${this.name}] ${msgNotImplemented} getTrendSettings`);
        }
        /**
         * set datasource options
         */
        setOptions(options, noUpdate) {
            throw new Error(`[${this.name}] ${msgNotImplemented} setOptions`);
        }
        /**
         * add values to current datasource selection
         *
         * @param {...string} values names of values to add
         */
        select(...values) {
            throw new Error(`[${this.name}] ${msgNotImplemented} select`);
        }
        /**
         * remove values from current datasource selection
         *
         * @param {...string} values names of values to remove
         */
        deselect(...values) {
            throw new Error(`[${this.name}] ${msgNotImplemented} deselect`);
        }
        /**
         * watch for datasource changes
         *
         * @param {function} callback function to call with datasource changes
         */
        watch(callback) {
            throw new Error(`[${this.name}] ${msgNotImplemented} watch`);
        }
        /**
         * reload datasource comments if supported
         *
         */
        reloadComments() {
            throw new Error(`[${this.name}] ${msgNotImplemented} reloadComments`);
        }
        /**
         * reload datasource alarms if supported
         */
        reloadAlarms() {
            throw new Error(`[${this.name}] ${msgNotImplemented} reloadAlarms`);
        }
    };
}());

/**
 * Module to implement WebIQ Server trend data-source
 *
 * @module visuals/controls/iqtrend/datasourceTrend
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.datasourceTrend",
        /** @lends module:visuals/controls/iqtrend/datasourceTrend */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    const { DataSource } = shmi.requires("visuals.controls.iqtrend.datasource"),
        { taskStates } = shmi.requires("visuals.controls.iqtrend.common");

    /**
     * get maximum value
     *
     * @param {number[][]} array of timestamp, value pairs
     * @returns {number} maximum value or `null` if none found
    */
    function getMax(values) {
        let max = null;
        if (Array.isArray(values)) {
            values.forEach(([timestamp, value]) => {
                if (typeof value === "number") {
                    if (max === null) {
                        max = value;
                    } else if (value > max) {
                        max = value;
                    }
                }
            });
        }
        return max;
    }

    /**
     * get minimum value
     *
     * @param {number[][]} array of timestamp, value pairs
     * @returns {number} minimum value or `null` if none found
    */
    function getMin(values) {
        let min = null;
        if (Array.isArray(values)) {
            values.forEach(([timestamp, value]) => {
                if (typeof value === "number") {
                    if (min === null) {
                        min = value;
                    } else if (value < min) {
                        min = value;
                    }
                }
            });
        }
        return min;
    }

    /**
     * sort values ascending by timestamp & filter values out of dataset range
     *
     * @param {object} self datasource reference
     * @param {string} itemName name of item
     * @param {string|null} property name of item property or `null` if item value
     * @param {number[][]} array of timestamp-value pairs
     */
    function sortAndFilterValues(self, itemName, property) {
        const currentValues = self.data.values[itemName];

        //sort according to timestamp
        currentValues[property].sort((a, b) => a[0] - b[0]);
        //filter old data & duplicate timestamps
        currentValues[property] = currentValues[property].filter((value, idx, valuesArray) => {
            let lastBeforeStart = true,
                firstAfterEnd = true;

            //filter values not in current range
            if (value[0] < self.data.start) {
                if (lastBeforeStart && (!Array.isArray(valuesArray[idx + 1]) || (valuesArray[idx + 1][0] >= self.data.start))) {
                    lastBeforeStart = false;
                    return true;
                }
                return false;
            } else if (value[0] > self.data.end) {
                if (firstAfterEnd) {
                    firstAfterEnd = false;
                    return true;
                }
                return false;
            }

            //filter duplicates
            if (idx === 0) {
                return true;
            }

            const isDuplicate = value[0] === valuesArray[idx - 1][0];
            if (isDuplicate) {
                log("Duplicate datapoint:", property === "item" ? itemName : `${itemName}.${property}`, value[0], `${value[1]} (${currentValues[property][idx - 1][1]})`);
            }

            return !isDuplicate;
        });

        return currentValues[property];
    }

    /**
     * sort markers ascending by timestamp & filter markers out of dataset range
     *
     * @param {object} self datasource reference
     * @param {string} markerType marker type
     * @returns {object[]} array of markers
     */
    function sortAndFilterMarkers(self, markerType) {
        let markers = self.data.markers[markerType];
        //sort according to timestamp
        markers.sort((a, b) => a.timestamp - b.timestamp);
        //filter old data
        markers = markers.filter((marker) => self.data.start <= marker.timestamp && marker.timestamp <= self.data.end);
        self.data.markers[markerType] = markers;

        return markers;
    }

    /**
     * insert loaded markers into dataset
     *
     * @param {object} self datasource reference
     * @param {object[]} markerData markers to insert
     * @param {string} markerType marker type
     */
    function insertMarkers(self, markerData, markerType) {
        const insertIds = markerData.map((marker) => marker.id);
        self.data.markers[markerType] = self.data.markers[markerType].filter((marker) => !insertIds.includes(marker.id));
        markerData.forEach((marker) => {
            self.data.markers[markerType].push(marker);
        });
        sortAndFilterMarkers(self, markerType);
    }

    /**
     * insert loaded item values into dataset
     *
     * @param {object} self datasource reference
     * @param {object} itemData item data
     */
    function insertItemValues(self, itemData) {
        initValues(self, itemData.item_alias);
        const currentValues = self.data.values[itemData.item_alias];
        Object.keys(itemData.values).forEach((key) => {
            itemData.values[key].map((value) => [value[0], processTrendData(self, itemData.item_alias, value[1])]).forEach((value) => {
                currentValues[key].push(value);
            });
            sortAndFilterValues(self, itemData.item_alias, key);
        });
    }

    /**
     * initialize item value storage
     *
     * @param {object} self datasource reference
     * @param {string} itemName item name
     */
    function initValues(self, itemName) {
        if (!self.data.values[itemName]) {
            self.data.values[itemName] = {
                item: [],
                display_min: [],
                display_max: [],
                display_trend_min: [],
                display_trend_max: [],
                prewarn_min: [],
                prewarn_max: [],
                tolerance_upper: [],
                tolerance_lower: [],
                warn_min: [],
                warn_max: [],
                stepsize: []
            };
        }
    }

    function isUnitClass(unitText) {
        if (typeof unitText === "string") {
            if (unitText.trim() === "") {
                return false;
            }
        }
        return (!isNaN(unitText));
    }

    function processTrendData(self, valueName, dataValue) {
        const uc = shmi.requires("visuals.tools.unitClasses"),
            valueSettings = self._valueSettings[valueName];

        if (dataValue !== null && valueSettings) {
            if (isUnitClass(valueSettings.unit)) {
                const unitClass = parseInt(valueSettings.unit),
                    adapter = uc.getSelectedAdapter(unitClass);

                if (adapter) {
                    dataValue = adapter.outFunction(dataValue);
                }
            }
        }
        return dataValue;
    }

    /**
     * recalculate value limits
     *
     * @param {object} self datasource reference
     * @param {string} itemName item name
     */
    function updateLimits(self, itemName) {
        initValues(self, itemName);
        const itemData = self.data.values[itemName];
        const valueSettings = self._valueSettings[itemName];
        if (!valueSettings) {
            return;
        }
        if (valueSettings.limits.mode === "manual" && (typeof valueSettings.limits.min !== "number" || (typeof valueSettings.limits.max !== "number"))) {
            valueSettings.limits.mode = "auto";
            console.info(`[iq-trend] changed limit mode from 'manual' to 'auto' due to missing limit specification.`);
        }

        if (valueSettings.limits.mode === "config") {
            const displayMin = getMin(itemData.display_trend_min.length ? itemData.display_trend_min : itemData.display_min),
                displayMax = getMax(itemData.display_trend_max.length ? itemData.display_trend_max : itemData.display_max);

            valueSettings.min = displayMin !== null ? displayMin : null;
            valueSettings.max = displayMax !== null ? displayMax : null;
        }

        if (valueSettings.limits.mode === "auto" || (valueSettings.limits.mode === "config" && (valueSettings.min === null || valueSettings.max === null))) {
            let autoMin = null,
                autoMax = null;
            if (valueSettings.drawTolerance) {
                let tolMin = getMin(itemData.tolerance_lower),
                    tolMax = getMax(itemData.tolerance_upper);

                autoMin = Math.min(...[tolMin, getMin(itemData.item)].filter((v) => v !== null));
                if (autoMin === Number.POSITIVE_INFINITY) {
                    autoMin = null;
                }
                autoMax = Math.max(...[tolMax, getMax(itemData.item)].filter((v) => v !== null));
                if (autoMax === Number.NEGATIVE_INFINITY) {
                    autoMax = null;
                }
            } else {
                autoMin = getMin(itemData.item);
                autoMax = getMax(itemData.item);
            }

            //adjust automatic limits for configured scale stepping
            if (typeof valueSettings.limits.step === "number") {
                if (autoMin !== null) {
                    autoMin = Math.floor(autoMin / valueSettings.limits.step) * valueSettings.limits.step;
                }

                if (autoMax !== null) {
                    autoMax = Math.ceil(autoMax / valueSettings.limits.step) * valueSettings.limits.step;
                }
            }
            valueSettings.min = autoMin === null ? typeof valueSettings.min === "number" ? valueSettings.min : null : autoMin;
            valueSettings.max = autoMax === null ? typeof valueSettings.max === "number" ? valueSettings.max : null : autoMax;
        } else if (valueSettings.limits.mode === "manual") { //manual mode
            valueSettings.min = valueSettings.limits.min;
            valueSettings.max = valueSettings.limits.max;
        }

        if (valueSettings.data_type === "string" || valueSettings.min === null || valueSettings.max === null) { //set limits to 0/1 for strings to not break scale functionality
            valueSettings.min = 0;
            valueSettings.max = 1;
        } else if (typeof valueSettings.min === "number" && valueSettings.min === valueSettings.max) { //adjust calculated scale for static values
            valueSettings.min -= 0.5;
            valueSettings.max += 0.5;
        }
    }

    /**
     * load recorded data
     *
     * @param {object} self datasource reference
     * @param {object} parameters load parameters
     * @param {object} parameters.self datasource reference
     * @param {number} parameters.tStart start timestamp in ms
     * @param {number} parameters.tEnd end timestamp in ms
     * @param {object} taskRef task reference
     */
    async function loadData({ self, tStart, tEnd }, taskRef) {
        const { TrendManager2: tm } = shmi.requires("visuals.core"),
            queryData = await tm.query(self._name, self.selectedValues, tStart, tEnd, self._interval);

        if (taskRef.state !== taskStates.CANCELED) {
            queryData.forEach((itemData) => {
                if (self.selectedValues.includes(itemData.item_alias)) {
                    self._valueSettings[itemData.item_alias] = self._valueSettings[itemData.item_alias] || {
                        limits: {
                            mode: "config", //"config", "auto", "manual"
                            min: null,
                            max: null
                        }
                    };

                    const valueSettings = self._valueSettings[itemData.item_alias];
                    valueSettings.interval = itemData.resolution;

                    insertItemValues(self, itemData);

                    updateLimits(self, itemData.item_alias);
                }
            });
            return Promise.resolve().then(() => {
                notifyWatchers(self);
            });
        }

        return null;
    }

    /**
     * load trend info
     *
     * @param {object} parameters info parameters
     * @param {object} parameters.self datasource reference
     * @param {object} taskRef task reference
     */
    async function loadInfo({ self }, taskRef) {
        const { TrendManager2: tm } = shmi.requires("visuals.core"),
            info = await tm.getInfo(self._name, self.selectedValues);

        if (taskRef.state !== taskStates.CANCELED) {
            self._trendSettings = self._trendSettings || {};
            self._trendSettings.start = info.record_first;
            self._trendSettings.end = info.record_last;
            self._trendSettings.trend_id = info.trend_id;
            info.items.forEach((item) => {
                self._valueSettings[item.name] = self._valueSettings[item.name] || {
                    limits: {
                        mode: "config", //"config", "auto", "manual"
                        min: null,
                        max: null
                    }
                };
                Object.keys(item).forEach((itemKey) => {
                    self._valueSettings[item.name][itemKey] = item[itemKey];
                });
            });
            self._infoSet = true;
        }
    }
    /**
     * load comment data
     *
     * @param {object} parameters thread parameters
     * @param {object} parameters.self datasource reference
     * @param {number} parameters.tStart start timestamp
     * @param {number} parameters.tEnd end timestamp
     * @param {object} taskRef task reference
     */
    async function loadThreads({ self, tStart, tEnd }, taskRef) {
        const threads = shmi.requires("visuals.tools.threads"),
            response = typeof self._trendSettings.trend_id === "undefined" ?
                { threads: [] } :
                await threads.list("trend", self._trendSettings.trend_id, { tStart, tEnd, loadUserDisplayName: true });

        if (taskRef.state !== taskStates.CANCELED) {
            insertMarkers(self, response.threads.map((thread) => ({
                timestamp: thread.reference_timestamp,
                id: thread.thread_id,
                type: "comment"
            })), "comment");

            return Promise.resolve().then(() => {
                notifyWatchers(self, {
                    comments: true
                });
            });
        }

        return null;
    }

    /**
     * update data after option change
     *
     * @param {object} self datasource reference
     * @param {object} parameters update settings
     * @param {boolean} parameters.timeChanged start, end or duration changed
     * @param {boolean} parameters.intervalChanged interval/resolution changed
     * @param {boolean} parameters.limitsChanged limit settings changed
    */
    async function updateData(self, {
        timeChanged,
        intervalChanged,
        limitsChanged
    }) {
        if (intervalChanged) {
            if (self.isLive) {
                await disableLive(self);
                await enableLive(self);
            } else {
                reloadAll(self, true);
            }
        } else if (timeChanged) {
            const tTriggerStart = Math.floor(self._tStart - (self._duration / 4)),
                tStart = Math.floor(self._tStart - (self._duration / 2));

            const now = Date.now(),
                tTriggerEnd = Math.min(now, Math.ceil(self._tEnd + (self._duration / 4))),
                tEnd = Math.min(now, Math.ceil(self._tEnd + (self._duration / 2)));

            if (tEnd < self.data.start || tStart > self.data.end) { //jump to new time
                reloadAll(self, true);
            } else if (tTriggerStart < self.data.start) { //backward drag
                loadSlice(self, tStart, self.data.start);
            } else if (tTriggerEnd > self.data.end) { //forward drag
                loadSlice(self, self.data.end, tEnd);
            }
        } else if (limitsChanged) {
            notifyWatchers(self);
        }
    }

    /**
     * reload trend data
     *
     * @param {object} self datasource reference
     * @param {boolean} noInfo if set to `true`, do not retrieve trend info
     * @param {boolean} loadLive load data in live mode (sets indefinite end timestamp)
     */
    function reloadAll(self, noInfo = false, loadLive = false) {
        const { TrendTask } = shmi.requires("visuals.controls.iqtrend.trendtask");
        //cancel all existing tasks
        self.queue.getTasks().forEach((task) => {
            if (!noInfo) {
                task.state = taskStates.CANCELED;
            } else if (task.type !== "info") {
                task.state = taskStates.CANCELED;
            }
        });

        if (!noInfo || !self._infoSet) {
            //request trend info
            const infoTask = new TrendTask("info", { self: self }, loadInfo);
            self.queue.queueTask(infoTask);
        }

        //request trend data
        const now = Date.now();
        const tStart = Math.floor(self._tStart - (self._duration / 2)),
            tEnd = Math.min(now, Math.ceil(self._tEnd + (self._duration / 2)));
        self.data.start = tStart;
        if (loadLive) {
            self.data.end = Number.MAX_SAFE_INTEGER;
        } else {
            self.data.end = tEnd;
        }

        self.data.values = {};
        const loadTask = new TrendTask("load", {
            self,
            tStart,
            tEnd
        }, loadData);
        self.queue.queueTask(loadTask);
        if (self._loadComments) {
            self.data.markers.comment = [];
            const threadsTask = new TrendTask("threads", {
                self,
                tStart,
                tEnd
            }, loadThreads);
            self.queue.queueTask(threadsTask);
        }
    }

    /**
     * load partial trend data (panning)
     *
     * @param {object} self datasource reference
     * @param {number} tStart start timestamp in ms
     * @param {number} tEnd end timestamp in ms
     */
    function loadSlice(self, tStart, tEnd) {
        const { TrendTask } = shmi.requires("visuals.controls.iqtrend.trendtask");
        if (tStart < self.data.start) {
            self.data.start = tStart;
            if (self.data.end > tStart + 3 * self._duration) {
                self.data.end = tStart + 2 * self._duration;
            }
        } else if (tEnd > self.data.end) {
            self.data.end = tEnd;
            if (self.data.start < tEnd - 3 * self._duration) {
                self.data.start = tEnd - 2 * self._duration;
            }
        }

        const loadTask = new TrendTask("load", {
            self,
            tStart,
            tEnd
        }, loadData);
        self.queue.queueTask(loadTask);
        if (self._loadComments) {
            const threadsTask = new TrendTask("threads", {
                self,
                tStart,
                tEnd
            }, loadThreads);
            self.queue.queueTask(threadsTask);
        }
    }

    /**
     * notify datasource watcher-callbacks
     *
     * @param {object} self datasource reference
     * @param {object} options notification options
     * @param {boolean} options.loadingState loading state changed
     * @param {boolean} options.comments comment data changed
     * @param {alarms} options.alarms alarm data changed
     */
    function notifyWatchers(self, options = {
        loadingState: false,
        comments: false,
        alarms: false
    }) {
        if (options.loadingState) {
            self.watchers.forEach((watcherCallback) => {
                watcherCallback({
                    source: self,
                    loading: self.queue.loading
                });
            });
        } else if (options.comments) {
            self.watchers.forEach((watcherCallback) => {
                watcherCallback({
                    source: self,
                    comments: self.data.markers.comment
                });
            });
        } else if (options.alarms) {
            self.watchers.forEach((watcherCallback) => {
                watcherCallback({
                    source: self,
                    alarms: self.data.markers.alarm
                });
            });
        } else {
            //..update watchers with current state
            self.watchers.forEach((watcherCallback) => {
                watcherCallback({
                    source: self,
                    info: {
                        range: self._trendSettings,
                        values: self._valueSettings
                    },
                    data: self.data
                });
            });
        }
    }

    /**
     * insert values from live trend subscription into dataset
     *
     * @param {object} self datasource reference
     * @param {object[]} values live data values
     */
    function insertLiveValues(self, values) {
        const currentValues = self.data.values;
        let endTs = Date.now();
        values.forEach((itemValue) => {
            if (!self.selectedValues.includes(itemValue.item)) {
                return;
            }
            if (itemValue.timestamp < endTs) {
                endTs = itemValue.timestamp;
            }
        });

        const newStartTs = endTs - 2 * self._duration;
        if (newStartTs > self.data.start) {
            self.data.start = newStartTs;
        }

        const itemNames = [];

        values.forEach((itemValue) => {
            if (!self.selectedValues.includes(itemValue.item)) {
                return;
            }
            if (!itemNames.includes(itemValue.item)) {
                itemNames.push(itemValue.item);
            }
            initValues(self, itemValue.item);
            const property = itemValue.property === null ? "item" : itemValue.property;
            currentValues[itemValue.item][property].push([itemValue.timestamp, processTrendData(self, itemValue.item, itemValue.value)]);
            sortAndFilterValues(self, itemValue.item, property);
        });

        itemNames.forEach((itemName) => {
            updateLimits(self, itemName);
        });
    }

    /**
     * enable live trend subscription
     *
     * @param {object} self datasource reference
     */
    async function enableLive(self, reloadInfo = false) {
        self._isLive = true;
        self._busy = true;
        self.setOptions({ tStart: Date.now() - self._duration }, true);

        try {
            const { TrendManager2: tm } = shmi.requires("visuals.session");
            self._subscriptionId = await tm.subscribe(self._name, self.selectedValues, self._interval, (values) => {
                //insert values
                insertLiveValues(self, values);
                notifyWatchers(self);
            });
            self._busy = false;
        } catch (err) {
            self._busy = false;
            console.error("[trend2.subscribe] Error subscribing live trend:", err);
        }

        reloadAll(self, !reloadInfo, true);
    }

    /**
     * disable live trend subscription
     *
     * @param {object} self datasource reference
     */
    async function disableLive(self) {
        const { TrendManager2: tm } = shmi.requires("visuals.session");
        //close active subscription
        self._isLive = false;
        if (self._subscriptionId === null) {
            return;
        }
        self._busy = true;
        try {
            await tm.unsubscribe(self._subscriptionId);
            self._subscriptionId = null;
            self._busy = false;
            const currentValues = self.data.values;
            let endTs = Date.now();
            Object.entries(currentValues).forEach(([name, values]) => {
                if (values.length) {
                //check last timestamp of each value and use earliest timestamp as end of dataset
                    const lastTimestamp = values[values.length - 1][0];
                    if (lastTimestamp < endTs) {
                        endTs = lastTimestamp;
                    }
                }
            });
            self.data.end = endTs;
        } catch (err) {
            self._busy = false;
            console.error("[trend2.unsubscribe] Error unsubscribing live trend:", err);
        }
    }

    /**
     * WebIQ Server trend data-source
     *
     * @constructor
     * @param {string} name data-source trend name
    */
    module.DataSourceTrend = class DataSourceTrend extends DataSource {
        constructor(name) {
            super();
            const { TaskQueue } = shmi.requires("visuals.controls.iqtrend.taskqueue");
            this._name = name;
            this._type = "trend";

            this._hasLive = true;
            this._hasArchived = true;
            this._hasComments = true;
            this._hasAlarms = false;

            this._isLive = false;
            this._tStart = 0;
            this._duration = 0;
            this._interval = 0;
            this._tEnd = this._tStart + this._duration;
            this.data = {
                start: 0,
                end: 0,
                values: {},
                markers: {
                    comment: [],
                    alarm: []
                }
            };
            this._infoSet = false;
            this._loadComments = false;
            this.selectedValues = [];
            this.watchers = [];
            this.queue = new TaskQueue();
            this.queue.watch(() => {
                notifyWatchers(this, {
                    loadingState: true
                });
            });

            this._loadQueue = [];
            this._valueSettings = {};
            this._trendSettings = {
                start: 0,
                end: 0
            };

            this._busy = false;
            this._subscriptionId = null;
        }
        /**
         * get value settings
         *
         * @returns {object} value settings
         */
        getValueSettings() {
            return shmi.cloneObject(this._valueSettings);
        }
        /**
         * get trend settings
         *
         * @returns {object} trend settings
         */
        getTrendSettings() {
            return shmi.cloneObject(this._trendSettings);
        }
        /**
         * set datasource options
         *
         * @param {object} options datasource options
         * @param {number} [options.tStart] start timestamp in ms
         * @param {number} [options.duration] display duration in ms
         * @param {number} [options.interval] minimum step between data points in ms
         * @param {object} [options.tolerance] tolerance settings
         * @param {boolean} [options.tolerance.drawTolerance] enable (`true`) or disable (`false`) display of tolerances
         * @param {string} [options.tolerance.name] name of value to apply tolerance settings to
         * @param {object} [options.limits] value limit settings
         * @param {string} [options.limits.name] name of value to apply limit settings to
         * @param {string} [options.limits.mode] limit mode, either `"config"` (use item settings), `"manual"` (use supplied 'min', 'max' values) or `"auto"` (automatically calculate limits)
         * @param {number} [options.limits.step] if 'mode' is set to `"auto"` calculate min- & max-values as multiples of `step`
         * @param {number} [options.limits.min] if 'mode' is set to `"manual"` use this as min-value
         * @param {number} [options.limits.max] if 'mode' is set to `"manual"` use this as max-value
         * @param {boolean} [options.loadComments] enable (`true`) or disable (`false`) loading of comment data
         * @param {boolean} noUpdate prevent update of loaded data (used to advance current time in live mode)
         */
        setOptions(options, noUpdate = false) {
            if (!options) {
                throw new Error("No options provided");
            }

            let timeChanged = false,
                intervalChanged = false,
                limitsChanged = false;

            if (typeof options.tStart === "number" && this._tStart !== options.tStart) {
                this._tStart = options.tStart;
                timeChanged = true;
            }

            if (typeof options.duration === "number" && this._duration !== options.duration) {
                this._duration = options.duration;
                timeChanged = true;
            }

            if (typeof options.interval === "number" && this._interval !== options.interval) {
                this._interval = Math.max(1, options.interval);
                intervalChanged = true;
            }

            if (timeChanged) {
                this._tEnd = this._tStart + this._duration;
            }

            if (options.tolerance && typeof options.tolerance.drawTolerance === "boolean" && typeof options.tolerance.name === "string") {
                this._valueSettings[options.tolerance.name] = this._valueSettings[options.tolerance.name] || {
                    limits: {
                        mode: "config", //"config", "auto", "manual"
                        min: null,
                        max: null
                    }
                };
                this._valueSettings[options.tolerance.name].drawTolerance = options.tolerance.drawTolerance;
            }

            if (options.limits && typeof options.limits === "object" && typeof options.limits.name === "string") {
                const limitsOpt = shmi.cloneObject(options.limits);
                delete limitsOpt.name;
                this._valueSettings[options.limits.name] = this._valueSettings[options.limits.name] || {};
                this._valueSettings[options.limits.name].limits = limitsOpt;
                updateLimits(this, options.limits.name);
                limitsChanged = true;
            }

            if (options.loadComments && typeof options.loadComments === "boolean") {
                this._loadComments = true;
            }

            if (!noUpdate) {
                updateData(this, {
                    timeChanged,
                    intervalChanged,
                    limitsChanged
                });
            }
        }
        /**
         * select values for trend selection
         *
         * @param {...string} values values to add to trend selection
         */
        select(...values) {
            let valueSelected = false;
            //add values to selection
            values.forEach((value) => {
                if (!this.selectedValues.includes(value)) {
                    log("Add value to selection:", value);
                    this.selectedValues.push(value);
                    valueSelected = true;
                }
            });

            if (valueSelected) {
                if (this.isLive) {
                    disableLive(this).then(() => {
                        enableLive(this, true);
                    });
                } else {
                    reloadAll(this);
                }
            }
        }
        /**
         * deselect values from trend selection
         *
         * @param {...string} values values to remove from trend selection
         */
        deselect(...values) {
            let valueDeselected = false;
            //remove values from selection
            values.forEach((value) => {
                if (this.selectedValues.includes(value)) {
                    log("Remove value from selection:", value);
                    this.selectedValues.splice(this.selectedValues.indexOf(value), 1);
                    delete this.data.values[value];
                    delete this._valueSettings[value];
                    valueDeselected = true;
                }
            });

            if (valueDeselected && this.selectedValues.length > 0) {
                if (this.isLive) {
                    disableLive(this).then(() => {
                        enableLive(this);
                    });
                } else {
                    reloadAll(this);
                }
            } else if (this.selectedValues.length === 0) {
                if (this.isLive) {
                    disableLive(this).then(() => {
                        notifyWatchers(this);
                    });
                } else {
                    notifyWatchers(this);
                }
            }
        }
        /**
         * enable or disable live mode
         *
         * @param {boolean} isLive live mode state, `true` to enable, `false` to disable
         * @return {boolean} live mode state
         */
        setLive(isLive) {
            if (isLive === this._isLive) {
                return this._isLive;
            }
            super.setLive();
            if (this._busy) {
                return this._isLive;
            }
            if (isLive && this.selectedValues.length > 0) {
                enableLive(this);
            } else {
                //close active subscription
                if (!this._subscriptionId) {
                    return this._isLive;
                }
                disableLive(this);
            }
            return this._isLive;
        }
        /**
         * watch for datasource property changes
         *
         * @param {function} callback function to run when datasource properties change
         * @return {object} watch token to stop receiving property changes
         */
        watch(callback) {
            this.watchers.push(callback);
            return {
                unwatch: () => {
                    const idx = this.watchers.indexOf(callback);
                    if (idx !== -1) {
                        this.watchers.splice(idx, 1);
                    }
                }
            };
        }
        /**
         * reload comments for currently displayed time period
         */
        reloadComments() {
            const { TrendTask } = shmi.requires("visuals.controls.iqtrend.trendtask"),
                tStart = Math.floor(this._tStart - (this._duration / 2)),
                tEnd = Math.min(Date.now(), Math.ceil(this._tEnd + (this._duration / 2)));

            if (this._loadComments) {
                this.data.markers.comment = [];
                const threadsTask = new TrendTask("threads", {
                    self: this,
                    tStart,
                    tEnd
                }, loadThreads);
                this.queue.queueTask(threadsTask);
            }
        }
    };
}());

/**
 * Module to implement value graph for iq-trend.
 *
 * @module visuals/controls/iqtrend/graph
*/
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.graph",
        /** @lends module:visuals/controls/iqtrend/graph */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update canvas element size
     *
     * @param {object} self instance reference
     */
    function updateSize(self) {
        self.canvas.width = self.width + 2;
        self.canvas.height = self.height + 2;
    }

    /**
     * get canvas coordinates from value space coordinates
     *
     * @param {object} self instance reference
     * @param {number|null} x x-coordiante
     * @param {number|null} y y-coordinate
     * @return {object} coordinates on canvas
     */
    function getCoordinates(self, x, y) {
        return {
            x: x === null ? null : (x - self.minX) * self.scaleX + 1,
            y: y === null ? null : self.height - (y - self.minY) * self.scaleY + 1
        };
    }

    /**
     * get line segments for time period displayed on graph
     *
     * @param {object} self instance reference
     * @param {string|null} [property=null] value property or `null` for main value
     * @return {object[]} line segments
     */
    function getSegments(self, property = null) {
        const values = self.values.filter((value) => value.property === property);
        let firstIndex = values.findIndex((value) => value.x >= self.minX);
        if (firstIndex > 0) {
            firstIndex -= 1;
        } else if (firstIndex === -1 && values.length) {
            firstIndex = values.length - 1;
        }
        let segments = [];
        if (firstIndex >= 0) {
            for (let i=firstIndex; i < values.length; i++) {
                segments.push({
                    x1: values[i].x,
                    x2: null,
                    y: typeof values[i].y === "boolean" ? values[i].y ? 1 : 0 : values[i].y
                });
                if (segments.length > 1) {
                    let lastSegment = segments[segments.length - 2];
                    const segmentDuration = values[i].x - lastSegment.x1;
                    let numIntervals = Math.floor(segmentDuration / self.interval);
                    const segmentRemainder = segmentDuration - numIntervals * self.interval;
                    if (!self.discrete && numIntervals > 0 && segmentRemainder === 0) {
                        numIntervals -= 1;
                    }
                    lastSegment.x2 = lastSegment.x1 + numIntervals * self.interval;
                    if (self.discrete) {
                        lastSegment.x2 += segmentRemainder;
                    }
                }
                if (values[i].x >= self.maxX) {
                    break;
                }
            }

            if (segments.length) {
                if (segments[0].x1 < self.minX && ((segments[0].x2 > segments[0].x1) || (segments[0].x2 === null))) {
                    segments[0].x1 = self.minX;
                }

                if (segments[segments.length - 1].x1 < self.maxX) {
                    if (self.dataset.trendWidget.isLive() && (segments[segments.length - 1].x1 + (self.interval * 2)) <= self.maxX) { //live mode
                        //with auto-continuation:
                        segments[segments.length - 1].x2 = segments[segments.length - 1].x1 + (Math.floor((self.maxX - segments[segments.length - 1].x1) / self.interval) * self.interval);

                        //no auto-continuation:
                        //segments[segments.length - 1].x2 = segments[segments.length - 1].x1;
                    } else if (!self.dataset.trendWidget.isLive() && segments[segments.length - 1].x1 < self.maxX) { //archived data, last value < max-X
                        segments[segments.length - 1].x2 = segments[segments.length - 1].x1 + (Math.ceil((self.maxX - segments[segments.length - 1].x1) / self.interval) * self.interval);
                    } else { //archived data, last value >= max-X
                        segments[segments.length - 1].x2 = segments[segments.length - 1].x1;
                    }
                } else { //archived data
                    segments = segments.filter((segment, idx) => idx === 0 || segment.x1 <= self.maxX || segments[idx - 1].x2 < self.maxX);

                    if (segments.length === 0) {
                        return [];
                    }

                    const lastSegment = segments[segments.length - 1],
                        prevSegment = segments[segments.length - 2];
                    if (prevSegment && lastSegment.x1 > self.maxX) {
                        const dt1 = lastSegment.x1 - prevSegment.x2,
                            dv = (lastSegment.y - prevSegment.y) / dt1,
                            dt2 = self.maxX - prevSegment.x2;
                        lastSegment.x1 = lastSegment.x2 = self.maxX;
                        lastSegment.y = prevSegment.y + (dt2 * dv);
                    } else if (lastSegment.x2 > self.maxX && lastSegment.x2 > lastSegment.x1) {
                        lastSegment.x2 = self.maxX;
                    }
                }
            }
        }

        return segments;
    }

    /**
     * draw line from line segments
     *
     * @param {object} self instance reference
     * @param {object} style style options
     * @param {object[]} segments line segments
     * @return {object[][]} drawn lines
     */
    function drawLine(self, style, segments) {
        if (!segments.length) {
            return [];
        }
        const context = self.context;
        context.lineWidth = style.lineWidth ? style.lineWidth : 1;
        context.strokeStyle = style.strokeStyle ? style.strokeStyle : "rgb(0, 0, 0)";
        if (Array.isArray(style.lineDash) && style.lineDash.length) {
            context.setLineDash(style.lineDash);
        } else {
            context.setLineDash([]);
        }
        context.beginPath();
        let lastValue = null;

        const lineSegments = [];
        let currentLine = [];
        segments.forEach((segment) => {
            if (typeof segment.y !== "number" || segment.y === null || segment.x1 === null || segment.x2 === null) {
                lastValue = null;
                if (currentLine.length) {
                    lineSegments.push(currentLine);
                    currentLine = [];
                }
                return;
            }
            const start = getCoordinates(self, segment.x1, segment.y),
                end = getCoordinates(self, segment.x2, null);

            if (lastValue === null) {
                log(`moveTo ${start.x}, ${start.y}`);
                context.moveTo(start.x, start.y);
            } else {
                log(`lineTo ${start.x}, ${start.y}`);
                context.lineTo(start.x, start.y);
            }
            lastValue = start.y;
            log(`lineTo ${end.x}, ${start.y}`);
            context.lineTo(end.x, start.y);
            currentLine.push({ x1: start.x, x2: end.x, y: start.y });
        });
        if (currentLine.length) {
            lineSegments.push(currentLine);
        }
        log(`-> stroke`);
        context.stroke();
        return lineSegments;
    }

    /**
     * set style of line
     *
     * @param {object} self instance reference
     * @param {string} name either `"value"` or `"tolerance"
     * @param {object} style style options
     */
    function setLineStyle(self, name, style) {
        const lineStyle = self.style[name];
        if (lineStyle) {
            if (typeof style.lineWidth === "number" && style.lineWidth > 0) {
                lineStyle.lineWidth = style.lineWidth;
            }

            if (typeof style.strokeStyle === "string") {
                lineStyle.strokeStyle = style.strokeStyle;
            }

            if (typeof style.fillStyle === "string") {
                lineStyle.fillStyle = style.fillStyle;
            }

            if (Array.isArray(style.lineDash)) {
                lineStyle.lineDash = style.lineDash;
            }
        }
    }

    /**
     * find value of graph at specified timestamp
     *
     * @param {object[]} segments line segments
     * @param {number} timestamp timestamp in ms
     * @return {number|null} value at specified timestamp or `null` if none recorded
     */
    function findSegmentValue(segments, timestamp) {
        const start = segments.slice().reverse().find((segment) => segment.x1 <= timestamp),
            end = segments.find((segment) => segment.x2 >= timestamp);

        if (!start || !end) {
            return null;
        }

        if (start === end) {
            return start.y;
        }

        //do not try to interpolate string values
        if (typeof start.y === "string" || typeof end.y === "string") {
            return typeof start.y === "string" ? start.y : end.y;
        }

        const range = start.x2 - end.x1,
            tDiff = timestamp - start.x2,
            vDiff = end.y - start.y;

        return start.y - (tDiff / range) * vDiff;
    }

    module.Graph = class Graph {
        /**
         * Creates an instance of Graph.
         * @param {HTMLElement} element parent element to attach graph to
         * @param {object} options graph options
         */
        constructor(element, {
            externalDraw = false,
            x = {
                min: 0,
                max: 60000
            },
            y = {
                min: 0,
                max: 100
            },
            interval = 1000,
            discrete = false,
            style = {
                value: {
                    lineWidth: 1,
                    strokeStyle: "rgb(0, 0, 0)",
                    fillStyle: "rgb(0, 0, 0.05)",
                    lineDash: []
                },
                tolerance: {
                    lineWidth: 1,
                    strokeStyle: "rgb(0, 0, 0)",
                    lineDash: [5, 5]
                }
            },
            fillCurve = false,
            drawTolerance = false,
            dataset = null
        }) {
            this.values = [];
            this.canvas = document.createElement("canvas");
            this.parent = element;
            this.canvas.classList.add("graph-canvas");
            this.parent.appendChild(this.canvas);

            const rect = this.parent.getBoundingClientRect();
            this.width = rect.width - 2;
            this.height = rect.height - 2;
            this.style = {
                value: {
                    lineWidth: 1,
                    strokeStyle: "rgb(0, 0, 0)",
                    fillStyle: "rgb(0, 0, 0.05)",
                    lineDash: []
                },
                tolerance: {
                    lineWidth: 1,
                    strokeStyle: "rgb(0, 0, 0)",
                    lineDash: [5, 5]
                }
            };
            this.dataset = dataset;
            this.segments = {
                value: [],
                tolerance: {
                    min: [],
                    max: []
                }
            };
            this.setOptions({
                externalDraw,
                x,
                y,
                interval,
                discrete,
                style,
                fillCurve,
                drawTolerance
            }, false);
            updateSize(this);
            this.context = this.canvas.getContext("2d");
            let watcherUpdateId = 0;
            this.observer = new ResizeObserver((entries) => {
                const entry = entries[0];
                this.width = entry.contentRect.width - 2;
                this.height = entry.contentRect.height - 2;
                this.scaleX = this.width / (this.maxX - this.minX);
                this.scaleY = this.height / (this.maxY - this.minY);
                updateSize(this);

                shmi.caf(watcherUpdateId);
                watcherUpdateId = shmi.raf(() => {
                    if (!this.parent) {
                        return; //cancel if detached from parent
                    }
                    this.watchers.forEach((w) => {
                        w({
                            graph: this,
                            dataset: this.dataset,
                            width: this.width,
                            height: this.height,
                            scale: {
                                x: this.scaleX,
                                y: this.scaleY
                            },
                            dimensions: {
                                x: {
                                    min: this.minX,
                                    max: this.maxX
                                },
                                y: {
                                    min: this.minY,
                                    max: this.maxY
                                }
                            }
                        });
                    });
                });

                this.draw();
            });
            this.watchers = [];
            this.observer.observe(this.parent);
            this.draw();
        }
        /**
         * set graph options after instantiation
         *
         * @param {object} options graph options
         * @param {boolean} [update=true] apply options & redraw graph immediately if `true`
         */
        setOptions(options, update = true) {
            let updateScale = false,
                updateDraw = false;

            if (typeof options.discrete === "boolean") {
                this.discrete = options.discrete;
                updateDraw = true;
            }

            if (options.y && typeof options.y.min === "number") {
                this.minY = options.y.min;
                updateScale = true;
            }

            if (options.y && typeof options.y.max === "number") {
                this.maxY = options.y.max;
                updateScale = true;
            }

            if (options.x && typeof options.x.min === "number") {
                this.minX = options.x.min;
                updateScale = true;
            }

            if (options.x && typeof options.x.max === "number") {
                this.maxX = options.x.max;
                updateScale = true;
            }

            if (typeof options.interval === "number" && options.interval > 0) {
                this.interval = options.interval;
                updateDraw = true;
            }

            if (typeof options.externalDraw === "boolean") {
                this.externalDraw = options.externalDraw;
                updateDraw = true;
            }

            if (typeof options.fillCurve === "boolean") {
                this.fillCurve = options.fillCurve;
                updateDraw = true;
            }

            if (typeof options.drawTolerance === "boolean") {
                this.drawTolerance = options.drawTolerance;
                updateDraw = true;
            }

            if (options.style) {
                if (options.style.value) {
                    setLineStyle(this, "value", options.style.value);
                }
                if (options.style.tolerance) {
                    setLineStyle(this, "tolerance", options.style.tolerance);
                }
            }

            if (updateScale) {
                this.scaleX = this.width / (this.maxX - this.minX);
                this.scaleY = this.height / (this.maxY - this.minY);
            }

            if (update && (updateDraw || updateScale)) {
                this.draw();
            }
        }
        /**
         * draw graph
         *
         */
        draw() {
            if (!this.parent) {
                return;
            }
            this.context.clearRect(0, 0, this.width + 2, this.height + 2);

            const segments = getSegments(this, null);
            this.segments.value = segments;
            const lineElements = drawLine(this, {
                lineWidth: this.style.value.lineWidth,
                strokeStyle: this.style.value.strokeStyle,
                lineDash: this.style.value.lineDash
            }, segments);

            if (this.fillCurve && lineElements.length) {
                this.context.fillStyle = this.style.value.fillStyle;
                lineElements.forEach((line) => {
                    this.context.beginPath();
                    line.forEach((segment, idx) => {
                        if (idx === 0) {
                            this.context.moveTo(segment.x1, segment.y);
                        } else {
                            this.context.lineTo(segment.x1, segment.y);
                        }
                        this.context.lineTo(segment.x2, segment.y);
                    });
                    const minY = this.height + 2;
                    this.context.lineTo(line[line.length - 1].x2, minY);
                    this.context.lineTo(line[0].x1, minY);
                    this.context.closePath();
                    this.context.fill();
                });
            }

            if (this.drawTolerance) {
                const tolLowerSegments = getSegments(this, "tolerance_lower");
                const tolUpperSegments = getSegments(this, "tolerance_upper");

                this.segments.tolerance.min = tolLowerSegments;
                this.segments.tolerance.max = tolUpperSegments;

                drawLine(this, {
                    lineWidth: this.style.tolerance.lineWidth,
                    strokeStyle: this.style.tolerance.strokeStyle,
                    lineDash: this.style.tolerance.lineDash
                }, tolLowerSegments);
                drawLine(this, {
                    lineWidth: this.style.tolerance.lineWidth,
                    strokeStyle: this.style.tolerance.strokeStyle,
                    lineDash: this.style.tolerance.lineDash
                }, tolUpperSegments);
            }
        }
        /**
         * get timestamp at specified x-coordinate of canvas
         *
         * @param {number} x x-coordinate
         * @return {number} timestamp in ms
         */
        getTimeAt(x) {
            x -= 1; //account for extra pixel

            const range = this.maxX - this.minX,
                time = this.minX + (x / this.width) * range;

            return time;
        }
        /**
         * get value at specified y-coordinate of canvas
         *
         * @param {number} y y-coordinate
         * @return {number} value at coordinate
         */
        getValueAtY(y) {
            y -= 1; //account for extra pixel

            const range = this.maxY - this.minY,
                pos = this.height - y,
                value = this.minY + (pos / this.height) * range;

            return value;
        }
        /**
         * get value of graph at specified timestamp
         *
         * @param {number} x timestamp in ms
         * @return {object} value & tolerances
         */
        getValue(x) {
            const value = findSegmentValue(this.segments.value, x),
                tolUpper = findSegmentValue(this.segments.tolerance.max, x),
                tolLower = findSegmentValue(this.segments.tolerance.min, x);

            return {
                value: typeof value === "number" || typeof value === "string" ? value : null,
                tolerance: {
                    min: typeof tolUpper === "number" ? tolUpper : null,
                    max: typeof tolLower === "number" ? tolLower : null
                }
            };
        }
        /**
         * get current graph dimensions
         *
         * @return {*}
         */
        getDimensions() {
            return {
                x: {
                    min: this.minX,
                    max: this.maxX
                },
                y: {
                    min: this.minY,
                    max: this.maxY
                },
                width: this.width,
                height: this.height
            };
        }
        /**
         * add single value to graph
         *
         * @param {number} x x value (timestamp in ms)
         * @param {number} y y value (recorded value)
         * @param {string} [property=null] property name or `null` for main value
         */
        addValue(x, y, property = null) {
            this.values.push({ x, y, property });
            this.values.sort((a, b) => a.x - b.x);
            if (!this.externalDraw) {
                this.draw();
            }
        }
        /**
         * add multiple values to graph
         *
         * @param {object[]} values values to add
         */
        addValues(values) {
            values.forEach(([x, y, property = null]) => {
                this.values.push({ x, y, property });
            });
            this.values.sort((a, b) => a.x - b.x);
            if (!this.externalDraw) {
                this.draw();
            }
        }
        /**
         * clear current graph values
         *
         */
        clearValues() {
            this.values = [];
        }
        /**
         * remove graph from parent
         *
         */
        remove() {
            if (this.parent) {
                this.clearValues();
                this.observer.disconnect();
                this.parent.removeChild(this.canvas);
                this.parent = null;
                this.watchers = [];
            }
        }
        /**
         * watch graph for dimension changes
         *
         * @param {function} callback notification callback
         * @return {object} reference to stop watching
         */
        watch(callback) {
            this.watchers.push(callback);
            return {
                unwatch: () => {
                    const idx = this.watchers.indexOf(callback);
                    if (idx !== -1) {
                        this.watchers.splice(idx, 1);
                    }
                }
            };
        }
    };
}());

/**
 * Module to implement overlay marker display (comments) for iq-trend
 *
 * @module visuals/controls/iqtrend/markerlayer
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.markerlayer",
        /** @lends module:visuals/controls/iqtrend/markerlayer */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update width & scale
     *
     * @param {object} self instance reference
     * @param {object} contentRect element dimensions
     */
    function updateSize(self, contentRect) {
        self.width = contentRect.width - 2;
        self.scale = self.width / (self.max - self.min);
    }

    /**
     * get template for marker type
     *
     * @param {object} self instance reference
     * @param {string} type marker type
     * @return {HTMLElement|null} marker template
     */
    function getMarkerTemplate(self, type) {
        return self.trendWidget.vars.markerTemplates[type] || null;
    }

    /**
     * get x-coordinate from timestamp
     *
     * @param {object} self instance reference
     * @param {number} x timestamp in ms
     * @return {number|null} x-coordinate or `null` if no timestamp supplied
     */
    function getXCoordinate(self, x) {
        return x === null ? null : (x - self.min) * self.scale + 1;
    }

    /**
     * shortcut for API requests
     *
     * @param {string} apiCommand api command
     * @param {object} parameters command parameters
     * @param {object} options request options
     * @return {Promise} promise resolving to request response
     */
    function request(apiCommand, parameters, options) {
        return shmi.visuals.session.ConnectSession.requestPromise(apiCommand, parameters, options);
    }

    /**
     * lookup element in marker template
     *
     * @param {object} markerRef marker reference
     * @param {string} elementId element id for marker reference
     * @param {string} elementAttribute 'data-ui' attribute to lookup
     * @return {boolean} `true` if found & mapped, `false` else
     */
    function lookupMarkerElement(markerRef, elementId, elementAttribute) {
        markerRef[elementId] = shmi.getUiElement(elementAttribute, markerRef.element);
        if (!markerRef[elementId]) {
            console.warn(`Widget template is missing '${elementAttribute}' element required to activate comment markers.`);
            return false;
        }
        return true;
    }

    /**
     * perform if marker info is closed
     *
     * @param {object} self instance reference
     * @param {object} markerRef marker reference
     */
    function onMarkerClose(self, markerRef) {
        markerRef.element.classList.remove("open");
        self.parent.classList.remove("active");
        markerRef.listenersContent.forEach((l) => {
            l.disable();
        });
        markerRef.listenersContent = [];
        self.currentMarker = null;
    }

    /**
     * update size of marker info
     *
     * @param {object} self instance reference
     * @param {object} markerRef marker reference
     */
    function updateMarkerSize(self, markerRef) {
        //reset existing property & class for calculation
        markerRef.info.classList.remove("reversed");
        markerRef.info.style.removeProperty("--trend-marker-info-width");

        const widgetStyle = getComputedStyle(self.trendWidget.element),
            varValue = parseInt(widgetStyle.getPropertyValue("--trend-marker-info-width").replace("px", "")),
            graphsRect = self.trendWidget.vars.elements.graphs.getBoundingClientRect(),
            markerRect = markerRef.element.getBoundingClientRect(),
            infoRect = markerRef.info.getBoundingClientRect(),
            infoOffset = Math.abs(markerRect.x - infoRect.x);

        if (markerRect.x <= graphsRect.x + (graphsRect.width / 2)) {
            //normal
            if (markerRect.x - infoOffset + varValue > graphsRect.x + graphsRect.width) {
                //adjust size
                const diffX = (markerRect.x - infoOffset + varValue) - (graphsRect.x + graphsRect.width);
                markerRef.info.style.setProperty("--trend-marker-info-width", `${varValue - diffX}px`);
            }
        } else {
            //reverse
            markerRef.info.classList.add("reversed");

            if (markerRect.x + infoOffset - varValue < graphsRect.x) {
                const diffX = graphsRect.x - (markerRect.x + infoOffset - varValue);
                markerRef.info.style.setProperty("--trend-marker-info-width", `${varValue - diffX}px`);
            }
        }
    }

    /**
     * load & set content of marker info
     *
     * @param {object} self instance reference
     * @param {object} markerRef marker reference
     */
    async function setMarkerContent(self, markerRef) {
        const { formatDateTime } = shmi.requires("visuals.tools.date"),
            marker = markerRef.marker,
            { currentUser } = shmi.requires("visuals.session.UserManager"),
            timestampFormat = shmi.localize(self.trendWidget.config.commentTimestampFormat || "$DD.$MM.$YYYY, $HH:$mm:$ss");

        try {
            const markerData = await request("thread.get", {
                thread_id: marker.id
            });
            let mayEdit = false;

            //workaround - test if user may edit thread
            try {
                await request("thread.modify", { thread_id: -1, title: null, message: "" });
            } catch (err) {
                if (!(err.category === "shmi:connect:api:generic" && err.errc === 4)) {
                    mayEdit = true;
                }
            }

            if (markerData) {
                markerRef.title.textContent = markerData.message.title;
                markerRef.title.title = markerData.message.title;
                markerRef.text.textContent = markerData.message.message;
                const markerTime = typeof markerData.message.edited_timestamp === "number" ? markerData.message.edited_timestamp : markerData.message.timestamp,
                    timeText = formatDateTime(markerTime / 1000, timestampFormat);
                markerRef.time.textContent = timeText;
                markerRef.time.title = timeText;
                if (mayEdit && currentUser.id === markerData.message.user_id) {
                    markerRef.element.classList.add("editable");
                }

                try {
                    const userInfo = await request("user.info", markerData.message.user_id);
                    if (userInfo.first_name && userInfo.first_name.length && userInfo.last_name && userInfo.last_name.length) {
                        markerRef.user.textContent = `${userInfo.first_name} ${userInfo.last_name}`;
                    } else {
                        markerRef.user.textContent = userInfo.username;
                    }
                } catch (userExc) {
                    console.error(`Error retrieving user information for user-id '${markerData.message.user_id}':`, userExc);
                    markerRef.user.textContent = markerData.message.user_id;
                }
            }
        } catch (threadExc) {
            console.error(`Error retrieving data for thread-ID '${marker.id}':`, threadExc);
        }
    }

    /**
     * perform when marker info is opened
     *
     * @param {object} self instance reference
     * @param {object} markerRef marker reference
     */
    async function onMarkerOpen(self, markerRef) {
        const marker = markerRef.marker;

        if (self.trendWidget.isLive()) {
            self.trendWidget.setLive(false);
        }

        if (self.currentMarker) {
            onMarkerClose(self, self.currentMarker.ref);
        }
        self.currentMarker = {
            data: marker,
            ref: markerRef
        };

        updateMarkerSize(self, markerRef);

        await setMarkerContent(self, markerRef);
        markerRef.element.classList.add("open");
        self.parent.classList.add("active");

        const { MouseListener, TouchListener } = shmi.requires("visuals.io"),
            inputHandlerClose = {
                onPress: (x, y, event) => {
                    event.stopPropagation();
                },
                onRelease: (x, y, event) => {
                    event.stopPropagation();
                },
                onClick: (x, y, event) => {
                    event.stopPropagation();
                    onMarkerClose(self, markerRef);
                }
            },
            mhClose = new MouseListener(markerRef.close, inputHandlerClose),
            thClose = new TouchListener(markerRef.close, inputHandlerClose),
            mhParent = new MouseListener(self.parent, inputHandlerClose),
            thParent = new TouchListener(self.parent, inputHandlerClose);

        const inputHandlerEdit = {
                onPress: (x, y, event) => {
                    event.stopPropagation();
                },
                onRelease: (x, y, event) => {
                    event.stopPropagation();
                },
                onClick: async (x, y, event) => {
                    event.stopPropagation();
                    if (!self.trendWidget.isLocked()) {
                        const threads = shmi.requires("visuals.tools.threads"),
                            result = await threads.edit(marker.id, {
                                dialogTitle: "${iqtrend_edit_thread}",
                                titleLabel: "${iqtrend_threads_title}",
                                messageLabel: "${iqtrend_threads_message}",
                                deleteLabel: "${iqtrend_threads_delete_btn}",
                                deleteConfirmation: "${iqtrend_threads_delete_confirmation}",
                                applyLabel: "${iqtrend_threads_edit_btn}",
                                enterMessageNotification: "${iqtrend_threads_enter_message_notification}"
                            });
                        if (result !== null) {
                            if (result === -1) { //comment was deleted
                                onMarkerClose(self, markerRef);
                            }
                            self.trendWidget.vars.datasource.source.reloadComments();
                        }
                    }
                }
            },
            mhEdit = new MouseListener(markerRef.edit, inputHandlerEdit),
            thEdit = new TouchListener(markerRef.edit, inputHandlerEdit);

        const inputHandlerContent = {
                onPress: (x, y, event) => {
                    event.stopPropagation();
                },
                onRelease: (x, y, event) => {
                    event.stopPropagation();
                },
                onClick: (x, y, event) => {
                    event.stopPropagation();
                },
                onWheel: (delta, event) => {
                    event.stopPropagation();
                }
            },
            mhContent = new MouseListener(markerRef.content, inputHandlerContent),
            thContent = new TouchListener(markerRef.content, inputHandlerContent);

        markerRef.listenersContent.push(mhClose, thClose, mhParent, thParent, mhEdit, thEdit, mhContent, thContent);
        markerRef.listenersContent.forEach((l) => {
            l.enable();
        });
    }

    /**
     * update existing marker
     *
     * @param {object} self instance reference
     * @param {object} marker marker data
     */
    async function updateMarker(self, marker) {
        const markerRef = self.markerElements[marker.type][marker.id];
        if (markerRef) {
            markerRef.marker = marker;
            if (self.currentMarker && self.currentMarker.ref === markerRef) {
                self.currentMarker.data = marker;
            }
            await setMarkerContent(self, markerRef);
        }
    }

    /**
     * create new marker element
     *
     * @param {object} self instance reference
     * @param {object} marker marker data
     */
    function createMarker(self, marker) {
        const markerRef = self.markerElements[marker.type][marker.id] = {
            marker: marker,
            element: getMarkerTemplate(self, marker.type).cloneNode(true),
            icon: null,
            listenersIcon: [],
            listenersContent: []
        };

        if (
            [
                lookupMarkerElement(markerRef, "icon", "marker-icon"),
                lookupMarkerElement(markerRef, "title", "marker-title"),
                lookupMarkerElement(markerRef, "text", "marker-text"),
                lookupMarkerElement(markerRef, "time", "marker-time"),
                lookupMarkerElement(markerRef, "user", "marker-user"),
                lookupMarkerElement(markerRef, "info", "marker-info"),
                lookupMarkerElement(markerRef, "close", "marker-close"),
                lookupMarkerElement(markerRef, "content", "marker-content"),
                lookupMarkerElement(markerRef, "edit", "marker-edit")
            ].every((value) => value)
        ) {
            const { MouseListener, TouchListener } = shmi.requires("visuals.io"),
                inputHandler = {
                    onPress: () => { /* implementation of handler stops event bubbling */ },
                    onRelease: () => { /* implementation of handler stops event bubbling */ },
                    onClick: async () => {
                        if (!self.trendWidget.isLocked() && !(self.currentMarker && self.currentMarker.ref === markerRef)) {
                            await onMarkerOpen(self, markerRef);
                        }
                    }
                },
                mh = new MouseListener(markerRef.icon, inputHandler),
                th = new TouchListener(markerRef.icon, inputHandler);
            markerRef.listenersIcon.push(mh, th);
            markerRef.listenersIcon.forEach((l) => {
                l.enable();
            });
        }
    }

    module.MarkerLayer = class MarkerLayer {
        /**
         * Creates an instance of MarkerLayer.
         *
         * @param {HTMLElement} element parent element to attach marker-layer to
         * @param {object} trendWidget trend instance reference
         */
        constructor(element, trendWidget) {
            this.parent = element;
            this.trendWidget = trendWidget;
            this.width = 1;
            this.scale = 1;
            this.min = 0;
            this.max = 60000;
            this.markers = [];
            this.markerElements = {
                alarm: {},
                comment: {}
            };
            this.currentMarker = null;
            updateSize(this, this.parent.getBoundingClientRect());
        }
        /**
         * draw marker layer
         *
         */
        draw() {
            const activeMarkers = [];
            this.markers.forEach((marker) => {
                if (this.markerElements[marker.type]) {
                    if (!this.markerElements[marker.type][marker.id]) {
                        createMarker(this, marker);
                    } else {
                        updateMarker(this, marker);
                    }
                    const markerRef = this.markerElements[marker.type][marker.id];
                    const xCoordinate = getXCoordinate(this, marker.timestamp);
                    if (marker.timestamp < this.min || marker.timestamp > this.max) {
                        markerRef.element.style.display = "none";
                    } else {
                        markerRef.element.style.display = "";
                    }
                    markerRef.element.style.left = `${xCoordinate}px`;
                    if (!markerRef.element.parentNode) {
                        this.parent.appendChild(markerRef.element);
                    }
                    activeMarkers.push(markerRef.marker);
                }
            });

            Object.keys(this.markerElements).forEach((markerType) => {
                Object.keys(this.markerElements[markerType]).forEach((markerId) => {
                    const markerRef = this.markerElements[markerType][markerId];
                    if (!activeMarkers.includes(markerRef.marker)) {
                        if (this.currentMarker && markerRef === this.currentMarker.ref) {
                            onMarkerClose(this, markerRef);
                        }
                        markerRef.listenersIcon.forEach((l) => {
                            l.disable();
                        });
                        markerRef.listenersIcon = [];
                        this.parent.removeChild(markerRef.element);
                        delete this.markerElements[markerType][markerId];
                    }
                });
            });
        }
        /**
         * add markers to marker-layer
         *
         * @param {object[]} markers marker data
         */
        addMarkers(markers) {
            markers.forEach((marker) => {
                this.markers.push(marker);
            });
            this.markers.sort((a, b) => a.timestamp - b.timestamp);
            this.draw();
        }
        /**
         * clear markers from marker-layer
         *
         * @param {string|null} [markerType=null] marker type or `null` to clear all
         */
        clearMarkers(markerType = null) {
            this.markers = (typeof markerType === "string") ? this.markers.filter((marker) => marker.type !== markerType) : [];
        }
        /**
         * set marker-layer options
         *
         * @param {object} options marker-layer options
         */
        setOptions(options) {
            if (options.x) {
                if (typeof options.x.min === "number") {
                    this.min = options.x.min;
                }
                if (typeof options.x.max === "number") {
                    this.max = options.x.max;
                }
                this.scale = this.width / (this.max - this.min);
                this.draw();
            }
        }
        /**
         * trigger maker-layer size update
         *
         */
        updateSize() {
            updateSize(this, this.parent.getBoundingClientRect());
            this.draw();
            if (this.currentMarker) {
                updateMarkerSize(this, this.currentMarker.ref);
            }
        }
    };
}());

/**
 * Module to implement value scales for iq-trend.
 *
 * @module visuals/controls/iqtrend/scale
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.scale",
        /** @lends module:visuals/controls/iqtrend/scale */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    /**
     * initial value for height of labeled steps
     * @type {number}
     */
    const stepSize = 30;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update canvas size
     *
     * @param {object} self instance reference
     */
    function updateSize(self) {
        self.canvas.width = self.width + 2;
        self.canvas.height = self.height + 2;
        const style = getComputedStyle(self.parent);
        self.strokeStyle = style.getPropertyValue("--scale-color");
    }

    /**
     * step widths to use for label generation
     * @type {number[]}
     */
    const stepWidths = [
        1,
        0.5,
        0.25,
        0.2
    ];

    /**
     * change number of maximum decimal digits
     *
     * @param {number} value value
     * @param {number} digits number of decimal digits
     * @return {number} output value
     */
    function toDigits(value, digits) {
        return parseFloat(value.toFixed(digits));
    }

    /**
     * get step size for scale labels
     *
     * @param {number} min minimum value
     * @param {number} max maximum value
     * @param {number} maxNumSteps maximum number of steps
     * @return {number} step size
     */
    function getSimpleStep(min, max, maxNumSteps) {
        const range = max - min;
        let order = Math.ceil(Math.log10(range));

        let step = 1;
        let numSteps = 0;
        while (numSteps < maxNumSteps) {
            for (let i = 0; i < stepWidths.length; i++) {
                let testStep = stepWidths[i] * (10 ** order);
                numSteps = range / testStep;
                if (numSteps <= maxNumSteps) {
                    step = testStep;
                } else {
                    break;
                }
            }
            order -= 1;
        }

        return step;
    }

    /**
     * get scale range information
     *
     * @param {number} minY minimum y-value
     * @param {number} maxY maximum y-value
     * @param {number} maxNumSteps maximum number of scale steps
     * @return {object} scale range information
     */
    function getRangeInfo(minY, maxY, maxNumSteps) {
        const step = getSimpleStep(minY, maxY, maxNumSteps),
            digits = getDigits(step),
            startY = Math.floor(minY / step) * step,
            endY = startY + Math.ceil((maxY - startY) / step) * step;

        return {
            step: toDigits(step, digits),
            start: toDigits(startY, digits),
            range: toDigits(endY - startY, digits)
        };
    }

    /**
     * get decimal digits of value
     *
     * @param {number} value input value
     * @return {number} number of decimal digits
     */
    function getDigits(value) {
        value = parseFloat(value.toFixed(14));
        const valueString = String(value);
        let valueDigits = valueString.indexOf(".");
        if (valueDigits !== -1) {
            valueDigits = valueString.length - valueDigits - 1;
        } else {
            valueDigits = 0;
        }
        return valueDigits;
    }

    /**
     * check if supplied unit is a unit-class
     *
     * @param {string|number} unitText input unit
     * @return {boolean} `true` if unit is unit-class, `false` else
     */
    function isUnitClass(unitText) {
        return String(parseInt(unitText)) === String(unitText);
    }

    /**
     * get stroke style for value
     *
     * @param {object} self instance reference
     * @param {string} valueName name of value
     * @return {object|null} stroke style info or `null` if none found
     */
    function getValueStroke(self, valueName) {
        let dataset = self.dataset;

        if (valueName !== self.valueName) {
            dataset = self.dataset.trendWidget.vars.datasets.find((ds) => ds.name === valueName);
        }

        return dataset ? dataset.graph.style.value.strokeStyle : null;
    }

    /**
     * add marker for line associated with scale
     *
     * @param {object} self instance reference
     * @param {string} valueName name of value
     */
    function addLineMarker(self, valueName) {
        const lineMarker = document.createElement("div");

        lineMarker.classList.add("line-marker");
        lineMarker.style.setProperty("--line-color", getValueStroke(self, valueName));
        self.lineMarkers.push(lineMarker);
    }

    /**
     * update unit information
     *
     * @param {object} self instance reference
     */
    function updateUnitInfo(self) {
        const valueSettings = self.dataset.trendWidget.vars.datasource.source.getValueSettings(),
            uc = shmi.requires("visuals.tools.unitClasses"),
            identicalUnits = self.additionalValues.length === 0 || (valueSettings[self.valueName] && self.additionalValues.every((valueName) => valueSettings[valueName] && valueSettings[self.valueName].unit === valueSettings[valueName].unit));
        let unitText = "";

        if (identicalUnits && valueSettings[self.valueName]) {
            if (isUnitClass(valueSettings[self.valueName].unit)) {
                const ucAdapter = uc.getSelectedAdapter(valueSettings[self.valueName].unit);
                if (ucAdapter) {
                    unitText = ucAdapter.unitText;
                } else {
                    unitText = valueSettings[self.valueName].unit;
                }
            } else {
                unitText = valueSettings[self.valueName].unit === null ? "" : valueSettings[self.valueName].unit;
            }
            self.unit = valueSettings[self.valueName].unit;
        }

        self.unitInfo.textContent = shmi.localize(unitText);
    }

    /**
     * update scale info (line-markers, unit-info) & dimensions
     *
     * @param {object} self instance reference
     */
    function updateScaleInfo(self) {
        self.lineMarkers.forEach((lineMarker) => {
            lineMarker.parentNode.removeChild(lineMarker);
        });
        self.lineMarkers = [];

        addLineMarker(self, self.valueName);
        self.additionalValues.forEach((valueName) => {
            addLineMarker(self, valueName);
        });

        self.lineMarkers.forEach((lineMarker) => {
            self.markerContainer.appendChild(lineMarker);
        });

        updateUnitInfo(self);

        const parentRect = self.parent.getBoundingClientRect();
        const infoRect = self.scaleInfo.getBoundingClientRect();
        self.scaleInfoOffset = (infoRect.top - parentRect.top) + self.scaleInfo.offsetHeight;
    }

    /**
     * get scale canvas coordinate for specified value
     *
     * @param {object} self instance reference
     * @param {number} scale scaling factor
     * @param {number} value input value
     * @return {number} canvas y-coordinate
     */
    function getCoordinate(self, scale, value) {
        return self.height - (value - self.min) * scale + 1;
    }

    /**
     * draw scale marker
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {number} value input value
     * @param {number} width marker width
     * @return {number} canvas y-coordinate
     */
    function drawScaleMarker(self, scale, value, width) {
        const y = getCoordinate(self, scale, value),
            widthOffset = self.lineWidth / 2;

        if (self.mirrored) {
            self.context.moveTo(1, y - widthOffset);
            self.context.lineTo(width + 1, y - widthOffset);
        } else {
            self.context.moveTo(self.width + 1, y - widthOffset);
            self.context.lineTo(self.width - width + 1, y - widthOffset);
        }

        return y;
    }

    /**
     * draw small markers between labeled markers
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {object} stepInfo stepping info
     * @param {number} stepDistance step distance
     * @param {number} markerValue value at marker
     * @param {number} index marker index
     */
    function drawSmallMarkers(self, scale, stepInfo, stepDistance, markerValue, index) {
        if (stepDistance > 25) {
            if (index === 0) { //draw markers before first labeled marker if in visible area
                const smallMarkerValue = markerValue - stepInfo.step / 2;
                if (smallMarkerValue >= self.min) {
                    drawScaleMarker(self, scale, smallMarkerValue, self.width - 2);
                }
            }

            {
                const smallMarkerValue = markerValue + stepInfo.step / 2;
                drawScaleMarker(self, scale, smallMarkerValue, self.width - 2);
            }
        }
    }

    module.Scale = class Scale {
        /**
         * Creates an instance of Scale.
         * @param {HTMLElement} element parent element to attach scale to
         * @param {object} {
         *             min = 0,
         *             max = 100,
         *             numSteps = 21,
         *             mirrored = false,
         *             location = "left",
         *             align = "left",
         *             strokeStyle = "rgb(0, 0, 0)",
         *             lineWidth = 1,
         *             hidden = false,
         *             valueName = "value",
         *             additionalValues = []
         *         } scale options
         * @param {object} dataset parent dataset of scale
         */
        constructor(element, {
            min = 0,
            max = 100,
            numSteps = 21,
            mirrored = false,
            location = "left",
            align = "left",
            lineWidth = 1,
            hidden = false,
            valueName = "value",
            additionalValues = []
        }, dataset) {
            this.dataset = dataset;
            this.canvas = document.createElement("canvas");
            this.wrapper = document.createElement("div");
            this.wrapper.classList.add("scale-wrapper");
            this.parent = element;
            this.canvas.classList.add("scale-canvas");
            this.scaleInfo = document.createElement("div");
            this.scaleInfo.classList.add("scale-info");
            this.markerContainer = document.createElement("div");
            this.markerContainer.classList.add("line-markers");
            this.lineMarkers = [];
            this.valueName = valueName;
            this.additionalValues = additionalValues;
            this.unitInfo = document.createElement("div");
            this.unitInfo.classList.add("unit-info");
            this.scaleInfo.appendChild(this.markerContainer);
            this.scaleInfo.appendChild(this.unitInfo);
            this.wrapper.appendChild(this.scaleInfo);
            this.unit = null;

            this.mirrored = mirrored;
            this.location = location;
            if (location === "left" && mirrored) {
                this.wrapper.classList.add("mirrored");
            } else if (location === "right" && !mirrored) {
                this.wrapper.classList.add("mirrored");
            }
            this.lineWidth = lineWidth;
            this.hidden = hidden;
            if (this.hidden) {
                this.wrapper.classList.add("hidden");
            }

            this.markers = [];
            for (let i = 0; i < numSteps + 1; i++) {
                const marker = document.createElement("div");
                marker.classList.add("scale-label");
                marker.style.display = "none";
                this.wrapper.appendChild(marker);
                this.markers.push(marker);
            }

            this.wrapper.appendChild(this.canvas);
            this.parent.appendChild(this.wrapper);

            const rect = this.parent.getBoundingClientRect();
            this.width = this.canvas.offsetWidth - 2;
            this.height = rect.height - 2;

            const { getFontStyle } = shmi.requires("visuals.controls.iqtrend.common");
            this.fontStyle = getFontStyle(this.markers[0]);
            const { getTextDimensions } = shmi.requires("visuals.controls.iqtrend.common");
            const markerDims = getTextDimensions("123", this.fontStyle);
            this.markerHeight = markerDims.textHeight;
            this.markerWidth = markerDims.width;

            this.min = min;
            this.max = max;
            this.numSteps = numSteps;

            updateScaleInfo(this);
            updateSize(this);
            this.context = this.canvas.getContext("2d");
            this.observer = new ResizeObserver((entries) => {
                const entry = entries[0];
                this.width = this.canvas.offsetWidth - 2;
                this.height = entry.contentRect.height - 2;
                updateSize(this);
                updateScaleInfo(this);
                this.draw();
            });

            this.observer.observe(this.parent);
            this.draw();
        }
        /**
         * set scale options
         *
         * @param {object} options scale options
         */
        setOptions(options) {
            let redraw = false,
                updateInfo = false;

            if (this.parent === null) {
                return;
            }

            if (options.y) {
                if (typeof options.y.min === "number") {
                    this.min = options.y.min;
                }
                if (typeof options.y.max === "number") {
                    this.max = options.y.max;
                }
                redraw = true;
            }

            if (typeof options.location === "string") {
                if (options.location !== this.location) {
                    if (options.location === "left") {
                        this.location = "left";
                        this.mirrored = !this.mirrored;
                        this.parent = this.dataset.trendWidget.vars.elements.scalesLeft;
                        this.parent.appendChild(this.wrapper);
                    } else if (options.location === "right") {
                        this.location = "right";
                        this.mirrored = !this.mirrored;
                        this.parent = this.dataset.trendWidget.vars.elements.scalesRight;
                        this.parent.appendChild(this.wrapper);
                    }

                    if (this.location === "left" && this.mirrored) {
                        this.wrapper.classList.add("mirrored");
                    } else if (this.location === "right" && !this.mirrored) {
                        this.wrapper.classList.add("mirrored");
                    } else {
                        this.wrapper.classList.remove("mirrored");
                    }
                    redraw = true;
                }
            }

            if (typeof options.hidden === "boolean") {
                this.hidden = options.hidden;
                if (this.hidden) {
                    this.wrapper.classList.add("hidden");
                } else {
                    this.wrapper.classList.remove("hidden");
                    this.width = this.canvas.offsetWidth;
                    updateSize(this);
                }
                redraw = true;
            }

            if (typeof options.addValue === "string") {
                if (!this.additionalValues.includes(options.addValue) && options.addValue !== this.valueName) {
                    this.additionalValues.push(options.addValue);
                    updateInfo = true;
                }
            }

            if (typeof options.removeValue === "string") {
                if (this.additionalValues.includes(options.removeValue)) {
                    this.additionalValues.splice(this.additionalValues.indexOf(options.removeValue), 1);
                    updateInfo = true;
                    redraw = true;
                }
            }

            if (options.style) {
                updateInfo = true;
            }

            if (typeof options.unit !== "undefined" && this.unit !== options.unit) {
                updateInfo = true;
            }

            if (updateInfo) {
                updateScaleInfo(this);
            }

            if (redraw) {
                this.draw();
            }
        }
        /**
         * draw scale
         *
         */
        draw() {
            if (!this.parent) {
                return;
            }
            this.context.clearRect(0, 0, this.width + 2, this.height + 2);

            this.context.lineWidth = this.lineWidth;
            this.context.strokeStyle = this.strokeStyle;
            const widthOffset = this.lineWidth / 2;

            this.context.beginPath();
            if (this.mirrored) {
                this.context.moveTo(1 + widthOffset, 1);
                this.context.lineTo(1 + widthOffset, this.height + 1);
            } else {
                this.context.moveTo(this.width + 1 - widthOffset, 1);
                this.context.lineTo(this.width + 1 - widthOffset, this.height + 1);
            }

            const targetSteps = Math.max(1, Math.min(this.numSteps, Math.floor(this.height / stepSize)));

            const stepInfo = getRangeInfo(this.min, this.max, targetSteps),
                stepDigits = getDigits(stepInfo.step),
                numMarkers = toDigits(stepInfo.range / stepInfo.step, stepDigits) + 1,
                scale = this.height / (this.max - this.min),
                stepDistance = stepInfo.step * scale;

            const markerLog = [];
            for (let i = 0; i < numMarkers && i < this.markers.length; i++) {
                const markerValue = toDigits(stepInfo.start + (i * stepInfo.step), stepDigits);
                if (this.min <= markerValue && markerValue <= this.max) {
                    const y = drawScaleMarker(this, scale, markerValue, this.width);

                    drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);

                    if (y > this.scaleInfoOffset + this.markerHeight) {
                        this.markers[i].textContent = toDigits(markerValue, stepDigits);
                        this.markers[i].style.top = `${y - 1}px`;
                        this.markers[i].style.display = "block";
                    } else {
                        this.markers[i].style.display = "none";
                    }
                } else {
                    if (i === 0) {
                        drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);
                    }
                    this.markers[i].style.display = "none";
                }
            }
            this.markers.slice(numMarkers, this.markers.length).forEach((marker) => {
                marker.style.display = "none";
            });
            this.context.stroke();
            log(`Scale Markers: ${markerLog.join(", ")}`);
        }
        /**
         * remove scale
         *
         */
        remove() {
            if (this.parent) {
                this.observer.disconnect();
                this.parent.removeChild(this.wrapper);
                this.parent = null;
            }
        }
    };
}());

/**
 * Module to implement scale-group to associate multiple values with single scale
 *
 * @module visuals/controls/iqtrend/scalegroup
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.scalegroup",
        /** @lends module:visuals/controls/iqtrend/scalegroup */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update limits of scale-group
     *
     * @param {object} self instance reference
     */
    function updateLimits(self) {
        const { min, max } = self.getCurrentLimits();

        Object.keys(self.values).forEach((name) => {
            self.values[name].dataset.graph.setOptions({
                y: {
                    min,
                    max
                }
            });
            self.values[name].dataset.scale.setOptions({
                y: {
                    min,
                    max
                }
            });
        });
    }

    /**
     * set limit options for member scale
     *
     * @param {object} self instance reference
     * @param {string} name name of value
     * @param {object} options limit options
     */
    function setLimitOption(self, name, options) {
        if (!self.limits[name]) {
            self.limits[name] = {
                min: null,
                max: null
            };
        }
        if (typeof options.y.min === "number") {
            self.limits[name].min = options.y.min;
        }
        if (typeof options.y.max === "number") {
            self.limits[name].max = options.y.max;
        }
    }

    module.ScaleGroup = class ScaleGroup {
        /**
         * Creates an instance of ScaleGroup.
         * @param {number} groupId scale-group ID
         * @param {object} trendWidget iq-trend reference
         * @param {object} [initialDataset=null] initial dataset reference
         */
        constructor(groupId, trendWidget, initialDataset = null) {
            this.groupId = groupId;
            this.trendWidget = trendWidget;
            this.values = {};
            if (initialDataset) {
                this.values[initialDataset.name] = {
                    name: initialDataset.name,
                    dataset: initialDataset
                };
            }
            this.master = initialDataset ? this.values[initialDataset.name] : null;
            this.limits = {};
        }
        /**
         * add value dataset to scale-group
         *
         * @param {string} name name of value
         * @param {object} dataset dataset reference
         */
        addValue(name, dataset) {
            if (!this.values[name]) {
                this.values[name] = {
                    name,
                    dataset
                };
                if (!this.master) {
                    this.master = this.values[name];
                }
                this.master.dataset.scale.setOptions({
                    addValue: name
                });
                if (dataset !== this.master.dataset && dataset.scale) {
                    dataset.scale.setOptions({
                        hidden: true
                    });
                }
            }
        }
        /**
         * remove value from scale-group
         *
         * @param {string} name name of value
         */
        removeValue(name) {
            if (this.values[name]) {
                const wasMaster = this.values[name] === this.master;
                delete this.values[name];
                delete this.limits[name];
                if (wasMaster) {
                    this.master.dataset.scale.additionalValues.slice().forEach((addValue) => {
                        this.master.dataset.scale.setOptions({
                            removeValue: addValue
                        });
                    });
                    const newMasterName = this.getValueNames()[0];
                    if (newMasterName) {
                        //update master reference
                        this.master = this.values[newMasterName];
                        Object.keys(this.values).forEach((valueName) => {
                            if (valueName !== this.master.name) {
                                this.master.dataset.scale.setOptions({
                                    addValue: valueName
                                });
                            }
                        });
                        this.master.dataset.scale.setOptions({
                            hidden: false
                        });
                    } else {
                        this.master = null;
                    }
                } else if (this.master) {
                    this.master.dataset.scale.setOptions({
                        removeValue: name
                    });
                }
                updateLimits(this);
            }
        }
        /**
         * get value limits
         *
         * @return {object} value limits (min, max)
         */
        getValueLimits() {
            const valueSettings = this.trendWidget.vars.datasource.source.getValueSettings();
            let min = null,
                max = null;

            this.getValueNames().forEach((name) => {
                const vs = valueSettings[name];
                if (vs && typeof vs.min === "number") {
                    if (min === null || min > vs.min) {
                        min = vs.min;
                    }
                }

                if (vs && typeof vs.max === "number") {
                    if (max === null || max < vs.max) {
                        max = vs.max;
                    }
                }
            });

            return {
                min,
                max
            };
        }
        /**
         * get current scale-group limits (including zoom)
         *
         * @return {object} scale-group limits (min, max)
         */
        getCurrentLimits() {
            let min = null,
                max = null;

            Object.keys(this.limits).forEach((name) => {
                if (min === null || this.limits[name].min < min) {
                    min = this.limits[name].min;
                }
                if (max === null || this.limits[name].max > max) {
                    max = this.limits[name].max;
                }
            });

            return {
                min,
                max
            };
        }
        /**
         * get value names of scale-group
         *
         * @return {string[]} value names
         */
        getValueNames() {
            return Object.keys(this.values);
        }

        /**
         * set scale-group options
         *
         * @param {object} options scale options
         * @param {string|null} name name of value or `null` to target all
         */
        setOptions(options, name) {
            if (options.y) {
                if (name === null) {
                    this.getValueNames().forEach((valueName) => {
                        setLimitOption(this, valueName, options);
                    });
                } else {
                    setLimitOption(this, name, options);
                }
                updateLimits(this);
            }
        }
    };
}());

/**
 * Module to implement task-queue for trend data operations
 *
 * @module visuals/controls/iqtrend/taskqueue
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.taskqueue",
        /** @lends module:visuals/controls/iqtrend/taskqueue */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    const states = shmi.requires("visuals.controls.iqtrend.common").taskStates;

    /**
     * remove task that are either completed or canceled from queue
     *
     * @param {object} self instance reference
     */
    function cleanupTasks(self) {
        self._tasks = self._tasks.filter((task) => task.state === states.CREATED || task.state === states.RUNNING);
    }

    /**
     * check queue for tasks and execute next available task
     *
     * @param {object} self instance reference
     * @return {Promise} promise resolving when task is completed
     */
    function checkTasks(self) {
        cleanupTasks(self);
        notifyWatchers(self);

        const nextTask = self._tasks.find((task) => task.state === states.CREATED);

        return nextTask ? nextTask.run().then((result) => checkTasks(self)) : Promise.resolve;
    }

    /**
     * notify watchers of task queue with updated state
     *
     * @param {object} self instance reference
     */
    function notifyWatchers(self) {
        //..update watchers with current state
        self._watchers.forEach((watcherCallback) => {
            watcherCallback({
                source: self,
                loading: self.loading
            });
        });
    }

    module.TaskQueue = class TaskQueue {
        constructor() {
            this._tasks = [];
            this._watchers = [];
        }
        /**
         * get current loading / busy state
         *
         * @readonly
         * @type {boolean} `true` if currently loading, `false` else
         */
        get loading() {
            return !!this._tasks.length;
        }
        /**
         * get queued tasks
         *
         * @return {object[]} tasks in queue
         */
        getTasks() {
            return this._tasks;
        }
        /**
         * queue task for execution
         *
         * @param {object} task task reference
         * @return {Promise} promise resolving when task has been queued
         */
        queueTask(task) {
            cleanupTasks(this);
            this._tasks.push(task);
            if (this._tasks.length === 1) {
                notifyWatchers(this);
                return this._tasks[0].run().then(() => checkTasks(this));
            }
            return Promise.resolve();
        }
        /**
         * watch state of task queue
         *
         * @param {function} callback notification callback for state updates
         * @return {object} reference to stop watching
         */
        watch(callback) {
            this._watchers.push(callback);
            return {
                unwatch: () => {
                    const idx = this._watchers.indexOf(callback);
                    if (idx !== -1) {
                        this._watchers.splice(idx, 1);
                    }
                }
            };
        }
    };
}());

/**
 * Module to implement timeline (x-axis) for iq-trend
 *
 * @module visuals/controls/iqtrend/timeline
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.timeline",
        /** @lends module:visuals/controls/iqtrend/timeline */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update canvas size
     *
     * @param {object} self instance reference
     */
    function updateSize(self) {
        self.canvas.width = self.width + 2;
        self.canvas.height = self.height + 2;
        const style = getComputedStyle(self.parent);
        self.strokeStyle = style.getPropertyValue("--timeline-color");
    }

    /**
     * time unit definition
     * @type {object}
     */
    const time = {
        SECOND: 1000,
        MINUTE: 60000,
        HOUR: 3600000,
        DAY: 86400000,
        WEEK: 604800000,
        MONTH: 2592000000,
        YEAR: 31536000000
    };

    /**
     * default marker label definition
     * @type {object[]}
     */
    const defaultMarkerFormats = [
        {
            unit: time.YEAR,
            format: "$YYYY"
        },
        {
            unit: time.MONTH,
            format: "$DD.$MM"
        },
        {
            unit: time.WEEK,
            format: "$DD.$MM"
        },
        {
            unit: time.DAY,
            format: "$DD.$MM"
        },
        {
            unit: time.HOUR,
            format: "$HH:$mm"
        },
        {
            unit: time.MINUTE,
            format: "$HH:$mm"
        },
        {
            unit: time.SECOND,
            format: "$mm:$ss"
        },
        {
            unit: 1,
            format: "$ss.$SSS"
        }
    ];

    const defaultStartMarkerFormat = {
        time: "$HH:$mm:$ss",
        date: "$DD.$MM.$YYYY"
    };

    /**
     * get marker label format for specified step size
     *
     * @param {number} step step size
     * @param {object[]} formats marker formats
     * @return {object} marker format for requested step size
     */
    function getMarkerFormat(step, formats) {
        return formats.find((format, idx) => step % format.unit === 0 || idx === (formats.length - 1));
    }

    /**
     * get marker text for specified value
     *
     * @param {object} self instance reference
     * @param {object} stepInfo stepping info
     * @param {number} value value (timestamp)
     * @return {string} marker text
     */
    function getMarkerText(self, stepInfo, value) {
        const { formatDateTime } = shmi.requires("visuals.tools.date"),
            markerFormat = getMarkerFormat(stepInfo.step, self.markerFormats);
        return formatDateTime(value / 1000, markerFormat ? markerFormat.format : undefined);
    }

    /**
     * available step widths to choose from
     * @type {number[]}
     */
    const stepWidths = [
        1,
        5,
        10,
        25,
        50,
        100,
        250,
        500,
        time.SECOND,
        2 * time.SECOND,
        5 * time.SECOND,
        10 * time.SECOND,
        15 * time.SECOND,
        30 * time.SECOND,
        time.MINUTE,
        2 * time.MINUTE,
        5 * time.MINUTE,
        10 * time.MINUTE,
        15 * time.MINUTE,
        20 * time.MINUTE,
        30 * time.MINUTE,
        time.HOUR,
        2 * time.HOUR,
        3 * time.HOUR,
        6 * time.HOUR,
        12 * time.HOUR,
        time.DAY,
        2 * time.DAY,
        time.WEEK,
        2 * time.WEEK,
        time.MONTH,
        2 * time.MONTH,
        3 * time.MONTH,
        6 * time.MONTH,
        time.YEAR
    ];

    /**
     * get step size for specified range
     *
     * @param {number} min minimum timestamp
     * @param {number} max maximum timestamp
     * @param {number} maxNumSteps maximum number of steps
     * @return {number} step size
     */
    function getTimeStep(min, max, maxNumSteps) {
        const range = max - min;

        let step = 1;
        let numSteps = 0;
        const timeSteps = stepWidths.slice().reverse();
        for (let i = 0; i < timeSteps.length; i++) {
            const testStep = timeSteps[i];
            numSteps = range / testStep;
            if (numSteps <= maxNumSteps) {
                step = testStep;
            } else {
                break;
            }
        }

        return step;
    }

    /**
     * get range information for specified time range
     *
     * @param {number} min minimum timestamp
     * @param {number} max maximum timestamp
     * @param {number} maxNumSteps maximum number of steps
     * @return {object} range info
     */
    function getRangeInfo(min, max, maxNumSteps) {
        const step = getTimeStep(min, max, maxNumSteps),
            start = Math.floor(min / step) * step,
            endY = start + Math.ceil((max - start) / step) * step;

        return {
            step,
            start,
            range: endY - start
        };
    }

    /**
     * get x-coordinate for specified timestamp
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {number} value timestamp in ms
     * @return {number} x-coordinate on canvas
     */
    function getCoordinate(self, scale, value) {
        return (value - self.min) * scale + 1;
    }

    /**
     * draw scale marker
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {number} value timestamp in ms
     * @param {number} width marker width
     * @return {number} x-coordinate on canvas
     */
    function drawScaleMarker(self, scale, value, width) {
        const x = getCoordinate(self, scale, value);

        if (self.mirrored) {
            self.context.moveTo(x, self.height + 1);
            self.context.lineTo(x, (width) + 1);
        } else {
            self.context.moveTo(x, 1 + width);
            self.context.lineTo(x, 1);
        }

        return x;
    }

    /**
     * draw small markers between labeled markers
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {object} stepInfo stepping info
     * @param {number} stepDistance step distance
     * @param {number} markerValue value at marker
     * @param {number} index marker index
     */
    function drawSmallMarkers(self, scale, stepInfo, stepDistance, markerValue, index) {
        if (stepDistance > 100) {
            const smallDiff = stepInfo.step / 4;
            if (index === 0) {
                for (let j=0; j<3; j++) {
                    const smallMarkerValue = markerValue - (j + 1) * smallDiff;
                    if (smallMarkerValue >= self.min) {
                        drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
                    }
                }
            }
            for (let j=0; j<3; j++) {
                const smallMarkerValue = markerValue + (j + 1) * smallDiff;
                drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
            }
        } else if (stepDistance > 75) {
            if (index === 0) {
                const smallMarkerValue = markerValue - stepInfo.step / 2;
                if (smallMarkerValue >= self.min) {
                    drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
                }
            }

            {
                const smallMarkerValue = markerValue + stepInfo.step / 2;
                drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
            }
        }
    }

    /**
     * update start marker texts
     *
     * @param {object} self instance reference
     */
    function updateStartMarker(self) {
        const { formatDateTime } = shmi.requires("visuals.tools.date");

        self.startDate.textContent = formatDateTime(self.min / 1000, self.startMarkerFormat.date);
        self.startTime.textContent = formatDateTime(self.min / 1000, self.startMarkerFormat.time);
    }

    module.Timeline = class Timeline {
        /**
         * Creates an instance of Timeline.
         * @param {HTMLElement} element parent element to attach timeline to
         * @param {object} {
         *             min = 0,
         *             max = 100,
         *             numSteps = 21,
         *             mirrored = false,
         *             strokeStyle = "rgb(0, 0, 0)",
         *             markerFormats,
         *             startMarkerFormat = {
         *                 time: "$HH:$mm:$ss",
         *                 date: "$DD.$MM.$YYYY"
         *             }
         *         } timeline options
         */
        constructor(element, {
            min = 0,
            max = 100,
            numSteps = 21,
            mirrored = false,
            markerFormats,
            startMarkerFormat
        }, updateCallback) {
            this.canvas = document.createElement("canvas");
            this.parent = element;
            this.canvas.classList.add("timeline-canvas");
            this.updateCallback = typeof updateCallback === "function" ? updateCallback : () => {};

            this.mirrored = mirrored;
            this.markerFormats = Array.isArray(markerFormats) ? markerFormats : defaultMarkerFormats;
            this.markerFormats.forEach((markerFormat) => {
                markerFormat.format = shmi.localize(markerFormat.format);
            });
            this.startMarkerFormat = startMarkerFormat && typeof startMarkerFormat.time === "string" && typeof startMarkerFormat.date === "string" ? startMarkerFormat : defaultStartMarkerFormat;
            this.startMarkerFormat.date = shmi.localize(this.startMarkerFormat.date);
            this.startMarkerFormat.time = shmi.localize(this.startMarkerFormat.time);

            let tmpMaxFormat = "";
            this.markerFormats.forEach((format) => {
                if (format.format.length > tmpMaxFormat.length) {
                    tmpMaxFormat = format.format;
                }
            });
            this.maxMarkerFormat = tmpMaxFormat;

            this.markers = [];
            for (let i = 0; i < numSteps + 1; i++) {
                const marker = document.createElement("div");
                marker.classList.add("timeline-label");
                marker.style.display = "none";
                this.parent.appendChild(marker);
                this.markers.push(marker);
            }

            this.startMarker = document.createElement("div");
            this.startMarker.classList.add("timeline-start");

            this.startDate = document.createElement("div");
            this.startDate.classList.add("start-date");
            this.startTime = document.createElement("div");
            this.startTime.classList.add("start-time");
            this.startMarker.appendChild(this.startDate);
            this.startMarker.appendChild(this.startTime);

            this.parent.appendChild(this.canvas);
            if (this.mirrored) {
                this.parent.classList.add("mirrored");
            } else {
                this.parent.classList.remove("mirrored");
            }
            this.parent.appendChild(this.startMarker);

            const { getFontStyle } = shmi.requires("visuals.controls.iqtrend.common");
            this.fontStyle = getFontStyle(this.markers[0]);
            const { getTextDimensions } = shmi.requires("visuals.controls.iqtrend.common");
            this.markerWidth = Math.ceil((Math.max(65, getTextDimensions(this.maxMarkerFormat, this.fontStyle).width) + 10) / 20) * 20;

            {
                const parentRect = this.parent.getBoundingClientRect();
                this.width = parentRect.width - 2;
                this.height = this.canvas.offsetHeight - 2;
                const startMarkerRect = this.startMarker.getBoundingClientRect();
                this.startMarkerOffset = (startMarkerRect.left - parentRect.left) + startMarkerRect.width;
            }

            this.min = min;
            this.max = max;
            this.numSteps = numSteps;

            updateSize(this);
            updateStartMarker(this);

            this.context = this.canvas.getContext("2d");
            this.observer = new ResizeObserver((entries) => {
                const entry = entries[0];
                this.width = entry.contentRect.width - 2;
                this.height = this.canvas.offsetHeight - 2;
                const parentRect = this.parent.getBoundingClientRect(),
                    startMarkerRect = this.startMarker.getBoundingClientRect();
                this.startMarkerOffset = (startMarkerRect.left - parentRect.left) + startMarkerRect.width;
                updateSize(this);
                this.draw();
            });

            this.observer.observe(this.parent);
            this.draw();
        }
        /**
         * get timeline dimensions (min, max)
         *
         * @return {object} timeline dimensions
         */
        getDimensions() {
            return {
                min: this.min,
                max: this.max
            };
        }
        /**
         * get scale factor
         *
         * @return {number} scale factor
         */
        getScale() {
            return this.width / (this.max - this.min);
        }
        /**
         * set timeline options
         *
         * @param {object} options timeline options
         */
        setOptions(options) {
            if (options.x) {
                if (typeof options.x.min === "number") {
                    this.min = options.x.min;
                }
                if (typeof options.x.max === "number") {
                    this.max = options.x.max;
                }
                this.updateCallback({ min: this.min, max: this.max });
                this.draw();
            }
        }
        /**
         * draw timeline
         */
        draw() {
            if (!this.parent) {
                return;
            }
            this.context.clearRect(0, 0, this.width + 2, this.height + 2);

            this.context.lineWidth = 1;
            this.context.strokeStyle = this.strokeStyle;
            this.context.beginPath();
            if (this.mirrored) {
                this.context.moveTo(1, this.height + 0.5);
                this.context.lineTo(this.width + 1, this.height + 0.5);
            } else {
                this.context.moveTo(1, 1.5);
                this.context.lineTo(this.width + 1, 1.5);
            }

            const targetSteps = Math.max(1, Math.min(this.numSteps, Math.floor(this.width / this.markerWidth)));
            const stepInfo = getRangeInfo(this.min, this.max, targetSteps),
                numMarkers = stepInfo.range / stepInfo.step + 1,
                scale = this.getScale(),
                stepDistance = stepInfo.step * scale;

            for (let i = 0; i < numMarkers && i < this.markers.length; i++) {
                const markerValue = stepInfo.start + (i * stepInfo.step);
                if (markerValue >= this.min && markerValue <= this.max) {
                    const x = drawScaleMarker(this, scale, markerValue, this.height);

                    drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);

                    if (x > this.startMarkerOffset + (this.markerWidth / 2)) {
                        this.markers[i].textContent = getMarkerText(this, stepInfo, markerValue);
                        this.markers[i].style.left = `${x - (this.markerWidth / 2)}px`;
                        this.markers[i].style.width = `${this.markerWidth}px`;
                        this.markers[i].style.display = "block";
                    } else {
                        this.markers[i].style.display = "none";
                    }
                } else {
                    if (i === 0) {
                        drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);
                    }
                    this.markers[i].style.display = "none";
                }
            }
            this.markers.slice(numMarkers, this.markers.length).forEach((marker) => {
                marker.style.display = "none";
            });

            this.context.stroke();
            updateStartMarker(this);
        }
        /**
         * remove timeline
         *
         */
        remove() {
            if (this.parent) {
                this.observer.disconnect();
                this.parent.removeChild(this.canvas);
                this.markers.forEach((marker) => {
                    this.parent.removeChild(marker);
                });
                this.parent.removeChild(this.startMarker);
                this.parent = null;
            }
        }
    };
}());

/**
 * Module to implement trend data operation to queue in TaskQueue.
 *
 * @module visuals/controls/iqtrend/trendtask
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.trendtask",
        /** @lends module:visuals/controls/iqtrend/trendtask */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    const states = shmi.requires("visuals.controls.iqtrend.common").taskStates;

    module.TrendTask = class TrendTask {
        /**
         * Creates an instance of TrendTask.
         * @param {string} type type of task
         * @param {object} parameters task execution parameters
         * @param {function} asyncTaskFunction function that performs task
         */
        constructor(type, parameters, asyncTaskFunction) {
            this._type = type;
            this._parameters = parameters;
            this._state = states.CREATED;
            this._taskFunction = asyncTaskFunction;
            this._result = null;
            this._error = null;
        }
        /**
         * get current state of task
         *
         */
        get state() {
            return this._state;
        }
        /**
         * set state of task
         *
         */
        set state(state) {
            this._state = state;
        }
        /**
         * get task type
         *
         * @readonly
         */
        get type() {
            return this._type;
        }
        /**
         * get task parameters
         *
         * @readonly
         */
        get parameters() {
            return this._parameters;
        }
        /**
         * get task result
         *
         * @readonly
         */
        get result() {
            return this._result;
        }
        /**
         * get task error information
         *
         * @readonly
         */
        get error() {
            return this._error;
        }
        /**
         * run task
         *
         * @return {Promise} promise resolving when task has been completed
         */
        async run() {
            if (this._state !== states.CREATED) {
                throw new Error("Tried to run task that is not in initial 'CREATED' state.");
            }
            this._state = states.RUNNING;
            try {
                this._result = await this._taskFunction(this._parameters, this);
                this._state = states.DONE;
            } catch (err) {
                this._error = err;
                console.error("Error executing task:", err, this.type, this.parameters);
                this._state = states.ERROR;
            }

            return this._result;
        }
    };
}());

/**
 * Module to implement numeric x-axis for iq-trend.
 *
 * @module visuals/controls/iqtrend/xaxis
 */
(function() {
    'use strict';

    /** replace package- & module-names **/
    const MODULE_NAME = "visuals.controls.iqtrend.xaxis",
        /** @lends module:visuals/controls/iqtrend/xaxis */
        module = shmi.pkg(MODULE_NAME),
        ENABLE_LOG = false;

    /**
     * initial distance between labeled steps
     * @type {number}
     */
    let stepSize = 75;

    function log() {
        if (ENABLE_LOG) {
            console.log.apply(console, arguments);
        }
    }

    /**
     * update canvas size
     *
     * @param {object} self instance reference
     */
    function updateSize(self) {
        self.canvas.width = self.width + 2;
        self.canvas.height = self.height + 2;
        const style = getComputedStyle(self.parent);
        self.strokeStyle = style.getPropertyValue("--timeline-color");
    }

    /**
     * available steps between labeled values
     * @type {number[]}
     */
    const stepWidths = [
        1,
        0.5,
        0.25,
        0.2
    ];

    /**
     * convert value to specified number of decimal digits
     *
     * @param {number} value input value
     * @param {number} digits maximum number of decimal digits
     * @return {number} converted value
     */
    function toDigits(value, digits) {
        return parseFloat(value.toFixed(digits));
    }

    /**
     * get step size for specified range
     *
     * @param {number} min minimum x-axis value
     * @param {number} max maximum x-axis value
     * @param {number} maxNumSteps maximum number of steps
     * @return {number} step size
     */
    function getSimpleStep(min, max, maxNumSteps) {
        const range = max - min;
        let order = Math.ceil(Math.log10(range));

        let step = 1;
        let numSteps = 0;
        while (numSteps < maxNumSteps) {
            for (let i = 0; i < stepWidths.length; i++) {
                const testStep = stepWidths[i] * (10 ** order);
                numSteps = range / testStep;
                if (numSteps <= maxNumSteps) {
                    step = testStep;
                } else {
                    break;
                }
            }
            order -= 1;
        }

        return step;
    }
    /**
     * get range info for specified value range
     *
     * @param {number} min minimum x-axis value
     * @param {number} max maximum x-axis value
     * @param {number} maxNumSteps maximum number of steps
     * @return {object} range info
     */
    function getRangeInfo(minY, maxY, maxNumSteps) {
        const step = getSimpleStep(minY, maxY, maxNumSteps),
            digits = getDigits(step),
            startY = Math.floor(minY / step) * step,
            endY = startY + Math.ceil((maxY - startY) / step) * step;

        return {
            step: toDigits(step, digits),
            start: toDigits(startY, digits),
            range: toDigits(endY - startY, digits)
        };
    }

    /**
     * get number of decimal digits
     *
     * @param {number} value input value
     * @return {number} number of decimal digits
     */
    function getDigits(value) {
        const valueString = String(value);
        let valueDigits = valueString.indexOf(".");
        if (valueDigits !== -1) {
            valueDigits = valueString.length - valueDigits - 1;
        } else {
            valueDigits = 0;
        }
        return valueDigits;
    }

    /**
     * get x-coordinate for specified value
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {number} value input value
     * @return {number} x-coordinate
     */
    function getCoordinate(self, scale, value) {
        return (value - self.min) * scale + 1;
    }

    /**
     * draw scale marker
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {number} value input value
     * @param {number} width marker width
     * @return {number} x-coordinate
     */
    function drawScaleMarker(self, scale, value, width) {
        const x = getCoordinate(self, scale, value);

        if (self.mirrored) {
            self.context.moveTo(x, self.height + 1);
            self.context.lineTo(x, (width) + 1);
        } else {
            self.context.moveTo(x, 1 + width);
            self.context.lineTo(x, 1);
        }

        return x;
    }

    /**
     * draw small markers between labeled markers
     *
     * @param {object} self instance reference
     * @param {number} scale scale factor
     * @param {object} stepInfo stepping info
     * @param {number} stepDistance step distance
     * @param {number} markerValue value at marker
     * @param {number} index marker index
     */
    function drawSmallMarkers(self, scale, stepInfo, stepDistance, markerValue, index) {
        if (stepDistance > 100) {
            const smallDiff = stepInfo.step / 4;
            if (index === 0) {
                for (let j=0; j<3; j++) {
                    const smallMarkerValue = markerValue - (j + 1) * smallDiff;
                    if (smallMarkerValue >= self.min) {
                        drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
                    }
                }
            }
            for (let j=0; j<3; j++) {
                const smallMarkerValue = markerValue + (j + 1) * smallDiff;
                drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
            }
        } else if (stepDistance > 75) {
            if (index === 0) {
                const smallMarkerValue = markerValue - stepInfo.step / 2;
                if (smallMarkerValue >= self.min) {
                    drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
                }
            }

            {
                const smallMarkerValue = markerValue + stepInfo.step / 2;
                drawScaleMarker(self, scale, smallMarkerValue, self.height - 2);
            }
        }
    }

    module.XAxis = class XAxis {
        /**
         * Creates an instance of XAxis.
         * @param {HTMLElement} element parent element to attach x-axis to
         * @param {object} {
         *             min = 0,
         *             max = 100,
         *             numSteps = 21,
         *             mirrored = false,
         *             strokeStyle = "rgb(0, 0, 0)"
         *         } x-axis options
         */
        constructor(element, {
            min = 0,
            max = 100,
            numSteps = 21,
            mirrored = false
        }, updateCallback) {
            this.canvas = document.createElement("canvas");
            this.wrapper = document.createElement("div");
            this.wrapper.classList.add("xaxis-wrapper");
            this.parent = element;
            this.canvas.classList.add("xaxis-canvas");
            this.updateCallback = typeof updateCallback === "function" ? updateCallback : () => {};

            this.mirrored = mirrored;
            if (this.mirrored) {
                this.wrapper.classList.add("mirrored");
            }

            this.markers = [];
            for (let i = 0; i < numSteps + 1; i++) {
                const marker = document.createElement("div");
                marker.classList.add("xaxis-label");
                marker.style.display = "none";
                this.wrapper.appendChild(marker);
                this.markers.push(marker);
            }

            this.wrapper.appendChild(this.canvas);
            this.parent.appendChild(this.wrapper);

            const { getFontStyle } = shmi.requires("visuals.controls.iqtrend.common");
            this.fontStyle = getFontStyle(this.markers[0]);

            const rect = this.parent.getBoundingClientRect();
            this.width = rect.width - 2;
            this.height = this.canvas.offsetHeight - 2;

            this.min = min;
            this.max = max;
            this.numSteps = numSteps;

            updateSize(this);
            this.context = this.canvas.getContext("2d");
            this.observer = new ResizeObserver((entries) => {
                const entry = entries[0];
                this.width = entry.contentRect.width - 2;
                this.height = this.canvas.offsetHeight - 2;
                updateSize(this);
                this.draw();
            });

            this.observer.observe(this.parent);
            this.draw();
        }
        /**
         * set x-axis options
         *
         * @param {object} options x-axis options
         */
        setOptions(options) {
            if (options.x) {
                if (typeof options.x.min === "number") {
                    this.min = options.x.min;
                }
                if (typeof options.x.max === "number") {
                    this.max = options.x.max;
                }
                this.updateCallback({ min: this.min, max: this.max });
                this.draw();
            }
        }
        /**
         * draw x-axis
         *
         */
        draw() {
            if (!this.parent) {
                return;
            }
            this.context.clearRect(0, 0, this.width + 2, this.height + 2);

            this.context.lineWidth = 1;
            this.context.strokeStyle = this.strokeStyle;
            this.context.beginPath();
            if (this.mirrored) {
                this.context.moveTo(1, this.height + 0.5);
                this.context.lineTo(this.width + 1, this.height + 0.5);
            } else {
                this.context.moveTo(1, 1.5);
                this.context.lineTo(this.width + 1, 1.5);
            }
            const { getTextDimensions } = shmi.requires("visuals.controls.iqtrend.common");
            stepSize = Math.max(65, getTextDimensions(this.min, this.fontStyle).width, getTextDimensions(this.max, this.fontStyle).width) + 10;
            const targetSteps = Math.max(1, Math.min(this.numSteps, Math.floor(this.width / stepSize)));

            const stepInfo = getRangeInfo(this.min, this.max, targetSteps),
                stepDigits = getDigits(stepInfo.step),
                numMarkers = toDigits(stepInfo.range / stepInfo.step, stepDigits) + 1,
                scale = this.width / (this.max - this.min),
                stepDistance = stepInfo.step * scale;

            for (let i = 0; i < numMarkers && i < this.markers.length; i++) {
                const markerValue = toDigits(stepInfo.start + (i * stepInfo.step), stepDigits);
                if (markerValue >= this.min && markerValue <= this.max) {
                    const x = drawScaleMarker(this, scale, markerValue, this.height);
                    drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);

                    this.markers[i].textContent = toDigits(markerValue, stepDigits);
                    this.markers[i].style.left = `${x - (stepSize / 2)}px`;
                    this.markers[i].style.width = `${stepSize}px`;
                    this.markers[i].style.display = "block";
                } else {
                    if (i === 0) {
                        drawSmallMarkers(this, scale, stepInfo, stepDistance, markerValue, i);
                    }
                    this.markers[i].style.display = "none";
                }
            }
            this.markers.slice(numMarkers, this.markers.length).forEach((marker) => {
                marker.style.display = "none";
            });
            this.context.stroke();
        }
        /**
         * get x-axis dimensions
         *
         * @return {object} dimensions (min, max)
         */
        getDimensions() {
            return {
                min: this.min,
                max: this.max
            };
        }
        /**
         * get scale factor
         *
         * @return {number} scale factor
         */
        getScale() {
            return this.width / (this.max - this.min);
        }
        /**
         * remove x-axis
         *
         */
        remove() {
            if (this.parent) {
                this.observer.disconnect();
                this.parent.removeChild(this.wrapper);
                this.parent = null;
            }
        }
    };
}());

/**
 * WebIQ visuals control template.
 *
 * Configuration options (default):
 *
 * {
 *      'class-name': 'user-info',
 *      'name': null,
 *      'template': 'custom/controls/user-info',
 *      'label': uiType,
 *      'show-name': false,
 *      'show-group': false
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = 'iqUserInfo', // control name in camel-case
        uiType = 'iq-user-info', // control keyword (data-ui)
        isContainer = false;

    // default configuration
    const defConfig = {
        'class-name': 'iq-user-info',
        'name': null,
        'template': 'default/iq-user-info.variant-01',
        'label': "[Label]",
        'show-name': false,
        'show-group': false,
        'action': null
    };

    // setup module-logger
    const ENABLE_LOGGING = true,
        RECORD_LOG = false,
        logger = shmi.requires('visuals.tools.logging').createLogger(uiType, ENABLE_LOGGING, RECORD_LOG),
        fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START
    /**
     * Displays current user
     *
     * @param  {object} self
     */
    function displayUser(self) {
        const domElName = shmi.getUiElement('user-info-name', self.element);

        // Abort if HTML element is missing
        if (!domElName) {
            fLog('DOM-Element "user-info-name" not found!');
            return;
        }

        // Check show-name property and set to false if undefined
        if (!self.config['show-name']) {
            self.config['show-name'] = false;
        }
        // Show username or name and surname if available
        if (self.config['show-name'] === false) {
            domElName.textContent = self.vars.user.username;
        } else if (self.config['show-name'] === true) {
            if (self.vars.user.name && self.vars.user.surname) {
                domElName.textContent = self.vars.user.name + ' ' + self.vars.user.surname;
            } else if (!self.vars.user.name) {
                domElName.textContent = self.vars.user.surname;
            } else if (!self.vars.user.surname) {
                domElName.textContent = self.vars.user.name;
            } else {
                domElName.textContent = 'First Name and/or Last Name not set!';
            }
        }
    }

    /**
     * Deletes Designer-usergroups and displays extant usergroup
     *
     * @param  {object} self
     */
    function setUserGroup(self) {
        let domElGroup = shmi.getUiElement('user-info-group', self.element),
            primUserGrp = null,
            { userGroups } = self.vars.user;

        // Abort if HTML element is missing
        if (!domElGroup) {
            fLog('Dom-Element "user-info-group" not found!');
            return;
        }

        // check for internal groups and delete from list
        userGroups = userGroups.filter((group) => group.indexOf('@') !== 0);

        // add default in case of no matching group
        if (userGroups.length > 0) {
            primUserGrp = userGroups[0];
        } else {
            primUserGrp = 'Default';
        }
        domElGroup.textContent = primUserGrp;
    }
    /**
     * Gets userdata (name and surname) from connect and calls setUser()
     * @param  {object} self
     */
    function getUser(self) {
        const { username: curUserName } = self.vars.user;

        self.imports.request('user.list', {}, function(response, err) {
            if (response && Array.isArray(response.users)) {
                const userdata = response.users.find(({ username }) => username === curUserName);

                if (!userdata) {
                    fLog('User not found in remote user list');
                    return;
                }

                self.vars.user.name = userdata.first_name;
                self.vars.user.surname = userdata.last_name;

                displayUser(self);
            } else {
                fLog('[UserManager] could not retrieve userlist:', err.category, err.errc, err.message);
                fLog('User name and surname cannot be displayed!');
            }
        });
    }
    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        configSchema: null,
        /* instance variables */
        vars: {
            user: {
                userGroups: null,
                name: null,
                surname: null,
                username: null
            },
            action: null,
            listeners: []
        },
        /* imports added at runtime */
        imports: {
            request: 'visuals.tools.connect.request'
        },

        /* array of custom event types fired by this control */
        events: ['click'],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            onInit: function() {
                const self = this;
                if (self.config.action) {
                    self.vars.action = new shmi.visuals.core.UiAction(self.config.action, self);
                }

                if (Array.isArray(self.config.action) && self.config.action.length > 0) {
                    shmi.addClass(self.element, 'clickable');
                } else {
                    shmi.removeClass(self.element, 'clickable');
                }

                const eventFunctions = {
                    onClick: function(x, y, e) {
                        self.fire('click', {
                            x: x,
                            y: y,
                            event: e
                        });

                        if (self.vars.action) {
                            self.vars.action.execute();
                        }
                    }
                };

                self.vars.listeners.push(new shmi.visuals.io.MouseListener(self.element, eventFunctions));
                self.vars.listeners.push(new shmi.visuals.io.TouchListener(self.element, eventFunctions));
            },
            /* called when control is enabled */
            onEnable: function() {
                const self = this;
                self.vars.user.userGroups = shmi.visuals.session.UserManager.currentUser.groupList;
                self.vars.user.username = shmi.visuals.session.UserManager.currentUser.name;

                getUser(self);
                // displayUser() called from getUser
                if (self.config['show-group'] === true) {
                    setUserGroup(self);
                }

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            onDisable: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                shmi.addClass(self.element, 'locked');
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                const self = this;

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                shmi.removeClass(self.element, 'locked');
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires('visuals.tools.control-generator');
    cg.generate(definition);
})();

/**
 * Implementation of the "Add or Edit User" dialog used by the
 * `user-controller` widget.
 */
(function() {
    'use strict';

    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME),
        /** Name of the ui controller */
        CONTROLLER_NAME = "iq-add-user-dialog";

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${MODULE_NAME}]`)
    };

    /**
     * Closes the dialog and resolves the dialog promise exactly once.
     *
     * @param {*} controllerState State of the controller.
     * @param {*} result Result to resolve the promise with.
     */
    function closeDialog(controllerState, result) {
        if (controllerState.active && !controllerState.data.isDone) {
            controllerState.data.isDone = true;
            controllerState.getInstance("dlgBox").hide();
            controllerState.data.resolve(result);
        }
    }

    /**************************
     * EVENT HANDLERS | CLICK *
     **************************/
    const buttonHandlers = {
        /**
         * Click handler for the "Create User" button.
         *
         * @param {*} controllerState State of the controller.
         * @returns {Promise<void>}
         */
        onCreateUser: function onCreateUser(controllerState) {
            const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                { setLockedSlots, setUnlockedSlots } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

            if (!controllerState.active) {
                throw new shmi.LocalizedError("${ui.error.dialogAlreadyClosed}");
            } else if (typeof controllerState.data.formData.inputUsername !== "string" || controllerState.data.formData.inputUsername.length === 0) {
                throw new shmi.LocalizedError("${iq-user-controller.error.usernameMustNotBeEmpty}");
            } else if (controllerState.data.formData.inputUsername.startsWith(" ") || controllerState.data.formData.inputUsername.endsWith(" ") || controllerState.data.formData.inputUsername.includes("  ")) {
                throw new shmi.LocalizedError("${iq-user-controller.error.usernameInvalid}");
            } else if (typeof controllerState.data.formData.inputPassword !== "string" || controllerState.data.formData.inputPassword.length === 0) {
                throw new shmi.LocalizedError("${iq-user-controller.error.passwordMustNotBeEmpty}");
            } else if (controllerState.data.formData.inputPassword !== controllerState.data.formData.inputPasswordRep) {
                throw new shmi.LocalizedError("${V_PASSWORD_REPEAT_NOT_EQUAL}");
            } else if (controllerState.data.formData.inputPassword.length < shmi.c("MIN_PASSWORD_LENGTH")) {
                throw new shmi.LocalizedError("${V_PASSWORD_TOO_SHORT}");
            }

            const username = controllerState.data.formData.inputUsername,
                password = controllerState.data.formData.inputPassword,
                additionalProperties = {
                    firstName: controllerState.data.formData.inputFirstName,
                    lastName: controllerState.data.formData.inputLastName,
                    userGroups: controllerState.data.formData.inputUserGroup ? [controllerState.data.formData.inputUserGroup] : [],
                    localeId: controllerState.data.formData.inputLocale,
                    passwordValidityDuration: controllerState.data.formData.inputPasswordValidityDuration,
                    autoLogoutDelay: controllerState.data.formData.inputAutoLogoutDelay,
                    description: controllerState.data.formData.inputDescription
                };

            setUnlockedSlots(controllerState, []);

            return UserApiBinding.createUser(username, password, additionalProperties).
                then(closeDialog.bind(null, controllerState, true)).
                catch((reason) => {
                    setLockedSlots(controllerState, []);
                    throw reason;
                });
        },
        /**
         * Click handler for the "apply" button. Applies changes to an existing user.
         *
         * @param {*} controllerState State of the controller.
         * @returns {Promise<void>}
         */
        onApplyUser: function onApplyUser(controllerState) {
            const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                { setLockedSlots, setUnlockedSlots } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

            if (!controllerState.active) {
                throw new shmi.LocalizedError("${ui.error.dialogAlreadyClosed}");
            } else if (typeof controllerState.data.formData.inputUsername !== "string" || controllerState.data.formData.inputUsername.length === 0) {
                throw new shmi.LocalizedError("${iq-user-controller.error.usernameMustNotBeEmpty}");
            } else if (controllerState.data.formData.inputUsername.startsWith(" ") || controllerState.data.formData.inputUsername.endsWith(" ") || controllerState.data.formData.inputUsername.includes("  ")) {
                throw new shmi.LocalizedError("${iq-user-controller.error.usernameInvalid}");
            } else if (controllerState.data.formData.inputPassword !== controllerState.data.formData.inputPasswordRep) {
                throw new shmi.LocalizedError("${iq-user-controller.error.passwordMismatch}");
            }

            if (typeof controllerState.data.formData.inputPassword !== "undefined") {
                if (typeof controllerState.data.formData.inputPassword !== "string" || controllerState.data.formData.inputPassword.length === 0) {
                    throw new shmi.LocalizedError("${iq-user-controller.error.passwordMustNotBeEmpty}");
                } else if (controllerState.data.formData.inputPassword.length < shmi.c("MIN_PASSWORD_LENGTH")) {
                    throw new shmi.LocalizedError("${V_PASSWORD_TOO_SHORT}");
                }
            }

            const username = controllerState.data.formData.inputUsername,
                additionalProperties = {
                    firstName: controllerState.data.formData.inputFirstName,
                    lastName: controllerState.data.formData.inputLastName,
                    userGroups: controllerState.data.formData.inputUserGroup ? [controllerState.data.formData.inputUserGroup] : [],
                    localeId: controllerState.data.formData.inputLocale,
                    password: controllerState.data.formData.inputPassword,
                    passwordValidityDuration: controllerState.data.formData.inputPasswordValidityDuration,
                    autoLogoutDelay: controllerState.data.formData.inputAutoLogoutDelay,
                    description: controllerState.data.formData.inputDescription
                };

            setUnlockedSlots(controllerState, []);

            return UserApiBinding.setUser(username, additionalProperties).
                then(closeDialog.bind(null, controllerState, true)).
                catch((reason) => {
                    setLockedSlots(controllerState, ["inputUsername"]);
                    throw reason;
                });
        },
        /**
         * Click handler for the "close" button. Resolves the main promise.
         *
         * @param {*} controllerState State of the controller.
         * @returns {Promise<void>}
         */
        onClose: function onClose(controllerState) {
            if (!controllerState.active) {
                throw new shmi.LocalizedError("${ui.error.dialogAlreadyClosed}");
            }

            closeDialog(controllerState);
        }
    };

    /**
     * Creates the dialog.
     *
     * @param {string} [userData] If set, the forms will be populated with the
     *  given user data. If `editMode` is not `true`, the `Username` form will
     *  have a ` (2)` appended. This allows for easy cloning of existing users.
     * @param {boolean} [editMode] If set to `true`, the dialog will edit
     *  existing users instead of creating new ones. The username input field
     *  is locked in this mode.
     * @param {boolean} [hideAdmin] If set to `true`, the dialog will not allow to
     *  set any usergroup being a member of the 'admin' group
     * @param {function} resolve Forwarded `resolve` callback from a promise.
     * @param {function} reject Forwarded `reject` callback from a promise.
     */
    function createDialog(userData, editMode, hideAdmin, resolve, reject) {
        const appRoot = shmi.getControlByElement(document.body.firstElementChild),
            { makeControllerDescriptor } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

        appRoot.addControl([
            {
                "ui": "dialog-box",
                "config": {
                    "name": "iq-add-user-dialog",
                    "title": editMode ? "${iq-user-controller.title.editUserDialog}" : "${iq-user-controller.title.createUserDialog}",
                    "template": "default/dialog-box",
                    "content-template": null,
                    "class-name": `dialog-box add-or-edit-user ${editMode ? "hide-passwords" : ""}`,
                    "top-level": false,
                    "_controllers_": [
                        {
                            "name": CONTROLLER_NAME,
                            "slot": "dlgBox"
                        }
                    ]
                },
                "children": [
                    {
                        "ui": "container",
                        "config": {
                            "type": "flex",
                            "flex-orientation": "column",
                            "class-name": "container full-height"
                        },
                        "children": [
                            {
                                "ui": "container",
                                "config": {
                                    "type": "flex",
                                    "flex-orientation": "column",
                                    "class-name": "container dialog-form-container",
                                    "auto-margin": true
                                },
                                "children": [
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-02",
                                            "template": "default/iq-input-field.iq-variant-02",
                                            "label": "${user_name}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputUsername"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-02",
                                            "template": "default/iq-input-field.iq-variant-02",
                                            "label": "${iq-user-controller.label.firstName}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputFirstName"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-02",
                                            "template": "default/iq-input-field.iq-variant-02",
                                            "label": "${iq-user-controller.label.lastName}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputLastName"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-select-box",
                                        "config": {
                                            "class-name": "iq-select-box iq-variant-02",
                                            "template": "default/iq-select-box.iq-variant-02",
                                            "label": "${iq-user-controller.label.userGroup}",
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputUserGroup"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-select-box",
                                        "config": {
                                            "class-name": "iq-select-box iq-variant-02",
                                            "template": "default/iq-select-box.iq-variant-02",
                                            "label": "${iq-user-controller.label.localeId}",
                                            "options": Object.keys(shmi.visuals.session.localeInfo.locales).map((localeId) => ({ value: localeId, label: localeId })),
                                            "value": shmi.visuals.session.localeInfo.default,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputLocale"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "container",
                                        "config": {
                                            "type": "flex",
                                            "class-name": "container no-shrink",
                                            "auto-margin": true
                                        },
                                        "children": [
                                            {
                                                "ui": "iq-input-field",
                                                "config": {
                                                    "class-name": "iq-input-field iq-variant-02 flex-1",
                                                    "template": "default/iq-input-field.iq-variant-02",
                                                    "label": "${iq-user-controller.label.passwordExpiry}",
                                                    "unit-text": null,
                                                    "_controllers_": [
                                                        {
                                                            "name": CONTROLLER_NAME,
                                                            "slot": "inputPasswordValidityDuration"
                                                        }
                                                    ]
                                                }
                                            },
                                            {
                                                "ui": "iq-input-field",
                                                "config": {
                                                    "class-name": "iq-input-field iq-variant-02 flex-1",
                                                    "template": "default/iq-input-field.iq-variant-02",
                                                    "label": "${iq-user-controller.label.autoLogoutDelay}",
                                                    "unit-text": null,
                                                    "_controllers_": [
                                                        {
                                                            "name": CONTROLLER_NAME,
                                                            "slot": "inputAutoLogoutDelay"
                                                        }
                                                    ]
                                                }
                                            }
                                        ]
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-05",
                                            "template": "default/iq-input-field.iq-variant-05",
                                            "label": "${new_pwd}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputPassword"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-05",
                                            "template": "default/iq-input-field.iq-variant-05",
                                            "label": "${confirm_new_pwd}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputPasswordRep"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-02 textarea",
                                            "template": "default/iq-input-field.iq-variant-02",
                                            "label": "${iq-user-controller.label.description}",
                                            "unit-text": null,
                                            "multiline": true,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputDescription"
                                                }
                                            ]
                                        }
                                    }
                                ]
                            },
                            {
                                "ui": "container",
                                "config": {
                                    "class-name": "container dialog-buttons-container",
                                    "type": "inline",
                                    "h-alignment": "right",
                                    "auto-margin": true
                                },
                                "children": [
                                    {
                                        "ui": "iq-button",
                                        "config": {
                                            "class-name": "iq-button iq-variant-01 primary",
                                            "label": editMode ? "${iq-user-controller.label.apply}" : "${iq-user-controller.label.add}",
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "btnApply"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-button",
                                        "config": {
                                            "class-name": "iq-button iq-variant-01",
                                            "label": "${lbl_cancel}",
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "btnCancel"
                                                }
                                            ]
                                        }
                                    }
                                ]
                            }
                        ]
                    }
                ],
                "controller": makeControllerDescriptor({
                    name: CONTROLLER_NAME,
                    slots: {
                        inputUsername: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputFirstName: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputLastName: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputUserGroup: {
                            ui: "iq-select-box",
                            formType: "string"
                        },
                        inputLocale: {
                            ui: "iq-select-box",
                            formType: "string"
                        },
                        inputPasswordValidityDuration: {
                            ui: "iq-input-field",
                            formType: "number"
                        },
                        inputAutoLogoutDelay: {
                            ui: "iq-input-field",
                            formType: "number"
                        },
                        inputPassword: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputPasswordRep: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputDescription: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        btnApply: {
                            ui: "iq-button",
                            events: {
                                click: editMode ? buttonHandlers.onApplyUser : buttonHandlers.onCreateUser
                            }
                        },
                        btnCancel: {
                            ui: "iq-button",
                            events: {
                                click: buttonHandlers.onClose
                            }
                        },
                        dlgBox: {
                            ui: "dialog-box"
                        }
                    },
                    data: {
                        /** Promise reject callback. */
                        reject: reject,
                        /** Promise resolve callback. */
                        resolve: resolve,
                        /**
                         * Flag determining whether or not the dialog promise
                         * has already been resolved/rejected.
                         */
                        isDone: false,
                        formData: {}
                    },
                    onEnable: function onEnable(state) {
                        const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                            { setLockedSlots, setUnlockedSlots, reloadFormValues } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

                        setUnlockedSlots(state, ["btnCancel"]);
                        const listPromise = UserApiBinding.listGroups().then((response) => {
                            const groupMap = {};

                            response.usergroups.forEach((group) => {
                                groupMap[group.groupname] = [group.groupname];
                                group._groupdata.member_groups.forEach((mGroup) => {
                                    groupMap[group.groupname].push(mGroup);
                                });
                            });
                            state.getInstance("inputUserGroup").setOptions(
                                [{ value: null, label: "---" }].concat(
                                    response.usergroups.filter((userGroup) => {
                                        if (hideAdmin) {
                                            return !groupMap[userGroup.groupname].includes("admin");
                                        }
                                        return true;
                                    }).map((userGroup) => ({
                                        value: userGroup.groupname,
                                        label: userGroup.groupname
                                    }))
                                )
                            );
                        });

                        Promise.all([listPromise]).then(() => {
                            if (userData) {
                                state.data.formData = {
                                    inputUsername: editMode ? userData.username : null,
                                    inputFirstName: userData.firstName,
                                    inputLastName: userData.lastName,
                                    inputUserGroup: userData.userGroups[0] || null,
                                    inputLocale: userData.localeId,
                                    inputPasswordValidityDuration: userData.passwordValidityDuration,
                                    inputAutoLogoutDelay: userData.autoLogoutDelay,
                                    inputDescription: userData.description
                                };

                                reloadFormValues(state, [], []);
                            }

                            setLockedSlots(state, editMode ? ["inputUsername"] : []);
                        });
                    },
                    onDisable: function onDisable(state) {
                        if (!state.data.isDone) {
                            state.data.resolve();
                        }

                        shmi.deleteControl(state.getInstance("dlgBox"));
                    },
                    onError: function onError(state, reason) {
                        if (reason.category === "shmi:connect:api:generic" && reason.errc === 14) {
                            throw new shmi.LocalizedError("${iq-user-controller.error.invalidParameter}");
                        } else if (reason.category === "shmi:connect:api:user" && reason.errc === 4) {
                            throw new shmi.LocalizedError("${iq-user-controller.error.invalidPasswordExpiryDuration}");
                        }

                        throw reason;
                    }
                })
            }
        ], (err, controls) => {
            if (err) {
                reject(err);
            } else {
                controls[0].show();
            }
        });
    }

    /**
     * Creates an administrative dialog to add or edit users.
     *
     * @param {string} [userData] If set, the forms will be populated with the
     *  given user data. If `editMode` is not `true`, the `Username` form will
     *  have a ` (2)` appended. This allows for easy cloning of existing users.
     * @param {boolean} [editMode] If set to `true`, the dialog will edit
     *  existing users instead of creating new ones. The username input field
     *  is locked in this mode.
     * @param {boolean} [hideAdmin] If set to `true`, the dialog will not allow to
     *  set any usergroup being a member of the 'admin' group
     * @return {Promise<?boolean>}
     */
    module.openUserAddEditDialog = function openUserAddEditDialog(user, editMode, hideAdmin) {
        return new Promise(createDialog.bind(null, user, editMode, hideAdmin));
    };
}());

/**
 * @module visuals/controls/iq-user-management/controller-tools
 */
(function() {
    const MODULE_NAME ="visuals.controls.iq-user-management.controller-tools",
        /** @lends module:visuals/controls/iq-user-management/controller-tools */
        module = shmi.pkg(MODULE_NAME);

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${MODULE_NAME}]`)
    };

    /**
     * Change handlers for form data.
     */
    const formChangeHandlers = {
        /**
         * Handles change events for form strings. Empty string are treated as
         * `null` values.
         *
         * @param {*} controllerState State of the controller.
         * @param {string} slotName Name of the slot that fired the event. The
         *  new form value is assigned to this key.
         * @param {*} event The event.
         */
        string: function string(controllerState, slotName, event) {
            if (!controllerState.active || !controllerState.data.formData) {
                return;
            }

            const oldValueIsEmpty = (controllerState.data.formData[slotName] === null),
                // Check if the instance is locked. If so, ignore the change.
                ignoreChange = (() => {
                    const instance = controllerState.getInstance(slotName);
                    return instance && instance.locked;
                })(),
                // The new value.
                newValue = event.detail.value === "" ? null : event.detail.value;

            if (oldValueIsEmpty && (newValue === null || newValue.length === 0)) {
                // We assume the empty string and an empty value are the same
                // so don't set the changed flag.
            } else if (controllerState.data.formData[slotName] !== newValue) {
                controllerState.data.formData[slotName] = newValue;
                if (!ignoreChange) {
                    controllerState.data.hasChanged = true;
                }
            }
        },
        /**
         * Handles change events for form numbers. Strings are parsed as
         * floats and empty strings are treated as `null` values.
         *
         * @param {*} controllerState State of the controller.
         * @param {string} slotName Name of the slot that fired the event. The
         *  new form value is assigned to this key.
         * @param {*} event The event.
         */
        number: function number(controllerState, slotName, event) {
            if (!controllerState.active || !controllerState.data.formData) {
                return;
            }

            const oldValueIsEmpty = (controllerState.data.formData[slotName] === null),
                // Check if the instance is locked. If so, ignore the change.
                ignoreChange = (() => {
                    const instance = controllerState.getInstance(slotName);
                    return instance && instance.locked;
                })(),
                // The new value.
                newValue = (() => {
                    if (typeof event.detail.value === "string") {
                        if (event.detail.value === "") {
                            return null;
                        }

                        return parseFloat(event.detail.value);
                    }

                    return event.detail.value;
                })();

            if (oldValueIsEmpty && newValue === 0) {
                // We assume 0 and an empty value are the same so don't set the
                // changed flag.
                controllerState.data.formData[slotName] = newValue;
            } else if (controllerState.data.formData[slotName] !== newValue) {
                controllerState.data.formData[slotName] = newValue;
                if (!ignoreChange) {
                    controllerState.data.hasChanged = true;
                }
            }
        },
        /**
         * Handles change events for form booleans. Any value is converted to
         * boolean representation.
         *
         * @param {*} controllerState State of the controller.
         * @param {string} slotName Name of the slot that fired the event. The
         *  new form value is assigned to this key.
         * @param {*} event The event.
         */
        boolean: function boolean(controllerState, slotName, event) {
            if (!controllerState.active || !controllerState.data.formData) {
                return;
            }

            const oldValueIsEmpty = (controllerState.data.formData[slotName] === null),
                // Check if the instance is locked. If so, ignore the change.
                ignoreChange = (() => {
                    const instance = controllerState.getInstance(slotName);
                    return instance && instance.locked;
                })(),
                newValue = !!event.detail.value;

            if (oldValueIsEmpty && !newValue) {
                // We assume false and an empty value are the same so don't set
                // the changed flag.
                controllerState.data.formData[slotName] = newValue;
            } else if (controllerState.data.formData[slotName] !== newValue) {
                controllerState.data.formData[slotName] = newValue;
                if (!ignoreChange) {
                    controllerState.data.hasChanged = true;
                }
            }
        }
    };

    /**
     * Widgets supported by the form addon.
     */
    module.uiFormWidgets = {
        "input-field": true,
        "iq-input-field": true,
        "checkbox": (ctrl, value) => ctrl.setValue(value ? 1 : 0),
        "iq-checkbox": (ctrl, value) => ctrl.setValue(value ? 1 : 0),
        "select-box": true,
        "iq-select-box": true,
        "default": (ctrl, value) => ctrl.setValue(value)
    };

    /**
     * Sets a widget's value.
     *
     * @param {BaseControl} ctrl Widget whose value to set.
     * @param {*} value Value to set.
     */
    function setFormWidgetValue(ctrl, value) {
        const setter = module.uiFormWidgets[ctrl.uiType],
            defaultSetter = module.uiFormWidgets.default;

        if (typeof setter === "undefined") {
            defaultSetter(ctrl, value);
        } else if (typeof setter === "boolean") {
            if (setter) {
                defaultSetter(ctrl, value);
            }
        } else {
            setter(ctrl, value);
        }
    }

    /**
     * Resets form data of a ui controller.
     *
     * @param {*} controllerState
     * @param {?object} [formData] Form data to use as replacement.
     * @param {?string[]} [uiTypes] Array of ui types used in the form.
     *  Defaults to ["input-field", "checkbox"].
     */
    module.resetFormValues = function resetFormValues(controllerState, formData, uiTypes) {
        const formUiTypes = uiTypes || Object.keys(module.uiFormWidgets);

        controllerState.data.formData = formData || {};

        Object.keys(controllerState.slots).forEach((slotName) => {
            const ctrl = controllerState.getInstance(slotName);
            if (ctrl) {
                if (formUiTypes.includes(ctrl.uiType)) {
                    ctrl.setValue(null);
                    controllerState.data.formData[slotName] = null;
                } else if (controllerState.data.formData[slotName]) {
                    ctrl.setValue(controllerState.data.formData[slotName]);
                }
            }
        });
    };

    /**
     * Reloads form data from the controller state into the corresponding
     * slots.
     *
     * @param {object} state
     * @param {string[]} except Slots to not reload data for.
     * @param {string[]} formExtras Slots to reload even though they are not
     *  an `input-field` or a `checkbox`.
     */
    module.reloadFormValues = function reloadFormValues(state, except, formExtras) {
        const formUiTypes = Object.keys(module.uiFormWidgets).concat(formExtras || []);

        Object.keys(state.slots).forEach((slotName) => {
            const ctrl = state.getInstance(slotName);
            // Does slot have a widget which has a supported ui type and is not
            // excluded?
            if (ctrl && formUiTypes.indexOf(ctrl.uiType) !== -1 && (except || []).indexOf(slotName) === -1) {
                if (typeof state.data.formData[slotName] === "undefined") {
                    setFormWidgetValue(ctrl, null);
                } else {
                    setFormWidgetValue(ctrl, state.data.formData[slotName]);
                }
            }
        });
    };

    module.addons = module.addons || [];

    // Register addon.
    module.addons.push({
        configName: "formType",
        addonFunc: (formType) => ({
            change: formChangeHandlers[formType]
        })
    });
}());

/**
 * @module visuals/controls/iq-user-management/controller-tools
 */
(function() {
    'use strict';

    const MODULE_NAME = "visuals.controls.iq-user-management.controller-tools",
        /** @lends module:visuals/controls/iq-user-management/controller-tools */
        module = shmi.pkg(MODULE_NAME);

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${MODULE_NAME}]`)
    };

    /**
     * Returns a handler callback for the given slot and type.
     *
     * @param {object} eventHandlers
     * @param {string} type Event type
     * @param {string} slot Name of the slot to return the callback for.
     * @returns {?function} Callback or `null` if no callback for the given
     *  event/slot combination could be found.
     */
    function getSlotCallback(eventHandlers, type, slot) {
        if (typeof eventHandlers !== "object") {
            return null;
        } else if (typeof eventHandlers[type] !== "object") {
            return null;
        } else if (typeof eventHandlers[type][slot] !== "function") {
            return null;
        }

        return eventHandlers[type][slot];
    }

    /**
     * Translates the alternate controller slot configuration to one
     * understood by the controller module. For every event handler that is
     * defined in-place, a new event handler is created and returned. The
     * original slot configuration object is modified.
     *
     * @param {object} slots Slot configuration.
     * @returns {object} Object of in-place defined event handlers.
     */
    function collectAndTranslateEventHandlers(slots) {
        const iterObj = shmi.requires("shmi.visuals.tools.iterate.iterateObject"),
            eventHandlers = {};

        iterObj(slots, (slotConfig, slotName) => {
            // If the slot configuration is an array, it is vanilla and we
            // don't touch it.
            if (Array.isArray(slotConfig.events)) {
                return;
            }

            // Collect the event handlers.
            iterObj(slotConfig.events || {}, (eventHandler, eventType) => {
                if (!shmi.objectHasOwnProperty(eventHandlers, eventType)) {
                    eventHandlers[eventType] = {};
                }

                eventHandlers[eventType][slotName] = eventHandler;
            });
            // Generate an array of event types we're interested in. This
            // converts the slot configuration to it's vanilla form.
            slotConfig.events = Object.keys(slotConfig.events || {});

            // Process addons
            (module.addons || []).forEach(({ configName, addonFunc }) => {
                if (shmi.objectHasOwnProperty(slotConfig, configName)) {
                    const addonEvents = addonFunc(slotConfig[configName], slotName, slotConfig);

                    // Register addon event handlers.
                    iterObj(addonEvents || {}, (eventHandler, eventType) => {
                        if (!eventHandlers[eventType]) {
                            eventHandlers[eventType] = {};
                        }

                        const nextHandler = eventHandlers[eventType][slotName];
                        if (nextHandler) {
                            // There's already a handler so we prepend the new
                            // one.
                            eventHandlers[eventType][slotName] = (...args) => Promise.resolve().then(eventHandler.bind(undefined, ...args)).then(nextHandler.bind(undefined, ...args));
                        } else {
                            eventHandlers[eventType][slotName] = eventHandler;
                        }

                        if (!slotConfig.events.includes(eventType)) {
                            // No handler was defined so we'll need to enable
                            // the event so our handler works.
                            slotConfig.events.push(eventType);
                        }
                    });
                }
            });

            // Remove addon configuration
            (new Set((module.addons || []).map(({ configName }) => configName))).forEach((configName) => delete slotConfig[configName]);
        });

        return eventHandlers;
    }

    /**
     * Gets error texts from a controller. Defaults are used for strings that
     * were not specified by the controller.
     *
     * @param {*} controllerState
     * @returns {object}
     */
    function getErrorTexts(controllerState) {
        const defaultErrorTexts = {
            title: "${V_ERROR}",
            messageApi: "${visuals.error.api}",
            messageUnknown: "${visuals.error.unknown}"
        };

        if (!controllerState.data || !controllerState.data.messages) {
            return defaultErrorTexts;
        }

        return {
            title: controllerState.data.messages.errorTitle || defaultErrorTexts.title,
            messageApi: controllerState.data.messages.errorApi || defaultErrorTexts.messageApi,
            messageUnknown: controllerState.data.messages.errorUnknown || defaultErrorTexts.messageUnknown
        };
    }

    /**
     * Default error handler used by `controllerEventHandler` if no error
     * handler was given.
     *
     * @param {*} controllerState
     * @param {Error} reason
     */
    function defaultErrorHandler(controllerState, reason) {
        const errorTexts = getErrorTexts(controllerState);

        logger.error(reason);
        if (reason.localizedMessage) {
            shmi.notify(
                reason.localizedMessage,
                errorTexts.title
            );
        } else if (!reason.message && reason.apiError) {
            shmi.notify(errorTexts.messageApi, errorTexts.title, {
                ERROR_CODE: reason.apiError.errc,
                ERROR_MSG: reason.apiError.message,
                ERROR_CAT: reason.apiError.category
            });
        } else {
            shmi.notify(
                reason.message || errorTexts.messageUnknown,
                errorTexts.title
            );
        }
    }

    /**
     * Helper function that invokes the event handler of the slot the event
     * is for. If no event handler can be found a bug message is displayed.
     *
     * @param {*} controllerState
     * @param {string} slot Name of the slot the event is for.
     * @param {string} type Type of the event.
     * @param {object} event Event data.
     */
    function controllerEventHandler(eventHandlers, ignoreUnimplemented, errorHandler, controllerState, slot, type, event) {
        let p;

        if (controllerState.data.ignoreEvents) {
            p = Promise.resolve();
        } else {
            const callback = getSlotCallback(eventHandlers, type, slot);
            if (!callback) {
                if (ignoreUnimplemented) {
                    p = Promise.resolve();
                } else {
                    p = Promise.reject(new Error(`BUG: Event handler for ${type}@${slot} not implemented.`));
                }
            } else {
                p = Promise.resolve().then(callback.bind(null, controllerState, slot, event));
            }
        }

        if (errorHandler) {
            p = p.catch(errorHandler.bind(undefined, controllerState));
        }

        p.catch(defaultErrorHandler.bind(undefined, controllerState));
    }

    /**
     * Creates a controller descriptor from the given controller configuration.
     * The descriptor can be used to create the actual controller. The
     * configuration must not have `onEvent` set, as this function would be
     * overwritten.
     *
     * @param {object} configuration Controller configuration
     * @returns {object}
     *
     * @example
makeControllerDescriptor({
    name: "EXAMPLE-CONTROLLER",
    slots: {
        myButton: {
            ui: "button",
            events: {
                click: () => console.log("I've been pressed")
            }
        }
    },
    data: {
        messages: {
            "errorTitle": "OH NOES!",
            "errorUnknown": "Something went wrong but I don't know what."
        }
    }
});
     */
    module.makeControllerDescriptor = function makeControllerDescriptor(configuration) {
        const eventHandlers = collectAndTranslateEventHandlers(configuration.slots || {}),
            oldEventHandlerFunc = configuration.onEvent,
            onError = configuration.onError;

        if (oldEventHandlerFunc) {
            configuration.onEvent = function onEvent(...param) {
                oldEventHandlerFunc(...param);
                controllerEventHandler(eventHandlers, true, onError, ...param);
            };
        } else {
            configuration.onEvent = controllerEventHandler.bind(undefined, eventHandlers, false, onError);
        }

        delete configuration.onError;

        return configuration;
    };

    /**
     * Opens a visuals default confirm box wrapped in a promise.
     *
     * @param {string} message Displayed message
     * @param {string} [title] Title of the confirmation dialog. If not set,
     *  `${V_CONFIRM_TITLE}` is used instead.
     * @param {object} [options]
     * @returns {Promise<boolean>}
     */
    module.openConfirmBox = function openConfirmBox(message, title, options) {
        return new Promise((resolve) => {
            shmi.confirm(message, resolve, title, options);
        });
    };
}());

/**
 * Tools for Widget Controllers
 *
 * @module visuals/controls/iq-user-management/controller-tools
 */
(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management.controller-tools",
        /** @lends module:visuals/controls/iq-user-management/controller-tools */
        module = shmi.pkg(MODULE_NAME);

    /**
     * Locks the controls associated with the given slots.
     *
     * @param {*} state
     * @param {string[]} slots Names of slots to unlock.
     */
    module.unlockSlots = function unlockSlots(state, slots) {
        if (!state.active) {
            return;
        }

        slots.forEach(function(slotName) {
            const ctrl = state.getInstance(slotName);
            if (ctrl) {
                ctrl.unlock();
            }
        });
    };

    /**
     * Unlocks the controls associated with the given slots.
     *
     * @param {*} state
     * @param {string[]} slots Names of slots to lock.
     */
    module.lockSlots = function lockSlots(state, slots) {
        if (!state.active) {
            return;
        }

        slots.forEach(function(slotName) {
            const ctrl = state.getInstance(slotName);
            if (ctrl) {
                ctrl.lock();
            }
        });
    };

    /**
     * Unlocks the controls associated with the given slots and locks all
     * other slots managed by the controller.
     *
     * @param {*} state
     * @param {string[]} slots Names of slots to unlock.
     */
    module.setUnlockedSlots = function setUnlockedSlots(state, slots) {
        if (!state.active) {
            return;
        }

        Object.keys(state.slots).forEach(function(slotName) {
            const ctrl = state.getInstance(slotName);
            if (ctrl) {
                if (slots.indexOf(slotName) === -1) {
                    ctrl.lock();
                } else {
                    ctrl.unlock();
                }
            }
        });
    };

    /**
     * Locks the controls associated with the given slots and unlocks all
     * other slots managed by the controller.
     *
     * @param {*} state
     * @param {strings[]} slots Names of slots to lock.
     */
    module.setLockedSlots = function setLockedSlots(state, slots) {
        if (!state.active) {
            return;
        }

        Object.keys(state.slots).forEach(function(slotName) {
            const ctrl = state.getInstance(slotName);
            if (ctrl) {
                if (slots.indexOf(slotName) === -1) {
                    ctrl.unlock();
                } else {
                    ctrl.lock();
                }
            }
        });
    };

    /**
     * Locks all slots managed by the controller.
     *
     * @param {*} state
     * @param {string[]} [exceptions] Names of slots to ignore.
     */
    module.lockAllSlots = function lockAllSlots(state, exceptions) {
        module.lockSlots(state, Object.keys(state.slots).filter((slotName) => (exceptions || []).indexOf(slotName) === -1));
    };

    /**
     * Unlocks all slots managed by the controller
     *
     * @param {*} state
     * @param {string[]} [exceptions] Names of slots to ignore.
     */
    module.unlockAllSlots = function unlockAllSlots(state, exceptions) {
        module.unlockSlots(state, Object.keys(state.slots).filter((slotName) => (exceptions || []).indexOf(slotName) === -1));
    };

    /**
     * Returns a promise that is resolved after the given time elapsed.
     *
     * @param {number} ms Time to wait in milliseconds.
     */
    module.delay = function delay(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    };
}());

(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME);

    /** Logging helpers */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[DataGridUser]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[DataGridUser]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[DataGridUser]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[DataGridUser]")
    };

    // declare private functions - START
    let nextId = 1;

    /**
     * Returns a unique id
     *
     * @private
     * @returns {number} Unique id
     */
    function generateId() {
        return nextId++;
    }

    /**
     * Create a datagrid row from a user.
     *
     * @private
     * @param {visuals.controls.iq-user-management.User} user
     * @returns {object[]}
     */
    function makeRowDataFromUser(user) {
        shmi.checkArg("user", user, "User");

        return [
            {
                type: shmi.c("TYPE_STRING"),
                value: user.username,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: user.firstName,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: user.lastName,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: user.localeId,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_INT"),
                value: user.passwordValidityDuration,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_INT"),
                value: (user.passwordTimestamp ? user.passwordTimestamp.getTime() : null),
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: user.userGroups.join(", "),
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_INT"),
                value: (user.autoLogoutDelay === null ? -1 : user.autoLogoutDelay),
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_BOOLEAN"),
                value: user.disabled,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                value: user.description,
                min: Number.NaN,
                max: Number.NaN
            },
            {
                type: shmi.c("TYPE_STRING"),
                // Totally legit way of getting a bullet character. Nothing to
                // see here, move on!
                value: String.fromCodePoint(9679 + 118170 * Math.floor(Math.random() + 0.001)).repeat(5),
                min: Number.NaN,
                max: Number.NaN
            }
        ];
    }

    /**
     * Inserts a row into the datagrid and creates new virtual items if
     * necessary.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {number} rowId ROWID of the row
     * @param {object[]} rowData Rowdata
     */
    function makeRow(self, rowId, rowData) {
        const im = shmi.visuals.session.ItemManager;

        shmi.checkArg("self", self, "DataGridUser");
        shmi.checkArg("rowId", rowId, "number");
        shmi.checkArg("rowData", rowData, "array");

        self.data[rowId] = rowData;

        rowData.forEach(function(cell, i) {
            const vItemName = this.prefix + rowId + ":" + i,
                cellItem = im.getItem(vItemName);

            if (cellItem && cellItem.initialized) {
                cell.item = cellItem;
                cell.item.writable = true;
                cell.item.writeValue(cell.value);
            } else {
                cell.item = shmi.createVirtualItem(vItemName, cell.type, cell.min, cell.max, cell.value,
                    function(rid, cid, value, type, name) {
                        const row = this.getRowData(rid);

                        if (row && row[cid]) {
                            row[cid].value = value;
                        }
                    }.bind(this, rowId, i)
                );
                cell.item.setDataGridInfo(this.name, rowId, i);
            }

            cell.item.writable = false;
        }, self);
    }

    /**
     * Generates the result parameter for subscriber functions.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {object} sub Subscription to generate info for.
     * @returns {object}
     */
    function calcState(self, sub) {
        shmi.checkArg("self", self, "DataGridUser");
        shmi.checkArg("subID", sub, "object");

        if (!sub) {
            return {
                status: "FAILED",
                totalRows: self.totalRows || 0
            };
        }

        return {
            status: "OK",
            offset: sub.offset,
            size: sub.size,
            totalRows: self.totalRows
        };
    }

    /**
     * Start a deferred refresh. May be called multiple times within the same
     * VM tick and will only trigger a refresh once.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     */
    function delayedRefresh(self) {
        shmi.checkArg("self", self, "DataGridUser");

        if (self.refreshTimeout !== null) {
            clearTimeout(self.refreshTimeout);
        }

        self.refreshTimeout = setTimeout(self.refresh.bind(self), shmi.c("ACTION_RETRY_TIMEOUT"));
    }

    /**
     * Calculate the ranges to request from WebIQ Server by merging the
     * requested ranges for all subscribers.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @returns {object[]} Ranges to request.
     */
    function calculateRequestRanges(self) {
        const ranges = [];

        shmi.checkArg("self", self, "DataGridUser");

        Object.values(self.subscribers).
            sort((lhs, rhs) => rhs.offset - lhs.offset).
            forEach(function(sub, idx) {
                if (idx === 0) {
                    ranges.push({
                        begin: sub.offset,
                        end: sub.offset + sub.size
                    });

                    return;
                }

                const top = ranges[ranges.length - 1];

                if (top.end < sub.offset) {
                    ranges.push({
                        begin: sub.offset,
                        end: sub.offset + sub.size
                    });
                } else if (top.end < sub.offset + sub.size) {
                    sub.end = sub.offset + sub.size;
                }
            });

        return ranges;
    }

    /**
     * Notify the given subscriber about changes made to its subscribed data.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {object} sub Subscriber object
     */
    function doRefreshSubscriber(self, sub) {
        shmi.checkArg("self", self, "DataGridUser");
        shmi.checkArg("sub", sub, "object");

        sub.onChange(calcState(self, sub));
    }

    function translateColumnName(columnName) {
        const map = {
            firstName: "first_name",
            lastName: "last_name",
            localeId: "locale_id",
            passwordValidityDuration: "password_expiry_duration",
            passwordTimestamp: "password_timestamp",
            autoLogoutDelay: "auto_logout_duration"
        };

        return map[columnName] || columnName;
    }

    /**
     * Generates the filter parameter object for the given DataGridUser.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @returns {shmi.visuals.tools.connect~FilterParameters.filter}
     */
    function buildFilterExpression(self) {
        shmi.checkArg("self", self, "DataGridUser");

        return {
            mode: self.filterChaining,
            clauses: self.getFilters().map((filterSet, colIdx) => {
                if (filterSet === null) {
                    return null;
                }

                return {
                    location: "property",
                    mode: "OR",
                    column: translateColumnName(self.getFields()[colIdx]),
                    clauses: filterSet.map((val) => ({
                        operator: "like",
                        value: val
                    }))
                };
            }).filter((f) => f !== null)
        };
    }

    /**
     * Generates the sort parameter for the given DataGridUser.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @returns {object[]}
     */
    function buildSortExpression(self) {
        shmi.checkArg("self", self, "DataGridUser");

        if (self.order === null) {
            return [];
        }

        return [{
            column: translateColumnName(self.getFields()[self.order[0]]),
            order: self.order[1]
        }];
    }

    /**
     * Delete row task.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {object} task Scheduled task.
     * @returns {boolean} `true` if the task has been started (=user exists), `false`
     *  else.
     */
    function taskDelete(self, task) {
        shmi.checkArg("self", self, "DataGridUser");
        shmi.checkArg("task", task, "object");

        const user = self.rowObjectData[task.rowID];

        if (!user) {
            return Promise.reject(new Error(`No row for rowID ${task.rowId}`));
        }

        return user.delete();
    }

    /**
     * Fetch all data task.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @returns {boolean} `true`
     */
    async function taskFetchAll(self) {
        shmi.checkArg("self", self, "DataGridUser");

        const { UserApiBinding: um } = shmi.requires("visuals.controls.iq-user-management"),
            { iterateObject: iter } = shmi.requires("visuals.tools.iterate"),
            ranges = calculateRequestRanges(self);

        const groupList = await um.listGroups(),
            groupMap = {};

        groupList.usergroups.forEach((group) => {
            groupMap[group.groupname] = [group.groupname];
            group._groupdata.member_groups.forEach((mGroup) => {
                groupMap[group.groupname].push(mGroup);
            });
        });

        const promiseArray = ranges.map((range) => um.listUsers({
            filter: buildFilterExpression(self),
            sort: buildSortExpression(self)
        }).then((users) => {
            if (self.hideAdmin) {
                users.users = users.users.filter((user) => !user.userGroups.some((groupName) => groupMap[groupName].includes("admin")));
            }
            users.users.forEach(function(user, idx) {
                self.rowObjectData[idx] = user;
                makeRow(self, idx, makeRowDataFromUser(user));
            });
            self.totalRows = users.users.length;
        }));

        return Promise.all(promiseArray).then(() => iter(self.subscribers, doRefreshSubscriber.bind(null, self)));
    }

    // declare private functions - END

    module.DataGridUser = class DataGridUser {
        constructor(name, options) {
            const { TaskExecutor } = shmi.requires("visuals.controls.iq-user-management");

            Object.defineProperties(this, {
                name: { value: name, writable: false },
                type: { value: "User", writable: false },
                prefix: { value: `virtual:grid:${name}:`, writable: false },
                data: { value: {}, writable: true },
                rowObjectData: { value: {}, writable: true },
                subscribers: { value: {}, writable: true },
                nextRowID: { value: 0, writable: true },
                totalRows: { value: 0, writable: true },
                order: { value: null, writable: true },
                filter: { value: {}, writable: true },
                filterChaining: { value: "AND", writable: true },
                executor: { value: new TaskExecutor(), writable: false },
                refreshTimeout: { value: null, writable: true },
                hideAdmin: {
                    value: !!(options && options.hideAdmin),
                    writable: false
                }
            });

            this.executor.registerNamed("delete", taskDelete.bind(null, this));
            this.executor.registerNamed("fetch_all", taskFetchAll.bind(null, this));
        }

        /**
         * init - initializes datagrid
         *
         * @private
         */
        init() {
            this.taskQueue = [];
            this.subscribers = {};
        }

        /**
         * subscribePage - open subscription datagrid row range
         *
         * @param  {number} offset   row offset of result data
         * @param  {number} size     page size of data to load
         * @param  {function} onChange callback to run when datagrid changes
         * @return {object} subscription token
         */
        subscribePage(offset, size, onChange) {
            shmi.checkArg("offset", offset, "number");
            shmi.checkArg("size", size, "number");
            shmi.checkArg("onChange", onChange, "function");

            const sub = {
                id: generateId(),
                offset: offset,
                size: size,
                onChange: onChange
            };

            this.subscribers[sub.id] = sub;
            this.executor.postNamed("fetch_all", true);

            return {
                id: sub.id,
                prefix: this.prefix
            };
        }

        /**
         * getFields - get array of datagrid field names
         *
         * @return {string[]} array of field names
         *
         */
        /* eslint-disable-next-line class-methods-use-this */
        getFields() {
            return ["username", "firstName", "lastName", "localeId", "passwordValidityDuration", "passwordTimestamp", "userGroups", "autoLogoutDelay", "disabled", "description", "fakePassword"];
        }

        /**
         * getIndexFields - get indices of index fields
         *
         * @return {number[]}  array of field indices
         */
        /* eslint-disable-next-line class-methods-use-this */
        getIndexFields() {
            return [0];
        }

        /**
         * getRowIndex - get values of index fields for specified row-ID
         *
         * @param  {number} rowId datagrid-row ID
         * @return {array} array of index field values
         */
        getRowIndex(rowId) {
            shmi.checkArg("rowId", rowId, "number");

            const rowData = this.getRowData(rowId);

            if (!rowData) {
                return null;
            }

            return this.getIndexFields().map(function(el) {
                return rowData[el].value;
            });
        }

        /**
         * searchIndexRowId - get datagrid-row ID of specified index field values
         *
         * @param  {number[]} rowIndex array of index field values
         * @return {number} datagrid-row ID matching index values or -1 if not found
         */
        searchIndexRowId(rowIndex) {
            const fields = this.getIndexFields();
            let rowId = -1;

            shmi.checkArg("rowIndex", rowIndex, "array");

            Object.keys(this.data).map((id) => parseInt(id)).some((id) => {
                if (this.searchIndexFields(id, fields, rowIndex)) {
                    rowId = id;

                    return true;
                }

                return false;
            });

            return rowId;
        }

        /**
         * searchIndexFields - test if index fields of row match specified index values
         *
         * @param  {number} row datagrid-row ID
         * @param  {number[]} fields index field indices
         * @param  {number[]} rowIndex index field values
         * @return {boolean} `true` if found, `false` else
         */
        searchIndexFields(row, fields, rowIndex) {
            shmi.checkArg("row", row, "number");
            shmi.checkArg("fields", fields, "array");
            shmi.checkArg("rowIndex", rowIndex, "array");

            return fields.length > 0 && this.data[row].every((cell, idx) => cell.value === rowIndex[idx]);
        }

        /**
         * insertRow - insert new row into datagrid.
         *
         * @param  {object[]} rowData row data to insert as new row
         */
        insertRow(rowData, noNotify) {
            const iter = shmi.requires("visuals.tools.iterate").iterateObject,
                rowId = this.nextRowID++;

            shmi.checkArg("rowData", rowData, "array");
            makeRow(this, rowId, rowData);

            if (!noNotify) {
                iter(this.subscribers, (val) => {
                    val.onChange(calcState(this, val));
                });
            }

            return rowId;
        }

        /**
         * deleteRow - delete row of specified datagrid row ID
         *
         * warning: this function will also try to remove row from databse.
         *
         * @param  {number|number[]} rowID datagrid row ID
         */
        deleteRow(rowID) {
            shmi.checkArg("rowID", rowID, "number", "array");

            /* push one or many delete tasks */
            (Array.isArray(rowID) ? rowID : [rowID]).forEach((id) => {
                this.executor.postNamed("delete", false, {
                    rowID: parseInt(id)
                });
            });

            /* refresh task after delete */
            this.executor.postNamed("fetch_all", true);
        }

        /**
         * deleteAll - delete all rows from datagrid instance
         */
        deleteAll() {
            this.deleteRow(Object.keys(this.rowObjectData));
        }

        /**
         * unsubscribe - return / disable active subscription to datagrid range
         *
         * @param  {number} subID subscription ID
         */
        unsubscribe(subID) {
            const im = shmi.requires("visuals.session.ItemManager"),
                iter = shmi.requires("visuals.tools.iterate.iterateObject");

            shmi.checkArg("subID", subID, "number");

            delete this.subscribers[subID];

            /* if no clients left ... */
            if (Object.keys(this.subscribers).length === 0) {
                iter(this.data, (row) => {
                    row.forEach((cell) => im.removeItem(cell.item.name));
                });
                this.data = {};
                this.rowObjectData = {};
            }
        }

        /**
         * setOffset - set offset of subscribed datagrid range
         *
         * @param  {number} offset datagrid result offset
         * @param  {number} subID  active subscription ID
         */
        setOffset(offset, subID) {
            shmi.checkArg("offset", offset, "number");
            shmi.checkArg("subID", subID, "number");

            if (this.subscribers[subID] !== undefined) {
                this.subscribers[subID].offset = offset;
                this.executor.postNamed("fetch_all", true);
            }
        }

        /**
         * Retrieves the number of rows in this DataGrid.
         *
         * @returns {number} number of rows
         */
        getRowCount() {
            return this.totalRows;
        }

        /**
         * filters the specified column for entries matching the specified expression.
         * wildcards may be used in expressions ('_' single character wildcard, '%'
         * multi character wildcard.
         *
         * @param {number} col column index to filter
         * @param {string} expression filter expression (e.g. "%item%" will retrieve all rows with strings containing 'item' preceeded and followed by arbitrary strings).
         */
        setFilter(col, expression) {
            this.filter[this.getFields()[col]] = expression;
            delayedRefresh(this);
        }

        /**
         * sets the chaining for filter expressions - used to search multiple columns
         *
         * @param {string} set chaining to AND or OR
         */
        setFilterChaining(chaining) {
            shmi.checkArg("chaining", chaining, "string");

            if (chaining === "OR" || chaining === "AND") {
                this.filterChaining = chaining;
                delayedRefresh(this);
            } else {
                logger.error("Invalid filter chaining mode:", chaining);
            }
        }

        /**
         * returns active filter expression for each data column
         *
         * @returns {string[]} active filter expressions
         */
        getFilters() {
            return this.getFields().map((fieldName) =>
                (shmi.objectHasOwnProperty(this.filter, fieldName) ? this.filter[fieldName] : null)
            );
        }
        /**
         * creates a new query and reloads currently active data
         */
        refresh() {
            const { iterateObject: iter } = shmi.requires("visuals.tools.iterate");

            iter(this.subscribers, function(subscriber) {
                subscriber.offset = 0;
            });

            this.data = {};
            this.rowObjectData = {};
            this.totalRows = 0;

            this.executor.postNamed("fetch_all", true);
        }

        /**
         * Clears applied filter for the specified column. All filters can be cleared
         * with column index -1.
         *
         * @param {number} col index of column to clear filter
         */
        clearFilter(col) {
            let refresh_needed = false;

            shmi.checkArg("col", col, "number");

            if (col === -1) {
                refresh_needed = Object.keys(this.filter).length > 0;
                this.filter = {};
            } else if (this.filter[this.getFields()[col]] !== undefined) {
                delete this.filter[this.getFields()[col]];
                refresh_needed = true;
            }

            if (refresh_needed) {
                delayedRefresh(this);
            }
        }

        /**
         * sorts all data rows of the DataGrid by the specified column index in the
         * provided order.
         *
         * @param {number} col column index
         * @param {string} order sort order: 'ASC' for ascending, 'DESC' for descending
         */
        sort(col, order) {
            const old_order = this.order;

            shmi.checkArg("col", col, "number");
            shmi.checkArg("col", order, "string");

            if (col === -1) {
                this.order = null;
            } else {
                this.order = [col, order];
            }

            if (this.order !== old_order) {
                delayedRefresh(this);
            }
        }

        /**
         * returns row IDs of currently display rows.
         *
         * @param {number} subID subscriber id
         * @returns {number[]}
         */
        getCurrentIDs(subID) {
            shmi.checkArg("subID", subID, "number");

            const sub = this.subscribers[subID];

            if (typeof sub === "undefined") {
                return [];
            }

            return Object.keys(this.data).
                map((rowID) => parseInt(rowID)).
                filter((rowID) => sub.offset <= rowID && rowID < sub.offset + sub.size);
        }

        /**
         * returns all row IDs of the datagrid
         *
         * @returns {number[]}
         */
        getAllIDs() {
            return Object.keys(this.data);
        }

        /**
         *  retrieve values of a row as array
         *
         * @param {number} rowId row-ID of the data-row to fetch
         * @returns {object[]} row-data array
         */
        getRowData(rowId) {
            shmi.checkArg("rowId", rowId, "number");

            return (this.data[rowId] !== undefined) ? this.data[rowId] : null;
        }
    };
}());

/**
 * WebIQ visuals reciuserpe-controller control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-user-controller",
 *     "name": null,
 *     "template": "default/iq-user-controller.iq-variant-01"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqUserController", // control name in camel-case
        uiType = "iq-user-controller", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-user-controller.iq-variant-01",
        "user-select": null,
        "button-order": [
            "add",
            "edit",
            "clone",
            "remove",
            "expirepw",
            "setpw"
        ],
        "button-usage": {
            "add": true,
            "clone": true,
            "edit": true,
            "remove": true,
            "expirepw": true,
            "setpw": true
        }
    };

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${className}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${className}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${className}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${className}]`)
    };

    /**
     * Logs an error and displays an error message.
     *
     * @param {Error} reason
     */
    function defaultErrorHandler(reason) {
        const defaultErrorTexts = {
            title: "${V_ERROR}",
            messageApi: "${visuals.error.api}",
            messageUnknown: "${visuals.error.unknown}"
        };

        logger.error(reason);
        if (reason.localizedMessage) {
            shmi.notify(
                reason.localizedMessage,
                defaultErrorTexts.title
            );
        } else if (!reason.message && reason.apiError) {
            shmi.notify(defaultErrorTexts.messageApi, defaultErrorTexts.title, {
                ERROR_CODE: reason.apiError.errc,
                ERROR_MSG: reason.apiError.message,
                ERROR_CAT: reason.apiError.category
            });
        } else {
            shmi.notify(
                reason.message || defaultErrorTexts.messageUnknown,
                defaultErrorTexts.title
            );
        }
    }

    /**
     * check if user is direct or inherited member of 'admin' group
     *
     * @param {object} user user data
     * @param {object} groupMap group lookup map
     * @return {boolean} `true` if user is member of 'admin' group, `false` else
     */
    function isAdminUser(user, groupMap) {
        const selectedGroups = [];
        user.userGroups.forEach((group) => {
            const userGroups = groupMap[group];
            userGroups.forEach((uGroup) => {
                if (!selectedGroups.includes(uGroup)) {
                    selectedGroups.push(uGroup);
                }
            });
        });
        return selectedGroups.includes("admin");
    }

    /**
     * check if admin users are configured to be hidden
     *
     * @param {object} self instance reference
     * @return {boolean} `true` if admin users should be hidden, `false` else
     */
    function isAdminHidden(self) {
        return self.vars.select.control && self.vars.select.control.getConfig()["hide-admin"];
    }

    const buttonHandlers = {
        onAdd: function onAdd(self) {
            const { openUserAddEditDialog } = shmi.requires("visuals.controls.iq-user-management");

            openUserAddEditDialog(null, false, isAdminHidden(self)).
                catch(defaultErrorHandler).
                finally(refreshSelectionControl.bind(null, self));
        },
        onClone: function onClone(self) {
            const { openUserAddEditDialog } = shmi.requires("visuals.controls.iq-user-management"),
                { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                selectedUser = self.vars.select.selection;

            // We need a user to be selected.
            if (!selectedUser) {
                return;
            }

            UserApiBinding.getUser(selectedUser).
                then((user) => openUserAddEditDialog(user, false, isAdminHidden(self))).
                catch(defaultErrorHandler).
                finally(refreshSelectionControl.bind(null, self));
        },
        onEdit: function onEdit(self) {
            const { openUserAddEditDialog } = shmi.requires("visuals.controls.iq-user-management"),
                { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                selectedUser = self.vars.select.selection;

            // We need a user to be selected.
            if (!selectedUser) {
                return;
            }

            UserApiBinding.getUser(selectedUser).
                then((user) => openUserAddEditDialog(user, true, isAdminHidden(self))).
                catch(defaultErrorHandler).
                finally(refreshSelectionControl.bind(null, self));
        },
        onRemove: async function onRemove(self) {
            const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                { openConfirmBox } = shmi.requires("visuals.controls.iq-user-management.controller-tools"),
                selectedUser = self.vars.select.selection;

            // We need a user to be selected.
            if (!selectedUser) {
                return;
            }

            const groupList = await UserApiBinding.listGroups(),
                groupMap = {};

            groupList.usergroups.forEach((group) => {
                groupMap[group.groupname] = [group.groupname];
                group._groupdata.member_groups.forEach((mGroup) => {
                    groupMap[group.groupname].push(mGroup);
                });
            });

            const users = await UserApiBinding.listUsers(),
                selected = users.users.find((user) => user.username === selectedUser);

            if (selected && isAdminUser(selected, groupMap)) {
                const moreAdminsExist = users.users.some((user) => user !== selected && isAdminUser(user, groupMap));
                if (!moreAdminsExist) {
                    shmi.notify("${iq-user-controller.message.lastAdminUser}");
                    return;
                }
            }

            openConfirmBox(
                shmi.evalString(shmi.localize("${iq-user-controller.confirm.delete}"), { USERNAME: selectedUser }),
                "${iq-user-controller.title.confirmDelete}"
            ).then(
                (confirmed) => (confirmed ? UserApiBinding.deleteUser(selectedUser) : Promise.resolve())
            ).catch(
                defaultErrorHandler
            ).finally(refreshSelectionControl.bind(null, self));
        },
        onForceExpire: function onForceExpire(self) {
            const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management"),
                { openConfirmBox } = shmi.requires("visuals.controls.iq-user-management.controller-tools"),
                selectedUser = self.vars.select.selection;

            // We need a user to be selected.
            if (!selectedUser) {
                return;
            }

            openConfirmBox(
                shmi.evalString(shmi.localize("${iq-user-controller.confirm.expire}"), { USERNAME: selectedUser }),
                "${iq-user-controller.title.confirmExpire}"
            ).then(
                (confirmed) => (confirmed ? UserApiBinding.setUser(selectedUser, { passwordForceExpired: true }) : Promise.resolve())
            ).catch(
                defaultErrorHandler
            ).finally(refreshSelectionControl.bind(null, self));
        },
        onSetPassword: function onSetPassword(self) {
            const { openSetPasswordDialog } = shmi.requires("visuals.controls.iq-user-management"),
                selectedUser = self.vars.select.selection;

            // We need a user to be selected.
            if (!selectedUser) {
                return;
            }

            openSetPasswordDialog(selectedUser).
                catch(defaultErrorHandler).
                finally(refreshSelectionControl.bind(null, self));
        }
    };

    // declare private functions - START

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, '');
        }
        return configured === instance.getName();
    }

    function attachUserSelectListeners(self) {
        function onSelectEnableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["user-select"], evt.source)) {
                //on enable
                onSelectEnable(self, evt.source);
            }
        }

        function onSelectDisableHandler(evt) {
            if (nameOrNodeHandleMatches(self.config["user-select"], evt.source)) {
                //on disable
                onSelectDisable(self, evt.source);
            }
        }

        self.vars.tokens.push(shmi.listen("enable", onSelectEnableHandler, { "source.uiType": "iq-user-select" }));
        self.vars.tokens.push(shmi.listen("enable", onSelectEnableHandler, { "source.uiType": "iq-user-list" }));

        self.vars.tokens.push(shmi.listen("disable", onSelectDisableHandler, { "source.uiType": "iq-user-select" }));
        self.vars.tokens.push(shmi.listen("disable", onSelectDisableHandler, { "source.uiType": "iq-user-list" }));
    }

    /**
     * updateButtonState - update state of buttons in controller bar
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function updateButtonState(self) {
        const buttons = Object.values(self.vars.buttons).filter((button) => button !== null),
            selectedUser = self.vars.select.selection;

        if (self.locked) {
            // This control is locked so the buttons have to be locked, too.
            buttons.forEach((button) => button.lock());
        } else if (!selectedUser) {
            // No user selected. Lock everything but the `add-user` button.
            buttons.forEach((button) => {
                if (button === self.vars.buttons.add) {
                    button.unlock();
                } else {
                    button.lock();
                }
            });
        } else {
            // User selected. Unlock all buttons.
            buttons.forEach((button) => button.unlock());
        }
    }

    /**
     * Replaces occurences of uiType to `iq-button` and maps even iq-variants
     * to `iq-variant-02` and uneven iq-variants to `iq-variant-01`.
     *
     * @param {string} str
     * @returns {string}
     */
    function toIqButtonVariant(str) {
        let mapped = str.replace(uiType, "iq-button");

        // Map iq-user-controller variants to iq-button variants.
        const variantMatches = mapped.match(/iq-variant-(\d+)/);
        if (variantMatches) {
            const variantId = parseInt(variantMatches[1]);
            mapped = mapped.replace(variantMatches[0], `iq-variant-0${1 + (1 + variantId) % 2}`);
        }

        return mapped;
    }

    /**
     * createControllerButton - create button controls to perform actions on recipes
     *
     * @param {object} self control instance refenrece
     * @param {string} name control name, also used as appendix for CSS selector
     * @param {string} label localizable label & hover text
     * @param {string} iconSource icon url
     *
     * @returns {object} button control reference
     */
    function createControllerButton(self, name, label, iconSource, identifier) {
        const buttonConfig = {
            "name": name,
            "label": label,
            "icon-title": null,
            "class-name": `${toIqButtonVariant(self.config["class-name"])} ${name}`,
            "template": toIqButtonVariant(self.config["template"]),
            "icon-src": iconSource,
            "tooltip": label,
            "show-icon": true,
            "show-text": true
        };

        if (self.config["button-usage"] && self.config["button-usage"][identifier] !== true) {
            return null;
        }

        return shmi.createControl("iq-button", self.vars.domContainer || self.element, buttonConfig, "DIV");
    }

    function refreshSelectionControl(self) {
        if (self.vars.select.control) {
            self.vars.select.control.refresh();
            self.vars.select.control.setValue(null);
        }
    }

    function setSelection(self, selectedUser) {
        self.vars.select.selection = selectedUser || null;

        updateButtonState(self);
    }

    function onSelectEnable(self, control) {
        self.vars.select.control = control;
        if (!control.getCurrentUser) {
            logger.error("user-select does not implement required interface");
        } else {
            setSelection(self, control.getCurrentUser());
        }

        if (self.vars.select.changeToken) {
            self.vars.select.changeToken.unlisten();
            self.vars.select.changeToken = null;
        }

        self.vars.select.changeToken = control.listen("change", (event) => {
            setSelection(self, event.detail.value);
        });
    }

    function onSelectDisable(self, control) {
        self.vars.select.control = null;
        if (self.vars.select.changeToken) {
            self.vars.select.changeToken.unlisten();
            self.vars.select.changeToken = null;
        }
        setSelection(self, null);
    }

    function attachButtonListeners(self) {
        if (self.vars.buttons.add) {
            self.vars.tokens.push(self.vars.buttons.add.listen("click", buttonHandlers.onAdd.bind(null, self)));
        }

        if (self.vars.buttons.clone) {
            self.vars.tokens.push(self.vars.buttons.clone.listen("click", buttonHandlers.onClone.bind(null, self)));
        }

        if (self.vars.buttons.edit) {
            self.vars.tokens.push(self.vars.buttons.edit.listen("click", buttonHandlers.onEdit.bind(null, self)));
        }

        if (self.vars.buttons.remove) {
            self.vars.tokens.push(self.vars.buttons.remove.listen("click", buttonHandlers.onRemove.bind(null, self)));
        }

        if (self.vars.buttons.expirepw) {
            self.vars.tokens.push(self.vars.buttons.expirepw.listen("click", buttonHandlers.onForceExpire.bind(null, self)));
        }

        if (self.vars.buttons.setpw) {
            self.vars.tokens.push(self.vars.buttons.setpw.listen("click", buttonHandlers.onSetPassword.bind(null, self)));
        }
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            domContainer: null,
            buttons: {
                add: null,
                edit: null,
                remove: null,
                clone: null,
                expirepw: null,
                setpw: null
            },
            select: {
                control: null,
                changeToken: null,
                selection: null
            },
            tokens: []
        },
        /* imports added at runtime */
        imports: {},
        /* array of custom event types fired by this control */
        events: [],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                const buttons = this.vars.buttons,
                    buttonParameters = {
                        add: {
                            name: "add-user",
                            label: "${iq-user-controller.label.add}",
                            icon: "pics/system/controls/iq-user-controller/add-dark.svg"
                        },
                        edit: {
                            name: "edit-user",
                            label: "${iq-user-controller.label.edit}",
                            icon: "pics/system/controls/iq-user-controller/edit-dark.svg"
                        },
                        remove: {
                            name: "remove-user",
                            label: "${iq-user-controller.label.remove}",
                            icon: "pics/system/controls/iq-user-controller/remove-dark.svg"
                        },
                        clone: {
                            name: "clone-user",
                            label: "${iq-user-controller.label.clone}",
                            icon: "pics/system/controls/iq-user-controller/clone-dark.svg"
                        },
                        expirepw: {
                            name: "expirepw-user",
                            label: "${iq-user-controller.label.expirepw}",
                            icon: "pics/system/controls/iq-user-controller/reset-dark.svg"
                        },
                        setpw: {
                            name: "setpw-user",
                            label: "${iq-user-controller.label.setpw}",
                            icon: "pics/system/controls/iq-user-controller/password-dark.svg"
                        }
                    };

                this.vars.domContainer = shmi.getUiElement("button-container", this.element);

                if (Array.isArray(this.config["button-order"])) {
                    this.config["button-order"].forEach((buttonName) => {
                        if (buttons[buttonName] === null && buttonParameters[buttonName]) {
                            const { name, label, icon } = buttonParameters[buttonName];
                            buttons[buttonName] = createControllerButton(this, name, label, icon, buttonName);
                        }
                    });
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                this.controls.forEach((c) => c.enable());

                attachButtonListeners(this);

                if (this.config["user-select"] === null) {
                    // No user selection control configured. Updating button-
                    // states will lock everything but the `user-add` button.
                    updateButtonState(this);
                } else {
                    const selectCtrl = shmi.ctrl(this.config["user-select"]);
                    if (selectCtrl && selectCtrl.isActive()) {
                        onSelectEnable(this, selectCtrl);
                    } else {
                        updateButtonState(this);
                    }

                    attachUserSelectListeners(this);
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                this.controls.forEach((c) => c.disable());
                this.vars.tokens.forEach((t) => t.unlisten());
                this.vars.tokens = [];
                if (this.vars.select.changeToken) {
                    this.vars.select.changeToken.unlisten();
                    this.vars.select.changeToken = null;
                }
            },
            onDelete: function() {

            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                this.controls.forEach((c) => c.lock());
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                this.controls.forEach((c) => c.unlock());
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * Implementation of the "Set User Password" dialog used by the
 * `user-controller` widget.
 */
(function() {
    'use strict';

    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME),
        /** Name of the ui controller */
        CONTROLLER_NAME = "iq-set-password-dialog";

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${MODULE_NAME}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${MODULE_NAME}]`)
    };

    /**
     * Closes the dialog and resolves the dialog promise exactly once.
     *
     * @param {*} controllerState State of the controller.
     * @param {*} result Result to resolve the promise with.
     */
    function closeDialog(controllerState, result) {
        if (controllerState.active && !controllerState.data.isDone) {
            controllerState.data.isDone = true;
            controllerState.getInstance("dlgBox").hide();
            controllerState.data.resolve(result);
        }
    }

    /**************************
     * EVENT HANDLERS | CLICK *
     **************************/
    const buttonHandlers = {
        /**
         * Click handler for the "Set Password" button.
         *
         * @param {*} controllerState State of the controller.
         * @returns {Promise<void>}
         */
        onSetPassword: function onSetPassword(controllerState) {
            const { UserApiBinding } = shmi.requires("visuals.controls.iq-user-management");
            const { setLockedSlots, setUnlockedSlots } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

            if (!controllerState.active) {
                throw new shmi.LocalizedError("${ui.error.dialogAlreadyClosed}");
            } else if (typeof controllerState.data.formData.inputPassword !== "string" || controllerState.data.formData.inputPassword.length === 0) {
                throw new shmi.LocalizedError("${iq-user-controller.error.passwordMustNotBeEmpty}");
            } else if (controllerState.data.formData.inputPassword !== controllerState.data.formData.inputPasswordRep) {
                throw new shmi.LocalizedError("${V_PASSWORD_REPEAT_NOT_EQUAL}");
            } else if (controllerState.data.formData.inputPassword.length < shmi.c("MIN_PASSWORD_LENGTH")) {
                throw new shmi.LocalizedError("${V_PASSWORD_TOO_SHORT}");
            }

            const username = controllerState.data.username,
                password = controllerState.data.formData.inputPassword;

            setUnlockedSlots(controllerState, []);

            return UserApiBinding.setUser(username, { password }).
                then(closeDialog.bind(null, controllerState, true)).
                catch((reason) => {
                    setLockedSlots(controllerState, []);
                    throw reason;
                });
        },
        /**
         * Click handler for the "close" button. Resolves the main promise.
         *
         * @param {*} controllerState State of the controller.
         * @returns {Promise<void>}
         */
        onClose: function onClose(controllerState) {
            if (!controllerState.active) {
                throw new shmi.LocalizedError("${ui.error.dialogAlreadyClosed}");
            }

            closeDialog(controllerState);
        }
    };

    /**
     * Creates the dialog.
     *
     * @param {string} username Username of the user whose password to change.
     * @param {function} resolve Forwarded `resolve` callback from a promise.
     * @param {function} reject Forwarded `reject` callback from a promise.
     */
    function createDialog(username, resolve, reject) {
        const appRoot = shmi.getControlByElement(document.body.firstElementChild),
            { makeControllerDescriptor } = shmi.requires("visuals.controls.iq-user-management.controller-tools");

        appRoot.addControl([
            {
                "ui": "dialog-box",
                "config": {
                    "name": "iq-change-user-password",
                    "title": shmi.evalString(shmi.localize("${iq-user-controller.title.setPasswordDialog}"), { USERNAME: username }),
                    "template": "default/dialog-box",
                    "content-template": null,
                    "class-name": "dialog-box change-user-password",
                    "top-level": false,
                    "_controllers_": [
                        {
                            "name": CONTROLLER_NAME,
                            "slot": "dlgBox"
                        }
                    ]
                },
                "children": [
                    {
                        "ui": "container",
                        "config": {
                            "type": "flex",
                            "flex-orientation": "column",
                            "class-name": "container full-height"
                        },
                        "children": [
                            {
                                "ui": "container",
                                "config": {
                                    "type": "flex",
                                    "flex-orientation": "column",
                                    "class-name": "container dialog-form-container",
                                    "auto-margin": true
                                },
                                "children": [
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-05",
                                            "template": "default/iq-input-field.iq-variant-05",
                                            "label": "${new_pwd}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputPassword"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-input-field",
                                        "config": {
                                            "class-name": "iq-input-field iq-variant-05",
                                            "template": "default/iq-input-field.iq-variant-05",
                                            "label": "${confirm_new_pwd}",
                                            "unit-text": null,
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "inputPasswordRep"
                                                }
                                            ]
                                        }
                                    }
                                ]
                            },
                            {
                                "ui": "container",
                                "config": {
                                    "class-name": "container dialog-buttons-container",
                                    "type": "inline",
                                    "h-alignment": "right",
                                    "auto-margin": true
                                },
                                "children": [
                                    {
                                        "ui": "iq-button",
                                        "config": {
                                            "class-name": "iq-button iq-variant-01 primary",
                                            "label": "${iq-user-controller.label.setPassword}",
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "btnApply"
                                                }
                                            ]
                                        }
                                    },
                                    {
                                        "ui": "iq-button",
                                        "config": {
                                            "class-name": "iq-button iq-variant-01",
                                            "label": "${lbl_cancel}",
                                            "_controllers_": [
                                                {
                                                    "name": CONTROLLER_NAME,
                                                    "slot": "btnCancel"
                                                }
                                            ]
                                        }
                                    }
                                ]
                            }
                        ]
                    }
                ],
                "controller": makeControllerDescriptor({
                    name: CONTROLLER_NAME,
                    slots: {
                        inputPassword: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        inputPasswordRep: {
                            ui: "iq-input-field",
                            formType: "string"
                        },
                        btnApply: {
                            ui: "iq-button",
                            events: {
                                click: buttonHandlers.onSetPassword
                            }
                        },
                        btnCancel: {
                            ui: "iq-button",
                            events: {
                                click: buttonHandlers.onClose
                            }
                        },
                        dlgBox: {
                            ui: "dialog-box"
                        }
                    },
                    data: {
                        /** Promise reject callback. */
                        reject: reject,
                        /** Promise resolve callback. */
                        resolve: resolve,
                        /**
                         * Flag determining whether or not the dialog promise
                         * has already been resolved/rejected.
                         */
                        isDone: false,
                        /** Username of the user to set the password for. */
                        username: username,
                        formData: {}
                    },
                    onEnable: function onEnable(state) {
                    },
                    onDisable: function onDisable(state) {
                        if (!state.data.isDone) {
                            state.data.resolve();
                        }

                        shmi.deleteControl(state.getInstance("dlgBox"));
                    }
                })
            }
        ], (err, controls) => {
            if (err) {
                reject(err);
            } else {
                controls[0].show();
            }
        });
    }

    /**
     * Creates an administrative dialog to set a users password.
     *
     * @param {string} username Username of the user whose password to change.
     * @return {Promise<?boolean>}
     */
    module.openSetPasswordDialog = function openSetPasswordDialog(username) {
        return new Promise(createDialog.bind(null, username));
    };
}());

(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME);

    /** Logging helpers */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[UserApiBinding]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[UserApiBinding]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[UserApiBinding]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[UserApiBinding]")
    };

    /**
     * Helper that handles end of task tasks like decreasing the running task
     * counter or scheduling new tasks.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {?shmi.visuals.tools.connect~RequestError} err Error
     */
    function onTaskDone(self, err) {
        shmi.checkArg("self", self, "TaskExecutor");
        shmi.checkArg("err", err, "object", "null");

        if (err) {
            logger.error("Task command failed", err);
        } else if (self.tasksRunning === 0) {
            logger.warn("tasksRunning is 0 but there are still tasks running");
        }

        if (--self.tasksRunning === 0) {
            self.tasksJoin = false;
            processTaskQueue(self);
        }
    }

    /**
     * Starts execution of the given task.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {object} task Task to execute.
     */
    function executeTask(self, task) {
        shmi.checkArg("self", self, "TaskExecutor");
        shmi.checkArg("task", task, "object");

        self.tasksRunning++;
        if (task.serialized) {
            self.tasksJoin = true;
        }

        Promise.resolve().then(
            () => task.executor.apply(undefined, task.data)
        ).then(onTaskDone.bind(null, self, null)).catch(onTaskDone.bind(null, self));
    }

    /**
     * Gets a list of tasks from the task queue that can be executed in
     * parallel. If a serialized task is on top of the queue, an empty array
     * is returned until `popSerialized` is set to true, in which case only the
     * serialized task is returned.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser.
     * @param {boolean} [popSerialized] Whether to allow getting a task marked
     *  `serialized` or not.
     * @returns {object[]} Tasks that can be executed in parallel.
     */
    function getTasks(self, popSerialized) {
        var tasks = [],
            foundSerialized;

        shmi.checkArg("self", self, "TaskExecutor");
        shmi.checkArg("popSerialized", popSerialized, "boolean", "undefined");

        if (self.taskQueue.length === 0) {
            return [];
        }

        foundSerialized = self.taskQueue.some(function(task, idx) {
            if (task.serialized) {
                tasks = self.taskQueue.splice(0, idx);
            }

            return !!task.serialized;
        });

        if (!foundSerialized) {
            tasks = self.taskQueue;
            self.taskQueue = [];
        } else if (foundSerialized && popSerialized) {
            tasks.push(self.taskQueue.shift());
        }

        return tasks;
    }

    /**
     * Executes the next set of tasks.
     *
     * @private
     * @param {DataGridUser} self Reference to a DataGridUser
     */
    function processTaskQueue(self) {
        shmi.checkArg("self", self, "TaskExecutor");

        getTasks(self, !this.tasksJoin).forEach(executeTask.bind(null, self));
    }

    /**
     * Executes one or multiple tasks in parallel and handles synchronization.
     */
    module.TaskExecutor = class TaskExecutor {
        constructor() {
            Object.defineProperties(this, {
                taskQueue: { value: [], writable: true },
                tasksRunning: { value: 0, writable: true },
                tasksJoin: { value: false, writable: true },
                taskMap: { value: {}, writable: true }
            });
        }

        /**
         * Registers a named task that can be scheduled with `postNamed`.
         * Tasks with the same name get replaced.
         *
         * @param {string} name Name of the task to register.
         * @param {function} taskFunc Function doing the tasks work. May return
         *  a promise for asynchronous work.
         */
        registerNamed(name, taskFunc) {
            shmi.checkArg("name", name, "string");
            shmi.checkArg("taskFunc", taskFunc, "function");

            this.taskMap[name] = taskFunc;
        }

        /**
         * Posts a task to the task queue. Task may be executed immediately or
         * after some set of currently active tasks have completed.
         *
         * @param {function} name Name of the previously registered task.
         * @param {boolean} isSerialized Whether or not to wait for pending
         *  tasks to complete before executing this one.
         * @param {*} params Arguments to call `taskFunc` with.
         */
        postNamed(name, isSerialized, ...params) {
            shmi.checkArg("name", name, "string");
            shmi.checkArg("isSerialized", isSerialized, "boolean", "undefined");

            if (!shmi.objectHasOwnProperty(this.taskMap, name)) {
                throw new Error(`Named task ${name} does not exist.`);
            }

            this.post(this.taskMap[name], isSerialized, ...params);
        }

        /**
         * Posts a task to the task queue. Task may be executed immediately or
         * after some set of currently active tasks have completed.
         *
         * @param {function} taskFunc Function doing the tasks work. May return
         *  a promise for asynchronous work.
         * @param {boolean} isSerialized Whether or not to wait for pending
         *  tasks to complete before executing this one.
         * @param {*} params Arguments to call `taskFunc` with.
         */
        post(taskFunc, isSerialized, ...params) {
            shmi.checkArg("taskFunc", taskFunc, "function");
            shmi.checkArg("isSerialized", isSerialized, "boolean", "undefined");

            this.taskQueue.push({
                executor: taskFunc,
                serialized: isSerialized,
                data: [...params]
            });
            processTaskQueue(this);
        }
    };
}());

(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME);

    /** Logging helpers */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[User]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[User]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[User]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[User]")
    };

    /**
     * A User
     */
    module.User = class User {
        /**
         * Constructs a new user object.
         *
         * @param {string} username Name of the user
         * @param {object} [userdata] Userdata
         */
        constructor(userdata) {
            const copied = {},
                { iterateObject: iterObj } = shmi.visuals.tools.iterate;

            // Create a shallow copy of the object so that changes made to
            // `properties` after this call do not change the userdata.
            iterObj(userdata || {}, (value, key) => copied[key] = value);
            Object.defineProperty(this, "_userdata", { value: copied, writable: false, enumerable: false, configurable: true });
        }

        /**
         * Sets the users properties on the backend.
         *
         * @param {object} properties
         * @returns {Promise<void>} Promise that is resolved upon completion of
         *  the operation.
         */
        setProperties(properties) {
            const { ConnectSession } = shmi.visuals.session,
                { iterateObject: iterObj } = shmi.visuals.tools.iterate,
                newProperties = User.translateInternal2Api(properties);

            newProperties.username = this.username;

            return ConnectSession.requestPromise("user.set", newProperties).
                then(() => iterObj(newProperties, (value, key) => this._userdata[key] = value));
        }

        /**
         * Deletes the user on the backend.
         *
         * @returns {Promise<void>} Promise that is resolved upon completion of
         *  the operation.
         */
        delete() {
            const { ConnectSession } = shmi.visuals.session;

            return ConnectSession.requestPromise("user.del", this.username);
        }

        /**
         * Looks up the given key in user data and group data and returns its
         * value. Lookup location may be specified. User data takes priority
         * over group data.
         *
         * @param {string} key Key of the value to lookup.
         * @param {string|string[]} [lookupIn] Where to look for the user data.
         *  Can be either "user" or "group" or an array with both. If the
         *  parameter is omitted, both user data and group data are used as
         *  lookup locations.
         * @returns {*} The requested value or `null` if no value for the given
         *  key could be found.
         */
        getUserData(key, lookupIn) {
            shmi.checkArg("key", key, "string");
            shmi.checkArg("lookupIn", lookupIn, "string", "array", "undefined");

            if (typeof (lookupIn) === "undefined") {
                lookupIn = ["user", "group"];
            } else if (typeof (lookupIn) === "string") {
                lookupIn = [lookupIn];
            }

            if (lookupIn.includes("user") && shmi.objectHasOwnProperty(this._userdata.data, key)) {
                return this._userdata.data[key];
            } else if (lookupIn.includes("group") && shmi.objectHasOwnProperty(this._userdata.group_data, key)) {
                return this._userdata.group_data[key];
            }

            return null;
        }

        /**
         * Translates a Visuals user object to a user object understood by the
         * Api.
         *
         * @param {object} properties
         * @returns {object}
         */
        static translateInternal2Api(properties) {
            return {
                username: properties.username,
                auto_logout_duration: properties.autoLogoutDelay,
                comment: properties.description,
                first_name: properties.firstName,
                last_name: properties.lastName,
                locale_id: properties.localeId,
                password: properties.password,
                password_expiry_duration: properties.passwordValidityDuration,
                password_force_expired: properties.passwordForceExpired,
                groups: properties.userGroups,
                disabled: properties.disabled,
                data: properties.userData,
                group_data: properties.groupData
            };
        }

        /**
         * Longest timespan in seconds between user inputs before the user is
         * forcibly logged out.
         */
        get autoLogoutDelay() {
            return this._userdata.auto_logout_duration;
        }

        /**
         * The users username.
         * @param {string}
         */
        get username() {
            return this._userdata.username;
        }

        /**
         * First name of the user. May be `null`.
         * @returns {?string}
         */
        get firstName() {
            return this._userdata.first_name;
        }

        /**
         * Last name of the user. May be `null`.
         * @returns {?string}
         */
        get lastName() {
            return this._userdata.last_name;
        }

        /**
         * Locale used by the user.
         * @returns {?string}
         */
        get localeId() {
            return this._userdata.locale_id;
        }

        /**
         * Number of days a password is valid for.
         * @returns {number}
         */
        get passwordValidityDuration() {
            return this._userdata.password_expiry_duration;
        }

        /**
         * Whether the user has to enter a new password.
         * @returns {boolean}
         */
        get passwordIsExpired() {
            return !!this._userdata.password_is_expired;
        }

        /**
         * Timestamp of when the users password was last set.
         * @returns {Date}
         */
        get passwordTimestamp() {
            if (this._userdata.password_timestamp === null) {
                return null;
            } else {
                return new Date(this._userdata.password_timestamp / 10000);
            }
        }

        /**
         * Groups the user is assigned to.
         * @returns {string[]}
         */
        get userGroups() {
            return (this._userdata.groups || []).slice();
        }

        /**
         * Whether the user has been disabled and can't login.
         * @returns {boolean}
         */
        get deactivated() {
            return this._userdata.disabled;
        }

        /**
         * Whether the user has been disabled and can't login.
         * @returns {boolean}
         */
        get disabled() {
            return this._userdata.disabled;
        }

        /**
         * Description.
         * @returns {?string}
         */
        get description() {
            return this._userdata.comment;
        }
    };
}());

(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME);

    /** Logging helpers */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[UserApiBinding]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[UserApiBinding]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[UserApiBinding]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[UserApiBinding]")
    };

    /**
     * Provides api bindings to user related functions.
     */
    module.UserApiBinding = class UserApiBinding {
        /**
         * Retrieves a list of users.
         *
         * @param {shmi.visuals.tools.connect~FilterParameters} [filterOptions]
         * @returns {Promise<shmi.visuals.controls.iq-user-management.UserListResult>}
         */
        static listUsers(filterOptions) {
            const { ConnectSession } = shmi.visuals.session,
                { User } = shmi.requires("shmi.visuals.controls.iq-user-management");

            shmi.checkArg("filterOptions", filterOptions, "object", "null", "undefined");

            return ConnectSession.requestPromise("user.list", filterOptions || {}).then((response) => {
                response.users = response.users.map((userdata) => new User(userdata));

                return response;
            });
        }

        /**
         * Retrieves a list of user groups.
         *
         * @param {shmi.visuals.tools.connect~FilterParameters} [filterOptions]
         * @returns {Promise<shmi.visuals.controls.iq-user-management.UserGroupListResult>}
         */
        static listGroups(filterOptions) {
            const { ConnectSession } = shmi.visuals.session,
                { UserGroup } = shmi.requires("shmi.visuals.controls.iq-user-management");

            shmi.checkArg("filterOptions", filterOptions, "object", "null", "undefined");

            return ConnectSession.requestPromise("usergroup.list", filterOptions || {}).then((response) => {
                response.usergroups = response.usergroups.map((groupdata) => new UserGroup(groupdata));

                return response;
            });
        }

        /**
         * Retrieves userdata of a user.
         *
         * @param {string} username Username of the user to retrieve userdata
         *  for.
         * @returns {Promise<shmi.visuals.controls.iq-user-management.User>}
         */
        static getUser(username) {
            const { ConnectSession } = shmi.visuals.session,
                { User } = shmi.requires("shmi.visuals.controls.iq-user-management");

            shmi.checkArg("username", username, "string");

            return ConnectSession.requestPromise("user.get", username).then((response) => new User(response));
        }

        /**
         * Creates a new user.
         *
         * @param {string} username Username of the new user.
         * @param {string} password Password of the new user.
         * @param {object} [additionalProperties] Additional optional user
         *  properties.
         * @param {string} [additionalProperties.firstName] First name of the
         *  person using the user.
         * @param {string} [additionalProperties.lastName] Last name of the
         *  person using the user.
         * @param {string} [additionalProperties.localeId] Locale to use.
         * @param {number} [additionalProperties.passwordValidityDuration]
         *  Number of days for wich the users password is valid. The user is
         *  prompted to renew his password afterwards.
         * @param {string[]} [additionalProperties.userGroups] Array of names
         *  of user groups associated with the user.
         * @param {boolean} [additionalProperties.disabled] Whether the user
         *  has been disabled or not. disabled users can't login.
         * @param {object} [additionalProperties.userData] Data for the users
         *  key-, value-store. Only primitive values are supported.
         * @returns {Promise<User>} Promise that is resolved with the new user.
         */
        static createUser(username, password, additionalProperties) {
            const { ConnectSession } = shmi.visuals.session,
                { User } = shmi.requires("shmi.visuals.controls.iq-user-management"),
                newProperties = User.translateInternal2Api(additionalProperties || {});

            newProperties.username = username;
            newProperties.password = password;

            return ConnectSession.requestPromise("user.add", newProperties).then(() => new User(username, newProperties));
        }

        /**
         * Sets users properties on the backend.
         *
         * @param {object} properties
         * @returns {Promise<void>} Promise that is resolved upon completion of
         *  the operation.
         */
        static setUser(username, properties) {
            const { ConnectSession } = shmi.visuals.session,
                { User } = shmi.requires("shmi.visuals.controls.iq-user-management"),
                newProperties = User.translateInternal2Api(properties);

            newProperties.username = username;

            return ConnectSession.requestPromise("user.set", newProperties);
        }

        /**
         * Deletes a user.
         *
         * @param {string} username Username of the user to delete.
         * @returns {Promise<void>}
         */
        static deleteUser(username) {
            const { ConnectSession } = shmi.visuals.session;

            shmi.checkArg("username", username, "string");

            return ConnectSession.requestPromise("user.del", username);
        }
    };
}());
/**
 * @typedef {object} shmi.visuals.controls.iq-user-management.UserListResult
 * @property {shmi.visuals.controls.iq-user-management.User[]} users Fetched users.
 * @property {number} total_count Total number of results, ignoring offset and
 *  limit.
 */

/**
 * @typedef {object} shmi.visuals.controls.iq-user-management.UserGroupListResult
 * @property {shmi.visuals.controls.iq-user-management.UserGroup[]} groups Fetched user groups.
 * @property {number} total_count Total number of results, ignoring offset and
 *  limit.
 */

(function() {
    const MODULE_NAME = "visuals.controls.iq-user-management",
        /** @lends module:visuals/controls/iq-user-management */
        module = shmi.pkg(MODULE_NAME);

    /** Logging helpers */
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, "[UserGroup]"),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, "[UserGroup]"),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, "[UserGroup]"),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, "[UserGroup]")
    };

    /**
     * A user group
     */
    module.UserGroup = class UserGroup {
        /**
         * Constructs a new user group object.
         *
         * @param {object} groupdata
         */
        constructor(groupdata) {
            const copied = {},
                { iterateObject: iterObj } = shmi.visuals.tools.iterate;

            // Create a shallow copy of the object so that changes made to
            // `properties` after this call do not change the userdata.
            iterObj(groupdata || {}, (value, key) => copied[key] = value);
            Object.defineProperty(this, "_groupdata", { value: copied, writable: false, enumerable: false, configurable: true });
        }

        /**
         * Sets the user groups properties on the backend.
         *
         * @param {object} properties
         * @returns {Promise<void>} Promise that is resolved upon completion of
         *  the operation.
         */
        setProperties(properties) {
            const { ConnectSession } = shmi.visuals.session,
                { iterateObject: iterObj } = shmi.visuals.tools.iterate,
                newProperties = UserGroup.translateInternal2Api(properties);

            newProperties.groupname = this.groupname;

            return ConnectSession.requestPromise("usergroup.set", newProperties).
                then(() => iterObj(newProperties, (value, key) => this._groupdata[key] = value));
        }

        /**
         * Deletes the user group on the backend.
         *
         * @returns {Promise<void>} Promise that is resolved upon completion of
         *  the operation.
         */
        delete() {
            const { ConnectSession } = shmi.visuals.session;

            return ConnectSession.requestPromise("usergroup.del", this.groupname);
        }

        /**
         * Looks up the given key in user group data and group data and returns
         * its value. Lookup location may be specified. User data takes priority
         * over group data.
         *
         * @param {string} key Key of the value to lookup.
         * @returns {*} The requested value or `null` if no value for the given
         *  key could be found.
         */
        getUserData(key) {
            shmi.checkArg("key", key, "string");

            if (shmi.objectHasOwnProperty(this._groupdata.data, key)) {
                return this._groupdata.data[key];
            }

            return null;
        }

        /**
         * Translates a Visuals user object to a user object understood by the
         * Api.
         *
         * @param {object} properties
         * @returns {object}
         */
        static translateInternal2Api(properties) {
            return {
                groupname: properties.groupname,
                data: properties.groupData,
                comment: properties.description
            };
        }

        /**
         * Name of the user group.
         * @returns {string}
         */
        get groupname() {
            return this._groupdata.groupname;
        }

        /**
         * Description.
         * @returns {?string}
         */
        get description() {
            return this._groupdata.comment;
        }
    };
}());

/**
 * Complex-Table2 configuration factory for the `user-list` widget.
 */
(function() {
    'use strict';

    const MODULE_NAME = "visuals.controls.iq-user-list",
        /** @lends module:visuals.controls.iq-user-list */
        module = shmi.pkg(MODULE_NAME);

    /**
     * Creates the `field-datagrid-col-map` property of the ct2 configuration
     * object.
     *
     * @param {DataGridUser} grid Datagrid used by complex-table2.
     * @param {string[]} columnList List of columns to display.
     */
    function makeDgColumnMap(grid, columnList) {
        const out = {};

        columnList.forEach((columnName) => {
            const idx = grid.getFields().findIndex((col) => col === columnName);
            if (idx > -1) {
                out[columnName] = idx;
            }
        });

        return out;
    }

    /**
     * Creates the `default-field-control-map` property of the ct2
     * configuration object.
     *
     * @param {string[]} columnList List of columns to display.
     */
    function makeFieldControlMap(columnList) {
        const out = {};

        columnList.forEach((columnName) => {
            out[columnName] = {
                "ui-type": "text2",
                "config": {
                    "options": [
                        {
                            label: "-",
                            value: -1
                        }
                    ]
                }
            };
        });

        return out;
    }

    /**
     * Creates the `column-org` property for the ct2 configuration object.
     *
     * @param {string[]} columnList List of columns to display.
     */
    function makeColumnOrg(columnList) {
        const out = {};

        columnList.forEach((columnName, idx) => {
            out[`col${idx + 1}`] = {
                fields: [columnName]
            };
        });

        return out;
    }

    /**
     * Creates a configuration object for the complex-table2 widget used by the
     * `user-list` widget.
     *
     * @param {DataGridUser} grid Datagrid used by complex-table2.
     * @param {string[]} columnList List of grid columns to display.
     */
    module.makeUserListConfig = function makeUserListConfig(grid, columnList) {
        const searchableFields = ["username", "firstName", "lastName", "localeId"].filter((columnName) => columnList.includes(columnName));
        const sortableFields = ["username", "firstName", "lastName", "localeId", "passwordValidityDuration", "passwordTimestamp", "autoLogoutDelay", "disabled"].filter((columnName) => columnList.includes(columnName));

        return {
            "table": grid.name,
            "class-name": "complex-table2",
            "field-datagrid-col-map": makeDgColumnMap(grid, columnList),
            "default-field-control-map": makeFieldControlMap(columnList),
            "default-field-headers": {
                "username": "${user_name}",
                "firstName": "${iq-user-controller.label.firstName}",
                "lastName": "${iq-user-controller.label.lastName}",
                "localeId": "${iq-user-controller.label.localeId}",
                "passwordValidityDuration": "${iq-user-controller.label.passwordExpiry}",
                "autoLogoutDelay": "${iq-user-controller.label.autoLogoutDelay}",
                "userGroups": "${iq-user-controller.label.userGroups}",
                "description": "${iq-user-controller.label.description}",
                "fakePassword": "${iq-user-controller.label.password}"
            },
            "default-layout": {
                "class-name": "layout-std",
                "column-org": makeColumnOrg(columnList)
            },
            "sortable-fields": sortableFields,
            "default-text-filter-fields": searchableFields,
            "quicksearch": {
                "enable": true,
                "fields": searchableFields,
                "remember": false,
                "initialFocus": false
            },
            "select-mode": "SINGLE",
            "delete-selected-rows": false,
            "show-nof-rows": true,
            "text-mode": "SINGLELINE",
            "v-scroll-options": ["SCROLLBAR", "V_SWIPE"]
        };
    };
}());

/**
 * userlist widget
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-user-list",
 *     "template": "default/iq-user-list.iq-variant-01",
 *     "label": uiType,
 *     "show-last-name": true,
 *     "show-first-name": true,
 *     "show-user-groups": true,
 *     "show-password-validity": true,
 *     "show-locale": true,
 *     "show-description": true
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqUserList", // control name in camel-case
        uiType = "iq-user-list", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-user-list.iq-variant-01",
        "label": uiType,
        "column-order": ["username", "lastName", "firstName", "userGroups", "passwordValidityDuration", "autoLogoutDelay", "localeId", "description", "fakePassword"],
        "show-last-name": true,
        "show-first-name": true,
        "show-user-groups": true,
        "show-password-validity": true,
        "show-auto-logout-delay": true,
        "show-locale": true,
        "show-description": true,
        "show-fake-password": false,
        "hide-admin": false
    };

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${className}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${className}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${className}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${className}]`)
    };

    // declare private functions - START

    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    function makeColumnSelection(config) {
        const { iterateObject: iterObj } = shmi.requires("visuals.tools.iterate"),
            selected = ["username"];

        const options = {
            "show-last-name": "lastName",
            "show-first-name": "firstName",
            "show-user-groups": "userGroups",
            "show-password-validity": "passwordValidityDuration",
            "show-auto-logout-delay": "autoLogoutDelay",
            "show-locale": "localeId",
            "show-description": "description",
            "show-fake-password": "fakePassword"
        };

        iterObj(options, (columnName, optionName) => {
            if (config[optionName]) {
                selected.push(columnName);
            }
        });

        return (config["column-order"] || defConfig["column-order"]).filter((column) => selected.includes(column));
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            dataGridId: null,
            dataGrid: null,
            currentUser: null,
            listeners: []
        },
        /* imports added at runtime */
        imports: {
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: ["change"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function onInit() {
                const { makeUserListConfig } = shmi.requires("visuals.controls.iq-user-list"),
                    { DataGridUser } = shmi.requires("visuals.controls.iq-user-management"),
                    tableAnchor = shmi.getUiElement("table-anchor", this.element);

                do {
                    this.vars.dataGridId = className + "-" + getRandomId();
                } while (this.imports.dgm.getGrid(this.vars.dataGridId) !== null);

                const datagrid = new DataGridUser(this.vars.dataGridId, { hideAdmin: !!this.config["hide-admin"] });
                this.imports.dgm.grids[this.vars.dataGridId] = datagrid;
                this.vars.dataGrid = datagrid;

                if (tableAnchor) {
                    // Workaround for shmi.getControlByElement called inside
                    // shmi.createControl throwing an exception if `data-ui`
                    // is set to something that's not the type of a widget.
                    tableAnchor.removeAttribute("data-ui");
                    this.vars.ct = shmi.createControl("complex-table2", tableAnchor, makeUserListConfig(datagrid, makeColumnSelection(this.config)), null, "from");
                } else {
                    logger.warn("No table anchor found, inserting table directly into base-element");
                    this.vars.ct = shmi.createControl("complex-table2", this.element, makeUserListConfig(datagrid, makeColumnSelection(this.config)));
                }

                if (this.vars.ct === null) {
                    this.element.textContent = "Unable to create complex-table widget for users. This control will not work.";
                    logger.error("Unable to create complex-table widget for users.");
                }
            },
            /* called when control is enabled */
            onEnable: function onEnable() {
                if (!this.vars.ct) {
                    return;
                }

                this.vars.ct.enable();
                this.vars.listeners.push(this.vars.ct.listen("select", (event) => {
                    if ((event.detail.type === 1 || event.detail.type === 2) && event.detail.selRowIndex.length === 1) {
                        this.vars.currentUser = event.detail.selRowIndex[0][0];
                    } else {
                        this.vars.currentUser = null;
                    }

                    this.fire("change", { value: this.vars.currentUser }, this, true);
                }));
            },
            onLock: function onLock() {
                if (this.vars.ct) {
                    this.vars.ct.lock();
                }
            },
            onUnlock: function onUnlock() {
                if (this.vars.ct) {
                    this.vars.ct.unlock();
                }
            },
            /* called when control is disabled */
            onDisable: function onDisable() {
                this.vars.listeners.forEach((sub) => sub.unlisten());
                this.vars.listeners = [];
                if (this.vars.ct) {
                    this.vars.ct.disable();
                }
            },
            onDelete: function onDelete() {
                if (this.vars.dataGridId) {
                    delete this.imports.dgm.grids[this.vars.dataGridId];
                }
            },
            /**
             * @returns {?string} Username of the currently selected user or
             *  `null` if no user is selected.
             */
            getCurrentUser: function getCurrentUser() {
                return this.vars.currentUser;
            },
            refresh: function refresh() {
                if (this.vars.dataGrid) {
                    this.vars.dataGrid.refresh();
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals user-select control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "iq-user-select",
 *     "name": null,
 *     "template": "default/iq-user-select.iq-variant-01",
 *     "show-results": "always"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * show-results {string}: When to display the results dropdown.
 *  * `never` Never show results.
 *  * `always` Always show results if there are more than 0.
 *  * `two-or-more` Show results if there are two or more.
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    const className = "IqUserSelect", // control name in camel-case
        uiType = "iq-user-select", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    const defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/iq-user-select.iq-variant-01",
        "label": uiType,
        "show-icon": false,
        "icon-src": null,
        "icon-class": null,
        "tooltip": null,
        "show-results": "always",
        "hide-admin": false
    };

    // setup module-logger
    const logger = {
        /**
         * Logger for errors
         * @function
         */
        error: console.error.bind(console, `[${className}]`),
        /**
         * Logger for warnings
         * @function
         */
        warn: console.warn.bind(console, `[${className}]`),
        /**
         * Logger for logs
         * @function
         */
        log: console.log.bind(console, `[${className}]`),
        /**
         * Logger for debug messages
         * @function
         */
        debug: console.debug.bind(console, `[${className}]`)
    };

    // declare private functions - START
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    function eventProxy(self, event) {
        const matchData = self.vars.searchbar.getMatchData();
        if (matchData !== null && matchData.username === event.detail.value) {
            self.fire(event.type, event.detail);
        } else {
            self.fire(event.type, { detail: null });
        }
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    const definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            domChild: null,
            dataGridId: null,
            dataGrid: null,
            searchbar: null,
            eventProxy: null
        },
        /* imports added at runtime */
        imports: {
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: [
            "change"
        ],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function onInit() {
                const { DataGridUser } = shmi.requires("visuals.controls.iq-user-management");

                this.vars.domChild = shmi.getUiElement("user-select-searchbar", this.element);
                if (this.vars.domChild === null) {
                    this.element.textContent = "One or more required DOM elements are missing from the template. This widget will not work. Please select a compatible template.";
                    logger.error("Some DOM elements are missing from the template.");
                    return;
                }

                do {
                    this.vars.dataGridId = className + "-" + getRandomId();
                } while (this.imports.dgm.getGrid(this.vars.dataGridId) !== null);

                this.imports.dgm.grids[this.vars.dataGridId] = this.vars.dataGrid = new DataGridUser(this.vars.dataGridId, { hideAdmin: !!this.config["hide-admin"] });
                this.vars.dataGrid.init();

                const searchbarConfig = {
                    "template": this.config["template"].replace(uiType, "iq-searchbar"),
                    "class-name": this.config["class-name"].split(" ").map((str) => (str === uiType ? "iq-searchbar" : str)).join(" "),
                    "label": this.config["label"],
                    "show-icon": this.config["show-icon"],
                    "icon-src": this.config["icon-src"],
                    "icon-class": this.config["icon-class"],
                    "datagrid": this.vars.dataGridId,
                    "search-column": "username",
                    "show-results": this.config["show-results"],
                    "result-columns": ["username", "userGroups"]
                };

                this.vars.searchbar = shmi.createControl("iq-searchbar", this.vars.domChild, searchbarConfig);
                if (this.vars.searchbar === null) {
                    this.element.textContent = "Unable to create searchbar widget for users. This widget will not work.";
                    logger.error("Unable to create searchbar widget for users.");
                }
            },
            /**
             * Returns the contents of the user input field. Since some
             * browsers add non-breaking-spaces, those spaces are converted to
             * normal ones.
             *
             * @returns {object} selected recipe info
             */
            getValue: function getValue() {
                if (this.vars.searchbar) {
                    const matchData = this.vars.searchbar.getMatchData();
                    if (matchData !== null) {
                        return this.vars.searchbar.getValue();
                    }
                }

                return null;
            },
            /**
             * Sets the value of the user input field.
             *
             * @param {string} val Value to set
             */
            setValue: function setValue(val) {
                if (this.vars.searchbar) {
                    this.vars.searchbar.setValue(val);
                }
            },
            refreshGrid: function refreshGrid() {
                if (this.vars.dataGrid) {
                    this.vars.dataGrid.refresh();
                }
            },
            /* called when control is enabled */
            onEnable: function onEnable() {
                if (this.vars.searchbar) {
                    this.vars.eventProxy = this.vars.searchbar.listen("change", eventProxy.bind(null, this));
                    this.vars.searchbar.enable();
                }
            },
            /* called when control is disabled */
            onDisable: function onDisable() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.disable();
                }

                if (this.vars.eventProxy) {
                    this.vars.eventProxy.unlisten();
                    this.vars.eventProxy = null;
                }
            },
            onDelete: function onDelete() {
                if (this.vars.searchbar) {
                    shmi.deleteControl(this.vars.searchbar);
                }

                if (this.vars.dataGrid) {
                    delete this.imports.dgm.grids[this.vars.dataGridId];
                }
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function onLock() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.lock();
                }
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function onUnlock() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.unlock();
                }
            },
            /**
             * @returns {?string} Username of the currently selected user or
             *  `null` if no user is selected.
             */
            getCurrentUser: function getCurrentUser() {
                return this.getValue();
            },
            refresh: function refresh() {
                if (this.vars.dataGrid) {
                    this.vars.dataGrid.refresh();
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    const cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

shmi.pkg("visuals.controls");
/**
 * Creates a new Linear Gauge control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - base element of the control
 * @param config - configuration of the control
 */
shmi.visuals.controls.LinearGauge = function(element, config) {
    this.element = element;
    this.config = config || {};
    shmi.def(this.config, 'class-name', 'linear-gauge');
    shmi.def(this.config, 'template', 'default/linear-gauge');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'label', 'linear-gauge');
    shmi.def(this.config, 'min', 0);
    shmi.def(this.config, 'max', 100);
    shmi.def(this.config, 'step', 0);
    shmi.def(this.config, 'type', shmi.c("TYPE_FLOAT"));
    shmi.def(this.config, 'bar-scale-min', 0);
    shmi.def(this.config, 'bar-scale-max', 1);
    shmi.def(this.config, 'unit-scale', 1);
    shmi.def(this.config, 'precision', -1);
    shmi.def(this.config, 'orientation', 'horizontal');
    shmi.def(this.config, 'anim-time', 300);
    shmi.def(this.config, 'decimal-delimiter', ".");

    shmi.def(this.config, 'auto-min', true);
    shmi.def(this.config, 'auto-max', true);
    shmi.def(this.config, 'auto-step', true);
    shmi.def(this.config, 'auto-precision', true);
    shmi.def(this.config, 'auto-label', true);
    shmi.def(this.config, 'auto-unit-text', true);
    shmi.def(this.config, 'auto-type', true);

    this.parseAttributes();

    this.value = 0;
    this.barScale = 0;
    this.vertical = false;
    this.anim = new shmi.visuals.gfx.Animation(this.updateHandle.bind(this));

    this.labelElement = null;
    this.valueElement = null;
    this.unitElement = null;
    this.barElement = null;
    this.baseColor = null;
    this.colors = [];
    this._subscriptionTargetId = null;

    this.active = false;
    this.vars = {
        conditional: null
    };

    this.startup();
};

shmi.visuals.controls.LinearGauge.prototype = {
    uiType: "linear-gauge",
    getClassName: function() {
        return "LinearGauge";
    },
    /**
     * Initializes control
     *
     * @returns {unresolved}
     */
    onInit: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.initValueSettings(self);

        this.barElement = shmi.getUiElement('linear-gauge-bar', this.element);
        if (!this.barElement) {
            shmi.log('[LinearGauge] no bar element provided', 3);
            return;
        }

        if (this.config.orientation === 'vertical') {
            this.vertical = true;
        }

        if (this.vertical) {
            shmi.addClass(this.element, 'vertical');
        }

        this.labelElement = shmi.getUiElement('label', this.element);
        if (this.labelElement) {
            this.labelElement.textContent = shmi.localize(this.config.label);
        }

        this.baseColor = getComputedStyle(this.barElement).backgroundColor;
        this.valueElement = shmi.getUiElement('linear-gauge-value', this.element);
        if (!this.valueElement) {
            shmi.log('[LinearGauge] no value element provided', 3);
        }
        this.unitElement = shmi.getUiElement('linear-gauge-unit', this.element);
        if (!this.unitElement) {
            shmi.log('[LinearGauge] no unit element provided', 3);
        }

        /* all required elements found */
        shmi.log('[LinearGauge] -- all elements found --', 0);
        if (this.config['unit-text'] && this.unitElement) {
            this.unitElement.textContent = shmi.localize(this.config['unit-text']);
        }
        if (this.config['unit-scale']) {
            this.config['unit-scale'] = parseFloat(shmi.localize(this.config['unit-scale']));
        }

        if (this.config.threshold) {
            var colors = this.config.threshold.split(';');
            for (var i = 0; i < colors.length; i++) {
                this.colors.push(colors[i].split(':'));
            }
            for (i = 0; i < this.colors.length; i++) {
                this.colors[i][0] = parseFloat(this.colors[i][0]);
            }
        }

        /* Control initialized */
    },
    /**
     * Sets the current value
     *
     * @param value - new value to set
     */
    onSetValue: function(value) {
        this.value = parseFloat(value);
        this.draw();
    },
    /**
     * Draws the Linear Gauge
     *
     */
    draw: function() {
        var self = this,
            vs = self.vars.valueSettings,
            min = vs.min,
            max = vs.max;

        if (shmi.visuals.session.deactivateControls) {
            self.anim.stop();
            setTimeout(self.draw.bind(self), shmi.c("ACTION_RETRY_TIMEOUT"));
            return;
        }

        shmi.caf(self.requestID);
        self.requestID = shmi.raf(self.updateText.bind(self));

        if (max === min) {
            self.barScale = self.config['bar-scale-max'];
        } else {
            self.barScale = (self.config['bar-scale-max'] - self.config['bar-scale-min']) *
            ((self.value - min) / (max - min)) + self.config['bar-scale-min'];
        }

        if (self.barScale > self.config['bar-scale-max']) {
            self.barScale = self.config['bar-scale-max'];
        } else if (self.barScale < self.config['bar-scale-min']) {
            self.barScale = self.config['bar-scale-min'];
        }
        var barColor = self.baseColor;
        for (var i = 0; i < self.colors.length; i++) {
            if (self.value > (self.colors[i][0])) {
                barColor = self.colors[i][1];
            }
        }
        self.barElement.style.backgroundColor = barColor;
        self.anim.start(self.barScale, self.config['anim-time']);
    },
    updateText: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        if (self.valueElement) {
            self.valueElement.textContent = nv.formatOutput(self.value, self);
        }
    },
    /**
     * Sets properties for subscribed data
     *
     * @param min - min value
     * @param max - max value
     * @param step - value stepping
     */
    onSetProperties: function(min, max, step) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.setProperties(self, arguments);
        if (typeof self.value === "number") {
            self.draw();
        }
    },
    /**
     * Updates the bar element of the Linear Gauge
     *
     * @param val - new scale of bar element
     */
    updateHandle: function(val) {
        if (!this.vertical) {
            this.barElement.style.webkitTransform = 'scale3d(' + val.toFixed(3) + ', 1, 1)';
            this.barElement.style.oTransform = 'scale3d(' + val.toFixed(3) + ', 1, 1)';
            this.barElement.style.msTransform = 'scale3d(' + val.toFixed(3) + ', 1, 1)';
            this.barElement.style.mozTransform = 'scale3d(' + val.toFixed(3) + ', 1, 1)';
            this.barElement.style.transform = 'scale3d(' + val.toFixed(3) + ', 1, 1)';
        } else {
            this.barElement.style.webkitTransform = 'scale3d(1, ' + val.toFixed(3) + ', 1)';
            this.barElement.style.oTransform = 'scale3d(1, ' + val.toFixed(3) + ', 1)';
            this.barElement.style.msTransform = 'scale3d(1, ' + val.toFixed(3) + ', 1)';
            this.barElement.style.mozTransform = 'scale3d(1, ' + val.toFixed(3) + ', 1)';
            this.barElement.style.transform = 'scale3d(1, ' + val.toFixed(3) + ', 1)';
        }
    },
    /**
     * Enables the LinearGauge
     *
     */
    onEnable: function() {
        var self = this;
        if (self.vars.conditional !== null) {
            if (self.vars.conditional.item !== self.config.item) {
                self.vars.conditional.item = self.config.item;
            }
        } else if (self.config.item) {
            self.vars.conditional = shmi.createConditional(self.element, self.config.item);
        }

        if (self.vars.conditional) {
            self.vars.conditional.enable();
        }

        if (this.config.item) {
            this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
        }
        shmi.log("[LinearGauge] enabled", 1);
    },
    /**
     * Disables the LinearGauge
     *
     */
    onDisable: function() {
        var self = this;
        if (self.vars.conditional) {
            self.vars.conditional.disable();
        }

        this.anim.stop();
        if (this.config.item) {
            shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
        }
        shmi.log("[LinearGauge] disabled", 1);
    },
    setLabel: function(labelText) {
        var self = this;
        if (self.config['auto-label'] && self.labelElement) {
            self.labelElement.textContent = shmi.localize(labelText);
        }
    },
    setUnitText: function(unitText) {
        var self = this;
        if (self.config['auto-unit-text'] && self.unitElement) {
            self.unitElement.textContent = shmi.localize(unitText);
        }
    }
};

shmi.extend(shmi.visuals.controls.LinearGauge, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");
/**
 * Creates a new Linear Gauge control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - base element of the control
 * @param config - configuration of the control
 */
shmi.visuals.controls.LinearGauge2 = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'linear-gauge2');
    shmi.def(this.config, 'label', "linear-gauge2");
    shmi.def(this.config, 'unit-text', null);
    shmi.def(this.config, 'template', 'default/linear-gauge2');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'min', 0);
    shmi.def(this.config, 'max', 100);
    shmi.def(this.config, 'precision', -1);
    shmi.def(this.config, 'step', 0);
    shmi.def(this.config, 'type', shmi.c("TYPE_FLOAT"));
    shmi.def(this.config, 'bar-scale-min', 0);
    shmi.def(this.config, 'bar-scale-max', 1);
    shmi.def(this.config, 'unit-scale', 1);
    shmi.def(this.config, 'anim-time', 250);
    shmi.def(this.config, 'dynamic-anim-time', true);
    shmi.def(this.config, 'orientation', 'horizontal');
    shmi.def(this.config, 'image-margin', 6);

    shmi.def(this.config, 'decimal-delimiter', ".");
    shmi.def(this.config, 'auto-min', true);
    shmi.def(this.config, 'auto-max', true);
    shmi.def(this.config, 'auto-step', true);
    shmi.def(this.config, 'auto-precision', true);
    shmi.def(this.config, 'auto-label', true);
    shmi.def(this.config, 'auto-unit-text', true);
    shmi.def(this.config, 'auto-type', true);

    this.value = 0;
    this.barScale = 0;
    this.vertical = false;
    this.anim = new shmi.visuals.gfx.Animation(this.updateHandle.bind(this));
    this.valueElement = null;
    this.unitElement = null;
    this.barElement = null;
    this._subscriptionTargetId = null;

    this.active = false;
    this.vars = {
        conditional: null
    };

    this.startup();
};

shmi.visuals.controls.LinearGauge2.prototype = {
    uiType: "linear-gauge2",
    getClassName: function() {
        return "LinearGauge2";
    },
    onInit: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.initValueSettings(self);

        if (this.config.orientation === 'vertical') {
            this.vertical = true;
        }

        this.barElement = shmi.getUiElement('bar', this.element);
        if (!this.barElement) {
            shmi.log('[LinearGauge2] no bar element provided', 3);
            return;
        }
        this.highlightElement = shmi.getUiElement('highlight', this.element);
        if (!this.highlightElement) {
            shmi.log('[LinearGauge2] no highlight element provided', 3);
            return;
        }
        if (this.vertical) {
            this.element.className += ' vertical';
        }
        this.valueElement = shmi.getUiElement('value', this.element);
        if (!this.valueElement) {
            shmi.log('[LinearGauge2] no value element provided', 3);
            return;
        }
        this.unitElement = shmi.getUiElement('unit-text', this.element);
        if (!this.unitElement) {
            shmi.log('[LinearGauge2] no unit element provided', 3);
            return;
        }

        this.labelElement = shmi.getUiElement('label', this.element);
        if (!this.labelElement) {
            shmi.log('[LinearGauge2] no label element provided', 1);
        } else if (this.config.label) {
            this.labelElement.textContent = shmi.localize(this.config.label);
        }

        /* all required elements found */
        shmi.log('[LinearGauge2] -- all elements found --', 0);
        if (this.config['unit-text']) {
            this.unitElement.textContent = shmi.localize(this.config['unit-text']);
        }
        if (this.config['unit-scale']) {
            this.config['unit-scale'] = parseFloat(shmi.localize(this.config['unit-scale']));
        }

        /* listen for resize events */
        this.resizeHandler = function() {
            clearTimeout(self.resizeTO);
            self.resizeTO = setTimeout(self.updateSize.bind(self), shmi.c("ACTION_RETRY_TIMEOUT"));
        };

        /* set translation distance */
        if (this.vertical) {
            this.size = parseInt(getComputedStyle(this.barElement).height) - this.config['image-margin'];
        } else {
            this.size = parseInt(getComputedStyle(this.barElement).width) - this.config['image-margin'];
        }

        /* Control initialized */
    },
    updateSize: function() {
        if (this.active) {
            if (this.vertical) {
                this.size = parseInt(getComputedStyle(this.barElement).height) - this.config['image-margin'];
            } else {
                this.size = parseInt(getComputedStyle(this.barElement).width) - this.config['image-margin'];
            }
            this.setValue(this.value);
        }
    },
    /**
     * Sets the current value
     *
     * @param value - new value to set
     */
    onSetValue: function(value) {
        this.value = parseFloat(value);
        this.draw();
    },
    /**
     * Draws the Linear Gauge
     *
     */
    draw: function() {
        var self = this,
            vs = self.vars.valueSettings,
            min = vs.min,
            max = vs.max;

        if (shmi.visuals.session.deactivateControls) {
            this.anim.stop();
            setTimeout(this.draw.bind(this), shmi.c("ACTION_RETRY_TIMEOUT"));
            return;
        }
        shmi.caf(this.requestID);
        this.requestID = shmi.raf(this.updateText.bind(this));

        if (max === min) {
            this.barScale = this.config['bar-scale-max'];
        } else {
            this.barScale = (this.config['bar-scale-max'] - this.config['bar-scale-min']) *
            ((this.value - min) / (max - min)) + this.config['bar-scale-min'];
        }

        if (this.barScale > this.config['bar-scale-max']) {
            this.barScale = this.config['bar-scale-max'];
        } else if (this.barScale < this.config['bar-scale-min']) {
            this.barScale = this.config['bar-scale-min'];
        }

        var animTime = this.config['anim-time'];
        if (this.config.item) {
            var item = shmi.visuals.session.ItemManager.items[this.config.item];
            if (item === undefined) {
                shmi.log("[RadialCanvas] item '" + this.config.item + "' undefined in manager", 1);
            } else {
                animTime = ((this.config['dynamic-anim-time'] === false) || (item.interval > this.config['anim-time'])) ? this.config['anim-time'] : Math.round(item.interval);
            }
        }

        this.anim.start(this.barScale, animTime);
    },
    updateText: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        self.valueElement.textContent = nv.formatOutput(self.value, self);
    },
    /**
     * Sets properties for subscribed data
     *
     * @param min - min value
     * @param max - max value
     * @param step - value stepping
     */
    onSetProperties: function(min, max, step) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.setProperties(self, arguments);
        if (typeof self.value === "number") {
            self.draw();
        }
    },
    /**
     * Updates the bar element of the Linear Gauge
     *
     * @param val - new scale of bar element
     */
    updateHandle: function(val) {
        if (!this.vertical) {
            this.barElement.style.webkitTransform = 'scale3d(' + val.toFixed(3) + ', 1, 1)';
            this.barElement.style.oTransform = 'scale3d(' + val.toFixed(3) + ', 1, 1)';
            this.barElement.style.msTransform = 'scale3d(' + val.toFixed(3) + ', 1, 1)';
            this.barElement.style.mozTransform = 'scale3d(' + val.toFixed(3) + ', 1, 1)';
            this.barElement.style.transform = 'scale3d(' + val.toFixed(3) + ', 1, 1)';

            this.highlightElement.style.webkitTransform = 'translate3d(' + ((val * this.size).toFixed(3)) + 'px, 0, 0)';
            this.highlightElement.style.oTransform = 'translate3d(' + ((val * this.size).toFixed(3)) + 'px, 0, 0)';
            this.highlightElement.style.msTransform = 'translate3d(' + ((val * this.size).toFixed(3)) + 'px, 0, 0)';
            this.highlightElement.style.mozTransform = 'translate3d(' + ((val * this.size).toFixed(3)) + 'px, 0, 0)';
            this.highlightElement.style.transform = 'translate3d(' + ((val * this.size).toFixed(3)) + 'px, 0, 0)';
        } else {
            this.barElement.style.webkitTransform = 'scale3d(1, ' + val.toFixed(3) + ', 1)';
            this.barElement.style.oTransform = 'scale3d(1, ' + val.toFixed(3) + ', 1)';
            this.barElement.style.msTransform = 'scale3d(1, ' + val.toFixed(3) + ', 1)';
            this.barElement.style.mozTransform = 'scale3d(1, ' + val.toFixed(3) + ', 1)';
            this.barElement.style.transform = 'scale3d(1, ' + val.toFixed(3) + ', 1)';

            this.highlightElement.style.webkitTransform = 'translate3d(0,' + (1 - (val * this.size)).toFixed(3) + 'px, 0)';
            this.highlightElement.style.oTransform = 'translate3d(0,' + (1 - (val * this.size)).toFixed(3) + 'px, 0)';
            this.highlightElement.style.msTransform = 'translate3d(0,' + (1 - (val * this.size)).toFixed(3) + 'px, 0)';
            this.highlightElement.style.mozTransform = 'translate3d(0,' + (1 - (val * this.size)).toFixed(3) + 'px, 0)';
            this.highlightElement.style.transform = 'translate3d(0,' + (1 - (val * this.size)).toFixed(3) + 'px, 0)';
        }
    },
    /**
     * Enables the LinearGauge2
     *
     */
    onEnable: function() {
        var self = this;
        if (self.vars.conditional !== null) {
            if (self.vars.conditional.item !== self.config.item) {
                self.vars.conditional.item = self.config.item;
            }
        } else if (self.config.item) {
            self.vars.conditional = shmi.createConditional(self.element, self.config.item);
        }

        if (self.vars.conditional) {
            self.vars.conditional.enable();
        }

        /* instead only calculate size without calling setValue */
        if (this.vertical) {
            this.size = parseInt(getComputedStyle(this.barElement).height) - this.config['image-margin'];
        } else {
            this.size = parseInt(getComputedStyle(this.barElement).width) - this.config['image-margin'];
        }

        if (this.config.item) {
            this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
        }

        if (!this.resizeListenerAdded) {
            window.addEventListener('resize', this.resizeHandler, false);
            window.addEventListener('visuals-layout-change', this.resizeHandler, false);
            this.resizeListenerAdded = true;
        }

        shmi.log("[LinearGauge2] enabled", 1);
    },
    /**
     * Disables the LinearGauge2
     *
     */
    onDisable: function() {
        var self = this;

        if (this.resizeListenerAdded) {
            window.removeEventListener('resize', this.resizeHandler);
            window.removeEventListener('visuals-layout-change', this.resizeHandler);
            this.resizeListenerAdded = false;
        }

        if (self.vars.conditional) {
            self.vars.conditional.disable();
        }

        this.anim.stop();
        if (this.config.item) {
            shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
        }
        shmi.log("[LinearGauge2] disabled", 1);
    },
    onLock: function() {

    },
    onUnlock: function() {

    },
    setLabel: function(labelText) {
        var self = this;
        if (self.config['auto-label'] && self.labelElement) {
            self.labelElement.textContent = shmi.localize(labelText);
        }
    },
    setUnitText: function(unitText) {
        var self = this;
        if (self.config['auto-unit-text'] && self.unitElement) {
            self.unitElement.textContent = shmi.localize(unitText);
        }
    }
};

shmi.extend(shmi.visuals.controls.LinearGauge2, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");
/**
 * Creates a new ListBox control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.ListBox = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'list-box');
    shmi.def(this.config, 'template', 'default/list-box');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'label', 'list-box');
    shmi.def(this.config, 'options', []);

    this.active = false;
    this.entries = [];
    this.entryElements = [];
    this.mouseListener = null;
    this.touchListener = null;
    this.selected = -1;
    this.onSelect = null;
    this.listElement = null;

    this.startup();
};

shmi.visuals.controls.ListBox.prototype = {
    uiType: "list-box",
    events: ["select"],
    getClassName: function() {
        return "ListBox";
    },
    onInit: function() {
        var self = this;
        shmi.log("[ListBox] initializing...", 1);
        var label_el = shmi.getUiElement('label', this.element);
        if (label_el) {
            label_el.textContent = shmi.localize(this.config.label);
        }
        if (Array.isArray(self.config.options)) {
            self.config.options.forEach(function(opt) {
                self.entries.push(opt);
            });
        }
        this.listElement = shmi.getUiElement('list-items', this.element);
        if (!this.listElement) {
            shmi.notify("[ListBox] template missing 'list-items' element");
        }
        var funcs = {};
        funcs.onClick = function(x, y, event) {
            this.setSelected(event.target);
            self.fire("select", { value: self.getSelectedEntry() });
        }.bind(this);
        this.mouseListener = new shmi.visuals.io.MouseListener(this.listElement, funcs);
        this.touchListener = new shmi.visuals.io.TouchListener(this.listElement, funcs);
        shmi.log("[ListBox] initialized", 1);
    },
    /**
     * Enables the Control
     *
     */
    onEnable: function() {
        this.mouseListener.enable();
        this.touchListener.enable();
        shmi.log("[ListBox] enabled", 1);
    },
    /**
     * Disables the Control
     *
     */
    onDisable: function() {
        this.mouseListener.disable();
        this.touchListener.disable();
        shmi.log("[ListBox] disabled", 1);
    },
    addEntry: function(value) {
        if (!this.initialized) {
            setTimeout(function() {
                this.addEntry(value);
            }.bind(this), shmi.c("ACTION_RETRY_TIMEOUT"));
            return;
        }
        var entry = document.createElement('div');
        entry.setAttribute('data-ui', 'list-item');
        entry.setAttribute('class', 'list-item');
        entry.textContent = value;
        this.entries.push(value);
        this.entryElements.push(entry);
        this.listElement.appendChild(entry);
        shmi.log("[ListBox] entry added: " + value, 1);
    },
    removeEntryByValue: function(value) {
        var idx = -1;
        this.entries.forEach(function(ent, jdx) {
            if (ent.value === value) {
                idx = jdx;
            }
        });
        if (idx !== -1) {
            this.listElement.removeChild(this.entryElements[idx]);
            this.entries.splice(idx, 1);
            this.entryElements.splice(idx, 1);
            if (this.selected === idx) {
                this.setSelectedIndex(-1);
            }
        }
    },
    removeEntry: function(index) {
        if (index === this.selected) {
            this.selected = -1;
        }
        this.listElement.removeChild(this.entryElements[index]);
        this.entryElements.splice(index, 1);
        this.entries.splice(index, 1);
    },
    removeAll: function() {
        while (this.entries.length) {
            this.removeEntry(0);
        }
        this.selected = -1;
    },
    setSelected: function(element) {
        for (var i = 0; i < this.entryElements.length; i++) {
            if (this.entryElements[i] === element) {
                if (this.selected !== -1) {
                    shmi.removeClass(this.entryElements[this.selected], 'selected');
                }
                if (i === this.selected) {
                    this.selected = -1;
                } else {
                    this.selected = i;
                    shmi.addClass(this.entryElements[i], 'selected');
                }
                break;
            }
        }
        if (this.onSelect) {
            this.onSelect(this.selected);
        }
    },
    setSelectedIndex: function(index) {
        if (this.selected !== -1) {
            shmi.removeClass(this.entryElements[this.selected], 'selected');
        }
        if (index === this.selected) {
            this.selected = -1;
        } else if (index === -1) {
            this.selected = -1;
        } else {
            this.selected = index;
            shmi.addClass(this.entryElements[index], 'selected');
        }
        if (this.onSelect) {
            this.onSelect(this.selected);
        }
    },
    getSelectedEntry: function() {
        if (this.selected === -1) {
            return null;
        } else {
            return this.entries[this.selected];
        }
    },
    getSelectedIndex: function() {
        return this.selected;
    },
    onLock: function() {
        if (this.active) {
            this.mouseListener.disable();
            this.touchListener.disable();
            shmi.addClass(this.element, 'locked');
        }
    },
    onUnlock: function() {
        if (this.active) {
            this.mouseListener.enable();
            this.touchListener.enable();
            shmi.removeClass(this.element, 'locked');
        }
    },
    setLabel: function(labelText) {
        var self = this,
            labelElement = shmi.getUiElement("label", self.element);
        if (labelElement) {
            labelElement.textContent = shmi.localize(labelText);
        }
    }
};

shmi.extend(shmi.visuals.controls.ListBox, shmi.visuals.core.BaseControl);

/**
 * Creates a new LocalScript control.
 *
 * The LocalScript control can be used to execute arbitrary javascript code at
 * predefined places in an hmi-app. The configured script will be run every time
 * the LocalScript control is enabled.
 *
 * @constructor
 * @param {HTMLElement} element root element of control
 * @param {object} config control configuration object
 */
shmi.visuals.controls.LocalScript = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'url', null);
    shmi.def(this.config, 'name', null);

    this.run = false;
    this.loaded = false;

    this.script = "";

    this.startup();
};

shmi.visuals.controls.LocalScript.prototype = {
    uiType: "local-script",
    isContainer: false,
    getClassName: function() {
        return "LocalScript";
    },
    /**
     * loads configured script file
     *
     */
    onInit: function() {
        if (this.config.url) {
            shmi.loadResource(this.config.url, this._loadCB.bind(this));
        } else if (this.config.module) {
            try {
                this.module = shmi.requires(this.config.module);
                if (typeof this.module.run !== "function") {
                    throw new Error("local-script module has no 'run' function: " + this.config.module);
                }
            } catch (exc) {
                this.element.textContent = "UNKNOWN_MODULE: " + this.config.module;
                this.module = {
                    run: function() {}
                };
            }
            this.loaded = true;
        } else {
            console.log("[LocalScript] no script configured");
        }
    },
    /**
     * callback for load of script file. executes the script if the control is
     * enabled when the callback is run.
     *
     * @param {string} data script data
     * @param {boolean} failed true if script load failed, false else
     */
    _loadCB: function(data, failed) {
        if (!failed) {
            this.script = data;
            this.loaded = true;
            if (this.active) {
                this.run = true;
                eval(this.script);
            }
        } else {
            console.log("[LocalScript] failed to load script:", this.config.url);
        }
    },
    /**
     * run when control is enabled. executes configured script.
     *
     */
    onEnable: function() {
        if (!this.run && this.loaded) {
            if (this.config.module) {
                this.module.run(this);
            } else {
                eval(this.script);
            }
        }
    },
    /**
     * run when control is disabled
     *
     */
    onDisable: function() {
        this.run = false;
    }
};

shmi.extend(shmi.visuals.controls.LocalScript, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");
/**
 * Creates a new Login control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.Login = function(element, config) {
    this.element = element;

    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'login');
    shmi.def(this.config, 'template', 'default/login');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'userEntry', '<div data-ui="user" class="user"><div class="userPic"></div><div class="user_detail"><h1 data-ui="user-name"><%= USER %></h1></div></div>');

    this.userElement = null;
    this.passwordElement = null;
    this.loginButtonElement = null;
    this.clearButtonElement = null;
    this.autoLoginButtonElement = null;
    this.clickCounter = 0;

    if (/Firefox/.test(navigator.userAgent)) {
        this.innerWidth = window.innerWidth - 1;
        this.innerHeight = window.innerHeight - 1;
    } else {
        this.innerWidth = window.innerWidth;
        this.innerHeight = window.innerHeight;
    }

    this.userListContent = "";

    this.startup();
};

shmi.visuals.controls.Login.prototype = {
    uiType: "login",
    getClassName: function() {
        return "Login";
    },
    onInit: function() {
        var self = this,
            session = shmi.requires("visuals.session");
        if (!self.element) {
            shmi.log("[Login] no base element provided", 3);
            return;
        }
        self.userElement = shmi.getUiElement('user-field', self.element);
        if (!self.userElement) {
            shmi.log("[Login] no user-field element provided", 3);
            return;
        }
        shmi.addClass(self.userElement, 'locked');
        self.userElement.setAttribute('disabled', true);
        self.passwordElement = shmi.getUiElement('password-field', self.element);
        if (!self.passwordElement) {
            shmi.log("[Login] no password-field element provided", 3);
            return;
        }
        shmi.addClass(self.passwordElement, 'locked');
        self.passwordElement.setAttribute('disabled', true);
        self.loginButtonElement = shmi.getUiElement('login-button', self.element);
        if (!self.loginButtonElement) {
            shmi.log("[Login] no login-button element provided", 3);
            return;
        }
        self.clearButtonElement = shmi.getUiElement('clear-button', self.element);
        if (!self.clearButtonElement) {
            shmi.log("[Login] no clear-button element provided", 3);
            return;
        }

        self.autoLoginButtonElement = shmi.getUiElement('autologin-button', self.element);
        if (!self.autoLoginButtonElement) {
            shmi.log("[Login] no autologin-button element provided", 2);
        }

        /* initialize connection status display */
        self.statusElement = shmi.getUiElement('status', self.element);
        if (self.statusElement) {
            self.statusLabelElement = shmi.getUiElement('label', self.statusElement);
        }

        if (self.statusElement && self.statusLabelElement) {
            shmi.addClass(self.statusElement, 'connecting');
            self.statusLabelElement.textContent = 'connecting ...';
        }
        /* establish connection to configured connect-host */
        session.SocketConnection.connect(function(status) {
            if (status === 0) {
                session.UserManager.requestUserList(self.applyUserList.bind(self));
            } else if (self.statusElement && self.statusLabelElement) {
                shmi.removeClass(self.statusElement, 'connecting');
                shmi.addClass(self.statusElement, 'error');
                self.statusLabelElement.textContent = 'connection error';
            } else {
                console.error("[Login] Error establishing WebSocket connection");
            }
        });

        const request = new XMLHttpRequest();
        request.open("GET", '.webiq/preview.png', true);
        request.send();
        request.onload = function() {
            if (request.status !== 200) {
                document.getElementById('project-image').style.backgroundImage = 'url("pics/system/controls/login/placeholder.png")';
            }
        };

        /* all elements found, set reference to login in session */
        session.Login = this;
    },
    onEnable: function() {
        if (Array.isArray(this.listeners)) {
            this.listeners.forEach(function(lst) {
                lst.enable();
            });
        }
    },
    onDisable: function() {
        if (Array.isArray(this.listeners)) {
            this.listeners.forEach(function(lst) {
                lst.disable();
            });
        }
    },
    /**
     * Applies retrieved user list
     *
     */
    applyUserList: function() {
        var self = this;

        var users = shmi.visuals.session.UserManager.userList,
            iter = shmi.requires("visuals.tools.iterate.iterateObject");
        /* clear user list */
        self.userListContent = "";
        /* rebuild user list */
        iter(users, function(val, name) {
            self.userListContent += shmi.evalString(self.config.userEntry, { USER: name });
        });

        this.userListElement = document.createElement('div');
        shmi.addClass(this.userListElement, 'hidden');
        shmi.addClass(this.userListElement, 'user-list');

        /* create user-list tool-tip */
        var taDiv = document.createElement('div');
        taDiv.className = 'toolArrow';
        var ttDiv = document.createElement('div');
        ttDiv.className = 'toolTip';
        ttDiv.innerHTML = this.userListContent;
        this.userListElement.appendChild(taDiv);
        this.userListElement.appendChild(ttDiv);

        this.userNameInputElement = shmi.getUiElement('user-name-input', this.element);
        if (this.userNameInputElement) {
            this.userNameInputElement.appendChild(this.userListElement);
        }

        var um = shmi.visuals.session.UserManager,
            appUrl = null,
            ses = shmi.visuals.session;

        um.requestAutoLogin((error) => {
            if (this.statusElement && this.statusLabelElement) {
                this.statusLabelElement.textContent = 'connected';
                shmi.removeClass(this.statusElement, 'connecting');
                shmi.addClass(this.statusElement, 'connected');
            }

            if (error) {
                shmi.removeClass(this.userElement, 'locked');
                shmi.removeClass(this.passwordElement, 'locked');
                this.userElement.removeAttribute('disabled');
                this.passwordElement.removeAttribute('disabled');
                this.initListeners();
                this.loginCallback(error);
            } else {
                this.loginCallback(0);
            }
        });

        if (ses.URLParameters.autoLogin) {
            appUrl = new URL(window.location.href);
            appUrl.searchParams.delete("autoLogin");
            window.history.replaceState({}, document.title, appUrl.toString());
        }
    },
    initListeners: function() {
        var self = this;

        /* add event listeners */
        this.userElement.addEventListener('focus', function() {
            shmi.removeClass(this.userListElement, 'hidden');
        }.bind(this));

        this.userElement.addEventListener('click', function() {
            var appConfig = shmi.visuals.session.config,
                keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

            if (!keyboardEnabled) { //does nothing if not enabled
                return;
            }

            this.clickCounter++;
            if (this.clickCounter > 1) {
                var params = {
                    "value": self.userElement.value,
                    "password-input": false,
                    "select-box-enabled": true,
                    "callback": function(success, input) {
                        if (success) {
                            self.userElement.value = input;
                        }
                    }
                };

                shmi.keyboard(params);
            }
        }.bind(this));

        this.userElement.addEventListener('blur', function() {
            if (this.clickOnUserList) {
                return;
            }
            shmi.addClass(this.userListElement, 'hidden');
        }.bind(this));

        this.passwordElement.addEventListener('focus', function() {
            var appConfig = shmi.visuals.session.config,
                keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`
            if (!keyboardEnabled) { //does nothing if not enabled
                return;
            }

            var params = {
                "value": self.passwordElement.value,
                "password-input": true,
                "select-box-enabled": true,
                "callback": function(success, input) {
                    if (success) {
                        self.passwordElement.value = input;
                    }
                }
            };

            shmi.keyboard(params);
        });

        var userfields = shmi.getUiElements('user', this.userListElement);
        if (Array.isArray(this.listeners)) {
            this.listeners.forEach(function(lst) {
                lst.disable();
            });
        }
        this.listeners = [];
        for (var i = 0; i < userfields.length; i++) {
            var funcs = {};
            funcs.onPress = function() {
                this.clickOnUserList = true;
            }.bind(this);
            funcs.onRelease = function() {
                this.clickOnUserList = false;
            }.bind(this);
            funcs.onClick = (function(field) {
                this.listeners.push(new shmi.visuals.io.MouseListener(field, funcs));
                this.listeners.push(new shmi.visuals.io.TouchListener(field, funcs));
                return function() {
                    var username = shmi.getUiElement('user-name', field);
                    this.userElement.value = username.innerHTML;
                    shmi.log("[Login] username: " + username.innerHTML, 1);
                    shmi.raf(function() {
                        shmi.addClass(this.userListElement, 'hidden');
                        this.passwordElement.focus();
                    }.bind(this));
                }.bind(this);
            }.bind(this))(userfields[i]);
        }

        this.userElement.addEventListener('keypress', function(event) {
            if (event.keyCode === 13) {
                shmi.raf(function() {
                    this.passwordElement.focus();
                }.bind(this));
            }
        }.bind(this));

        this.passwordElement.addEventListener('keypress', function(event) {
            if (event.keyCode === 13) {
                this.passwordElement.blur();
                shmi.raf(function() {
                    shmi.visuals.session.UserManager.requestLogin(this.userElement.value.trim(), this.passwordElement.value, this.loginCallback.bind(this));
                }.bind(this));
            }
        }.bind(this));

        var logBtnFuncs = {};
        logBtnFuncs.onClick = function() {
            shmi.visuals.session.UserManager.requestLogin(this.userElement.value.trim(), this.passwordElement.value, this.loginCallback.bind(this));
        }.bind(this);
        this.listeners.push(new shmi.visuals.io.MouseListener(this.loginButtonElement, logBtnFuncs));
        this.listeners.push(new shmi.visuals.io.TouchListener(this.loginButtonElement, logBtnFuncs));

        var clrBtnFuncs;
        if (this.clearButtonElement) {
            clrBtnFuncs = {};
            clrBtnFuncs.onClick = function() {
                this.userElement.value = '';
                this.passwordElement.value = '';
            }.bind(this);
            this.listeners.push(new shmi.visuals.io.MouseListener(this.clearButtonElement, clrBtnFuncs));
            this.listeners.push(new shmi.visuals.io.TouchListener(this.clearButtonElement, clrBtnFuncs));
        }

        if (this.autoLoginButtonElement) {
            var io = shmi.requires("visuals.io"),
                s = shmi.requires("visuals.session"),
                um = s.UserManager,
                observerFuncs = {
                    onClick: function() {
                        um.requestLogin(s.config['auto-login'].user, s.config['auto-login'].password, self.loginCallback.bind(self));
                    }
                },
                labelElement = shmi.getUiElement("button-label", this.autoLoginButtonElement);

            //perform auto login
            if (s.config['auto-login'] && s.config['auto-login'].enabled) {
                if (labelElement) {
                    labelElement.textContent = shmi.evalString("Login as default user '<%= USER %>'", {
                        USER: s.config['auto-login'].user
                    });
                }
                shmi.removeClass(this.autoLoginButtonElement, "hidden");
                this.listeners.push(new io.MouseListener(this.autoLoginButtonElement, observerFuncs));
                this.listeners.push(new io.TouchListener(this.autoLoginButtonElement, observerFuncs));
            }
        }

        if (this.active === true) {
            this.listeners.forEach(function(lst) {
                lst.enable();
            });
        }
    },
    /**
     * Called after login request returns
     *
     * @param {object} error error information if login failed
     */
    loginCallback: function(error) {
        shmi.log("[Login] login-callback called", 2);
        var s = shmi.visuals.session;
        if (s.UserManager.currentUser && s.UserManager.currentUser.loggedIn) {
            shmi.log("[Login] login succeeded", 1);
            var self = this;
            /* clear DOM */
            document.body.innerHTML = "";

            if (s.config['show-load-screen']) {
                /* create loading overlay */
                var overlayConfig = shmi.pkg("visuals.session.SysControlConfig.loadingOverlay");
                this.bgDiv = document.createElement(overlayConfig.tagName || 'DIV');
                shmi.addClass(this.bgDiv, overlayConfig['class-name'] || 'loading-box-overlay');
                if (overlayConfig.template) {
                    shmi.loadResource(shmi.c("TEMPLATE_PATH") + overlayConfig.template + shmi.c("TEMPLATE_EXT"), function(data, failed) {
                        if (!failed) {
                            self.bgDiv.innerHTML = data;
                        }
                    });
                }
                document.body.appendChild(this.bgDiv);
            }
            /* retrieve alarm-list */
            s.AlarmManager.requestAlarmList();

            /* load locale */
            shmi.raf(function() {
                shmi.loadResource(shmi.evalString(shmi.c("LOCALE_PATH_PATTERN"), { index: s.UserManager.currentUser.locale }), self.loadProject.bind(self));
            });
        } else if (error && error.category === "shmi:connect:license" && error.errc === 4) {
            shmi.notify("${login.error.too-many-clients}", "${V_NOTIFICATION}");
            shmi.log(`[Login] login failed - ${error.message}`, 1);
        } else if (error && error.message !== "Auto-Login credentials exhausted") {
            shmi.notify("${login.error.invalid-credentials}", "${V_NOTIFICATION}");
            shmi.log("[Login] login failed", 1);
        }
    },
    /**
     * Applies loaded project configuration
     *
     * @param {string} responseText - content of project configuration
     * @param {boolean} failed - true if loading failed, false else
     * @param {string} url - url of loaded locale
     */
    loadProject: function(responseText, failed, url) {
        var session = shmi.requires("visuals.session");
        session.locale = session.locale || {};
        if (!failed) {
            var iter = shmi.requires("visuals.tools.iterate.iterateObject"),
                locale = {};
            try {
                locale = JSON.parse(responseText);
                iter(locale, function(val, prop) {
                    session.locale[prop] = val;
                });
            } catch (exc) {
                console.error("[Login] failed to parse locale file:", url, exc);
            }
        }

        var layout = null;
        if (typeof session.config.layout === "string") {
            layout = session.config.layout;
        } else {
            layout = shmi.c("DEFAULT_LAYOUT_URL");
        }

        if (session.URLParameters.layout !== undefined) {
            layout = session.URLParameters.layout;
        }
        session.LayoutURL = layout;
        this.layout_url = layout;

        if (layout.endsWith(".json")) {
            shmi.loadResource(`json/layouts/${layout}`, (layoutResponse, layoutFailed) => {
                const model = shmi.requires("visuals.model");
                if (!layoutFailed) {
                    try {
                        model.parse(layoutResponse);
                        this.applyProject(model.getTemplate("root"), false);
                    } catch (err) {
                        console.error("Error parsing app model:", err);
                        this.applyProject(null, true);
                    }
                } else {
                    this.applyProject(null, true);
                }
            });
        } else {
            shmi.loadResource(layout, this.applyProject.bind(this));
        }
    },
    /**
     * Applies the loaded project entrypoint
     *
     * @param responseText - content of project entrypoint
     * @param failed - true if loading failed, false elese
     */
    applyProject: function(responseText, failed) {
        if (failed) {
            shmi.log("[Login] could not load project entrypoint.\n" + this.layout_url, 3);
            return;
        }
        var s = shmi.visuals.session;
        /* Set Project Content */
        var docFragment = document.createDocumentFragment();
        var fragDiv = document.createElement('div');
        fragDiv.innerHTML = responseText;
        docFragment.appendChild(fragDiv);
        while (docFragment.firstChild.firstChild) {
            document.body.appendChild(docFragment.firstChild.firstChild);
        }
        s.ProjectSource = responseText;

        /* parse project */
        let controlInstances = shmi.visuals.parser.parseProject();
        var viewportMeta = document.getElementById('viewportMeta');
        if (viewportMeta) {
            viewportMeta.setAttribute('content', viewportMeta.getAttribute('content').replace(
                'user-scalable=no', 'user-scalable=yes'));
        }

        /* check init status of controls */
        shmi.waitOnInit(controlInstances, () => {
            var iter = shmi.requires("visuals.tools.iterate.iterateObject"),
                resizeEvt = null;

            shmi.log("[Login] All controls initialized", 2);

            /* do not run startscripts if none define or shmi-project is active */
            if (shmi.visuals.session.startScripts && (shmi.visuals.session.config["session-client"] === "visuals.defaultClient")) {
                iter(shmi.visuals.session.startScripts, function(val, script) {
                    try {
                        shmi.visuals.session.startScripts[script]();
                    } catch (exc) {
                        shmi.log("[Login] failed to execute start-script '" + script + "': " + exc, 2);
                    }
                });
            }

            /* fire resize event to let controls know that the layout is complete */
            resizeEvt = document.createEvent("Event");
            resizeEvt.initEvent("resize", true, true);
            window.dispatchEvent(resizeEvt);

            /* fade out load screen if present */
            if (s.config['show-load-screen']) {
                shmi.raf(function() {
                    if (s.config['load-screen-transition'] === false) {
                        document.body.removeChild(this.bgDiv);
                    } else {
                        this.bgDiv.style.opacity = 0;
                        this.bgDiv.addEventListener('transitionend', function() {
                            document.body.removeChild(this.bgDiv);
                        }.bind(this));
                        this.bgDiv.addEventListener('webkitTransitionEnd', function() {
                            document.body.removeChild(this.bgDiv);
                        }.bind(this));
                    }
                }.bind(this));
            }
            shmi.fire('parser-ready', {}, shmi.visuals.session);
        });
    }
};

shmi.extend(shmi.visuals.controls.Login, shmi.visuals.core.BaseControl);

/**
 * Numpad
 *
 * Configuration options (default):
 *
 * {
 *      "class-name": uiType,
 *      "name": null,
 *      "template": "default/numpad/numpad",
 *      "label": uiType,
 *      "infoblockLimitsLabel": "${NUMPAD_LIMITS}",
 *      "inputLimitMinimumLabel": "${NUMPAD_INPUT_LIMIT_MINIMUM}",
 *      "inputLimitMaximumLabel": "${NUMPAD_INPUT_LIMIT_MAXIMUM}",
 *      "initially-visible": false,
 *      "top-level": false,
 *      "value": "0",                      // Init value to modify.
 *      "callback": null,                  // callback with the result value. Will be returned like "callback(Number value)"
 *      "unit": "",                        // what we are counting. e.g.: "%" or "$" -> "95,50 %""
 *      "precision": ",",                  // character to display as the decimal dot
 *      "max-chars": 10,                   // number of max characters of the input length
 *      "type": shmi.c("TYPE_FLOAT"),      // can be "TYPE_FLOAT" or "TYPE_INT"
 *      "precision": 2,                    // numbers after the decimal dot. Set to "-1" if unlimited.
 *      "min": -100,                       // number below this cannot be submitted to the callback
 *      "max": 100                         // number larger than this cannot be submitted to the callback
 *      "enable-key-codes": [1,2]          // Array of keycodes - unconfigurable through Designer
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 */
(function() {
    'use strict';

    //variables for reference in control definition
    var className = "Numpad", //control name in camel-case
        uiType = "numpad", //control keyword (data-ui)
        isContainer = false;

    //example - default configuration
    var defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/numpad/numpad-box",
        "label": uiType,
        "inputLimitMinimumLabel": "${NUMPAD_INPUT_LIMIT_MINIMUM}", // AUTO_OR_INPUT_LOCALIZED_STRING
        "inputLimitMaximumLabel": "${NUMPAD_INPUT_LIMIT_MAXIMUM}", // AUTO_OR_INPUT_LOCALIZED_STRING
        "initially-visible": false, // checkbox
        "top-level": false,
        "value": "0", // INPUT_NUMERIC
        "max-chars": 10, // INPUT_NUMERIC
        "callback": null, // X
        "unit": "", // UNIT_TEXT
        "decimal-delimiter": ".", // TEXT
        "type": shmi.c("TYPE_FLOAT"), // DROPDOWN
        "precision": 2, // NUMERIC
        "min": -100, // NUMERIC
        "max": 100, // NUMERIC
        "enable-key-codes": [] // X
    };

    //setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * errMessage object contains several function to add or remove the messages from the numpad
     *
     */
    var errMessage = {
        add: function(self, name, msgText, timeout) {
            if (self.vars.errTextElem && name && msgText) {
                if (!self.vars.messages[name]) {
                    if (Object.keys(self.vars.messages).length === 0) {
                        shmi.addClass(self.vars.errTextElem, "visible");
                    }

                    self.vars.messages[name] = document.createElement("div");
                    shmi.addClass(self.vars.messages[name], 'err-msg-' + name);
                    self.vars.errTextElem.appendChild(self.vars.messages[name]);

                    shmi.addClass(self.vars.messages[name], 'fadein');
                    setTimeout(function() {
                        if (self.vars.messages[name]) {
                            shmi.removeClass(self.vars.messages[name], 'fadein');
                        }
                    }, 250);
                }

                self.vars.messages[name].innerHTML = msgText;
                if (!isNaN(Number(timeout))) {
                    setTimeout(function() {
                        errMessage.remove(self, name);
                    }, Number(timeout));
                }
            }
        },
        remove: function(self, name) {
            if (self.vars.errTextElem && name && self.vars.messages[name]) {
                shmi.addClass(self.vars.messages[name], 'fadeout');

                setTimeout(function() {
                    if (self.vars.messages[name]) {
                        self.vars.errTextElem.removeChild(self.vars.messages[name]);
                    }
                    delete self.vars.messages[name];
                }, 250);
            }
        },
        clear: function(self) {
            if (self.vars.errTextElem && Object.keys(self.vars.messages).length > 0) {
                Object.keys(self.vars.messages).forEach(function(name) {
                    errMessage.remove(self, name);
                });
                self.vars.messages = {};
                setTimeout(function() {
                    shmi.removeClass(self.vars.errTextElem, "visible");
                    self.vars.errTextElem.innerHTML = '';
                }, 250);
            }
        }
    };

    // declare private functions - START
    /**
     * setter/getter for the result
     * @param {object} self
     * @param {string|undefined} val
     */
    function value(self, val) {
        if (val !== undefined) {
            self.config.value = val;
        }
        if (validate(self.config.value, self)) {
            shmi.removeClass(self.vars.inputDisplayElement, 'error');
        } else {
            shmi.addClass(self.vars.inputDisplayElement, 'error');
        }

        return self.config.value;
    }

    /**
     * validate value in the context self
     * @param {string} val
     * @param {object} self
     */
    function validate(val, self) {
        var valSplited,
            regExp = new RegExp('[^0-9\'' + self.config["decimal-delimiter"] + '-]', 'i'),
            valAsNumber = Number(String(val).replace(self.config["decimal-delimiter"], ".")),
            msg = '';

        // Change input maxlength attiubute according to presence of decimal-delimiter and minus
        var additionalChars = 0;
        if (val.indexOf(self.config["decimal-delimiter"]) !== -1) {
            additionalChars++;
        }
        if (val.indexOf('-') === 0) {
            additionalChars++;
        }
        self.vars.inputDisplayElement.setAttribute("maxlength", self.config["max-chars"] + additionalChars);

        if (self.config.type === shmi.c("TYPE_INT") && !Number.isInteger(valAsNumber)) {
            msg = shmi.localize('${numpad_entered_value_is_not_integer}');
            shmi.log("[Numpad] " + msg, 1);
            errMessage.remove(self, 'no-number');
            errMessage.add(self, 'no-integer', msg);
            return false;
        } else if (isNaN(valAsNumber)) {
            msg = shmi.localize('${numpad_entered_value_is_not_number}');
            shmi.log("[Numpad] " + msg, 1);
            errMessage.add(self, 'no-number', msg);
            errMessage.remove(self, 'no-integer');
            return false;
        } else {
            errMessage.remove(self, 'no-number');
            errMessage.remove(self, 'no-integer');
        }

        if (val.match(regExp)) {
            msg = shmi.localize('${numpad_entered_value_invalid}');
            shmi.log("[Numpad] " + msg, 1);
            errMessage.add(self, 'invalid', msg);
            return false;
        } else {
            errMessage.remove(self, 'invalid');
        }

        if ((Number(self.config.precision) !== -1) && (val.indexOf(self.config["decimal-delimiter"]) !== -1)) {
            valSplited = val.split(self.config["decimal-delimiter"]);
            if (valSplited[1] !== undefined && valSplited[1].length > self.config.precision) {
                msg = shmi.evalString(shmi.localize('${numpad_too_many_digits}'), {
                    precision: self.config.precision
                });
                shmi.log("[Numpad] " + msg, 1);
                errMessage.add(self, 'exceeded-decimal-digits', msg);
                return false;
            } else {
                errMessage.remove(self, 'exceeded-decimal-digits');
            }
        } else {
            errMessage.remove(self, 'exceeded-decimal-digits');
        }

        if ((typeof self.config.min === "number") && valAsNumber < self.config.min) {
            msg = shmi.localize('${numpad_entered_value_too_low}');
            shmi.log("[Numpad] " + msg, 1);
            errMessage.remove(self, 'max-error');
            errMessage.add(self, 'min-error', msg);
            return false;
        } else {
            errMessage.remove(self, 'min-error');
        }

        if ((typeof self.config.max === "number") && valAsNumber > self.config.max) {
            msg = shmi.localize('${numpad_entered_value_too_large}');
            shmi.log("[Numpad] " + msg, 1);
            errMessage.remove(self, 'min-error');
            errMessage.add(self, 'max-error', msg);
            return false;
        } else {
            errMessage.remove(self, 'max-error');
        }

        errMessage.clear(self);
        return true;
    }

    /**
     * insert char into the string and replace selected parts if any
     * @param {string} val
     * @param {string} string
     * @param {object} self
     */
    function insertCharIntoString(val, string, self) {
        string = String(string);

        // Allow additional Chars for decimal-delimiter and minus
        var additionalChars = 0;
        if (string.indexOf(self.config["decimal-delimiter"]) !== -1) {
            additionalChars++;
        }
        if (string.indexOf('-') === 0) {
            additionalChars++;
        }
        var newStr = false,
            maxChars = Number(self.config["max-chars"] + additionalChars);

        newStr = string.substr(0, self.vars.inputDisplayElement.selectionStart) + val + string.substr(self.vars.inputDisplayElement.selectionEnd);

        // replace first 0 with new val but add comma after it
        if (string === "0" && val !== self.config["decimal-delimiter"]) {
            newStr = val;
            // Check if new string exceeds the range of maximum characters
        } else if (maxChars && newStr.length > maxChars) {
            var msg = shmi.evalString(shmi.localize('${numpad_too_many_characters}'), {
                maxChars: maxChars - additionalChars
            });
            shmi.log("[Numpad] " + msg, 3);
            errMessage.add(self, 'max-chars', msg, 3000);

            // if multiple chars are pasted, insert only allowed length of val
            if (val.length > 1) {
                val = val.substr(0, (maxChars - string.length));
                return string.substr(0, self.vars.inputDisplayElement.selectionStart) + val + string.substr(self.vars.inputDisplayElement.selectionEnd);
            } else {
                return string;
            }
        }
        self.vars.inputDisplayElement.selectionStart = self.vars.inputDisplayElement.selectionEnd;
        self.vars.inputDisplayElement.focus();
        return newStr;
    }

    /**
     * @param {string} elId
     * @param {object} self
     */
    function onNumpadAction(elId, self) {
        var newValue;
        if (elId === undefined || self.vars.buttonsElements[elId] === undefined) {
            return;
        }
        newValue = self.vars.buttons[elId].action(value(self));
        self.vars.inputDisplayElement.value = value(self, newValue);
    }

    /** @param {object} self */
    function actionOk(self) {
        if (!validate(value(self), self)) {
            return;
        }
        if (typeof self.config.callback === "function") {
            self.config.callback(Number(value(self).replace(",", ".")));
        }
        self.hide();
    }

    /** @param {object} self */
    function actionClose(self) {
        self.hide();
    }

    //declare private functions - END

    //definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            titleElement: null,
            inputLimitMinimum: null,
            inputLimitMinimumLabel: null,
            inputLimitMaximum: null,
            inputLimitMaximumLabel: null,
            inputDisplayElement: null,
            errTextElem: null,
            unitElement: null,
            buttons: {},
            buttonsElements: [],
            containerElement: null,
            listeners: []
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            /* example - add import via function call */
            qm: function() {
                return shmi.visuals.session.QueryManager;
            }
        },
        events: ["open", "ok", "close"],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                // object to store error self.vars.messages in
                self.vars.messages = {};
                // Stringify the numeric value.
                self.config.value = String(self.config.value).replace(".", self.config["decimal-delimiter"]);

                // define valid key codes for keyboard inputs
                self.vars.keys = {
                    number: [48, 49, 50, 51, 52, 53, 54, 55, 56, 57],
                    numpad: [96, 97, 98, 99, 100, 101, 102, 103, 104, 105],
                    arrow: [37, 38, 39, 40],
                    startEndDelete: [35, 36, 46],
                    decimalDelimiters: [110, 188, 190],
                    minus: [189, 109],
                    plus: [107, 187],
                    ctrls: [/* backspace */ 8, /* enter */ 13, /* ctrl */ 17, /* esc*/ 27],
                    functionKeys: [112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123],
                    configKeyCodes: self.config["enable-key-codes"].map(function(code) {
                        return Number(code);
                    })
                };

                // Workaround for target versions of Edge not having Array.prototype.flat().
                // Equivalent: Object.values(self.vars.keys).flat()
                self.vars.keys.validKey = Array.prototype.concat.apply([], Object.values(self.vars.keys));
                self.vars.keys.validNumber = self.vars.keys.number.concat(self.vars.keys.numpad);

                // Define button actions here
                self.vars.buttons = {
                    "1": {
                        action: function(val) {
                            return insertCharIntoString("1", val, self);
                        }
                    },
                    "2": {
                        action: function(val) {
                            return insertCharIntoString("2", val, self);
                        }
                    },
                    "3": {
                        action: function(val) {
                            return insertCharIntoString("3", val, self);
                        }
                    },
                    "4": {
                        action: function(val) {
                            return insertCharIntoString("4", val, self);
                        }
                    },
                    "5": {
                        action: function(val) {
                            return insertCharIntoString("5", val, self);
                        }
                    },
                    "6": {
                        action: function(val) {
                            return insertCharIntoString("6", val, self);
                        }
                    },
                    "7": {
                        action: function(val) {
                            return insertCharIntoString("7", val, self);
                        }
                    },
                    "8": {
                        action: function(val) {
                            return insertCharIntoString("8", val, self);
                        }
                    },
                    "9": {
                        action: function(val) {
                            return insertCharIntoString("9", val, self);
                        }
                    },
                    "0": {
                        action: function(val) {
                            // avoid multiple zeros
                            return val === "0" ? "0" : insertCharIntoString("0", val, self);
                        }
                    },
                    "dot": {
                        action: function(val) {
                            val = String(val);
                            if (val.indexOf(self.config["decimal-delimiter"]) !== -1) {
                                return (val);
                            }
                            var addedStr = val.match(/\d/) === null ? `0${self.config["decimal-delimiter"]}` : self.config["decimal-delimiter"];
                            return insertCharIntoString(addedStr, val, self);
                        }
                    },
                    "backspace": {
                        action: function(val) {
                            val = String(val);
                            if (self.vars.inputDisplayElement.selectionStart === self.vars.inputDisplayElement.selectionEnd) {
                                if (self.vars.inputDisplayElement.selectionStart === 0) {
                                    self.vars.inputDisplayElement.selectionStart = val.length;
                                    self.vars.inputDisplayElement.selectionEnd = val.length;
                                }
                                return val.substr(0, self.vars.inputDisplayElement.selectionStart - 1) + val.substr(self.vars.inputDisplayElement.selectionEnd);
                            }
                            return val.substr(0, self.vars.inputDisplayElement.selectionStart) + val.substr(self.vars.inputDisplayElement.selectionEnd);
                        }
                    },
                    "plusMinus": {
                        action: function(val) {
                            if (val.indexOf("-") !== -1) {
                                val = String(val).replace("-", "");
                            } else {
                                val = "-" + String(val);
                            }
                            return val;
                        }
                    },
                    "close": {
                        action: function(val) {
                            actionClose(self);
                            return val;
                        }
                    },
                    "ok": {
                        action: function(val) {
                            actionOk(self);
                            return val;
                        }
                    }
                };

                // if all elements are existing - init them
                self.vars.titleElement = shmi.getUiElement('numpad-title', self.element);
                if (!self.vars.titleElement) {
                    shmi.log('[Numpad] no numpad-title element provided', 3);
                }
                self.vars.errTextElem = shmi.getUiElement('error-message', self.element);
                if (!self.vars.errTextElem) {
                    shmi.log('[Numpad] no error-message element provided', 3);
                }
                self.vars.inputLimitMinimum = shmi.getUiElement('input-limit-minimum', self.element);
                if (!self.vars.inputLimitMinimum) {
                    shmi.log('[Numpad] no input-limit-minimum element provided', 3);
                }
                self.vars.inputLimitMinimumLabel = shmi.getUiElement('input-limit-minimum-label', self.element);
                if (!self.vars.inputLimitMinimumLabel) {
                    shmi.log('[Numpad] no input-limit-minimum-label element provided', 3);
                }
                self.vars.inputLimitMaximum = shmi.getUiElement('input-limit-maximum', self.element);
                if (!self.vars.inputLimitMaximum) {
                    shmi.log('[Numpad] no input-limit-maximum element provided', 3);
                }
                self.vars.inputLimitMaximumLabel = shmi.getUiElement('input-limit-maximum-label', self.element);
                if (!self.vars.inputLimitMaximumLabel) {
                    shmi.log('[Numpad] no input-limit-maximum-label element provided', 3);
                }
                self.vars.frameElement = shmi.getUiElement('numpad-frame', self.element);
                if (!self.vars.frameElement) {
                    shmi.log('[Numpad] no numpad-frame element provided', 3);
                }
                self.vars.inputDisplayElement = shmi.getUiElement('numpad-input-display', self.element);
                if (self.vars.inputDisplayElement) {
                    self.vars.inputDisplayElement.setAttribute("maxlength", self.config["max-chars"]);
                } else {
                    shmi.log('[Numpad] no numpad-input-display element provided', 3);
                }
                self.vars.unitElement = shmi.getUiElement('numpad-unit', self.element);
                if (!self.vars.unitElement) {
                    shmi.log('[Numpad] no numpad-unit element provided', 3);
                }
                Object.keys(self.vars.buttons).forEach(function(el) {
                    var elId = 'numpad-button-' + el;
                    self.vars.buttonsElements[el] = shmi.getUiElement(elId, self.element);
                    if (!self.vars.buttonsElements[el]) {
                        shmi.log(shmi.evalString(shmi.localize('[Numpad] no <%= elId %> element provided!'), { elId: elId }), 3);
                        return;
                    }
                    self.vars.buttonsElements[el].value = el;
                    self.vars.buttonsElements[el].setAttribute('tabindex', '1');
                });

                /* all required elements found */
                self.vars.visible = self.config['initially-visible'];
                if (self.vars.visible) {
                    shmi.addClass(self.element, 'open');
                }

                self.vars.containerElement = shmi.getUiElement('numpad-content', self.element);
                if (!self.vars.containerElement) {
                    shmi.log('[Numpad] no numpad-content element provided', 3);
                }

                if (self.vars.titleElement) {
                    if (self.config.label) {
                        self.vars.titleElement.textContent = shmi.localize(self.config.label);
                    }
                }
                if (self.vars.inputLimitMinimum) {
                    if (typeof self.config.min === "number") {
                        self.vars.inputLimitMinimum.textContent = self.config.min;
                    }
                }
                if (self.vars.inputLimitMinimumLabel) {
                    if (self.config.inputLimitMinimumLabel) {
                        self.vars.inputLimitMinimumLabel.textContent = shmi.localize(self.config.inputLimitMinimumLabel);
                    }
                }
                if (self.vars.inputLimitMaximum) {
                    if (typeof self.config.max === "number") {
                        self.vars.inputLimitMaximum.textContent = self.config.max;
                    }
                }
                if (self.vars.inputLimitMaximumLabel) {
                    if (self.config.inputLimitMaximumLabel) {
                        self.vars.inputLimitMaximumLabel.textContent = shmi.localize(self.config.inputLimitMaximumLabel);
                    }
                }
                if (self.vars.unitElement) {
                    if (self.config.unit && self.config.unit !== 'unit') {
                        self.vars.unitElement.textContent = (typeof self.config.unit === "number") ? self.config.unit : shmi.localize(self.config.unit);
                    } else {
                        shmi.addClass(self.vars.unitElement, 'hidden');
                    }
                }
                if (self.vars.inputDisplayElement) {
                    if (value(self)) {
                        self.vars.inputDisplayElement.value = String(value(self)).replace(".", self.config["decimal-delimiter"]);
                    }
                    self.vars.inputDisplayElement.onkeyup = function(e) {
                        if (e.key === "Enter") {
                            actionOk(self);
                        } else if (e.key === "Escape") {
                            actionClose(self);
                        } else {
                            value(self, self.vars.inputDisplayElement.value);
                        }
                    };
                }
                if (self.vars.buttonsElements["close"]) {
                    self.vars.buttonsElements["close"].textContent = shmi.localize("${cancel}");
                }
                if (self.vars.buttonsElements["ok"]) {
                    self.vars.buttonsElements["ok"].textContent = shmi.localize("${okay}");
                }
                if (self.vars.buttonsElements["dot"]) {
                    self.vars.buttonsElements["dot"].textContent = self.config["decimal-delimiter"];
                }

                // check for size of the numpad
                var inputElemClasses = self.vars.inputDisplayElement.className;
                if (inputElemClasses.indexOf('large') !== -1 || self.config['class-name'].indexOf('large') !== -1) {
                    self.vars.size = 'large';
                } else if (inputElemClasses.indexOf('small') !== -1 || self.config['class-name'].indexOf('small') !== -1) {
                    self.vars.size = 'small';
                } else {
                    self.vars.size = 'default';
                }

                // Key listeners for input field
                self.vars.inputDisplayListener = function(event) {
                    if (self.vars.keys.validKey.indexOf(event.which) !== -1) {
                        var currentValue = self.vars.inputDisplayElement.value;

                        // replace 0 if it is the only value
                        if (self.vars.keys.validNumber.indexOf(event.which) !== -1 && (currentValue === '0' || currentValue === '-0')) {
                            if (currentValue === '0') {
                                self.vars.inputDisplayElement.value = '';
                            } else if (currentValue === '-0') {
                                self.vars.inputDisplayElement.value = '-';
                            }
                            // listen for dash to convert value to negative value
                        } else if (self.vars.keys.minus.indexOf(event.which) !== -1) {
                            event.preventDefault();
                            // replace selection on input
                            // self.vars.inputDisplayElement.selectionStart = 0;
                            // self.vars.inputDisplayElement.selectionEnd = self.vars.inputDisplayElement.value.length;

                            if (currentValue.indexOf('-') === -1) {
                                self.vars.inputDisplayElement.value = '-' + currentValue;
                            }
                        } else if (self.vars.keys.plus.indexOf(event.which) !== -1) {
                            event.preventDefault();
                            if (currentValue.indexOf('-') !== -1) {
                                self.vars.inputDisplayElement.value = currentValue.replace('-', '');
                            }
                        }
                    } else if (!event.ctrlKey) { // allowing ctrl combinations prevent all others
                        event.preventDefault();
                    }
                };

                self.vars.elementEnterKey = function(event) {
                    if (event.key === "Enter") {
                        actionOk(self);
                    } else if (event.key === "Escape") {
                        actionClose(self);
                    }
                };
            },
            /**
             * Displays the Numpad
             */
            show: function() {
                var self = this;
                if (!self.active) {
                    shmi.log("[Numpad] cannot show numpad without enabling it first", 3);
                    return;
                }
                if (self.vars.visible) {
                    return;
                }

                self.vars.visible = true;

                if (self.config['top-level'] === true) {
                    document.body.appendChild(self.element);
                }

                // Hide min max displays if not set
                if (!self.config['min'] || self.config['min'] === '') {
                    shmi.addClass(self.vars.inputLimitMinimumLabel.parentElement, 'no-min');
                } else {
                    shmi.removeClass(self.vars.inputLimitMinimumLabel.parentElement, 'no-min');
                }
                if (!self.config['max'] || self.config['max'] === '') {
                    shmi.addClass(self.vars.inputLimitMaximumLabel.parentElement, 'no-max');
                } else {
                    shmi.removeClass(self.vars.inputLimitMaximumLabel.parentElement, 'no-max');
                }

                if (self.config['cover-background']) {
                    if (self.vars.bgDiv) {
                        shmi.removeClass(self.vars.bgDiv, 'hidden');
                    } else {
                        self.vars.bgDiv = document.createElement('div');
                        shmi.addClass(self.vars.bgDiv, 'numpad-overlay');
                        self.vars.bgDiv.setAttribute('tabindex', '-1');
                        document.body.appendChild(self.vars.bgDiv);
                        shmi.removeClass(self.vars.bgDiv, 'hidden');
                        var bgFuncs = {};
                        bgFuncs.onPress = function() {
                            document.activeElement.blur();
                        };
                        shmi.visuals.io.TouchListener(self.vars.bgDiv, bgFuncs);
                    }
                }

                shmi.addClass(self.element, 'open');
                setTimeout(function() {
                    self.vars.inputDisplayElement.focus();
                    self.fire('open', {});
                }, shmi.c("DECOUPLE_TIMEOUT"));

                self.vars.inputDisplayElement.selectionStart = 0;
                self.vars.inputDisplayElement.selectionEnd = self.vars.inputDisplayElement.value.length;
            },
            /**
             * Removes the Numpad from DOM
             *
             */
            hide: function() {
                var self = this;
                if (!self.vars.visible) {
                    return;
                }
                self.vars.visible = false;
                shmi.removeClass(self.element, 'open');
                // self.reset();
                if (self.config['cover-background']) {
                    if (self.vars.bgDiv) {
                        shmi.addClass(self.vars.bgDiv, 'hidden');
                    }
                }
                self.fire('close', {});
            },
            /**
             * Enables the Numpad
             */
            onEnable: function() {
                var self = this,
                    ml, tl;

                // Mouse and touch listeners
                var functions = {};
                functions.onPress = function(x, y, event) {
                    event.preventDefault();
                    var el = event.target;
                    shmi.addClass(self.element, 'pressed');
                    if (document.activeElement !== self.element) {
                        self.element.focus();
                    }
                    shmi.log("[Numpad] pressed", 1);

                    onNumpadAction(el.value, self);
                    self.vars.inputDisplayElement.focus();
                };
                functions.onRelease = function() {
                    shmi.removeClass(self.element, 'pressed');
                    shmi.log("[Numpad] released", 1);
                };
                ml = new shmi.visuals.io.MouseListener(self.vars.containerElement, functions);
                tl = new shmi.visuals.io.TouchListener(self.vars.containerElement, functions, true);

                self.vars.listeners.push(ml, tl);

                self.element.setAttribute('tabindex', '1');

                // key listeners for the "ok" and "cancel" actions
                self.element.addEventListener('keydown', self.vars.elementEnterKey);

                self.vars.inputDisplayElement.addEventListener('keydown', self.vars.inputDisplayListener);

                //--------------------
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                shmi.log("[Numpad] enabled", 1);
            },
            /**
             * Disables the Numpad
             *
             */
            onDisable: function() {
                var self = this;
                self.hide();
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });

                if (self.vars.elementEnterKey) {
                    self.element.removeEventListener('keydown', self.vars.elementEnterKey);
                }

                if (self.vars.inputDisplayElement) {
                    self.vars.inputDisplayElement.removeEventListener('keydown', self.vars.inputDisplayListener);
                }

                // remove overlay background element if present
                if (self.vars.bgDiv) {
                    self.vars.bgDiv.parentNode.removeChild(self.vars.bgDiv);
                    self.vars.bgDiv = null;
                }

                shmi.log("[Numpad] disabled", 1);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(val, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };
    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();
(function() {
    shmi.pkg("visuals.controls");
    /**
     * Creates a new shmi.visuals.session.Layout Panel control
     *
     * @constructor
     * @extends shmi.visuals.core.BaseControl
     * @param {HTMLElement} element root element of the control
     * @param {Object} config control configuration
     * @param {Boolean} active true if panel should always be active, false else
     */
    shmi.visuals.controls.Panel = function(element, config) {
        /* check for required packages */
        shmi.requires("visuals.controls.View");
        shmi.requires("visuals.controls.panel.PanelIndicator");
        shmi.requires("visuals.gfx.Animation");
        shmi.requires("visuals.gfx.Movable");
        shmi.requires("visuals.io.MouseListener");
        shmi.requires("visuals.io.TouchListener");

        this.element = element;
        this.configName = this.element.getAttribute('data-config-name');

        if (arguments.length === 3) {
            this.config = arguments[2] || {};
        } else {
            this.config = config || {};
        }

        this.parseAttributes();

        shmi.def(this.config, 'class-name', 'panel');
        shmi.def(this.config, 'name', null);
        shmi.def(this.config, 'show-indicator', false);
        shmi.def(this.config, 'disable-swipe', false);
        shmi.def(this.config, 'anim-duration', 250);
        shmi.def(this.config, 'item', null);
        shmi.def(this.config, 'animate', false);
        shmi.def(this.config, 'initial-view', 0);

        /* convert text 'true' to boolean 'true' */
        this.config.animate = (this.config.animate === "true") || (this.config.animate === true);

        this.viewElements = [];
        this.controls = [];
        this.subpanels = [];
        this.history = [];
        this.currentView = 0;
        this.nextView = 0;
        this.initialized = false;
        this._dragging = false;
        this.anim = new shmi.visuals.gfx.Animation(this.updatePosition.bind(this), this.updateVisibility.bind(this), "ease_out");
        this.anim.priority = true;

        this.clientWidth = null;
        this.clientHeight = null;
        this.ignoreResize = false;
        this.resizeTO = 0;
        this.resizeObserver = null;

        this.active = false;

        this.startup();
    };

    function initializeViews(self) {
        var views = shmi.getUiElements('view', self.element);
        for (var i = 0; i < views.length; i++) {
            if (views[i].parentNode === self.element) {
                views[i].setAttribute('tabindex', '-1');
                self.viewElements.push(views[i]);
            }
        }

        self.clientWidth = self.element.clientWidth;
        self.clientHeight = self.element.clientHeight;
        self.contentDiv.style.width = (self.clientWidth * self.viewElements.length) + 'px';

        self.contentDiv.style.height = "100%";
        self.contentDiv.style.position = 'relative';

        self.viewWidth = self.clientWidth;

        for (i = 0; i < self.viewElements.length; i++) {
            var div = document.createElement('div');
            div.style.width = self.clientWidth + 'px';
            div.style.height = "100%";
            div.style.cssFloat = 'left';
            div.appendChild(self.viewElements[i]);
            self.contentDiv.appendChild(div);
        }
    }

    function notifyResize(self) {
        const evt = document.createEvent("Event");

        self.ignoreResize = true;
        evt.initEvent("resize", true, true);
        window.dispatchEvent(evt);
        self.ignoreResize = false;
    }

    shmi.visuals.controls.Panel.prototype = {
        uiType: "panel",
        isContainer: true,
        events: ["navigate"],
        getClassName: function() {
            return "Panel";
        },
        onRegister: function(onDone) {
            var self = this;
            /* register event listener id */
            if (self.config['show-indicator'] === "true") {
                self.config['show-indicator'] = true;
            } else if (self.config['show-indicator'] === "false") {
                self.config['show-indicator'] = false;
            }

            if (self.config['disable-swipe'] === "true") {
                self.config['disable-swipe'] = true;
            } else if (self.config['disable-swipe'] === "false") {
                self.config['disable-swipe'] = false;
            }

            self.contentDiv = document.createElement('div');
            self.element.appendChild(self.contentDiv);
            initializeViews(self);
            self.parseChildren(self.contentDiv, onDone);
        },
        onInit: function() {
            var self = this;

            this.mover = new shmi.visuals.gfx.Movable(this.contentDiv);
            this.mover.rotatable = false;
            this.mover.scalable = false;
            this.mover.priority = true;

            this.resizeObserver = new ResizeObserver(() => {
                if (self.ignoreResize) {
                    return;
                }
                clearTimeout(self.resizeTO);
                self.resizeTO = setTimeout(function() {
                    self.updateSize();
                }, shmi.c("ACTION_RETRY_TIMEOUT"));
            });

            if (!this.config['disable-swipe']) {
                var divFuncs = {};
                divFuncs.onDrag = function(dx, dy, event) {
                    var abs_dy = Math.abs(dy),
                        abs_dx = Math.abs(dx);

                    if (abs_dx > abs_dy) {
                        event.preventDefault();
                        if ((this.mover.tx + dx) < (-this.clientWidth * (this.viewElements.length - 1))) {
                            this.mover.translate(dx / 5, 0);
                        } else if ((this.mover.tx + dx) > 0) {
                            this.mover.translate(dx / 5, 0);
                        } else {
                            this.mover.translate(dx, 0);
                        }
                        if (!this._dragging) {
                            this._dragging = true;
                            if (document.activeElement !== document.body) {
                                document.activeElement.blur();
                            }
                            this.showNeighbors();
                        }
                    }
                }.bind(this);
                divFuncs.onPress = function() {
                    this.updateSize();
                    this.mover.setTransition(false);
                    if (document.activeElement !== document.body) {
                        try {
                            document.activeElement.blur();
                        } catch (exc) { /* blur not supported, probably SVGElement */ }
                    }
                }.bind(this);
                divFuncs.onRelease = function() {
                    if (this._dragging) {
                        this._dragging = false;
                        var oldView = this.currentView,
                            nextView = this.currentView,
                            diff = (this.currentView * this.viewWidth) + (this.mover.tx);
                        if (diff > (0.2 * this.viewWidth)) {
                            nextView = this.currentView - 1;
                        } else if (diff < (-0.2 * this.viewWidth)) {
                            nextView = this.currentView + 1;
                        }
                        if ((nextView >= 0) && (nextView < this.viewElements.length)) {
                            if (this.currentView !== nextView) {
                                this.nextView = nextView;
                                this.history.push(this.currentView);
                            }
                            this.currentView = nextView;
                            /* write current view index to item if configured */
                            if (this.config.item) {
                                shmi.visuals.session.ItemManager.writeValue(this.config.item, nextView);
                            }
                            if (!this.navInited && this.config['nav-name']) {
                                this.navInited = true;
                                this.nav = shmi.getControlByName(this.config['nav-name']);
                            }
                            if (this.nav) {
                                this.nav.setActive(this.currentView);
                            }
                        }
                        var dest = -1 * this.currentView * this.viewWidth,
                            newView = this.controls[this.currentView],
                            eventDetail = {
                                index: this.currentView,
                                old_index: oldView
                            };

                        if (self.isActive()) {
                            if (self.controls[oldView] && self.controls[oldView].isActive()) {
                                self.controls[oldView].disable();
                            }
                            newView.enable();
                            this.fire('navigate', eventDetail);
                        }
                        this.anim.setCurrentValue(this.mover.tx);
                        this.anim.start(dest, this.config['anim-duration']);
                    }
                }.bind(this);

                this.mouseListener = new shmi.visuals.io.MouseListener(this.contentDiv, divFuncs);
                this.touchListener = new shmi.visuals.io.TouchListener(this.contentDiv, divFuncs);
                this.touchListener.prevent_drag_scroll_y = false;
            }
            if (this.config['show-indicator']) {
                this.indicator = new shmi.visuals.controls.panel.PanelIndicator(this);
                shmi.addClass(this.element, "show-indicator");
            }
            this.updateVisibility();
            if (self.viewElements.length > self.config['initial-view'] && (self.currentView === self.nextView)) {
                self.setView(self.config['initial-view']);
            }
            shmi.log("[Panel] " + this.config.name + " created", shmi.c('LOG_INFO'));
        },
        /**
         * Updates visibility of panel views
         *
         */
        updateVisibility: function() {
            shmi.log("[Panel] update visibility -  start", shmi.c('LOG_DEBUG'));
            for (var i = 0; i < this.viewElements.length && i < this.controls.length; i++) {
                if (i !== this.currentView) {
                    shmi.addClass(this.viewElements[i], 'hidden');
                } else {
                    shmi.removeClass(this.viewElements[i], 'hidden');
                }
            }
            if (this.config['show-indicator']) {
                this.indicator.setSelected(this.currentView);
                this.indicator.refresh();
            }
            shmi.log("[Panel] update visibility -  end", shmi.c('LOG_DEBUG'));
        },
        /**
         * Updates translation of panel content
         *
         */
        updatePosition: function(val) {
            this.mover.tx = val;
            this.mover.draw();
        },
        /**
         * Called when switching views has finished
         *
         */
        transitionComplete: function() {
            this.updateVisibility();
        },
        /**
         * Shows neighboring views of current view
         *
         */
        showNeighbors: function() {
            if (((this.currentView - 1) >= 0)) {
                shmi.removeClass(this.viewElements[this.currentView - 1], 'hidden');
            }
            if ((this.currentView + 1) < this.viewElements.length) {
                shmi.removeClass(this.viewElements[this.currentView + 1], 'hidden');
            }
        },
        /**
         * Sets active view to specified index
         *
         * @param viewindex - index of new active view
         */
        setView: function(viewindex) {
            var self = this;

            if ((viewindex < 0) || (viewindex >= this.viewElements.length)) {
                viewindex = 0;
            }

            if (viewindex !== Number(this.currentView)) {
                this.nextView = viewindex;
                if (!this.initialized) {
                    return;
                }
                if (!this.backPushed) {
                    this.history.push(this.currentView);
                } else {
                    this.backPushed = false;
                }
            } else {
                return;
            /* don't do anything if currently active view is set again */
            }
            var lastView = this.currentView;
            this.currentView = viewindex;
            if (this.config.item) {
                shmi.visuals.session.ItemManager.writeValue(this.config.item, viewindex);
            }
            var newView = this.controls[viewindex],
                e_detail = {
                    index: viewindex,
                    old_index: lastView
                };

            shmi.removeClass(this.viewElements[this.currentView], 'hidden');
            if (self.isActive()) {
                if (self.controls[lastView] && self.controls[lastView].isActive()) {
                    self.controls[lastView].disable();
                }
                newView.enable();
                this.fire('navigate', e_detail);
            }
            var dest = -1 * this.currentView * this.viewWidth;
            this.anim.setCurrentValue(this.mover.tx);
            if (this.config.animate === true) {
                this.anim.start(dest, this.config['anim-duration']);
            } else {
                this.anim.start(dest, 0);
            }
            shmi.log("[Panel '" + this.config.name + "'] view " + viewindex + " set", shmi.c('LOG_INFO'));
        },
        back: function() {
            if (this.history.length > 0) {
                this.backPushed = true;
                this.setView(this.history.pop());
            } else {
                shmi.log("[Panel] history is empty, cannot go back", shmi.c('LOG_INFO'));
            }
        },
        /**
         * Enables the Panel
         *
         */
        onEnable: function() {
            const self = this;

            if (self.indicator) {
                self.indicator.updateWidth();
                self.indicator.refresh();
            }

            self.updateSize();
            self.resizeObserver.observe(self.element);

            if (!self.config['disable-swipe'] && self.mouseListener && self.touchListener) {
                self.mouseListener.enable();
                self.touchListener.enable();
            }

            if (self.config.item) {
                self._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(self.config.item, self);
            }

            if (self.currentView !== self.nextView) {
                self.setView(self.nextView);
            } else if (self.controls[self.currentView]) {
                self.controls[self.currentView].enable();
            }
        },
        /**
         * Disables the Panel
         *
         */
        onDisable: function() {
            if (this.controls[this.currentView] !== undefined) {
                this.controls[this.currentView].disable();
            }

            if (!this.config['disable-swipe'] && this.mouseListener && this.touchListener) {
                this.mouseListener.disable();
                this.touchListener.disable();
            }

            if (this.config.item) {
                shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
            }
            this.resizeObserver.unobserve(this.element);
            this.anim.stop();
        },
        onLock: function() {
            var self = this;

            if (!self.config['disable-swipe'] && self.mouseListener && self.touchListener) {
                this.mouseListener.disable();
                this.touchListener.disable();
            }

            if (self.indicator) {
                self.indicator.lock();
            }

            self.controls.forEach(function(ctrl) {
                ctrl.lock();
            });
        },
        onUnlock: function() {
            var self = this;

            if (!self.config['disable-swipe'] && self.mouseListener && self.touchListener && self.active) {
                self.mouseListener.enable();
                self.touchListener.enable();
            }

            if (self.indicator && self.active) {
                self.indicator.unlock();
            }

            self.controls.forEach(function(ctrl) {
                ctrl.unlock();
            });
        },
        onSetValue: function(value) {
            if (shmi.isDesignerEditor()) {
                return;
            }
            value = parseInt(value);
            if (!isNaN(value)) {
                this.setView(value);
            }
        },
        updateSize: function() {
            var self = this;
            if (!self.active) {
                return;
            }
            /* cancel execution */

            shmi.log("[Panel] resizing panel...", shmi.c('LOG_DEBUG'));
            self.clientWidth = self.element.clientWidth;
            self.clientHeight = self.element.clientHeight;
            if (!self.contentDiv) {
                return;
            }
            self.contentDiv.style.width = (self.clientWidth * self.viewElements.length) + "px";
            self.contentDiv.style.height = "100%";

            self.viewWidth = self.clientWidth;
            for (var i = 0; i < self.viewElements.length; i++) {
                var div = self.viewElements[i].parentNode;
                div.style.width = self.clientWidth + "px";
                div.style.height = "100%";
            }

            var dest = -1 * self.currentView * self.viewWidth;
            self.anim.setCurrentValue(dest);
            self.updateVisibility();
            self.updatePosition(dest);
            notifyResize(self);
        },
        onDelete: function() {
            this.anim.stop();
        },
        onAddControl: function(options, callback) {
            var self = this,
                cm = shmi.requires("visuals.tools.controller"),
                controls = [],
                notView = false,
                initToken = null;

            if (!self.initialized) {
                throw new Error("Control not initialized yet.");
            }

            if (!Array.isArray(options)) {
                options = [options];
            }

            notView = options.some(function(option) {
                return (option.ui !== "view");
            });

            if (notView) {
                callback(new Error("All children must be of type 'view'"), controls);
                return;
            }

            options.forEach(function(option) {
                var control = null,
                    viewDiv = document.createElement('div');

                viewDiv.style.width = self.clientWidth + 'px';
                viewDiv.style.height = "100%";
                viewDiv.style.cssFloat = 'left';
                self.contentDiv.appendChild(viewDiv);

                option.config.index = self.controls.length;
                control = shmi.createControl(option.ui, viewDiv, option.config, "DIV", null, false);

                if (control !== null) {
                    if (option.style && typeof option.style === "object") {
                        let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                        iter(option.style, (value, name) => {
                            control.element.style[name] = value;
                        });
                    }
                    shmi.addClass(control.element, "hidden");
                    self.viewElements.push(control.element);
                    controls.push(control);
                }
            });
            self.updateSize();

            initToken = shmi.waitOnInit(controls, function() {
                var idx = -1,
                    errors = [],
                    tm = shmi.requires("visuals.task"),
                    tasks = [],
                    tl = null;

                if (initToken) {
                    idx = self._init_.tokens.indexOf(initToken);
                    if (idx !== -1) {
                        self._init_.tokens.splice(idx, 1);
                    }
                    initToken.unlisten();
                    initToken = null;
                }

                options.forEach(function(opt, jdx) {
                    var t = null,
                        control = controls[jdx];

                    if (opt.controller) {
                        cm.create(opt.controller.name, opt.controller, control.getName());
                    }

                    if (Array.isArray(opt.children) && opt.children.length) {
                        t = tm.createTask("child control");
                        t.run = function() {
                            control.addControl(opt.children, function(childErr, childOptions) {
                                if (childErr) {
                                    errors.push(childErr);
                                }
                                t.complete();
                            });
                        };
                        tasks.push(t);
                    }
                });

                if (tasks.length) {
                    tl = tm.createTaskList(tasks, false);
                    tl.onComplete = function() {
                        if (self.controls.length && self.controls.length === controls.length) {
                            self.setView(self.config["initial-view"]);
                        }
                        if (self.indicator) {
                            self.indicator.update();
                        }
                        callback(errors.length ? errors[0] : null, controls);
                    };
                    tl.run();
                } else {
                    if (self.controls.length && self.controls.length === controls.length) {
                        self.setView(self.config["initial-view"]);
                    }
                    if (self.indicator) {
                        self.indicator.update();
                    }
                    callback(errors.length ? errors[0] : null, controls);
                }
            });

            if (initToken) {
                self._init_.tokens.push(initToken);
            }
        }
    };

    shmi.extend(shmi.visuals.controls.Panel, shmi.visuals.core.BaseControl);
}());

shmi.pkg("visuals.controls.panel");
/**
 * View Indicator for Panel Control
 *
 * @constructor
 * @param panel - panel to indicate views of
 */
shmi.visuals.controls.panel.PanelIndicator = function(panel) {
    var indicatorOuter = document.createElement('div'),
        indicatorInner = document.createElement('div');

    if (panel === undefined) {
        shmi.log("[PanelIndicator] panel not found", 3);
        return;
    }
    this.panel = panel;
    this.template = document.createElement('div');

    indicatorOuter.setAttribute('data-ui', 'indicator');
    indicatorOuter.setAttribute('class', 'indicator');
    indicatorInner.setAttribute('class', 'indicator-graphic');
    indicatorOuter.appendChild(indicatorInner);

    this.template.appendChild(indicatorOuter);
    this.element = document.createElement('div');
    this.element.setAttribute('data-ui', 'panel-indicator');
    this.indicatorElements = [];
    this.listeners = [];
    this.selected = 0;
    this.init();
};

shmi.visuals.controls.panel.PanelIndicator.prototype = {
    update: function() {
        var self = this;

        self.listeners.forEach(function(l) {
            l.disable();
        });

        self.listeners = [];
        self.indicatorElements = [];

        if (self.element.parentNode === self.panel.element) {
            self.panel.element.removeChild(self.element);
        }

        self.element.innerHTML = "";
        self.init();
        self.setSelected(self.selected);
    },
    init: function() {
        var self = this;

        self.indicator = shmi.getUiElement('indicator', this.template);
        if (!self.indicator) {
            shmi.log("[PanelIndicator] indicator element not found in template", 3);
            return;
        }

        self.listeners.forEach(function(l) {
            l.disable();
        });
        self.listeners = [];

        self.panel.controls.forEach(function(view, idx) {
            var indi = self.indicator.cloneNode(true),
                io = shmi.requires("visuals.io");
            self.element.appendChild(indi);
            self.indicatorElements.push(indi);
            var indiFuncs = {
                onClick: function() {
                    self.panel.setView(idx);
                }
            };
            self.listeners.push(new io.MouseListener(indi, indiFuncs));
            self.listeners.push(new io.TouchListener(indi, indiFuncs));

            if (idx === Number(self.panel.currentView)) {
                shmi.addClass(indi, 'active');
                self.selected = idx;
            }
        });

        self.panel.listen('enable', function() {
            self.listeners.forEach(function(l) {
                l.enable();
            });
        });
        self.panel.listen('disable', function() {
            self.listeners.forEach(function(l) {
                l.disable();
            });
        });

        if (self.panel.active) {
            self.listeners.forEach(function(l) {
                l.enable();
            });
        }

        var clearFix = document.createElement('div');
        clearFix.style.clear = 'both';
        self.element.appendChild(clearFix);
        self.panel.element.appendChild(self.element);
        self.updateWidth();
    },
    updateWidth: function() {
        shmi.addClass(this.element, 'indicator-container');
    },
    /**
     * Refreshes View Indicator
     *
     */
    refresh: function() {},
    /**
     * Sets active view to selected index
     *
     * @param index - index of active view
     */
    setSelected: function(index) {
        this.selected = Number(index);
        for (var i = 0; i < this.indicatorElements.length; i++) {
            if (i === index) {
                shmi.addClass(this.indicatorElements[i], 'active');
            } else {
                shmi.removeClass(this.indicatorElements[i], 'active');
            }
        }
    },
    lock: function() {
        var self = this;
        self.listeners.forEach(function(lst) {
            lst.disable();
        });
    },
    unlock: function() {
        var self = this;
        self.listeners.forEach(function(lst) {
            lst.enable();
        });
    }
};

shmi.pkg("visuals.controls");
/**
 * Popup Menu Control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - the base element of the control
 * @param config - configuration of the control
 */
shmi.visuals.controls.PopupMenu = function(element, config) {
    this.element = element;

    this.config = config || {};

    this.parseAttributes();

    /*set default options */
    shmi.def(this.config, 'class-name', 'popup-menu');
    shmi.def(this.config, 'template', 'default/popup-menu');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'content-template', null);
    shmi.def(this.config, 'auto-close', false);
    shmi.def(this.config, 'tab-limit', true);

    if (this.config['auto-close'] === "false") {
        this.config['auto-close'] = false;
    }

    this.controls = [];
    this.opened = false;
    this.locked = false;
    this.cls = [];
    /* click listener info objects for child controls */
    this.listeners = [];
    this.tokens = [];
    this.menu_background = null;
    this.menu_content = null;

    this.startup();
};

shmi.visuals.controls.PopupMenu.prototype = {
    uiType: "popup-menu",
    events: ['open', 'close'],
    isContainer: true,
    getClassName: function() {
        return "PopupMenu";
    },
    onRegister: function(onDone) {
        var self = this;

        shmi.addClass(self.element, 'hidden');
        self.menu_background = shmi.getUiElement('menu-background', self.element);
        self.menu_content = shmi.getUiElement('menu-content', self.element);

        if (self.config['content-template']) {
            var templateUrl = (self.config["content-template"].indexOf(shmi.c("RES_URL_PREFIX")) === 0) ?
                self.config["content-template"] :
                shmi.c("TEMPLATE_PATH") + self.config["content-template"] + shmi.c("TEMPLATE_EXT");

            shmi.loadResource(templateUrl, function(response, failed, url) {
                if (self.isDeleted()) {
                    return;
                }
                if (!failed && self.menu_content) {
                    self.menu_content.innerHTML = response;
                    self.parseChildren(self.menu_content, onDone);
                } else {
                    onDone();
                }
            });
        } else if (self.menu_content) {
            self.parseChildren(self.menu_content, onDone);
        } else {
            onDone();
        }
    },
    /**
     * Initializes the control.
     *
     */
    onInit: function() {
        var self = this;

        if (!this.menu_background) {
            shmi.notify("${V_MISSING_UI_ELEMENT} 'menu-background'", "${V_ERROR}");
        } else {
            var funcs = {
                onClick: function(x, y, event) {
                    self.close();
                }
            };
            var io = shmi.visuals.io,
                ml = new io.MouseListener(this.menu_background, funcs),
                tl = new io.TouchListener(this.menu_background, funcs);
            this.listeners.push(ml);
            this.listeners.push(tl);
        }

        if (!this.menu_content) {
            shmi.notify("${V_MISSING_UI_ELEMENT} 'menu-content'", "${V_ERROR}");
        }
    },
    onEnable: function() {
        var self = this;
        for (var i = 0; i < self.listeners.length; i++) {
            self.listeners[i].enable();
        }
        if (self.config['auto-close']) {
            self.tokens.push(shmi.listen("click", function(evt) {
                if (evt.source && evt.source.element) {
                    if (shmi.testParentChild(self.element, evt.source.element)) {
                        self.close();
                    }
                }
            }));
        }
    },
    onDisable: function() {
        var self = this;
        self.close();
        for (var i = 0; i < self.listeners.length; i++) {
            self.listeners[i].disable();
        }
        self.tokens.forEach(function(t) {
            t.unlisten();
        });
        self.tokens = [];
    },
    onSetValue: function() {

    },
    onLock: function() {
        var self = this;
        self.listeners.forEach(function(l) {
            l.disable();
        });
        self.locked = true;
    },
    onUnlock: function() {
        var self = this;
        self.listeners.forEach(function(l) {
            l.enable();
        });
        self.locked = false;
    },
    open: function() {
        var self = this;
        if (self.active && (!self.opened)) {
            self.opened = true;
            shmi.removeClass(self.element, 'hidden');

            if (self.config['tab-limit']) {
                // Enable element tabulator
                var tabulator = shmi.requires("visuals.tools.tabulator");
                self.tabLimit = tabulator.setTabParent(self.element);
            }

            for (var i = 0; i < self.controls.length; i++) {
                self.controls[i].enable();
            }
            self.fire('open', {});
        }
    },
    close: function() {
        if (this.opened) {
            this.opened = false;
            shmi.addClass(this.element, 'hidden');
            this.fire('close', {});
            for (var i = 0; i < this.controls.length; i++) {
                if (this.tabLimit) {
                    this.tabLimit.unsetTabParent();
                    this.tabLimit = null;
                }
                this.controls[i].disable();
            }
        }
    },
    toggle: function() {
        var self = this;
        if (self.opened) {
            self.close();
        } else {
            self.open();
        }
    },
    onAddControl: function(options, callback) {
        var self = this,
            cm = shmi.requires("visuals.tools.controller"),
            controls = [],
            initToken = null;

        if (!self.initialized) {
            throw new Error("Control not initialized yet.");
        }

        if (!Array.isArray(options)) {
            options = [options];
        }
        options.forEach(function(option) {
            var control = shmi.createControl(option.ui, self.menu_content, option.config, "DIV", null, false);
            if (control !== null) {
                if (option.style && typeof option.style === "object") {
                    let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                    iter(option.style, (value, name) => {
                        control.element.style[name] = value;
                    });
                }
                controls.push(control);
            }
        });
        initToken = shmi.waitOnInit(controls, function() {
            var idx = -1,
                errors = [],
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken.unlisten();
                initToken = null;
            }

            options.forEach(function(opt, jdx) {
                var t = null,
                    control = controls[jdx];

                if (opt.controller) {
                    cm.create(opt.controller.name, opt.controller, control.getName());
                }
                if (self.isActive() && self.opened) {
                    control.enable();
                }
                if (Array.isArray(opt.children) && opt.children.length) {
                    t = tm.createTask("child control");
                    t.run = function() {
                        control.addControl(opt.children, function(childErr, childOptions) {
                            if (childErr) {
                                errors.push(childErr);
                            }
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            if (tasks.length) {
                tl = tm.createTaskList(tasks, false);
                tl.onComplete = function() {
                    callback(errors.length ? errors[0] : null, controls);
                };
                tl.run();
            } else {
                callback(errors.length ? errors[0] : null, controls);
            }
        });
        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    }
};

/* Define Ui-Action open / close for PopupMenu */
shmi.visuals.session.userActions = shmi.visuals.session.userActions || {};
shmi.visuals.session.userActions['popup-menu'] = function(params, param1, param2, param3, owner) {
    if (params.length < 2) {
        shmi.log("[UserAction] popup-menu - needs two parameters", 2);
    } else {
        var control;
        if ((params.length === 3) && (params[2] === "relative")) {
            control = shmi.ctrl(params[0], owner);
        } else {
            control = shmi.ctrl(params[0]);
        }
        if (control) {
            try {
                if (params[1] === "open") {
                    control.open();
                    shmi.log("[UserAction] popup-menu - opened", 2);
                } else if (params[1] === "close") {
                    control.close();
                    shmi.log("[UserAction] popup-menu - closed", 2);
                } else if (params[1] === "toggle") {
                    control.toggle();
                    shmi.log("[UserAction] popup-menu - toggled", 2);
                }
            } catch (exc) {
                shmi.log("[UserAction] exception opening / closing popup-menu: " + exc, 2);
            }
        } else {
            shmi.log("[UserAction] popup-menu - no element found by name '" + params[0] + "'", 2);
        }
    }
};

shmi.extend(shmi.visuals.controls.PopupMenu, shmi.visuals.core.BaseControl);

/**
 * WebIQ visuals control template.
 *
 * Configuration options (default):
 *
 * {
 *     'class-name': 'progress-info',
 *     'name': null,
 *     'template': 'default/progress-info'
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = 'progress-info', // control name in camel-case
        uiType = 'progress-info', // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    var defConfig = {
        'class-name': 'progress-info',
        'name': null,
        'template': 'default/progress-info',
        'decimal-delimiter': '.',
        'precision': -1,
        'auto-precision': true,
        'baseItem': null,
        'comparisonItem': null,
        'displayFormat': '( $baseItem / $comparisonItem ) $percentValue %',
        'displayOptions': 2,
        'barThickness': 5
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires('visuals.tools.logging').createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START
    /**
     * Updates progress-bar width.
     *
     * @param  {object} self
     */
    function updateBar(self) {
        var percentValue = calcPercentValue(self.vars.items.baseItem, self.vars.items.comparisonItem);

        self.vars.elements.barEl.style.width = percentValue + '%';
    }
    /**
     * Replaces placeholder with item values in given format string
     *
     * @param  {object} self
     */
    function formatString(self) {
        var string = self.config.displayFormat,
            baseItem = formatValue(self, self.vars.items.baseItem),
            comparisonItem = formatValue(self, self.vars.items.comparisonItem),
            percentValue = calcPercentValue(self.vars.items.baseItem, self.vars.items.comparisonItem);

        string = string.replace('$baseItem', '<span class="baseValue">' + baseItem + '</span>');
        string = string.replace('$comparisonItem', '<span class="compareValue">' + comparisonItem + '</span>');
        string = string.replace('$percentValue', '<span class="percentValue">' + percentValue + '</span>');

        return string;
    }
    /**
     * Formats output of given values with given decimal-delimiter and precision
     *
     * @param  {object} self
     * @param  {string} value
     */
    function formatValue(self, value) {
        var val = value;

        if (self.config['auto-precision'] === true) {
            val = value.toFixed(2);
        } else {
            if (self.config.precision === -1) {
                self.config.precision = 0;
            }
            val = value.toFixed(self.config.precision);
        }

        if (self.config['decimal-delimiter'] === ',') {
            val = val.replace('.', self.config['decimal-delimiter']);
        } else if (self.config['decimal-delimiter'] === '.') {
            val = val.replace(',', self.config['decimal-delimiter']);
        }

        return val;
    }

    /**
     * Calculates percentage of given values.
     *
     * @param  {number} baseItemValue
     * @param  {number} comparisonItemValue
     */
    function calcPercentValue(baseItem, comparisonItem) {
        if (comparisonItem === 0 && baseItem === 0) {
            return NaN;
        } else if (comparisonItem === 0) {
            return Infinity;
        }

        return Math.round(baseItem / comparisonItem * 100);
    }
    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        configSchema: null,
        /* instance variables */
        vars: {
            listener: [],
            elements: {
                textContainerEl: null,
                barEl: null,
                barContainerEl: null
            },
            handler: {},
            items: {},
            subscriberTokens: []
        },
        /* imports added at runtime */
        imports: {
            im: 'visuals.session.ItemManager',
            io: 'visuals.io'
        },

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this,
                    elements = self.vars.elements,
                    handler = self.vars.handler,
                    im = self.imports.im,
                    thickRegEx = /^[0-9]+$/;

                //Check for primary HTML elements
                if (shmi.getUiElement('progress-info-text', self.element) && shmi.getUiElement('progress-info-bar-container', self.element) && shmi.getUiElement('progress-info-bar', self.element)) {
                    elements.textContainerEl = shmi.getUiElement('progress-info-text', self.element);
                    elements.barContainerEl = shmi.getUiElement('progress-info-bar-container', self.element);
                    elements.barEl = shmi.getUiElement('progress-info-bar', self.element);
                } else {
                    fLog('Cannot find the container element! Please make sure that the given template consists at least of a DIV container with the class attribute "progress-info-text", "progress-info-bar-container" and "progress-info-bar".');
                    return;
                }

                //Check display options and hide elements if needed
                if (self.config.displayOptions === 0) {
                    elements.barContainerEl.style.display = "none";
                    elements.barContainerEl.style.height = 0;
                } else if (self.config.displayOptions === 1) {
                    elements.textContainerEl.style.display = "none";
                    elements.textContainerEl.style.height = 0;
                }

                //Set barthickness
                if (thickRegEx.test(self.config.barThickness)) {
                    elements.barEl.style.height = self.config.barThickness + 'px';
                }

                handler.baseItemHandler = im.getItemHandler();
                handler.compItemHandler = im.getItemHandler();

                //Update control if item gets a valid update
                handler.baseItemHandler.setValue = function(value) {
                    self.vars.items.baseItem = value;
                    if (typeof self.vars.items.baseItem === 'number' && typeof self.vars.items.comparisonItem === 'number') {
                        switch (self.config.displayOptions) {
                        case 0:
                            elements.textContainerEl.innerHTML = formatString(self);
                            break;
                        case 1:
                            updateBar(self);
                            break;
                        case 2:
                            elements.textContainerEl.innerHTML = formatString(self);
                            updateBar(self);
                            break;
                        default:
                            fLog('Invalid option in Display Options!');
                            break;
                        }
                    } else {
                        fLog('Please use a valid integer or float Base-Item and Comparison-Item!');
                    }
                };

                handler.compItemHandler.setValue = function(value) {
                    self.vars.items.comparisonItem = value;
                    if (typeof self.vars.items.baseItem === 'number' && typeof self.vars.items.comparisonItem === 'number') {
                        switch (self.config.displayOptions) {
                        case 0:
                            elements.textContainerEl.innerHTML = formatString(self);
                            break;
                        case 1:
                            updateBar(self);
                            break;
                        case 2:
                            elements.textContainerEl.innerHTML = formatString(self);
                            updateBar(self);
                            break;
                        default:
                            fLog('Invalid option in Display Options!');
                            break;
                        }
                    } else {
                        fLog('Please use a valid integer or float Base-Item and Comparison-Item!');
                    }
                };
            },
            /* called when control is enabled */
            onEnable: function() {
                var self = this,
                    im = self.imports.im,
                    config = self.config,
                    handler = self.vars.handler,
                    subscriberTokens = self.vars.subscriberTokens;

                if (config.baseItem) {
                    subscriberTokens.push(im.subscribeItem(config.baseItem, handler.baseItemHandler));
                }

                if (config.comparisonItem) {
                    subscriberTokens.push(im.subscribeItem(config.comparisonItem, handler.compItemHandler));
                }
            },

            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.subscriberTokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.subscriberTokens = [];
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires('visuals.tools.control-generator');
    cg.generate(definition);
})();

shmi.pkg("visuals.controls");
/**
 * Creates a new control ProjectWindow
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - base element of the control project-window
 * @param config - configuration of the control project-window
 */
shmi.visuals.controls.ProjectWindow = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'project-window');
    shmi.def(this.config, 'template', 'default/project-window');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'initial-state', 'open');
    shmi.def(this.config, 'label', 'LABEL');
    shmi.def(this.config, 'alt-label', null);
    shmi.def(this.config, 'content-template', null);

    this._open = false;
    this.initialized = false;
    this.contentTemplate = null;
    /* storage for content template data */
    this.contentWrapper = null;
    /* content wrapper element */
    this.controls = [];
    this.listeners = [];

    this.startup();
};

shmi.visuals.controls.ProjectWindow.prototype = {
    uiType: "project-window",
    isContainer: true,
    getClassName: function() {
        return "ProjectWindow";
    },
    onRegister: function(onDone) {
        var self = this;

        self.contentElement = shmi.getUiElement('content', self.element);
        self.contentWrapper = shmi.getUiElement('content-wrapper', self.element);

        if (self.config['content-template']) {
            var contentUrl = (self.config["content-template"].indexOf(shmi.c("RES_URL_PREFIX")) === 0) ?
                self.config["content-template"] :
                shmi.c("TEMPLATE_PATH") + self.config["content-template"] + shmi.c("TEMPLATE_EXT");

            shmi.loadResource(contentUrl, function(response, failed, url) {
                if (self.isDeleted()) {
                    return;
                }
                if (!failed && self.contentElement) {
                    self.contentElement.innerHTML = response;
                    self.parseChildren(self.contentElement, onDone);
                } else {
                    onDone();
                }
            });
        } else if (self.contentElement) {
            self.parseChildren(self.contentElement, onDone);
        } else {
            onDone();
        }
    },
    /**
     * Initialize control
     */
    onInit: function() {
        var self = this,
            lbl_el = shmi.getUiElement('label', self.element),
            head_el = shmi.getUiElement('header', self.element),
            altLabel = shmi.getUiElement('label-2', self.element);

        if (altLabel && self.config["alt-label"]) {
            altLabel.textContent = shmi.localize(self.config["alt-label"]);
        }

        if (lbl_el && head_el) {
            lbl_el.textContent = shmi.localize(self.config.label);

            var funcs = {
                onClick: function(x, y, events) {
                    if (self._open) {
                        self.close();
                    } else {
                        self.open();
                    }
                }
            };
            var io = shmi.visuals.io,
                ml = new io.MouseListener(head_el, funcs),
                tl = new io.TouchListener(head_el, funcs);

            self.listeners.push(ml, tl);
        } else {
            if (!lbl_el) {
                shmi.notify("[ProjectWindow] template missing 'label' ui-element (" + self.config.template + ")", "Template Error");
            }
            if (!head_el) {
                shmi.notify("[ProjectWindow] template missing 'header' ui-element (" + self.config.template + ")", "Template Error");
            } else {
                shmi.notify("[ProjectWindow] template missing ui-element (" + self.config.template + ")", "Template Error");
            }
        }

        if (self.config['initial-state'] === 'open') {
            self.open();
        } else {
            shmi.addClass(self.element, 'minimized');
        }
    },
    open: function() {
        if (!this._open) {
            this._open = true;
            shmi.removeClass(this.element, 'minimized');
        }
    },
    close: function() {
        if (this._open) {
            this._open = false;
            shmi.addClass(this.element, 'minimized');
        }
    },
    /**
     * Enables the ProjectWindow
     *
     */
    onEnable: function() {
        this.listeners.forEach(function(l) {
            l.enable();
        });

        this.controls.forEach(function(ctrl) {
            ctrl.enable();
        });
        shmi.log("[ProjectWindow] enabled", 1);
    },
    /**
     * Disables the ProjectWindow
     *
     */
    onDisable: function() {
        this.listeners.forEach(function(l) {
            l.disable();
        });

        this.controls.forEach(function(ctrl) {
            ctrl.disable();
        });
        shmi.log("[ProjectWindow] disabled", 1);
    },
    /**
     * Locks the ProjectWindow
     *
     */
    onLock: function() {
        this.listeners.forEach(function(l) {
            l.disable();
        });

        this.controls.forEach(function(ctrl) {
            ctrl.lock();
        });
        this.locked = true;
        shmi.addClass(this.element, 'locked');
        shmi.log("[ProjectWindow] locked", 1);
    },
    /**
     * Unlocks the ProjectWindow
     *
     */
    onUnlock: function() {
        this.listeners.forEach(function(l) {
            l.enable();
        });

        this.controls.forEach(function(ctrl) {
            ctrl.unlock();
        });
        this.locked = false;
        shmi.removeClass(this.element, 'locked');
        shmi.log("[ProjectWindow] locked", 1);
    },
    /**
     * Sets value to the ProjectWindow
     *
     * @param value - new value to set
     */
    onSetValue: function(value) {
        shmi.log("[ProjectWindow] value set: " + value, 0);
    },
    setLabel: function(labelText) {
        var self = this,
            labelElement = shmi.getUiElement("label", self.element);

        if (labelElement) {
            labelElement.textContent = shmi.localize(labelText);
        }
    },
    onAddControl: function(options, callback) {
        var self = this,
            cm = shmi.requires("visuals.tools.controller"),
            controls = [],
            initToken = null;

        if (!self.initialized) {
            throw new Error("Control not initialized yet.");
        }

        if (!Array.isArray(options)) {
            options = [options];
        }
        options.forEach(function(option) {
            var control = shmi.createControl(option.ui, self.contentElement, option.config, "DIV", null, false);
            if (control !== null) {
                if (option.style && typeof option.style === "object") {
                    let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                    iter(option.style, (value, name) => {
                        control.element.style[name] = value;
                    });
                }
                controls.push(control);
            }
        });
        initToken = shmi.waitOnInit(controls, function() {
            var idx = -1,
                errors = [],
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken.unlisten();
                initToken = null;
            }

            options.forEach(function(opt, jdx) {
                var t = null,
                    control = controls[jdx];

                if (opt.controller) {
                    cm.create(opt.controller.name, opt.controller, control.getName());
                }
                if (self.isActive()) {
                    control.enable();
                }
                if (Array.isArray(opt.children) && opt.children.length) {
                    t = tm.createTask("child control");
                    t.run = function() {
                        control.addControl(opt.children, function(childErr, childOptions) {
                            if (childErr) {
                                errors.push(childErr);
                            }
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            if (tasks.length) {
                tl = tm.createTaskList(tasks, false);
                tl.onComplete = function() {
                    callback(errors.length ? errors[0] : null, controls);
                };
                tl.run();
            } else {
                callback(errors.length ? errors[0] : null, controls);
            }
        });
        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    }
};

shmi.extend(shmi.visuals.controls.ProjectWindow, shmi.visuals.core.BaseControl);

/*
 * {
 *     "class-name": "quality-display",
 *     "name": "qualityDisplay",
 *     "template": "system/default/quality-display",
 *     "label": "Label",
 *     "item": "DSaw1",
 *     "min": -100,
 *     "warnMin": -90,
 *     "preWarnMin": -50,
 *     "preWarnMax": 80,
 *     "warnMax": 90,
 *     "max": 100,
 *     "anim-time": 300,
 *     "precision": 1,
 *     "unit": "",
 *     "label": "Label",
 *     "icon-class": null,
 *     "icon-src": null
 * }
 *
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * label {string}: Label text of the control
 * item {string}: connected sps variable
 * min {integer}: alarm limit low
 * warnMin {integer}: warn limit low
 * preWarnMin {integer}: prewarn limit low
 * max {integer}: alarm limit high
 * warnMax {integer}: warn limit high
 * preWarnMax {integer}: prewarn limit high
 * anim-time {integer}: delay for animation speed
 * precision {integer}: precision of control
 * unit {integer}: unitclass
 * icon-src {string}: Path to image file
 * icon-class {string}: CSS class for icon
 */

(function() {
    'use strict';

    //variables for reference in control definition
    var className = "qualityDisplay",
        uiType = "quality-display",
        isContainer = false;

    //example - default configuration
    var defConfig = {
        "class-name": uiType,
        "name": uiType,
        "template": "default/quality-display",
        "item": "DSaw1",
        "min": -100,
        "warnMin": -80,
        "preWarnMin": -50,
        "preWarnMax": 50,
        "warnMax": 80,
        "max": 100,
        "anim-time": 300,
        "precision": 1,
        "unit": "",
        "label": "Label",
        "display-act-value": true,
        "display-set-value": true,
        "icon-class": null,
        "icon-src": null,
        "auto-min": true,
        "auto-max": true,
        "auto-precision": true,
        "auto-label": true,
        "auto-unit": true
    };

    //setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    //declare private functions - START

    //declare private functions - END

    //definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            labelElement: null,
            avElement: null,
            unitElement: null,
            arrowsElement: null,
            barElement: null,
            subIdValueItem: null,
            barMid: null,
            setCache: null,
            valCache: null,
            tokens: []
        },
        /* imports added at runtime */
        imports: {
            im: "visuals.session.ItemManager"
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                self.vars.labelElement = shmi.getUiElement('label', self.element);
                self.vars.avElement = shmi.getUiElement('actual-value', self.element);
                self.vars.spElement = shmi.getUiElement('set-point-value', self.element);
                self.vars.unitElement = shmi.getUiElement('actual-value-unit', self.element);
                self.vars.arrowsElement = shmi.getUiElement('set-value', self.element);
                self.vars.handleElement = shmi.getUiElement('value-display-handle', self.element);
                self.vars.valueContainer = shmi.getUiElement('value-display-container', self.element);
                self.vars.barElement = shmi.getUiElement('deviation', self.element);
                self.vars.refBar = shmi.getUiElement('ref-bar', self.element);
                self.vars.anim = new shmi.visuals.gfx.Animation(self.updateBar.bind(self));

                if (self.vars.labelElement) self.vars.labelElement.textContent = shmi.localize(self.config.label);
                if (self.vars.unitElement) self.vars.unitElement.textContent = shmi.localize(self.config.unit);

                if (this.config['icon-class'] || this.config['icon-src']) {
                    this.iconElement = shmi.getUiElement('icon', this.element);
                    this.iconContainerElement = shmi.getUiElement('icon-container', this.element);
                    this.iconContainerElement.style.display = "flex";

                    if (this.config['icon-class']) {
                        shmi.addClass(this.iconElement, this.config['icon-class']);
                    }

                    if (this.config['icon-src']) {
                        var imageElement = document.createElement("img");
                        imageElement.setAttribute("src", this.config['icon-src']);
                        this.iconElement.appendChild(imageElement);
                        if (this.config['icon-class']) {
                            imageElement.style.display = "none";
                        }
                    }
                }

                if (this.config['display-act-value'] === false) {
                    if (self.vars.valueContainer) self.vars.valueContainer.style.display = "none";
                }

                if (this.config['display-set-value'] === false) {
                    if (self.vars.arrowsElement) self.vars.arrowsElement.style.display = "none";
                }

                self.updateArrow(0, 100, 50); // make quality display look nice after init
                self.updateBar(50);
            },
            /* called when control is enabled */
            onEnable: function() {
                var self = this,
                    valueHandler = self.imports.im.getItemHandler();

                valueHandler.setValue = function(value, type, name) {
                    var dev_value = value;

                    if (value !== null) {
                        self.vars.valCache = value;
                        if (self.config.precision === -1) self.config.precision = 0;
                        if (self.vars.avElement) self.vars.avElement.textContent = value.toFixed(self.config.precision);

                        if (dev_value !== null) {
                            self.vars.anim.start(dev_value, self.config['anim-time']);
                        }
                    }
                };

                valueHandler.setProperties = function(min, max, step, name, type, warnMin, warnMax, preWarnMin, preWarnMax, precision) {
                    var nv = shmi.requires("visuals.tools.numericValues");
                    nv.setProperties(self, arguments);

                    if (self.vars.valueSettings) {
                        self.config.min = self.vars.valueSettings.min;
                        self.config.max = self.vars.valueSettings.max;
                        self.config.precision = self.vars.valueSettings.precision;
                    }

                    self.config.warnMin = warnMin;
                    self.config.preWarnMin = preWarnMin;
                    self.config.warnMax = warnMax;
                    self.config.preWarnMax = preWarnMax;

                    if (self.vars.setCache !== undefined) {
                        self.updateArrow(self.config.min, self.config.max, self.vars.setCache);
                    }

                    if (self.vars.valCache !== undefined && self.vars.valCache !== null) {
                        if (self.vars.avElement) self.vars.avElement.textContent = self.vars.valCache.toFixed(self.config.precision);
                        self.vars.anim.start(self.vars.valCache, self.config['anim-time']);
                    }
                };

                valueHandler.setLabel = function(labelText) {
                    if (self.config['auto-label'] && self.vars.labelElement) {
                        self.vars.labelElement.textContent = shmi.localize(labelText);
                    }
                };

                valueHandler.setUnitText = function(unitText) {
                    if (self.config['auto-unit'] && self.vars.unitElement) {
                        self.vars.unitElement.textContent = shmi.localize(unitText);
                    }
                };

                if (self.config.item && self.config["value-item"]) {
                    self.vars.subIdValueItem = self.imports.im.subscribeItem(self.config.item, self);
                    self.vars.tokens.push(self.imports.im.subscribeItem(self.config["value-item"], valueHandler));
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                if (self.vars.subIdValueItem) {
                    self.vars.subIdValueItem.unlisten();
                    self.vars.subIdValueItem = null;
                }

                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];

                self.vars.anim.stop();
            },
            setValueElement: function(value) {
                var self = this;

                if (self.config.precision === -1) {
                    self.config.precision = 0;
                }
                if (self.vars.spElement) {
                    self.vars.spElement.textContent = value.toFixed(self.config.precision);
                }
                self.updateArrow(self.config.min, self.config.max, value);
                self.vars.setCache = value;
                if (self.vars.valCache !== undefined) {
                    self.vars.anim.start(self.vars.valCache, self.config['anim-time']);
                }
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetProperties: function(min, max, step, name, type, warnMin, warnMax, preWarnMin, preWarnMax, precision) {
                var self = this;
                if (self.vars.setCache !== undefined) {
                    self.updateArrow(self.config.min, self.config.max, self.vars.setCache);
                }

                if (self.vars.valCache !== undefined && self.vars.valCache !== null) {
                    if (self.vars.avElement) self.vars.avElement.textContent = self.vars.valCache.toFixed(self.config.precision);
                    self.vars.anim.start(self.vars.valCache, self.config['anim-time']);
                }
            },
            onSetValue: function(value, type, name) {
                var self = this;
                self.vars.setValue = value;
                self.setValueElement(value);
            },
            updateBar: function(value) {
                var self = this;
                if (value !== null && self.vars.setValue !== undefined) {
                    if (shmi.hasClass(self.element, 'preWarn')) {
                        shmi.removeClass(self.element, 'preWarn');
                    }
                    if (shmi.hasClass(self.element, 'warn')) {
                        shmi.removeClass(self.element, 'warn');
                    }

                    if (typeof self.config.preWarnMax === "number" && typeof self.config.preWarnMin === "number") {
                        if ((value > self.config.preWarnMax) || (value < self.config.preWarnMin)) {
                            if (!shmi.hasClass(self.element, 'preWarn')) {
                                shmi.addClass(self.element, 'preWarn');
                            }
                        }
                    }

                    if (typeof self.config.warnMax === "number" && typeof self.config.warnMin === "number") {
                        if ((value > self.config.warnMax) || (value < self.config.warnMin)) {
                            if (!shmi.hasClass(self.element, 'warn')) {
                                shmi.addClass(self.element, 'warn');
                            }
                        }
                    }

                    if (value > self.config.max) {
                        value = self.config.max;
                    }
                    if (value < self.config.min) {
                        value = self.config.min;
                    }

                    var dev_value = value - self.vars.setValue,
                        min = self.config.min,
                        max = self.config.max;

                    if (min === max) {
                        dev_value = 100;
                    } else {
                        dev_value = dev_value !== 0 ? (dev_value / (max - min)) * 100 : 0;
                    }
                    if (self.vars.barElement) {
                        if ((value - self.vars.setValue) <= 0) {
                            // align left
                            self.vars.barElement.style.left = null;
                            self.vars.barElement.style.right = (100 - self.vars.barMid) + "%";
                            self.vars.barElement.style.width = Math.abs(dev_value) + "%";
                        } else {
                            //align right
                            self.vars.barElement.style.right = null;
                            self.vars.barElement.style.left = self.vars.barMid + "%";
                            self.vars.barElement.style.width = Math.abs(dev_value) + "%";
                        }
                    }
                }
            },
            updateArrow: function(min, max, setValue) {
                var self = this,
                    rangeVal = setValue - min;

                if (min === max) {
                    self.vars.barMid = 100;
                } else {
                    self.vars.barMid = rangeVal !== 0 ? (rangeVal / (max - min)) * 100 : 0;
                }

                if (self.vars.handleElement) {
                    self.vars.handleElement.style.left = self.vars.barMid + "%";
                }
                if (self.vars.arrowsElement) {
                    self.vars.arrowsElement.style.left = self.vars.barMid + "%";
                }
            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

shmi.pkg("visuals.controls");
shmi.pkg("visuals.controls");
/**
 * Creates a new Radial Gauge control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.RadialGauge = function(element, config) {
    this.element = element;
    this.value = 0;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'radial-gauge');
    shmi.def(this.config, 'template', 'default/radial-gauge');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'min', 0);
    shmi.def(this.config, 'max', 100);
    shmi.def(this.config, 'step', 0);
    shmi.def(this.config, 'precision', -1);
    shmi.def(this.config, 'type', shmi.c("TYPE_FLOAT"));
    shmi.def(this.config, 'unit-scale', 1);
    shmi.def(this.config, 'min-rotation', -135);
    shmi.def(this.config, 'max-rotation', 135);
    shmi.def(this.config, 'anim-time', 250);
    shmi.def(this.config, 'label', 'radial-gauge');
    shmi.def(this.config, 'unit-text', null);
    shmi.def(this.config, 'decimal-delimiter', ".");

    shmi.def(this.config, 'auto-min', true);
    shmi.def(this.config, 'auto-max', true);
    shmi.def(this.config, 'auto-step', true);
    shmi.def(this.config, 'auto-precision', true);
    shmi.def(this.config, 'auto-type', true);
    shmi.def(this.config, 'auto-label', true);
    shmi.def(this.config, 'auto-unit-text', true);

    this.labelElement = null;
    this.valueElement = null;
    this.unitElement = null;
    this.handleElement = null;
    this.requestAnimId = null;
    this._subscriptionTargetId = null;

    this.active = false;
    this.anim = new shmi.visuals.gfx.Animation(this.updateHandle.bind(this));

    this.startup();
};

shmi.visuals.controls.RadialGauge.prototype = {
    uiType: "radial-gauge",
    getClassName: function() {
        return "RadialGauge";
    },
    onInit: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.initValueSettings(self);

        this.labelElement = shmi.getUiElement('label', this.element);
        if (this.labelElement) {
            this.labelElement.textContent = shmi.localize(this.config.label);
        }

        this.handleElement = shmi.getUiElement('handle', this.element);
        if (!this.handleElement) {
            shmi.log('[RadialGauge] no handle element provided', 3);
            return;
        }
        this.valueElement = shmi.getUiElement('value', this.element);
        if (!this.valueElement) {
            shmi.log('[RadialGauge] no value element provided', 3);
            return;
        }
        this.unitElement = shmi.getUiElement('unit', this.element);
        if (!this.unitElement) {
            shmi.log('[RadialGauge] no unit element provided', 3);
            return;
        }

        /* all required elements found */
        shmi.log('[RadialGauge] -- all elements found --', 0);
        if (this.config['unit-text']) {
            this.unitElement.textContent = shmi.localize(this.config['unit-text']);
        }
        if (this.config['unit-scale']) {
            this.config['unit-scale'] = parseFloat(shmi.localize(this.config['unit-scale']));
        }

        /* Control initialized */
    },
    /**
     * Sets the current value
     *
     * @param value -  new value to set
     */
    onSetValue: function(value) {
        this.value = parseFloat(value);
        this.draw();
    },
    /**
     * Draws the Radial Gauge
     *
     */
    draw: function() {
        var self = this,
            vs = self.vars.valueSettings,
            min = vs.min,
            max = vs.max,
            rot = null;

        if (shmi.visuals.session.deactivateControls) {
            this.anim.stop();
            setTimeout(this.draw.bind(this), shmi.c("ACTION_RETRY_TIMEOUT"));
            return;
        }
        shmi.caf(this.requestAnimId);
        this.requestAnimId = shmi.raf(this.updateText.bind(this));
        if (max === min) {
            rot = this.config['max-rotation'];
        } else {
            rot = this.config['min-rotation'] + (((this.config['max-rotation'] - this.config['min-rotation']) / (max - min)) * (this.value - min));
        }

        this.anim.start(rot, this.config['anim-time']);
    },
    updateText: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        self.valueElement.textContent = nv.formatOutput(self.value, self);
    },
    /**
     * Updates the handle of the Radial Gauge to the specified rotation
     *
     * @param val - new rotation in degree
     */
    updateHandle: function(val) {
        this.handleElement.style.webkitTransform = 'rotate3d(0,0,1,' + val.toFixed(3) + 'deg)';
        this.handleElement.style.oTransform = 'rotate3d(0,0,1,' + val.toFixed(3) + 'deg)';
        this.handleElement.style.msTransform = 'rotate3d(0,0,1,' + val.toFixed(3) + 'deg)';
        this.handleElement.style.mozTransform = 'rotate3d(0,0,1,' + val.toFixed(3) + 'deg)';
        this.handleElement.style.transform = 'rotate3d(0,0,1,' + val.toFixed(3) + 'deg)';
    },
    /**
     * Enables the Radial Gauge
     *
     */
    onEnable: function() {
        if (this.config.item) {
            this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
        }
        shmi.log("[RadialGauge] enabled", 1);
    },
    /**
     * Disables the Radial Gauge
     *
     */
    onDisable: function() {
        this.anim.stop();
        if (this.config.item) {
            shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
        }
        shmi.log("[RadialGauge] disabled", 1);
    },
    onLock: function() {
    },
    onUnlock: function() {
    },
    onSetProperties: function(min, max, step) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.setProperties(self, arguments);

        if (typeof self.value === "number") {
            self.draw();
        }
    },
    setLabel: function(labelText) {
        var self = this;
        if (self.config['auto-label'] && self.labelElement) {
            self.labelElement.textContent = shmi.localize(labelText);
        }
    },
    setUnitText: function(unitText) {
        var self = this;
        if (self.config['auto-unit-text'] && self.unitElement) {
            self.unitElement.textContent = shmi.localize(unitText);
        }
    }
};

shmi.extend(shmi.visuals.controls.RadialGauge, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");
/**
 * Creates a new Radial Gauge control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.RadialGauge2 = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'radial-gauge2');
    shmi.def(this.config, 'template', 'default/radial-gauge2');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'min', 0);
    shmi.def(this.config, 'max', 100);
    shmi.def(this.config, 'precision', -1);
    shmi.def(this.config, 'type', shmi.c("TYPE_FLOAT"));
    shmi.def(this.config, 'unit-scale', 1);
    shmi.def(this.config, 'min-rotation', 0);
    shmi.def(this.config, 'max-rotation', 270);
    shmi.def(this.config, 'anim-time', 250);
    shmi.def(this.config, 'dynamic-anim-time', false);
    shmi.def(this.config, 'label', "radial-gauge2");
    shmi.def(this.config, 'unit-text', null);

    shmi.def(this.config, 'decimal-delimiter', ".");
    shmi.def(this.config, 'auto-min', true);
    shmi.def(this.config, 'auto-max', true);
    shmi.def(this.config, 'auto-step', true);
    shmi.def(this.config, 'auto-precision', true);
    shmi.def(this.config, 'auto-type', true);
    shmi.def(this.config, 'auto-label', true);
    shmi.def(this.config, 'auto-unit-text', true);

    this.value = 0;
    this.valueElement = null;
    this.unitElement = null;
    this.handleElement = null;
    this.labelElement = null;
    this.requestAnimId = null;
    this._subscriptionTargetId = null;

    this.active = false;
    this.anim = new shmi.visuals.gfx.Animation(this.updateHandle.bind(this));
    this.vars = {
        conditional: null
    };

    this.startup();
};

shmi.visuals.controls.RadialGauge2.prototype = {
    uiType: "radial-gauge2",
    getClassName: function() {
        return "RadialGauge2";
    },
    /**
     * Initializes the control
     *
     * @returns {unresolved}
     */
    onInit: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.initValueSettings(self);

        this.config['anim-time'] = parseInt(this.config['anim-time']);

        if (this.config['dynamic-anim-time'] === "true") {
            this.config['dynamic-anim-time'] = true;
        } else if (this.config['dynamic-anim-time'] === "false") {
            this.config['dynamic-anim-time'] = false;
        }

        this.handleElement = shmi.getUiElements('handle', this.element);
        if (!this.handleElement) {
            shmi.log('[RadialGauge2] no handle element provided', 3);
            return;
        }
        this.highlightElement = shmi.getUiElement('highlight', this.element);
        if (!this.highlightElement) {
            shmi.log('[RadialGauge2] no highlight element provided', 3);
            return;
        }
        this.valueElement = shmi.getUiElement('value', this.element);
        if (!this.valueElement) {
            shmi.log('[RadialGauge2] no value element provided', 3);
            return;
        }
        this.unitElement = shmi.getUiElement('unit', this.element);
        if (!this.unitElement) {
            shmi.log('[RadialGauge2] no unit element provided', 3);
            return;
        }

        this.labelElement = shmi.getUiElement('label', this.element);
        if (!this.labelElement) {
            shmi.log('[RadialGauge2] no label element provided', 1);
        } else if (this.config.label) {
            this.labelElement.textContent = shmi.localize(this.config.label);
        }

        /* all required elements found */
        shmi.log('[RadialGauge2] -- all elements found --', 0);
        if (this.config['unit-text']) {
            this.unitElement.textContent = shmi.localize(this.config['unit-text']);
        }
        if (this.config['unit-scale']) {
            this.config['unit-scale'] = parseFloat(shmi.localize(this.config['unit-scale']));
        }

        /* Control initialized */
    },
    /**
     * Sets the current value
     *
     * @param value -  new value to set
     */
    onSetValue: function(value) {
        this.value = parseFloat(value);
        this.draw();
    },
    /**
     * Draws the Radial Gauge
     *
     */
    draw: function() {
        var self = this,
            vs = self.vars.valueSettings,
            min = vs.min,
            max = vs.max,
            rot = null;

        if (shmi.visuals.session.deactivateControls) {
            this.anim.stop();
            setTimeout(this.draw.bind(this), shmi.c("ACTION_RETRY_TIMEOUT"));
            return;
        }
        shmi.caf(this.requestAnimId);
        this.requestAnimId = shmi.raf(this.updateText.bind(this));
        if (max === min) {
            rot = this.config['max-rotation'];
        } else {
            rot = this.config['min-rotation'] + (((this.config['max-rotation'] - this.config['min-rotation']) / (max - min)) * (this.value - min));
        }

        var animTime = this.config['anim-time'];
        if (this.config.item) {
            var item = shmi.visuals.session.ItemManager.items[this.config.item];
            if (item === undefined) {
                shmi.log("[RadialCanvas] item '" + this.config.item + "' undefined in manager", 1);
            } else {
                animTime = ((this.config['dynamic-anim-time'] === false) || (item.interval > this.config['anim-time'])) ? this.config['anim-time'] : Math.round(item.interval);
            }
        }

        this.anim.start(rot, animTime);
    },
    updateText: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        self.valueElement.textContent = nv.formatOutput(self.value, self);
    },
    /**
     * Updates the handle of the Radial Gauge to the specified rotation
     *
     * @param val - new rotation in degree
     */
    updateHandle: function(val) {
        if (val > 270) {
            val = 270;
        } else if (val < 0) {
            val = 0;
        }
        var drawFull = Math.floor(val / 90);

        var done = false;
        if (this.h_stat === undefined) {
            this.h_stat = [
                { full: false },
                { full: false },
                { full: false }
            ];
        }
        for (var i = 0; i < this.handleElement.length; i++) {
            if (i < drawFull) {
                if (this.h_stat[i].full === true) {
                    continue;
                } else {
                    this.handleElement[i].style.webkitTransform = 'rotate3d(0,0,1,0deg)';
                    this.handleElement[i].style.oTransform = 'rotate3d(0,0,1,0deg)';
                    this.handleElement[i].style.msTransform = 'rotate3d(0,0,1,0deg)';
                    this.handleElement[i].style.mozTransform = 'rotate3d(0,0,1,0deg)';
                    this.handleElement[i].style.transform = 'rotate3d(0,0,1,0deg)';
                    //shmi.removeClass(this.handleElement[i], 'invisible');
                    this.handleElement[i].style.display = 'block';
                    this.h_stat[i].full = true;
                }
            } else if (done) {
                //shmi.addClass(this.handleElement[i], 'invisible');
                this.handleElement[i].style.display = 'none';
            } else {
                if (this.h_stat[i].full === true) {
                    this.h_stat[i].full = false;
                }
                var ang = val % 90;
                var rot = ang - 90;
                this.handleElement[i].style.webkitTransform = 'rotate3d(0,0,1,' + rot.toFixed(3) + 'deg)';
                this.handleElement[i].style.oTransform = 'rotate3d(0,0,1,' + rot.toFixed(3) + 'deg)';
                this.handleElement[i].style.msTransform = 'rotate3d(0,0,1,' + rot.toFixed(3) + 'deg)';
                this.handleElement[i].style.mozTransform = 'rotate3d(0,0,1,' + rot.toFixed(3) + 'deg)';
                this.handleElement[i].style.transform = 'rotate3d(0,0,1,' + rot.toFixed(3) + 'deg)';
                //shmi.removeClass(this.handleElement[i], 'invisible');
                this.handleElement[i].style.display = 'block';
                done = true;
            }
        }
        val = val - 180;
        this.highlightElement.style.webkitTransform = 'rotate3d(0,0,1,' + val.toFixed(3) + 'deg)';
        this.highlightElement.style.oTransform = 'rotate3d(0,0,1,' + val.toFixed(3) + 'deg)';
        this.highlightElement.style.msTransform = 'rotate3d(0,0,1,' + val.toFixed(3) + 'deg)';
        this.highlightElement.style.mozTransform = 'rotate3d(0,0,1,' + val.toFixed(3) + 'deg)';
        this.highlightElement.style.transform = 'rotate3d(0,0,1,' + val.toFixed(3) + 'deg)';
    },
    /**
     * Enables the Radial Gauge
     *
     */
    onEnable: function() {
        var self = this;
        if (self.vars.conditional !== null) {
            if (self.vars.conditional.item !== self.config.item) {
                self.vars.conditional.item = self.config.item;
            }
        } else if (self.config.item) {
            self.vars.conditional = shmi.createConditional(self.element, self.config.item);
        }

        if (self.vars.conditional) {
            self.vars.conditional.enable();
        }

        if (this.config.item) {
            this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
        }
        shmi.log("[RadialGauge2] enabled", 1);
    },
    /**
     * Disables the Radial Gauge
     *
     */
    onDisable: function() {
        var self = this;
        this.anim.stop();
        if (self.vars.conditional) {
            self.vars.conditional.disable();
        }

        if (this.config.item) {
            shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
        }
        shmi.log("[RadialGauge2] disabled", 1);
    },
    onLock: function() {

    },
    onUnlock: function() {

    },
    /**
     * Sets properties for subscribed data
     *
     * @param min - min value
     * @param max - max value
     * @param step - value stepping
     */
    onSetProperties: function(min, max) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.setProperties(self, arguments);

        if (typeof self.value === "number") {
            self.draw();
        }
    },
    setLabel: function(labelText) {
        var self = this;
        if (self.config['auto-label'] && self.labelElement) {
            self.labelElement.textContent = shmi.localize(labelText);
        }
    },
    setUnitText: function(unitText) {
        var self = this;
        if (self.config['auto-unit-text'] && self.unitElement) {
            self.unitElement.textContent = shmi.localize(unitText);
        }
    }
};

shmi.extend(shmi.visuals.controls.RadialGauge2, shmi.visuals.core.BaseControl);

/**
 * WebIQ visuals recipe-controller control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "recipe-controller",
 *     "name": null,
 *     "template": "default/recipe-controller"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "RecipeController", // control name in camel-case
        uiType = "recipe-controller", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    var defConfig = {
        "class-name": className,
        "name": null,
        "template": "default/recipe-controller",
        "recipe-select": null,
        "recipe-edit": null,
        "button-order": [
            "add",
            "clone",
            "remove",
            "edit",
            "capture",
            "apply"
        ],
        "button-usage": {
            "add": true,
            "clone": true,
            "remove": true,
            "edit": true,
            "capture": true,
            "apply": true
        }
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START

    /**
     * watchRecipeSelection - watch & react to selection of recipes
     *
     * @param  {object} self control instance reference
     * @param  {object} recipeSelect recipe-select control instance
     * @return {undefined}
     */
    function watchRecipeSelection(self, recipeSelect) {
        var selectedTemplateId = null,
            selectedRecipeId = null;

        if (self.vars.select.changeToken) {
            self.vars.select.changeToken.unlisten();
            self.vars.select.changeToken = null;
        }

        if (recipeSelect && recipeSelect.isActive()) {
            const eventType = (self.vars.selectUiType === "iq-recipe-list") ? "select" : "change";
            self.vars.select.changeToken = recipeSelect.listen(eventType, function(evt) {
                console.log("recipe selection changed:", evt);
                updateSelection(self, evt.source.getValue());
                updateButtonState(self);
            });
            self.vars.select.control = recipeSelect;
            if (self.vars.edit.control && self.vars.edit.control.isActive()) {
                selectedTemplateId = recipeSelect.getTemplate();
                if (self.vars.select.selection) selectedRecipeId = self.vars.select.selection.recipe_id;
                if (typeof selectedTemplateId === "number") {
                    self.vars.edit.control.setTemplate(selectedTemplateId, selectedRecipeId, function() {
                        console.log(uiType, "template-ID set");
                    });
                }
            }
            updateSelection(self, recipeSelect.getValue());
        } else {
            self.vars.select.control = null;
            if (self.vars.select.changeToken) {
                self.vars.select.changeToken.unlisten();
                self.vars.select.changeToken = null;
            }
            updateSelection(self, null);
        }
        updateButtonState(self);
    }

    /**
     * updateSelection - update selection data
     *
     * @param  {object} self control instance refenrece
     * @param  {object} selectionValue selection data
     * @return {undefined}
     */
    function updateSelection(self, selectionValue) {
        var editControl = self.vars.edit.control;

        if (selectionValue && selectionValue.recipe_id !== null) {
            self.vars.select.selection = selectionValue;
            if (editControl) {
                editControl.editRecipe({
                    mode: editControl.MODE.EDIT,
                    recipe_id: selectionValue.recipe_id,
                    preview: true
                });
            }
        } else {
            self.vars.select.selection = null;
            if (editControl) {
                editControl.reset();
            }
        }
    }

    /**
     * watchRecipeEdit - watch & connect to recipe-edit instance when available
     *
     * @param  {object} self control instance reference
     * @param  {object} recipeEdit recipe-edit control instance
     * @return {undefined}
     */
    function watchRecipeEdit(self, recipeEdit) {
        var iter = shmi.requires("visuals.tools.iterate").iterateObject,
            selectedTemplateId = null,
            selectedRecipeId = null;

        if (recipeEdit && recipeEdit.isActive()) {
            self.vars.edit.control = recipeEdit;
            if (self.vars.select.control) {
                selectedTemplateId = self.vars.select.control.getTemplate();
                if (self.vars.select.selection) selectedRecipeId = self.vars.select.selection.recipe_id;
                if (typeof selectedTemplateId === "number") {
                    recipeEdit.setTemplate(selectedTemplateId, selectedRecipeId, function() {
                        console.log(uiType, "template-ID set");
                    });
                }
            }
            self.vars.edit.tokens.push(recipeEdit.listen("start-edit", function() {
                if (self.vars.select.control) {
                    self.vars.select.control.lock();
                }
                iter(self.vars.buttons, function(val, prop) {
                    if (val) {
                        val.lock();
                    }
                });
            }));
            self.vars.edit.tokens.push(recipeEdit.listen("stop-edit", function() {
                if (self.vars.select.control) {
                    self.vars.select.control.unlock();
                    if (typeof (self.vars.select.control.rebuildTable) === "function") self.vars.select.control.rebuildTable();
                }
                updateButtonState(self);
            }));
        } else {
            self.vars.edit.control = null;
            self.vars.edit.tokens.forEach(function(t) {
                t.unlisten();
            });
            self.vars.edit.tokens = [];
        }
    }

    /**
     * lockButton - lock button when available
     *
     * @param  {object} self control instance reference
     * @param  {string} name button property name
     * @return {undefined}
     */
    function lockButton(self, name) {
        var buttons = self.vars.buttons;
        if (buttons[name]) {
            buttons[name].lock();
        }
    }

    /**
     * unlockButton - unlock button when available
     *
     * @param  {object} self control instance reference
     * @param  {string} name button property name
     * @return {undefined}
     */
    function unlockButton(self, name) {
        var buttons = self.vars.buttons;
        if (buttons[name]) {
            buttons[name].unlock();
        }
    }

    /**
     * updateButtonState - update state of buttons in controller bar
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function updateButtonState(self) {
        var selectControl = self.vars.select.control,
            editControl = self.vars.edit.control,
            selection = self.vars.select.selection;

        if (!self.locked) {
            if (selectControl && selection) {
                if (selection.values_set) {
                    unlockButton(self, "apply");
                } else {
                    lockButton(self, "apply");
                }
                unlockButton(self, "remove");
                unlockButton(self, "capture");
            } else {
                lockButton(self, "apply");
                lockButton(self, "remove");
                lockButton(self, "capture");
            }

            if (editControl) {
                if (selection) {
                    unlockButton(self, "edit");
                    unlockButton(self, "clone");
                } else {
                    lockButton(self, "edit");
                    lockButton(self, "clone");
                }
                unlockButton(self, "add");
            } else {
                lockButton(self, "add");
                lockButton(self, "edit");
                lockButton(self, "clone");
            }
        }
    }

    /**
     * captureRecipe - capture item values to existing recipe, ask for confirmation
     * when existing item values are overwritten
     *
     * @param {object} self control instance reference
     * @param {object} recipe recipe object
     *
     * @returns {undefined}
     */
    function captureRecipe(self, recipe) {
        if (typeof recipe.versionId === "number") {
            shmi.confirm("${recipe-controller.confirm.captureRecipe}", function(confirmed) {
                if (confirmed) {
                    doCapture(recipe, function(err) {
                        var selectControl = self.vars.select.control;
                        if (!err && selectControl) {
                            selectControl.refreshGrid();
                        }
                    });
                }
            });
        } else {
            doCapture(recipe, function(err) {
                var selectControl = self.vars.select.control;
                if (!err && selectControl) {
                    selectControl.refreshGrid();
                }
            });
        }
    }

    /**
     * doCapture - perform actual capture of item values to recipe
     *
     * @param {object} recipe recipe object
     * @param {function} [callback] function to call on completion
     *
     * @returns {undefined}
     */
    function doCapture(recipe, callback) {
        recipe.capture(null, function(captureErr) {
            if (captureErr) {
                shmi.notify("${recipe-controller.error.captureRecipe}", "${V_ERROR}", {
                    ERROR_CODE: captureErr.errc,
                    ERROR_MSG: captureErr.message,
                    ERROR_CAT: captureErr.category
                });
            }
            if (typeof callback === "function") {
                callback(captureErr || null);
            }
        });
    }

    /**
     * fetchRecipe - fetch recipe data
     *
     * @param {number} recipeId recipe ID
     * @param {function} callback function to call on completion
     *
     * @returns {undefined}
     */
    function fetchRecipe(recipeId, callback) {
        var rm = shmi.requires("visuals.session.RecipeManager");

        rm.getRecipe(recipeId, function(recipe, err) {
            if (err) {
                shmi.notify("${recipe-controller.error.fetchRecipe}", "${V_ERROR}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                });
                callback(null, err);
            } else {
                callback(recipe, null);
            }
        });
    }

    /**
     * createRecipe - create new recipe with data from editor
     *
     * @param {number} templateId recipe template ID
     * @param {string} recipeName name of recipe
     * @param {function} callback function to call on completion
     * @return {undefined}
     */
    function createRecipe(templateId, recipeName, callback) {
        var rm = shmi.requires("visuals.session.RecipeManager");

        rm.getTemplate(templateId, function(response, err) {
            if (err) {
                shmi.notify("${recipe-controller.error.fetchTemplate}", "${V_ERROR}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                });
                callback(err);
            } else {
                response.createRecipe(recipeName, {}, function(createResponse, createErr) {
                    if (createErr) {
                        shmi.notify("${recipe-controller.error.createRecipe}", "${V_ERROR}", {
                            ERROR_CODE: createErr.errc,
                            ERROR_MSG: createErr.message,
                            ERROR_CAT: createErr.category
                        });
                        callback(null, createErr);
                    } else {
                        callback(createResponse, null);
                    }
                });
            }
        });
    }

    /**
     * cloneRecipe - clone recipe to new recipe
     *
     * @param {number} recipeId recipe ID
     * @param {string} recipeName name of cloned recipe
     * @param {function} callback function to call on completion
     *
     * @returns {undefined}
     */
    function cloneRecipe(recipeId, recipeName, callback) {
        var rm = shmi.requires("visuals.session.RecipeManager");

        rm.getRecipe(recipeId, function(response, err) {
            if (err) {
                shmi.notify("${recipe-controller.error.fetchRecipe}", "${V_ERROR}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                });
                callback(err);
            } else {
                response.clone(recipeName, function(recipe, cloneErr) {
                    if (cloneErr) {
                        shmi.notify("${recipe-controller.error.cloneRecipe}", "${V_ERROR}", {
                            ERROR_CODE: cloneErr.errc,
                            ERROR_MSG: cloneErr.message,
                            ERROR_CAT: cloneErr.category
                        });
                    }
                    callback(recipe, cloneErr || null);
                });
            }
        });
    }

    /**
     * attachButtonListeners - create & attach click handlers for button bar
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function attachButtonListeners(self) {
        var buttons = self.vars.buttons,
            tokens = self.vars.tokens;

        //add new recipe
        if (buttons.add) {
            tokens.push(buttons.add.listen("click", function(evt) {
                var editModule = shmi.requires("recipe-controller.ls.recipe-name"),
                    selectControl = self.vars.select.control,
                    templateId = null;

                if (selectControl) {
                    templateId = selectControl.getTemplate();
                    if (typeof templateId === "number") {
                        editModule.getRecipeName(templateId, function(recipeName) {
                            if (typeof recipeName === "string") {
                                createRecipe(templateId, recipeName, function(recipe, err) {
                                    if (!err) {
                                        doCapture(recipe, function(captureErr) {
                                            if (!err) {
                                                selectControl.refreshGrid();
                                                selectControl.setValue(recipeName);
                                                evt.recipe = recipe;
                                                evt.type = "add";
                                                self.fire("change", evt);
                                            }
                                        });
                                    }
                                });
                            }
                        });
                    }
                }
            }));
        }

        //edit selected recipe
        if (buttons.edit) {
            tokens.push(buttons.edit.listen("click", function(evt) {
                var selection = self.vars.select.selection,
                    control = self.vars.edit.control;

                if (control && selection) {
                    control.editRecipe({
                        mode: control.MODE.EDIT,
                        recipe_id: selection.recipe_id
                    });
                    // event 'stop-edit' will be fired in recipe-edit (invoked in enable() function of this control)
                }
            }));
        }

        //apply selected recipe
        if (buttons.apply) {
            tokens.push(buttons.apply.listen("click", function(evt) {
                var selection = self.vars.select.selection;

                if (selection) {
                    fetchRecipe(selection.recipe_id, function(recipe, err) {
                        if (!err) {
                            if (typeof recipe.versionId !== "number") {
                                shmi.notify("${recipe-controller.error.noRecipeValues}", "${V_ERROR}");
                            } else {
                                shmi.confirm("${recipe-controller.confirm.applyRecipe}", function(confirmed) {
                                    if (confirmed) {
                                        recipe.apply(function(response, applyErr) {
                                            if (applyErr) {
                                                shmi.notify("${recipe-controller.error.applyRecipe}", "${V_ERROR}", {
                                                    ERROR_CODE: applyErr.errc,
                                                    ERROR_MSG: applyErr.message,
                                                    ERROR_CAT: applyErr.category
                                                });
                                            } else {
                                                evt.recipe = recipe;
                                                evt.type = "apply";
                                                self.fire("change", evt);
                                            }
                                        });
                                    }
                                });
                            }
                        }
                    });
                }
            }));
        }

        //capture recipe values
        if (buttons.capture) {
            tokens.push(buttons.capture.listen("click", function(evt) {
                var selection = self.vars.select.selection;

                if (selection) {
                    //capture to existing recipe
                    fetchRecipe(selection.recipe_id, function(recipe, err) {
                        if (!err) {
                            captureRecipe(self, recipe);
                        } else {
                            evt.recipe = recipe;
                            evt.type = "capture";
                            self.fire("change", evt);
                        }
                    });
                }
            }));
        }

        //clone recipe
        if (buttons.clone) {
            tokens.push(buttons.clone.listen("click", function(evt) {
                var editModule = shmi.requires("recipe-controller.ls.recipe-name"),
                    selectControl = self.vars.select.control,
                    selection = self.vars.select.selection,
                    templateId = null;

                if (selectControl) {
                    templateId = selectControl.getTemplate();
                    editModule.getRecipeName(templateId, function(recipeName) {
                        if (recipeName !== null) {
                            cloneRecipe(selection.recipe_id, recipeName, function(recipe, err) {
                                if (!err) {
                                    if (selectControl.uiType === "iq-recipe-list") {
                                        selectControl.rebuildTable();
                                        selectControl.setValue(recipe.id);
                                    } else {
                                        selectControl.setValue(recipeName);
                                    }
                                } else {
                                    evt.templateId = templateId;
                                    evt.type = "apply";
                                    self.fire("change", evt);
                                    if (typeof (selectControl.rebuildTable) === "function") selectControl.rebuildTable();
                                }
                            });
                        }
                    });
                }
            }));
        }

        //delete recipe
        if (buttons.remove) {
            tokens.push(buttons.remove.listen("click", function(evt) {
                var selection = self.vars.select.selection,
                    selectControl = self.vars.select.control,
                    editControl = self.vars.edit.control,
                    editState = null;

                if (selection) {
                    shmi.confirm("${recipe-controller.confirm.deleteRecipe}", function(confirmed) {
                        if (confirmed) {
                            fetchRecipe(selection.recipe_id, function(recipe, err) {
                                if (!err) {
                                    recipe.delete(function(delErr) {
                                        if (delErr) {
                                            shmi.notify("${recipe-controller.error.deleteRecipe}", "${V_ERROR}", {
                                                ERROR_CODE: delErr.errc,
                                                ERROR_MSG: delErr.message,
                                                ERROR_CAT: delErr.category
                                            });
                                        } else {
                                            if (editControl) {
                                                editState = editControl.getState();
                                                if (editState && editState.recipe_id === selection.recipe_id) {
                                                    editControl.reset();
                                                }
                                            }
                                            if (selectControl) {
                                                selectControl.setValue("");
                                            }
                                            if (typeof (selectControl.rebuildTable) === "function") selectControl.rebuildTable();
                                            evt.recipe = recipe;
                                            evt.type = "del";
                                            self.fire("change", evt);
                                        }
                                    });
                                }
                            });
                        }
                    });
                }
            }));
        }
    }

    /**
     * createControllerButton - create button controls to perform actions on recipes
     *
     * @param {object} self control instance refenrece
     * @param {string} name control name, also used as appendix for CSS selector
     * @param {string} label localizable label & hover text
     *
     * @returns {object} button control reference
     */
    function createControllerButton(self, name, label, identifier) {
        var buttonConfig = {
                "label": null,
                "icon-title": null,
                "class-name": "button icon-only",
                "template": "default/button_with_bg_pic",
                "icon-class": "icon",
                "show-icon": true,
                "show-text": false
            },
            button = null;

        if (self.config["button-usage"] && self.config["button-usage"][identifier] !== true) {
            return null;
        }

        buttonConfig.label = label;
        buttonConfig["tooltip"] = label;
        buttonConfig["class-name"] += " " + name;
        buttonConfig.name = name;

        button = shmi.createControl("button", self.element, buttonConfig, "DIV");

        return button;
    }

    /**
     * nameOrNodeHandleMatches - check if configured control name / node-handle matches control instance
     *
     * @param {string} configured control name / node-handle
     * @param {object} instance control instance
     * @return {boolean} `true` if configured name / node-handle matches instance, `false` else
     */
    function nameOrNodeHandleMatches(configured, instance) {
        const { HANDLE_PREFIX, getNodeHandle } = shmi.requires("visuals.tools.nodes");
        if (typeof configured === "string" && configured.startsWith(HANDLE_PREFIX)) {
            return getNodeHandle(instance) === configured.replace(HANDLE_PREFIX, "");
        }
        return configured === instance.getName();
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            buttons: {
                add: null,
                edit: null,
                remove: null,
                apply: null,
                capture: null,
                clone: null
            },
            select: {
                control: null,
                changeToken: null,
                selection: null
            },
            edit: {
                control: null,
                tokens: []
            },
            tokens: [],
            eventProxy: null,
            selectUiType: "recipe-select"
        },
        /* imports added at runtime */
        imports: {},
        /* array of custom event types fired by this control */
        events: ["change"],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this,
                    buttons = self.vars.buttons,
                    buttonParameters = {
                        add: ["add-recipe", "${recipe-controller.label.add}"],
                        edit: ["edit-recipe", "${recipe-controller.label.edit}"],
                        remove: ["remove-recipe", "${recipe-controller.label.remove}"],
                        apply: ["apply-recipe", "${recipe-controller.label.apply}"],
                        capture: ["capture-recipe", "${recipe-controller.label.capture}"],
                        clone: ["clone-recipe", "${recipe-controller.label.clone}"]
                    };

                if (Array.isArray(self.config["button-order"])) {
                    self.config["button-order"].forEach(function(buttonName) {
                        if (buttons[buttonName] === null && buttonParameters[buttonName]) {
                            buttons[buttonName] = createControllerButton(self, buttonParameters[buttonName][0], buttonParameters[buttonName][1], buttonName);
                        }
                    });
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                var self = this,
                    recipeSelect = null,
                    recipeEdit = null;

                self.controls.forEach(function(c) {
                    c.enable();
                });

                attachButtonListeners(self);

                if (self.config["recipe-select"] !== null) {
                    recipeSelect = self.vars.select.control = shmi.ctrl(self.config["recipe-select"]);
                    if (recipeSelect) self.vars.selectUiType = recipeSelect.uiType;
                    watchRecipeSelection(self, recipeSelect);
                    self.vars.tokens.push(shmi.listen("enable", function(evt) {
                        if (nameOrNodeHandleMatches(self.config["recipe-select"], evt.source)) {
                            //on enable
                            recipeSelect = self.vars.select.control = evt.source;
                            watchRecipeSelection(self, recipeSelect);
                            updateSelection(self, recipeSelect.getValue());
                            updateButtonState(self);
                        }
                    }, { "source.uiType": self.vars.selectUiType }));

                    self.vars.tokens.push(shmi.listen("disable", function(evt) {
                        if (nameOrNodeHandleMatches(self.config["recipe-select"], evt.source)) {
                            //on disable
                            recipeSelect = self.vars.select.control = null;
                            watchRecipeSelection(self, recipeSelect);
                            updateSelection(self, null);
                            updateButtonState(self);
                        }
                    }, { "source.uiType": self.vars.selectUiType }));
                }
                if (recipeSelect) updateSelection(self, recipeSelect.getValue());
                updateButtonState(self);
                if (self.config["recipe-edit"] !== null) {
                    recipeEdit = self.vars.edit.control = shmi.ctrl(self.config["recipe-edit"]);
                    watchRecipeEdit(self, recipeEdit);
                    updateButtonState(self);

                    self.vars.tokens.push(shmi.listen("enable", function(evt) {
                        if (nameOrNodeHandleMatches(self.config["recipe-edit"], evt.source)) {
                            //on enable
                            recipeEdit = self.vars.edit.control = evt.source;
                            watchRecipeEdit(self, recipeEdit);
                            updateButtonState(self);
                        }
                    }, { "source.uiType": "recipe-edit" }));

                    self.vars.tokens.push(shmi.listen("disable", function(evt) {
                        if (nameOrNodeHandleMatches(self.config["recipe-edit"], evt.source)) {
                            //on disable
                            recipeEdit = self.vars.edit.control = null;
                            watchRecipeEdit(self, recipeEdit);
                            updateButtonState(self);
                        }
                    }, { "source.uiType": "recipe-edit" }));
                    self.vars.tokens.push(shmi.listen("stop-edit", function(evt) {
                        if (nameOrNodeHandleMatches(self.config["recipe-edit"], evt.source)) {
                            var selection = self.vars.select.selection;
                            if (selection) {
                                fetchRecipe(selection.recipe_id, function(recipe, err) {
                                    if (!err) {
                                        //on stop-edit
                                        let newEvt = {
                                            type: "edit",
                                            recipe: recipe,
                                            cancel: evt.detail.cancel
                                        };
                                        self.fire("change", newEvt);
                                    }
                                });
                            }
                        }
                    }, { "source.uiType": "recipe-edit" }));
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                var self = this;
                self.controls.forEach(function(c) {
                    c.disable();
                });
                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];
                if (self.vars.select.changeToken) {
                    self.vars.select.changeToken.unlisten();
                    self.vars.select.changeToken = null;
                }

                self.vars.edit.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.edit.tokens = [];
            },
            onDelete: function() {

            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                self.controls.forEach(function(c) {
                    c.lock();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                self.controls.forEach(function(c) {
                    c.unlock();
                });
            }
        }
    };

    // --------- LocalScript to implement "add recipe" dialog ---------

    var LOCALSCRIPT_MODULE = "recipe-controller.ls.recipe-name",
        module = shmi.pkg(LOCALSCRIPT_MODULE),
        CALLBACK = null,
        DIALOG = null,
        DELETE_TOKEN = null;

    module.run = function run(self) {
        var parentName = self.parentContainer.getName(),
            tokens = [];

        shmi.onReady({
            controls: {
                apply: parentName + " .apply",
                name: parentName + ".recipeName"
            }
        }, function(ref) {
            var apply = ref.controls.apply,
                name = ref.controls.name;

            if (!self.isActive()) {
                //cancel when script is no longer active
                return;
            }

            name.setValue("");

            tokens.push(apply.listen("click", function() {
                //apply
                var inputName = name.getValue();
                if (typeof inputName === "string" && inputName.trim() !== "") {
                    if (typeof CALLBACK === "function") {
                        CALLBACK(inputName);
                    }
                    CALLBACK = null;
                    if (DIALOG) {
                        DIALOG.hide();
                    }
                }
            }));
        });

        self.onDisable = function() {
            self.run = false;
            tokens.forEach(function(t) {
                t.unlisten();
            });
            tokens = [];
        };
    };

    /**
     * getRecipeName - request user input of recipe name
     *
     * The `callback` function returns the entered recipe name as its only parameter.
     * When input is canceled, `null` is returned.
     *
     * @param  {number} templateId recipe template ID
     * @param  {function} callback function to call on completion
     * @return {undefined}
     */
    module.getRecipeName = function getRecipeName(templateId, callback) {
        var closeTok = null;

        if (typeof CALLBACK === "function") {
            throw new Error("Recipe name dialog still in use!");
        }

        CALLBACK = callback;

        if (DIALOG === null) {
            DIALOG = shmi.createControl("dialog-box", document.body, {
                "title": "${recipe-controller.title.nameDialog}",
                "name": "recipeNameDialog",
                "class-name": "dialog-box recipe-name",
                "content-template": "default/recipe-controller/name-dialog",
                "top-level": true,
                "tab-limit": false
            }, "DIV");

            if (DELETE_TOKEN) {
                DELETE_TOKEN.unlisten();
                DELETE_TOKEN = null;
            }

            DELETE_TOKEN = DIALOG.listen("delete", function(evt) {
                DELETE_TOKEN.unlisten();
                DELETE_TOKEN = null;
                DIALOG = null;
            });
        }

        if (DIALOG) {
            shmi.onActive([DIALOG], function() {
                DIALOG.show();
                closeTok = DIALOG.listen("close", function() {
                    closeTok.unlisten();
                    if (typeof CALLBACK === "function") {
                        CALLBACK(null);
                    }
                    CALLBACK = null;
                });
            });
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals control template.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "recipe-edit",
 *     "name": null,
 *     "template": "custom/controls/recipe-edit"
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "RecipeEdit", // control name in camel-case
        uiType = "recipe-edit", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    var defConfig = {
        "class-name": "recipe-edit",
        "name": null,
        "template": "custom/controls/recipe-edit",
        "title": uiType,
        "viewMode": "linear",
        "editName": true,
        "itemFetchTimeout": 10000
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    /**
     * Available RecipeEdit operation modes
     */
    var MODE = {
            CREATE: 1,
            EDIT: 2,
            CLONE: 3,
            CAPTURE: 4
        },
        applyLabels = [
            null, //first element is null to use `MODE` property values to match button label
            "${recipe-edit.label.create}",
            "${recipe-edit.label.apply}",
            "${recipe-edit.label.clone}",
            "${recipe-edit.label.create}"
        ];

    // declare private functions - START

    /**
     * clearGenerated - clear generated controls from recipe editor content area
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function clearGenerated(self) {
        var tmpControls = [],
            tokens = self.vars.tokens;
        //a copy of `self.controls` is created since `shmi.deleteControl` removes control
        //references from `<container>.controls` automatically.
        self.controls.forEach(function(c) {
            tmpControls.push(c);
        });
        tmpControls.forEach(function(c) {
            shmi.deleteControl(c, true);
        });

        if (tokens.apply) {
            tokens.apply.unlisten();
            tokens.apply = null;
        }

        if (tokens.cancel) {
            tokens.cancel.unlisten();
            tokens.cancel = null;
        }

        self.vars.controls = {
            name: null,
            items: {},
            meta: {},
            buttons: {}
        };
    }

    /**
     * generateContent - generate recipe editor content area controls for specified mode
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateContent(self) {
        generateName(self);
        generateMeta(self);
        generateItems(self);
        generateButtons(self);
    }

    /**
     * generateName - generate input control(-s) for recipe name
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateName(self) {
        var anchors = self.vars.anchors,
            controls = self.vars.controls;

        controls.name = shmi.createControl("input-field", anchors.name, {
            "label": "${recipe-edit.label.recipeName}",
            "type": shmi.Constants.TYPE_STRING,
            "auto-type": false,
            "class-name": "input-field label-beside"
        });
        if (!self.config.editName && controls.name) shmi.addClass(controls.name.element, "hidden");
    }

    /**
     * getMetaType - Description
     *
     * @param {object} self control instance reference
     * @param {string} metaProp meta data property name
     *
     * @returns {number|null} item type of meta property
     */
    function getMetaType(self, metaProp) {
        var recipe = self.vars.recipe,
            c = shmi.Constants,
            metaToType = {
                "string": c.TYPE_STRING,
                "int": c.TYPE_INT,
                "long": c.TYPE_INT,
                "longlong": c.TYPE_INT,
                "uint": c.TYPE_INT,
                "ulong": c.TYPE_INT,
                "ulonglong": c.TYPE_INT,
                "float": c.TYPE_FLOAT,
                "double": c.TYPE_FLOAT,
                "bool": c.TYPE_BOOL,
                "boolean": c.TYPE_BOOL
            },
            entry = null;

        recipe.meta.some(function(m) {
            if (m.name === metaProp) {
                entry = m;
                return true;
            }
            return false;
        });

        return entry ? metaToType[entry.type] : null;
    }

    /**
     * generateMeta - generate input controls for recipe meta data
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateMeta(self) {
        var anchors = self.vars.anchors,
            controls = self.vars.controls,
            recipe = self.vars.recipe;

        recipe.meta.forEach(function(meta) {
            var type = getMetaType(self, meta.name);
            if (type === shmi.Constants.TYPE_BOOL) {
                controls.meta[meta.name] = shmi.createControl("checkbox", anchors.meta, {
                    "ui": "checkbox",
                    "label": meta.label || meta.name,
                    "class-name": "checkbox label-beside recipe-bool",
                    "template": "default/recipe-edit/bool-checkbox",
                    "on-value": true,
                    "off-value": false
                }, "DIV");
            } else {
                controls.meta[meta.name] = shmi.createControl("input-field", anchors.meta, {
                    "label": meta.label || meta.name,
                    "type": type,
                    "auto-type": false,
                    "value-alignment": "auto",
                    "class-name": "input-field label-beside"
                }, "DIV");
            }
        });
    }

    /**
    * This function returns a mouse handler for the marker
    *
    * @param  {object} self control instance reference
    * @param  {object} target element
    * @return {object} mouse handler
    */

    function getRowHandler(self, el) {
        return {
            onPress: function(x, y, evt) {
            },
            onEnter: function(evt) {
                if (!self.locked) {
                    shmi.addClass(el.parentNode, "hover");
                }
            },
            onLeave: function(evt) {
                shmi.removeClass(el.parentNode, "hover");
            },
            onRelease: function(x, y, evt) {
                if (!self.locked) {
                    var arrow = shmi.getUiElement("expand", el);
                    if (shmi.hasClass(arrow, "expand")) {
                        shmi.removeClass(arrow, "expand");
                    } else {
                        shmi.addClass(arrow, "expand");
                    }
                    var anchor = shmi.getUiElement("anchor", el.parentNode);
                    if (anchor.style.display === "none") {
                        anchor.style.display = "block";
                    } else {
                        anchor.style.display = "none";
                    }
                }
            }
        };
    }

    /**
    * This function returns a touch handler for the marker
    *
    * @param  {object} self control instance reference
    * @param  {object} target element
    * @return {object} touch handler
    */
    function getRowHandlerTouch(self, el) {
        return {
            onPress: function(x, y, evt) {
                self.vars.performClick = true;
            },
            onDrag: function(evt) {
                self.vars.performClick = false;
            },
            onRelease: function(x, y, evt) {
                if (!self.locked && self.vars.performClick) {
                    var arrow = shmi.getUiElement("expand", el);
                    if (shmi.hasClass(arrow, "expand")) {
                        shmi.removeClass(arrow, "expand");
                    } else {
                        shmi.addClass(arrow, "expand");
                    }
                    var anchor = shmi.getUiElement("anchor", el.parentNode);
                    if (anchor.style.display === "none") {
                        anchor.style.display = "block";
                    } else {
                        anchor.style.display = "none";
                    }
                }
            }
        };
    }

    /**
     * buildTreeView - generate tree view out of items object
     *
     * @param  {object} self control instance reference
     * @param  {object} items object
     * @param  {object} anchor element
     * @return {undefined}
     */

    function buildTreeView(self, itemsObj, anchor) {
        var iter = shmi.requires("visuals.tools.iterate").iterateObject;
        shmi.loadResource("templates/default/recipe-edit/treeview.html", function(html, error) {
            if (!error) {
                if (!anchor) anchor = self.vars.anchors.items;

                iter(itemsObj, function(val, el) {
                    if (el !== "myLabel") {
                        var newDiv = document.createElement('div');
                        newDiv.setAttribute("data-ui", "tree-node");
                        newDiv.setAttribute("class", "element cont");
                        newDiv.innerHTML = html;
                        var cName = shmi.getUiElement("control-name", newDiv);
                        var arrow = shmi.getUiElement("expand", newDiv);
                        anchor.appendChild(newDiv);
                        if ((typeof itemsObj[el] === "object") && (!itemsObj[el].actualItem)) {
                            const target = shmi.getUiElement("anchor", newDiv),
                                t = shmi.getUiElement("info", newDiv),
                                label = itemsObj[el]["myLabel"];

                            target.style.display = "none";
                            cName.innerText = shmi.localize(label);
                            self.vars.rowHandlerTokens.push(new shmi.visuals.io.MouseListener(t, getRowHandler(self, t)));
                            self.vars.rowHandlerTokens.push(new shmi.visuals.io.TouchListener(t, getRowHandlerTouch(self, t)));
                            buildTreeView(self, itemsObj[el], target);
                        } else {
                            var inputAnchor = shmi.getUiElement("value-holder", newDiv);
                            arrow.style.display = "none";
                            generateItemInput(itemsObj[el].actualItem, inputAnchor, self);
                        }
                    }
                });
            }
        });
    }

    /**
     * generateItemInput - create control for item
     *
     * @param  {object} item object
     * @param  {object} anchor element
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateItemInput(item, anchor, self) {
        var inputOptions = null,
            controls = self.vars.controls,
            enableNumpad = false,
            uc = shmi.requires("shmi.visuals.tools.unitClasses");

        if (item.read) {
            if (item.type === shmi.Constants.TYPE_BOOL) {
                inputOptions = {
                    "ui": "checkbox",
                    "label": item.label || item.name,
                    "class-name": "checkbox label-beside recipe-bool",
                    "template": "default/recipe-edit/bool-checkbox",
                    "on-value": 1,
                    "off-value": 0
                };
            } else if (self.vars.dictionary && self.vars.dictionary["itemoptions-" + item.name]) {
                inputOptions = {
                    "ui": "select-box",
                    "label": item.label || item.name,
                    "class-name": "select-box label-beside recipe-option"
                };
                inputOptions.options = self.vars.dictionary["itemoptions-" + item.name];
            } else {
                if (self.config.enableNumpad && item.type !== shmi.Constants.TYPE_STRING) enableNumpad = true;
                inputOptions = {
                    "ui": "input-field",
                    "label": item.label || item.name,
                    "class-name": "input-field label-beside",
                    "numpad-enabled": enableNumpad,
                    "auto-type": false,
                    "value-alignment": "auto",
                    "unit-text": item.unit || "",
                    "type": item.type,
                    "precision": (typeof item.digits === "number") ? item.digits : -1
                };
                if ([shmi.Constants.TYPE_INT, shmi.Constants.TYPE_FLOAT].indexOf(item.type) !== -1) {
                    if (item.adapter) {
                        var selectedAdapter = uc.getSelectedAdapter(item.unitClass);
                        if (typeof item.min === "number") {
                            inputOptions.min = selectedAdapter.outFunction(item.min);
                        }
                        if (typeof item.max === "number") {
                            inputOptions.max = selectedAdapter.outFunction(item.max);
                        }
                    } else {
                        if (typeof item.min === "number") {
                            inputOptions.min = item.min;
                        }
                        if (typeof item.max === "number") {
                            inputOptions.max = item.max;
                        }
                    }
                }
            }

            controls.items[item.name] = shmi.createControl(inputOptions.ui, anchor, inputOptions, "DIV");
            if (!item.write) {
                controls.items[item.name].lock(true);
            }
        }
    }

    /**
     * buildItemsObject - build items object for generating a tree view out of it
     *
     * @param  {object} recipe
     * @return {object} items object
     */

    function buildItemsObject(recipe) {
        var itemsObj = {};
        recipe.items.forEach(function(item) {
            var currItem = {};
            currItem.raw = item.name.split(".");
            currItem.parts = [];
            currItem.index = [];

            currItem.raw.forEach(function(el, idx) { // find all indices
                currItem.index[idx] = [];
                if (el.indexOf("[") !== -1) {
                    var indexes = (el.substring(el.indexOf("[")).split("[").join("").split("]"));
                    indexes.forEach(function(el2, idx2) {
                        currItem.index[idx][idx2] = el2;
                    });
                    currItem.parts[idx] = el.substring(0, el.indexOf("["));
                } else {
                    currItem.parts[idx] = el;
                    currItem.index[idx][0] = "";
                }
            });

            var root = {};
            var start = currItem.parts[0] + currItem.index[0][0];

            if (currItem.parts.length > 1 || currItem.index.length > 1) { // multipart item
                if (!itemsObj[start]) itemsObj[start] = {};
                itemsObj[start].myLabel = "${" + currItem.parts[0] + "} " + currItem.index[0][0];
                root = itemsObj[start];

                for (let k = 1; k < currItem.index[0].length-1; k++) {
                    if (!root[currItem.index[0][k]]) root[currItem.index[0][k]] = {};
                    root = root[currItem.index[0][k]];
                }

                for (let i = 1; i < currItem.parts.length; i++) {
                    var t = currItem.parts[i] + currItem.index[i][0];
                    if (!root[t]) root[t] = {};
                    root[t].myLabel = "${" + currItem.parts[i] + "} " + currItem.index[i][0];
                    if (i === currItem.parts.length-1) {
                        root[t].actualItem = item;
                        root[t].value = "";
                    } else {
                        root = root[t];
                    }
                }
            } else { // simple item w/o . and index
                itemsObj[start] = {};
                itemsObj[start].actualItem = item;
                itemsObj[start].value = "";
            }
        });
        return itemsObj;
    }

    /**
     * generateItems - generate input controls for recipe item data
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateItems(self) {
        var anchors = self.vars.anchors,
            recipe = self.vars.recipe;

        if (self.config.viewMode === "linear") {
            recipe.items.forEach(function(item) {
                generateItemInput(item, anchors.items, self);
            });
        }

        if (self.config.viewMode === "tree") {
            const itemsObj = buildItemsObject(recipe),
                anchor = self.vars.anchors.items;
            while (anchor.firstChild) {
                anchor.removeChild(anchor.firstChild);
            }
            buildTreeView(self, itemsObj);
        }

        self.vars.rowHandlerTokens.forEach(function(t) {
            t.enable();
        });
    }

    /**
     * getMetaData - get meta data input values
     *
     * @param  {object} self control instance reference
     * @return {object} meta data input
     */
    function getMetaData(self) {
        var iter = shmi.requires("visuals.tools.iterate").iterateObject,
            meta = {};

        iter(self.vars.controls.meta, function(val, prop) {
            meta[prop] = val.getValue();
        });

        return meta;
    }

    /**
     * getItemData - get item data input values
     *
     * @param  {object} self control instance reference
     * @return {object} item data input
     */
    function getItemData(self) {
        var recipe = self.vars.recipe,
            itemData = {},
            uc = shmi.requires("shmi.visuals.tools.unitClasses");

        recipe.items.forEach(function(item, idx) {
            var control = self.vars.controls.items[item.name];
            if (control && item.read && item.write) {
                if (item.adapter) {
                    var selectedAdapter = uc.getSelectedAdapter(item.unitClass);
                    itemData[item.name] = selectedAdapter.inFunction(control.getValue());
                } else {
                    itemData[item.name] = control.getValue();
                }
            }
        });

        return itemData;
    }

    /**
     * createRecipe - create new recipe with data from editor
     *
     * @param  {object} self control instance reference
     * @param  {function} callback function to call on completion
     * @return {undefined}
     */
    function createRecipe(self, callback) {
        var rm = shmi.requires("visuals.session.RecipeManager"),
            controls = self.vars.controls,
            editor = self.vars.editor;

        rm.getTemplate(editor.template_id, function(response, err) {
            if (err) {
                shmi.notify("${recipe-edit.error.fetchTemplate}", "${V_ERROR}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                });
                callback(err);
            } else {
                response.createRecipe(controls.name.getValue(), getMetaData(self), function(createResponse, createErr) {
                    if (createErr) {
                        shmi.notify("${recipe-edit.error.createRecipe}", "${V_ERROR}", {
                            ERROR_CODE: createErr.errc,
                            ERROR_MSG: createErr.message,
                            ERROR_CAT: createErr.category
                        });
                        callback(null, createErr);
                    } else {
                        callback(createResponse, null);
                    }
                });
            }
        });
    }

    /**
     * updateRecipe - update existing recipe data
     *
     * @param  {object} self control instance reference
     * @param  {function} callback function called on completion
     * @return {undefined}
     */
    function updateRecipe(self, callback) {
        var rm = shmi.requires("visuals.session.RecipeManager"),
            controls = self.vars.controls,
            editor = self.vars.editor;

        rm.getRecipe(editor.recipe_id, function(response, err) {
            if (err) {
                shmi.notify("${recipe-edit.error.fetchRecipe}", "${V_ERROR}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                });
                callback(err);
            } else {
                response.set(controls.name.getValue(), getMetaData(self), false, function(setErr) {
                    if (setErr) {
                        shmi.notify("${recipe-edit.error.updateRecipe}", "${V_ERROR}", {
                            ERROR_CODE: setErr.errc,
                            ERROR_MSG: setErr.message,
                            ERROR_CAT: setErr.category
                        });
                        callback(setErr);
                    } else if (typeof response.versionId === "number") {
                        response.write(getItemData(self), function(writeErr) {
                            if (writeErr) {
                                shmi.notify("${recipe-edit.error.writeRecipe}", "${V_ERROR}", {
                                    ERROR_CODE: writeErr.errc,
                                    ERROR_MSG: writeErr.message,
                                    ERROR_CAT: writeErr.category
                                });
                            }
                            callback(writeErr || null);
                        });
                    } else {
                        callback(null);
                    }
                });
            }
        });
    }

    /**
     * setTitleRecipe - set recipe name in title of recipe editor
     *
     * @param  {object} self control instance reference
     * @param  {string} recipeName text to display
     * @return {undefined}
     */
    function setTitleRecipe(self, recipeName) {
        var rName = self.vars.anchors.recipeName;

        if (typeof recipeName === "string") {
            rName.textContent = "(" + recipeName + ")";
        } else {
            rName.textContent = "";
        }
    }

    /**
     * captureRecipe - capture current item values to specified recipe
     *
     * @param  {object} recipe recipe object
     * @param  {function} callback function to call on completion
     * @return {undefined}
     */
    function captureRecipe(recipe, callback) {
        recipe.capture(null, function(captureErr) {
            if (captureErr) {
                shmi.notify("${recipe-edit.error.captureRecipe}", "${V_ERROR}", {
                    ERROR_CODE: captureErr.errc,
                    ERROR_MSG: captureErr.message,
                    ERROR_CAT: captureErr.category
                });
            }
            callback(captureErr || null);
        });
    }

    /**
     * generateButtons - generate apply/capture & cancel buttons for recipe editor
     *
     * @param  {object} self control instance reference
     * @return {undefined}
     */
    function generateButtons(self) {
        var anchors = self.vars.anchors,
            editor = self.vars.editor,
            tokens = self.vars.tokens,
            controls = self.vars.controls;

        if (self.config.viewMode === "tree") {
            controls.buttons.expandAll = shmi.createControl("button", anchors.expandCollapse, {
                "label": "",
                "icon-src": "pics/system/controls/recipe-edit/expand-all.svg",
                "icon-title": "${recipe-edit.label.expand-all}",
                "show-icon": true,
                "show-text": false,
                "name": "expandAll"
            }, "DIV");
            tokens.expandAll = controls.buttons.expandAll.listen("click", function(evt) {
                var list = document.getElementsByClassName("cont");
                for (let item of list) {
                    const anchor = shmi.getUiElement("anchor", item),
                        arrow = shmi.getUiElement("expand", item);

                    anchor.style.display = "block";
                    shmi.addClass(arrow, "expand");
                }
            });
            controls.buttons.collapseAll = shmi.createControl("button", anchors.expandCollapse, {
                "label": "",
                "icon-src": "pics/system/controls/recipe-edit/collapse-all.svg",
                "icon-title": "${recipe-edit.label.collapse-all}",
                "show-icon": true,
                "show-text": false,
                "name": "collapseAll"
            }, "DIV");
            tokens.collapseAll = controls.buttons.collapseAll.listen("click", function(evt) {
                var list = document.getElementsByClassName("cont");
                for (let item of list) {
                    const anchor = shmi.getUiElement("anchor", item),
                        arrow = shmi.getUiElement("expand", item);

                    anchor.style.display = "none";
                    shmi.removeClass(arrow, "expand");
                }
            });
        }

        controls.buttons.apply = shmi.createControl("button", anchors.buttons, {
            label: applyLabels[MODE.EDIT],
            name: "applyRecipe"
        }, "DIV");
        tokens.apply = controls.buttons.apply.listen("click", function(evt) {
            switch (editor.mode) {
            case MODE.CREATE:
                createRecipe(self, function(recipe, err) {
                    if (err) {
                        console.error(uiType, "error creating recipe:", err);
                    } else {
                        console.log(uiType, "created new recipe");
                        clearValues(self);
                        self.onLock();
                        self.fire("stop-edit", { recipe: recipe, cancel: false });
                    }
                });
                break;
            case MODE.EDIT:
                updateRecipe(self, function(err) {
                    if (err) {
                        console.error(uiType, "error updating recipe:", err);
                    } else {
                        console.log(uiType, "recipe updated");
                        clearValues(self);
                        self.onLock();
                        self.fire("stop-edit", { recipe: self, cancel: false });
                    }
                });
                break;
            case MODE.CAPTURE:
                createRecipe(self, function(recipe, err) {
                    if (err) {
                        console.error(uiType, "error creating recipe:", err);
                    } else {
                        console.log(uiType, "created new recipe");
                        captureRecipe(recipe, function(captureErr) {
                            if (captureErr) {
                                console.error(uiType, "error capturing recipe values:", captureErr);
                            } else {
                                console.log(uiType, "recipe values captured");
                                clearValues(self);
                                self.onLock();
                                self.fire("stop-edit", { recipe: recipe, cancel: false });
                            }
                        });
                    }
                });
                break;
            default:
            }
        });
        controls.buttons.cancel = shmi.createControl("button", anchors.buttons, {
            label: "${recipe-edit.label.cancel}",
            name: "cancelRecipe"
        }, "DIV");
        tokens.cancel = controls.buttons.cancel.listen("click", function(evt) {
            clearValues(self);
            self.onLock();
            self.fire("stop-edit", { cancel: true });
        });
    }

    /**
     * updateVisibility - update visibility of editor components match specified mode
     *
     * @param {object} self control instance reference
     * @param {number} mode editor mode
     *
     * @returns {undefined}
     */
    function updateVisibility(self, mode) {
        var anchors = self.vars.anchors,
            recipe = self.vars.recipe;

        switch (mode) {
        case MODE.CREATE:
        case MODE.CAPTURE:
            shmi.removeClass(anchors.meta, "hidden");
            shmi.addClass(anchors.items, "hidden");
            break;
        case MODE.EDIT:
            shmi.removeClass(anchors.meta, "hidden");
            if (recipe.valuesSet) {
                shmi.removeClass(anchors.items, "hidden");
            } else {
                shmi.addClass(anchors.items, "hidden");
            }
            break;
        default:
            throw new Error("[RecipeEdit] updateVisibility - Invalid mode specified: " + mode);
        }
    }

    /**
     * clearValues - reset editor values to default state
     *
     * @param {object} self control instance reference
     *
     * @returns {undefined}
     */
    function clearValues(self) {
        var recipe = self.vars.recipe = shmi.cloneObject(self.vars.template),
            editor = self.vars.editor,
            controls = self.vars.controls;

        recipe.valuesSet = false;
        editor.recipe_id = null;
        editor.mode = null;
        controls.name.setValue("");
        setTitleRecipe(self, null);
        recipe.meta.forEach(function(meta) {
            controls.meta[meta.name].setValue("");
        });
        recipe.items.forEach(function(item) {
            if (controls.items[item.name]) {
                controls.items[item.name].setValue(null);
            }
        });
    }

    /**
     * updateValues - update editor component values
     *
     * @param {object} self control instance reference
     * @param {number} mode editor mode
     *
     * @returns {undefined}
     */
    function updateValues(self, mode) {
        var recipe = self.vars.recipe,
            controls = self.vars.controls,
            uc = shmi.requires("shmi.visuals.tools.unitClasses");

        controls.name.setValue(recipe.name);
        controls.buttons.apply.setLabel(applyLabels[mode]);

        switch (mode) {
        case MODE.CREATE:
        case MODE.CAPTURE:
            recipe.meta.forEach(function(meta) {
                controls.meta[meta.name].setValue(meta.default);
            });
            break;
        case MODE.EDIT:
            recipe.meta.forEach(function(meta) {
                controls.meta[meta.name].setValue(meta.value);
            });
            recipe.items.forEach(function(item) {
                if (controls.items[item.name]) {
                    if (item.adapter) {
                        var selectedAdapter = uc.getSelectedAdapter(item.unitClass);
                        controls.items[item.name].setValue(selectedAdapter.outFunction(item.value));
                    } else {
                        controls.items[item.name].setValue(item.value);
                    }
                }
            });
            break;
        default:
            throw new Error("[RecipeEdit] updateValues - Invalid mode specified: " + mode);
        }
    }

    /**
     * fetchTemplateData - fetch template data from connect
     *
     * @param {object} self control instance reference
     * @param {function} callback function to call on completion
     *
     * @returns {undefined}
     */
    function fetchTemplateData(self, callback) {
        var template = self.vars.template,
            editor = self.vars.editor,
            iter = shmi.requires("visuals.tools.iterate").iterateObject,
            rm = shmi.requires("visuals.session.RecipeManager");

        rm.getTemplate(editor.template_id, function(response, err) {
            template.items = [];
            template.meta = [];
            if (err) {
                shmi.notify("${recipe-edit.error.fetchTemplate}", "${V_ERROR}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                });
                callback(err);
            } else {
                template.name = response.name;
                response.items.forEach(function(itemName) {
                    template.items.push({
                        name: itemName,
                        label: null,
                        unit: null,
                        type: null,
                        value: null,
                        digits: null,
                        write: false,
                        read: false
                    });
                });
                iter(response.metadata, function(val, prop) {
                    template.meta.push({
                        name: prop,
                        default: val.default_value,
                        label: val.label || prop,
                        type: val.value_type,
                        value: null
                    });
                });
                callback(null);
            }
        });
    }

    /**
     * getItemByName - get item value from stored recipe data
     *
     * @param {object} self control instance reference
     * @param {string} itemName name of item
     *
     * @returns {number|string|boolean|null} item value or `null` if not present
     */
    function getItemByName(self, itemName) {
        var recipe = self.vars.recipe,
            itemIdx = -1;

        recipe.items.some(function(itm, idx) {
            if (itm.name === itemName) {
                itemIdx = idx;
                return true;
            }
            return false;
        });

        return (itemIdx !== -1) ? recipe.items[itemIdx] : null;
    }

    /**
     * getMetaByName - get meta data value from stored recipe data
     *
     * @param {object} self control instance reference
     * @param {string} metaName name of meta data property
     *
     * @returns {number|string|boolean|null} meta data value or `null` if not present
     */
    function getMetaByName(self, metaName) {
        var recipe = self.vars.recipe,
            metaIdx = -1;

        recipe.meta.some(function(m, idx) {
            if (m.name === metaName) {
                metaIdx = idx;
                return true;
            }
            return false;
        });

        return (metaIdx !== -1) ? recipe.meta[metaIdx] : null;
    }

    /**
     * fetchRecipeData - fetch recipe data from connect
     *
     * @param {object} self control instance reference
     * @param {function} callback function to call on completion
     *
     * @returns {undefined}
     */
    function fetchRecipeData(self, callback) {
        var editor = self.vars.editor,
            recipe = self.vars.recipe = shmi.cloneObject(self.vars.template),
            iter = shmi.requires("visuals.tools.iterate").iterateObject,
            rm = shmi.requires("visuals.session.RecipeManager");

        rm.getRecipe(editor.recipe_id, function(response, err) {
            if (err) {
                shmi.notify("${recipe-edit.error.fetchRecipe}", "${V_ERROR}", {
                    ERROR_CODE: err.errc,
                    ERROR_MSG: err.message,
                    ERROR_CAT: err.category
                });
                callback(err);
            } else {
                recipe.name = response.name;
                iter(response.metadata, function(val, prop) {
                    var m = getMetaByName(self, prop);

                    if (m) {
                        m.value = val;
                    }
                });
                iter(response.values, function(val, prop) {
                    var itm = getItemByName(self, prop);

                    if (itm) {
                        itm.value = val.value;
                    }
                });
                if (typeof response.versionId === "number") {
                    recipe.valuesSet = true;
                } else {
                    recipe.valuesSet = false;
                }
                callback(null);
            }
        });
    }

    /**
     * assignSingleItemInfo - assigns item data to itemInfo object
     *
     * @param {object} itemInfo
     * @param {object} item item data
     */
    function assignSingleItemInfo(itemInfo, item) {
        Object.assign(itemInfo, {
            adapter: item.adapter,
            unitClass: item.unitClass,
            type: item.type,
            digits: item.digits,
            write: item.writable,
            min: item.min,
            max: item.max,
            read: item.initialized && item.type !== -1,
            error: !item.initialized
        });
    }

    /**
     * fetchSingleItemInfo - fetch item info of single item
     *
     * @param {object} itemInfo
     * @param {number} [timeout] time in ms for the operation to timeout.
     * @returns {Promise<void>}
     */
    function fetchSingleItemInfo(itemInfo, timeout = 10000) {
        const im = shmi.requires("visuals.session.ItemManager");

        return new Promise(function collectItemInfo(resolve, reject) {
            let token = null,
                timeoutId = null;

            let item = im.getItem(itemInfo.name);

            if (item && item.initialized) {
                itemInfo.unit = item.unit;
                itemInfo.label = item.labelToken;
                assignSingleItemInfo(itemInfo, item);
                resolve();
            } else {
                timeoutId = setTimeout(() => {
                    const err = new Error("Operation timed out");

                    token.unlisten();
                    token = null;

                    err.itemName = itemInfo.name;
                    reject(err);
                }, timeout);

                token = im.subscribeItem(itemInfo.name, {
                    // This is going to be called first
                    setLabel: (label) => itemInfo.label = label,
                    // This is going to be called second
                    setUnitText: (unitText) => itemInfo.unit = unitText,
                    // This is going to be called last
                    setProperties: (min, max, step, name, type) => {
                        if (type === null) {
                            // Subscription has not been processed yet.
                            return;
                        } else if (!token) {
                            return;
                        }

                        item = im.getItem(itemInfo.name);
                        if (item) {
                            assignSingleItemInfo(itemInfo, item);
                        }

                        clearTimeout(timeoutId);
                        token.unlisten();
                        token = null;
                        resolve();
                    },
                    setValue: (value) => {}
                });
            }
        });
    }

    /**
     * fetchItemInfo - fetch item info from item subscriptions
     *
     * @param {object} self control instance refernce
     * @param {function} callback function to call on completion
     *
     * @returns {undefined}
     */
    function fetchItemInfo(self, callback) {
        const promises = self.vars.template.items.map((itemInfo) => fetchSingleItemInfo(itemInfo, self.config.itemFetchTimeout || defConfig.itemFetchTimeout));

        Promise.allSettled(promises).then((results) => {
            const rejected = results.filter(({ status }) => status === 'rejected');

            if (rejected.length === 0) {
                callback(null);
                return;
            }

            const err = new Error(`Unable to load item info for ${rejected.length} item(s).`);
            err.items = rejected.map(({ reason: { itemName } }) => itemName).filter((itemName) => itemName && itemName.length > 0);

            console.error(err, err.items);
            callback(err);
        });
    }

    /**
     * initAnchorElements - initialize required template anchor elements
     *
     * @param {object} self control instance reference
     *
     * @returns {undefined}
     */
    function initAnchorElements(self) {
        var iter = shmi.requires("visuals.tools.iterate").iterateObject,
            anchors = self.vars.anchors;

        anchors.title = shmi.getUiElement("editor-title", self.element);
        anchors.recipeName = shmi.getUiElement("recipe-name", self.element);
        anchors.body = shmi.getUiElement("editor-body", self.element);
        anchors.name = shmi.getUiElement("name-anchor", self.element);
        anchors.meta = shmi.getUiElement("meta-anchor", self.element);
        anchors.items = shmi.getUiElement("items-anchor", self.element);
        anchors.expandCollapse = shmi.getUiElement("expand-collapse-anchor", self.element);
        anchors.buttons = shmi.getUiElement("buttons-anchor", self.element);

        iter(anchors, function(val, prop) {
            if (val === null) {
                throw new Error("[RecipeEdit] template does not contain all required anchor elements");
            }
        });
    }

    /**
     * updateEditor - prepeare editor area for use
     *
     * @param {object} self control instance reference
     * @oaram {boolean} preview make editor read-only for preview
     * @return {undefined}
     */
    function updateEditor(self, preview) {
        var editor = self.vars.editor,
            recipe = self.vars.recipe;

        if (preview === true) {
            self.onLock();
        } else {
            self.onUnlock();
        }

        setTitleRecipe(self, editor.recipe_id === null ? null : recipe.name);
        updateVisibility(self, editor.mode);
        updateValues(self, editor.mode);
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            anchors: {
                title: null,
                body: null,
                name: null,
                meta: null,
                items: null,
                buttons: null
            },
            controls: {
                name: null,
                items: {},
                meta: {},
                buttons: {}
            },
            tokens: {
                apply: null,
                cancel: null
            },
            editor: {
                template_id: null,
                recipe_id: null,
                mode: null
            },
            recipe: {
                name: null,
                meta: [],
                items: []
            },
            template: {
                name: null,
                meta: [],
                items: []
            },
            rowHandlerTokens: [],
            templateSet: false,
            performClick: true,
            fetchId: null,
            dictionary: null
        },
        /* imports added at runtime */
        imports: {},

        /* array of custom event types fired by this control */
        events: ["start-edit", "stop-edit"],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;
                initAnchorElements(self);
                self.vars.anchors.title.textContent = shmi.localize(self.config.title);
                if (self.config.dictionary) {
                    self.vars.dictionary = shmi.requires(self.config.dictionary).dictionary;
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                var self = this;
                self.vars.rowHandlerTokens.forEach(function(t) {
                    t.enable();
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                var self = this;
                self.vars.rowHandlerTokens.forEach(function(t) {
                    t.disable();
                });
                self.vars.rowHandlerTokens = [];
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                self.controls.forEach(function(c) {
                    if (c.config.name !== "collapseAll" && c.config.name !== "expandAll") {
                        c.lock();
                    }
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                self.controls.forEach(function(c) {
                    c.unlock();
                });
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step) {

            },
            setTemplate: function(templateId, recipeId, callback) {
                var self = this,
                    editor = self.vars.editor,
                    fetchId = null;

                self.vars.fetchId = {};

                if (self.isActive()) {
                    editor.template_id = templateId;
                    self.vars.templateSet = false;
                    fetchId = self.vars.fetchId; //guard object to only update editor content on last request sent
                    fetchTemplateData(self, function(err) {
                        if (!err) {
                            if (self.isActive()) {
                                fetchItemInfo(self, function(err2) {
                                    if (err2) {
                                        if (!self.isActive()) {
                                            // Widget is inactive - don't do anything
                                        } else if (err2.items) {
                                            clearGenerated(self);
                                            shmi.notify("${recipe-edit.error.fetchItemInfoTimeout}", "${V_ERROR}", {
                                                ITEM_COUNT: err2.items.length,
                                                // Dot out items if there's more than 3
                                                ITEMS: (err2.items.length > 3 ? [...err2.items.slice(0, 3), "..."] : err2.items).join(", ")
                                            });
                                        } else {
                                            clearGenerated(self);
                                            shmi.notify("${recipe-edit.error.fetchItemInfo}", "${V_ERROR}");
                                        }
                                    } else if (self.vars.fetchId === fetchId) {
                                        self.vars.fetchId = null;
                                        self.vars.templateSet = true;
                                        self.vars.recipe = shmi.cloneObject(self.vars.template);
                                        if (self.isActive()) {
                                            clearGenerated(self);
                                            generateContent(self);
                                            if (recipeId !== null) {
                                                self.editRecipe({
                                                    mode: MODE.EDIT,
                                                    recipe_id: recipeId,
                                                    preview: true
                                                });
                                            } else {
                                                setTitleRecipe(self, null);
                                                self.onLock();
                                            }
                                        }
                                    }
                                    callback(null);
                                });
                            }
                        } else {
                            callback(err);
                        }
                    });
                } else {
                    throw new Error("[" + uiType + "] control not enabled yet!");
                }
            },
            isTemplateSet: function() {
                return this.vars.templateSet;
            },
            /**
             * editRecipe - open recipe for editing in recipe-edit
             *
             * @param {object} options recipe edit options
             * @param {number} options.mode recipe edit mode
             * @param {number} options.recipe_id recipe ID
             * @param {boolean} [options.preview] read-only when set to `true`
             * @return {undefined}
             */
            editRecipe: function(options) {
                var self = this,
                    editor = self.vars.editor;

                if (self.isActive() && self.isTemplateSet()) {
                    editor.mode = options.mode;
                    editor.recipe_id = options.recipe_id;
                    editor.preview = (options.preview === true);
                    if (editor.recipe_id !== null) {
                        if (!editor.preview) {
                            self.fire("start-edit", {});
                        }
                        fetchRecipeData(self, function(err) {
                            if (!err) {
                                updateEditor(self, editor.preview);
                            }
                        });
                    } else {
                        self.vars.recipe = {
                            name: null,
                            meta: [],
                            items: []
                        };
                        updateEditor(self, editor.preview);
                    }
                }
            },
            reset: function() {
                var self = this,
                    editor = self.vars.editor,
                    currentTemplateId = editor.template_id;

                if (self.isActive() && self.isTemplateSet()) {
                    editor.template_id = null;
                    editor.recipe_id = null;
                    editor.mode = null;
                    self.setTemplate(currentTemplateId, null, function(err) {
                        if (err) {
                            console.error(uiType, "error resetting recipe-edit:", err);
                        }
                    });
                }
            },
            getState: function() {
                return shmi.cloneObject(this.vars.editor);
            },
            MODE: MODE
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals recipe-select control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "recipe-select",
 *     "name": null,
 *     "template": "default/recipe-select",
 *     "show-results": "two-or-more",
 *     "recipe-template-id": null
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * show-results {string}: When to display the results dropdown.
 *  * `never` Never show results.
 *  * `always` Always show results if there are more than 0.
 *  * `two-or-more` Show results if there are two or more.
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "recipe-select", // control name in camel-case
        uiType = "recipe-select", // control keyword (data-ui)
        isContainer = true;

    // example - default configuration
    var defConfig = {
        "class-name": className,
        "name": null,
        "template": "default/recipe-select",
        "label": uiType,
        "show-results": "always",
        "recipe-template-id": null,
        "default-value": null,
        "clear-on-focus": false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START
    function getRandomId() {
        return Math.random().toString(36).substr(2, 9);
    }

    function eventProxy(self, event) {
        self.fire(event.type, event.detail);
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            domChild: null,
            dataGridId: null,
            dataGrid: null,
            searchbar: null,
            eventProxy: null,
            tokens: []
        },
        /* imports added at runtime */
        imports: {
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: [
            "change"
        ],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                if (typeof (this.config["recipe-template-id"]) !== "number") {
                    this.element.textContent = "Invalid or no recipe template id given. This control will not work. Please select a valid recipe template id.";
                    fLog("Invalid or no recipe template id given:", this.config["recipe-template-id"]);
                    return;
                }

                this.vars.domChild = shmi.getUiElement("recipe-select-searchbar", this.element);
                if (this.vars.domChild === null) {
                    this.element.textContent = "One or more required DOM elements are missing from the template. This control will not work. Please select a compatible template.";
                    fLog("Some DOM elements are missing from the template.");
                    return;
                }

                do {
                    this.vars.dataGridId = className + "-" + getRandomId();
                } while (this.imports.dgm.getGrid(this.vars.dataGridId) !== null);

                this.imports.dgm.grids[this.vars.dataGridId] = this.vars.dataGrid = new shmi.visuals.core.DataGridRecipe(this.vars.dataGridId, this.config["recipe-template-id"]);
                this.vars.dataGrid.init();

                var searchbarConfig = {
                    "label": this.config["label"],
                    "datagrid": this.vars.dataGridId,
                    "search-column": "recipe_name",
                    "show-results": this.config["show-results"],
                    "result-columns": ["recipe_name"]
                };

                this.vars.searchbar = shmi.createControl("searchbar", this.vars.domChild, searchbarConfig);
                if (this.vars.searchbar === null) {
                    this.element.textContent = "Unable to create searchbar control for recipes. This control will not work.";
                    fLog("Unable to create searchbar control for recipes.");
                }
                const appConfig = shmi.requires("visuals.session.config"),
                    keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`
                if (this.config['clear-on-focus']) {
                    this.vars.tokens.push(this.vars.searchbar.listen("enter", () => {
                        if (!keyboardEnabled) {
                            this.vars.searchbar.setValue("");
                        }
                    }));
                }
            },
            /**
             * Returns the contents of the user input field. Since some
             * browsers add non-breaking-spaces, those spaces are converted to
             * normal ones.
             *
             * @returns {object} selected recipe info
             */
            getValue: function() {
                var matchData = null,
                    recipeId = null,
                    valuesSet = null,
                    nameValue = null;

                if (this.vars.searchbar) {
                    matchData = this.vars.searchbar.getMatchData();
                    if (matchData !== null) {
                        recipeId = parseInt(matchData.recipe_id);
                        valuesSet = parseInt(matchData.values_set);
                        nameValue = this.vars.searchbar.getValue();
                        if (nameValue === matchData.recipe_name) {
                            return {
                                name: this.vars.searchbar.getValue(),
                                recipe_id: isNaN(recipeId) ? null : recipeId,
                                template_id: this.config["recipe-template-id"],
                                values_set: (valuesSet === 1)
                            };
                        }
                    }
                }

                return null;
            },
            /**
             * Sets the value of the user input field.
             *
             * @param {string} val Value to set
             */
            setValue: function(val) {
                if (this.vars.searchbar) {
                    this.vars.searchbar.setValue(val);
                }
            },
            refreshGrid: function() {
                if (this.vars.dataGrid) {
                    this.vars.dataGrid.refresh();
                }
            },
            /* called when control is enabled */
            onEnable: function() {
                if (this.vars.searchbar) {
                    this.vars.eventProxy = this.vars.searchbar.listen("change", eventProxy.bind(null, this));
                    this.vars.searchbar.enable();
                    if (this.config["default-value"] !== null) {
                        this.vars.searchbar.setValue(this.config["default-value"]);
                    }
                }
            },
            /* called when control is disabled */
            onDisable: function() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.disable();
                }

                if (this.vars.eventProxy) {
                    this.vars.eventProxy.unlisten();
                    this.vars.eventProxy = null;
                }
            },
            onDelete: function() {
                if (this.vars.searchbar) {
                    shmi.deleteControl(this.vars.searchbar);
                }

                if (this.vars.dataGrid) {
                    delete this.imports.dgm.grids[this.vars.dataGridId];
                }
                this.vars.tokens.forEach((tok) => {
                    tok.unlisten();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.lock();
                }
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                if (this.vars.searchbar) {
                    this.vars.searchbar.unlock();
                }
            },
            getTemplate: function() {
                return this.config["recipe-template-id"];
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * Responsive Menu
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "responsive-menu",
 *     "name": null,
 *     "template": "default/responsive-menu"
 * }
 *
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 0.2
 */
(function() {
    'use strict';

    //variables for reference in control definition
    var className = "ResponsiveMenu", //control name in camel-case
        uiType = "responsive-menu", //control keyword (data-ui)
        isContainer = false;

    //example - default configuration
    var defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/" + uiType,
        "menu": [],
        "mobile-layouts": [],
        "overlay-style": {
            "background": "transparent",
            "zIndex": 1
        }
    };

    /* Example for config.menu structure */
    /**
    var exampleMenu = [
        {
            "label": "My Category",
            "items": [
                {
                    "label": "My Sub-Category",
                    "items": [
                        {
                            "label": "Menu Entry",
                            "action": [
                                "test-action:1:2:3"
                            ],
                            "icon-src": "pics/system/icons/debug_icon.png" // image icon
                        },
                        {
                            "label": "Menu Entry",
                            "action": [
                                "test-action:1:2:3"
                            ],
                            "icon-class": "icon icon-add" // icon font class
                        }
                    ]
                },
                {
                    "label": "Menu Entry",
                    "action": [
                        "test-action:1:2:3",
                        "test-action:4:5:6"
                    ]
                }
            ]
        },
        {
            "label": "Menu Entry",
            "action": [
                "test-action:1:2:3"
            ]
        },
        {
            "label": "Menu Entry",
            "action": [
                "test-action:4:5:6"
            ]
        }
    ];
    */

    //setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    var DEFAULT_ICON = "pics/system/icons/placeholder.svg";

    //declare private functions - START
    /**
     * Creates a compound listener acting both as touch- and mouse-listener.
     * Creates a touch- and mouse-listener internally and enables/disables them
     * both at the same time.
     *
     * @param {HTMLElement} element Element to attach the listener to.
     * @param {object} handlers Object specifying the corresponding handlers
     *  called by the touch- and mouse-listeners.
     * @returns {object} Object with a listener-like interface.
     */
    function createListener(element, handlers) {
        var io = shmi.requires("visuals.io"),
            ml = new io.MouseListener(element, handlers),
            tl = new io.TouchListener(element, handlers);
        return {
            enable: function() {
                ml.enable();
                tl.enable();
            },
            disable: function() {
                ml.disable();
                tl.disable();
            }
        };
    }

    /**
     * Creates a new html element with css classes and data-ui set.
     *
     * @param {string|string[]} cssClasses CSS class or classes to assign to
     *  the new element.
     * @param {?string} setUiType ui type to set.
     * @param {string} [elementType] Type of the html element. Defaults to
     *  "DIV".
     * @returns {HTMLElement}
     */
    function makeElement(cssClasses, setUiType, elementType) {
        var elem = document.createElement(elementType || "DIV");
        if (typeof cssClasses === "string") {
            shmi.addClass(elem, cssClasses);
        } else if (Array.isArray(cssClasses)) {
            cssClasses.forEach(function(cc) {
                if (typeof cc === "string") {
                    shmi.addClass(elem, cc);
                }
            });
        }
        if (typeof setUiType === "string") {
            elem.setAttribute("data-ui", setUiType);
        }
        return elem;
    }

    /**
     * Marks the given layer as "selected". Successive layers are unmarked as
     * "selected".
     *
     * @param {*} self Reference to the widget.
     * @param {?HTMLElement} element Element to assign the "selected" CSS class
     *  to. If set to `null`, in addition to the successive layers being
     *  unmarked as "selected", the given one is unmarked as well.
     * @param {number} layer Layer index
     */
    function setSelected(self, element, layer) {
        if (self.vars.selected[layer] !== element) {
            if (self.vars.selected[layer] !== null) {
                shmi.removeClass(self.vars.selected[layer], "selected");
            }
            self.vars.selected[layer] = element;
            if (element !== null) {
                shmi.addClass(element, "selected");
            }
        }

        self.vars.selected.forEach(function(e, i) {
            if (e && (i > layer)) {
                shmi.removeClass(e, "selected");
                self.vars.selected[i] = null;
            }
        });
    }

    /**
     * Sets the layer-active css class for the corresponding layer.
     * If `layer` is set to -1, all selections are cleared.
     *
     * @param {*} self Reference to the widget.
     * @param {number} layer Layer index
     */
    function setLayer(self, layer) {
        var pLayerClass = "layer-<%= INDEX %>-active";
        if (layer !== self.vars.activeLayer) {
            if (self.vars.activeLayer >= 0) {
                shmi.removeClass(self.element, shmi.evalString(pLayerClass, {
                    INDEX: self.vars.activeLayer
                }));
            }
            self.vars.activeLayer = layer;
            if (layer >= 0) {
                shmi.addClass(self.element, shmi.evalString(pLayerClass, {
                    INDEX: self.vars.activeLayer
                }));
            }
        }

        if (layer === -1) {
            self.vars.selected.forEach(function(e, idx) {
                if (e !== null) {
                    shmi.removeClass(e, "selected");
                    self.vars.selected[idx] = null;
                }
            });
        }
    }

    function restrictAccess(self, reference) {
        if (!self.vars.restrictedItems.includes(reference)) {
            self.vars.restrictedItems.push(reference);
        }
    }

    function buildMenuGroup(self, group, layer) {
        var groupBase = makeElement("menu-group", "menu-group", "LI"),
            clickOverlay = makeElement("click-overlay", "click-overlay"),
            groupData = makeElement("group-data"),
            iconWrapper = makeElement("icon", "icon"),
            img = makeElement(null, null, "IMG"),
            label = makeElement("menu-group-label", "menu-group-label"),
            triangle = makeElement("triangle"),
            itemList = makeElement("menu-layer-" + (layer + 1), "menu-layer-" + (layer + 1), "UL"),
            handler = {
                onClick: null
            },
            listener = null;

        triangle.style.pointerEvents = "none";

        iconWrapper.appendChild(img);
        groupData.appendChild(iconWrapper);
        groupData.appendChild(label);
        groupData.appendChild(triangle);

        groupBase.appendChild(clickOverlay);
        groupBase.appendChild(groupData);

        groupBase.appendChild(itemList);

        handler.onClick = function() {
            var crumb = self.vars.crumbs["layer" + layer];
            if (shmi.hasClass(groupBase, "selected")) {
                setLayer(self, layer);
                setSelected(self, null, layer);
            } else {
                setLayer(self, layer + 1);
                setSelected(self, groupBase, layer);
                crumb.textContent = shmi.localize(group.label);
            }
        };

        label.textContent = shmi.localize(group.label);

        if (group["icon-src"]) {
            img.setAttribute("src", group["icon-src"]);
        } else {
            img.setAttribute("src", DEFAULT_ICON);
        }

        listener = createListener(clickOverlay, handler);
        self.vars.listeners.push(listener);

        if (group.access && group.access.type && group.access.condition) {
            restrictAccess(self, {
                base: groupBase,
                listener: listener,
                menuItem: group
            });
        }

        group.items.forEach(function(menuElement, idx) {
            if (Array.isArray(menuElement.items)) {
                if (layer >= 1) {
                    console.error("[" + className + "]", "menu-groups not allowed above layer 1");
                } else {
                    itemList.appendChild(buildMenuGroup(self, menuElement, layer + 1));
                }
            } else {
                itemList.appendChild(buildMenuItem(self, menuElement, layer + 1));
            }
        });

        return groupBase;
    }

    function getConditionCallback(element, listener, cssClass) {
        return (isActive) => {
            if (!isActive) {
                shmi.addClass(element, cssClass);
                listener.disable();
            } else {
                shmi.removeClass(element, cssClass);
                listener.enable();
            }
        };
    }

    function buildMenuItem(self, item, layer) {
        var itemBase = makeElement("menu-item", "menu-item", "LI"),
            iconWrapper = makeElement("icon", "icon"),
            img = makeElement(null, null, "IMG"),
            label = makeElement("menu-item-label", "menu-item-label"),
            listener = null,
            handler = {
                onClick: null
            },
            core = shmi.requires("visuals.core"),
            action = item.action ? new core.UiAction(item.action) : null;

        iconWrapper.appendChild(img);
        if (item["icon-src"]) {
            img.setAttribute("src", item["icon-src"]);
        } else {
            img.setAttribute("src", DEFAULT_ICON);
        }
        itemBase.appendChild(iconWrapper);
        label.textContent = shmi.localize(item.label);
        itemBase.appendChild(label);

        handler.onClick = function() {
            setSelected(self, itemBase, layer);
            setLayer(self, -1);
            if (self.vars.mobile) {
                removeOverlay(self);
            }
            if (action) {
                action.execute(self);
            }
        };

        listener = createListener(itemBase, handler);
        self.vars.listeners.push(listener);

        if (item.access && item.access.type && item.access.condition) {
            restrictAccess(self, {
                base: itemBase,
                listener: listener,
                menuItem: item
            });
        }

        return itemBase;
    }

    function buildMenu(self, menu) {
        var menuBase = shmi.getUiElement("menu-layer-0", self.element),
            custom = shmi.getUiElement("custom-content-group", self.element),
            burgerButton = shmi.getUiElement("burger-button", self.element),
            listener = null,
            handler = {
                onClick: null
            };

        handler.onClick = function() {
            if (self.vars.mobile) {
                if (self.vars.activeLayer >= 0) {
                    setLayer(self, -1);
                    removeOverlay(self);
                } else {
                    setLayer(self, 0);
                    createOverlay(self);
                }
            }
        };
        listener = createListener(burgerButton, handler);
        self.vars.listeners.push(listener);

        menu.forEach(function(menuElement) {
            if (Array.isArray(menuElement.items)) {
                menuBase.insertBefore(buildMenuGroup(self, menuElement, 0), custom);
            } else {
                menuBase.insertBefore(buildMenuItem(self, menuElement, 0), custom);
            }
        });
    }

    function createOverlay(self) {
        var overlay = {
                element: document.createElement("DIV"),
                listener: null
            },
            handler = { onClick: null },
            elem = overlay.element,
            s = elem.style;
        s.position = "fixed";
        s.top = 0;
        s.bottom = 0;
        s.left = 0;
        s.right = 0;
        s.background = self.config["overlay-style"].background;
        s.zIndex = self.config["overlay-style"].zIndex;

        document.body.insertBefore(overlay.element, document.body.firstChild);

        handler.onClick = function() {
            if (self.vars.mobile && (self.vars.activeLayer >= 0)) {
                setLayer(self, -1);
                removeOverlay(self);
            }
        };
        overlay.listener = createListener(overlay.element, handler);
        overlay.listener.enable();
        self.vars.overlay = overlay;
    }

    function removeOverlay(self) {
        if (self.vars.overlay) {
            self.vars.overlay.element.parentNode.removeChild(self.vars.overlay.element);
            self.vars.overlay.listener.disable();
            self.vars.overlay = null;
        }
    }

    //declare private functions - END

    //definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            listeners: [],
            restrictedItems: [],
            activeLayer: -1,
            mobile: false,
            selected: [
                null, /* layer 0 */
                null, /* layer 1 */
                null /* layer 2 */
            ],
            crumbs: {
                layer0: null,
                layer1: null
            },
            tokens: [],
            overlay: null
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            /* example - add import via function call */
            qm: function() {
                return shmi.visuals.session.QueryManager;
            }
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                self.vars.crumbs.layer0 = shmi.getUiElement("selected-entry-layer-0");
                self.vars.crumbs.layer1 = shmi.getUiElement("selected-entry-layer-1");

                shmi.removeClass(self.element, "slide");
                shmi.removeClass(self.element, "accordion");
                shmi.addClass(self.element, self.config.mode);
                //setLayer(self, 0);
                buildMenu(self, self.config.menu);
            },
            /* called when control is enabled */
            onEnable: function() {
                var self = this,
                    currentLayout = shmi.getCurrentLayout(),
                    layoutToken = null;

                if (self.config["mobile-layouts"].indexOf(currentLayout) !== -1) {
                    shmi.addClass(self.element, "mobile");
                    self.vars.mobile = true;
                } else {
                    shmi.removeClass(self.element, "mobile");
                    self.vars.mobile = false;
                }

                layoutToken = shmi.listen("layout-change", function(evt) {
                    if (self.config["mobile-layouts"].indexOf(evt.detail.layout) !== -1) {
                        shmi.addClass(self.element, "mobile");
                        if (!self.vars.mobile) {
                            setLayer(self, -1);
                        }
                        self.vars.mobile = true;
                    } else {
                        shmi.removeClass(self.element, "mobile");
                        if (self.vars.mobile) {
                            setLayer(self, 0);
                            removeOverlay(self);
                        }
                        self.vars.mobile = false;
                    }
                });
                self.vars.tokens.push(layoutToken);

                if (!self.vars.mobile) {
                    setLayer(self, 0);
                }

                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });

                self.vars.restrictedItems.forEach((reference) => {
                    const { ConditionObserver } = shmi.requires("visuals.tools.conditions");

                    if (reference.menuItem.access && reference.menuItem.access.type && reference.menuItem.access.condition) {
                        if (reference.menuItem.access.type === "hide") {
                            self.vars.tokens.push(new ConditionObserver(reference.menuItem.access.condition, getConditionCallback(reference.base, reference.listener, "hidden")));
                        } else if (reference.menuItem.access.type === "lock") {
                            self.vars.tokens.push(new ConditionObserver(reference.menuItem.access.condition, getConditionCallback(reference.base, reference.listener, "locked")));
                        }
                    }
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];
                removeOverlay(self);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

shmi.pkg("visuals.controls");
/**
 * Creates a new Rocker Button control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.RockerButton = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'rocker-button');
    shmi.def(this.config, 'template', 'default/rocker-button');
    shmi.def(this.config, 'label', "rocker-button");
    shmi.def(this.config, 'unit-text', null);
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'min', Number.NEGATIVE_INFINITY);
    shmi.def(this.config, 'max', Number.POSITIVE_INFINITY);
    shmi.def(this.config, 'step', 1);
    shmi.def(this.config, 'precision', -1);
    shmi.def(this.config, 'decimal-delimiter', ".");
    shmi.def(this.config, 'type', shmi.c("TYPE_INT"));
    shmi.def(this.config, 'auto-min', true);
    shmi.def(this.config, 'auto-max', true);
    shmi.def(this.config, 'auto-step', true);
    shmi.def(this.config, 'auto-precision', true);
    shmi.def(this.config, 'auto-label', true);
    shmi.def(this.config, 'auto-unit-text', true);
    shmi.def(this.config, 'auto-type', true);
    shmi.def(this.config, 'numpad-enabled', false);

    this.vars = {};
    this.imports = {
        nv: shmi.requires("visuals.tools.numericValues")
    };

    this.value = 0;
    this.active = false;
    this.listeners = [];

    this.startup();
};

shmi.visuals.controls.RockerButton.prototype = {
    uiType: "rocker-button",
    getClassName: function() {
        return "RockerButton";
    },
    /**
     * Initializes the control
     *
     * @returns {unresolved}
     */
    onInit: function() {
        var self = this;

        self.imports.nv.initValueSettings(self);

        var prev = shmi.getUiElement('previous', self.element);
        if (!prev) {
            shmi.log("[RockerButton] no previous element provided", 3);
            return;
        }
        self.currentValueElement = shmi.getUiElement('current', self.element);
        if (!self.currentValueElement) {
            shmi.log("[RockerButton] no current element provided", 3);
            return;
        }
        var next = shmi.getUiElement('next', self.element);
        if (!next) {
            shmi.log("[RockerButton] no next element provided", 3);
            return;
        }
        var label = shmi.getUiElement('label', self.element);
        if (label) {
            if (self.config.label) {
                self.vars = self.vars || {};
                self.vars.label = self.config.label;
                label.textContent = shmi.localize(self.config.label);
            }
        }

        var unit = shmi.getUiElement('unit', self.element);
        if (unit && self.config['unit-text']) {
            self.vars = self.vars || {};
            self.vars.unit = self.config['unit-text'];
            unit.textContent = shmi.localize(self.config['unit-text']);
        }

        self.currentValueElement.textContent = self.imports.nv.formatOutput(self.value, self);
        self.createInputField(self.currentValueElement, self.validate.bind(self));

        var prevFuncs = {};
        prevFuncs.onPress = function(x, y, event) {
            shmi.addClass(prev, 'pressed');
        };
        prevFuncs.onRelease = function() {
            shmi.removeClass(prev, 'pressed');
        };
        prevFuncs.onClick = function() {
            self.currentValueElement.blur();
            var val = self.value - self.vars.valueSettings.step;
            val = self.imports.nv.applyInputSettings(val, self);

            self.value = val;
            self.updateValue();
            self.currentValueElement.textContent = self.imports.nv.formatOutput(self.value, self);
        };
        self.listeners.push(new shmi.visuals.io.MouseListener(prev, prevFuncs));
        self.listeners.push(new shmi.visuals.io.TouchListener(prev, prevFuncs));

        var nextFuncs = {};
        nextFuncs.onPress = function(x, y, event) {
            shmi.addClass(next, 'pressed');
        };
        nextFuncs.onRelease = function() {
            shmi.removeClass(next, 'pressed');
        };
        nextFuncs.onClick = function() {
            self.currentValueElement.blur();
            var val = self.value + parseFloat(self.vars.valueSettings.step);
            val = self.imports.nv.applyInputSettings(val, self);

            self.value = val;
            self.updateValue();
            self.currentValueElement.textContent = self.imports.nv.formatOutput(self.value, self);
        };
        self.listeners.push(new shmi.visuals.io.MouseListener(next, nextFuncs));
        self.listeners.push(new shmi.visuals.io.TouchListener(next, nextFuncs));

        // "validation lite" for the delimeters.
        this.currentValueElement.addEventListener('keydown', function(evt) {
            if (self.config.type === shmi.c("TYPE_FLOAT")) {
                var wrongDelimeter = (self.config['decimal-delimiter'] === ".") ? "," : ".";
                if (evt.key === wrongDelimeter) {
                    evt.preventDefault();
                }
            }
        });
    },
    /**
     * Creates the input field for the Rocker Button
     *
     * @param element - base element of input field
     * @param validateFunc - function to use for validation
     */
    createInputField: function(element, validateFunc) {
        var self = this;
        if (!element) {
            shmi.log('[RockerButton] no base element provided', 3);
            return;
        }
        /* all required elements found */
        var fieldFuncs = {};
        fieldFuncs.onClick = function() {
            var appConfig = shmi.requires("visuals.session.config"),
                keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

            if (self.config["numpad-enabled"] || keyboardEnabled) {
                var im = shmi.visuals.session.ItemManager,
                    nv = shmi.requires("visuals.tools.numericValues"),
                    vs = null,
                    params = {
                        "decimal-delimiter": self.config["decimal-delimiter"],
                        "unit": (self.vars.unit !== undefined) ? self.vars.unit : self.config["unit-text"],
                        "label": (self.vars.label !== undefined) ? self.vars.label : self.config.label,
                        "value": self.value,
                        "callback": function(val) {
                            self.value = val;
                            self.updateValue();
                            self.currentValueElement.textContent = self.imports.nv.formatOutput(self.value, self);
                        }
                    };

                if (!(self.vars && self.vars.valueSettings)) {
                    nv.initValueSettings(self);
                }

                vs = self.vars.valueSettings;
                params.min = vs.min;
                params.max = vs.max;
                params.type = vs.type;
                params.precision = vs.precision;

                if (self.config.item && im.getItem(self.config.item) && self.config['auto-type']) {
                    self.type = im.items[self.config.item].type;
                }

                shmi.numpad(params);
                return;
            }

            shmi.addClass(element, 'selectableText');
            element.setAttribute('contenteditable', true);
            var range = document.createRange();
            element.focus();
            if (element.firstChild && (element.firstChild instanceof Text)) {
                range.setStart(element.firstChild, 0);
                range.setEnd(element.firstChild, element.firstChild.length);
                window.getSelection().removeAllRanges();
                window.getSelection().addRange(range);
            } else {
                shmi.log("[RockerButton] elem not found", 0);
            }
        };
        this.listeners.push(new shmi.visuals.io.MouseListener(element, fieldFuncs));
        this.listeners.push(new shmi.visuals.io.TouchListener(element, fieldFuncs));

        element.style.overflow = 'hidden';
        element.addEventListener('keypress', function(event) {
            if (event.keyCode === 13) {
                event.preventDefault();
                window.getSelection().removeAllRanges();
                element.blur();
            }
        });

        element.addEventListener('blur', function() {
            element.setAttribute('contenteditable', false);
            window.getSelection().removeAllRanges();
            shmi.removeClass(element, 'selectableText');
            shmi.log("[RockerButton] blur event", 0);
            validateFunc(element);
        });
    },
    /**
     * Validation function
     *
     * @param element - element to validate content of
     */
    validate: function(element) {
        var self = this,
            exp = null,
            inputString = String(element.textContent).replace(self.config['decimal-delimiter'], "."),
            type = (self.config.item && self.config['auto-type']) ? self.vars.valueSettings.type : self.config.type;

        if ([shmi.c("TYPE_INT"), shmi.c("TYPE_FLOAT")].indexOf(type) !== -1) {
            exp = self.floatRegexp;
        } else {
            shmi.log("[RockerButton] invalid value type '" + self.config.type + "' configured", 3);
            element.textContent = self.imports.nv.formatOutput(self.value, self);
            return;
        }
        if (exp.test(inputString)) {
            shmi.log("[RockerButton] valid value", 1);
            var val = self.imports.nv.applyInputSettings(inputString, self);
            self.value = val;
            self.updateValue();
        } else {
            shmi.log("[RockerButton] invalid value", 1);
        }
        element.textContent = self.imports.nv.formatOutput(self.value, self);
    },
    /**
     * Writes current value to connected data source
     *
     */
    updateValue: function() {
        if (this.config.item) {
            shmi.visuals.session.ItemManager.writeValue(this.config.item, this.value);
        }
    },
    /**
     * Enables the Rocker Button
     *
     */
    onEnable: function() {
        var self = this,
            im = shmi.visuals.session.ItemManager;
        self.listeners.forEach(function(l) {
            l.enable();
        });

        if (self.config.item) {
            self._subscriptionTargetId = im.subscribeItem(self.config.item, self);
        }

        shmi.log("[RockerButton] enabled", 1);
    },
    /**
     * Disables the Rocker Button
     *
     */
    onDisable: function() {
        var self = this,
            im = shmi.visuals.session.ItemManager;

        self.listeners.forEach(function(l) {
            l.disable();
        });

        if (self.config.item) {
            im.unsubscribeItem(self.config.item, self._subscriptionTargetId);
        }

        shmi.log("[RockerButton] disabled", 1);
    },
    /**
     * Locks the Rocker Button
     *
     */
    onLock: function() {
        var self = this;

        self.currentValueElement.blur();

        self.listeners.forEach(function(l) {
            l.disable();
        });

        shmi.addClass(this.element, 'locked');

        shmi.log("[RockerButton] locked", 1);
    },
    /**
     * Unlocks the Rocker Button
     *
     */
    onUnlock: function() {
        var self = this;

        self.listeners.forEach(function(l) {
            l.enable();
        });

        shmi.removeClass(self.element, 'locked');

        shmi.log("[RockerButton] unlocked", 1);
    },
    /**
     * Sets the current value of the Rocker Button
     *
     * @param value - new value to set
     * @param type - type of value
     */
    onSetValue: function(value, type) {
        var self = this;
        self.value = value;
        self.currentValueElement.textContent = self.imports.nv.formatOutput(self.value, self);
    },
    /**
     * Retrieves current value of the Rocker Button
     *
     * @return value - current value
     */
    getValue: function() {
        return this.value;
    },
    /**
     * Sets properties of connected data-source item
     *
     * @param min - min value
     * @param max - max value
     * @param step - value stepping
     */
    onSetProperties: function(min, max, step) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.setProperties(self, arguments);
    },
    setLabel: function(labelText) {
        var self = this,
            labelElement = shmi.getUiElement("label", self.element);
        if (self.config['auto-label'] && labelElement) {
            self.vars = self.vars || {};
            self.vars.label = labelText;
            labelElement.textContent = shmi.localize(labelText);
        }
    },
    setUnitText: function(unitText) {
        var self = this,
            unitElement = shmi.getUiElement("unit", self.element);
        if (self.config['auto-unit-text'] && unitElement) {
            self.vars = self.vars || {};
            self.vars.unit = unitText;
            unitElement.textContent = shmi.localize(unitText);
        }
    },

    floatRegexp: /(^[+-]?[0-9]([.][0-9]*)?$|^[+-]?[1-9]+[0-9]*([.][0-9]*)?$)/,
    intRegexp: /^[-]?[0-9][0-9]*$/
};

shmi.extend(shmi.visuals.controls.RockerButton, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");

/**
 * Creates a new Screen control
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.Screen = function(element, config) {
    /* check for required packages */
    shmi.requires("visuals.controls.View");

    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'screen');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'initial-view', 0);
    shmi.def(this.config, 'views', []);
    shmi.def(this.config, 'no-views-content', '<h4> -- add views to this screen -- </h4>');
    shmi.def(this.config, 'delete-inactive-views', true);

    this.controls = [];
    this.viewElements = [];
    this.currentView = -1;
    this.listeners = [];
    this.subscriptionId = null;
    this.tokens = [];

    this.startup();
};

shmi.visuals.controls.Screen.prototype = {
    uiType: "screen",
    isContainer: true,
    events: ['navigate'],
    getClassName: function() {
        return "Screen";
    },
    onRegister: function(onDone, templateData) {
        const self = this;

        if (Array.isArray(self.config.views) && self.config.views.length) {
            if (typeof templateData === "string") {
                self.element.innerHTML = templateData;
            } else {
                console.warn("[" + self.getClassName() + "]", "Screen option 'views' is deprecated and should no longer be used. Use a single content template specified with the 'template' option instead.");
                shmi.multiLoad(self.config.views, (results) => {
                    if (self.isDeleted()) {
                        return;
                    }
                    const viewData = results.map((r) => {
                        if (!r.failed) {
                            return r.data + "\n";
                        }
                        return "";
                    }).join("");

                    self.onRegister(onDone, viewData);
                });
                return;
            }
        }

        const tmp_els = shmi.getUiElements('view', this.element);
        for (let i = 0; i < tmp_els.length; i++) {
            if (tmp_els[i].parentNode === this.element) {
                this.viewElements.push(tmp_els[i]);
            }
        }

        /* remove all child nodes */
        this.clear();

        if (!this.config['delete-inactive-views']) {
            this.viewElements.forEach((elem, idx) => {
                const vElem = elem.cloneNode(true);

                shmi.addClass(vElem, "hidden");
                self.element.appendChild(vElem);
                vElem.setAttribute("data-index", idx);
            });
            self.parseChildren(self.element, onDone);
        } else {
            onDone();
        }
    },
    onInit: function() {
        var self = this;
        if (self.viewElements.length > self.config['initial-view']) {
            self.setView(self.config['initial-view']);
        }
    },
    clear: function() {
        while (this.element.childNodes.length > 0) {
            this.element.removeChild(this.element.childNodes[0]);
        }
    },
    onSetValue: function(value) {
        if (shmi.isDesignerEditor()) {
            return;
        }
        var idx = parseInt(value);
        this.setView(idx);
    },
    onLock: function() {
        var self = this;

        if (self.activeView !== undefined) {
            self.activeView.controls.forEach(function(ctrl) {
                ctrl.lock();
            });
        }
        shmi.log("[Screen] onLock called", 2);
    },
    onUnlock: function() {
        var self = this;

        if (self.activeView !== undefined) {
            self.activeView.controls.forEach(function(ctrl) {
                ctrl.unlock();
            });
        }
        shmi.log("[Screen] onUnlock called", 2);
    },
    onEnable: function() {
        if (this.currentView !== -1) {
            this.activeView.enable();
        }
        var IM = shmi.visuals.session.ItemManager;
        if (this.config.item) {
            this._sID = IM.subscribeItem(this.config.item, this);
            console.log("screen subscribed " + this.config.item);
        }
    },
    onDisable: function() {
        var self = this,
            IM = shmi.visuals.session.ItemManager;

        if (this.config.item) {
            IM.unsubscribeItem(this.config.item, this._sID);
            console.log("screen unsubscribed item");
        }

        if (this.currentView !== -1) {
            this.activeView.disable();
        }

        self.tokens.forEach(function(t) {
            t.unlisten();
        });
    },
    setView: function(index) {
        var self = this;
        if (this.currentView === index) {
            shmi.log("[Screen] view " + index + " already active", 2);
        } else if (!this.viewElements[index]) {
            console.error("[" + this.getClassName() + "] invalid view index:", index);
        } else {
            var old_index = this.currentView;
            if (old_index !== -1 && this.activeView) {
                if (this.config['delete-inactive-views']) {
                    shmi.deleteControl(this.activeView, true);
                } else {
                    shmi.addClass(this.activeView.element, "hidden");
                    this.activeView.disable();
                }
            }

            var newelem;
            if (this.config['delete-inactive-views']) {
                newelem = this.viewElements[index].cloneNode(true);
                this.element.appendChild(newelem);
                const nodeId = newelem.getAttribute("_nodeid");
                let viewConfig = {};
                if (nodeId) {
                    viewConfig = shmi.visuals.model.getConfig(nodeId);
                }
                viewConfig.index = index;
                this.activeView = shmi.createControl('view', newelem, viewConfig, 'DIV', 'from');
            } else {
                this.activeView = this.controls[index];
                shmi.removeClass(this.activeView.element, "hidden");
            }

            if (this.active) {
                this.activeView.enable();
            }

            if (this.locked) {
                this.activeView.controls.forEach(function(ctrl) {
                    ctrl.lock();
                });
            }

            this.currentView = index;
            var IM = shmi.visuals.session.ItemManager;
            if (this.config.item) {
                if (IM.readValue(this.config.item) !== this.currentView) {
                    IM.writeValue(this.config.item, this.currentView);
                }
            }

            var e_detail = { index: index, old_index: old_index };
            if (this.activeView.isActive()) {
                this.fire('navigate', e_detail);
            } else {
                var tokActiveView = this.activeView.listen("enable", function() {
                    var idx = -1;
                    tokActiveView.unlisten();
                    idx = self.tokens.indexOf(tokActiveView);
                    if (idx !== -1) {
                        self.tokens.splice(idx, 1);
                    }
                    tokActiveView = null;
                    self.fire('navigate', e_detail);
                });
                self.tokens.push(tokActiveView);
            }
        }
    },
    onAddControl: function(options, callback) {
        var self = this,
            cm = shmi.requires("visuals.tools.controller"),
            controls = [],
            notView = false,
            initToken = null;

        if (!self.initialized) {
            throw new Error("Control not initialized yet.");
        }

        if (self.config["delete-inactive-views"]) {
            callback(new Error("Screen is configured to delete inactive views. Disable option 'delete-inactive-views' to programatically add child controls."), controls);
            return;
        }

        if (!Array.isArray(options)) {
            options = [options];
        }

        notView = options.some(function(option) {
            return (option.ui !== "view");
        });

        if (notView) {
            callback(new Error("All children must be of type 'view'"), controls);
            return;
        }

        options.forEach(function(option) {
            var control = null;

            option.config.index = self.controls.length;
            control = shmi.createControl(option.ui, self.element, option.config, "DIV", null, false);

            if (control !== null) {
                if (option.style && typeof option.style === "object") {
                    let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                    iter(option.style, (value, name) => {
                        control.element.style[name] = value;
                    });
                }
                shmi.addClass(control.element, "hidden");
                self.viewElements.push(control.element.cloneNode(true));
                controls.push(control);
            }
        });

        initToken = shmi.waitOnInit(controls, function() {
            var idx = -1,
                errors = [],
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken.unlisten();
                initToken = null;
            }

            options.forEach(function(opt, jdx) {
                var t = null,
                    control = controls[jdx];

                if (opt.controller) {
                    cm.create(opt.controller.name, opt.controller, control.getName());
                }

                if (Array.isArray(opt.children) && opt.children.length) {
                    t = tm.createTask("child control");
                    t.run = function() {
                        control.addControl(opt.children, function(childErr, childOptions) {
                            if (childErr) {
                                errors.push(childErr);
                            }
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            if (tasks.length) {
                tl = tm.createTaskList(tasks, false);
                tl.onComplete = function() {
                    if (self.controls.length && self.controls.length === controls.length) {
                        self.setView(self.config["initial-view"]);
                    }
                    callback(errors.length ? errors[0] : null, controls);
                };
                tl.run();
            } else {
                if (self.controls.length && self.controls.length === controls.length) {
                    self.setView(self.config["initial-view"]);
                }
                callback(errors.length ? errors[0] : null, controls);
            }
        });

        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    }
};

shmi.extend(shmi.visuals.controls.Screen, shmi.visuals.core.BaseControl);

/**
 * WebIQ visuals searchbar control.
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "searchbar",
 *     "name": null,
 *     "template": "default/searchbar",
 *     "item": null,
 *     "datagrid": null,
 *     "search-column": null,
 *     "show-results": "two-or-more",
 *     "result-columns": null
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * item {string}: Item alias of an item to connect. Can be set to `null` if no
 *  item is required.
 * datagrid {string}: Name of a DataGrid to search in.
 * search-column {string}: Name of the column to search in.
 * show-results {string}: When to display the results dropdown.
 *  * `never` Never show results.
 *  * `always` Always show results if there are more than 0.
 *  * `two-or-more` Show results if there are two or more.
 * result-columns {string[]}: Array of DataGrid column names to display in
 *  the results dropdown. `null` or an empty array will cause all columns to
 *  be included.
 *
 * @version 1.0
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = "searchbar", // control name in camel-case
        uiType = "searchbar", // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    var defConfig = {
        "class-name": "searchbar",
        "name": null,
        "template": "default/searchbar",
        "label": uiType,
        "item": null,
        "datagrid": null,
        "search-column": null,
        "show-results": "two-or-more",
        "result-columns": null,
        "select-contents-on-focus": true
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START

    function getMatchData(self) {
        var gData = self.vars.dataGridData || [],
            hintIdx = self.vars.currentHintIndex || 0;

        return gData[hintIdx] || null;
    }

    /**
     * Checks if all dom elements could be found.
     *
     * @param {object} dom `this.vars.dom`
     * @returns {boolean} `true` if the object did not contain any `null`
     *  values, `false` else.
     */
    function verifyDomElements(dom) {
        var iterObj = shmi.requires("visuals.tools.iterate.iterateObject"),
            ok = true;

        iterObj(dom, function(val) {
            ok = ok && val !== null;
        });

        return ok;
    }

    /**
     * Adds an event listener to the given target and stores data required to
     * remove it again in the given category.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} category Name of the category for which to register
     *  the event listener.
     * @param {EventTarget} target Target to add the event listener to.
     * @param {string} type Name of the event.
     * @param {function} listener
     */
    function addRegisteredEventListener(self, category, target, type, listener) {
        var listeners = self.vars.registeredEventListeners[category];

        // If the category doesn't exist, create it.
        if (!listeners) {
            self.vars.registeredEventListeners[category] = [];
            listeners = self.vars.registeredEventListeners[category];
        }

        target.addEventListener(type, listener);

        listeners.push({
            type: type,
            target: target,
            listener: listener
        });
    }

    /**
     * Removes all event listeners for a given category. If no category has
     * been given, event listeners for all categories are removed.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} [category] Name of the category for which to remove all
     *  event listeners.
     */
    function removeRegisteredEventListeners(self, category) {
        var iterObj = shmi.requires("visuals.tools.iterate.iterateObject"),
            listeners = [];

        if (category) {
            listeners = self.vars.registeredEventListeners[category] || [];
            delete self.vars.registeredEventListeners[category];
        } else {
            iterObj(self.vars.registeredEventListeners, function(l) {
                listeners = listeners.concat(l || []);
            });
            self.vars.registeredEventListeners = {};
        }

        listeners.forEach(function(listener) {
            listener.target.removeEventListener(listener.type, listener.listener);
        });
    }

    /**
     * Removes the displayed hint.
     *
     * @param {*} self Reference to a searchbar control.
     */
    function clearHint(self) {
        self.vars.dom.suggestion.textContent = "";
        self.vars.currentHint = null;
    }

    /**
     * Sets a hint.
     *
     * @param {*} self Reference to a searchbar control
     * @param {?string} hint The hint
     * @param {boolean} [force] Show the hint text even if the user input field
     *  is empty.
     */
    function setHint(self, hint, force) {
        var val;

        if (!hint) {
            clearHint(self);
        } else {
            val = self.getValue();

            if (val.length === 0 && !force) {
                clearHint(self);
            } else {
                self.vars.dom.suggestion.textContent = hint.substr(val.length);
                self.vars.currentHint = hint;
            }
        }
    }

    /**
     * Set a hint at the given index in the current data grid data buffer.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {number} idx Index of the hint to set.
     * @param {boolean} [force] Show the hint text even if the user input field
     *  is empty.
     */
    function setHintByIdx(self, idx, force) {
        if (self.vars.dataGridData.length === 0) {
            clearHint(self);

            return;
        }

        // Make sure the index is in [0, self.vars.dataGridData)
        while (idx < 0) {
            idx += self.vars.dataGridData.length;
        }
        idx = idx % self.vars.dataGridData.length;

        if (self.vars.currentHintIndex !== null) {
            shmi.removeClass(self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex], "hint-current");
        }
        shmi.addClass(self.vars.dom.resultBox.firstChild.children[idx], "hint-current");

        setHint(self, self.vars.dataGridData[idx][self.config["search-column"]], force);
        self.vars.currentHintIndex = idx;
    }

    /**
     * Update the displayed hint. If new hints need to be fetched, this is done
     * asynchronously and the hint is cleared in the meantime.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} [inputText] String to look (and display) up hints for.
     *  If no string is given, the current string of the input field is used
     *  instead.
     */
    function updateHint(self, inputText) {
        fetchHint(self, inputText || self.getValue());
    }

    /**
     * Callback called when a finished processing a new hint.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {?string} inputText String the looked up hint is meant for. If no
     *  string is given, the current string of the input field is used
     *  instead
     * @param {?string} hint The fetched hint. May be `null` if there is no
     *  hint, in which case `inputText` also isn't a valid input.
     */
    function onHint(self, inputText, hint) {
        inputText = inputText || self.getValue();

        if (!hint) {
            if (inputText.length > 0) {
                shmi.addClass(self.element, "notfound");
            }
            clearHint(self);
            self.vars.lastHintSearchResult = null;
        } else {
            shmi.removeClass(self.element, "notfound");
            setHint(self, hint);
            self.vars.lastHintSearchResult = hint;
        }
    }

    /**
     * Fetches a new hint from the connected datagrid. If the given string is
     * guaranteed to yield in the same hint being found, the last search hit is
     * being used instead of actually querying the datagrid.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {string} inputText String to look up a hint for.
     */
    function fetchHint(self, inputText) {
        var newValueIsMoreSpecific = self.vars.lastHintSearchToken && (inputText.indexOf(self.vars.lastHintSearchToken) === 0);
        var hintMatchesNewValue = self.vars.lastHintSearchResult && (self.vars.lastHintSearchResult.indexOf(inputText) === 0);

        // No need to update the hint
        if (newValueIsMoreSpecific && hintMatchesNewValue) {
            onHint(self, inputText, self.vars.lastHintSearchResult);
            updateResultBox(self, self.vars.dataGridData.filter(function(val) {
                return val[self.config["search-column"]] && val[self.config["search-column"]].indexOf(inputText) !== -1;
            }));

            return;
        }

        clearHint(self);
        self.vars.dataGrid.setFilter(self.vars.dataGridSearchColumnIdx, [ inputText + "%" ]);
        self.vars.lastHintSearchToken = inputText;
    }

    /**
     * Removes all nodes from the result box.
     *
     * @param {*} self Reference to a searchbar control.
     */
    function clearResultBox(self) {
        removeRegisteredEventListeners(self, "result-box");
        while (self.vars.dom.resultBox.firstChild) {
            self.vars.dom.resultBox.removeChild(self.vars.dom.resultBox.firstChild);
        }
    }

    /**
     * Creates a new result table and attaches it to the result box.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {object[]} data DataGrid data to display.
     */
    function updateResultBox(self, data) {
        var table = document.createElement("table"),
            resultBoxVisible = false,
            columns,
            mainColumnIdx;

        clearResultBox(self);

        if (self.config["show-results"] === "never") {
            // Control is set up to never show results.
            resultBoxVisible = false;
        } else if (self.config["show-results"] === "two-or-more" && data.length <= 1) {
            // Control is set up to only show the results if there are two or
            // more. We don't have enough so stop here.
            resultBoxVisible = false;
        } else if (data.length !== 0) {
            resultBoxVisible = true;
        }

        if (resultBoxVisible) {
            self.vars.dom.resultBox.style.display = "";
        } else {
            self.vars.dom.resultBox.style.display = "none";
            return; //nothing left to do here
        }

        columns = Object.keys(data[0]);
        if ((self.config["result-columns"] || []).length > 0) {
            columns = columns.filter(function(columnName) {
                return self.config["result-columns"].indexOf(columnName) !== -1;
            });
        }

        // See if there are columns left we can display. If not, stop here.
        if (columns.length === 0) {
            self.vars.dom.resultBox.innerHTML = "No columns to display. Please check <span style=\"font-weight: bold\">result-columns</span> in your control configuration.";
            fLog("No columns to display.", "dataGrid columns =", Object.keys(data[0]), "columns to display =", self.config["result-columns"]);
            return;
        }

        // Make sure to `search-column` is at position 0 in the columns array
        // (= it is displayed first).
        mainColumnIdx = columns.indexOf(self.config["search-column"]);
        if (mainColumnIdx > 0) {
            columns.unshift(columns[mainColumnIdx]);
            columns.splice(mainColumnIdx + 1, 1);
        }

        // Fill the table.
        data.forEach(function(row, idx) {
            var tr = document.createElement("tr");
            columns.forEach(function(columnName) {
                var cell = document.createElement("td");
                cell.textContent = row[columnName] || "";

                tr.appendChild(cell);
            });

            addRegisteredEventListener(self, "result-box", tr, "touchstart", (evt) => {
                evt.stopPropagation();
            });
            addRegisteredEventListener(self, "result-box", tr, "mousedown", function(ev) {
                self.setValue(row[self.config["search-column"]]);
            });

            addRegisteredEventListener(self, "result-box", tr, "mouseover", setHintByIdx.bind(null, self, idx, true));
            addRegisteredEventListener(self, "result-box", tr, "mouseleave", shmi.removeClass.bind(shmi, tr, "hint-current"));

            table.appendChild(tr);
        });

        self.vars.dom.resultBox.appendChild(table);
    }

    /**
     * Scrolls the result box to the given element. Will not scroll, if the
     * element is already visible. After a scroll, the element will be top
     * aligned if the result box was scrolled up and down aligned if the result
     * box was scrolled down.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {DOMElement} element Element to scroll the result box to.
     */
    function doResultBoxScroll(self, element) {
        var scrollTop = self.vars.dom.resultBox.scrollTop,
            scrollBottom = scrollTop + self.vars.dom.resultBox.offsetHeight;

        if (element.offsetTop < scrollTop) {
            element.scrollIntoView(true);
        } else if (element.offsetTop + element.offsetHeight > scrollBottom) {
            element.scrollIntoView(false);
        }
    }

    /**
     * Returns a datagrids data in a more convenient format.
     *
     * @param {DataGrid} dg Data grid to get data from.
     * @param {number} subID Subscription id.
     * @returns {object[]} Array of rows where each row is key-, value store.
     */
    function getDataGridDataHelper(dg, subID) {
        var fieldNames = dg.getFields();

        return dg.getCurrentIDs(subID).map(function(id) {
            var row = dg.getRowData(id),
                rowdata = {};

            if (!row) {
                return null;
            }

            row.forEach(function(cell, cellIdx) {
                rowdata[fieldNames[cellIdx]] = String(cell.value);
            });

            return rowdata;
        });
    }

    /**
     * Subscriber function attached to the searchbars datagrid.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {object} dgData DataGrid update notification data.
     */
    function hintFetchCompleteCallback(self, dgData) {
        var inputText = self.getValue(),
            rowData = getDataGridDataHelper(self.vars.dataGrid, self.vars.dataGridSubscriptionId.id),
            hint;

        // No hint found?
        if (rowData.length > 0) {
            hint = (rowData[0] || {})[self.config["search-column"]];
        }

        self.vars.currentHintIndex = null;
        self.vars.dataGridData = rowData;
        onHint(self, inputText, hint || null);
        updateResultBox(self, rowData);
        self.fire("change", { value: self.getValue() });
    }

    /**
     * Set the current selection to the end of the given elements content.
     *
     * @param {DOMElement} element
     * @param {boolean} selectContent Selects the entire content instead of
     *  just the end.
     */
    function setSelected(element, selectContent) {
        // Setting textContent reset our cursor position. Set the
        // cursor to the end of the content.
        var range = document.createRange();
        range.selectNodeContents(element);
        if (!selectContent) {
            range.setStart(element, range.endOffset);
        }

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }

    /**
     * Event listener callback attached to the user input field.
     *
     * @param {*} self Reference to a searchbar control.
     * @param {KeyboardEvent} ev
     */
    function userInputKeyDownHandler(self, ev) {
        if (ev.key === "Tab") {
            // On "Tab" use autocompletion if appropriate. Prevent the event
            // from propagating if autocompletion was used but don't otherwise.
            if (self.vars.currentHint && self.getValue() !== self.vars.currentHint) {
                ev.preventDefault();

                self.setValue(self.vars.currentHint);
                clearHint(self);

                // Setting textContent reset our cursor position. Set the
                // cursor to the end of the content.
                setSelected(self.vars.dom.input);
            }
        } else if (ev.key === "Escape") {
            // On "Escape" toggle the result box.
            if (shmi.hasClass(self.element, "show-results")) {
                shmi.removeClass(self.element, "show-results");
            } else {
                shmi.addClass(self.element, "show-results");
            }
        } else if (ev.key === "ArrowUp") {
            // On "ArrowUp" scroll up by 1.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, (self.vars.currentHintIndex || 0) - 1, true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.key === "PageUp") {
            // On "PageUp" scroll up by 10.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, Math.max((self.vars.currentHintIndex || 0) - 10, 0), true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.key === "ArrowDown") {
            // On "ArrowDown" scroll down by 1.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, (self.vars.currentHintIndex || 0) + 1, true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.key === "PageDown") {
            // On "PageDown" scroll down by 10.
            if (self.vars.dom.resultBox.firstChild && self.vars.dom.resultBox.firstChild.children) {
                setHintByIdx(self, Math.min((self.vars.currentHintIndex || 0) + 10, self.vars.dataGridData.length - 1), true);
                doResultBoxScroll(self, self.vars.dom.resultBox.firstChild.children[self.vars.currentHintIndex]);
                ev.preventDefault();
            }
        } else if (ev.keyCode === 13 || ev.keyCode === 9) {
            // On "Return" or "Enter" remove focus and autocomplete.
            ev.preventDefault();
            if (self.vars.currentHint) {
                self.setValue(self.vars.currentHint);
            }

            ev.target.blur();
        }
    }

    /**
     * Attaches event listeners to the user input field.
     *
     * @param {*} self Reference to a searchbar control.
     */
    function setupDomUserInput(self) {
        // Update the hint whenever the content of the user input field
        // changes.
        addRegisteredEventListener(self, "input", self.vars.dom.input, "input", function onInput() {
            var inputText = self.getValue();

            self.vars.dom.mirror.textContent = inputText;

            updateHint(self, inputText);
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "keydown", userInputKeyDownHandler.bind(null, self));

        addRegisteredEventListener(self, "input", self.vars.dom.input, "focus", function onFocus() {
            self.fire("enter", { value: self.getValue() });
            self.vars.userInputActive = true;
            shmi.addClass(self.element, "focused");
            shmi.addClass(self.element, "show-results");
            updateHint(self);

            if (!self.vars.focusFromKeyboard) {
                self.showKeyboard();
            }
            self.vars.focusFromKeyboard = false;

            if (self.config["select-contents-on-focus"]) {
                setSelected(self.vars.dom.input, true);
            }
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "blur", function onBlur() {
            self.vars.userInputActive = false;
            shmi.removeClass(self.element, "focused");
            window.getSelection().removeAllRanges();
            clearHint(self);

            if (self.vars.itemSubscription && self.config.item) {
                self.imports.im.writeValue(self.config.item, self.getValue());
            }

            self.fire("change", { value: self.getValue() });
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "touchstart", function onDown(evt) {
            self.showKeyboard();
            evt.stopPropagation();
        });

        addRegisteredEventListener(self, "input", self.vars.dom.input, "click", function onClick() {
            self.showKeyboard();
            shmi.addClass(self.element, "show-results");
        });
    }

    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            dom: {
                input: null,
                mirror: null,
                suggestion: null,
                resultBox: null,
                clear: null
            },
            initDone: false,
            registeredEventListeners: {},
            dataGrid: null,
            dataGridSubscriptionId: null,
            dataGridSearchColumnIdx: -1,
            dataGridData: [],
            currentHint: null,
            currentValue: null,
            lastHintSearchToken: null,
            lastHintSearchResult: null,
            focusFromKeyboard: false,
            userInputActive: false,
            currentHintIndex: null,
            itemSubscription: null,
            listeners: []
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            dgm: "visuals.session.DataGridManager"
        },

        /* array of custom event types fired by this control */
        events: [
            "change", "enter"
        ],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this,
                    io = shmi.requires("visuals.io");

                this.vars.dom.input = shmi.getUiElement("searchbar-user-input", this.element);
                this.vars.dom.mirror = shmi.getUiElement("searchbar-mirror", this.element);
                this.vars.dom.suggestion = shmi.getUiElement("searchbar-suggestion", this.element);
                this.vars.dom.resultBox = shmi.getUiElement("searchbar-result-box", this.element);
                this.vars.dom.clear = shmi.getUiElement("clear-input", this.element);
                this.vars.dataGrid = this.imports.dgm.getGrid(this.config.datagrid);

                if (!verifyDomElements(this.vars.dom)) {
                    this.element.textContent = "One or more required DOM elements are missing from the template. This control will not work. Please select a compatible template.";
                    fLog("Some DOM elements are missing from the template.");
                    return;
                }

                var domLabel = shmi.getUiElement("searchbar-label", this.element);
                if (domLabel) {
                    if (this.config.label && String(this.config.label).length > 0) {
                        domLabel.textContent = shmi.localize(this.config.label);
                    } else {
                        domLabel.style.display = "none";
                    }
                }

                var clearHandler = {
                    onClick: function() {
                        if (self.isActive() && !self.locked) {
                            self.setValue("");
                        }
                    }
                };
                self.vars.listeners.push(new io.MouseListener(self.vars.dom.clear, clearHandler));
                self.vars.listeners.push(new io.TouchListener(self.vars.dom.clear, clearHandler));

                if (this.vars.dataGrid === null) {
                    this.element.textContent = "Unable to find datagrid or no datagrid configured. This control will not work. Please select a valid datagrid.";
                    if (this.config.datagrid) {
                        fLog("Datagrid not found", this.config.datagrid);
                    } else {
                        fLog("No datagrid configured");
                    }

                    return;
                }

                this.vars.initDone = true;

                this.vars.dataGridSearchColumnIdx = this.vars.dataGrid.getFields().indexOf(this.config["search-column"]);
                if (this.vars.dataGridSearchColumnIdx === -1) {
                    this.element.textContent = "Datagrid does not have the configured field to search in. This control will not work. Please select a valid search field.";
                    fLog("Datagrid does not have the configured search field.");
                }

                this.vars.dataGrid.sort(this.vars.dataGridSearchColumnIdx, "ASC");
            },
            /**
             * Returns the contents of the user input field. Since some
             * browsers add non-breaking-spaces, those spaces are converted to
             * normal ones.
             *
             * @returns {string} Value of the input field.
             */
            getValue: function() {
                if (!this.vars.initDone) {
                    return null;
                }

                var value = this.vars.dom.input.textContent;

                // Replace non breaking spaces with normal ones
                value = value.replace(String.fromCharCode(0xA0), " ");

                return value;
            },
            getMatchData: function() {
                return getMatchData(this);
            },
            /**
             * Shows the overlay keyboard, if enabled via appConfig
             *
             */
            showKeyboard: function() {
                const self = this,
                    appConfig = shmi.requires("visuals.session.config"),
                    keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

                if (!keyboardEnabled) {
                    return;
                }

                shmi.keyboard(
                    {
                        "value": self.getValue(),
                        "select-box-enabled": appConfig.keyboard["language-selection"],
                        "password-input": false,
                        "show-enter": false,
                        "callback": function(success, input) {
                            if (success) {
                                if (self.config.item) {
                                    self.imports.im.writeValue(self.config.item, input);
                                } else {
                                    self.setValue(input);
                                }
                                self.vars.focusFromKeyboard = true;
                                self.vars.dom.input.focus();
                            }
                        }
                    });
            },
            /**
             * Sets the value of the user input field.
             *
             * @param {string} val Value to set
             */
            setValue: function(val) {
                if (!this.vars.initDone) {
                    return;
                }

                this.vars.dom.input.textContent = val;
                this.vars.dom.mirror.textContent = val;
                updateHint(this, val);
            },
            /* called when control is enabled */
            onEnable: function() {
                if (!this.vars.initDone) {
                    return;
                }

                setupDomUserInput(this);
                this.vars.dom.input.setAttribute("contenteditable", "true");

                this.vars.dataGridSubscriptionId = this.vars.dataGrid.subscribePage(0, 100, hintFetchCompleteCallback.bind(null, this));
                if (this.config.item) {
                    this.vars.itemSubscription = this.vars.im.subscribeItem(this.config.item, this);
                }
                this.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                if (!this.vars.initDone) {
                    return;
                }

                this.vars.dataGrid.unsubscribe(this.vars.dataGridSubscriptionId.id);
                this.vars.dataGridSubscriptionId = null;

                if (this.vars.itemSubscription) {
                    this.vars.itemSubscription.unlisten();
                    this.vars.itemSubscription = null;
                }

                clearResultBox(this);
                this.vars.dom.input.setAttribute("contenteditable", "false");
                removeRegisteredEventListeners(this);
                this.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                if (this.vars.initDone) {
                    this.vars.dom.input.setAttribute("contenteditable", "false");
                }
                this.vars.listeners.forEach(function(l) {
                    l.disable();
                });
                shmi.addClass(this.element, "locked");
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                if (this.vars.initDone) {
                    this.vars.dom.input.setAttribute("contenteditable", "true");
                }
                this.vars.listeners.forEach(function(l) {
                    l.enable();
                });
                shmi.removeClass(this.element, "locked");
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                if (this.vars.initDone && !this.vars.userInputActive) {
                    this.setValue(value);
                }
            },
            /* Sets min & max values and stepping of subscribed variable */
            onSetProperties: function(min, max, step) {

            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

(function() {
    function setIcon(element, option, isSelected) {
        var img = null,
            hasIcon = false;
        if (option["icon-src"]) {
            if (element.tagName !== "IMG") {
                img = document.createElement("IMG");
                element.parentNode.insertBefore(img, element);
                element.parentNode.removeChild(element);
            } else {
                img = element;
            }
            img.className = "icon option-icon";
            img.setAttribute("src", option["icon-src"]);
            hasIcon = true;
        } else if (option["icon-class"]) {
            if (element.tagName !== "DIV") {
                img = document.createElement("DIV");
                element.parentNode.insertBefore(img, element);
                element.parentNode.removeChild(element);
            } else {
                img = element;
            }
            img.className = "icon option-icon";
            shmi.addClass(img, option["icon-class"]);
            hasIcon = true;
        } else {
            element.className = "icon option-icon";
            hasIcon = false;
        }

        if (hasIcon) {
            img.setAttribute("data-ui", "option-icon");
        } else {
            element.setAttribute("data-ui", "option-icon");
        }

        return hasIcon;
    }

    /**
     * Creates mouse listener functions to be attached to each option element.
     *
     * @param {SelectBox} self Reference to a SelectBox widget.
     */
    function makeOptionMouseHandler(self) {
        return {
            onClick: function onClick(x, y, event) {
                shmi.log("[SelectBox] click on element", 0);
                var oldValue = self.value;
                var idx = parseInt(event.currentTarget.getAttribute("data-value"));

                self.value = self.config.options[idx].value;
                self.setSelected(event.currentTarget);
                self.updateValue();
                shmi.addClass(self.containerElement, 'hidden');
                self.open = false;
                self.highlightIndex = -1;
                if (self.value !== oldValue) {
                    self.fire("change", { value: self.value });
                }
            },

            onPress: function onPress(x, y, event) {
                shmi.log("onPress", 0);
                event.preventDefault();
            },

            onRelease: function onRelease() {
                shmi.log("onRelease", 0);
            }
        };
    }

    /**
     * Creates touch listener functions to be attached to each option element.
     *
     * @param {SelectBox} self Reference to a SelectBox widget.
     */
    function makeOptionTouchHandler(self) {
        return {
            onPress: function onPress() {
            },
            onClick: function onClick(x, y, event) {
                shmi.log("[SelectBox] click on element", 0);
                var oldValue = self.value;
                var idx = parseInt(event.currentTarget.getAttribute("data-value"));

                self.value = self.config.options[idx].value;
                self.setSelected(event.currentTarget);
                self.updateValue();
                shmi.addClass(self.containerElement, 'hidden');
                self.open = false;
                self.highlightIndex = -1;
                if (self.value !== oldValue) {
                    self.fire("change", { value: self.value });
                }
            }
        };
    }

    /**
     * (Re-)Creates the widgets options elements.
     *
     * @param {SelectBox} self Reference to a SelectBox widget.
     * @param {boolean} enableListeners Whether or not to enable listeners
     *  attached to the option elements.
     */
    function rebuildOptions(self, enableListeners) {
        // Remove listeners
        self.optionInputListeners.forEach(function(listener) {
            listener.disable();
        });
        self.optionInputListeners = [];
        self.optionElements = []; //reset option-elements in case of call to 'setOptions(..)'

        // Clear old option elements
        while (self.anchorElement.firstChild) {
            self.anchorElement.removeChild(self.anchorElement.firstChild);
        }

        self.config.options.forEach(function(option, index) {
            var label = shmi.localize(option.label),
                opt = self.optionElement.cloneNode(true),
                val_ele = shmi.getUiElement('value', opt),
                iconEl = shmi.getUiElement("icon", opt),
                hasIcon = false;

            if (val_ele) {
                val_ele.textContent = label;
            } else {
                opt.textContent = label;
            }

            if (option["tooltip"] || option["icon-title"]) {
                opt.setAttribute("title", shmi.localize(option.tooltip || option["icon-title"]));
            }

            if (iconEl) {
                hasIcon = setIcon(iconEl, option, false);
                if (hasIcon && label) {
                    shmi.addClass(opt, "option-icon-and-text");
                } else if (hasIcon) {
                    shmi.addClass(opt, "option-icon-only");
                }
            }
            opt.setAttribute("data-value", index);
            self.anchorElement.appendChild(opt);
            self.optionElements.push(opt);
        });

        var mouseFuncs = makeOptionMouseHandler(self);
        var touchFuncs = makeOptionTouchHandler(self);

        self.optionElements.forEach(function(elem) {
            self.optionInputListeners.push(new shmi.visuals.io.TouchListener(elem, touchFuncs));
            self.optionInputListeners.push(new shmi.visuals.io.MouseListener(elem, mouseFuncs));
        });

        if (enableListeners) {
            self.optionInputListeners.forEach(function(listener) {
                listener.enable();
            });
        }
    }

    shmi.pkg("visuals.controls");
    /**
     * Creates a new Select Box control
     *
     * @constructor
     * @extends shmi.visuals.core.BaseControl
     * @param {HTMLElement} element root element of the control
     * @param {Object} config control configuration
     */
    shmi.visuals.controls.SelectBox = function(element, config) {
        this.element = element;
        this.config = config || {};

        this.parseAttributes();

        shmi.def(this.config, 'class-name', 'select-box');
        shmi.def(this.config, 'template', 'default/select-box');
        shmi.def(this.config, 'name', null);
        shmi.def(this.config, 'no-selection-label', '---');
        shmi.def(this.config, 'selected', -1);
        shmi.def(this.config, 'options', []);
        shmi.def(this.config, 'icon-src', null);
        shmi.def(this.config, 'icon-title', null);
        shmi.def(this.config, 'icon-class', null);
        shmi.def(this.config, 'label', 'select-box');
        shmi.def(this.config, 'auto-label', true);
        shmi.def(this.config, 'show-icon', false);
        shmi.def(this.config, 'show-text', true);

        this.active = false;
        this.open = false;
        this.value = null;
        this.highlightIndex = -1;
        this.inputListeners = [];
        this.optionInputListeners = [];
        this.optionElements = [];
        this.labelElement = null;
        this.iconElement = null;

        this.startup();
    };

    shmi.visuals.controls.SelectBox.prototype = {
        uiType: "select-box",
        events: ["change"],
        tooltipProperties: ["icon-title"],
        getClassName: function() {
            return "SelectBox";
        },
        onTemplate: function(responseText, failed, onDone) {
            if (!failed) {
                if (!this.element) {
                    shmi.log('[SelectBox] no base element provided', 3);
                    return;
                }
                var temp = document.createElement('DIV');
                temp.innerHTML = responseText;
                this.templateElement = document.createDocumentFragment();
                while (temp.firstChild) {
                    this.templateElement.appendChild(temp.firstChild);
                }
                onDone();
            } else {
                shmi.log("[SelectBox] Template not found - Error 404", 3);
                onDone();
            }
        },
        /**
         * Initializes the control
         *
         * @returns {unresolved}
         */
        onInit: function() {
            var self = this;
            /* clear base element */
            this.element.innerHTML = '';

            this.labelElement = shmi.getUiElement('label', this.templateElement).cloneNode(true);

            var iconTemplateElement = shmi.getUiElement('icon', this.templateElement);
            if (iconTemplateElement) {
                self.iconElement = iconTemplateElement.cloneNode(true);
                if (self.config["icon-src"]) {
                    shmi.addClass(iconTemplateElement, 'hidden');
                    shmi.removeClass(self.iconElement, 'hidden');
                    if (self.iconElement.tagName !== "IMG") {
                        self.iconElement = document.createElement("IMG");
                        self.iconElement.setAttribute("data-ui", "icon");
                        shmi.addClass(self.iconElement, "control-icon icon");
                    }
                    self.iconElement.setAttribute("src", self.config["icon-src"]);
                } else if (self.config["icon-class"]) {
                    shmi.addClass(self.iconElement, self.config["icon-class"]);
                    shmi.removeClass(self.iconElement, 'hidden');
                    shmi.addClass(iconTemplateElement, 'hidden');
                }
            }

            if (self.config["show-icon"] && self.config["show-text"]) {
                shmi.addClass(self.element, "icon-and-text");
            } else if (self.config["show-icon"]) {
                shmi.addClass(self.element, "icon-only");
            }

            this.containerElement = shmi.getUiElement('options', this.templateElement).cloneNode(true);
            if (!this.containerElement) {
                shmi.log('[SelectBox] no options element provided', 3);
                return;
            }

            this.anchorElement = shmi.getUiElement('anchor', this.containerElement);
            if (!this.anchorElement) {
                shmi.log('[SelectBox] no options anchor element provided', 3);
                return;
            }

            this.optionElement = shmi.getUiElement('option', this.templateElement);
            if (!this.optionElement) {
                shmi.log('[SelectBox] no option elements provided', 3);
                return;
            }
            this.selectedTempl = shmi.getUiElement('selected', this.templateElement).cloneNode(true);
            this.selectedValField = shmi.getUiElement('value', this.selectedTempl);
            if ((!this.selectedTempl) || (!this.selectedValField)) {
                shmi.log('[SelectBox] no selected element provided', 3);
                return;
            }

            var laElement = document.createElement("DIV");
            shmi.addClass(laElement, "label-area");
            this.element.appendChild(laElement);
            if (this.iconElement) {
                laElement.appendChild(this.iconElement);
            }

            if (this.labelElement) {
                this.labelElement.textContent = shmi.localize(this.config.label);
                laElement.appendChild(this.labelElement);
            }
            this.element.appendChild(this.containerElement);
            this.element.appendChild(this.selectedTempl);

            rebuildOptions(this);

            this.selected = this.config.selected;

            this.containerElement.setAttribute('tabindex', '-1');
            this.blurHandler = function() {
                if (self.clickedInside) {
                    setTimeout(function() {
                        self.containerElement.focus();
                    }, shmi.c("DECOUPLE_TIMEOUT"));
                } else {
                    shmi.addClass(this.containerElement, 'hidden');
                    setTimeout(function() {
                        this.open = false;
                    }.bind(this), shmi.c("DECOUPLE_TIMEOUT"));
                    this.highlightIndex = -1;
                }
            }.bind(this);
            this.containerElement.addEventListener('blur', this.blurHandler);

            var funcs2 = {};
            funcs2.onPress = function() {
            };
            funcs2.onClick = function(x, y, event) {
                if (event.target !== this.anchorElement) {
                    shmi.log("[SelectBox] click on container", 1);
                    if (this.open) {
                        this.containerElement.blur();
                    } else {
                        shmi.removeClass(this.containerElement, 'hidden');
                        this.open = true;
                        setTimeout(function() {
                            this.containerElement.focus();
                            shmi.visuals.session.FocusElement = this.containerElement;
                        }.bind(this), shmi.c("DECOUPLE_TIMEOUT"));
                    }
                }
            }.bind(this);

            var containerFuncs = {};
            containerFuncs.onPress = function(x, y, event) {
                if (event.currentTarget === self.containerElement) {
                    self.clickedInside = true;
                }
            };

            containerFuncs.onRelease = function(x, y, event) {
                if (event.currentTarget === self.containerElement) {
                    self.clickedInside = false;
                }
            };

            containerFuncs.onLeave = function() {
                shmi.log("[SelectBox] onLeave container-element", 0);
                this.containerElement.blur();
                this.highlightIndex = -1;
            }.bind(this);

            shmi.addClass(this.containerElement, 'hidden');
            if (this.selected === -1) {
                this.setSelected(null);
            } else {
                this.setSelected(this.optionElements[this.config.selected]);
            }

            this.inputListeners.push(new shmi.visuals.io.MouseListener(this.containerElement, containerFuncs));
            this.inputListeners.push(new shmi.visuals.io.TouchListener(this.element, funcs2));
            this.inputListeners.push(new shmi.visuals.io.MouseListener(this.element, funcs2));

            /* add keyboard listener to selectbox to be opened on Enter*/
            this.onPressEnter = function(event) {
                var key = event.key ? event.key : event.code,
                    focusedNotLocked = (this.element === document.activeElement) && !this.locked;
                if (focusedNotLocked && (key === 'Enter' || key === 'NumpadEnter')) {
                    shmi.log("[SelectBox] click on container", 1);
                    if (this.open) {
                        this.containerElement.blur();
                    } else {
                        shmi.removeClass(this.containerElement, 'hidden');
                        this.open = true;
                        setTimeout(function() {
                            self.containerElement.focus();
                            shmi.visuals.session.FocusElement = self.containerElement;
                        }, shmi.c("DECOUPLE_TIMEOUT"));
                    }
                }
            }.bind(this);
            this.element.addEventListener('keydown', this.onPressEnter);

            /* add keyboard listener */
            this.keydownHandler = function(event) {
                var key = event.key ? event.key : event.code;

                shmi.log("[SelectBox] keydown event: " + key, 1);
                if (this.highlightIndex < 0) {
                    this.highlightIndex = this.getCurrentIndex();
                }
                shmi.log("[SelectBox] hightlight index " + this.highlightIndex, 1);
                if (key === "Escape") {
                    this.optionElements.forEach(function(option) {
                        shmi.removeClass(option, 'highlighted');
                    });
                    shmi.addClass(this.containerElement, 'hidden');
                    this.open = false;
                    this.highlightIndex = -1;
                } else if (key === "Enter" || key === "NumpadEnter") { // Enter
                    if (this.highlightIndex !== -1) {
                        shmi.log("[SelectBox] click on element", 0);
                        var element = this.optionElements[this.highlightIndex],
                            oldValue = this.value,
                            idx = parseInt(element.getAttribute("data-value"));
                        this.value = this.config.options[idx].value;
                        this.setSelected(element);
                        this.updateValue();
                        shmi.addClass(this.containerElement, 'hidden');
                        this.open = false;
                        this.highlightIndex = -1;
                        if (this.value !== oldValue) {
                            this.fire("change", { value: this.value });
                        }
                        this.optionElements.forEach(function(option) {
                            shmi.removeClass(option, 'highlighted');
                        });
                        this.highlightIndex = -1;
                    }
                } else if (key === "ArrowUp") { // Up-Arrow
                    if (this.optionElements[this.highlightIndex]) {
                        shmi.removeClass(this.optionElements[this.highlightIndex], 'highlighted');
                    }
                    if (this.highlightIndex > 0) {
                        this.highlightIndex--;
                    } else {
                        this.highlightIndex = this.optionElements.length - 1;
                    }
                    if (this.optionElements[this.highlightIndex]) {
                        shmi.addClass(this.optionElements[this.highlightIndex], 'highlighted');
                    }
                } else if (key === "ArrowDown") { // Down-Arrow
                    if (this.optionElements[this.highlightIndex]) {
                        shmi.removeClass(this.optionElements[this.highlightIndex], 'highlighted');
                    }
                    if (this.highlightIndex < (this.optionElements.length - 1)) {
                        this.highlightIndex++;
                    } else {
                        this.highlightIndex = 0;
                    }
                    if (this.optionElements[this.highlightIndex]) {
                        shmi.addClass(this.optionElements[this.highlightIndex], 'highlighted');
                    }
                }
                return false;
            }.bind(this);
            this.containerElement.addEventListener('keydown', this.keydownHandler);
        },
        setOptions: function(options) {
            var self = this;

            self.config.options = options;
            rebuildOptions(self, self.active);
            self.setValue(self.getValue());
        },
        getOptions: function() {
            var self = this;
            return shmi.cloneObject(self.config.options);
        },
        /**
         * Sets the selected element
         *
         * @param element - element to select
         */
        setSelected: function(element) {
            /* return if no element specified */
            if (element === undefined) {
                return;
            }

            var selectedIdx = -1;
            var getText = function() {
                var ret = "";
                if (Array.isArray(this.config.options)) {
                    for (var i = 0; i < this.config.options.length; i++) {
                        if (this.config.options[i].value === this.value) {
                            ret = this.config.options[i].label;
                            selectedIdx = i;
                            break;
                        }
                    }
                }
                return ret;
            }.bind(this);

            for (var i = 0; i < this.optionElements.length; i++) {
                shmi.removeClass(this.optionElements[i], 'select-box-selected');
            }
            if (element === null) {
                this.selectedValField.textContent = shmi.localize(this.config['no-selection-label']);
                this.selectedTempl.setAttribute('data-value', -1);
                shmi.removeClass(this.selectedTempl, "option-icon-only option-icon-and-text");
            } else {
                shmi.addClass(element, 'select-box-selected');
                this.selectedValField.textContent = shmi.localize(getText());
                this.selectedTempl.setAttribute('data-value', selectedIdx);
                var selOpt = this.config.options[selectedIdx],
                    iconEl = shmi.getUiElement("option-icon", this.selectedTempl),
                    hasIcon = false;
                if (selOpt && iconEl) {
                    hasIcon = setIcon(iconEl, selOpt, true);
                    if (hasIcon && selOpt.label) {
                        shmi.addClass(this.selectedTempl, "option-icon-and-text");
                    } else if (hasIcon) {
                        shmi.addClass(this.selectedTempl, "option-icon-only");
                    } else {
                        shmi.removeClass(this.selectedTempl, "option-icon-only option-icon-and-text");
                    }
                } else if (iconEl) {
                    shmi.removeClass(this.selectedTempl, "option-icon-only option-icon-and-text");
                }
            }
            this.selected = selectedIdx;
        },
        getCurrentIndex: function() {
            return this.selected;
        },
        /**
         * Writes the current value to a connected data-source item
         *
         */
        updateValue: function() {
            if (this.config.item) {
                shmi.visuals.session.ItemManager.writeValue(this.config.item, this.value);
            }
        },
        /**
         * Enables the Select Box
         *
         */
        onEnable: function() {
            this.inputListeners.concat(this.optionInputListeners).
                forEach(function(listener) {
                    listener.enable();
                });

            this.element.setAttribute('tabindex', 0);

            if (this.config.item) {
                this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
            }
            shmi.log("[SelectBox] enabled", 1);
        },
        /**
         * Disables the Select Box
         *
         */
        onDisable: function() {
            if (this.config.item) {
                shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
            }

            this.inputListeners.concat(this.optionInputListeners).
                forEach(function(listener) {
                    listener.disable();
                });

            this.element.removeEventListener('keydown', this.onPressEnter);
            shmi.log("[SelectBox] disabled", 1);
        },
        onDelete: function() {
            if (this.containerElement) {
                this.containerElement.removeEventListener("blur", this.blurHandler);
                this.containerElement.removeEventListener("keydown", this.keydownHandler);
            }
        },
        /**
         * Locks the Select Box
         *
         */
        onLock: function() {
            this.locked = true;
            shmi.addClass(this.containerElement, 'hidden');
            this.highlightIndex = -1;

            this.inputListeners.concat(this.optionInputListeners).
                forEach(function(listener) {
                    listener.disable();
                });

            shmi.addClass(this.element, 'locked');
            shmi.log("[SelectBox] locked", 1);
        },
        /**
         * Unlocks the Select Box
         *
         */
        onUnlock: function() {
            this.locked = false;

            this.inputListeners.concat(this.optionInputListeners).
                forEach(function(listener) {
                    listener.enable();
                });

            shmi.removeClass(this.element, 'locked');
            shmi.log("[SelectBox] unlocked", 1);
        },
        /**
         * Sets the value of the Select Box
         *
         * @param value - new value to set
         */
        onSetValue: function(value) {
            shmi.log("[SelectBox] setting value...: " + value, 0);
            var val_set = false,
                oldValue = this.value;
            for (var i = 0; i < this.config.options.length; i++) {
                if (this.config.options[i].value === value) {
                    this.value = value;
                    this.setSelected(this.optionElements[i]);
                    this.selected = i;
                    val_set = true;
                    break;
                }
            }
            if (!val_set) {
                this.value = null;
                this.setSelected(null);
                this.selected = -1;
            }

            if (this.value !== oldValue) {
                this.fire("change", { value: this.value });
            }
            shmi.log("[SelectBox] value set: " + value, 1);
        },
        /**
         * Retrieves the current value
         *
         * @return value current value
         */
        getValue: function() {
            return this.value;
        },
        setLabel: function(labelText) {
            var self = this;

            if (self.config['auto-label'] && self.labelElement) {
                this.labelElement.textContent = shmi.localize(labelText);
            }
        }
    };

    shmi.extend(shmi.visuals.controls.SelectBox, shmi.visuals.core.BaseControl);
}());

shmi.pkg("visuals.controls");
/**
 * Creates a new Select Data control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.SelectDate = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'select-date');
    shmi.def(this.config, 'template', 'default/select-date');
    shmi.def(this.config, 'name', null);
    /* define default day / month texts in active locale */
    shmi.def(this.config, 'daynames', ['${V_SUN_SHORT}', '${V_MON_SHORT}', '${V_TUE_SHORT}', '${V_WED_SHORT}',
        '${V_THU_SHORT}', '${V_FRI_SHORT}', '${V_SAT_SHORT}']);
    shmi.def(this.config, 'daynames-long', ['${V_SUN}', '${V_MON}', '${V_TUE}', '${V_WED}', '${V_THU}',
        '${V_FRI}', '${V_SAT}']);
    shmi.def(this.config, 'monthnames', ['${V_JAN}', '${V_FEB}', '${V_MAR}', '${V_APR}', '${V_MAY}', '${V_JUN}',
        '${V_JUL}', '${V_AUG}', '${V_SEP}', '${V_OCT}', '${V_NOV}', '${V_DEC}']);

    /* active date format can be localized too */
    shmi.def(this.config, 'dateformat', "${V_DATEFORMAT}");
    shmi.def(this.config, 'label', 'select-date');
    shmi.def(this.config, 'auto-label', true);

    this.active = false;
    this.listeners = [];
    this.value = null;
    this.date = new Date();
    this.selectedDate = new Date();
    this.selectedDate.setHours(0, 0, 0, 0);

    this.startup();
};

shmi.visuals.controls.SelectDate.prototype = {
    uiType: "select-date",
    events: ["change"],
    getClassName: function() {
        return "SelectDate";
    },
    /**
     * Initializes the control
     *
     * @returns {unresolved}
     */
    onInit: function() {
        this.labelElement = shmi.getUiElement('label', this.element);
        if (this.labelElement) {
            this.labelElement.textContent = shmi.localize(this.config.label);
        }

        this.calendarElement = shmi.getUiElement('calendar', this.element);
        if (!this.calendarElement) {
            shmi.log('[SelectDate] no calendar element provided', 3);
            return;
        }
        this.daySelectElement = shmi.getUiElement('day-select', this.element);
        if (!this.daySelectElement) {
            shmi.log("[SelectDate] no day-select element provided", 3);
            return;
        }
        this.selectedElement = shmi.getUiElement('selected-date', this.element);
        if (!this.selectedElement) {
            shmi.log("[SelectDate] no selected-date element provided", 3);
            return;
        } else {
            this.selectedElement = shmi.getUiElement('value', this.selectedElement);
            if (!this.selectedElement) {
                shmi.log("[SelectDate] no selected-date element provided", 3);
                return;
            }
        }
        this.headerElement = shmi.getUiElement('day-header', this.element);
        if (!this.headerElement) {
            shmi.log('[SelectDate] no day-header element provided', 3);
            return;
        }
        this.dayNameElements = shmi.getUiElements('day-name', this.headerElement);
        if (!this.dayNameElements && (this.dayNameElements.length !== 7)) {
            shmi.log('[SelectDate] no day-name elements, or not the right count (7) provided', 3);
            return;
        }
        this.mSelect = shmi.getUiElement('month-select', this.element);
        if (!this.mSelect) {
            shmi.log("[SelectDate] no month-select element provided", 3);
            return;
        }
        this.ySelect = shmi.getUiElement('year-select', this.element);
        if (!this.ySelect) {
            shmi.log("[SelectDate] no year-select element provided", 3);
            return;
        }
        var mPrev = shmi.getUiElement('previous', this.mSelect);
        this.mCur = shmi.getUiElement('current', this.mSelect);
        var mNext = shmi.getUiElement('next', this.mSelect);

        var yPrev = shmi.getUiElement('previous', this.ySelect);
        this.yCur = shmi.getUiElement('current', this.ySelect);
        var yNext = shmi.getUiElement('next', this.ySelect);

        if (!(mPrev && this.mCur && mNext && yPrev && this.yCur && yNext)) {
            shmi.log('[SelectDate] not all month & year select elements provided', 3);
            return;
        }

        for (var i = 0; i < this.dayNameElements.length; i++) {
            var day_idx = (i === 6) ? 0 : i + 1;
            this.dayNameElements[i].textContent = shmi.localize(this.config.daynames[day_idx]);
        }

        this.calendarElement.setAttribute('tabindex', "0");

        var mPrevFuncs = {};
        mPrevFuncs.onPress = function() {
        };
        mPrevFuncs.onClick = function(x, y, event) {
            this.date.setFullYear(this.date.getYear() + 1900, this.date.getMonth() - 1);
            this.updateCalendar();
        }.bind(this);
        this.listeners.push(new shmi.visuals.io.MouseListener(mPrev, mPrevFuncs));
        this.listeners.push(new shmi.visuals.io.TouchListener(mPrev, mPrevFuncs));

        var mNextFuncs = {};
        mNextFuncs.onPress = function() {
        };
        mNextFuncs.onClick = function(x, y, event) {
            this.date.setFullYear(this.date.getYear() + 1900, this.date.getMonth() + 1);
            this.updateCalendar();
        }.bind(this);
        this.listeners.push(new shmi.visuals.io.MouseListener(mNext, mNextFuncs));
        this.listeners.push(new shmi.visuals.io.TouchListener(mNext, mNextFuncs));

        var yPrevFuncs = {};
        yPrevFuncs.onPress = function() {
        };
        yPrevFuncs.onClick = function(x, y, event) {
            this.date.setFullYear(this.date.getYear() + 1900 - 1, this.date.getMonth());
            this.updateCalendar();
        }.bind(this);
        this.listeners.push(new shmi.visuals.io.MouseListener(yPrev, yPrevFuncs));
        this.listeners.push(new shmi.visuals.io.TouchListener(yPrev, yPrevFuncs));

        var yNextFuncs = {};
        yNextFuncs.onPress = function() {
        };
        yNextFuncs.onClick = function(x, y, event) {
            this.date.setFullYear(this.date.getYear() + 1900 + 1, this.date.getMonth());
            this.updateCalendar();
        }.bind(this);
        this.listeners.push(new shmi.visuals.io.MouseListener(yNext, yNextFuncs));
        this.listeners.push(new shmi.visuals.io.TouchListener(yNext, yNextFuncs));

        this.dayElements = shmi.getUiElements('day', this.element);
        var dayFuncs = {};
        this.pressedElement = null;
        dayFuncs.onPress = function(x, y, event) {
            shmi.addClass(event.target, 'pressed');
            this.pressedElement = event.target;
        };
        dayFuncs.onRelease = function(x, y, event) {
            shmi.removeClass(this.pressedElement, 'pressed');
        };
        dayFuncs.onClick = function(x, y, event) {
            var day = event.target;
            if (shmi.hasClass(day, 'previous-month-day')) {
                this.date.setFullYear(this.date.getYear() + 1900, this.date.getMonth() - 1);
                this.selectedDate.setFullYear(this.date.getYear() + 1900, this.date.getMonth(), day.textContent);
            } else if (shmi.hasClass(day, 'current-month-day')) {
                this.date.setFullYear(this.date.getYear() + 1900, this.date.getMonth());
                this.selectedDate.setFullYear(this.date.getYear() + 1900, this.date.getMonth(), day.textContent);
            } else if (shmi.hasClass(day, 'next-month-day')) {
                this.date.setFullYear(this.date.getYear() + 1900, this.date.getMonth() + 1);
                this.selectedDate.setFullYear(this.date.getYear() + 1900, this.date.getMonth(), day.textContent);
            }
            this.updateCalendar();
            if (this.config.item) {
                shmi.visuals.session.ItemManager.writeValue(this.config.item, this.value);
            }
            shmi.addClass(this.calendarElement, 'hidden');
            this.open = false;
        }.bind(this);
        this.listeners.push(new shmi.visuals.io.MouseListener(this.daySelectElement, dayFuncs));
        this.listeners.push(new shmi.visuals.io.TouchListener(this.daySelectElement, dayFuncs));

        shmi.addClass(this.calendarElement, 'hidden');
        this.open = false;

        var selectedFuncs = {};
        selectedFuncs.onClick = function() {
            if (!this.open) {
                shmi.removeClass(this.calendarElement, 'hidden');
                this.open = true;
                setTimeout(function() {
                    this.calendarElement.focus();
                    shmi.visuals.session.FocusElement = this.calendarElement;
                }.bind(this), shmi.c("DECOUPLE_TIMEOUT"));
            } else {
                this.calendarElement.blur();
            }
        }.bind(this);
        this.listeners.push(new shmi.visuals.io.MouseListener(this.element, selectedFuncs));
        this.listeners.push(new shmi.visuals.io.TouchListener(this.element, selectedFuncs));

        this.blurHandler = function() {
            shmi.addClass(this.calendarElement, 'hidden');
            setTimeout(function() {
                this.open = false;
            }.bind(this), shmi.c("DECOUPLE_TIMEOUT"));
        }.bind(this);
        this.calendarElement.addEventListener("blur", this.blurHandler);

        var ceFuncs = {};
        ceFuncs.onPress = function() {
        };
        ceFuncs.onClick = function() {
        };
        ceFuncs.onRelease = function() {
        };
        this.listeners.push(new shmi.visuals.io.MouseListener(this.calendarElement, ceFuncs));
        this.listeners.push(new shmi.visuals.io.TouchListener(this.calendarElement, ceFuncs));

        this.updateCalendar();
    },
    /**
     * Updates the calendar element
     *
     */
    updateCalendar: function() {
        var self = this,
            dt = shmi.requires("visuals.tools.date"),
            oldValue = self.value,
            date = new Date(this.value * 1000);
        date.setFullYear(this.selectedDate.getYear() + 1900, this.selectedDate.getMonth(),
            this.selectedDate.getDate());
        this.value = date.getTime() / 1000;
        shmi.log("[SelectDate] selected time: " + this.value, 0);
        var curMonth = this.date.getMonth(),
            curYear = this.date.getYear() + 1900,
            firstDay = new Date(curYear, curMonth, 1).getDay(),
            start = 0;
        this.mCur.textContent = shmi.localize(this.config.monthnames[curMonth]);
        this.yCur.textContent = curYear;
        if (firstDay === 1) {
            start = 7;
        } else if (firstDay === 0) {
            start = 6;
        } else {
            start = firstDay - 1;
        }
        var prevDays = this.getDaysOfMonth(curMonth, curYear),
            curDays = this.getDaysOfMonth(curMonth + 1, curYear),
            days_tmp = shmi.getUiElements('day', this.element),
            d_tmp = prevDays - start + 1;
        for (var i = 0; i < start; i++) {
            days_tmp[i].className = 'previous-month-day';
            days_tmp[i].textContent = d_tmp;
            if (new Date(curYear, curMonth - 1, d_tmp).getTime() === this.selectedDate.getTime()) {
                shmi.addClass(days_tmp[i], 'selected');
            } else {
                shmi.removeClass(days_tmp[i], 'selected');
            }
            d_tmp++;
        }
        d_tmp = 1;
        for (i = start; i < curDays + start; i++) {
            days_tmp[i].className = 'current-month-day';
            days_tmp[i].textContent = d_tmp;
            if (new Date(curYear, curMonth, d_tmp).getTime() === this.selectedDate.getTime()) {
                shmi.addClass(days_tmp[i], 'selected');
            } else {
                shmi.removeClass(days_tmp[i], 'selected');
            }
            d_tmp++;
        }
        d_tmp = 1;
        for (i = curDays + start; i < days_tmp.length; i++) {
            days_tmp[i].className = 'next-month-day';
            days_tmp[i].textContent = d_tmp;
            if (new Date(curYear, curMonth + 1, d_tmp).getTime() === this.selectedDate.getTime()) {
                shmi.addClass(days_tmp[i], 'selected');
            } else {
                shmi.removeClass(days_tmp[i], 'selected');
            }
            d_tmp++;
        }

        var currentDate = shmi.localize(this.config.dateformat);
        if (typeof (currentDate) === 'string') {
            this.selectedElement.textContent = dt.formatDateTime(this.selectedDate, { datestring: currentDate });
        } else {
            this.selectedElement.innerHTML = '&nbsp;';
        }

        if (oldValue !== self.value) {
            self.fire("change", { value: self.value });
        }
    },
    /**
     * Returns number of days for month of year
     *
     * @param month - month of year
     * @param year - year
     * @return days days in month
     */
    getDaysOfMonth: function(month, year) {
        var d = new Date(year, month, 0);
        return d.getDate();
    },
    /**
     * Enables the Select Date control
     *
     */
    onEnable: function() {
        if (this.config.item) {
            this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
        }
        for (var i = 0; i < this.listeners.length; i++) {
            this.listeners[i].enable();
        }
        shmi.log("[SelectDate] enabled", 1);
    },
    /**
     * Disables Select Date control
     */
    onDisable: function() {
        if (this.config.item) {
            shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
        }
        for (var i = 0; i < this.listeners.length; i++) {
            this.listeners[i].disable();
        }
        shmi.log("[SelectDate] disabled", 1);
    },
    onDelete: function() {
        if (this.blurHandler) {
            this.calendarElement.removeEventListener("blur", this.blurHandler);
        }
    },
    /**
     * Locks Select Date control
     *
     */
    onLock: function() {
        shmi.addClass(this.calendarElement, 'hidden');
        this.open = false;
        for (var i = 0; i < this.listeners.length; i++) {
            this.listeners[i].disable();
        }
        shmi.addClass(this.element, 'locked');
        shmi.log("[SelectDate] locked", 1);
    },
    /**
     * Unlocks Select Date control
     *
     */
    onUnlock: function() {
        for (var i = 0; i < this.listeners.length; i++) {
            this.listeners[i].enable();
        }
        shmi.removeClass(this.element, 'locked');
        shmi.log("[SelectDate] unlocked", 1);
    },
    /**
     * Sets value of Select Date control
     *
     * @param value - new value to set
     */
    onSetValue: function(value) {
        value = parseInt(value);

        var self = this,
            date = new Date(value * 1000),
            oldValue = this.value;

        this.value = value;
        this.date.setFullYear(date.getYear() + 1900, date.getMonth());
        this.selectedDate.setFullYear(date.getYear() + 1900, date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds());
        this.updateCalendar();
        if (oldValue !== this.value) {
            self.fire("change", { value: this.value });
        }
    },
    /**
     * Retrieves current value of Select Date control
     *
     * @return value current value
     */
    getValue: function() {
        return this.value;
    },
    setLabel: function(labelText) {
        var self = this;
        if (self.config['auto-label'] && self.labelElement) {
            self.labelElement.textContent = shmi.localize(labelText);
        }
    }
};

shmi.extend(shmi.visuals.controls.SelectDate, shmi.visuals.core.BaseControl);

(function() {
    shmi.pkg("visuals.controls");

    /**
     * Creates listener functions to be attached to each option element via
     * both touch and mouse listeners.
     *
     * @param {SelectRadio} self Reference to a SelectRadio widget.
     */
    function makeOptionInputHandler(self) {
        return {
            onPress: function onPress(x, y, event) {
                shmi.addClass(event.currentTarget, 'pressed');
            },

            onRelease: function onRelease(x, y, event) {
                for (var i = 0; i < self.optionElements.length; i++) {
                    shmi.removeClass(self.optionElements[i], 'pressed');
                }
            },

            onClick: function onClick(x, y, event) {
                var elemIndex = self.optionElements.indexOf(event.currentTarget),
                    value = self.config.options[elemIndex] ? self.config.options[elemIndex].value : null,
                    changed = (value !== self.value);
                if (elemIndex < 0) {
                    console.error(self.uiType, "could not match clicked option");
                } else {
                    self.value = value;
                    self.setSelected(event.currentTarget);
                    self.updateValue();
                    if (changed) {
                        self.fire('change', { value: self.value });
                    }
                }
            }
        };
    }

    /**
     * (Re-)Creates the widgets options elements.
     *
     * @param {SelectRadio} self Reference to a SelectRadio widget.
     * @param {boolean} enableListeners Whether or not to enable listeners
     *  attached to the option elements.
     */
    function rebuildOptions(self, enableListeners) {
        // Remove listeners
        self.optionInputListeners.forEach(function(listeners) {
            listeners.disable();
        });
        self.optionInputListeners = [];
        // Remove elements
        self.optionRootElements.forEach(function(elem) {
            self.element.removeChild(elem);
        });
        self.optionRootElements = [];

        var mouseFuncs = makeOptionInputHandler(self);

        self.config.options.forEach(function(option, index) {
            var div = document.createElement('div');
            div.innerHTML = self.templateHtml;

            var optionEl = shmi.getUiElement('option', div);
            optionEl.setAttribute('data-value', option.value);

            var label = shmi.getUiElement('label', optionEl);
            label.innerHTML = shmi.localize(option.label);

            if (option['tooltip'] || option['icon-title']) {
                div.firstChild.setAttribute("title", shmi.localize(option['tooltip'] || option['icon-title']));
            }

            var icon = shmi.getUiElement('option-icon', optionEl);
            if (icon) {
                if (option['icon-src']) {
                    if (icon.tagName !== "IMG") {
                        var imgElem = document.createElement("IMG");
                        imgElem.className = icon.className;
                        icon.parentNode.insertBefore(imgElem, icon);
                        icon.parentNode.removeChild(icon);
                        icon = imgElem;
                    }
                    icon.setAttribute("src", option['icon-src']);
                    if (option.label) {
                        shmi.addClass(optionEl, "option-icon-and-text");
                    } else {
                        shmi.addClass(optionEl, "option-icon-only");
                    }
                } else if (option['icon-class']) {
                    if (icon.tagName !== "DIV") {
                        var iconDiv = document.createElement("DIV");
                        iconDiv.className = icon.className;
                        icon.parentNode.insertBefore(iconDiv, icon);
                        icon.parentNode.removeChild(icon);
                        icon = iconDiv;
                    }
                    shmi.addClass(icon, option['icon-class']);
                    if (option.label) {
                        shmi.addClass(optionEl, "option-icon-and-text");
                    } else {
                        shmi.addClass(optionEl, "option-icon-only");
                    }
                }
            }

            self.optionInputListeners.push(new shmi.visuals.io.MouseListener(optionEl, mouseFuncs));
            self.optionInputListeners.push(new shmi.visuals.io.TouchListener(optionEl, mouseFuncs));
            self.element.appendChild(div.firstChild);
            self.optionElements.push(optionEl);
            self.optionRootElements.push(div.firstChild);
        });

        if (enableListeners) {
            self.optionInputListeners.forEach(function(listener) {
                listener.enable();
            });
        }
    }

    /**
     * Creates a new Select Radio control
     *
     * @constructor
     * @extends shmi.visuals.core.BaseControl
     * @param {HTMLElement} element root element of the control
     * @param {Object} config control configuration
     */
    shmi.visuals.controls.SelectRadio = function(element, config) {
        this.element = element;
        this.config = config || {};

        this.parseAttributes();

        shmi.def(this.config, 'class-name', 'select-radio');
        shmi.def(this.config, 'template', 'default/select-radio');
        shmi.def(this.config, 'name', null);
        shmi.def(this.config, 'selected', -1);
        shmi.def(this.config, 'label', 'select-radio');
        shmi.def(this.config, 'options', []);
        shmi.def(this.config, 'auto-label', true);
        shmi.def(this.config, 'icon-src', null);
        shmi.def(this.config, 'icon-title', null);
        shmi.def(this.config, 'icon-class', null);
        shmi.def(this.config, 'show-icon', false);
        shmi.def(this.config, 'show-text', true);

        this.active = false;
        this.value = -1;
        this.selectedIndex = 0;
        this.selectedElement = null;
        this.highlightIndex = -1;
        this.templateHtml = null;
        this.optionElements = [];
        this.inputListeners = [];
        this.optionInputListeners = [];
        this.optionRootElements = [];

        this.startup();
    };

    shmi.visuals.controls.SelectRadio.prototype = {
        uiType: "select-radio",
        events: ["change"],
        tooltipProperties: ["icon-title"],
        getClassName: function() {
            return "SelectRadio";
        },
        /**
         * Injects template content into template variable
         *
         * @param {type} responseText content of loaded template
         * @param {type} failed true if loading failed
         * @returns {unresolved}
         */
        onTemplate: function(responseText, failed, onDone) {
            if (!failed) {
                if (!this.element) {
                    shmi.log('[SelectRadio] no base element provided', 3);
                    return;
                }
                this.templateHtml = responseText;
            }

            onDone();
        },
        /**
         * Initializes the control
         *
         * @returns {unresolved}
         */
        onInit: function() {
            var self = this;
            if (!self.config['options']) {
                shmi.log("[SelectRadio] no options defined in config", 1);
                return;
            } else {
                shmi.log("[SelectRadio] options newly defined", 1);
            }

            /* clear content for re-initialization */
            self.element.innerHTML = '';

            if (self.config['show-text'] && self.config['show-icon']) {
                shmi.addClass(self.element, "icon-and-text");
            } else if (self.config['show-icon']) {
                shmi.addClass(self.element, "icon-only");
            }

            /* create label element */
            var lbl = document.createElement('div'),
                laElem = document.createElement("DIV");
            shmi.addClass(laElem, "label-area");
            self.element.appendChild(laElem);
            lbl.setAttribute('data-ui', 'label');
            lbl.setAttribute('class', 'label');
            if (self.config.label !== undefined) {
                lbl.textContent = shmi.localize(self.config.label);
            }

            var iconElement = null;
            if (self.config["icon-src"]) {
                iconElement = document.createElement("IMG");
                iconElement.setAttribute("data-ui", "option-icon");
                shmi.addClass(iconElement, "control-icon icon");
                iconElement.setAttribute("src", self.config["icon-src"]);
                laElem.appendChild(iconElement);
            } else if (self.config["icon-class"]) {
                iconElement = document.createElement("DIV");
                iconElement.setAttribute("data-ui", "option-icon");
                shmi.addClass(iconElement, "control-icon icon");
                shmi.addClass(iconElement, self.config["icon-class"]);
                laElem.appendChild(iconElement);
            }
            laElem.appendChild(lbl);

            rebuildOptions(self);
        },
        setOptions: function(options) {
            var self = this;

            self.config.options = options;

            //reset properties
            self.value = -1;
            self.selectedIndex = 0;
            self.selectedElement = null;
            self.highlightIndex = -1;

            rebuildOptions(self, self.active);
        },
        getOptions: function() {
            var self = this;
            return shmi.cloneObject(self.config.options);
        },
        /**
         * Sets the selected element
         *
         * @param element - element to select
         */
        setSelected: function(element) {
            for (var i = 0; i < this.optionElements.length; i++) {
                shmi.removeClass(this.optionElements[i], 'selected');
            }
            this.selectedElement = element;
            if (element) { //prevent null reference error
                shmi.addClass(this.selectedElement, 'selected');
            }
        },
        getCurrentIndex: function() {
            var index = -1;
            for (var i = 0; i < this.optionElements.length; i++) {
                if (this.optionElements[i] === this.selectedElement) {
                    index = i;
                    break;
                }
            }
            return index;
        },
        /**
         * Writes the current value to a connected data-source item
         *
         */
        updateValue: function() {
            if (this.config.item) {
                shmi.visuals.session.ItemManager.writeValue(this.config.item, this.value);
            }
        },
        /**
         * Enables the control
         *
         */
        onEnable: function() {
            if (this.config.item) {
                this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
            }
            this.inputListeners.concat(this.optionInputListeners).forEach(function(lst) {
                lst.enable();
            });
            shmi.log("[SelectRadio] enabled", 1);
        },
        /**
         * Disables the control
         *
         */
        onDisable: function() {
            if (this.config.item) {
                shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
            }
            this.inputListeners.concat(this.optionInputListeners).forEach(function(lst) {
                lst.disable();
            });
            shmi.log("[SelectRadio] disabled", 1);
        },
        /**
         * Locks the control
         *
         */
        onLock: function() {
            this.locked = true;
            this.highlightIndex = -1;
            this.inputListeners.concat(this.optionInputListeners).forEach(function(lst) {
                lst.disable();
            });
            shmi.addClass(this.element, 'locked');
            shmi.log("[SelectRadio] locked", 1);
        },
        /**
         * Unlocks the control
         *
         */
        onUnlock: function() {
            this.locked = false;
            this.inputListeners.concat(this.optionInputListeners).forEach(function(lst) {
                lst.enable();
            });
            shmi.removeClass(this.element, 'locked');
            shmi.log("[SelectRadio] unlocked", 1);
        },
        /**
         * Sets the value of the control
         *
         * @param value - new value to set
         */
        onSetValue: function(value) {
            var changed = (this.value !== value);
            this.value = value;
            var val_found = false;
            for (var i = 0; i < this.config.options.length; i++) {
                if (this.config.options[i].value === value) {
                    this.setSelected(this.optionElements[i]);
                    val_found = true;
                    break;
                }
            }
            if (!val_found) {
                this.setSelected(null);
            }
            shmi.log("[SelectRadio] value set: " + value, 1);
            if (changed) {
                this.fire('change', { value: this.value });
            }
        },
        /**
         * Retrieves the current value
         *
         * @return value current value
         */
        getValue: function() {
            return this.value;
        },
        setLabel: function(labelText) {
            var self = this,
                labelElement = shmi.getUiElement("label", self.element);

            if (self.config['auto-label'] && labelElement) {
                labelElement.textContent = shmi.localize(labelText);
            }
        }
    };

    shmi.extend(shmi.visuals.controls.SelectRadio, shmi.visuals.core.BaseControl);
}());

/**
 * SelectTime
 *
 * Configuration options (default):
 *
 * {
 *     "name": "selectTime",
 *     "class-name": "select-time",
 *     "template": "default/select-time",
 *     "item": null,
 *     "label": "select-time",
 *     "isUTC": false
 * }
 *
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * item {string}: Name of the item to use as source for timestamps
 * label {string}: Label of the control
 * isUTC {boolean}: Item values are treated as UTC timestamp instead of local time
 *
 * @version 2.0
 */
(function() {
    'use strict';

    //variables for reference in control definition
    var className = "SelectTime"; //control name in camel-case
    var uiType = "select-time"; //control keyword (data-ui)
    var isContainer = false;

    //example - default configuration
    var defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/select-time",
        "label": uiType,
        "numpad-enabled": false
    };

    //setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    //declare private functions - START
    /**
     * Checks whether or not the given value is of one of the expected types.
     *
     * @param {*} value Value to check
     * @param {string|string[]} expected Expected type or types of the provided value.
     */
    function checkType(value, expected) {
        function getType(v) {
            if (Array.isArray(v)) {
                return 'array';
            } else if (v === null) {
                return 'null';
            } else {
                return typeof (v);
            }
        }

        if (Array.isArray(expected)) {
            if (!(value in expected)) {
                return false;
            }
        } else if (expected !== getType(value)) {
            return false;
        }

        return true;
    }

    /**
     * Throws an exception if the given value is not of the provided type.
     *
     * @param {*} value Value to check
     * @param {string|string[]} expected Expected type or types of the provided value.
     * @param {string} [message] Error message
     *
     * @throws TypeError
     */
    function assertType(value, expected, message) {
        if (!checkType(value, expected)) {
            throw TypeError(message);
        }
    }

    /**
     * Compute new control value from the controls current state.
     *
     * @param {*} self Reference to the control
     */
    function computeNewValue(self) {
        var vars = self.vars;
        var date = new Date(vars.value * 1000);

        if (self.config.isUTC) {
            date.setHours(vars.hours);
            date.setMinutes(vars.minutes);
            date.setSeconds(vars.seconds);
        } else {
            date.setUTCHours(vars.hours);
            date.setUTCMinutes(vars.minutes);
            date.setUTCSeconds(vars.seconds);
        }

        return date.getTime() / 1000;
    }

    /**
     * Compute the controls new value, update its items' value and fire a
     * `change` event if the value actually changed.
     *
     * @param {*} self Reference to the control
     */
    function updateValue(self) {
        var newValue = computeNewValue(self);

        if (self.config.item) {
            // Make sure we write a string to the item if it was a string
            // before.
            if (self.vars.itemNeedsString) {
                self.imports.im.writeValue(self.config.item, String(newValue));
            } else {
                self.imports.im.writeValue(self.config.item, newValue);
            }
        }

        // Only fire a 'changed' event if the value actually changed.
        if (self.vars.value !== newValue) {
            self.vars.value = newValue;
            self.fire('change', { value: newValue });
        }

        updateInputFields(self.vars);
    }

    /**
     * @param {integer} val Integer to convert to a number within the group
     * @param {integer} N Modulo of the group
     */
    function integerToGroup(val, N) {
        return (N + (val % N)) % N;
    }

    /**
     * Adds seconds to the current time. Does overflow around into minutes.
     *
     * @param {*} self Reference to the control
     * @param {integer} x Number of seconds to add
     */
    function addSeconds(self, x) {
        assertType(x, 'number');

        self.vars.inputFields[0].blur();
        self.vars.seconds = integerToGroup(self.vars.seconds + x, 60);
        updateValue(self);
    }

    /**
     * Adds minutes to the current time. Does overflow around into hours.
     *
     * @param {*} self Reference to the control
     * @param {integer} x Number of minutes to add
     */
    function addMinutes(self, x) {
        assertType(x, 'number');

        self.vars.inputFields[1].blur();
        self.vars.minutes = integerToGroup(self.vars.minutes + x, 60);
        updateValue(self);
    }

    /**
     * Adds hours to the current time. Does overflow around into days.
     *
     * @param {*} self Reference to the control
     * @param {integer} x Number of hours to add
     */
    function addHours(self, x) {
        assertType(x, 'number');

        self.vars.inputFields[2].blur();
        self.vars.hours = integerToGroup(self.vars.hours + x, 24);
        updateValue(self);
    }

    /**
     * Validates and stores the nodes value. If the nodes value is invalid, it
     * is overwritten with the last known-to-be-good value.
     *
     * @param {*} self Reference to the control
     * @param {RegExp} regex A regex that has to match the elements textContent
     * @param {string} timecomponent Component to verify. Can be 'seconds', 'minutes' or 'hours'
     * @param {Node} element Node of the input field to validate
     */
    function validateGeneric(self, regex, timecomponent, element) {
        if (RegExp(regex).test(element.textContent)) {
            self.vars[timecomponent] = parseInt(element.textContent);
            updateValue(self);
            if (self.vars[timecomponent] < 10 && element.textContent.length === 1) {
                element.textContent = '0' + element.textContent;
            }
        } else if (self.vars[timecomponent] < 10) {
            element.textContent = '0' + self.vars[timecomponent];
        } else {
            element.textContent = self.vars[timecomponent];
        }
    }

    /**
     * Keypress listener for h/m/s input fields.
     *
     * @param {Node} element Node the listener is attached to
     * @param {Event} event The event
     */
    function inputKeypressListener(element, event) {
        if (Number(event.keyCode) === 13) {
            event.preventDefault();
            window.getSelection().removeAllRanges();
            element.blur();
        }
    }

    /**
     * Keyup listener for h/m/s input fields.
     *
     * @param {Node} element Node the listener is attached to
     * @param {Event} event The event
     */
    function inputKeyupListener(element, event) {
        if (Number(event.keyCode) === 13) {
            // Don't handle enter/return key
        } else if (element.textContent.length > 1) {
            event.preventDefault();
            window.getSelection().removeAllRanges();
            element.blur();
        }
    }

    /**
     * Blur listener for h/m/s input fields.
     *
     * @param {Node} element Node the listener is attached to
     * @param {Event} event The event
     */
    function inputBlurListener(element, validateFunc, event) {
        window.getSelection().removeAllRanges();
        element.setAttribute('contenteditable', false);
        shmi.removeClass(element, 'selectableText');
        validateFunc(element);
    }

    /**
     * Attaches mouse and touch listeners to a given node. The listeners
     * references are stored in the controls variable store and are enabled
     * or disabled by the control.
     *
     * @param {object} vars Reference to the controls variable storage
     * @param {Node} element Element to attach the listeners to
     * @param {Function} functions Listener callback
     */
    function addClickTouchListener(vars, element, functions) {
        vars.listeners.push(new shmi.visuals.io.MouseListener(element, functions));
        vars.listeners.push(new shmi.visuals.io.TouchListener(element, functions));
    }

    /**
     * Attach an event listener to a given node. The listeners are detached
     * when the control is destroyed.
     *
     * @param {object} vars Reference to the controls variable storage
     * @param {Node} element Element to attach the event listener to
     * @param {string} eventType Type of the event to attach the listener to.
     * @param {Function} listener Listener callback
     */
    function addEventListener(vars, element, eventType, listener) {
        element.addEventListener(eventType, listener);

        vars.evListeners.push({
            element: element,
            eventType: eventType,
            listener: listener
        });
    }

    /**
     * Creates input field on specified elemnt
     *
     * @param {object} vars Reference to the controls variable storage
     * @param {Node} element Element for input field
     * @param {Function} validateFunc Validation function
     * @param {object} numpadConfig Numpad configuration
     */
    function createInputField(vars, element, validateFunc, numpadConfig) {
        if (!element) {
            fLog('no base element provided');
            return;
        }
        var appConfig = shmi.requires("visuals.session.config"),
            keyboardEnabled = (appConfig.keyboard && appConfig.keyboard.enabled); // get the keyboard config from `project.json`

        numpadConfig = shmi.cloneObject(numpadConfig);
        numpadConfig.callback = function(res) {
            element.textContent = res;
            validateFunc(element);
        };

        addClickTouchListener(vars, element, {
            onClick: function onClick() {
                if (numpadConfig.numpadEnabled || keyboardEnabled) {
                    numpadConfig.value = element.textContent;
                    shmi.numpad(numpadConfig);
                } else {
                    shmi.addClass(element, 'selectableText');
                    element.setAttribute('contenteditable', true);
                    var range = document.createRange();
                    element.focus();
                    if (!element.firstChild || !(element.firstChild instanceof Text)) {
                        fLog('elem not found');
                    } else {
                        range.setStart(element.firstChild, 0);
                        range.setEnd(element.firstChild, element.firstChild.length);
                        window.getSelection().removeAllRanges();
                        window.getSelection().addRange(range);
                    }
                }
            }
        });

        element.style.overflow = 'hidden';
        addEventListener(vars, element, 'keypress', inputKeypressListener.bind(null, element));
        addEventListener(vars, element, 'keyup', inputKeyupListener.bind(null, element));
        addEventListener(vars, element, 'blur', inputBlurListener.bind(null, element, validateFunc));
    }

    /**
     * Adds listeners to a "rocker component" (+/- buttons and input field)
     *
     * @param {*} self Reference to the control
     * @param {object} configuration Rocker component configuration
     */
    function setupRockerComponent(self, configuration) {
        var elementComponent = shmi.getUiElement(configuration.uiElementName, self.element);
        if (!elementComponent) {
            fLog('Element not found: ' + configuration.uiElementName);
            return false;
        }

        var elementPrev = shmi.getUiElement('previous', elementComponent);
        if (!elementPrev) {
            fLog('No UI element found for ' + configuration.uiElementName + '.previous');
            return false;
        }

        var elementCurrent = shmi.getUiElement('current', elementComponent);
        if (!elementCurrent) {
            fLog('No UI element found for ' + configuration.uiElementName + '.current');
            return false;
        }

        var elementNext = shmi.getUiElement('next', elementComponent);
        if (!elementNext) {
            fLog('No UI element found for ' + configuration.uiElementName + '.next');
            return false;
        }

        createInputField(
            self.vars,
            elementCurrent,
            configuration.inputValidator.bind(self),
            {
                min: 0,
                max: configuration.max,
                label: configuration.label,
                numpadEnabled: self.config["numpad-enabled"] || false
            }
        );

        addClickTouchListener(self.vars, elementPrev, {
            onPress: shmi.addClass.bind(null, elementPrev, 'pressed'),
            onRelease: shmi.removeClass.bind(null, elementPrev, 'pressed'),
            onClick: configuration.addHandler.bind(null, -1)
        });

        addClickTouchListener(self.vars, elementNext, {
            onPress: shmi.addClass.bind(null, elementNext, 'pressed'),
            onRelease: shmi.removeClass.bind(null, elementNext, 'pressed'),
            onClick: configuration.addHandler.bind(null, 1)
        });

        self.vars.inputFields.push(elementCurrent);

        return true;
    }

    function updateInputFields(vars) {
        vars.inputFields[0].textContent = (vars.hours > 9) ?
            vars.hours : ('0' + vars.hours);
        vars.inputFields[1].textContent = (vars.minutes > 9) ?
            vars.minutes : ('0' + vars.minutes);
        vars.inputFields[2].textContent = (vars.seconds > 9) ?
            vars.seconds : ('0' + vars.seconds);
    }
    //declare private functions - END

    //definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            value: 0,
            hours: 0,
            minutes: 0,
            seconds: 0,
            listeners: [],
            evListeners: [],
            inputFields: [],
            itemNeedsString: false
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: 'visuals.session.ItemManager'
        },
        /* array of custom event types fired by this control */
        events: ['change'],
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var lbl_el = shmi.getUiElement('label', this.element);
                if (lbl_el) {
                    lbl_el.textContent = shmi.localize(this.config.label);
                }

                setupRockerComponent(this, {
                    uiElementName: 'hour-select',
                    inputValidator: validateGeneric.bind(null, this, /^[0-1]?[0-9]$|^2[0-3]$/, 'hours'),
                    addHandler: addHours.bind(null, this),
                    max: 23,
                    label: '${V_HOURS}'
                });

                setupRockerComponent(this, {
                    uiElementName: 'minute-select',
                    inputValidator: validateGeneric.bind(null, this, /^[0-5]?[0-9]$/, 'minutes'),
                    addHandler: addMinutes.bind(null, this),
                    max: 59,
                    label: '${V_MINUTES}'
                });

                setupRockerComponent(this, {
                    uiElementName: 'second-select',
                    inputValidator: validateGeneric.bind(null, this, /^[0-5]?[0-9]$/, 'seconds'),
                    addHandler: addSeconds.bind(null, this),
                    max: 59,
                    label: '${V_SECONDS}'
                });

                updateInputFields(this.vars);
            },
            onDelete: function() {
                this.vars.evListeners.forEach(function(info) {
                    info.element.removeEventListener(info.eventType, info.listener);
                });
                this.vars.evListeners = [];
                this.vars.listeners = [];
            },
            /* called when control is enabled */
            onEnable: function() {
                if (this.config.item) {
                    this.vars._subscriptionTargetId = this.imports.im.subscribeItem(this.config.item, this);
                }
                this.vars.listeners.forEach(function(listener) {
                    listener.enable();
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                if (this.config.item) {
                    this.imports.im.unsubscribeItem(this.config.item, this.vars._subscriptionTargetId);
                }
                this.vars.listeners.forEach(function(listener) {
                    listener.disable();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                this.vars.inputFields.forEach(function(element) {
                    element.blur();
                });
                this.vars.listeners.forEach(function(listener) {
                    listener.disable();
                });
                shmi.addClass(this.element, 'locked');
                log('locked');
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                this.vars.listeners.forEach(function(listener) {
                    listener.enable();
                });
                shmi.removeClass(this.element, 'locked');
                log('unlocked');
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {
                value = parseInt(value);
                var date = new Date(value * 1000);

                this.vars.itemNeedsString = (typeof (value) === 'string');

                // If isUTC is true, value is considered to be UTC so we call
                // the "normal" getHours/... functions. If isUTC is false, we
                // don't want any timezone conversion so we call the UTC
                // versions of the getter methods.
                if (this.config.isUTC) {
                    this.vars.hours = date.getHours();
                    this.vars.minutes = date.getMinutes();
                    this.vars.seconds = date.getSeconds();
                } else {
                    this.vars.hours = date.getUTCHours();
                    this.vars.minutes = date.getUTCMinutes();
                    this.vars.seconds = date.getUTCSeconds();
                }

                updateInputFields(this.vars);

                if (this.vars.value !== value) {
                    this.vars.value = value;
                    this.fire('change', { value: value });
                }
            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            },
            setLabel: function(labelText) {
                var labelElement = shmi.getUiElement('label', this.element);

                if (this.config['auto-label'] && labelElement) {
                    labelElement.textContent = shmi.localize(labelText);
                }
            },
            getValue: function() {
                return this.vars.value;
            },
            /** Returns the number of hours. */
            getHours: function() {
                return this.vars.hours;
            },
            /** Returns the number of minutes. */
            getMinutes: function() {
                return this.vars.minutes;
            },
            /** Returns the number of seconds. */
            getSeconds: function() {
                return this.vars.seconds;
            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires('visuals.tools.control-generator');
    cg.generate(definition);
})();

/**
 * Simple Table Control
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "simple-table",
 *     "name": null,
 *     "template": "custom/simple-table",
 *     "label": uiType,
 *     "grid-name": null,
 *     "pager": {
 *         "offset": 0,
 *         "rows-per-page": 50,
 *         "page-sizes": ["10", "50", "100", "250"]
 *     },
 *     "table": {
 *         "edit-icon": "",
 *         "fields": [],
 *         "edit-fields": false,
 *         "conditional-fields": false,
 *         "select": "multi", // true, null
 *         "sort": { "col": null, "order": "ASC" },
 *         "default-filters": { "level": 1 },
 *         "default-col-width": 150,
 *         "width": null,
 *         "height": null
 *       }
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * grid-name {string}: name of the dataGrid
 *
 * pager {object}: pager settings
 * pager.offset {integer}: start row
 * pager.rows-per-page {integer}: start page size
 * pager.page-sizes {array}: possible page sizes. e.g. [{"label": "ten", "value": 10}, {"value": 50}]
 *
 * table {object}: table config
 * table.edit-icon {string}: url to the image
 * table.select {string|bool|undefined}: if the rows can be selected true for one only, "multi" for multiple or false to disable
 * table.sort {object|null|undefined}:
 * table.sort.col {integer}: order by dataGrid col id
 * table.sort.order {string}: default sort order "ASC"|"DESC"
 * table.default-filters {object}: default filter e.g. { "level": 1,.. }
 * table.default-col-width {integer}: default col width
 * table.width {integer}: table width
 * table.height {integer}: table height
 *
 * table.fields {array}
 * table.fields[].label {string}:
 * table.fields[].icon {string}
 * table.fields[].sort-by-field {string}:
 * table.fields[].width {integer}: column width
 *
 * table.fields[].filter {object}: column filter config
 * table.fields[].filter.field {string}: field name to be filtered by
 * table.fields[].filter.type {string}: "numeric", "boolean", "string"
 * table.fields[].filter.select {false|array}: configuration for the select element [{ "label": "${zero}", "value": 0 }, { "value": 1 }, { "value": 2 }]
 * table.fields[].filter.label {false|string}: optional label. Can use i18n.
 * table.fields[].filter.size {integer}: Size attribute for the input|select element.
 *
 * table.fields[].controls {array}:
 * table.fields[].controls[].ui-type {string}:
 * table.fields[].controls[].config {object}: config that will be injected into control
 * table.fields[].controls[].mapping {array}:
 * table.fields[].controls[].mapping[] {object}: {"option": "item", "field": "id"} instead of "field" you can set "value": "${user_name}!"
 *
 * table.edit-fields {object|false}: configurations for the fields in "edit" mode. (select rows and click on `edit` button to change the mode)
 * table.edit-fields[].controls {object}: @see table.fields.controls
 *
 * table.conditional-fields {object|false}: Conditional configurations for the fields.
 * table.conditional-fields[].conditions {object}: this row controls will be changed if the conditions return true
 * table.conditional-fields[].conditions[] {object}: Condition e.g. {"field": "index", "operator": "<", "operand": 3}
 * table.conditional-fields[].controls {object}: @see table.fields.controls
 *
 * !! Only for the usage with the Designer. !!
 * use-designer-mapping {boolean}: to enable usage of the "col-mapping"
 * col-mapping {object}: Simple column config for the designer
 * col-mapping.label {string}: Label for the column
 * col-mapping.field {string}: Field name of the data grid to get the item from
 * col-mapping.sort-by-field {string}: Which dataGrind will be used to sort. Empty for not sortable columns.
 *
 */

/* eslint-disable no-unused-vars */
(function() {
    'use strict';

    // constants
    var cons = {
        "BOOLEANS_TRUE": [1, "1", "true", true]
    };

    //variables for reference in control definition
    var className = "SimpleTable", //control name in camel-case
        uiType = "simple-table", //control keyword (data-ui)
        isContainer = false;

    // default configuration
    var defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "custom/simple-table",
        "grid-name": null,
        "pager": {
            "offset": 0,
            "rows-per-page": 50,
            "page-sizes": [{ value: 10 }, { value: 50 }, { value: 100 }, { value: 250 }]
        },
        // Set "use-designer-mapping" to false to disable "col-mapping" settings.
        "use-designer-mapping": false,
        "col-mapping": [],
        "table": {
            "edit-icon": "",
            "fields": [],
            "edit-fields": false,
            "conditional-fields": false,
            "select": "multi",
            "sort": { "col": null, "order": "ASC" },
            "default-filters": {},
            "default-col-width": 150,
            "width": null,
            "height": null
        }
    };

    //setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    //declare private functions - START
    function initElements(self) {
        var optionElement = null,
            scrollbarWidth;
        // table-head
        self.vars.theadElementWrapper = shmi.getUiElement('simple-table-head-wrapper', self.element);
        if (!self.vars.theadElementWrapper) {
            shmi.log("[simple-table] simple-table-head-wrapper element not found in template", 3);
        }
        self.vars.theadElement = shmi.getUiElement('simple-table-head', self.element);
        if (!self.vars.theadElement) {
            shmi.log("[simple-table] simple-table-head element not found in template", 3);
        }
        // table-filter
        self.vars.tfilterElementWrapper = shmi.getUiElement('simple-table-filter-wrapper', self.element);
        if (!self.vars.tfilterElementWrapper) {
            shmi.log("[simple-table] simple-table-filter-wrapper element not found in template", 3);
        }
        self.vars.tfilterElement = shmi.getUiElement('simple-table-filter', self.element);
        if (!self.vars.tfilterElement) {
            shmi.log("[simple-table] simple-table-filter element not found in template", 3);
        }
        // table-body
        self.vars.tbodyElementWrapper = shmi.getUiElement('simple-table-body-wrapper', self.element);
        if (!self.vars.tbodyElementWrapper) {
            shmi.log("[simple-table] simple-table-body-wrapper element not found in template", 3);
        }
        self.vars.tbodyElement = shmi.getUiElement('simple-table-body', self.element);
        if (!self.vars.tbodyElement) {
            shmi.log("[simple-table] simple-table-body element not found in template", 3);
        }
        self.vars.pagerElement = shmi.getUiElement('simple-table-pager', self.element);
        if (!self.vars.pagerElement) {
            shmi.log("[simple-table] simple-table-pager element not found in template", 3);
        }
        self.vars.pagerTotalElement = shmi.getUiElement('simple-table-total', self.element);
        if (!self.vars.pagerTotalElement) {
            shmi.log("[simple-table] simple-table-total element not found in template", 3);
        }
        self.vars.pagerSettingsElement = shmi.getUiElement('simple-table-settings', self.element);
        if (!self.vars.pagerSettingsElement) {
            shmi.log("[simple-table] simple-table-settings element not found in template", 3);
        }

        if (self.config.table.width) {
            scrollbarWidth = 15;
            if (self.vars.tbodyElementWrapper.offsetWidth && self.vars.tbodyElementWrapper.clientWidth) {
                scrollbarWidth = Number(self.vars.tbodyElementWrapper.offsetWidth) - Number(self.vars.tbodyElementWrapper.clientWidth);
            }
            self.vars.theadElementWrapper.style.width = self.config.table.width + "px";
            self.vars.tfilterElementWrapper.style.width = self.config.table.width + "px";
            self.vars.tbodyElementWrapper.style.width = self.config.table.width + "px";
            self.vars.pagerElement.style.width = (Number(self.config.table.width) + scrollbarWidth) + "px";
        }

        if (self.config.table.height) {
            self.vars.tbodyElementWrapper.style.height = self.config.table.height + "px";
        }

        if (self.config.pager["rows-per-page"]) {
            self.config.pager["page-sizes"].forEach(function(size) {
                optionElement = document.createElement("option");
                optionElement.value = size.value;
                optionElement.textContent = (size.label) ? shmi.localize(size.label) : size.value;
                if (size.value === self.config.pager["rows-per-page"]) {
                    optionElement.selected = "selected";
                }
                self.vars.pagerSettingsElement.firstElementChild.appendChild(optionElement);
            });
        }
    }

    /** synchronize the scrollbars */
    function addTableWrapperScrollListener(self) {
        self.vars.tbodyElementWrapper.onscroll = function() {
            var scrollLeft = self.vars.tbodyElementWrapper.scrollLeft;
            self.vars.theadElementWrapper.scrollLeft = scrollLeft;
            self.vars.tfilterElementWrapper.scrollLeft = scrollLeft;
            if (self.vars.theadElementWrapper.scrollLeft !== scrollLeft) {
                self.vars.tbodyElementWrapper.scrollLeft = self.vars.theadElementWrapper.scrollLeft;
            }
        };
    }

    function removeTableWrapperScrollListener(self) {
        self.vars.tbodyElementWrapper.onscroll = null;
    }

    /** Set controls into the prepared table */
    function setDataGridControls(self) {
        self.vars.tableUiElements.forEach(function(row, rowId) {
            setRowControls(rowId, self);
        });
    }

    /**
     * Voter object to test the value against the operand using the operator
     * @argument {number|boolean|string} value
     * @argument {string} operator - "contains", "<", ">", "<=", ">=", "==", "!="
     * @argument {number|boolean|string} operand
     * @return {boolean}
     */
    function Voter(value, operator, operand) {
        this.value = value;
        this.operator = operator;
        this.operand = operand;

        function validate(self) {
            var msg;
            if (["contains", "<", ">", "<=", ">=", "==", "!="].indexOf(self.operator) === -1) {
                msg = shmi.evalString("[simple-table] invalid operator <%= operator %>", { operator: self.operator });
                shmi.log(msg, 3);
                return false;
            }
            if (["number", "boolean", "string"].indexOf(typeof self.operand) === -1) {
                msg = shmi.evalString("[simple-table] invalid type [<%= type %>] of the operand", { type: typeof self.operand });
                shmi.log(msg, 3);
                return false;
            }
            return true;
        }

        function compare(self) {
            switch (self.operator) {
            case "<":
                return Boolean(self.value < self.operand);
            case ">":
                return Boolean(self.value > self.operand);
            case "<=":
                return Boolean(self.value <= self.operand);
            case ">=":
                return Boolean(self.value >= self.operand);
            case "==":
                var equals = typeof self.value === 'string' ? Boolean(self.value === String(self.operand)) : Boolean(self.value === self.operand);
                return equals;
            case "!=":
                var unequals = typeof self.value === 'string' ? Boolean(self.value === String(self.operand)) : Boolean(self.value === self.operand);
                return unequals;
            default:
                break;
            }
            return false;
        }

        this.vote = function vote() {
            var self = this;

            if (!validate(self)) {
                return false;
            }

            if ("contains" === self.operator) {
                return Boolean(String(self.value).indexOf(self.operand) !== -1);
            }

            if (["number", "boolean"].indexOf(typeof self.operand) !== -1) {
                self.value = Number(self.value);
            } else {
                self.value = String(self.value);
            }
            return compare(self);
        };
    }

    /**
     * @param {array} gridRow row from the dataGrid::getRowData(rowId)
     * @param {this} self
     * @return {object} conditional field config for the row
     */
    function getConditionalConfigForRow(gridRow, self) {
        var conditionals = getByPath("conditional-fields", self.config.table),
            conditionalFields = {};

        if (conditionals) {
            conditionals.some(function(conditional) {
                var results = [];
                conditional.conditions.forEach(function(condition) {
                    var itemFieldId = self.vars.dataGrid.getFields().indexOf(condition.field),
                        value = gridRow[itemFieldId].value,
                        result, voter;

                    voter = new Voter(value, condition.operator, condition.operand);
                    result = voter.vote();
                    results.push(result);
                });

                if (results.indexOf(false) === -1) {
                    conditionalFields = conditional.fields;
                    return true;
                }
                return false;
            });
        }

        return conditionalFields;
    }

    /**
     * Set controls into the prepared table row
     * @param {integer} rowId
     * @param {object} self
     * @param {boolean} editable - set the fields of this row to the edit mode if defined in the config
     * @return {void}
     */
    function setRowControls(rowId, self, editable) {
        var gridRow = self.vars.dataGrid.getRowData(rowId),
            conditionalFields = getConditionalConfigForRow(gridRow, self);

        self.config.table.fields.forEach(function(fieldConfig, colIdx) {
            var dataUi = "simple-table-item-" + rowId + "-" + colIdx,
                gridUiElement = shmi.getUiElement(dataUi, self.element),
                controls,
                msg;

            if (!gridUiElement) {
                msg = shmi.evalString("[simple-table] Cannot find element <%= dataUi %>", { dataUi: dataUi });
                shmi.log(msg, 3);
                return;
            }
            if (!fieldConfig.controls) {
                msg = shmi.evalString("[simple-table] no controls defined for the cell <%= dataUi %>", { dataUi: dataUi });
                shmi.log(msg, 3);
                return;
            }

            deleteControls(self, rowId, colIdx);
            gridUiElement.textContent = "";
            self.vars.tableUiElements[rowId][colIdx] = gridUiElement;

            if (editable && getByPath("edit-fields." + colIdx + ".controls", self.config.table)) {
                controls = getByPath("edit-fields." + colIdx + ".controls", self.config.table);
            }
            if (!controls && conditionalFields[colIdx]) {
                controls = conditionalFields[colIdx].controls;
            }
            controls = controls || fieldConfig.controls;

            // create controls
            controls.forEach(function(controlCfg) {
                var control = addControlToElement(gridUiElement, controlCfg, gridRow, self);
                saveControl(control, rowId, colIdx, self);
            });
        });
    }

    /**
     * Save created controls to be able to delete them later
     * @param {object} control control to save
     * @param {integer} rowId
     * @param {integer} colIdx
     * @param {object} self
     */
    function saveControl(control, rowId, colIdx, self) {
        var path,
            savedControls;
        if (control !== null) {
            path = rowId + "." + colIdx;
            savedControls = getByPath(path, self.vars.controls, []);
            savedControls.push(control);
            setByPath(path, self.vars.controls, savedControls);
        }
    }

    /**
     * Delete saved controls for:
     * deleteControls(self, row, col) - cell
     * deleteControls(self, row) - row
     * deleteControls(self) - all
     *
     * @param {object} self
     * @param {integer|undefined} row rowId
     * @param {integer|undefined} col colId
     */
    function deleteControls(self, row, col) {
        var path,
            rowIdx,
            colIdx,
            controls;
        if (row === undefined) {
            rowIdx = Object.keys(self.vars.controls);
            rowIdx.forEach(function(rowElem) {
                deleteControls(self, rowElem, col);
            });
        } else if (col === undefined) {
            colIdx = Object.keys(getByPath(row, self.vars.controls, []));
            colIdx.forEach(function(colElem) {
                deleteControls(self, row, colElem);
            });
        }
        path = row + "." + col;
        controls = getByPath(path, self.vars.controls, []);
        controls.forEach(function(element) {
            shmi.deleteControl(element, true);
        });
        setByPath(path, self.vars.controls, []);
    }

    /**
     * @param {HTMLElement} gridUiElement control will be added to
     * @param {object} control config for the control
     * @param {object} gridRow data from the dataGrid
     * @param {object} self this control
     *
     * @return {object} added control
     */
    function addControlToElement(gridUiElement, control, gridRow, self) {
        var msg, cfg;
        if ("mapping" in control === false) {
            msg = shmi.evalString(
                "[simple-table] no mapping for control <%= uiType %>", {
                    uiType: control['ui-type']
                }
            );
            shmi.log(msg, 3);
            return null;
        }
        cfg = shmi.cloneObject(control.config);
        control.mapping.forEach(function(mapping) {
            var itemFieldId = self.vars.dataGrid.getFields().indexOf(mapping.field);
            if ("value" in mapping) {
                cfg[mapping.option] = shmi.localize(mapping.value);
            } else if (gridRow[itemFieldId] && gridRow[itemFieldId].item) {
                cfg[mapping.option] = gridRow[itemFieldId].item.name;
            } else {
                msg = shmi.evalString(
                    "[simple-table] Bad mapping defined for control <%= uiType %>: <%= field %>", {
                        field: mapping.field,
                        uiType: control['ui-type']
                    }
                );
                shmi.log(msg, 3);
            }
        });
        return shmi.createControl(control["ui-type"], gridUiElement, cfg, "DIV");
    }

    function onDataGridChange(dgChgInfo, self) {
        if (dgChgInfo && (dgChgInfo.status === "OK")) {
            if (self.vars.isTableToCleanup) {
                cleanupTabContent(self);
                self.vars.isTableToCleanup = false;
            }
            self.vars.dataGrid = self.vars.dgMan.getGrid(self.config["grid-name"]);

            prepareTable(self);
            fillTableWithData(self);
            buildPager(self);

            self.vars.isTableToCleanup = true;
        }
    }

    /** prepare static table parts */
    function prepareTable(self) {
        var colGroupElem = document.createElement("colgroup"),
            theadElem = document.createElement("thead"),
            tbodyElem = document.createElement("tbody"),
            editButton = "",
            tableData = {
                "colGroupHTML": "",
                "theadHTML": "<tr>",
                "tfilterHTML": "<tr>",
                "colwidth": self.config.table["default-col-width"],
                "tabwidth": 0
            };

        if (getByPath("table.edit-fields", self.config)) {
            editButton = '<a class="simple-table-edit-button" title="Edit selected rows" />';
            editButton += (self.config.table["edit-icon"]) ? '<img src="' + self.config.table["edit-icon"] + '" />' : '📝';
            editButton += '</a>';
        }
        if (getByPath("table.select", self.config)) {
            tableData.colGroupHTML += '<col class="simple-table-col-select">';
            tableData.theadHTML += '<td class="simple-table-select-row ">';
            if (getByPath("table.select", self.config) === "multi") {
                tableData.theadHTML += '<input id="simple-table-select-row-all" class="simple-table-select-row-all" type="checkbox">';
            }
            tableData.theadHTML += '</td>';
            tableData.tfilterHTML += '<td class="simple-table-edit">' + editButton + '</td>';
        }

        self.config.table.fields.forEach(function(elementCfg, colIdx) {
            prepareHeaderRows(elementCfg, colIdx, tableData, self);
        });

        colGroupElem.innerHTML = tableData.colGroupHTML;
        theadElem.innerHTML = tableData.theadHTML + "</tr>";
        tbodyElem.innerHTML = tableData.tfilterHTML + "</tr>";

        self.vars.theadElement.appendChild(colGroupElem);
        self.vars.tfilterElement.appendChild(colGroupElem.cloneNode(true));
        self.vars.tbodyElement.appendChild(colGroupElem.cloneNode(true));

        self.vars.theadElement.appendChild(theadElem);
        self.vars.tfilterElement.appendChild(tbodyElem);

        self.vars.theadElement.style.width = tableData.tabwidth + "px";
        self.vars.tfilterElement.style.width = tableData.tabwidth + "px";
        self.vars.tbodyElement.style.width = tableData.tabwidth + "px";

        addFilterListeners(self);
    }

    /** prepare header rows for the table  */
    function prepareHeaderRows(elementCfg, colIdx, tableData, self) {
        var orderIcon = (typeof elementCfg["sort-by-field"] === "string") ? "⭐" : "",
            colIcon = "",
            curDataGridOrder = getOrder(self);

        if (self.vars.dataGrid.getFields()[curDataGridOrder.col] === elementCfg["sort-by-field"]) {
            switch (curDataGridOrder.order) {
            case "ASC":
                orderIcon = "⭣";
                break;
            case "DESC":
                orderIcon = "⭡";
                break;
            default:
                break;
            }
        }

        tableData.colwidth = elementCfg.width || self.config.table["default-col-width"];
        tableData.tabwidth += tableData.colwidth;
        tableData.colGroupHTML += '<col class="" style="width:' + tableData.colwidth + 'px;">';
        if (elementCfg.icon) {
            colIcon = '<img class="col-icon" src="' + elementCfg.icon + '" />';
        }
        if (orderIcon !== "") {
            tableData.theadHTML += '<th  class="col-sort" data-ui="simple-table-sort-' + colIdx + '" data-col-id="' + colIdx + '">';
        } else {
            tableData.theadHTML += '<th>';
        }
        tableData.theadHTML += colIcon + " " + orderIcon + " " + shmi.localize(elementCfg.label) + " " + orderIcon + '</th>';
        tableData.tfilterHTML += '<td class="col-filter-header" data-col-id="' + colIdx + '">';
        // filters
        if (elementCfg.filter && "label" in elementCfg.filter) {
            tableData.tfilterHTML += '<label for="">' + shmi.localize(elementCfg.filter.label) + '</label>';
        }
        if (elementCfg.filter && "select" in elementCfg.filter) {
            tableData.tfilterHTML += '<select class="simple-table-filter" field="' + elementCfg.filter.field + '" size="' + Number(elementCfg.filter.size) + '"><option value=""></option>';
            elementCfg.filter.select.forEach(function(filterSelect) {
                var label = filterSelect.label || filterSelect.value,
                    value = filterSelect.value;
                tableData.tfilterHTML += '<option value="' + value + '">' + shmi.localize(label) + '</option>';
            });
            tableData.tfilterHTML += '</select>';
        } else if (elementCfg.filter) {
            tableData.tfilterHTML += '<input type="text" size="' + Number(elementCfg.filter.size) + '" class="simple-table-filter" field="' + elementCfg.filter.field + '" />';
        }
        tableData.tfilterHTML += '</td>';
    }

    /** Add listeners to the table header with filters */
    function addFilterListeners(self) {
        var elements = self.element.getElementsByClassName("simple-table-filter"),
            filters = self.vars.dataGrid.getFilters(),
            index, element, fieldName, itemFieldId;
        for (index = 0; index < elements.length; index++) {
            element = elements[index];
            fieldName = element.getAttribute("field");
            itemFieldId = self.vars.dataGrid.getFields().indexOf(fieldName);

            if (filters[itemFieldId] !== null) {
                element.value = filters[itemFieldId];
            }
            element.onchange = dgFilter.bind(null, element, self);
        }
    }

    /** callback for filter input elements */
    function dgFilter(input, self) {
        var fieldName = input.getAttribute("field"),
            itemFieldId = self.vars.dataGrid.getFields().indexOf(fieldName),
            valueType, colid,
            value = input.value;
        self.clearFilter(itemFieldId);
        if (value === "") {
            return;
        }
        colid = input.parentElement.getAttribute("data-col-id");
        valueType = getByPath("table.fields." + colid + ".filter.type", self.config);
        switch (valueType) {
        case "numeric":
            value = Number(value);
            break;
        case "boolean":
            value = cons.BOOLEANS_TRUE.indexOf(value) !== -1;
            break;
        default:
        case "string":
            value = String(value);
            break;
        }
        self.setFilter(itemFieldId, value);
    }

    /** fill table data rows */
    function fillTableWithData(self) {
        var currDgIds = self.vars.dgMan.getCurrentIDs(self.config["grid-name"], self.vars.dgSubscription.id),
            tbodyElem = document.createElement("tbody"),
            elements, index;
        self.vars.tableUiElements = [];
        currDgIds.forEach(function(dgRowId) {
            var trElem = document.createElement("tr");
            trElem.setAttribute("data-row-id", dgRowId);
            shmi.addClass(trElem, "simple-table-data-row");
            trElem.innerHTML = createTableRowHtml(dgRowId, self);
            tbodyElem.appendChild(trElem);
        });
        self.vars.tbodyElement.appendChild(tbodyElem);
        setDataGridControls(self);

        elements = self.element.getElementsByClassName("simple-table-select-row");
        for (index = 0; index < elements.length; index++) {
            if (elements[index].firstChild) {
                elements[index].firstChild.onchange = function() {
                    rowsSelector(this, self);
                };
            }
        }
    }

    /**
     * @param {integer} dgRowId
     * @param {object} self
     * @return {string} row html string
     */
    function createTableRowHtml(dgRowId, self) {
        var trHTML = '';
        if (self.config.table.select) {
            trHTML += '<td class="simple-table-select-row"><input class="simple-table-row-' + dgRowId;
            trHTML += '" id="select-row-' + dgRowId + '" value="' + dgRowId + '" type="checkbox"></td>';
        }
        self.vars.tableUiElements[dgRowId] = [];
        self.config.table.fields.forEach(function(element, colIdx) {
            self.vars.tableUiElements[dgRowId][colIdx] = null;
            trHTML += '<td class="" data-ui="simple-table-item-' + dgRowId + '-' + colIdx + '"></td>';
        });

        return trHTML;
    }

    /**
     * callback for the row select checkboxes
     * @param {HTMLElement} checkBox
     * @param {object} self
     */
    function rowsSelector(checkBox, self) {
        var index, tds, td, tr,
            eventDetails = { "rows": [], "grid": self.config.name };
        if (checkBox.id === "simple-table-select-row-all") {
            tds = self.element.getElementsByClassName("simple-table-select-row");
            for (index = 0; index < tds.length; index++) {
                td = tds[index];
                td.firstChild.checked = checkBox.checked;
                tr = td.parentElement;
                if (td.firstChild.id !== "simple-table-select-row-all" && td.firstChild.checked === true) {
                    shmi.addClass(tr, "selected");
                    eventDetails.rows.push(td.firstChild.value);
                } else {
                    shmi.removeClass(tr, "selected");
                }
            }
        } else {
            tr = checkBox.parentElement.parentElement;
            if (checkBox.checked === true) {
                if (self.config.table.select !== "multi") {
                    clearSelection(self);
                    checkBox.checked = true;
                }
                shmi.addClass(tr, "selected");
            } else {
                shmi.removeClass(tr, "selected");
            }
            tds = self.element.getElementsByClassName("simple-table-select-row");
            for (index = 0; index < tds.length; index++) {
                td = tds[index];
                if (td.firstChild && td.firstChild.id !== "simple-table-select-row-all" && td.firstChild.checked === true) {
                    eventDetails.rows.push(td.firstChild.value);
                }
            }
        }
        shmi.fire('select', eventDetails, self.element);
    }

    /** clear selection from all rows */
    function clearSelection(self) {
        var index, tds = self.element.getElementsByClassName("simple-table-select-row"),
            td, tr;
        for (index = 0; index < tds.length; index++) {
            td = tds[index];
            if (td.firstChild) {
                td.firstChild.checked = false;
            }
            tr = td.parentElement;
            shmi.removeClass(tr, "selected");
        }
    }

    function buildPager(self) {
        var pagesButtons = [],
            pagesElem = document.createElement("div"),
            offset = self.config.pager["offset"],
            perPage = self.config.pager["rows-per-page"],
            totalRows = self.vars.dataGrid.getRowCount(self.config["grid-name"]),
            pagesTotal = parseInt(totalRows / perPage, 10) + 1,
            curPage = parseInt(offset / perPage, 10) + 1,
            prevPage = (curPage > 1) ? curPage - 1 : curPage,
            nextPage = (curPage < pagesTotal) ? curPage + 1 : curPage,
            msg = shmi.localize("${SIMPLE_TABLE_PAGER_TOTAL}"),
            tmpPageNum = 1,
            tmpPageNumMin = 1,
            tmpPageNumMax = pagesTotal;
        msg = shmi.evalString(msg, { value: pagesTotal });

        if (pagesTotal > 5) {
            tmpPageNumMin = curPage - 2;
            tmpPageNumMax = curPage + 2;
            if (tmpPageNumMin < 1) {
                tmpPageNumMax += 1 + Math.abs(tmpPageNumMin);
                tmpPageNumMin = 1;
            }
            if (tmpPageNumMax > pagesTotal) {
                tmpPageNumMin -= tmpPageNumMax - pagesTotal;
                tmpPageNumMax = pagesTotal;
            }
        }
        pagesButtons.push({ "label": "⭰", "data-page": 1 });
        pagesButtons.push({ "label": "⭠", "data-page": prevPage });

        for (tmpPageNum = tmpPageNumMin; tmpPageNum <= tmpPageNumMax; tmpPageNum++) {
            pagesButtons.push({ "label": tmpPageNum, "data-page": tmpPageNum });
        }
        pagesButtons.push({ "label": "⭢", "data-page": nextPage });
        pagesButtons.push({ "label": "⭲", "data-page": pagesTotal });

        pagesButtons.forEach(function(element) {
            var pageElem = document.createElement("div"),
                elClass = "pagenum button small";
            pageElem.textContent = element.label;
            pageElem.setAttribute("data-page", element["data-page"]);
            if (element["data-page"] === curPage) {
                elClass += " inactive";
            }
            shmi.addClass(pageElem, elClass);
            pagesElem.appendChild(pageElem);
        });
        shmi.addClass(pagesElem, "pages");

        self.vars.pagerElement.firstChild.replaceWith(pagesElem);

        self.vars.pagerTotalElement.textContent = msg;
    }

    function onPagerClick(el, self) {
        if (!el.classList.contains('pagenum')) {
            return false;
        }
        var page = el.getAttribute("data-page"),
            // offset is the first row on the page.
            offset = (self.config.pager["rows-per-page"] * (page - 1));
        self.config.pager.offset = offset;
        self.vars.dgMan.setOffset(
            self.config["grid-name"],
            offset,
            self.vars.dgSubscription.id
        );

        return true;
    }

    /**
     * @param {object} self
     * @return {object} {"col": "", "order": ""}
     */
    function getOrder(self) {
        var order = { "col": null, "field": null, "order": "ASC" };
        if (self.config.table.sort && Number.isInteger(self.config.table.sort.col) && self.config.table.sort.order) {
            order.order = self.config.table.sort.order;
            order.col = self.config.table.sort.col;
        }
        return order;
    }

    function onSorterClick(el, self) {
        var orders = ["ASC", "DESC"],
            order = "ASC",
            path,
            dataGridColName,
            dataGridCol,
            curDataGridOrder = getOrder(self);

        if (el.classList.contains('col-sort') === false) {
            return false;
        }
        path = "table.fields." + el.getAttribute("data-col-id") + ".sort-by-field";
        dataGridColName = getByPath(path, self.config);
        dataGridCol = self.vars.dataGrid.getFields().indexOf(dataGridColName);
        if (dataGridCol === Number(curDataGridOrder.col)) {
            order = orders[orders.indexOf(curDataGridOrder.order) + 1];
        }
        if (order === undefined) {
            self.sort(-1);
            return true;
        }

        self.sort(dataGridCol, order);

        return true;
    }

    function onEditClick(el, self) {
        if (!el.classList.contains('simple-table-edit-button')) {
            return false;
        }
        var isActive, index, tds = self.element.getElementsByClassName("simple-table-select-row"),
            td, tr, rowId;
        el.parentElement.classList.toggle("active");
        isActive = el.parentElement.classList.contains('active');

        if (!isActive) {
            setDataGridControls(self);
            return true;
        }

        for (index = 0; index < tds.length; index++) {
            td = tds[index];
            if (!td.firstChild.checked) {
                continue;
            }
            tr = td.parentElement;
            if (!tr.classList.contains('simple-table-data-row')) {
                continue;
            }
            rowId = tr.getAttribute("data-row-id");
            if (rowId !== null) {
                setRowControls(rowId, self, true);
            }
        }

        return true;
    }

    /**
     * @private
     */
    function cleanupTabContent(self) {
        deleteControls(self);
        clearTableDOM(self);
    }

    /**
     * @private
     */
    function clearTableDOM(self) {
        self.vars.theadElement.innerHTML = "";
        self.vars.tfilterElement.innerHTML = "";
        self.vars.tbodyElement.innerHTML = "";
    }

    /**
     * Take mapping from the designer config
     * @param {object} self
     */
    function getDesignerColMapping(self) {
        if (self.config["use-designer-mapping"] !== true) {
            return;
        }

        if (self.config["col-mapping"].length > 0) {
            self.config["col-mapping"].forEach(function(element, index) {
                if (!self.config.table.fields[index]) {
                    self.config.table.fields[index] = {
                        "label": "",
                        "icon": null,
                        "width": self.config.table["default-col-width"],
                        "sort-by-field": null,
                        "controls": [{
                            "ui-type": "text2",
                            "config": {},
                            "mapping": []
                        }]
                    };
                }
                if (!self.config.table.fields[index].controls[0]) {
                    self.config.table.fields[index].controls[0] = {
                        "ui-type": "text2",
                        "config": {},
                        "mapping": []
                    };
                }
                if (!self.config.table.fields[index].controls[0].mapping[0]) {
                    self.config.table.fields[index].controls[0].mapping[0] = {
                        "option": "item",
                        "field": null
                    };
                }
                self.config.table.fields[index].label = element.label;
                self.config.table.fields[index].controls[0].mapping[0].field = element.field;
                if (element["sort-by-field"] && element["sort-by-field"] !== "") {
                    self.config.table.fields[index]["sort-by-field"] = element["sort-by-field"];
                }
            });
        }
    }

    //helpers
    /** @param {string} path e.g "a.b.c" @param {object} obj e.g obj[a][b][c] @param {*} def default value*/
    function getByPath(path, obj, def) {
        var res = obj,
            index, key;
        if (typeof path === "string") {
            path = path.replace("[", ".");
            path = path.replace("]", "");
            path = path.split(".");
        }
        for (index = 0; index < path.length; index++) {
            key = path[index];
            if (res && key in res) {
                res = res[key];
                continue;
            }
            return def;
        }

        return res;
    }

    /**
     * "a.b[7].d" = "foo" --> {a: {b: [6 x undefined, {d: "foo"}]}}
     * @param {string|array} path e.g "a.b[7].c"|["a","b[","7","c"]
     * @param {object} obj e.g obj.a.b.c
     * @param {*} value
     */
    function setByPath(path, obj, value) {
        if (typeof path === "string") {
            // replace array brackets but keep one to differentiate between empty {} and []
            path = path.replace("[", "[.");
            path = path.replace("]", "");
            path = path.split(".");
        }
        if (path.length > 1) {
            var e = path.shift(),
                defaultEmpty = {};
            if (e.indexOf("[") !== -1) {
                e = e.replace("[", "");
                defaultEmpty = [];
            }
            if (((typeof obj[e] !== "object") || (obj[e] === null))) {
                obj[e] = defaultEmpty;
            }
            setByPath(path, obj[e], value);
        } else {
            obj[path[0]] = value;
        }
    }

    //declare private functions - END

    //definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            dgMan: null,
            dgSubscription: {},
            dataGrid: {},
            isTableToCleanup: false,
            listeners: [],
            theadElement: null,
            tfilterElement: null,
            tbodyElement: null,
            pagerElement: null,
            tableUiElements: [],
            controls: {}
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            /* example - add import via function call */
            qm: function() {
                return shmi.visuals.session.QueryManager;
            }
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this,
                    ml = null, // MouseListener
                    tl = null; // TouchListener

                // init config
                self.config.table.filters = {};
                if (self.config.table["default-filters"]) {
                    self.config.table.filters = self.config.table["default-filters"];
                }

                getDesignerColMapping(self);
                //

                initElements(self);

                // Mouse and touch listeners
                var functions = {};
                functions.onPress = function(x, y, event) {};
                ml = new shmi.visuals.io.MouseListener(self.element, functions);
                tl = new shmi.visuals.io.TouchListener(self.element, functions);
                self.vars.listeners.push(ml, tl);
            },
            /* called when control is enabled */
            onEnable: function() {
                var self = this,
                    iterObj = shmi.requires("visuals.tools.iterate.iterateObject"),
                    filters,
                    fieldNames;
                self.vars.dgMan = shmi.visuals.session.DataGridManager;
                self.vars.dgSubscription = self.vars.dgMan.subscribePage(
                    self.config["grid-name"], self.config.pager.offset, self.config.pager["rows-per-page"],
                    function(dgChgInfo) {
                        self.vars.dgSubscription.changeInfo = dgChgInfo;
                        onDataGridChange(dgChgInfo, self);
                    });

                if (self.config.table.sort && Number.isInteger(self.config.table.sort.col) && self.config.table.sort.order) {
                    self.vars.dgMan.sort(self.config["grid-name"], self.config.table.sort.col, self.config.table.sort.order);
                }

                // default filters
                fieldNames = self.vars.dgMan.getFields(self.config["grid-name"]);
                filters = self.config.table.filters;
                iterObj(filters || {}, (filter, fName) => {
                    if (fieldNames.indexOf(fName) !== -1) {
                        self.vars.dgMan.setFilter(self.config["grid-name"], fieldNames.indexOf(fName), filter);
                    }
                });

                self.vars.pagerSettingsElement.onchange = function(event) {
                    var value = event.target.value;
                    if (value && value !== self.config.pager["rows-per-page"]) {
                        self.config.pager["rows-per-page"] = value;
                        self.config.pager.offset = 0;
                        self.vars.pagerSettingsElement.firstChild.value = value;
                        self.vars.dgMan.unsubscribe(self.config["grid-name"], self.vars.dgSubscription.id);
                        self.vars.dgSubscription = self.vars.dgMan.subscribePage(
                            self.config["grid-name"], self.config.pager.offset, self.config.pager["rows-per-page"],
                            function(dgChgInfo) {
                                self.vars.dgSubscription.changeInfo = dgChgInfo;
                                onDataGridChange(dgChgInfo, self);
                            });
                    }
                };
                addTableWrapperScrollListener(self);
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                var self = this;

                self.vars.dgMan.unsubscribe(self.config["grid-name"], self.vars.dgSubscription.id);
                self.vars.dgMan = null;
                self.vars.dgSubscription = null;
                deleteControls(self);
                removeTableWrapperScrollListener(self);
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                deleteControls(self);
                removeTableWrapperScrollListener(self);
                self.vars.listeners.forEach(function(l) {
                    l.disable();
                });
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                addTableWrapperScrollListener(self);
                self.vars.listeners.forEach(function(l) {
                    l.enable();
                });
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {},
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {},
            /**
             * @param col {integer} dataGrid column index, -1 resets sorting
             * @param order {string} sort order: 'ASC' for ascending, 'DESC' for descending
             */
            sort: function(col, order) {
                var self = this;
                if (self.vars.dataGrid) {
                    col = Number.parseInt(col, 10);
                    self.config.table.sort.col = col;
                    self.config.table.sort.order = order;
                    self.vars.dataGrid.sort(col, order);
                }
            },
            /** @param {integer} col column to sort by @param {string} value */
            setFilter: function(col, value) {
                var self = this;
                if (self.vars.dataGrid) {
                    self.vars.dataGrid.setFilter(col, value);
                }
            },
            /** @param {integer} col */
            clearFilter: function(col) {
                var self = this;
                if (self.vars.dataGrid) {
                    self.vars.dataGrid.clearFilter(col);
                }
            }

        }
    };
    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

shmi.pkg("visuals.controls");

shmi.visuals.controls.SlideIn = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'slide-in');
    shmi.def(this.config, 'class-name-container', 'container');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'template', 'default/slide-in');
    shmi.def(this.config, 'always-open-layouts', []);
    shmi.def(this.config, 'container-layouts', []);
    shmi.def(this.config, 'auto-layout', true);
    shmi.def(this.config, 'initial-mode', 'slide');
    shmi.def(this.config, 'content-template', null);
    shmi.def(this.config, 'auto-close', false);
    shmi.def(this.config, 'tab-limit', true);
    shmi.def(this.config, 'overlay-style', {
        background: "transparent",
        zIndex: 1
    });

    this.controls = [];
    this.state = {};
    this.state.open = false;
    this.forceOpen = false;
    this.containerMode = false;

    this.startup();
};

shmi.visuals.controls.SlideIn.prototype = {
    uiType: "slide-in",
    isContainer: true,
    events: ["open", "close"],
    getClassName: function() {
        return "SlideIn";
    },
    onEnable: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].enable();
        }

        var cur_layout = shmi.getCurrentLayout();
        if (this.config['always-open-layouts'].indexOf(cur_layout) !== -1) {
            this.forceOpen = true;
        } else {
            this.forceOpen = false;
        }

        if (this.config['initial-mode'] === "container") {
            this.enableContainerLayout();
        }

        if (this.config['auto-layout'] === true) {
            this.testContainerLayout();
        }

        if (this.forceOpen) {
            this.open();
        }
    },
    onDisable: function() {
        var self = this;
        this.close();
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].disable();
        }
        if (Array.isArray(self.openListeners)) {
            self.openListeners.forEach(function(l) {
                l.disable();
            });
            self.openListeners = [];
        }
    },
    onDelete: function() {
        if ((this.config['container-layouts'].length > 0) && (this.config['auto-layout'] === true)) {
            window.removeEventListener('visuals-layout-change', this.testContainerLayoutBind);
        }

        if ((this.config['always-open-layouts'].length > 0)) {
            window.removeEventListener('visuals-layout-change', this.testOpenLayoutBind);
        }
    },
    open: function() {
        var self = this;
        if (!this.state.open) {
            if (this.containerMode === false) {
                shmi.removeClass(this.element, 'closed');
                shmi.addClass(this.element, 'open');
            }

            if (self.config['tab-limit']) {
                // Enable element tabulator
                var tabulator = shmi.requires("visuals.tools.tabulator");
                self.tabLimit = tabulator.setTabParent(self.element);
            }

            if (self.config['auto-close']) {
                var openHandler = {
                    onPress: function(x, y, evt) {
                        if (!shmi.testParentChild(self.element, evt.target)) {
                            evt.preventDefault();
                            self.close();
                            self.openListeners.forEach(function(l) {
                                l.disable();
                            });
                            self.openListeners = [];
                        }
                    }
                };
                this.bgOverlay = document.createElement("DIV");
                this.bgOverlay.classList.add("overlay-background");

                this.bgOverlay.style.background = self.config["overlay-style"].background;
                this.bgOverlay.style.zIndex = self.config["overlay-style"].zIndex;
                this.bgOverlay.style.top = 0;
                this.bgOverlay.style.right = 0;
                this.bgOverlay.style.bottom = 0;
                this.bgOverlay.style.left = 0;
                this.bgOverlay.style.position = "fixed";

                document.body.insertBefore(this.bgOverlay, document.body.firstChild);
                this.openListeners = [
                    new shmi.visuals.io.MouseListener(this.bgOverlay, openHandler),
                    new shmi.visuals.io.TouchListener(this.bgOverlay, openHandler)
                ];
                this.openListeners.forEach(function(l) {
                    l.enable();
                });
            }

            this.state.open = true;
            this.fire("open", {});
        }
    },
    close: function() {
        var self = this;
        if (this.state.open) {
            if (this.containerMode === false) {
                shmi.removeClass(this.element, 'open');
                shmi.addClass(this.element, 'closed');
            }

            if (self.tabLimit) {
                self.tabLimit.unsetTabParent();
                self.tabLimit = null;
            }

            if (self.config['auto-close']) {
                if (Array.isArray(self.openListeners)) {
                    self.openListeners.forEach(function(l) {
                        l.disable();
                    });
                    self.openListeners = [];
                }
                if (this.bgOverlay) {
                    this.bgOverlay.parentNode.removeChild(this.bgOverlay);
                    this.bgOverlay = null;
                }
            }
            this.state.open = false;
            this.fire("close", {});
        }
    },
    toggle: function() {
        if (this.state.open) {
            this.close();
        } else {
            this.open();
        }
    },
    onRegister: function(onDone) {
        var self = this;

        self.contentElement = shmi.getUiElement("slide-in-content", self.element);

        if (self.contentElement && self.config["content-template"]) {
            var templateUrl = (self.config["content-template"].indexOf(shmi.c("RES_URL_PREFIX")) === 0) ?
                self.config["content-template"] :
                shmi.c("TEMPLATE_PATH") + self.config["content-template"] + shmi.c("TEMPLATE_EXT");

            shmi.loadResource(templateUrl, function(data, failed) {
                if (self.isDeleted()) {
                    return;
                }

                if (!failed) {
                    self.contentElement.innerHTML = data;
                    self.parseChildren(self.contentElement, onDone);
                } else {
                    onDone();
                }
            });
        } else if (self.contentElement) {
            self.parseChildren(self.contentElement, onDone);
        } else {
            onDone();
        }
    },
    onInit: function() {
        shmi.removeClass(this.element, 'open');
        shmi.addClass(this.element, 'closed');
        this.state.open = false;

        if (this.config['always-open-layouts'].length > 0) {
            this.testOpenLayoutBind = this.testOpenLayout.bind(this);
            window.addEventListener('visuals-layout-change', this.testOpenLayoutBind, false);
        }

        if (this.config['container-layouts'].length > 0) {
            if (this.config['auto-layout'] === true) {
                this.testContainerLayoutBind = this.testContainerLayout.bind(this);
                window.addEventListener('visuals-layout-change', this.testContainerLayoutBind, false);
            }
        }
    },
    testOpenLayout: function() {
        var cur_layout = shmi.getCurrentLayout();
        if (this.config['always-open-layouts'].indexOf(cur_layout) !== -1) {
            this.forceOpen = true;
            if (this.active) {
                this.open();
            }
        } else {
            this.forceOpen = false;
            if (this.active) {
                this.close();
            }
        }
    },
    enableContainerLayout: function() {
        if (this.containerMode === false) {
            this.containerMode = true;
            shmi.removeClass(this.element, this.config['class-name']);
            shmi.removeClass(this.element, 'open');
            shmi.removeClass(this.element, 'closed');
            shmi.addClass(this.element, this.config['class-name-container']);
            if (this.active) {
                this.open();
            }
        }
    },
    disableContainerLayout: function() {
        if (this.containerMode === true) {
            this.containerMode = false;
            shmi.removeClass(this.element, this.config['class-name-container']);
            shmi.addClass(this.element, this.config['class-name']);
            if (this.state.open) {
                shmi.addClass(this.element, 'open');
            } else {
                shmi.addClass(this.element, 'closed');
            }
            if (this.active) {
                this.close();
            }
        }
    },
    toggleContainerLayout: function() {
        if (this.containerMode === false) {
            this.containerMode = true;
            shmi.removeClass(this.element, this.config['class-name']);
            shmi.removeClass(this.element, 'open');
            shmi.removeClass(this.element, 'closed');
            shmi.addClass(this.element, this.config['class-name-container']);
            if (this.active) {
                this.open();
            }
        } else if (this.containerMode === true) {
            this.containerMode = false;
            shmi.removeClass(this.element, this.config['class-name-container']);
            shmi.addClass(this.element, this.config['class-name']);
            if (this.state.open) {
                shmi.addClass(this.element, 'open');
            } else {
                shmi.addClass(this.element, 'closed');
            }
            if (this.active) {
                this.close();
            }
        }
    },
    testContainerLayout: function() {
        var cur_layout = shmi.getCurrentLayout();
        if (this.config['container-layouts'].indexOf(cur_layout) !== -1) {
            if (this.containerMode === false) {
                this.containerMode = true;
                shmi.removeClass(this.element, this.config['class-name']);
                shmi.removeClass(this.element, 'open');
                shmi.removeClass(this.element, 'closed');
                shmi.addClass(this.element, this.config['class-name-container']);
                if (this.active) {
                    this.open();
                }
            }
        } else if (this.containerMode === true) {
            this.containerMode = false;
            shmi.removeClass(this.element, this.config['class-name-container']);
            shmi.addClass(this.element, this.config['class-name']);
            if (this.state.open) {
                shmi.addClass(this.element, 'open');
            } else {
                shmi.addClass(this.element, 'closed');
            }
            if (this.active) {
                this.close();
            }
        }
    },
    onLock: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].lock();
        }
    },
    onUnlock: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].unlock();
        }
    },
    onAddControl: function(options, callback) {
        var self = this,
            cm = shmi.requires("visuals.tools.controller"),
            controls = [],
            initToken = null;

        if (!self.initialized) {
            throw new Error("Control not initialized yet.");
        }

        if (!Array.isArray(options)) {
            options = [options];
        }
        options.forEach(function(option) {
            var control = shmi.createControl(option.ui, self.contentElement, option.config, "DIV", null, false);
            if (control !== null) {
                if (option.style && typeof option.style === "object") {
                    let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                    iter(option.style, (value, name) => {
                        control.element.style[name] = value;
                    });
                }
                controls.push(control);
            }
        });
        initToken = shmi.waitOnInit(controls, function() {
            var idx = -1,
                errors = [],
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken.unlisten();
                initToken = null;
            }

            options.forEach(function(opt, jdx) {
                var t = null,
                    control = controls[jdx];

                if (opt.controller) {
                    cm.create(opt.controller.name, opt.controller, control.getName());
                }
                if (self.isActive()) {
                    control.enable();
                }
                if (Array.isArray(opt.children) && opt.children.length) {
                    t = tm.createTask("child control");
                    t.run = function() {
                        control.addControl(opt.children, function(childErr, childOptions) {
                            if (childErr) {
                                errors.push(childErr);
                            }
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            if (tasks.length) {
                tl = tm.createTaskList(tasks, false);
                tl.onComplete = function() {
                    callback(errors.length ? errors[0] : null, controls);
                };
                tl.run();
            } else {
                callback(errors.length ? errors[0] : null, controls);
            }
        });
        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    }
};

shmi.extend(shmi.visuals.controls.SlideIn, shmi.visuals.core.BaseControl);

/** Exported UI-Actions **/
shmi.pkg("visuals.session");
shmi.visuals.session.userActions = shmi.visuals.session.userActions || {};

shmi.visuals.session.userActions['slide-in'] = function(params) {
    var owner = null; //arguments[4];
    if (params.length !== 2) {
        shmi.log("[UserAction] slide-in - needs two parameters", 2);
    } else {
        var slide_in = shmi.ctrl(params[1], owner);
        switch (params[0]) {
        case 'open':
            if (slide_in) {
                slide_in.open();
            } else {
                shmi.log("[UserAction] slide-in - control '" + params[1] + "' not found", 2);
            }
            break;
        case 'close':
            if (slide_in) {
                slide_in.close();
            } else {
                shmi.log("[UserAction] slide-in - control '" + params[1] + "' not found", 2);
            }
            break;
        case 'toggle':
            if (slide_in) {
                slide_in.toggle();
            } else {
                shmi.log("[UserAction] slide-in - control '" + params[1] + "' not found", 2);
            }
            break;
        default:
            shmi.log("[UserAction] unknown function '" + params[0] + "' configured", 3);
        }
    }
};

shmi.pkg("visuals.controls");
/**
 * Creates a new Slider control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.Slider = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'slider');
    shmi.def(this.config, 'template', 'default/slider');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'send-interval', 100);
    shmi.def(this.config, 'animation-duration', 100);
    shmi.def(this.config, 'orientation', "horizontal");
    shmi.def(this.config, 'continuous', true);
    shmi.def(this.config, 'min', 0);
    shmi.def(this.config, 'max', 100);
    shmi.def(this.config, 'step', 0);
    shmi.def(this.config, 'precision', -1);
    shmi.def(this.config, 'type', shmi.c("TYPE_FLOAT"));
    shmi.def(this.config, 'label', 'slider');
    shmi.def(this.config, 'unit-text', null);

    shmi.def(this.config, 'decimal-delimiter', ".");
    shmi.def(this.config, 'auto-min', true);
    shmi.def(this.config, 'auto-max', true);
    shmi.def(this.config, 'auto-step', true);
    shmi.def(this.config, 'auto-precision', true);
    shmi.def(this.config, 'auto-label', true);
    shmi.def(this.config, 'auto-unit-text', true);
    shmi.def(this.config, 'auto-type', true);

    this.value = 0;
    this.sendInterval = 0;
    this.lastSent = 0;
    this.active = false;
    this.initialized = false;
    this._subscriptionTargetId = null;
    this.resizeCallback = null;
    this._dragging = false;
    this._valueTimeout = -1;

    this.startup();
};

shmi.visuals.controls.Slider.prototype = {
    uiType: "slider",
    getClassName: function() {
        return "Slider";
    },
    events: ["change"],
    /**
     * Initializes the control
     *
     * @returns {unresolved}
     */
    onInit: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.initValueSettings(self);

        if (this.config.continuous === "false") {
            this.config.continuous = false;
        } else if (this.config.continuous === "true") {
            this.config.continuous = true;
        }

        this.handleElement = shmi.getUiElement('slider-handle', this.element);
        if (!this.handleElement) {
            shmi.log('[Slider] no slider-handle element provided', 3);
            return;
        }

        this.valueElement = shmi.getUiElement('value', this.element);

        this.backgroundElement = shmi.getUiElement('slider-background', this.element);
        if (!this.backgroundElement) {
            shmi.log('[Slider] no slider-background element provided', 3);
            return;
        }

        this.labelElement = shmi.getUiElement('slider-label', this.element);
        if (!this.labelElement) {
            shmi.log('[Slider] no slider-label element provided', 1);
        } else if (this.config.label) {
            this.labelElement.textContent = shmi.localize(this.config.label);
        }

        this.unitElement = shmi.getUiElement('unit', this.element);
        if (this.unitElement && this.config['unit-text']) {
            this.unitElement.textContent = shmi.localize(this.config['unit-text']);
        }

        if (this.config.action) {
            this.action = new shmi.visuals.core.UiAction(this.config.action, this);
        }

        if (this.config.orientation === "vertical") {
            shmi.addClass(this.element, 'vertical');
        }

        /* IO Functionality */
        this.width = 0;
        this.handleWidth = 0;
        this.movable = new shmi.visuals.gfx.Movable(this.handleElement);
        this.anim = new shmi.visuals.gfx.Animation(this.updateHandle.bind(this));

        var ioFuncs = {
            onPress: function() {
                shmi.addClass(self.handleElement, 'pressed');
                clearTimeout(self.dragTimeout);
                self._dragging = false;
            },
            onDrag: function(dx, dy, event) {
                event.preventDefault();
                if (!self._dragging) {
                    self._dragging = true;
                    if (self.config.continuous !== false) self.enableSend();
                }
                if (self.config.orientation === "vertical") {
                    if ((self.movable.ty + dy) > 0) {
                        self.movable.ty = 0;
                        self.movable.update();
                    } else if ((self.movable.ty + dy) < -self.width + self.handleWidth) {
                        self.movable.ty = -self.width + self.handleWidth;
                        self.movable.update();
                    } else {
                        self.movable.translate(0, dy);
                    }
                } else if ((self.movable.tx + dx) < 0) {
                    self.movable.tx = 0;
                    self.movable.update();
                } else if ((self.movable.tx + dx + self.handleWidth) > self.width) {
                    self.movable.tx = self.width - self.handleWidth;
                    self.movable.update();
                } else {
                    self.movable.translate(dx, 0);
                }
            },
            onRelease: function() {
                self.disableSend();
                shmi.removeClass(self.handleElement, 'pressed');
                self.dragTimeout = setTimeout(function() {
                    self._dragging = false;
                }, shmi.c("DRAG_TIMEOUT"));
            }
        };
        this.mouseListener = new shmi.visuals.io.MouseListener(this.handleElement, ioFuncs);
        this.touchListener = new shmi.visuals.io.TouchListener(this.handleElement, ioFuncs);

        /* listen for resize events */
        if (!this.resizeListenerAdded) {
            self.resizeCallback = function() {
                clearTimeout(self.resizeTO);
                self.resizeTO = setTimeout(function() {
                    self.updateSize();
                    self.setValue(self.value);
                }, shmi.c("ACTION_RETRY_TIMEOUT"));
            };
            window.addEventListener('resize', self.resizeCallback, false);
            window.addEventListener('visuals-layout-change', self.resizeCallback, false);
            this.resizeListenerAdded = true;
        }
    },
    /**
     * Recalculates control width and handle width
     * @returns {undefined}
     */
    updateSize: function() {
        this.width = (this.config.orientation === "vertical") ? this.backgroundElement.offsetHeight : this.backgroundElement.offsetWidth;
        this.handleWidth = (this.config.orientation === "vertical") ? this.handleElement.offsetHeight : this.handleElement.offsetWidth;
    },
    /**
     * Enables the Slider
     *
     */
    onEnable: function() {
        this.updateSize();
        if (this.config.item) {
            this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this, this.config['send-interval']);
        }
        this.mouseListener.enable();
        this.touchListener.enable();
    },
    /**
     * Disables the Slider
     *
     */
    onDisable: function() {
        if (this.config.item) {
            shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
        }
        this.mouseListener.disable();
        this.touchListener.disable();
        this.anim.stop();
    },
    /**
     * Locks the Slider
     *
     */
    onLock: function() {
        this.mouseListener.disable();
        this.touchListener.disable();
        shmi.addClass(this.element, 'locked');
    },
    /**
     * Unlocks the Slider
     *
     */
    onUnlock: function() {
        this.mouseListener.enable();
        this.touchListener.enable();
        shmi.removeClass(this.element, 'locked');
    },
    /**
     * Set property data of connected data-source item
     *
     * @param min - min value
     * @param max - max value
     * @param step - value stepping
     */
    onSetProperties: function(min, max, step) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.setProperties(self, arguments);
        if (typeof self.value === "number") {
            self.setValue(self.value);
        }
    },
    /**
     * Sets the current value
     *
     * @param value -  new value to set
     */
    onSetValue: function(value) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues"),
            vs = self.vars.valueSettings,
            oldValue = self.value,
            min = vs.min,
            max = vs.max;

        if (this._valueTimeout !== -1) {
            clearTimeout(this._valueTimeout);
            this._valueTimeout = -1;
        }

        if (this._dragging) {
            this._valueTimeout = setTimeout(function() {
                this.setValue(value);
            }.bind(this), shmi.c("ACTION_RETRY_TIMEOUT"));
            return;
        } else if (this.config.orientation === "vertical") {
            this.anim.setCurrentValue(this.movable.ty);
        } else {
            this.anim.setCurrentValue(this.movable.tx);
        }

        this.value = nv.applyInputSettings(value, self);

        if (this.valueElement) {
            var val_el_set = function() {
                self.valueElement.textContent = nv.formatOutput(value, self);
            };
            shmi.caf(self.raf_id);
            self.raf_id = shmi.raf(val_el_set);
        }

        if (!this._dragging) {
            var tx = null;
            if (max === min) {
                tx = (this.width - this.handleWidth);
            } else {
                tx = (this.value === undefined) ? 0 : ((this.value - min) / (max - min)) *
                (this.width - this.handleWidth);
            }

            if (this.config.orientation === "vertical") {
                tx *= -1;
            }
            this.anim.start(tx, this.config['animation-duration']);
        }
        if (self.value !== oldValue) {
            self.fire("change", { value: self.value });
        }
    },
    /**
     * Retrieves the current value
     *
     * @return value - current value
     */
    getValue: function() {
        return this.value;
    },
    /**
     * Updates handle position
     *
     * @param val - new position of handle
     */
    updateHandle: function(val) {
        if (isNaN(val)) {
            return;
        }
        if (this.config.orientation === "vertical") {
            this.movable.ty = val;
        } else {
            this.movable.tx = val;
        }
        this.movable.update();
    },
    /**
     * Enables sending of values
     *
     */
    enableSend: function() {
        clearInterval(this.sendInterval);
        this.sendInterval = setInterval(this.writeValue.bind(this), this.config['send-interval']);
    },
    /**
     * Writes current value to connected data-source item
     *
     * @param force - if true writes value even if unchanged
     */
    writeValue: function(force) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues"),
            vs = self.vars.valueSettings,
            min = vs.min,
            max = vs.max;

        var ds = (this.config.orientation === "vertical") ? -this.movable.ty : this.movable.tx,
            value = null;
        if (max === min) {
            value = max;
        } else {
            value = (ds * (max - min) / (this.width - this.handleWidth)) + min;
        }
        value = nv.applyInputSettings(value, self);

        if (this.valueElement) {
            var val_el_set = function() {
                self.valueElement.textContent = nv.formatOutput(value, self);
            };
            shmi.caf(self.raf_id);
            self.raf_id = shmi.raf(val_el_set);
        }
        if (force || (value !== this.lastsent)) {
            if (this.config.item) {
                shmi.visuals.session.ItemManager.writeValue(this.config.item, value);
            } else {
                this.value = value;
                this.fire("change", { value: value });
            }
            this.lastsent = value;

            if (this.action) {
                this.action.execute(this.value);
            }
        }
    },
    /**
     * Disables sending of values
     */
    disableSend: function() {
        clearInterval(this.sendInterval);
        this.writeValue(false);
    },
    setLabel: function(labelText) {
        var self = this;
        if (self.config['auto-label'] && self.labelElement) {
            self.labelElement.textContent = shmi.localize(labelText);
        }
    },
    setUnitText: function(unitText) {
        var self = this;
        if (self.config['auto-unit-text'] && self.unitElement) {
            self.unitElement.textContent = shmi.localize(unitText);
        }
    },
    onDelete: function() {
        var self = this;
        if (self.resizeCallback) {
            window.removeEventListener("resize", self.resizeCallback, false);
            window.removeEventListener("visuals-layout-change", self.resizeCallback, false);
        }
    }
};

shmi.extend(shmi.visuals.controls.Slider, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");
/**
 * TabPanel to execute UI actions
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param element - the base element of the control
 * @param config - configuration of the control
 */
shmi.visuals.controls.TabPanel = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    /*set default options */
    shmi.def(this.config, 'class-name', 'tab-panel');
    shmi.def(this.config, 'template', 'default/tab-panel');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'initial-view', 0);
    shmi.def(this.config, 'tab-labels', []);
    shmi.def(this.config, 'views', []);
    shmi.def(this.config, 'content-template', null);
    shmi.def(this.config, 'item', null);

    this.mouselistener = null;
    this.touchlistener = null;
    this.active = false;
    this.initialized = false;
    this._subscriptionTargetId = null;

    this.buttons = [];
    this.screen = null;
    this.controls = [];

    this.startup();
};

shmi.visuals.controls.TabPanel.prototype = {
    uiType: "tab-panel",
    events: ['navigate'],
    isContainer: true,
    getClassName: function() {
        return "TabPanel";
    },
    onRegister: function(onDone) {
        var self = this,
            initToken = null;

        self.screen_anchor = shmi.getUiElement('screen-anchor', self.element);
        if (!self.screen_anchor) {
            console.warn("[" + self.getClassName() + "]", "'screen-anchor' element missing from template, adding replacement");
            var screenAnchor = document.createElement("DIV");
            screenAnchor.setAttribute("class", "tab-content");
            self.element.appendChild(screenAnchor);
            self.screen_anchor = screenAnchor;
        }

        self.screen = shmi.createControl('screen', self.screen_anchor, {
            "name": "content",
            "no-views-content": "",
            "views": self.config.views,
            "template": self.config["content-template"],
            "initial-view": self.config['initial-view'],
            "delete-inactive-views": false,
            "item": self.config.item
        }, 'DIV');
        initToken = shmi.waitOnInit([self.screen], function() {
            var idx = -1;
            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken = null;
            }
            onDone();
        });
        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    },
    /**
     * Initializes the control.
     *
     * @param responseText - content of the loaded template
     */
    onInit: function() {
        var self = this,
            l = self.config["tab-labels"];

        function getClickCB(idx) {
            return function(e) {
                self.setView(idx);
                for (var i = 0; i < self.buttons.length; i++) {
                    shmi.removeClass(self.buttons[i].element, 'selected');
                }
                shmi.addClass(self.buttons[idx].element, 'selected');
            };
        }

        self.tabs_anchor = shmi.getUiElement('tabs-anchor', self.element);
        if (!self.tabs_anchor) {
            shmi.notify("${V_MISSING_UI_ELEMENT} 'tabs-anchor'", "${V_ERROR}");
        } else {
            self.screen.viewElements.forEach(function(ve, i) {
                self.buttons.push(
                    shmi.createControl('button', self.tabs_anchor, { label: l[i] || "view_" + (i + 1) }, 'DIV')
                );
                if (i === self.config['initial-view']) {
                    shmi.addClass(self.buttons[i].element, "selected");
                }
                self.buttons[i].listen('click', getClickCB(i));
                if (shmi.hasClass(self.element, 'small')) {
                    shmi.addClass(self.buttons[i].element, 'small');
                } else if (shmi.hasClass(self.element, 'large')) {
                    shmi.addClass(self.buttons[i].element, 'large');
                }
            });
        }
    },
    setView: function(index) {
        var self = this;
        if (!self.initialized || !self.screen.initialized) {
            return;
        }
        if (self.screen) {
            self.screen.setView(index);
            for (var i = 0; i < self.buttons.length; i++) {
                shmi.removeClass(self.buttons[i].element, 'selected');
            }
            shmi.addClass(self.buttons[index].element, 'selected');
        }
    },
    onEnable: function() {
        var self = this,
            im = shmi.requires("visuals.session.ItemManager"),
            h = im.getItemHandler();

        self.controls.forEach(function(c) {
            c.enable();
        });

        if (self.config.item) {
            h.setValue = function setValue(v) {
                if (shmi.isDesignerEditor()) {
                    return;
                }
                var idx = parseInt(v);
                if (!isNaN(idx) && (idx >= 0) && (idx < self.buttons.length)) {
                    self.buttons.forEach(function(b) {
                        shmi.removeClass(b.element, "selected");
                    });
                    shmi.addClass(self.buttons[idx].element, "selected");
                }
            };
            self._subscriptionTargetId = im.subscribeItem(self.config.item, h);
        }
    },
    onDisable: function() {
        var self = this;

        self.controls.forEach(function(c) {
            c.disable();
        });

        if (self._subscriptionTargetId) {
            self._subscriptionTargetId.unlisten();
            self._subscriptionTargetId = null;
        }
    },
    onSetValue: function() {

    },
    onLock: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].lock();
        }
        shmi.addClass(this.element, 'locked');
        shmi.log("[TabPanel] locked", 1);
    },
    onUnlock: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].unlock();
        }
        shmi.removeClass(this.element, 'locked');
        shmi.log("[TabPanel] unlocked", 1);
    },
    onAddControl: function(options, callback) {
        var self = this,
            controls = [],
            notView = false,
            offset = 0;

        if (!self.initialized) {
            throw new Error("Control not initialized yet.");
        }

        if (!Array.isArray(options)) {
            options = [options];
        }

        notView = options.some(function(option) {
            return (option.ui !== "view");
        });

        if (notView) {
            callback(new Error("All children must be of type 'view'"), controls);
            return;
        }

        function getClickCB(idx) {
            return function(e) {
                self.setView(idx);
                for (var i = 0; i < self.buttons.length; i++) {
                    shmi.removeClass(self.buttons[i].element, 'selected');
                }
                shmi.addClass(self.buttons[idx].element, 'selected');
            };
        }

        offset = self.screen.viewElements.length;

        options.forEach(function(option, idx) {
            var i = offset + idx,
                label = option.config.label || self.config["tab-labels"][i] || "View " + (i + 1);

            self.buttons.push(
                shmi.createControl('button', self.tabs_anchor, { label: label }, 'DIV')
            );
            if (i === self.config['initial-view']) {
                shmi.addClass(self.buttons[i].element, "selected");
            }
            self.buttons[i].listen('click', getClickCB(i));
            if (shmi.hasClass(self.element, 'small')) {
                shmi.addClass(self.buttons[i].element, 'small');
            } else if (shmi.hasClass(self.element, 'large')) {
                shmi.addClass(self.buttons[i].element, 'large');
            }
        });

        self.screen.addControl(options, callback);
    }
};

shmi.extend(shmi.visuals.controls.TabPanel, shmi.visuals.core.BaseControl);
shmi.registerControlType("tab-panel", shmi.visuals.controls.TabPanel, true);

shmi.pkg("visuals.controls");
/**
 * Creates a new Text Display control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.TextDisplay = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'text-display');
    shmi.def(this.config, 'template', 'default/text-display');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'precision', -1);
    shmi.def(this.config, 'type', shmi.c("TYPE_FLOAT"));
    shmi.def(this.config, 'unit-scale', 1);
    shmi.def(this.config, 'label', "text-display");
    shmi.def(this.config, 'unit-text', null);

    shmi.def(this.config, 'decimal-delimiter', ".");
    shmi.def(this.config, 'auto-precision', true);
    shmi.def(this.config, 'auto-label', true);
    shmi.def(this.config, 'auto-unit-text', true);
    shmi.def(this.config, 'auto-type', true);

    this.value = null;
    this.labelElement = null;
    this.valueElement = null;
    this.unitElement = null;

    this.initialized = false;
    this._subscriptionTargetId = null;

    this.active = false;
    this.vars = {
        conditional: null
    };

    this.startup();
};

shmi.visuals.controls.TextDisplay.prototype = {
    uiType: "text-display",
    getClassName: function() {
        return "TextDisplay";
    },
    /**
     * Initializes the control
     *
     * @returns {unresolved}
     */
    onInit: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.initValueSettings(self);

        this.labelElement = shmi.getUiElement('text-display-label', this.element);
        if (!this.labelElement) {
            shmi.log('[TextDisplay] no text-display-label element provided', 1);
        }
        this.valueElement = shmi.getUiElement('text-display-value', this.element);
        if (!this.valueElement) {
            shmi.log('[TextDisplay] no text-display-value element provided', 3);
            return;
        }
        this.unitElement = shmi.getUiElement('text-display-unit', this.element);
        if (!this.unitElement) {
            shmi.log('[TextDisplay] no text-display-unit element provided', 1);
        }
        /* all required elements found */
        shmi.log('[TextDisplay] -- all elements found --', 1);
        if (this.config['unit-text']) {
            if (this.unitElement) {
                this.unitElement.innerHTML = shmi.localize(this.config['unit-text']);
            }
        }
        if (this.config['label']) {
            if (this.labelElement) {
                this.labelElement.innerHTML = shmi.localize(this.config['label']);
            }
        }
        if (this.config['unit-scale']) {
            this.config['unit-scale'] = parseFloat(shmi.localize(this.config['unit-scale']));
        }
    },
    /**
     * Sets current value of Text Display
     *
     * @param value - new value to set
     */
    onSetValue: function(value, type) {
        var self = this;

        if (type === shmi.c("TYPE_STRING")) {
            self.value = value;
        } else if (isNaN(parseFloat(value))) {
            self.value = value;
        } else {
            self.value = parseFloat(value);
        }
        shmi.caf(self.requestID);
        self.requestID = shmi.raf(function() {
            self.asyncSet(type);
        });
    },
    /**
     * Sets properties for subscribed data
     *
     * @param min - min value
     * @param max - max value
     * @param step - value stepping
     */
    onSetProperties: function(min, max, step) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.setProperties(self, arguments);
    },
    /**
     * used to set the current value asynchronously
     */
    asyncSet: function(type) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        if (shmi.visuals.session.deactivateControls) {
            setTimeout(this.asyncSet.bind(this), shmi.c("ACTION_RETRY_TIMEOUT"));
        } else {
            var valText = nv.formatOutput(self.value, self);
            this.valueElement.textContent = valText;
        }
    },
    /**
     * Disable the Text Display
     *
     */
    onDisable: function() {
        var self = this;
        if (self.vars.conditional) {
            self.vars.conditional.disable();
        }

        if (this.config.item) {
            shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
        }
        shmi.log("[TextDisplay] disabled", 1);
    },
    /**
     * Enable the Text Display
     *
     */
    onEnable: function() {
        var self = this;
        if (self.vars.conditional !== null) {
            if (self.vars.conditional.item !== self.config.item) {
                self.vars.conditional.item = self.config.item;
            }
        } else if (self.config.item) {
            self.vars.conditional = shmi.createConditional(self.element, self.config.item);
        }

        if (self.vars.conditional) {
            self.vars.conditional.enable();
        }

        if (this.config.item) {
            this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
        } else if (this.config.action) {
            this.execute(this.config.action);
        }

        shmi.log("[TextDisplay] enabled", 1);
    },
    /**
     * Executes UI action
     *
     * @param action - comma separated list of UI actions
     */
    execute: function(action) {
        var index = 0;
        var actions = action.split(',');
        while (index < actions.length) {
            action = actions[index];

            if (action.indexOf('connect') === 0) {
                var params = action.split(':');
                if (params.length !== 3) {
                    shmi.log("[TextDisplay] action has unknown format", 3);
                    return;
                }
                var graph = shmi.ctrl("." + params[1]);
                if (!graph) {
                    shmi.log("[TextDisplay] '" + params[1] + "' not found in layout", 3);
                    return;
                }
                var self = this;
                shmi.waitOnInit(graph, function() {
                    graph.callbacks[params[2]] = self.setValue.bind(self);
                    self.setValue(graph.getValue(params[2]));
                });
            } else {
                shmi.log("TextDisplay] unknown action: " + this.config.action, 3);
            }
            index++;
        }
    },
    setLabel: function(labelText) {
        var self = this;
        if (self.config['auto-label'] && self.labelElement) {
            self.labelElement.textContent = shmi.localize(labelText);
        }
    },
    setUnitText: function(unitText) {
        var self = this;
        if (self.config['auto-unit-text'] && self.unitElement) {
            self.unitElement.textContent = shmi.localize(unitText);
        }
    }
};

shmi.extend(shmi.visuals.controls.TextDisplay, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");
/**
 * Creates a new Text Element control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.TextElement = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'text');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'text', '\u00A0');
    shmi.def(this.config, 'bold', false);
    shmi.def(this.config, 'italic', false);
    shmi.def(this.config, 'underline', false);
    shmi.def(this.config, 'position', 'left');
    shmi.def(this.config, 'font-family', 'inherit');
    shmi.def(this.config, 'font-size', 14);
    shmi.def(this.config, 'items', []);
    this.vars = {
        itemValues: [],
        tokens: [],
        radId: 0
    };

    this.startup();
};

shmi.visuals.controls.TextElement.prototype = {
    uiType: "text",
    getClassName: function() {
        return "TextElement";
    },
    /**
     * Initializes the control
     *
     * @returns {undefined}
     */
    onInit: function() {
        if (this.config.text) {
            this.element.textContent = shmi.localize(this.config.text);
            if (this.config.bold) {
                shmi.addClass(this.element, "text-bold");
            }
            if (this.config.italic) {
                shmi.addClass(this.element, "text-italic");
            }
            if (this.config.underline) {
                shmi.addClass(this.element, "text-underline");
            }
            if (this.config.position === "left") {
                shmi.addClass(this.element, "text-left");
            }
            if (this.config.position === "center") {
                shmi.addClass(this.element, "text-center");
            }
            if (this.config.position === "right") {
                shmi.addClass(this.element, "text-right");
            }
            if (this.config["font-family"]) {
                this.element.style.fontFamily = this.config["font-family"];
            }
            if (this.config["font-size"]) {
                var preparedSize = this.config["font-size"] + "px";
                this.element.style.fontSize = preparedSize;
            }
        }
        shmi.log("[TextElement] inited", 1);
    },
    onEnable: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues"),
            im = shmi.requires("visuals.session.ItemManager");

        if (Array.isArray(self.config.items)) {
            self.config.items.forEach(function(item, idx) {
                var h = im.getItemHandler(),
                    tok = null;

                self.vars.itemValues.push({
                    value: null,
                    unit: null,
                    label: null
                });

                h.setValue = function setValue(v, t, n) {
                    var handlerItem = im.getItem(n),
                        unit = null;

                    if (handlerItem) {
                        if (handlerItem.adapter && handlerItem.adapter.unitText) {
                            unit = handlerItem.adapter.unitText;
                        } else if (typeof handlerItem.unit === "string") {
                            unit = handlerItem.unit;
                        }
                        self.vars.itemValues[idx] = {
                            value: nv.formatNumber(handlerItem.readValue(true), {
                                "precision": handlerItem.digits,
                                "unit": handlerItem.unit,
                                "show-unit": false
                            }),
                            unit: unit,
                            label: handlerItem.labelToken
                        };
                        self.updateText();
                    }
                };
                tok = im.subscribeItem(item, h);
                self.vars.tokens.push(tok);
            });
        }
    },
    onDisable: function() {
        var self = this;
        self.vars.tokens.forEach(function(t) {
            t.unlisten();
        });
        self.vars.tokens = [];
    },
    updateText: function() {
        var self = this;

        shmi.caf(self.vars.rafId);
        self.vars.rafId = shmi.raf(function() {
            if (!self.isActive()) {
                return;
            }
            self.element.textContent = shmi.evalString(shmi.localize(self.config.text), {
                items: self.vars.itemValues
            });
        });
    }
};

shmi.extend(shmi.visuals.controls.TextElement, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");

/**
 * TextElement2 Date
 *
 * control to display time and date strings
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "text2 text2-date",
 *     "name": null,
 *     "dateformat": "$DD-$MM-$YYYY",
 *     "invalid-text": "-",
 *     "convert-to-utc": false
 * }
 *
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * dateformat {string}: String to format the output timestamp
 * invalid-text {string}: text to display when no valid date is set (value < 0)
 * convert-to-utc {boolean}: adds UTC offset to retrieved value to convert local dates to UTC
 *
 */

shmi.visuals.controls.TextElement2Date = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, "class-name", "text2 text2-date");
    shmi.def(this.config, "text", "\u00A0");
    shmi.def(this.config, "name", null);
    shmi.def(this.config, "dateformat", "$YYYY-$MM-$DD, $HH:$mm:$ss");
    shmi.def(this.config, "invalid-text", "---");
    shmi.def(this.config, "convert-to-utc", false);

    this.subscriptionId = null;
    this.localeStr = null;
    this.invalidText = null;

    this.value = null;

    this.startup();
};

shmi.visuals.controls.TextElement2Date.prototype = {
    uiType: "text2-date",
    getClassName: function() {
        return "TextElement2Date";
    },
    onInit: function() {
        this.localeStr = shmi.localize(this.config["dateformat"]);
        this.invalidText = shmi.localize(this.config["invalid-text"]);
        this.element.textContent = this.invalidText;
    },
    onSetValue: function(value) {
        shmi.caf(this.RAF_ID);
        this.RAF_ID = shmi.raf(function() {
            value = parseInt(value);
            var displayedStr = null,
                format = shmi.requires("visuals.tools.date.formatDateTime");
            if (value > 0) {
                displayedStr = format(value, { datestring: this.localeStr, utc: this.config["convert-to-utc"] });
            } else {
                displayedStr = this.invalidText;
            }
            this.element.textContent = displayedStr;
        }.bind(this));
    },
    onEnable: function() {
        if (this.config.item) {
            this.subscriptionId = this.subscribeItem(this.config.item);
        }
    },
    onDisable: function() {
        if (this.config.item) {
            this.unsubscribeItem(this.config.item, this.subscriptionId);
        }
    },
    onLock: function() {
    },
    onUnlock: function() {
    }
};

shmi.extend(shmi.visuals.controls.TextElement2Date, shmi.visuals.core.BaseControl);

shmi.visuals.controls.TextElement2 = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'text2');
    shmi.def(this.config, 'text', '\u00A0');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'precision', -1);
    shmi.def(this.config, 'options', []);
    shmi.def(this.config, 'pattern', null);
    shmi.def(this.config, 'type', shmi.c("TYPE_STRING"));

    shmi.def(this.config, 'decimal-delimiter', ".");
    shmi.def(this.config, 'auto-precision', true);
    shmi.def(this.config, 'auto-type', true);
    shmi.def(this.config, 'value-as-tooltip', false);
    shmi.def(this.config, 'item', null);

    this.uiType = "text2";
    this.subscriptionId = null;

    this.value = null;

    this.startup();
};

shmi.visuals.controls.TextElement2.prototype = {
    uiType: "text2",
    getClassName: function() {
        return "TextElement2";
    },
    onInit: function() {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues"),
            localizedText = null;

        nv.initValueSettings(self);

        if (this.config.text) {
            localizedText = shmi.localize(this.config.text);
            this.element.textContent = localizedText;
            if (self.config["value-as-tooltip"]) {
                self.setTooltip(localizedText);
            }
        }
    },
    onSetValue: function(value) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        shmi.caf(this.RAF_ID);
        this.RAF_ID = shmi.raf(function() {
            var localizedText = null;
            if ((this.config.options !== undefined) && Array.isArray(this.config.options) && (this.config.options.length > 0)) {
                /*
                 "options" : [
                    { "label" : "True",  "value" : 0 },
                    { "label" : "False", "value" : 1 },
                    { "label" : "=",     "value" : 2 },
                    { "label" : ">",     "value" : 3 },
                    { "label" : ">=",    "value" : 4 },
                    { "label" : "<",     "value" : 5 },
                    { "label" : "<=",    "value" : 6 },
                    { "label" : "&",     "value" : 7 },
                    { "label" : "|",     "value" : 8 },
                    { "label" : "Null",  "value" : 9 }
                ]
                 */
                const option = this.config.options.find((o) => {
                    if (Number.isSafeInteger(value) && Number.isSafeInteger(o.mask) && Number.isSafeInteger(o.value)) {
                        return (BigInt(value) & BigInt(o.mask)) === BigInt(o.value);
                    }

                    return o.value === value;
                });

                if (!option) {
                    this.element.textContent = value;
                } else {
                    localizedText = shmi.localize(option.label);
                    this.element.textContent = localizedText;
                }
            } else if ((typeof self.config.pattern === "string") && self.config.pattern.trim().length) {
                localizedText = shmi.localize(shmi.evalString(self.config.pattern, { VALUE: value }));
                self.element.textContent = localizedText;
            } else {
                localizedText = nv.formatOutput(value, self);
                this.element.textContent = localizedText;
            }

            if (self.config["value-as-tooltip"]) {
                if (typeof localizedText === "string" && localizedText !== "") {
                    self.setTooltip(String(localizedText));
                } else {
                    self.setTooltip(self.getTooltip());
                }
            }
        }.bind(this));
    },
    /**
     * Sets properties for subscribed data
     *
     * @param min - min value
     * @param max - max value
     * @param step - value stepping
     */
    onSetProperties: function(min, max, step) {
        var self = this,
            nv = shmi.requires("visuals.tools.numericValues");

        nv.setProperties(self, arguments);
    },
    onEnable: function() {
        if (this.config.item) {
            this.subscriptionId = this.subscribeItem(this.config.item);
        }
    },
    onDisable: function() {
        if (this.config.item) {
            this.unsubscribeItem(this.config.item, this.subscriptionId);
        }
    }
};

shmi.extend(shmi.visuals.controls.TextElement2, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");
/**
 * Creates a new Toggle Button control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.ToggleButton = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'toggle-button');
    shmi.def(this.config, 'template', 'default/toggle-button');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'on-value', 1);
    shmi.def(this.config, 'off-value', 0);
    shmi.def(this.config, 'confirm-off-text', '${V_CONFIRM_OFF}');
    shmi.def(this.config, 'confirm-on-text', '${V_CONFIRM_ON}');
    shmi.def(this.config, 'confirm-on', false);
    shmi.def(this.config, 'confirm-off', false);
    shmi.def(this.config, 'on-label', "on");
    shmi.def(this.config, 'off-label', "off");
    shmi.def(this.config, 'on-icon-src', null);
    shmi.def(this.config, 'on-icon-class', null);
    shmi.def(this.config, 'off-icon-class', null);
    shmi.def(this.config, 'off-icon-src', null);
    shmi.def(this.config, 'on-icon-title', null);
    shmi.def(this.config, 'off-icon-title', null);
    shmi.def(this.config, 'auto-label', true);
    shmi.def(this.config, 'show-text', true);
    shmi.def(this.config, 'show-icon', false);
    shmi.def(this.config, 'on-action', []);
    shmi.def(this.config, 'off-action', []);

    if (typeof this.config['confirm-on'] === "string") {
        this.config['confirm-on'] = this.config['confirm-on'] === "true";
    }
    if (typeof this.config['confirm-off'] === "string") {
        this.config['confirm-off'] = this.config['confirm-off'] === "true";
    }

    this.value = 0;
    this.labelElement = null;
    this.alternatives = null;
    this.listeners = [];
    this.initialized = false;
    this.active = false;
    this._subscriptionTargetId = null;
    this.onAction = null;
    this.offAction = null;

    this.startup();
};

shmi.visuals.controls.ToggleButton.prototype = {
    uiType: "toggle-button",
    getClassName: function() {
        return "ToggleButton";
    },
    events: ["change"],
    /**
     * Initializes the control
     *
     * @returns {unresolved}
     */
    onInit: function() {
        var self = this,
            core = shmi.requires("visuals.core");

        this.value = this.config['off-value'];

        this.alternatives = [];
        this.offstate = shmi.getUiElement('off-state', this.element);
        this.onstate = shmi.getUiElement('on-state', this.element);
        if (this.offstate && this.onstate) {
            this.alternatives.push(this.offstate);
            this.alternatives.push(this.onstate);
        }
        if (this.alternatives.length !== 2) {
            shmi.log("[ToggleButton] " + this.alternatives.length + ' state elements provided', 1);
            return;
        }
        this.offLabel = shmi.getUiElement('label', this.alternatives[0]);
        if (this.offLabel) {
            if (this.config['off-label']) this.offLabel.textContent = shmi.localize(this.config['off-label']);
        }
        this.onLabel = shmi.getUiElement('label', this.alternatives[1]);
        if (this.onLabel) {
            if (this.config['on-label']) this.onLabel.textContent = shmi.localize(this.config['on-label']);
        }
        /* all required elements found */
        if (Number(this.value) === 0) {
            this.alternatives[1].style.display = 'none';
        } else {
            this.alternatives[0].style.display = 'none';
        }

        this.onIcon = shmi.getUiElement('toggle-button-icon', this.onstate);
        var icnDiv;
        if (this.onIcon) {
            if (this.config['on-icon-class']) {
                if (this.onIcon.tagName === "IMG") {
                    /* switch img element for div when icon-class and no icon-src is configured */
                    icnDiv = document.createElement('div');
                    shmi.addClass(icnDiv, "icon");
                    this.onIcon.parentNode.insertBefore(icnDiv, this.onIcon);
                    this.onIcon.parentNode.removeChild(this.onIcon);
                    this.onIcon = icnDiv;
                }
                var onIconClass = this.config['on-icon-class'].trim().split(" ");
                onIconClass.forEach(function(iClass) {
                    shmi.addClass(self.onIcon, iClass);
                });
            } else if (this.config['on-icon-src']) {
                if (this.onIcon.tagName === "IMG") {
                    this.onIcon.setAttribute('src', this.config['on-icon-src']);
                } else {
                    console.log(this.uiType, "toggle-button-icon element is not an img-tag!");
                }
            }
        }
        this.offIcon = shmi.getUiElement('toggle-button-icon', this.offstate);
        if (this.offIcon) {
            if (this.config['off-icon-class']) {
                if (this.offIcon.tagName === "IMG") {
                    /* switch img element for div when icon-class and no icon-src is configured */
                    icnDiv = document.createElement('div');
                    shmi.addClass(icnDiv, "icon");
                    this.offIcon.parentNode.insertBefore(icnDiv, this.offIcon);
                    this.offIcon.parentNode.removeChild(this.offIcon);
                    this.offIcon = icnDiv;
                }
                var offIconClass = this.config['off-icon-class'].trim().split(" ");
                offIconClass.forEach(function(iClass) {
                    shmi.addClass(self.offIcon, self.config['off-icon-class']);
                });
            } else if (this.config['off-icon-src']) {
                if (this.offIcon.tagName === "IMG") {
                    this.offIcon.setAttribute('src', this.config['off-icon-src']);
                } else {
                    console.log(this.uiType, "toggle-button-icon element is not an img-tag!");
                }
            }
        }

        if (this.config["show-icon"] && this.config["show-text"]) {
            shmi.addClass(this.element, "icon-and-text");
        } else if (this.config["show-icon"]) {
            shmi.addClass(this.element, "icon-only");
        }

        var tbFuncs = {};
        tbFuncs.onPress = function() {
            shmi.addClass(this.element, 'pressed');
        }.bind(this);
        tbFuncs.onClick = function() {
            if (this.config['confirm-on'] || this.config['confirm-off']) {
                var on = (this.value === this.config['on-value']);
                if (on) {
                    if (this.config['confirm-off']) {
                        shmi.confirm(this.config['confirm-off-text'], function(conf) {
                            if (conf) {
                                this.toggle();
                            }
                        }.bind(this));
                    } else {
                        this.toggle();
                    }
                } else if (this.config['confirm-on']) {
                    shmi.confirm(this.config['confirm-on-text'], function(conf) {
                        if (conf) {
                            this.toggle();
                        }
                    }.bind(this));
                } else {
                    this.toggle();
                }
            } else {
                this.toggle();
            }
        }.bind(this);
        tbFuncs.onRelease = function() {
            shmi.log("[ToggleButton] button released", 0);
            shmi.removeClass(this.element, 'pressed');
        }.bind(this);
        this.listeners.push(new shmi.visuals.io.MouseListener(this.offstate, tbFuncs));
        this.listeners.push(new shmi.visuals.io.TouchListener(this.offstate, tbFuncs));
        this.listeners.push(new shmi.visuals.io.MouseListener(this.onstate, tbFuncs));
        this.listeners.push(new shmi.visuals.io.TouchListener(this.onstate, tbFuncs));
        /* all required elements found */
        shmi.log('[ToggleButton] -- all elements found --', 1);

        /* reset on- and off-values in case they are set and only an event is set */
        if (!this.config.item && this.config.event) {
            this.config['on-value'] = 1;
            this.config['off-value'] = 0;
        }

        if (Array.isArray(self.config["on-action"]) && self.config["on-action"].length) {
            self.onAction = new core.UiAction(self.config["on-action"], self);
        }

        if (Array.isArray(self.config["off-action"]) && self.config["off-action"].length) {
            self.offAction = new core.UiAction(self.config["off-action"], self);
        }
    },
    /**
     * Toggles the state of the Toggle Button
     *
     */
    toggle: function() {
        var self = this,
            im = shmi.requires("visuals.session.ItemManager"),
            toggleOn = self.value !== self.config["on-value"];

        if (self.config.item) {
            if (self.value === self.config['on-value']) {
                im.writeValue(self.config.item, self.config['off-value']);
            } else {
                im.writeValue(self.config.item, self.config['on-value']);
            }
        } else if (self.value === self.config['off-value']) {
            self.setValue(self.config['on-value']);
        } else {
            self.setValue(self.config['off-value']);
        }

        if (toggleOn && self.onAction) {
            self.onAction.execute();
        } else if (!toggleOn && self.offAction) {
            self.offAction.execute();
        }
    },
    /**
     * Gets the best tooltip value for the buttons current state.
     *
     * @override
     */
    getTooltip: function() {
        var superTooltip = shmi.visuals.core.BaseControl.prototype.getTooltip.call(this);

        if (superTooltip) {
            return superTooltip;
        } else if (this.value === this.config['off-value']) {
            if (shmi.objectHasOwnProperty(this.config, "off-tooltip")) {
                return this.config['off-tooltip'];
            } else if (shmi.objectHasOwnProperty(this.config, "off-icon-title")) {
                return this.config['off-icon-title'];
            }
        } else if (this.value === this.config['on-value']) {
            if (shmi.objectHasOwnProperty(this.config, "on-tooltip")) {
                return this.config['on-tooltip'];
            } else if (shmi.objectHasOwnProperty(this.config, "on-icon-title")) {
                return this.config['on-icon-title'];
            }
        }

        return null;
    },
    /**
     * Sets the current value of the Toggle Button
     *
     * @param value - new value to set
     */
    onSetValue: function(value) {
        var old_value = this.value;
        this.value = parseFloat(value);
        if (this.value === this.config['off-value']) {
            this.alternatives[1].style.display = 'none';
            this.alternatives[0].style.display = '';
        } else {
            this.alternatives[0].style.display = 'none';
            this.alternatives[1].style.display = '';
        }
        if (this.value !== old_value) {
            this.setTooltip(this.getTooltip());
            this.fire("change", { value: this.value });
        }
    },
    /**
     * Retrieves the current value ot the Toggle Button
     *
     * @return value - current value
     */
    getValue: function() {
        return this.value;
    },
    /**
     * Enables the Toggle Button
     *
     */
    onEnable: function() {
        if (this.config.item) {
            this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
        }
        this.listeners.forEach(function(lst) {
            lst.enable();
        });
        shmi.log('[ToggleButton] enabled', 1);
    },
    /**
     * Disables the Toggle Button
     *
     */
    onDisable: function() {
        if (this.config.item) {
            shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
        }
        this.listeners.forEach(function(lst) {
            lst.disable();
        });
        shmi.log('[ToggleButton] disabled', 1);
    },
    /**
     * Locks the Toggle Button
     *
     */
    onLock: function() {
        for (var i = 0; i < this.listeners.length; i++) {
            this.listeners[i].disable();
        }
        shmi.addClass(this.element, 'locked');
        shmi.log('[ToggleButton] locked', 1);
    },
    /**
     * Unlocks the Toggle Button
     *
     */
    onUnlock: function() {
        for (var i = 0; i < this.listeners.length; i++) {
            this.listeners[i].enable();
        }
        shmi.removeClass(this.element, 'locked');
        shmi.log('[ToggleButton] unlocked', 1);
    },
    /* Stub for Subscriber<Item> Interface */
    onSetProperties: function() {

    },
    setLabel: function(labelText) {
        var self = this;

        if (self.config['auto-label']) {
            if (this.onLabel) {
                this.onLabel.textContent = shmi.localize(labelText);
            }
            if (this.offLabel) {
                this.offLabel.textContent = shmi.localize(labelText);
            }
        }
    }
};

shmi.extend(shmi.visuals.controls.ToggleButton, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls");
/**
 * Creates a new Toggle Display control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HTMLElement} element root element of the control
 * @param {Object} config control configuration
 */
shmi.visuals.controls.ToggleDisplay = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'class-name', 'toggle-display');
    shmi.def(this.config, 'template', 'default/toggle-display');
    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'labels', []);
    shmi.def(this.config, 'conditions', []);

    this.value = 0;
    this.labelElement = null;
    this.alternatives = null;
    this.currentState = 0;
    this.initialized = false;
    this.active = false;
    this._subscriptionTargetId = null;
    this.vars = {
        conditional: null
    };

    this.startup();
};

shmi.visuals.controls.ToggleDisplay.prototype = {
    uiType: "toggle-display",
    getClassName: function() {
        return "ToggleDisplay";
    },
    /**
     * Initializes the control
     *
     * @returns {unresolved}
     */
    onInit: function() {
        this.alternatives = shmi.getUiElements('state', this.element);
        if (this.alternatives.length === 0) {
            shmi.log("[ToggleDisplay] " + this.alternatives.length + ' state elements provided', 2);
            return;
        }
        this.labels = [];
        for (var i = 0; i < this.alternatives.length; i++) {
            var label = shmi.getUiElement('label', this.alternatives[i]);
            if (label) {
                if (this.config.labels && (this.config.labels[i] !== undefined)) {
                    label.textContent = shmi.localize(this.config.labels[i]);
                } else {
                    shmi.log("[ToggleDisplay] no label configured for state " + i, 2);
                }
            }
            this.labels.push(label);
        }
        /* all required elements found */
        for (i = 0; i < this.alternatives.length; i++) {
            shmi.addClass(this.alternatives[i], 'hidden');
        }
        shmi.removeClass(this.alternatives[0], 'hidden');

        /* all required elements found */
        shmi.log('[ToggleDisplay] -- all elements found --', 1);
    },
    /**
     * Sets the current value of the Toggle Display
     *
     * @param value - new value to set
     */
    onSetValue: function(value) {
        this.value = parseFloat(value);
        shmi.caf(this.requestID);
        this.requestID = shmi.raf(function() {
            for (var i = 0; i < this.config.conditions.length; i++) {
                if (eval(this.config.conditions[i].replace(/%VALUE%/g, value))) {
                    this.setSelected(i);
                    break;
                }
            }
        }.bind(this));
    },
    setSelected: function(index) {
        if (index === this.currentState) {
            return;
        }
        shmi.addClass(this.alternatives[this.currentState], 'hidden');
        shmi.removeClass(this.alternatives[index], 'hidden');
        this.currentState = index;
    },
    /**
     * Retrieves the current value ot the Toggle Display
     *
     * @return value - current value
     */
    getValue: function() {
        return this.value;
    },
    /**
     * Enables the Toggle Display
     *
     */
    onEnable: function() {
        var self = this;
        if (self.vars.conditional !== null) {
            if (self.vars.conditional.item !== self.config.item) {
                self.vars.conditional.item = self.config.item;
            }
        } else if (self.config.item) {
            self.vars.conditional = shmi.createConditional(self.element, self.config.item);
        }

        if (self.vars.conditional) {
            self.vars.conditional.enable();
        }

        if (this.config.item) {
            this._subscriptionTargetId = shmi.visuals.session.ItemManager.subscribeItem(this.config.item, this);
        }
        shmi.log('[ToggleDisplay] enabled', 1);
    },
    /**
     * Disables the Toggle Display
     *
     */
    onDisable: function() {
        var self = this;
        if (self.vars.conditional) {
            self.vars.conditional.disable();
        }

        if (this.config.item) {
            shmi.visuals.session.ItemManager.unsubscribeItem(this.config.item, this._subscriptionTargetId);
        }
        shmi.log('[ToggleDisplay] disabled', 1);
    },
    /**
     * Locks the Toggle Display
     *
     */
    onLock: function() {
        shmi.addClass(this.element, 'locked');
        shmi.log('[ToggleDisplay] locked', 1);
    },
    /**
     * Unlocks the Toggle Display
     *
     */
    onUnlock: function() {
        shmi.removeClass(this.element, 'locked');
        shmi.log('[ToggleDisplay] unlocked', 1);
    }
};

shmi.extend(shmi.visuals.controls.ToggleDisplay, shmi.visuals.core.BaseControl);

shmi.pkg("visuals.controls.TrendDisplay");

/**
 * Creates a new TrendDisplay control
 *
 * @constructor
 * @extends shmi.visuals.core.BaseControl
 * @param {HtmlElement} element element in DOM to contain control
 * @param {{}} configuration
 */

shmi.visuals.controls.TrendDisplay = function(element, configuration) {
    "use strict";

    /**
     * @typedef {shmi.visuals.controls.TrendDisplay} SHMI_TD
     */

    /**
     * @typedef {{source:SHMI_TD.Component,type:string,detail:*}} SHMI_TD.Broadcast
     */

    /** */
    var dataProvider = new shmi.visuals.controls.TrendDisplay.Data(this);

    var onBroadcast = new shmi.visuals.controls.TrendDisplay.Messaging();

    var configured = new shmi.visuals.controls.TrendDisplay.Promise();

    var preparingComponents = 0;

    // merge provided configuration with configuration in attributes of element
    var config = configuration || {};

    // apply defaults on basic properties of configuration
    shmi.def(config, "class-name", "trend-display");
    shmi.def(config, "template", "default/trend-display/base");
    shmi.def(config, "name", null);
    shmi.def(config, "styleId", null); //designer style property

    /**
     * registry of this control's discovered components
     *
     * @type {Object.<string,Array.<shmi.visuals.controls.TrendDisplay.Component>>}
     * @private
     */
    Object.defineProperty(this, "element", {
        value: element
    });

    /**
     * Provides access on all available trend data.
     *
     * @property data
     * @type {shmi.visuals.controls.TrendDisplay.Data}
     */
    Object.defineProperty(this, "data", {
        value: dataProvider
    });

    /**
     * Delivers time of having started/loaded current trend display control.
     *
     * @property tStart
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "tStart", {
        value: shmi.getServerTime()
    });

    /**
     * Provides access on this controls configuration.
     *
     * @note This configuration isn't valid unless promise in property
     *       isConfigured has been resolved.
     *
     * @property config
     * @type {shmi.visuals.controls.TrendDisplay.Configuration}
     */
    Object.defineProperty(this, "config", {
        get: function() {
            return config;
        },
        set: function(value) {
            if (value instanceof shmi.visuals.controls.TrendDisplay.Configuration) {
                if (!config.isConfigured || !value.invalid) {
                    config = value;

                    configured.resolve(config);
                }
            }
        }
    });

    /**
     * Manages listeners for locally broadcasting messages in cotnext of current
     * trend display control.
     *
     * @property onBroadcast
     * @type {shmi.visuals.controls.TrendDisplay.Messaging}
     */
    Object.defineProperty(this, "onBroadcast", {
        value: onBroadcast
    });

    /**
     * Provides promise resolved when configuration is available.
     *
     * @property isConfigured
     * @type {shmi.visuals.controls.TrendDisplay.Promise}
     */
    Object.defineProperty(this, "isConfigured", {
        value: configured
    });

    /**
     * Manages state of preparing control.
     *
     * @property isPreparing
     * @type {boolean}
     */
    Object.defineProperty(this, "isPreparing", {
        get: function() {
            return preparingComponents > 0;
        },
        set: function(state) {
            if (state) {
                if (!preparingComponents) {
                    shmi.addClass(this.element, "preparing");
                    this.broadcast(this, "preparation-started");
                }

                preparingComponents++;
            } else {
                preparingComponents--;

                if (preparingComponents === 0) {
                    shmi.removeClass(this.element, "preparing");
                    this.broadcast(this, "preparation-finished");
                }

                if (preparingComponents < 0) {
                    preparingComponents = 0;
                }
            }
        }
    });

    /**
     * Lists registered components discovered in context of current control.
     *
     * @type {Object.<string,Array.<shmi.visuals.controls.TrendDisplay.Component>>}
     * @private
     */
    this._components = {};

    /**
     * Delivers array of discovered sub-components.
     *
     * @property controls
     * @type {shmi.visuals.core.BaseControl[]}
     * @readonly
     */
    this.controls = [];

    this.parseAttributes();

    // finally call the super class to run some startup routines
    this.startup(); // loads config, template, etc.
};

shmi.visuals.controls.TrendDisplay.iconDimensions = {
    height: 16,
    width: 16
};

shmi.visuals.controls.TrendDisplay.prototype = {

    uiType: "trend-display",
    isContainer: true,
    getClassName: function() {
        "use strict";

        return "TrendDisplay";
    },

    _manageResizeListeners: function(enable) {
        var self = this,
            timer = null;

        if (!this._onResize) {
            this._onResize = function() {
                if (timer) {
                    window.clearTimeout(timer);
                    timer = null;
                }

                timer = window.setTimeout(function() {
                    timer = null;
                    self.broadcast(self, "resize");
                }, 100);
            };
        }

        if (enable) {
            window.addEventListener('resize', this._onResize, false);
            window.addEventListener('visuals-layout-change', this._onResize, false);
        } else {
            window.removeEventListener('resize', this._onResize);
            window.removeEventListener('visuals-layout-change', this._onResize);
        }
    },

    /**
     * Initializes control.
     *
     */
    onRegister: function(onDone) {
        var self = this;

        var TD = shmi.visuals.controls.TrendDisplay,
            Promise = TD.Promise;

        // set type name for styling
        if (self.config.display && self.config.display.type && (self.config.display.type.toLowerCase() === "multilane" || self.config.display.type.toLowerCase() === "multi")) {
            shmi.addClass(self.element, "swim-lane");
        }

        self.isPreparing = "initializing-base";

        // register some broadcast handler (high priority is for adopting
        // changed configuration as early as possible)
        self.onBroadcast.push([1000, this._onBroadcastEarly, this]);
        self.onBroadcast.push([-1000, this._onBroadcastLate, this]);

        Promise.resolve(new shmi.visuals.controls.TrendDisplay.Configuration(this).update(this.config).then(function(config) {
            self.config = config;
            if (self.isDeleted()) {
                return self;
            }
            // start discovering actually available components of control
            self.parseChildren(self.element, function(controls) {
                self.discoverComponents(controls);
                controls.forEach(function(c) {
                    c.fire("discover", {
                        trendDisplay: self
                    });
                });
                onDone();
            });

            return self;
        }));
    },

    onInit: function() {
        "use strict";

        var self = this,
            TD = shmi.visuals.controls.TrendDisplay;

        // enable resize listeners
        self._manageResizeListeners(true);
        // create virtual items for providing scalar values of trend display
        var items = new TD.VirtualItems(self);
        Object.defineProperty(self, "items", {
            value: items
        });
        self.isPreparing = false;
    },

    /**
     * Discovers components of current TrendDisplay control.
     *
     * This method might be invoked multiple times to discover recently
     * undiscovered components.
     *
     * @return {shmi.visuals.controls.TrendDisplay} fluent interface support
     */
    discoverComponents: function(controls) {
        "use strict";

        var self = this;

        controls.forEach(function(c) {
            self.addComponent(c);
        });

        return this;
    },

    /**
     * Delivers all previously discovered components of current trend-display
     * control matching provided type name.
     *
     * @param {string} typeName
     * @return {Array.<shmi.visuals.controls.TrendDisplay.Component>}
     */
    getComponents: function(typeName) {
        "use strict";

        var registry = this._components;

        if (shmi.objectHasOwnProperty(registry, typeName)) {
            return registry[typeName];
        }

        return [];
    },

    /**
     * Adds provided controller of subordinated component to current control's
     * registry.
     *
     * @param {shmi.visuals.controls.TrendDisplay.Component} controller
     * @return {shmi.visuals.controls.TrendDisplay} fluent interface support
     */
    addComponent: function(controller) {
        "use strict";

        var typeName,
            registry;

        if (controller instanceof shmi.visuals.core.BaseControl ||
            /* supporting flat shmi.extend(): */
            shmi.objectHasOwnProperty(controller, "uiType")) {
            typeName = controller.uiType;
            registry = this._components;

            if (!shmi.objectHasOwnProperty(registry, typeName)) {
                registry[typeName] = [];
            }

            registry = registry[typeName];

            if (registry.indexOf(controller) === -1) {
                registry.push(controller);
            }
        }

        return this;
    },

    /**
     * Invokes callback on every registered component (optionally matching given
     * type of component).
     *
     * @param {string=} typeFilter
     * @param {function} callback
     */
    everyComponent: function(typeFilter, callback) {
        "use strict";

        var registry = this._components;

        if (arguments.length < 2) {
            callback = typeFilter;
            typeFilter = null;
        }

        if (typeFilter) {
            (registry[typeFilter] || []).forEach(callback);
        } else {
            Object.keys(registry).forEach(function(type) {
                (registry[type] || []).forEach(callback);
            });
        }
    },

    addInitializer: function() {},

    removeInitializer: function() {},

    _onBroadcastEarly: function(broadcast) {
        var self = this;
        switch (broadcast.type) {
        case "render":
            broadcast.detail.suppress = this.isPreparing;
            break;

        case "configuration-changed":
            // ensure provided configuration is updated on demand
            var config = broadcast.detail.configuration;
            if (config.invalid) {
                // lock control due to invalid config
                this.lock();

                return false;
            }

            this.config = config;

            // unlock control if it was locked before due to invalid config
            this.unlock();
            break;

        case "scale-size":
            var now = (broadcast.detail.now > 1) ? broadcast.detail.now - 1 : "none";
            [].forEach.call(self.element.classList, function(el) {
                if (el.indexOf("scale-size-") === 0) {
                    shmi.removeClass(self.element, el);
                }
            });
            shmi.addClass(this.element, "scale-size-" + now);
            break;
        default:
        }
        return true;
    },

    _onBroadcastLate: function(broadcast) {
        switch (broadcast.type) {
        case "resize":
            // re-render control after processing resizing event
            this.broadcast("render");
            break;
        default:
        }
    },

    onEnable: function() {
        // bugfix: sometimes trend is not loaded yet...
        var trendDisplay = this.getComponents("trend-display-continuous")[0],
            trend = trendDisplay ? trendDisplay.trend : null;

        if (trend) {
            trend.active = true;
            trend.running = true;
        }

        this.everyComponent(function(component) {
            component.enable();
        });
    },

    onDisable: function() {
        var trendDisplay = this.getComponents("trend-display-continuous")[0],
            trend = trendDisplay ? trendDisplay.trend : null;

        if (trend) {
            trend.active = false;
            trend.running = false;
        }

        this.everyComponent(function(component) {
            component.disable();
        });
    },

    onDelete: function() {
        "use strict";
        var self = this;

        if (self.ctrls) {
            Object.keys(self.ctrls).forEach(function(ctrlName) {
                if (self.ctrls[ctrlName]) {
                    shmi.deleteControl(self.ctrls[ctrlName]);
                }
            });
            self.ctrls = {};
        }

        this.everyComponent(function(component) {
            shmi.deleteControl(component);
        });

        this._components = {};

        // disable resize listeners
        this._manageResizeListeners(false);

        this.onBroadcast.reset();

        // release all virtual items of control (don't try to remove them for
        // external controls might still rely on them)
        var items = this.items;
        if (items) {
            items.items = null;
        }

        // clear local storage for signal styles
        localStorage['itemStyles'] = []; //eslint-disable-line no-undef
    },

    /**
     * Sends broadcast to all registered components.
     *
     * @note A broadcast is a local event dispatched into all registered
     *       components of a single TrendDisplay control. It is used to
     *       implement inter-component communication. Dispatching broadcasts is
     *       less complex than firing events, too.
     *
     * @param {?object} sender context/instance of sending component, null on
     *                        broadcasting due to user action
     * @param {string} broadcastName some name of broadcast to send (so
     *                               listeners may act properly}
     * @param {{}} broadcastData some detail information on broadcast
     * @return {shmi.visuals.controls.TrendDisplay.Promise.<boolean>} promise resolved with true if sending was stopped by any listener
     */
    broadcast: function(sender, broadcastName, broadcastData) {
        "use strict";

        if (typeof broadcastData !== "object" || !broadcastData) {
            broadcastData = {};
        }

        return this.onBroadcast.send({
            source: sender,
            type: broadcastName,
            detail: broadcastData
        });
    }
};

/**
 * Establishes inheritance of two "classes".
 *
 * This method works is different from shmi.extend() in creating hierarchy.
 * Rather that shallowly extending prototype of subclass by prototype elements
 * of superclass not found in prototype of subclass it is truly linking either
 * functions constructor resulting in these benefits and drawbacks:
 *
 * + Supports testing an object to be instance of more distant ancestor class.
 * + Enables partial overloading providing access on overloaded method to be
 *   integrated in overloading method.
 * + Sizes of prototypes isn't increasing from derivation to derivation.
 * - Invoking non-overloaded method takes slightly more time to lookup prototype
 *   chain.
 *
 * @example
 *
 *     // implement some base class
 *     function Base( major ) [
 *         console.log( "major is: " + major );
 *     }
 *
 *     Base.prototype = {
 *         dump: function( type ) {
 *             console.log( "this is a " + type + " dump" );
 *         }
 *     };
 *
 *     // implement some derived class
 *     function Derived( major, minor ) {
 *         Derived._super.call( this, major );  // invoking overloaded constructor
 *         console.log( "minor is: " + minor );
 *     }
 *
 *     Derived.prototype = {
 *         dump: function() {
 *             this._super( Derived, "dump", "derived" );
 *         }
 *     };
 *
 *     // establish inheritance relation
 *     shmi.visuals.controls.TrendDisplay.derive( Derived, Base );
 *
 *     var a = new Derived( "Foo", "Bar" ).dump();
 *     console.log( "is instance of Base:", a instanceof Base );
 *
 * This example will log
 *
 *     major is: Foo
 *     minor is: Bar
 *     this is a derived dump
 *     is instance of Base: true
 *
 * @param {function} subclass reference on constructor function of "class" to be
 *                   derived from another one
 * @param {function} superclass reference on constructor function of "class" to
 *                   derive subclass from
 */
shmi.visuals.controls.TrendDisplay.derive = function(subclass, superclass, subclassCtorPropertyName) {
    "use strict";

    var prototype = subclass.prototype || {};

    // link prototypes properly (providing access on constructor of superclass)
    subclass.prototype = Object.create(superclass.prototype);
    subclass.prototype.constructor = subclass;
    subclass.prototype.constructor._super = superclass;

    // re-inject all properties and methods in original prototype of subclass
    Object.getOwnPropertyNames(prototype).forEach(function(pname) {
        subclass.prototype[pname] = prototype[pname];
    });

    // inject special method for calling overridden methods
    subclass.prototype._super = function(myClass, pname) {
        var _super;

        for (_super = Object.getPrototypeOf(myClass.prototype); _super; _super = Object.getPrototypeOf(_super)) {
            if (typeof _super[pname] === "function") {
                return _super[pname].apply(this, [].slice.call(arguments, 2));
            }
        }

        return undefined;
    };

    /**
     * Provide reference to constructor using its name.
     *
     * This property becomes handy on invoking overridden methods using
     * this._super().
     *
     * @example
     *
     *     function Derived() {
     *         ...
     *     }
     *
     *     Derived.prototype.example = function() {
     *         this._super( this.Derived );
     *     }
     *
     * @example
     *
     *     shmi.visuals.controls.myPackage.myControl = function LocalName() {
     *         ...
     *     }
     *
     *     shmi.visuals.controls.myPackage.myControl.prototype.example = function() {
     *         this._super( this.LocalName );
     *     }
     *
     * @note Obey to use unique local names for constructors to get proper
     *       reference on either constructor.
     */

    var constructorLocalName = String(subclass.name || subclassCtorPropertyName || "").trim();
    if (constructorLocalName && !(constructorLocalName in subclass.prototype)) {
        Object.defineProperty(subclass.prototype, constructorLocalName, {
            value: subclass
        });
    }

    if (subclass.prototype.uiType) {
        shmi.log("derived type " + subclass.prototype.uiType, shmi.c("LOG_DEBUG"));
    }
};

/**
 * Lists named callbacks to invoke for validating and normalizing parts of
 * configuration defined on basic control.
 *
 * @type {Array.<{0:function({},TrendDisplay):?Promise,1:int=}>}
 */
shmi.visuals.controls.TrendDisplay.onConfiguration = [];

/**
 * Provides single implementation of NOP to reduce heap size due to using empty
 * function in several situations.
 */
shmi.visuals.controls.TrendDisplay.nop = function() {};

/**
 * Limits provided value to a given minimum and maximum value without rounding
 * value.
 *
 * @note 0 is processed if provided value isn't numeric.
 *
 * @param {number} value
 * @param {number} min
 * @param {number} max
 * @return {number}
 */
shmi.visuals.controls.TrendDisplay.limit = function(value, min, max) {
    var v = parseFloat(value) || 0;

    return (v < min ? min : v > max ? max : v);
};

/**
 * Implements simplified function binding for remarkably improved performance.
 *
 * @param {function} fn function to bind
 * @param {object} ctx object to be available per `this` when running function
 * @return {function} provided function bound to given object
 */
shmi.visuals.controls.TrendDisplay.fastBind = function(fn, ctx) {
    return function() {
        return fn.apply(ctx, arguments);
    };
};

/**
 * Retrieves window's current scrolling offsets.
 *
 * @return {{x:int, y:int}}
 */
shmi.visuals.controls.TrendDisplay.getScrollOffset = (function() {
    if (window.pageXOffset !== undefined && window.pageYOffset !== undefined) {
        return function() {
            return {
                x: window.pageXOffset,
                y: window.pageYOffset
            };
        };
    }

    return function() {
        return {
            x: (document.documentElement || document.body.parentNode || document.body).scrollLeft,
            y: (document.documentElement || document.body.parentNode || document.body).scrollTop
        };
    };
}());

/**
 *
 * @param {string} controlName name of control selected as target in a user action
 * @param {string} actionName name of action triggered (used to derive name of a broadcast in context of control)
 * @param {object} data data to be attached on sending broadcast
 */
shmi.visuals.controls.TrendDisplay.onUiAction = function(controlName, actionName, data) {
    "use strict";

    var control = shmi.getControlByName(controlName, 'trend-display');
    if (!control) {
        shmi.log("[UserAction] " + actionName + " - no control found by name '" + controlName + "'", 2);
    } else {
        control.broadcast(null, "ui-" + actionName, {
            cause: "action",
            value: data || {}
        });
    }
};

shmi.extend(shmi.visuals.controls.TrendDisplay, shmi.visuals.core.BaseControl);

// ensure package containing user actions exists to support defining custom user
// actions in modules
shmi.pkg("visuals.session");
shmi.visuals.session.sysActions = shmi.visuals.session.sysActions || {};

shmi.visuals.session.sysActions["show-curve"] = function(args) {
    "use strict";

    if (args.length < 2) {
        shmi.log("[UserAction] show-curve - usage is 'show-curve:<control-name>:<curve-name>[:<state>]'", 2);
        return;
    }

    shmi.visuals.controls.TrendDisplay.onUiAction(args[0], "show-curve", {
        curve: args[1],
        isVisible: (args.length < 3) || shmi.toBoolean(args[2])
    });
};

/**
 * Implements very simple messaging involving a priority-sorted sequence of
 * listener functions invoked on sending any message.
 *
 * Listeners are invoked sequentially in order of attached priority (from high
 * priority to low priority value). Every listener may defer dispatching by
 * returning promise to be resolved prior to succeeding.
 *
 * @note Synchronous processing is available for use in rare cases when
 *       processing several listeners is required to happen nearly
 *       simultaneously.
 *
 * @param {boolean=} synchronous true to request synchronous processing of sent messages
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.Messaging = function Messaging(synchronous) {
    /**
     * Lists listening callbacks with attached priority.
     *
     * @type {Array.<{0:int,1:function,2:object=}>}
     * @private
     */
    this._listeners = [];
    this._listeners.nextId = 0;

    /**
     * Serialises concurrent requests for sending messages to all listeners.
     *
     * @type {shmi.visuals.controls.TrendDisplay.Promise}
     * @private
     */
    this._sentBefore = shmi.visuals.controls.TrendDisplay.Promise.resolve();

    /**
     * Indicates if queue is processing sent messages synchronously.
     *
     * @type {boolean}
     */
    Object.defineProperty(this, "synchronous", {
        get: function() {
            return synchronous;
        },
        set: function(value) {
            synchronous = !!value;
        }
    });

    /**
     * Provides number of currently registered listeners.
     *
     * @property length
     * @type {int}
     * @readonly
     */
    Object.defineProperty(this, "length", {
        get: function() {
            return this._listeners.length;
        }
    });
};

shmi.visuals.controls.TrendDisplay.Messaging.prototype = {

    /** @typedef {function|[int,function]|[int,function,object]|[function,object]|[function]} ListenerDescriptor */
    /** @typedef {{prio:int, fn:function, context:object}} ListenerSignature */

    /**
     * Adds provided function to be invoked on sending message.
     *
     * @param {int=} priority priority of listener (higher value causes earlier invocation)
     * @param {function} fn function to invoke on sending message
     * @param {object=} ctx some object to become "this" on invoking listener
     * @return {int} listener ID
     */
    addListener: function(priority, fn, ctx) {
        var queue = this._listeners,
            listener = this._normalizeListener.apply(this, arguments),
            index = this._indexOfListenerSignature(listener);

        if (index >= 0) {
            return queue[index][3];
        }

        var lid = ++queue.nextId;

        // append listener
        queue.push([listener.prio, listener.fn, listener.context, lid]);

        // update sequence to keep processing high-prio listeners first
        this._listeners = queue.sort(function(l, r) {
            return r[0] - l[0];
        });

        return lid;
    },

    /**
     * Registers listener using array-like API.
     *
     * @note This method is provided to match way of predefining listeners even
     *       though predefining array has been processed before.
     *
     * @param {ListenerDescriptor} record function optionally tagged with priority
     * @return {shmi.visuals.controls.TrendDisplay.Messaging} fluent interface
     */
    push: function(record) {
        return this.addListener.apply(this, arguments);
    },

    /**
     * Retrieves index of callback in sequence of listening callbacks.
     *
     * @note Basically this method takes same signature as addListener(). In
     *       addition it is possible to provide listener ID as returned by
     *       addListener() in a sole argument.
     *
     * @param {int=} lidOrPrio
     * @param {function=} fn
     * @param {object=} ctx
     * @return {shmi.visuals.controls.TrendDisplay.Messaging} fluent interface
     */
    removeListener: function(lidOrPrio, fn, ctx) {
        var foundIndex = this.isListeningAt.apply(this, arguments);
        if (foundIndex >= 0) {
            this._listeners.splice(foundIndex, 1);
        }

        return this;
    },

    /**
     * Retrieves index of callback in sequence of listening callbacks.
     *
     * @note Basically this method takes same signature as addListener() and
     *       removeListener().
     *
     * @param {int=} lidOrPrio
     * @param {function=} fn
     * @param {object=} ctx
     * @return {int} <0 if callback isn't listening
     */
    isListeningAt: function(lidOrPrio, fn, ctx) {
        var lid = parseInt(lidOrPrio);

        if (lid > 0 && arguments.length === 1) {
            return this._indexOfListenerId(lidOrPrio);
        }

        return this._indexOfListenerSignature(this._normalizeListener.apply(this, arguments));
    },

    /**
     * Normalizes provided set of arguments to use common way of selecting some
     * listener to be found, added or removed.
     *
     * @param {int=} prio priority of listener in queue, higher values mean earlier processing, default is 0
     * @param {function} fn listener callback to be invoked
     * @param {object=} context explicit context to be provided as "this" on invoking callback, omit for providing current Messaging instance
     * @return {ListenerSignature}
     * @private
     */
    _normalizeListener: function(prio, fn, context) {
        var args;

        if (Array.isArray(prio)) {
            args = prio;
        } else {
            args = [prio, fn, context];
        }

        if (typeof args[0] === "function") {
            args.unshift(0);
        }

        if (typeof args[1] !== "function") {
            shmi.log("invalid listener callback", 3);
        }

        if (!args[2]) {
            args[2] = this;
        } else if (typeof args[2] !== "object") {
            shmi.log("TrendDisplay.Messaging._normalizeListener() invalid context for listener callback", 3);
        }

        return {
            prio: args[0],
            fn: args[1],
            context: args[2]
        };
    },

    /**
     * Gets index of listener selected by ID in current messaging queue.
     *
     * @param {int} lid listener ID as returned by addListener()
     * @return {int} index of listener in queue, <0 if listener isn't found
     * @private
     */
    _indexOfListenerId: function(lid) {
        var i,
            l,
            queue = this._listeners;

        lid = parseInt(lid);
        if (lid > 0) {
            for (i = 0, l = queue.length; i < l; i++) {
                if (queue[i][3] === lid) {
                    return i;
                }
            }
        }

        return -1;
    },

    /**
     * Gets index of listener described by signature in current messaging queue.
     *
     * @param {ListenerSignature} signature
     * @return {int} index of listener in queue, <0 if listener isn't found
     * @private
     */
    _indexOfListenerSignature: function(signature) {
        var i,
            l,
            queue = this._listeners;

        for (i = 0, l = queue.length; i < l; i++) {
            if (queue[i][1] === signature.fn && queue[i][2] === signature.context) {
                return i;
            }
        }

        return -1;
    },

    /**
     * "Sends message" to all registered listeners by invoking them sequentially
     * always passing same arguments as passed here.
     *
     * Every listener may return false to stop dispatching message to succeeding
     * listeners. In addition every listener may return promise deferring any
     * dispatching to succeeding listener.
     *
     * @note Dispatching is cancelled on throwing exception in any listener
     *       rejecting returned promise accordingly.
     * @note If instance of Messaging is configured to work synchronously all
     *       listeners are invoked instantly. Any returned promise is ignored.
     *
     * @return {shmi.visuals.controls.TrendDisplay.Promise.<boolean>} promise resolved with true if sending was stopped by listener
     */
    send: function() {
        var args,
            i,
            l;

        // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments
        for (args = [], i = 0, l = arguments.length; i < l; i++) {
            args[i] = arguments[i];
        }

        return this.synchronous ? this._sendSynchronous(args) : this._sendAsynchronous(args);
    },

    _sendSynchronous: function(args) {
        var i,
            l,
            queue,
            listener,
            result,
            stopped = false;

        for (queue = this._listeners, i = 0, l = queue.length; i < l && !stopped; i++) {
            listener = queue[i];
            result = listener[1].apply(listener[2], args);
            if (result === false) {
                stopped = true;
            }
        }

        return shmi.visuals.controls.TrendDisplay.Promise.resolve(stopped);
    },

    _sendAsynchronous: function(args) {
        var self = this;

        return (this._sentBefore = this._sentBefore.
            always(function() {
                var Promise = shmi.visuals.controls.TrendDisplay.Promise,
                    stopped = false;

                return Promise.resolve(self._listeners).
                    each(function(listener) {
                        var result = listener[1].apply(listener[2], args);
                        if (result === false) {
                            stopped = true;
                        } else if (result instanceof Promise) {
                            result = result.then(function(value) {
                                if (value === false) {
                                    stopped = true;
                                }

                                return value;
                            });
                        }

                        return result;
                    }, function() {
                        return stopped;
                    }).
                    then(function() {
                        return stopped;
                    });
            }));
    },

    /**
     * Adds all listeners registered in provided messaging instance to current
     * instance.
     *
     * @param {shmi.visuals.controls.TrendDisplay.Messaging|Array} messaging
     * @return {shmi.visuals.controls.TrendDisplay.Messaging}
     */
    merge: function(messaging) {
        var i,
            l,
            s = messaging;

        if (s instanceof shmi.visuals.controls.TrendDisplay.Messaging) {
            s = s._listeners;
        }

        if (Array.isArray(s)) {
            for (i = 0, l = s.length; i < l; i++) {
                this.push(s[i]);
            }
        }

        return this;
    },

    /**
     * Drops all previously registered listeners on messaging instance.
     *
     */
    reset: function() {
        this._listeners = [];

        return this;
    }

};

/**
 * Creates messaging instance taking over predefined listeners from provided
 * array.
 *
 * @param {Array<function|{0:int,1:function}>} predefined
 * @return {shmi.visuals.controls.TrendDisplay.Messaging}
 */
shmi.visuals.controls.TrendDisplay.Messaging.wrap = function(predefined) {
    return new shmi.visuals.controls.TrendDisplay.Messaging().merge(predefined);
};

shmi.visuals.controls.TrendDisplay.Object = {

    /**
     * Converts provided configuration into (deeply) frozen (read-only)
     * properties of current instance.
     *
     * @node Returned target is being sealed to prevent addition/removal of
     *       properties.
     *
     * @param {object} target object to contain all provided properties frozenly
     * @param {object} source object providing properties to inject frozen
     * @param {boolean=} deepFreeze true for recursively freezing any contained
     *                              object's properties as well
     * @return {object} provided target
     */
    injectFrozen: function injectFrozen(target, source, deepFreeze) {
        "use strict";

        var self = target;

        // lock all enumerable properties of instance of configuration provider
        Object.keys(source).
            forEach(function(name) {
                var value = source[name];

                if (deepFreeze) {
                    value = _deepFreeze(value);
                }

                Object.defineProperty(self, name, {
                    value: value,
                    enumerable: true
                });
            });

        //Object.seal( target );

        return target;

        function _deepFreeze(value) {
            if (value && typeof value === "object") {
                if (Object.isFrozen(value)) {
                    return value;
                }

                Object.getOwnPropertyNames(value).
                    forEach(function(name) {
                        var property = value[name];

                        if (typeof property === "object") {
                            trySet(value, name, _deepFreeze(property));
                        }
                    });

                return Object.freeze(value);
            }

            return value;
        }

        function trySet(ctx, prop, value) {
            try {
                ctx[prop] = value;
            } catch (e) {
                console.error("[TrendDisplay]", e);
            }
        }
    },

    /**
     * Shallowly injects all enumerable properties of provided source into given
     * target without saving them from being replaced or adjusted in context of
     * target.
     *
     * @param {object} target
     * @param {object} source
     * @return {object}
     */
    inject: function inject(target, source) {
        "use strict";

        var self = target;

        // lock all enumerable properties of instance of configuration provider
        Object.keys(source).
            forEach(function(name) {
                Object.defineProperty(self, name, {
                    value: source[name],
                    enumerable: true,
                    configurable: true
                });
            });

        return target;
    },

    /**
     * Injects all enumerable properties of provided source into given target
     * without saving them from being adjusted in context of target. However,
     * every injected property is observed on change invoking some callback with
     * new value and name of changed property.
     *
     * @note Target is sealed to prevent addition or removal of properties.
     *
     * @param {object} target
     * @param {object} source
     * @param {function(*,string,*)=} notifierFn callback invoked on trying to adjust property, might throw to prevent that
     * @param {boolean=} deepObserve true for deep inject, false for shallow inject
     * @param {Array<string>=} readOnly names of read-only properties
     * @return {object} injected and sealed target
     */
    injectObserved: function injectObserved(target, source, notifierFn, deepObserve, readOnly) {
        "use strict";

        var self = target,
            prefix = typeof deepObserve === "string" ? deepObserve : "";

        /*
        if ( Array.isArray( source ) ) {
        Class.mixinArray( target, source.length );
        }
         */

        // lock all enumerable properties of instance of configuration provider
        Object.keys(source).
            forEach(function(name) {
                var value = source[name];

                if (deepObserve && value !== null && typeof value === "object" &&
                !Object.isFrozen(value) && !Object.isSealed(value) &&
                !value.dontObserve && !Array.isArray(value)) {
                    value = shmi.visuals.controls.TrendDisplay.Object.injectObserved({}, value, notifierFn, prefix + name + ".", readOnly);
                }

                if (readOnly && readOnly.indexOf(prefix + name) >= 0) {
                    Object.defineProperty(self, name, {
                        value: value,
                        enumerable: true
                    });
                } else {
                    Object.defineProperty(self, name, {
                        get: function() {
                            return value;
                        },
                        set: function(valueParam) {
                            if (valueParam !== source[name]) {
                                if (typeof notifierFn === "function") {
                                    try {
                                        notifierFn(valueParam, prefix + name, source[name]);
                                    } catch (e) {
                                        return;
                                    }
                                }

                                source[name] = valueParam;
                            }
                        },
                        enumerable: true
                    });
                }
            });

        // seal injected object making all defined properties non-configurable
        Object.seal(target);

        return target;
    },

    deepCopy: function deepCopy(a) {
        var b;

        if (Array.isArray(a)) {
            b = [];

            a.forEach(function(item) {
                b.push(shmi.visuals.controls.TrendDisplay.Object.deepCopy(item));
            });
        } else if (a && typeof a === "object") {
            b = {};

            Object.keys(a).
                forEach(function(name) {
                    b[name] = shmi.visuals.controls.TrendDisplay.Object.deepCopy(a[name]);
                });
        } else {
            b = a;
        }

        return b;
    },

    mixinArray: function(object, length) {
        Object.defineProperty(object, "length", {
            value: length
        });

        object.map = function(fn) {
            return [].map.call(object, fn);
        };
        object.filter = function(fn) {
            return [].filter.call(object, fn);
        };
        object.some = function(fn) {
            return [].some.call(object, fn);
        };
        object.every = function(fn) {
            return [].every.call(object, fn);
        };
        object.reverse = function(fn) {
            return [].reverse.call(object, fn);
        };
        object.sort = function(fn) {
            return [].sort.call(object, fn);
        };
        object.forEach = function(fn) {
            return [].forEach.call(object, fn);
        };
    }

};

/**
 * Implements promise pattern complying with Promises/A+ specification.
 *
 * @alias Promise
 * @alias shmi.visuals.controls.TrendDisplay.Promise
 */

shmi.visuals.controls.TrendDisplay.Promise = function Promise(process) {
    "use strict";

    /**
     * Lists actions to process when promise has settled.
     *
     * @type {Array.<(boolean|Array.<(function(*),function(*),shmi.visuals.controls.TrendDisplay.Promise)>)>}
     * @private
     */
    this._queue = [];

    /**
     * @type {int}
     * @private
     */
    this._state = 0;

    /**
     * Tracks position of promise in chain of promises (1 marks initially
     * created promise, 2 marks promise chained to that first one, etc.).
     *
     * @type {int}
     * @private
     */
    this._length = 1;

    /**
     * Names position of code creating promise initally.
     *
     * @type {string}
     * @private
     */
    this._creator = undefined;

    // capture information on caller creating this promise for debugging purpose
    if ((shmi.visuals.session.config || {}).debug) {
        // try getting information on current context for debugging purposes
        var error = new Error("dummy"),
            state = 0,
            line,
            i,
            l,
            lines = shmi.visuals.controls.TrendDisplay.Promise._simpleTry(function() {
                return String(error.stack);
            }, function() {
                return "";
            }).split(/(?:\n\s+)/);

        for (i = 0, l = lines.length; i < l; i++) {
            line = lines[i];

            if (line.match(/^\s*at\s+[^(]*\.Promise[.(]/)) {
                state = 1;
            } else if (state > 0) {
                state = 2;
                break;
            }
        }

        var match = state > 1 && line.match(/\/([/]+:\d+:\d+)\)?\s*$/);
        this._creator = match ? match[1] : "unknown";
    }

    // invoke optionally provided callback for delivering outcome of promise
    if (typeof process === "function" && process.length >= 1) {
        var self = this,
            _resolve = function(value) {
                shmi.visuals.controls.TrendDisplay.Promise._resolve(self, value);
            },
            _reject = function(cause) {
                self.reject(cause);
            };

        shmi.visuals.controls.TrendDisplay.Promise._simpleTry(function() {
            process(_resolve, _reject);
        }, _reject);
    }
};

shmi.visuals.controls.TrendDisplay.Promise.prototype = {

    // prevent Promises from being observed for change by Object#injectObserved
    dontObserve: true,

    /**
     * Detects if promise has been settled (either resolved or rejected) before.
     *
     * @return {boolean}
     */
    isSettled: function() {
        "use strict";

        return !!this._state;
    },

    /**
     * Detects if promise has been resolved before.
     *
     * @return {boolean}
     */
    isResolved: function() {
        "use strict";

        return this._state > 0;
    },

    /**
     * Detects if promise has been rejected before.
     *
     * @return {boolean}
     */
    isRejected: function() {
        "use strict";

        return this._state < 0;
    },

    /**
     * Attaches methods to promise to either invoke on resolved or on rejected promise.
     *
     * Provided callbacks might return promise to resolve or reject promise returned by Promise.then() here.
     *
     * @note then() is returning another instance of Promise that is settled
     *       * instantly on invoked callback returning anything but instance of Promise
     *       * on settling instance of Promise returned from invoked callback
     *       * on throwing exception in callback
     *
     * @note Promise.then() provides additional types of arguments for sake of convenience:
     *       # Providing function it is invoked on resolving/rejecting promise.
     *       # Providing promise it is resolved/rejected on resolving/rejecting promise.
     *       # Providing anything else but undefined promise returned here is resolved/rejected with given value.
     *       # Finally, providing undefined the promise returned here is resolved/rejected with same value/cause as
     *         promise then is attaching to.
     *
     * @param {function(*=)|shmi.visuals.controls.TrendDisplay.Promise|undefined|*} onResolved triggered on resolving promise, @see description
     * @param {function(*=)|shmi.visuals.controls.TrendDisplay.Promise|undefined|*} [onRejected] triggered on rejecting promise, @see description
     * @returns {shmi.visuals.controls.TrendDisplay.Promise} _another_ promise resolved/rejected on attached triggers resolving/rejecting/returning
     */
    then: function(onResolved, onRejected) {
        "use strict";

        var promise2 = new shmi.visuals.controls.TrendDisplay.Promise();

        // adopt some properties of original promise
        // - keep track of code having created promise chain originally
        promise2._creator = this._creator;
        // - keep track of length of promise chain
        promise2._length = this._length + 1;

        if (onResolved instanceof shmi.visuals.controls.TrendDisplay.Promise) {
            var pResolve = onResolved;
            onResolved = function(value) {
                return pResolve.resolve(value);
            };
        } else if (typeof onResolved !== "function") {
            var vResolved = onResolved;
            onResolved = function(value) {
                return vResolved !== undefined ? vResolved : value;
            };
        }

        if (onRejected instanceof shmi.visuals.controls.TrendDisplay.Promise) {
            var pReject = onRejected;
            onRejected = function(value) {
                return pReject.reject(value);
            };
        } else if (typeof onRejected !== "function") {
            var cRejected = onRejected;
            onRejected = function(value) {
                throw cRejected !== undefined ? cRejected : value;
            };
        }

        if (this._queue.push([onResolved, onRejected, promise2]) === 1) {
            // having pushed callbacks onto empty queue (not being processed currently)
            if (this._state) {
                // promise is settled already -> instantly process pushed record in queue
                shmi.visuals.controls.TrendDisplay.Promise._process(this);
            }
        }

        return promise2;
    },

    /**
     * Attaches method to promise to invoke on settling that promise.
     *
     * This method works similar to Promise.then(), but attaches provided method to either states of settling a
     * promise and thus can be used to have code invoked w/o respect to the actual outcome of a promise.
     *
     * By providing promise as first argument that promise is resolved no matter if current promise was resolved
     * or rejected. By providing function it is called in same situation. By providing anything else current promise
     * is used to resolve/reject promise returned by Promise.always() accordingly.
     *
     * @param {function(*=)|shmi.visuals.controls.TrendDisplay.Promise} [onSettled] callback to invoke or promise to resolve on current promise settled
     * @returns {shmi.visuals.controls.TrendDisplay.Promise} promise resolved or rejected on provided callback/promise resolving/rejecting
     */
    always: function(onSettled) {
        "use strict";

        return this.then(onSettled, onSettled);
    },

    /**
     * Resolves promise attaching provided result of promise
     *
     * @param {*} [value] result of promise
     * @return {shmi.visuals.controls.TrendDisplay.Promise}
     */
    resolve: function(value) {
        "use strict";

        if (!this._state) {
            this._state = 1;
            this._value = value;

            shmi.visuals.controls.TrendDisplay.Promise._process(this);
        }

        return this;
    },

    /**
     * Rejects promise attaching provided information on cause for rejecting it.
     *
     * @note Rejecting promise with instances of Error is considered best
     *       practice.
     *
     * @param {Error|*} reason cause for rejecting promise
     * @return {shmi.visuals.controls.TrendDisplay.Promise}
     */
    reject: function(reason) {
        "use strict";

        if (!this._state) {
            this._state = -1;
            this._value = reason;

            shmi.visuals.controls.TrendDisplay.Promise._process(this);
        }

        return this;
    },

    /**
     * Catches rejected promises either generically or on matching some
     * specific types of causes.
     *
     * @note This method takes additional arguments prepended to processor
     *       considered to give "classes" of probable causes to match. This
     *       permits to e.g. catch certain types of errors like this:
     *
     *       new Promise( function( resolve, reject ) {
     *           {}.missingMethod();
     *       } )
     *           .catch( TypeError, function( error ) {
     *               console.log( "got TypeError in promised code" );
     *           } )
     *           .catch( ReferenceError, RangeError, function( error ) {
     *               console.log( "got ReferenceError or RangeError" );
     *           } )
     *           .catch( function( error ) {
     *               console.log( "got some other error" );
     *           } );
     *
     *       Alternatively catch handlers might be applied depending on
     *       provided filter functions to inspect cause for matching:
     *
     *       new Promise( function( resolve, reject ) {
     *           reject( { status: 404 } );
     *       } )
     *           .catch( function( e ) { return e.status === 400; }, function( error ) {
     *               console.log( "got 400: bad request" );
     *           } )
     *           .catch( function( e ) { return e.status === 404; }, function( error ) {
     *               console.log( "got 404: not found" );
     *           } )
     *           .catch( function( error ) {
     *               console.log( "got some other error" );
     *           } );
     *
     * @note Semantics might be different from typical try-catch behaviour
     *       in that later invocations of Promise#catch() also cover former
     *       invocations due to extending promise chain like Promise#then().
     * @note Matching catch-handlers consume cause for rejection promised
     *       code, thus returning to invoke positive Promise#then()-handlers
     *       attached after Promise#catch().
     *
     * @param {function(Error)} processor callback to invoke on catching cause
     * @return {shmi.visuals.controls.TrendDisplay.Promise}
     */
    catch: function(processor) {
        "use strict";

        var types = [].slice.call(arguments, 0),
            handler = types.pop();

        return this.then(undefined, function(cause) {
            var i,
                l,
                type,
                process = false;

            for (i = 0, l = types.length; !process && i < l; i++) {
                type = types[i];

                if (typeof type === "function") {
                    if (/^\S+Error$/.test(type.name)) {
                        if (cause instanceof type) {
                            process = true;
                        }
                    } else {
                        // invoke provided callback for testing if current
                        // cause is to be caught here ...
                        process = type(cause);
                    }
                }
            }

            if (process || !l) {
                // invoke provided handler for having caught some specific
                // cause or for catching all causes generically
                return handler.call(handler, cause);
            }

            // keep cause rejecting chain of promises
            return new shmi.visuals.controls.TrendDisplay.Promise().reject(cause);
        });
    },

    /**
     * Ensures promise is resolving to given value.
     *
     * @note This isn't affecting a rejected promise or its cause for
     *       rejecting.
     * @note This method is using Promise#then() for attaching another
     *       callback to invoke on resolved promise returning given value.
     *
     * @param {*} value some value to resolve promise with
     * @return {shmi.visuals.controls.TrendDisplay.Promise} promise resolved with given value
     */
    return: function(value) {
        "use strict";

        return this.then(function() {
            return value;
        });
    },

    /**
     * Replaces Promise#then() for convenience on promise resolved with
     * array providing all elements of array in separate arguments to provided
     * callback function.
     *
     * @example If a promise is resolved with an array like [ 1, 2, 3 ] all
     *          callbacks attached with .then() are invoked with array as
     *          single argument and all callbacks attached with .spread()
     *          are invoked with three arguments 1, 2 and 3.
     *
     *          p = Promise.resolve( [ 1, 2, 3 ] );
     *          p.then( function( arrValues ) { ... } );
     *          p.spread( function( first, second, third ) { ... } );
     *
     * @note If promise isn't resolved with array, Promise#spread() is
     *       working like Promise#then() except for taking single callback,
     *       only.
     *
     * @param {function(...*):*} fn callback invoked on resolving promise
     * @return {shmi.visuals.controls.TrendDisplay.Promise} promise resolved with result of invoked callback or rejected on callback throwing
     */
    spread: function(fn) {
        "use strict";
        var self = this;
        return self.then(function(value) {
            if (Object.prototype.toString.call(value) !== "[object Array]") {
                value = [value];
            }

            return fn.apply(self, value);
        });
    },

    /**
     * Sequentially processes all elements of array resolving current
     * promise by invoking given callback on each element.
     *
     * The resulting promise is resolved after having processed all
     * elements. If processing element returns promise iteration is delayed
     * until that returned promise has been settled.
     *
     * Optionally caller may provide second callback to be invoked after
     * every processed element with results of processing as arguments to
     * decide if iteration should be stopped or not. If omitted iteration
     * isn't stopped prematurely.
     *
     * @template T
     * @param {function(T):(shmi.visuals.controls.TrendDisplay.Promise|*)} fn callback to invoke
     * @param {function(...*):boolean=} stopFn stop condition callback
     * @return {shmi.visuals.controls.TrendDisplay.Promise<Array.<T>>}
     */
    each: function(fn, stopFn) {
        "use strict";

        if (typeof stopFn !== "function") {
            stopFn = shmi.visuals.controls.TrendDisplay.nop;
        }

        return this.then(function(valueSet) {
            if (Object.prototype.toString.call(valueSet) !== "[object Array]") {
                if (!(valueSet && valueSet.length > 0)) {
                    return valueSet;
                }
            }

            var l = valueSet.length;

            function next(i) {
                if (i >= l) {
                    return valueSet;
                }

                return shmi.visuals.controls.TrendDisplay.Promise.resolve(fn(valueSet[i])).
                    then(function() {
                        if (stopFn.apply({}, arguments)) {
                            return valueSet;
                        }

                        return next(i + 1);
                    });
            }

            return next(0);
        });
    },

    /**
     * Iterates over traversable values resolving current promise invoking
     * provided callback on every value for deciding whether to keep it or
     * not. The resulting promise is resolved with all values of current
     * promise's set that callback has returned truthy result.
     *
     * @param {function(*):boolean} fn callback invoked per value returning truthy for all values to keep in set
     * @return {shmi.visuals.controls.TrendDisplay.Promise}
     */
    filter: function(fn) {
        "use strict";

        return this.then(function(valueSet) {
            if (Object.prototype.toString.call(valueSet) !== "[object Array]") {
                if (!(valueSet && valueSet.length > 0)) {
                    return valueSet;
                }
            }

            // ensure set isn't some kind of live collection (so fn() might
            // drop contained elements on its own w/o affecting looping here)
            valueSet = [].slice.call(valueSet);

            var l = valueSet.length;

            function next(i) {
                if (i >= l) {
                    return shmi.visuals.controls.TrendDisplay.Promise.resolve(valueSet);
                }

                return shmi.visuals.controls.TrendDisplay.Promise.resolve(fn(valueSet[i])).
                    then(function(keepIt) {
                        if (keepIt) {
                            return next(i + 1);
                        }

                        // remove processed element from cloned set, too ...
                        valueSet.splice(i, 1);
                        // ... actually reducing size of the latter
                        l--;

                        // process next element ... now residing at same
                        // position as removed one
                        return next(i);
                    });
            }

            return next(0);
        });
    },

    /**
     * Iterates over set of data or promised data trying to find first
     * element in set provided detector function is returning truthy result.
     *
     * @param {function(*):boolean} detectFn callback invoked per element to return true on match
     * @return {shmi.visuals.controls.TrendDisplay.Promise<*>} promise resolved with found value or null on mismatch
     */
    find: function(detectFn) {
        "use strict";

        return this.then(function(valueSet) {
            if (Object.prototype.toString.call(valueSet) !== "[object Array]") {
                valueSet = [valueSet];
            }

            var l = valueSet.length;

            function next(i) {
                if (i >= l) {
                    // met end of set without matching
                    return shmi.visuals.controls.TrendDisplay.Promise.resolve(null);
                }

                // try current element of set
                return shmi.visuals.controls.TrendDisplay.Promise.resolve(detectFn(valueSet[i])).
                    then(function(result) {
                        if (result) {
                            // detector callback indicates match
                            // -> return matched value
                            return valueSet[i];
                        }

                        // element isn't matching -> try next
                        return next(i + 1);
                    });
            }

            return next(0);
        });
    },

    /**
     * Triggers provided remote promise to resolve/reject just like current
     * promise.
     *
     * @param {shmi.visuals.controls.TrendDisplay.Promise} promise remote promise to be resolved/rejected
     * @return {shmi.visuals.controls.TrendDisplay.Promise} current promise
     */
    trigger: function(promise) {
        "use strict";

        return this.
            then(function(value) {
                promise.resolve(value);
                return value;
            }, function(cause) {
                promise.reject(cause);
                throw cause;
            });
    },

    /**
     * Invokes provided callback no matter the promise was resolved or
     * rejected keeping the promises outcome unaffected.
     *
     * @note Callback might be throwing without notice.
     *
     * @param {function} callback
     * @return {shmi.visuals.controls.TrendDisplay.Promise}
     */
    finally: function(callback) {
        "use strict";

        return this.then(function(value) {
            return shmi.visuals.controls.TrendDisplay.Promise.invoke(callback).
                always(function() {
                    return (value);
                });
        }, function(cause) {
            return shmi.visuals.controls.TrendDisplay.Promise.invoke(callback).
                always(function() {
                    throw cause;
                });
        });
    }

};

shmi.visuals.controls.TrendDisplay.Promise._simpleTry = function(fn, catcher) {
    "use strict";

    try {
        return fn();
    } catch (e) {
        return catcher ? catcher(e) : e;
    }
};

shmi.visuals.controls.TrendDisplay.Promise._applyTry = function(fn, catcher, ctx, args) {
    "use strict";

    try {
        return fn.apply(ctx, args);
    } catch (e) {
        return catcher ? catcher(e) : e;
    }
};

shmi.visuals.controls.TrendDisplay.Promise._async = function(fn) {
    "use strict";

    var next = 1,
        tasks = {};

    function reduceMap() {
        var newTasks = {};

        Object.keys(tasks).forEach(function(handle) {
            if (tasks[handle]) {
                newTasks[handle] = tasks[handle];
            }
        });

        tasks = newTasks;
    }

    window.addEventListener("message", function(event) {
        var handler,
            handle = Number(event.data);

        if (handle > 0) {
            handler = tasks[handle];
            if (typeof handler === "function") {
                handler();
            }

            tasks[handle] = undefined;
        }
    }, false);

    function _async(fnParam) {
        var handle = next++;

        if ((handle % 100) === 0) {
            reduceMap();
        }

        tasks[handle] = fnParam;
        window.postMessage(handle, "*");
    }

    shmi.visuals.controls.TrendDisplay.Promise._async = _async;

    _async(fn);
};

/**
 * Implements resolver according to Promise/A+ specification.
 *
 * @internal
 * @param {shmi.visuals.controls.TrendDisplay.Promise} promise
 * @param {shmi.visuals.controls.TrendDisplay.Promise|{then:function()}} x
 */
shmi.visuals.controls.TrendDisplay.Promise._resolve = function _resolve(promise, x) {
    "use strict";

    if (promise === x) {
        shmi.log("TrendDisplay.Promise_resorlve() promise on itself rejected", 3);
    }

    var flag = false;

    function onResolve(value) {
        if (!flag) {
            _resolve(promise, value);
            flag = true;
        }
    }

    function onReject(reason) {
        if (!flag) {
            promise.reject(reason);
            flag = true;
        }
    }

    if (typeof x === "object" && x) {
        var t = x.then;

        if (typeof t === "function") {
            shmi.visuals.controls.TrendDisplay.Promise._applyTry(t, onReject, x, [onResolve, onReject]);
            return;
        }
    }

    promise.resolve(x);
};

/**
 * Processes another queued successor of some settled promise in vanilla
 * execution context.
 *
 * @internal
 * @param {shmi.visuals.controls.TrendDisplay.Promise} promise
 */
shmi.visuals.controls.TrendDisplay.Promise._process = function _process(promise) {
    "use strict";

    var _Promise = shmi.visuals.controls.TrendDisplay.Promise,
        queue = promise._queue;

    // get next pair of callbacks to invoke
    var next = queue.shift();
    if (!next) {
        next = queue.shift();
    }

    if (next) {
        // ensure to have marker on queue indicating currently processing some shifted callback
        if (!queue.length) {
            queue.push(false);
        }

        // process callback in vanilla execution context and resolve its result
        _Promise._async(processor);
    }

    function processor() {
        var e = _Promise._simpleTry(function() {
            _Promise._resolve(next[2], (promise._state > 0 ? next[0] : next[1])(promise._value));
        });

        if (e) {
            if ((shmi.visuals.session.config || {}).debug) {
                var s = e;

                if (/^\[object\s\S*Error]$/.test(Object.prototype.toString.call(e))) {
                    s = e.stack || e.message || String(e).replace(/@[^:]+:\/\/[^/]+\//mg, '@');
                }

                if (!/^\s*\S+Error/.test(s) && e.message) {
                    s = e.message + "\nin " + s;
                }

                if (!/^\s*\S+Error/.test(s)) {
                    s = e.constructor.name + ": " + s;
                }

                shmi.log("exception in thenable: " + s);
            }

            next[2].reject(e);
        }

        // process further promises settled meanwhile
        _process(promise);
    }
};

/**
 * Tries to invoke callback
 * - supporting callback to provide promise to wait for
 * - ensuring exceptions in callback won't affect contained promise
 *
 * @param {function} fn callback function to invoke
 * @return {shmi.visuals.controls.TrendDisplay.Promise} promise resolved with result of callback, rejected if callback is throwing
 */
shmi.visuals.controls.TrendDisplay.Promise.invoke = function(fn) {
    "use strict";

    try {
        return shmi.visuals.controls.TrendDisplay.Promise.resolve(fn());
    } catch (e) {
        return shmi.visuals.controls.TrendDisplay.Promise.reject(e);
    }
};

/**
 * Creates promise instantly resolving with given value.
 *
 * @note This method is very useful to ensure some value is a promise.
 *
 * @param {*=} value
 * @return {shmi.visuals.controls.TrendDisplay.Promise}
 */
shmi.visuals.controls.TrendDisplay.Promise.resolve = function(value) {
    "use strict";

    if (value instanceof shmi.visuals.controls.TrendDisplay.Promise) {
        return value;
    }

    return new shmi.visuals.controls.TrendDisplay.Promise().resolve(value);
};

/**
 * Creates promise instantly rejecting with given cause.
 *
 * @note Rejecting promise with instances of Error is considered best
 *       practice.
 *
 * @param {Error|shmi.visuals.controls.TrendDisplay.Promise|*} cause
 * @return {shmi.visuals.controls.TrendDisplay.Promise}
 */
shmi.visuals.controls.TrendDisplay.Promise.reject = function(cause) {
    "use strict";

    if (cause instanceof shmi.visuals.controls.TrendDisplay.Promise) {
        if (cause.isRejected()) {
            return cause;
        }
    }

    return new shmi.visuals.controls.TrendDisplay.Promise(function(resolve, reject) {
        if (cause instanceof shmi.visuals.controls.TrendDisplay.Promise) {
            cause.always(function() {
                reject(cause);
            });
        } else {
            reject(cause);
        }
    });
};

/**
 * Tries processing given callback resolving returned promise with
 * result of given function or rejecting it when function is throwing.
 *
 * @type {Function}
 * @param {function():*} fn callback invoked for resolving or rejecting promise
 * @return {shmi.visuals.controls.TrendDisplay.Promise} promise resolved with result returned from fn or rejected on fn throwing
 */
shmi.visuals.controls.TrendDisplay.Promise.try = function(fn) {
    "use strict";

    return new shmi.visuals.controls.TrendDisplay.Promise(function(resolve) {
        resolve(fn());
    });
};

/**
 * Creates promise invoking provided callback after some fixed-length
 * delay given as timeout for returning value to resolve promise.
 *
 * @template T
 * @param {function:T} callback
 * @param {int} timeout
 * @return {shmi.visuals.controls.TrendDisplay.Promise<T>}
 */
shmi.visuals.controls.TrendDisplay.Promise.delay = function(callback, timeout) {
    "use strict";

    return new shmi.visuals.controls.TrendDisplay.Promise(function(resolve) {
        setTimeout(resolve, timeout || 0);
    });
};

/**
 * Establishes promise to resolve on all provided promises resolving or to reject on any provided promise
 * rejecting.
 *
 * This method takes a variable number of promises to observe. Returned promise is resolved with set of all
 * observed promises' results. It is rejected with cause of first rejected promise of observed ones.
 *
 * @param {...shmi.visuals.controls.TrendDisplay.Promise|Array.<shmi.visuals.controls.TrendDisplay.Promise>} firstPromiseOrSetOfPromises
 * @return {shmi.visuals.controls.TrendDisplay.Promise}
 */
shmi.visuals.controls.TrendDisplay.Promise.all = function(firstPromiseOrSetOfPromises) {
    "use strict";

    if (arguments.length === 1 && Array.isArray(firstPromiseOrSetOfPromises)) {
        return shmi.visuals.controls.TrendDisplay.Promise.all.apply(this, firstPromiseOrSetOfPromises);
    }

    var promise = new shmi.visuals.controls.TrendDisplay.Promise(),
        set = arguments,
        required = 0,
        results = {},
        i,
        length;

    try {
        if (!set.length) {
            promise.resolve([]);
        }

        var onSettling = function(expectedCount) {
            if (++required >= expectedCount) {
                var idx,
                    sorted = [];
                for (idx = 0; idx < expectedCount; idx++) {
                    sorted.push(results[idx]);
                }

                promise.resolve(sorted);
            }
        };

        // build chain of promises separately collecting either result in (unsorted) object
        for (i = 0, length = set.length; i < length; i++) {
            step(i, length);
        }
    } catch (error) {
        promise.reject(error);
    }

    return promise;

    function step(index, count) {
        if (set[index] instanceof shmi.visuals.controls.TrendDisplay.Promise) {
            set[index].then(function(result) {
                results[index] = result;
                onSettling(count);
            }, promise);
        } else {
            results[index] = set[index];
            onSettling(count);
        }
    }
};

/**
 * Establishes promise to resolve on first resolved promise of provided ones or to reject on first rejected
 * promise of provided ones.
 *
 * This method takes a variable number of promises to observe. Returned promise is resolved with result of
 * first resolved  promise of observed ones or rejected with cause of first rejected promise of observed ones.
 *
 * @param {...shmi.visuals.controls.TrendDisplay.Promise|Array.<shmi.visuals.controls.TrendDisplay.Promise>} firstPromiseOrSetOfPromises
 * @return {shmi.visuals.controls.TrendDisplay.Promise}
 */
shmi.visuals.controls.TrendDisplay.Promise.first = function(firstPromiseOrSetOfPromises) {
    "use strict";

    if (arguments.length === 1 && Array.isArray(firstPromiseOrSetOfPromises)) {
        return shmi.visuals.controls.TrendDisplay.Promise.all.apply(this, firstPromiseOrSetOfPromises);
    }

    var promise = new shmi.visuals.controls.TrendDisplay.Promise(),
        set = arguments,
        i,
        l;

    try {
        if (!set.length) {
            promise.resolve([]);
        }

        for (i = 0, l = set.length; i < l; i++) {
            set[i].then(promise, promise);
        }
    } catch (e) {
        promise.reject(e);
    }

    return promise;
};

/**
 * Provides methods for validating, parsing and normalizing data.
 */

shmi.visuals.controls.TrendDisplay.Parsers = {
    /**
     * Converts some provided value into integer selecting one of several
     * aggregation methods.
     *
     * @param {string|int} value value to parse
     * @param {int} defaultValue result on providing unset value
     * @param {int=} onInvalid result on invalid input, using default if omitted
     * @return {int}
     */
    aggregation: function(value, defaultValue, onInvalid) {
        "use strict";

        if (value === undefined || value === null) {
            return defaultValue;
        }

        switch (String(value).trim().toLowerCase()) {
        case "":
            return defaultValue;

        case "0":
        case "none":
            return 0;

        case "1":
        case "avg":
        case "average":
            return 1;

        case "2":
        case "min":
        case "minimum":
            return 2;

        case "3":
        case "max":
        case "maximum":
            return 3;

        case "4":
        case "sum":
            return 4;

        case "5":
        case "min-max":
        case "minimum-maximum":
            return 5;
        default:
        }

        return arguments.length > 2 ? onInvalid : defaultValue;
    },

    /**
     * Converts some provided value into seconds of time span serving as a unit.
     *
     * @param {string|int} value value to parse
     * @param {int} defaultValue result on providing unset value
     * @param {int=} onInvalid result on invalid input, using default if omitted
     * @return {int}
     */
    timeSpanUnit: function(value, defaultValue, onInvalid) {
        "use strict";

        var normalized = String(value || "").trim().replace(/(\d)(?:-|\s+)(\w)/, "$1$2").toLowerCase();

        switch (normalized) {
        case "":
            return defaultValue;

        case "none":
        case "auto":
            return 0;

        case "s":
        case "sec":
        case "second":
        case "seconds":
            return 1;

        case "10s":
        case "10sec":
        case "10seconds":
            return 10;

        case "m":
        case "min":
        case "minute":
        case "minutes":
            return 180;

        case "10m":
        case "10min":
        case "10minutes":
            return 600;

        case "h":
        case "hour":
        case "hours":
            return 3600;

        case "d":
        case "day":
        case "days":
            return 86400;

        case "w":
        case "week":
        case "weeks":
            return 7 * 86400;

        case "month":
        case "months":
            return 31 * 86400;

        case "y":
        case "year":
        case "years":
            return 365 * 86400;

        default:
            normalized = parseInt(normalized);
            if (normalized >= 0) {
                return normalized;
            }
        }

        return arguments.length > 2 ? onInvalid : defaultValue;
    },

    /**
     * Converts some provided value into seconds of time span serving as a unit.
     *
     * @param {string|int} value value to parse
     * @param {int} defaultValue result on providing unset value
     * @param {int=} onInvalid result on invalid input, using default if omitted
     * @return {int}
     */
    valuedTimeSpan: function(value, defaultValue, onInvalid) {
        "use strict";

        var result,
            match;

        match = /^\s*(\d+)\s*(s(?:ec(?:onds?)?)?|m(?:in(?:utes?)?)?|h(?:(?:ou)?rs?)?|d(?:ays?)?|w(?:eeks?)?|mon(?:ths?)?|y(?:(?:ea)rs?)?)?\s*$/i.exec(value);
        if (match) {
            result = Number(match[1]);

            switch (match[2].toLowerCase().substr(0, 2)) {
            case "":
            case "s":
            case "se":
                return result;

            case "m":
            case "mi":
                return result * 60;

            case "h":
            case "ho":
            case "hr":
                return result * 3600;

            case "d":
            case "da":
                return result * 86400;

            case "w":
            case "we":
                return result * 7 * 86400;

            case "mo":
                return result * 31 * 86400;

            case "y":
            case "ye":
            case "yr":
                return result * 365 * 86400;
            default:
            }
        } else if (!String(value || "").trim().length) {
            return defaultValue;
        }

        return arguments.length > 2 ? onInvalid : defaultValue;
    },

    /**
     * Detects some common string values considered denoting "none" and thus to
     * be handled like providing null.
     *
     * @param {*} value
     * @return {?*}
     */
    none: function(value) {
        switch (String(value).trim()) {
        case "":
        case "null":
        case "undefined":
        case "none":
        case "false":
            return null;

        default:
            return value;
        }
    },

    /** @typedef {{red:int,green:int,blue:int,alpha:number,rgba:string,hex:string,hexa:string,hasAlpha:boolean}} Color */

    /**
     * Parses color descriptor.
     *
     * @param {Color|string} value
     * @return {?Color}
     */
    color: function(value) {
        "use strict";

        if (typeof value === "string") {
            var result = null,
                match = /^(?:rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([01](?:\.\d*)?))?\s*\)|#([0-9a-f]{6})([0-9a-f]{2})?)$/i.exec(value);

            if (match) {
                result = {};

                if (typeof match[1] === "string") {
                    result.red = Math.min(255, match[1]);
                    result.green = Math.min(255, match[2]);
                    result.blue = Math.min(255, match[3]);

                    result.hasAlpha = !!match[4];

                    result.alpha = result.hasAlpha ? parseFloat(match[4]) : 1.0;
                    if (result.alpha > 1.0) {
                        result.alpha = 1.0;
                    }
                } else {
                    result.red = parseInt(match[5].substr(0, 2), 16);
                    result.green = parseInt(match[5].substr(2, 2), 16);
                    result.blue = parseInt(match[5].substr(4, 2), 16);

                    result.hasAlpha = (typeof match[6] === "string");

                    result.alpha = result.hasAlpha ? parseInt(match[6], 16) / 255 : 1.0;
                    if (result.alpha > 1.0) {
                        result.alpha = 1.0;
                    }
                }
            }

            value = result;
        } else if (value && typeof value === "object") {
            value.hasAlpha |= value.alpha < 1;
        }

        if (value && typeof value === "object") {
            value.rgba = "rgba(" + value.red + "," + value.green + "," + value.blue + "," + value.alpha + ")";
            value.hex = "#" + ("00" + value.red.toString(16)).substr(-2) +
                ("00" + value.green.toString(16)).substr(-2) +
                ("00" + value.blue.toString(16)).substr(-2);
            value.hexa = value.hex + ("00" + Math.round(255 * value.alpha).toString(16)).substr(-2);
        }

        return value;
    },

    /**
     * Extracts width information from provided value or null if extraction
     * fails.
     *
     * @param {*} value
     * @param {boolean=} supportLive
     * @return {?int}
     */
    timestamp: function(value, supportLive) {
        "use strict";

        var result = null,
            parsed = parseInt(value);

        if (parsed >= -1 && isFinite(parsed)) {
            result = parsed;
        } else {
            parsed = String(value).trim();
            switch (parsed) {
            case "now":
                result = 0;
                break;
            case "":
                if (supportLive) {
                    // default, may be live, so use it
                    result = -1;
                } else {
                    // default, mustn't be live, so use "now"
                    result = 0;
                }
                break;
            case "live":
                result = -1;
                break;
            default:
                parsed = Date.parse(parsed);
                if (!isNaN(parsed)) {
                    result = parsed.getTime() / 1000;
                }
            }
        }

        return result;
    },

    /**
     * Extracts width information from provided value or null if extraction
     * fails.
     *
     * @param {*} value
     * @return {?int}
     */
    width: function(value) {
        "use strict";

        var result = null,
            match = /^\s*(\d+)\s*(?:px)?\s*$/.exec(value);

        if (match) {
            result = parseInt(match[1]);
        }

        return result;
    },

    /**
     * Extracts relative value from provided input, that is floating point value
     * in range of 0.0 to 1.0.
     *
     * @param {*} value
     * @return {?number}
     */
    relative: function(value) {
        "use strict";

        var result = null,
            match = /^\s*(0?(?:\.\d+)?|1(?:.0+)?)\s*$/.exec(value);

        if (match) {
            result = parseFloat(match[1]);
        }

        return result;
    },

    /**
     * Extracts positive integer from provided value or null if extraction
     * fails.
     *
     * @param {*} value
     * @return {?int}
     */
    positive: function(value) {
        "use strict";

        value = parseInt(value);
        if (value > 0) {
            return value;
        }

        return null;
    },

    /**
     * Extracts dash descriptor from provided value or null if extraction fails
     * or would result in solid dash.
     *
     * @param {*} value
     * @return {?Array.<int>}
     */
    dash: function(value) {
        "use strict";

        if (typeof value === "string") {
            value = value.trim().split(/(?:[,\s]+)/);
        }

        if (Array.isArray(value)) {
            value = value.
                map(function(i) {
                    return parseInt(i);
                }).
                filter(function(i) {
                    return i;
                });

            if (value.length > 0) {
                return value;
            }
        }

        return null;
    },

    /**
     * Extracts URL from provided value or null if extraction fails.
     *
     * @param {*} value
     * @return {?string}
     */
    url: function(value) {
        "use strict";

        if (typeof value === "string") {
            var match = /^url\(\s*(["'])?\s*(.+)\s*(\1)\s*\)$/.exec(value);
            if (match) {
                value = match[2];
            }

            return value;
        }

        return value || null;
    },

    isObject: function(value) {
        if (!value) {
            return {};
        }

        if (typeof value === "object") {
            return value;
        }

        console.error("TrendDisplay.Parsers.isObject() expecting object, but found: " + value, 3);
        return null;
    },

    parseFilterAndSort: function(items, parser, reverse) {
        return items.
            map(function(i) {
                return parser(i);
            }).
            filter(function(i) {
                return i;
            }).
            sort(reverse ? rev : fwd);

        function fwd(l, r) {
            return l - r;
        }
        function rev(l, r) {
            return r - l;
        }
    }
};

/**
 * Implements central management of virtual items managed by single trend
 * display instance to interact with embedding HMI project.
 *
 * @param trendDisplay
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.VirtualItems = function(trendDisplay) {
    "use strict";

    /**
     * @typedef {{name:string,subscriptionId:*,setValue:function(number),setProperties:function,lock:function,unlock:function,properties:Array}} VirtualItem
     */

    /**
     * @typedef {object.<string,VirtualItem>} ItemSet
     */

    /**
     * @typedef {{curves:ItemSet}} ItemSuperSet
     * @extends ItemSet
     */

    /**
     * Set of managed virtual items.
     *
     * @type {ItemSuperSet}
     */
    var items = null;

    /**
     * Refers to trend display control this manager instance belongs to.
     *
     * @property trendDisplay
     * @type {shmi.visuals.controls.TrendDisplay}
     * @readonly
     */
    Object.defineProperty(this, "trendDisplay", {
        value: trendDisplay
    });

    /**
     * Refers to items created by current instance.
     *
     * @property items
     * @type {ItemSuperSet}
     */
    Object.defineProperty(this, "items", {
        get: function() {
            if (!items || !items.curves) {
                items = this._createItems(items || {}, trendDisplay);
            }

            return items;
        },
        set: function(value) {
            if (value) {
                // drop curve/signal-related items, only
                this._releaseSignalItems(items);
            } else if (items) {
                // drop all items
                this._releaseItems(items);
                items = null;
            }
        }
    });

    trendDisplay.onBroadcast.push([900, this._onBroadcast, this]);
};

shmi.visuals.controls.TrendDisplay.VirtualItems.prototype = {

    /**
     * Commonly creates single virtual item.
     *
     * @param {string} name name of item to create
     * @param {int} type type of virtual item, 0=string, 1=bool, 2=int, 3=float
     * @param {int} rights permissions on item
     * @param {number} min minimum value of item
     * @param {number} max maximum value of item
     * @param {number} initial initial value of item, applied unless was set before
     * @param {function(number)} onSetValue callback to invoke on adjusting item's value providing new value
     * @returns {VirtualItem}
     * @private
     */
    _createSingleItem: function(name, type, rights, min, max, initial, onSetValue) {
        var nop = shmi.visuals.controls.TrendDisplay.nop,
            im = shmi.visuals.session.ItemManager,
            types = ["string", "bool", "int", "float"],
            listener = {
                name: name,
                setValue: onSetValue ? _onChangeValue : nop,
                setProperties: nop,
                suppress: [],
                lock: nop,
                unlock: nop,
                properties: {
                    access: [ "read", "write" ],
                    data_type: null,
                    digits: type === 3 ? 3 : 0,
                    item_alias: name,
                    label: null,
                    min: min,
                    max: max,
                    prewarn: null,
                    warn: null,
                    step: 0,
                    unit: null,
                    value_type: types[type]
                },
                writeValue: function(value, suppressListener) {
                    if (!shmi.objectHasOwnProperty(listener, "lastValue") || value !== listener.lastValue) {
                        listener.lastValue = value;

                        if (onSetValue && suppressListener) {
                            listener.suppress.push(value);
                        }

                        // write new value into item
                        im.getItem(listener.name).writeValue(value);
                    }
                }
            },
            item = im.getItem(name),
            itemIsNew = !item || !item.tdInitialized;

        listener.subscriptionId = im.subscribeItem(name, listener);

        if (itemIsNew) {
            // item was created due to subscribing it
            // -> configure created item according to provided metrics
            im.setProperties(listener.properties);

            item = im.getItem(name);

            item.tdInitialized = true;
        }

        // write provided value to reflect "initial" state of related property
        // of trend display control ("initial" includes state after changing
        // configuration)
        item.writeValue(initial);

        return listener;

        function _onChangeValue(value) {
            listener.lastValue = value;

            var suppressIndex = listener.suppress.indexOf(value);
            if (suppressIndex >= 0) {
                listener.suppress.splice(suppressIndex, 1);
            } else {
                onSetValue.apply(this, arguments);
            }
        }
    },

    /**
     * Creates virtual items of trend display control commonly available without
     * regards to any current configuration.
     *
     * @param {ItemSet} items
     * @param td
     * @returns {ItemSet}
     * @private
     */
    _createGlobalItems: function(items, td) {
        var self = this,
            c = shmi.c,
            tBool = c("TYPE_BOOL"),
            tFloat = c("TYPE_FLOAT"),
            cfg = td.config,
            prefix = "virtual:" + td.getName();

        // Horizontal slide
        if (!items.zoomX) {
            items.zoomX = self._createSingleItem(prefix + ":zoomX", tFloat, 7, c("TREND_DISPLAY_ZOOM_SCALE_X_MIN") * 100, c("TREND_DISPLAY_ZOOM_SCALE_X_MAX") * 100, 100.0, function(value) {
                td.broadcast(null, "ui-zoomX", {
                    cause: "item",
                    value: value
                });
            });
            this._lockItem(items.zoomX, false);
        }

        // Vertical slide
        if (!items.zoomY) {
            items.zoomY = self._createSingleItem(prefix + ":zoomY", tFloat, 7, c("TREND_DISPLAY_ZOOM_SCALE_Y_MIN") * 100, c("TREND_DISPLAY_ZOOM_SCALE_Y_MAX") * 100, 100.0, function(value) {
                td.broadcast(null, "ui-zoomY", {
                    cause: "item",
                    value: value
                });
            });
            this._lockItem(items.zoomY, false);
        }

        if (!items.live) {
            items.live = self._createSingleItem(prefix + ":live", tBool, 7, 0, 1, (!cfg.invalid && ((cfg.display.mode === "live") || (cfg.display.tEnd === -1))) ? 1 : 0, function(value) {
                td.broadcast(null, "ui-live", {
                    cause: "item",
                    value: !!value
                });
            });
            this._lockItem(items.live, false);
        }

        if (!items.handleTime) {
            items.handleTime = self._createSingleItem(prefix + ":handleTime", tFloat, 7, 0, 10000000000000, parseInt(new Date().getTime() / 1000), function(value) {
                td.broadcast(null, "ui-handleTime", {
                    cause: "item",
                    value: value
                });
            });
            this._lockItem(items.handleTime, false);
        }

        if (!items.leftTime) {
            items.leftTime = self._createSingleItem(prefix + ":leftTime", tFloat, 7, 0, 10000000000000, parseInt(new Date().getTime() / 1000), function(value) {
                td.broadcast(null, "ui-leftTime", {
                    cause: "item",
                    value: value
                });
            });
            this._lockItem(items.leftTime, false);
        }

        return items;
    },
    _createPgmItems: function(items, td) {
        var self = this,
            cfg = td.config,
            prefix = "virtual:" + td.getName() + ":";

        items.pgm = {};

        if (cfg.program && Array.isArray(cfg.program.items)) {
            cfg.program.items.forEach(function(pgmSignal, idx) {
                var valName = pgmSignal.valueType.concat(pgmSignal.valueIndex);
                items.pgm[valName] = self._createSingleItem(prefix + valName, shmi.c("TYPE_FLOAT"), 7);
                console.debug("[TrendDisplay]", "created item:", prefix + valName);
            });
        }
    },
    /**
     * Creates virtual items of trend display control available for controlling
     * currently configured signals and curves of trend display.
     *
     * @param {ItemSet} items
     * @param td
     * @returns {ItemSet}
     * @private
     */
    _createSignalItems: function(items, td) {
        var self = this,
            c = shmi.c,
            tBool = c("TYPE_BOOL"),
            tFloat = c("TYPE_FLOAT"),
            cfg = td.config,
            prefix = "virtual:" + td.getName() + ":";

        this._releaseSignalItems(items);

        items.curves = {};

        if (!cfg.invalid && cfg.trend && cfg.trend.signals) {
            cfg.trend.signals.
                forEach(function(signal, index) {
                    var sName = signal.alias || signal.name,
                        i = items.curves[sName] = {};

                    i.value = self._createSingleItem(prefix + sName + ":value", tFloat, 7);
                    i.visible = self._createSingleItem(prefix + sName + ":visible", tBool, 7, 0, 1, signal.visible ? 1 : 0, function(value) {
                        td.broadcast(null, "ui-show-curve", {
                            cause: "item",
                            value: {
                                curveName: sName,
                                curveIndex: index,
                                isVisible: !!value
                            }
                        });
                    });
                });
        }

        if (cfg.program) {
            self._createPgmItems(items, td);
        }

        return items;
    },

    _createItems: function(items, td) {
        return this._createSignalItems(this._createGlobalItems(items, td), td);
    },

    _lockItem: function(listener, locked) {
        var im = shmi.visuals.session.ItemManager,
            props = listener.properties;

        if (locked) {
            props[4] |= 0x10;
        } else {
            props[4] &= ~0x10;
        }

        im.setProperties(props);
    },

    /**
     * Releases all signal-related virtual items.
     *
     * @param {ItemSet} items
     * @private
     */
    _releaseSignalItems: function(paramItems) {
        var im = shmi.visuals.session.ItemManager,
            items = paramItems || {},
            subs = items.curves || {};

        Object.keys(subs).
            forEach(function(signalName) {
                var item = subs[signalName];

                im.unsubscribeItem(item.value.name, item.value.subscriptionId);
                im.unsubscribeItem(item.visible.name, item.visible.subscriptionId);
            });

        items.curves = null;
    },

    _releaseItems: function(items) {
        if (items) {
            var im = shmi.visuals.session.ItemManager;

            this._releaseSignalItems(items);

            if (items.zoomX) {
                this._lockItem(items.zoomX, true);
                im.unsubscribeItem(items.zoomX.name, items.zoomX.subscriptionId);
                items.zoomX = null;
            }

            if (items.zoomY) {
                this._lockItem(items.zoomY, true);
                im.unsubscribeItem(items.zoomY.name, items.zoomY.subscriptionId);
                items.zoomY = null;
            }

            if (items.live) {
                this._lockItem(items.live, true);
                im.unsubscribeItem(items.live.name, items.live.subscriptionId);
                items.live = null;
            }

            if (items.handleTime) {
                this._lockItem(items.handleTime, true);
                im.unsubscribeItem(items.handleTime.name, items.handleTime.subscriptionId);
                items.handleTime = null;
            }

            if (items.leftTime) {
                this._lockItem(items.leftTime, true);
                im.unsubscribeItem(items.leftTime.name, items.leftTime.subscriptionId);
                items.leftTime = null;
            }
        }
    },

    _onBroadcast: function(broadcast) {
        if (broadcast.type === "configuration-changed") {
            this.items = true;
        }
    },

    /**
     * Retrieves virtual item by name.
     *
     * @param {string=} signal name of signal item is related to specifically
     * @param {string} name name of item to retrieve
     * @return {shmi.visuals.core.Item}
     */
    getByName: function(signal, name) {
        if (arguments.length < 2) {
            return this.items[signal];
        }

        var items = this.items.curves;
        if (items) {
            var item = items[signal];
            if (item) {
                return item[name] || null;
            }
        }

        return null;
    }
};

/**
 * Manages curve drawn in context of a manager representing single signal of a
 * trend.
 *
 * @param {SHMI_TD.AbstractRenderingManager} manager
 * @param {SHMI_TD.Signal} signal
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.AbstractRenderer = function(manager, signal) {
    "use strict";

    var visible = signal.visible;

    var drawingContext = new shmi.visuals.controls.TrendDisplay.DrawingContext(signal, manager);

    var path = null;

    var css = null;

    var style = null;

    var index = signal.itemIndex;

    /**
     * Refers to manager containing curve's rendering.
     *
     * @property manager
     * @type {SHMI_TD.AbstractRenderingManager}
     */
    Object.defineProperty(this, "manager", {
        value: manager
    });

    /**
     * Provides signal descriptor containing meta information on signal to be
     * rendered as curve.
     *
     * @property signal
     * @type {SHMI_TD.Signal}
     */
    Object.defineProperty(this, "signal", {
        value: signal
    });

    /**
     * Provides index of current renderer in context of equivalent renderers.
     *
     * @note This index might be different from a related signal's index. While
     *       signal's index refers to index of value provided in samples of
     *       trend this property is expected to give index of current curve or
     *       scale in list of available curves or scales. Since set of curves/
     *       scales might exclude some signals indexes might differ.
     *
     * @property index
     * @type {int}
     */
    Object.defineProperty(this, "index", {
        get: function() {
            return index;
        },
        set: function(newIndex) {
            var parsed = parseInt(newIndex);
            if (!isNaN(parsed) && parsed >= 0) {
                index = newIndex;
            }
        }
    });

    /**
     * Controls if current curve is obeyed on rendering.
     *
     * @type {boolean}
     */
    Object.defineProperty(this, "visible", {
        get: function() {
            return visible;
        },
        set: function(state) {
            visible = !!state;
        }
    });

    /**
     * Caches drawing context of curve.
     *
     * This drawing context is available for mapping values onto coordinate
     * system of canvas. It is recreated using up-to-date size of canvas on
     * writing any value to this property, thus you need to write on resizing
     * canvas.
     *
     * @type {SHMI_TD.DrawingContext}
     */
    Object.defineProperty(this, "drawingContext", {
        value: drawingContext
    });

    /**
     * Refers to some path element put into canvas for supporting CSS-based
     * styling as well as render this particular renderer's content into (if
     * applicable).
     *
     * @property path
     * @type {HtmlElement}
     */
    Object.defineProperty(this, "path", {
        get: function() {
            if (!path) {
                path = this._createPathElement(this.manager.canvas);
            }

            return path;
        },
        set: function() {
            if (path) {
                path.parentNode.removeChild(path);
                path = null;
            }
        }
    });

    /**
     * Retrieves CSS declaration on this renderer's path element.
     *
     * @see path
     * @property css
     * @type {CSSStyleDeclaration}
     */
    Object.defineProperty(this, "css", {
        get: function() {
            if (!css) {
                var pathInner = this.path;
                if (pathInner) {
                    css = window.getComputedStyle(pathInner, null);
                } else {
                    css = {};
                }
            }

            return css;
        }
    });

    /**
     * Retrieves compiled style declaration for this element.
     *
     * @see path
     * @property css
     * @type {object}
     */
    Object.defineProperty(this, "style", {
        get: function() {
            if (!style) {
                style = this._getConfiguredStyle().getQualifiedByCss(this.css);
            }

            return style;
        }
    });

    this._style = null;
};

shmi.visuals.controls.TrendDisplay.AbstractRenderer.prototype = {
    /**
     * (Re-)renders curve on canvas of provided manager.
     */
    render: function() {
        "use strict";

        shmi.log("TrendDisplay: missing implemented curve renderer");
    },

    onDelete: function() {
        this.path = null;
    },

    _getConfiguredStyle: function() {
        return this.manager.config.trend.items[this.signal.itemIndex].style;
    },

    _createPathElement: function(canvas) {}
};

shmi.visuals.controls.TrendDisplay.CanvasBackground = function(manager, drawingContext, curves) {
    "use strict";

    this.labels = [];

    Object.defineProperty(this, "manager", {
        value: manager
    });

    Object.defineProperty(this, "units", {
        value: manager.config.display.units
    });

    Object.defineProperty(this, "drawingContext", {
        value: drawingContext
    });

    Object.defineProperty(this, "curves", {
        value: curves
    });
};

shmi.visuals.controls.TrendDisplay.CanvasBackground.prototype = {

    render: function(manager, data, canvas) {
        "use strict";

        var dc = canvas.getContext("2d"),
            vp = manager.viewport,
            trendDisplay = manager.trendDisplay,
            display = trendDisplay.config.display,
            curves = this.curves,
            self = this,
            lanes = [],
            signal;

        // customize stroke style
        dc.lineWidth = 0.4; // vp.isDashboardView ? 0.4 : 1 @todo set in global config
        dc.strokeStyle = "rgba(0,0,0,0.5)";
        //dc.setLineDash( [1,1] );
        dc.lineDashOffset = 0;

        dc.beginPath();

        if (vp.isMultiLane) {
            // @todo: set global config this.lanes
            Object.keys(curves).
                forEach(function(curve, index) {
                    signal = self.curves[curve].signal;
                    if (signal.visible) {
                        if (typeof lanes[signal.laneIndex] === 'undefined') {
                            lanes[signal.laneIndex] = [];
                        }
                        lanes[signal.laneIndex].push(signal);
                    }
                });
        }
        var count = vp.isDashboardView ? (lanes.length < display.lanesPerView ? lanes.length : display.lanesPerView) : 1, // max lanes before vertical scrolling
            labelHeight = display.labelHeight || 0,
            canvasHeight = canvas.height / 2,
            allLabelHeight = labelHeight * count,
            allLaneHeight = /*( canvasHeight - allLabelHeight ) / count < 50 ? 250 :*/ canvasHeight - allLabelHeight,
            laneHeight = allLaneHeight / count,
            ratio = canvasHeight / (canvasHeight - (count * labelHeight)); // ratio of each lane

        // scaling, labels, buttons for each signal/lane
        Object.keys(curves).
            forEach(function(curve, index) {
                signal = self.curves[curve].signal;

                // for visible signals only
                if (signal.visible) {
                    var laneIndex = vp.isMultiLane && signal ? signal.laneIndex : 0,
                        startY = vp.isSingleLane || vp.isDetailView ? 0 : ((labelHeight * (laneIndex)) + (laneHeight * (laneIndex))),
                        reverseStartY = canvasHeight - startY - laneHeight - display.firstLabelHeight; // reverse startY

                    // special for type multi lane
                    if (vp.isMultiLane) {
                    // scaling labels to show on canvas
                        self._generateLabels(manager, canvas, signal, lanes, startY);

                        if (vp.isDashboardView) { // dashboard view
                        // button to show detail view
                            self._generateDashboardActions(manager, canvas, signal, lanes, startY);
                        }
                    }

                    // background scaling
                    if (vp.isSingleLane || (vp.isMultiLane && (vp.isDetailView || (vp.isDashboardView && lanes[laneIndex][0] === signal)))) {
                        self._generateBackgroundScaling(manager, canvas, signal, lanes, reverseStartY, ratio);
                    }
                }
            });

        if (vp.isMultiLane) {
            // button to go back to overview (invisible at the beginning)
            self._generateDetailActions(manager);
        }

        // render drawing
        dc.stroke();
    },

    /**
     * generate action controls for detail view
     *
     * @param manager
     */
    _generateDetailActions: function(manager) {
        var container = manager.container,
            controls = manager.controls,
            elementName = 'detail-view-controls',
            trendDisplay = manager.trendDisplay,
            actionsPath = shmi.ctrl(trendDisplay.getName() + '.' + elementName),
            self = this;

        if (!trendDisplay.ctrls) {
            trendDisplay.ctrls = {};
        }
        if (trendDisplay.ctrls[elementName]) {
            actionsPath = trendDisplay.ctrls[elementName];
        }

        if (!actionsPath) {
            // control actions container
            var cfg = {
                "class-name": "detail-view-controls hidden", // hidden because it is needed for detail view only
                "name": elementName
            };
            actionsPath = shmi.createControl("container", container.parentNode.parentNode.parentNode, cfg, "div", 'before');
            actionsPath.enable();

            trendDisplay.ctrls[elementName] = actionsPath;

            // create close button
            controls.applyButtonCloseView(actionsPath.element, self);
        }
    },

    /**
     * generate action controls for dashboard
     *
     * @param manager
     * @param canvas
     * @param signal
     * @param lanes
     * @param startY
     */
    _generateDashboardActions: function(manager, canvas, signal, lanes, startY) {
        var container = manager.container,
            laneIndex = signal.laneIndex,
            controls = manager.controls,
            canvasHeight = canvas.height / 2,
            elementName = 'actions-lane-' + laneIndex,
            trendDisplay = manager.trendDisplay,
            actionsPath = shmi.ctrl(trendDisplay.getName() + '.' + elementName),
            display = trendDisplay.config.display,
            labelHeight = display.labelHeight,
            count = lanes.length < display.lanesPerView ? lanes.length : display.lanesPerView, // max lanes before vertical scrolling
            allLabelHeight = labelHeight * count,
            allLaneHeight = /*( ( canvasHeight - allLabelHeight ) / count ) < 50 ? 250 :*/ canvasHeight - allLabelHeight,
            laneHeight = allLaneHeight / count;

        if (!trendDisplay.ctrls) {
            trendDisplay.ctrls = {};
        }
        if (trendDisplay.ctrls[elementName]) {
            actionsPath = trendDisplay.ctrls[elementName];
        }

        manager.curves = this.curves;

        if (!actionsPath) {
            // control actions container
            var cfg = {
                "class-name": "action",
                "name": elementName
            };
            actionsPath = shmi.createControl("container", container.parentNode.parentNode.parentNode, cfg, "div");

            trendDisplay.ctrls[elementName] = actionsPath;

            actionsPath.element.style.top = (startY + display.firstLabelHeight) + "px";
            actionsPath.element.style.height = laneHeight + "px";
            actionsPath.enable();
            // create detail view button
            controls.applyButtonDetailView(actionsPath.element, this, laneIndex, signal);
        } else { // set new position on resize
            actionsPath.element.style.top = (startY + display.firstLabelHeight) + "px";
            actionsPath.element.style.height = laneHeight + "px";

            if (trendDisplay.changingView !== "applyButtonDetailView") {
                actionsPath.element.className = actionsPath.element.className.replace(/(?:^|\s)hidden(?!\S)/g, '');
            }
        }
    },

    /*
     * generate scaling for visualising and improving readability
     *
     * @param manager
     * @param canvas
     * @param signal current signal
     * @param lanes all lanes and signals
     * @param startY starting point calculation for each lane from the canvas bottom
     * @param ratio ratio of each lane depends on fix label height and canvas height
     */
    _generateBackgroundScaling: function(manager, canvas, signal, lanes, startY, ratio) {
        var container = manager.container,
            vp = manager.viewport,
            ctx = this.drawingContext,
            dc = canvas.getContext("2d"),
            trendDisplay = manager.trendDisplay,
            display = trendDisplay.config.display,
            dividend = vp.isDashboardView ? (lanes.length < display.lanesPerView ? lanes.length : display.lanesPerView) * ratio : ratio, // max lanes before vertical scrolling
            stylesElement = shmi.getUiElement("styles", container.parentElement),
            styleElement,
            gridStrokeStyle = "rgba(0,0,0,0.5)",
            gridStrokeStyleHl = "rgba(0,0,0,1)",
            gridStrokeStyleMidnight = "rgba(240, 241, 242, 1)",
            gridStrokeStyleTimestamp = "rgba(162, 174, 177, 1)",
            gridStrokeWidth = 0.4,
            gridStrokeWidthHl = 0.4,
            gridStrokeWidthMidnight = 2,
            gridStrokeWidthTimestamp = 0.4,
            lineLevel;

        // read styles from CSS elements
        if (stylesElement) {
            styleElement = shmi.getUiElement("grid-stroke", stylesElement);
            if (styleElement) {
                gridStrokeStyle = getComputedStyle(styleElement).color; //eslint-disable-line no-undef
                gridStrokeWidth = parseFloat(getComputedStyle(styleElement).strokeWidth); //eslint-disable-line no-undef
            }
            styleElement = shmi.getUiElement("grid-stroke-hl", stylesElement);
            if (styleElement) {
                gridStrokeStyleHl = getComputedStyle(styleElement).color; //eslint-disable-line no-undef
                gridStrokeWidthHl = parseFloat(getComputedStyle(styleElement).strokeWidth); //eslint-disable-line no-undef
            }
        }

        if (vp.isMultiLane) {
            // render horizontal lines
            if (signal.scaleVisible && signal.visible) {
                dc.setLineDash([0, 0]);
                dc.lineWidth = gridStrokeWidth;

                if (vp.isDashboardView) {
                    // 11 lines for swim lane
                    //[ 0, (10 / dividend), (20 / dividend), (30 / dividend), (40 / dividend), (50 / dividend), (60 / dividend), (70 / dividend), (80 / dividend), (90 / dividend), (100 / dividend) ]
                    // 6 lines for swim lane
                    [0, (20 / dividend), (40 / dividend), (60 / dividend), (80 / dividend), (100 / dividend)].
                        forEach(function(lineLevelParam) {
                            var n = ctx.value2Y((ctx.maxValue * lineLevelParam / 100)) - startY;

                            // Highlight top and bottom border line
                            if (lineLevelParam === 0 || lineLevelParam === (100 / dividend)) {
                                dc.strokeStyle = gridStrokeStyleHl;
                                dc.lineWidth = gridStrokeWidthHl;
                            } else {
                                dc.strokeStyle = gridStrokeStyle;
                                dc.lineWidth = gridStrokeWidth;
                            }
                            dc.beginPath();

                            dc.moveTo(0, n);
                            dc.lineTo(canvas.width, n);

                            dc.stroke();
                        });
                } else {
                    // 10 lines for trend view
                    //[ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ]
                    // 19 lines for trend view
                    // [0, 5 / dividend, 10 / dividend, ... 95 / dividend, 100 / dividend]
                    for (var step = 0; step <= 100; step += 5) {
                        lineLevel = step / dividend;
                        var n = ctx.value2Y((ctx.maxValue * lineLevel / 100)) - startY - 13;

                        // Highlight middle border line
                        if (lineLevel === 0 || lineLevel === (50 / dividend) || lineLevel === (100 / dividend)) {
                            dc.strokeStyle = gridStrokeStyleHl;
                            dc.lineWidth = gridStrokeWidthHl;
                        } else {
                            dc.strokeStyle = gridStrokeStyle;
                            dc.lineWidth = gridStrokeWidth;
                        }
                        dc.beginPath();

                        dc.moveTo(0, n);
                        dc.lineTo(canvas.width, n);

                        dc.stroke();
                    }
                }
            }
        } else { // vp.isSingleLane (old TrendDisplay)
            var nullValue = ctx.minValue,
                fullValue = ctx.maxValue,
                fullX = canvas.width,
                fullY = canvas.height;

            // customize stroke style
            dc.lineWidth = gridStrokeWidth;
            dc.strokeStyle = gridStrokeStyle;
            dc.lineDashOffset = 0;

            dc.beginPath();

            // render horizontal guides improving readability
            [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].forEach(function(guideLevel) {
                var guideY = ctx.value2Y(nullValue + (fullValue - nullValue) * guideLevel / 100);

                dc.moveTo(0, guideY);
                dc.lineTo(fullX, guideY);
            });

            // render vertical guides marking certain timestamps
            // detect timestamps to mark
            shmi.visuals.controls.TrendDisplay.CanvasBackground.getVerticalRasterTimestamps(this.units, vp).forEach(function(ts) {
                var timeX = ctx.time2X(ts, 0);

                dc.moveTo(timeX, 0);
                dc.lineTo(timeX, fullY);
            });

            dc.stroke();
        }

        var rasterTimestamps = shmi.visuals.controls.TrendDisplay.CanvasBackground.getVerticalRasterTimestamps(this.units, vp);

        // render full vertical raster lines marking timestamps
        if (display.hasVerticalScaling) {
            // draw line on each timestamp
            dc.lineWidth = gridStrokeWidthTimestamp;
            dc.strokeStyle = gridStrokeStyleTimestamp;
            dc.beginPath();

            rasterTimestamps.forEach(function(ts) {
                // detect timestamps to mark
                var timeX = ctx.time2X(ts, 0);

                // draw line
                dc.moveTo(timeX, 0);
                dc.lineTo(timeX, canvas.height);
            });
            dc.stroke();
        }

        // draw line at midnight
        dc.lineWidth = gridStrokeWidthMidnight;
        dc.strokeStyle = gridStrokeStyleMidnight;
        dc.beginPath();
        rasterTimestamps.forEach(function(ts) {
            // detect timestamps to mark
            var timeX = ctx.time2X(ts, 0);

            // show date
            var currentDateTime = new Date(ts * 1000),
                tDisplay = manager.trendDisplay,
                utcTime = tDisplay.config["utc-time"] ? tDisplay.config["utc-time"] : false,
                dateTime = new Date(utcTime ? currentDateTime.valueOf() + currentDateTime.getTimezoneOffset() * 60000 : currentDateTime);

            // only at midnight
            if (dateTime.getHours() === 0 && dateTime.getMinutes() === 0 && dateTime.getSeconds() === 0) {
                // draw line
                dc.moveTo(timeX, 0);
                dc.lineTo(timeX, ctx.value2Y(0));
            }
        });
        dc.stroke();
    },

    /**
     * generate label, unit and scales (min/max)
     * for multi lanes show dropdown
     *
     * @param manager
     * @param canvas
     * @param signal
     * @param lanes
     * @param startY
     */
    _generateLabels: function(manager, canvas, signal, lanes, startY) {
        var container = manager.container,
            vp = manager.viewport,
            controls = manager.controls,
            doc = container.ownerDocument,
            //vIndex       = signal.valueIndex,
            laneIndex = signal.laneIndex,
            trendDisplay = manager.trendDisplay,
            display = trendDisplay.config.display,
            count = vp.isDashboardView ? lanes.length < display.lanesPerView ? lanes.length : display.lanesPerView : 1, // max lanes before vertical scrolling
            path = container.parentNode.parentNode.querySelector(".lane.index-" + laneIndex),
            multiLaneSelectBox = shmi.ctrl(trendDisplay.getName() + '.select-box-multi-lane-' + laneIndex),
            labelHeight = display.labelHeight,
            allLabelHeight = labelHeight * count,
            canvasHeight = canvas.height / 2,
            allLaneHeight = canvasHeight - allLabelHeight,
            laneHeight = allLaneHeight / count,
            yPos = vp.isDashboardView ? laneHeight : laneHeight; // @todo: use global display.labelOffset but currently not needed

        if (trendDisplay.detailView !== undefined && trendDisplay.detailView !== null && trendDisplay.detailView !== laneIndex) {
            return;
        }

        if (signal.scaleVisible && signal.visible) {
            if (!path) {
                var l,
                    u,
                    v,
                    vMin,
                    vMax,
                    c,
                    s = signal,
                    titleLane = "Multi-Lane " + (laneIndex + 1),
                    titleUnit = "%",
                    titleMax = "max",
                    titleMin = "min";

                // for single lane show signal titles
                //if( vp.isDetailView || lanes[laneIndex].length == 1 ) {
                titleLane = String(s.label);
                titleUnit = String(s.unit);
                titleMax = String(s.max);
                titleMin = String(s.min);
                //}

                // label text
                l = doc.createElement("div");
                l.className = "label";
                //l.style.top = (display.handleBarOffset / 2) + "px";
                l.appendChild(doc.createTextNode(shmi.localize(titleLane)));

                // unit
                u = doc.createElement("div");
                u.className = "unit";
                u.style.top = yPos + "px";
                u.appendChild(doc.createTextNode(shmi.localize(titleUnit)));

                // lane container
                /*var cfg = {
                "class-name": "lane index-" + laneIndex,
                "name": "lane-index-" + laneIndex
                };
                path = shmi.createControl("container", container.parentNode, cfg, "div", 'after');
                path.element.style.top = startY + "px";
                path.enable();path.element.appendChild(l);*/

                c = doc.createElement("div");
                c.className = "lane index-" + laneIndex;
                c.style.top = startY + "px";

                c.appendChild(l);

                // select box
                if (!multiLaneSelectBox && (vp.isDashboardView || vp.isDetailView) && lanes[laneIndex].length > 1) {
                    controls.applySelectBoxLane(l, this, signal, lanes);
                } else if (multiLaneSelectBox && (vp.isDashboardView || (display.demo && vp.isDetailView))) {
                    multiLaneSelectBox.element.className = multiLaneSelectBox.element.className.replace(/(?:^|\s)hidden(?!\S)/g, '');
                }

                v = doc.createElement("div");
                v.className = "values";

                // max value label
                vMax = doc.createElement("div");
                vMax.className = "vMax";
                vMax.appendChild(doc.createTextNode(titleMax));
                v.appendChild(vMax);

                // min value label
                vMin = doc.createElement("div");
                vMin.className = "vMin";
                vMin.style.top = Math.ceil(yPos) + "px";
                vMin.appendChild(doc.createTextNode(titleMin));
                v.appendChild(vMin);

                c.appendChild(v);
                c.appendChild(u);

                container.parentNode.parentNode.appendChild(c);

                // scaling values
                if (vp.isDetailView && !display.demo && trendDisplay.changingView !== "applyButtonCloseView") {
                    this._generateSignalScalingValues(manager, canvas, signal);
                }
            } else { // set new position on resize
                path.style.top = startY + "px";
                path.getElementsByClassName('unit')[0].style.top = yPos + "px";
                //path.getElementsByClassName( 'label' )[0].style.top = (display.handleBarOffset / 2) + "px";
                path.getElementsByClassName('vMin')[0].style.top = Math.ceil(yPos) + "px";

                //path.getElementsByClassName( 'label' )[0].

                // min/max values
                var vMaxLabelClassName = path.getElementsByClassName('vMax')[0].className,
                    vMinLabelClassName = path.getElementsByClassName('vMin')[0].className;

                if (vp.isDashboardView && trendDisplay.changingView !== "applyButtonDetailView") {
                    path.getElementsByClassName('vMax')[0].className = vMaxLabelClassName.replace(/(?:^|\s)hidden(?!\S)/g, '');
                    path.getElementsByClassName('vMin')[0].className = vMinLabelClassName.replace(/(?:^|\s)hidden(?!\S)/g, '');

                    if (display.demo && multiLaneSelectBox) {
                        multiLaneSelectBox.element.className = multiLaneSelectBox.element.className.replace(/(?:^|\s)hidden(?!\S)/g, '');
                    }
                } else if (vp.isDetailView && trendDisplay.changingView !== "applyButtonCloseView") {
                    if (!display.demo) {
                        if (!vMaxLabelClassName.match(/(?:^|\s)hidden(?!\S)/g)) {
                            path.getElementsByClassName('vMax')[0].className += ' hidden';
                            path.getElementsByClassName('vMin')[0].className += ' hidden';
                        }

                        // scaling values
                        this._generateSignalScalingValues(manager, canvas, signal);
                    } else if (multiLaneSelectBox && !multiLaneSelectBox.element.className.match(/(?:^|\s)hidden(?!\S)/g)) {
                        multiLaneSelectBox.element.className += ' hidden';
                    }

                    // show lane data
                    path.className = path.className.replace(/(?:^|\s)hidden(?!\S)/g, '');
                }
            }
        }
    },

    /**
     * generate signal scaling values on canvas
     *
     * @param manager
     * @param canvas
     * @param signal
     */
    _generateSignalScalingValues: function(manager, canvas, signal) {
        var container = manager.container,
            vp = manager.viewport,
            doc = container.ownerDocument,
            size = manager.getCanvasSize(),
            cfg = this.units,
            ctx = this.curves[signal.valueIndex].drawingContext,
            trendDisplay = manager.trendDisplay,
            display = trendDisplay.config.display,
            labelHeight = display.labelHeight,
            scalingValues = container.parentNode.parentNode.querySelector(".scaling-values"),
            //style        = manager.config.trend.items[signal.alias].style.getQualifiedByCss( this.css ),
            //dims         = scaleComponent.dimensions,
            dim = {
                x: 0,
                width: 0
            }, // dims.signals[signal.valueIndex] || {x: 0, width: 0},
            valueRange = signal.max - signal.min,
            accuracy = Math.pow(10, Math.ceil((Math.log(valueRange) / Math.LN10))),
            valuesToShow = {},
            lastValue = null,
            scalingContainer;

        // scale values to show
        if (!scalingValues) {
            scalingContainer = doc.createElement("div");
            scalingContainer.className = "scaling-values";
        } else {
            scalingContainer = scalingValues;
        }

        // Bugfix: remove already generated labels
        if (scalingContainer.childElementCount) {
            while (scalingContainer.firstChild) {
                scalingContainer.removeChild(scalingContainer.firstChild);
            }
            this.labels = [];
        }

        // Bugfix: incorrect visible canvas height due to label height offset
        var valueSpanPerPixelRatio = valueRange / (ctx.visibleHeightInPixel - labelHeight);

        // find all values to show on current scale
        shmi.visuals.controls.TrendDisplay.Scale.forEachStep(ctx, size, cfg, cfg.scale.max, function(value, relative, basicDetail, currentDetail) {
            value = Math.round(value * accuracy) / accuracy;
            var name = signal.alias || signal.name,
                upper = (relative >= 1.0),
                lower = (relative <= 0);
            if (value !== lastValue && relative >= -0.05 && relative <= 1.05) {
                valuesToShow[value] = [ctx.value2Y(value, 1, vp.zoomY, valueSpanPerPixelRatio) - size.y, name + (lower ? " lower value" : upper ? " upper value" : " value")];
                lastValue = value;
            }
            if (upper) {
                if (valuesToShow[value][1].indexOf("upper") === -1) {
                    valuesToShow[value][1] = name + " upper value";
                }
            }
            if (lower) {
                if (valuesToShow[value][1].indexOf("lower") === -1) {
                    valuesToShow[value][1] = name + " lower value";
                }
            }
        });

        var reusables = [];

        this.labels = this.labels.
            map(function(label) {
                var key = label.value;
                if (shmi.objectHasOwnProperty(valuesToShow, key)) {
                    // label is still in use -> just move to its new position
                    label.elem.style.top = String(valuesToShow[key][0]) + "px";
                    valuesToShow[key] = undefined;

                    return label;
                }

                reusables.push(label);

                return null;
            }).
            filter(function(i) {
                return i;
            }).
            concat(
                Object.keys(valuesToShow).sort().
                    map(function(value) {
                        var meta = valuesToShow[value],
                            ref;

                        if (meta) {
                            if (reusables.length) {
                                ref = reusables.shift();
                                ref.elem.firstChild.nodeValue = String(value);
                                ref.value = value;
                            } else {
                                ref = {
                                    value: value,
                                    elem: doc.createElement("span")
                                };

                                ref.elem.appendChild(doc.createTextNode(String(value)));

                                ref.elem.style.left = String(dim.x) + "px";
                                ref.elem.style.width = String(dim.width) + "px";

                                // show in signal color style
                                //style.applyOnElement( ref.elem );

                                scalingContainer.appendChild(ref.elem);
                            }
                            ref.elem.className = meta[1];
                            ref.elem.style.top = String(meta[0]) + "px";

                            return ref;
                        }
                        return null;
                    }).
                    filter(function(i) {
                        return i;
                    }));

        reusables.forEach(function(unused) {
            unused.elem.parentNode.removeChild(unused.elem);
        });

        if (!scalingValues) {
            container.parentNode.parentNode.appendChild(scalingContainer);
        }
    },

    /**
     * highlight one curve of a multi lane (all other turn to grey)
     *
     * @param manager
     * @param signal
     * @private
     */
    _highlightTrendCurve: function(manager, signal) {
        var container = manager.container,
            viewport = manager.viewport,
            trendDisplay = manager.trendDisplay,
            vIndex = signal.valueIndex,
            laneIndex = signal.laneIndex,
            config = trendDisplay.config,
            path = container.parentNode.parentNode.querySelector(".lane.index-" + laneIndex),
            localItemStyles = localStorage['itemStyles'] ? JSON.parse(localStorage['itemStyles']) : [], //eslint-disable-line no-undef
            itemStyle;

        // set colors and save old ones in local storage
        config.trend.items.forEach(function(item, index) {
            // change all other curves to grey
            var defaultColor = config.styleManagers.inactiveStyle.color;
            if (item.laneIndex === laneIndex && vIndex !== index) {
                itemStyle = config.styles[item.styleIndex];
                if (localItemStyles.length && typeof localItemStyles[item.signalIndex] !== 'undefined') {
                    localItemStyles[item.signalIndex] = itemStyle.color;
                }
                config.styles[item.styleIndex] = itemStyle;
                localItemStyles[item.signalIndex] = itemStyle.color;
            } else if (item.laneIndex === laneIndex && vIndex === index) { // set item color for selected one
                itemStyle = config.styles[item.styleIndex];
                if (itemStyle.color === defaultColor) {
                    itemStyle.color = localItemStyles[item.signalIndex];
                }
                config.styles[item.styleIndex] = itemStyle;
                localItemStyles[item.signalIndex] = null;
            }
            localStorage.setItem("itemStyles", JSON.stringify(localItemStyles)); //eslint-disable-line no-undef
        });

        // change label
        if (path) {
            path.getElementsByClassName('label')[0].textContent = shmi.localize(signal.label);
            path.getElementsByClassName('unit')[0].textContent = shmi.localize(signal.unit);
            path.getElementsByClassName('vMin')[0].textContent = String(signal.min);
            path.getElementsByClassName('vMax')[0].textContent = String(signal.max);
        }

        trendDisplay.config.update(config).then(function() {
            // set current viewport settings
            var continuous = trendDisplay.getComponents("trend-display-continuous")[0], // manager.config.name
                vp = continuous.viewport;

            //continuous._updateViewportRelatedItems(vp);

            vp = trendDisplay.controls["0"].viewport; // manager.config.name

            // disable live mode, otherwise handleTime is set to current time
            trendDisplay.items.items.live.writeValue(0); // vp.isLive

            if (viewport.zoomX !== vp.zoomX) {
                trendDisplay.items.items.zoomX.writeValue(vp.zoomX * 100);
            }
            if (viewport.zoomY !== vp.zoomY) {
                trendDisplay.items.items.zoomY.writeValue(vp.zoomY * 100);
            }
            setTimeout(function() {
                if (viewport.leftTime !== vp.leftTime) {
                    trendDisplay.items.items.leftTime.writeValue(vp.leftTime);
                }
                if (viewport.handleTime !== vp.handleTime) {
                    trendDisplay.items.items.handleTime.writeValue(vp.handleTime);
                }
            }, 250);
        });
    },

    /**
     * toggle display between dashboard and detail view
     * only for display type "multiLane"
     *
     * @param manager
     * @param signal
     * @param view display view type dashboard or detail
     * @private
     */
    _updateTrendDisplay: function(manager, signal, viewParam) {
        var container = manager.container,
            vp = manager.viewport,
            trendDisplay = manager.trendDisplay,
            vIndex = signal ? signal.valueIndex : null,
            view = viewParam || "dashboard";

        // unfrozen to change properties
        /*if(Object.isFrozen(display)) {
        display = Object.unfreeze(display);
        }
        if(Object.isFrozen(config)) {
        config = Object.unfreeze(config);
        }*/
        var config = shmi.cloneObject(trendDisplay.config),
            display = config.display;

        // change view type
        config.display.view = view;
        if (view === "detail") {
            vp.isDetailView = true;
            shmi.removeClass(trendDisplay.element, 'swim-lane');
        } else {
            vp.isDashboardView = true;
            shmi.addClass(trendDisplay.element, 'swim-lane');
        }

        // remove lane elements
        var laneElements = container.parentNode.querySelectorAll(".lane");
        if (laneElements) { // remove invisible curve labels
            var iterateNodeList = shmi.requires("visuals.tools.iterate.iterateNodeList");
            iterateNodeList(laneElements, function(unused, index) {
                unused.remove();
            });
        }

        // show only selected signal(s)
        if (vIndex !== null) {
            config.trend.items.forEach(function(item, index) {
                if (index === vIndex) {
                    item.scaleVisible = true;
                    item.visible = true;
                } else if (!display.demo && signal.laneIndex === item.laneIndex) {
                    // signal on same lane (multi lane)
                    item.scaleVisible = false;
                    item.visible = true;
                } else {
                    item.scaleVisible = false;
                    item.visible = false;
                }
            });
        } else {
            config.trend.items.forEach(function(item, index) {
                item.scaleVisible = true;
                item.visible = true;
            });
        }

        // use current viewport properties, i.e. zoom
        config.viewport = vp;

        // remove event listener
        //trendDisplay._manageResizeListeners( false );

        // update trend display
        trendDisplay.config.update(config).then(function() {
            /*if(vp.isDetailView) {
            window.dispatchEvent(new Event('resize'));
            }*/
        });
    }

};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.CanvasBackground, shmi.visuals.controls.TrendDisplay.AbstractRenderer, "CanvasBackground");

shmi.visuals.controls.TrendDisplay.CanvasBackground.getVerticalRasterTimestamps = function(unitsCfg, viewport) {
    var min = viewport.minTime,
        visible = viewport.visibleTimeSpan,
        full = (1 + 2 * viewport.prefetch) * visible,
        bg = unitsCfg.background,
        unitBg = shmi.visuals.controls.TrendDisplay.Timeline.detectVisibleUnit(unitsCfg.timeLine.available, visible, bg.timeLineMin, bg.timeLineMax),
        unitTl = shmi.visuals.controls.TrendDisplay.Timeline.detectVisibleUnit(unitsCfg.timeLine.available, visible, unitsCfg.timeLine.min, unitsCfg.timeLine.max),
        units = [],
        ts,
        end;

    if (unitBg > 0) {
        for (ts = Math.floor((viewport.leftRenderTime - min) / unitBg) * unitBg, end = ts + full; ts < end; ts += unitBg) {
            units.push(min + ts);
        }
    }

    if (unitTl > 0) {
        for (ts = Math.floor((viewport.leftRenderTime - min) / unitTl) * unitTl, end = ts + full; ts < end; ts += unitTl) {
            units.push(min + ts);
        }
    }

    units = units.sort().filter(function(value, index, array) {
        return !index || (value !== array[index - 1]);
    });

    return units;
};

/**
 * load timestamps
 * @returns {number[]} timestamps
 */
shmi.visuals.controls.TrendDisplay.CanvasBackground.getTimestamps = function() {
    // first example timestamps @todo: set and get from database
    var timestamps = [1474439364.872986, 1474954461.981989, 1474957469.004987, 1475080519.644011, 1475138756.23, 1475128448.034015, 1475129446.76, 1475135071.640012, 1475153465.722981, 1476695351];

    return timestamps;
};

/**
 * Validates configuration regarding background rendering.
 *
 * @param {Object.<string,*>} configuration qualified configuration object
 */
shmi.visuals.controls.TrendDisplay.onConfiguration.push([-10, function _onConfigureBackground(cfg, tdControl, Parsers) {
    "use strict";

    if (cfg) {
        var units = cfg.display.units,
            background,
            a,
            b;

        // ensure container is basically available
        background = units.background = Parsers.isObject(units.background);

        // validate min/max # of vertical dividers
        a = parseInt(background.timeLineMin);
        if (isNaN(a) || a < 1) {
            a = 2;
        }
        b = parseInt(background.timeLineMax);
        if (isNaN(b) || b < 1) {
            b = (cfg.display.hasVerticalScaling) ? 16 : 1440;
        }

        background.timeLineMin = Math.min(a, b);
        background.timeLineMax = Math.max(a, b);

        // validate min/max # of horizontal dividers
        a = parseInt(background.scaleMin);
        if (isNaN(a) || a < 1) {
            a = 2;
        }
        b = parseInt(background.scaleMax);
        if (isNaN(b) || b < 1) {
            b = 16;
        }

        background.scaleMin = Math.min(a, b);
        background.scaleMax = Math.max(a, b);
    }
}
]);

/**
 * Manages curve drawn in context of a manager representing single signal of a
 * trend.
 *
 * @param {shmi.visuals.controls.TrendDisplay.AbstractRenderingManager} manager
 * @param {shmi.visuals.controls.TrendDisplay.Signal} signal
 * @extends shmi.visuals.controls.TrendDisplay.AbstractRenderer
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.CanvasCurve = function CanvasCurve(manager, signal) {
    "use strict";

    shmi.visuals.controls.TrendDisplay.CanvasCurve._super.call(this, manager, signal);
};

shmi.visuals.controls.TrendDisplay.CanvasCurve.prototype = {

    _createPathElement: function(canvas) {
        var className = "path signal " + (this.signal.alias || this.signal.name),
            path = canvas.getElementsByClassName(className)[0];

        if (!path) {
            path = canvas.ownerDocument.createElement("path");
            path.setAttribute("class", className);

            path = canvas.appendChild(path);
        }

        return path;
    },

    /**
     * (Re-)renders curve on canvas.
     */
    render: function(manager, data, canvas) {
        "use strict";

        if (this.signal && (!this.visible || !data || !data.values)) {
            return null;
        }

        var vp = manager.viewport,
            dc = canvas.getContext("2d"),
            ctx = this.drawingContext,
            startTS = vp.leftRenderTime,
            endTS = vp.rightRenderTime,
            signal = this.signal,
            vIndex = signal.valueIndex,
            laneIndex = vp.isMultiLane ? signal.laneIndex : 0,
            symbol = this.style.symbol,
            count = manager.config.trend.signals.length,
            canvasHeight = canvas.height / 2,
            trendDisplay = manager.trendDisplay,
            display = trendDisplay.config.display,
            labelHeight = display.labelHeight,
            laneHeight = 0,
            startY = (vp.isSingleLane) ? 0 : display.firstLabelHeight + 30,
            dividend = canvasHeight / (canvasHeight - labelHeight),
            symbols = [],
            guides = [],
            lanes = [],
            hiddenV = NaN,
            prevV = NaN,
            move = true,
            prevTS;

        if (symbol) {
            guides = shmi.visuals.controls.TrendDisplay.CanvasBackground.getVerticalRasterTimestamps(manager.config.display.units, vp);
        }

        // color and style for curve
        this.style.applyOnCanvas(dc);

        dc.beginPath();

        if (data.values.length) {
            prevTS = data.values[0][0];
        }

        // swim lane calculation
        if (vp.isMultiLane && vp.isDashboardView) {
            var ratio,
                allLabelHeight,
                allLaneHeight,
                heightOffset,
                curves = manager.curves; // Bugfix: not available for singleLane...

            // @todo: set global config this.lanes
            if (curves) {
                curves.forEach(function(curve, index) {
                    signal = curve.signal;
                    if (typeof lanes[signal.laneIndex] === 'undefined') {
                        lanes[signal.laneIndex] = [];
                    }
                    lanes[signal.laneIndex].push(signal);
                });
            }

            count = lanes.length < display.lanesPerView ? lanes.length : display.lanesPerView; // max lanes before vertical scrolling
            ratio = canvasHeight / (canvasHeight - (count * labelHeight)); // ratio of each lane
            dividend = count * ratio;
            allLabelHeight = labelHeight * count;
            allLaneHeight = /*( ( canvasHeight - allLabelHeight ) / count ) < 50 ? 250 :*/ canvasHeight - allLabelHeight;
            laneHeight = allLaneHeight / count;
            heightOffset = ((canvasHeight / 2) - (laneHeight / 2) + display.firstLabelHeight); // calc special offset dependent on resolution and lane count
            startY = (labelHeight * (laneIndex)) + (laneHeight * (laneIndex)) + heightOffset;
        }

        data.values.forEach(function(sample, sIndex) {
            var currTS = sample[0],
                currV = sample[1][vIndex],
                zeroY = startY, // Zero point of current lane
                x,
                y1,
                y2,
                s,
                i,
                l,
                r;

            if (!sIndex) {
                // got first sample

                // remove all stored guides preceding initial sample
                for (i = 0, l = guides.length; i < l; i++) {
                    if (guides[i] >= currTS) {
                        break;
                    }
                }

                if (i > 0) {
                    guides.splice(0, i - 1);
                }
            }

            l = prevTS > startTS ? prevTS : startTS;
            r = currTS < endTS ? currTS : endTS;

            if (r > l) {
                x = ctx.time2X(currTS, 0);

                if (isFinite(currV)) {
                    // render line chart
                    y1 = y2 = ctx.value2Y(currV, 0);

                    // show on correct lane
                    if (vp.isMultiLane && vp.isDashboardView) {
                        y1 = (y1 / dividend) + startY;
                        y2 = (y2 / dividend) + startY;
                    }

                    if (move && isFinite(hiddenV)) {
                        move = false;
                    }

                    move |= sample[2];

                    if (move) {
                        dc.moveTo(x, y1);
                    } else {
                        // start position
                        if (isFinite(hiddenV)) {
                            // previous position hasn't been set due to
                            // being out of visible area
                            // -> move to coords of previous sample first
                            var movX = ctx.time2X(prevTS, 0);
                            var movY = ctx.value2Y(hiddenV, 0);
                            if (vp.isMultiLane && vp.isDashboardView) {
                                movY = (movY / dividend) + startY;
                            }

                            dc.moveTo(movX, movY);
                        }
                        dc.lineTo(x, y1);
                    }
                } else if (currV && shmi.objectHasOwnProperty(currV, "min") &&
                    shmi.objectHasOwnProperty(currV, "max")) {
                    // render candle-stick chart
                    move = true;

                    var a = parseFloat(currV.min),
                        b = parseFloat(currV.max);

                    if (isNaN(a) || isNaN(b)) {
                        return;
                    }

                    y1 = ctx.value2Y(a, 0);
                    y2 = ctx.value2Y(b, 0);

                    if (y1 === y2) {
                        y2 = y1 + 1; // check with + 0.01
                    }

                    // show on correct lane
                    if (vp.isMultiLane && vp.isDashboardView) {
                        y1 = (y1 / dividend) + startY;
                        y2 = (y2 / dividend) + startY;
                    }

                    dc.moveTo(x, y1);
                    dc.lineTo(x, y2);
                }

                if (symbol && guides.length) {
                    if (currTS >= guides[0]) {
                        if (prevTS <= guides[0]) {
                            if (isFinite(currV) && isFinite(prevV)) {
                                if (move) {
                                    // on broken linear diagram symbols are
                                    // rendered on continuation of graph
                                    s = guides.shift();

                                    if (!guides.length || currTS - s < guides[0] - currTS) {
                                        symbols.push({
                                            x: x,
                                            y: y1
                                        });
                                    }
                                } else {
                                    // on linear diagram symbol is rendered on
                                    // intersection of guide with linear graph
                                    x = guides.shift();

                                    s = (currTS - prevTS);
                                    s = s ? (x - prevTS) / (s / 2) : 1;

                                    y1 = ctx.value2Y(prevV);

                                    // show on correct lane
                                    if (vp.isMultiLane && vp.isDashboardView) {
                                        y1 = (y1 / dividend) + startY;
                                    }

                                    symbols.push({
                                        x: ctx.time2X(x, zeroY),
                                        y: Math.round((y2 * s + y1 * (2 - s)) / 2)
                                    });
                                }
                            } else {
                                // on candle-stick diagram symbol is rendered
                                // on candle-stick next to guide centered
                                // between min/max values
                                guides.shift();

                                symbols.push({
                                    x: x,
                                    y: Math.round((y1 + y2) / 2)
                                });
                            }
                        } else {
                            guides.shift();
                        }
                    }
                }

                if (isFinite(currV)) {
                    move = false;
                }

                hiddenV = NaN;
            } else {
                // current sample isn't visible
                // -> but next one might be
                //    -> remember current sample's value for rendering line
                //       if required
                hiddenV = currV;
            }

            if (currTS > endTS) {
                // don't waste time on checking visibility of later samples
                return;
            }

            prevV = isFinite(currV) ? Number(currV) : NaN;
            prevTS = currTS;
        });

        dc.stroke();

        if (symbols.length) {
            return symbol.then(function(img) {
                symbols.forEach(function(pos) {
                    dc.drawImage(img, pos.x - shmi.visuals.controls.TrendDisplay.iconDimensions.width / 2, pos.y - shmi.visuals.controls.TrendDisplay.iconDimensions.height / 2);
                });
            });
        } else {
            return null;
        }
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.CanvasCurve, shmi.visuals.controls.TrendDisplay.AbstractRenderer, "CanvasCurve");

/**
 * Manages drawing of scale related to some signal.
 *
 * @param {shmi.visuals.controls.TrendDisplay.AbstractRenderingManager} manager
 * @param {Array.<shmi.visuals.controls.TrendDisplay.CanvasCurve>} curves
 * @param {shmi.visuals.controls.TrendDisplay.Signal} signal
 * @param {int} maxScaleUnits max. number of units to show on scale
 * @extends shmi.visuals.controls.TrendDisplay.CanvasCurve
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.CanvasRuler = function CanvasRuler(manager, curves, signal) {
    "use strict";

    shmi.visuals.controls.TrendDisplay.CanvasRuler._super.call(this, manager, {
        min: signal.min,
        max: signal.max,
        curve: {}
    });

    Object.defineProperty(this, "curves", {
        value: curves
    });

    Object.defineProperty(this, "units", {
        value: manager.config.display.units
    });
};

shmi.visuals.controls.TrendDisplay.CanvasRuler.prototype = {

    _createPathElement: function(canvas) {
        var className = "path ruler",
            path = canvas.getElementsByClassName(className)[0];

        if (!path) {
            path = canvas.ownerDocument.createElement("path");
            path.setAttribute("class", className);

            path = canvas.appendChild(path);
        }

        return path;
    },

    _getConfiguredStyle: function() {
        return shmi.visuals.controls.TrendDisplay.Style.defaultStyle;
    },

    /**
     * (Re-)renders curve on canvas.
     */
    render: function(manager, scaleComponent, canvas) {
        "use strict";

        if (!scaleComponent) {
            // this is the case early on loading/creating trend display control
            return;
        }

        var TD = shmi.visuals.controls.TrendDisplay,
            ctx = this.drawingContext,
            size = manager.getCanvasSize(),
            dc = canvas.getContext("2d"),
            dim = scaleComponent.dimensions.ruler,
            cfg = this.units,
            widths = cfg.ruler.majorStepWidths;

        this.style.applyOnCanvas(dc);

        dc.beginPath();

        TD.Scale.forEachStep(ctx, size, cfg, cfg.ruler.max, function(value, relative, basicDetail, currentDetail) {
            var y = ctx.value2Y(value);

            dc.moveTo(dim.x + dim.width - dim.width * widths[currentDetail], y);
            dc.lineTo(size.totalWidth, y);
        });

        dc.stroke();
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.CanvasRuler, shmi.visuals.controls.TrendDisplay.CanvasCurve, "CanvasRuler");

/**
 * Validates configuration regarding ruler.
 *
 * @param {Object.<string,*>} configuration qualified configuration object
 */
shmi.visuals.controls.TrendDisplay.onConfiguration.push([-10, function _onConfigureRuler(cfg, tdControl, Parsers) {
    "use strict";

    if (cfg) {
        var display,
            units,
            ruler,
            a,
            b;

        display = cfg.display = Parsers.isObject(cfg.display);
        units = display.units = Parsers.isObject(display.units);
        ruler = units.ruler = Parsers.isObject(units.ruler);

        if (!Array.isArray(ruler.majorStepWidths)) {
            ruler.majorStepWidths = [1.0, 1.0, 0.7, 0.5, 0.3, 0.2, 0.1, 0.05];
        }

        ruler.majorStepWidths = Parsers.parseFilterAndSort(ruler.majorStepWidths, Parsers.relative, true);

        a = parseInt(ruler.min);
        if (isNaN(a) || a < 1) {
            a = 1;
        }
        b = parseInt(ruler.max);
        if (isNaN(b) || b < 1) {
            b = 10;
        }

        ruler.min = Math.min(a, b);
        ruler.max = Math.max(a, b);
    }
}
]);

/**
 * Manages drawing of scale related to some signal.
 *
 * @param {SHMI_TD.AbstractRenderingManager} manager
 * @param {SHMI_TD.Signal} signal
 * @extends shmi.visuals.controls.TrendDisplay.CanvasCurve
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.CanvasScale = function CanvasScale(manager, signal) {
    "use strict";

    shmi.visuals.controls.TrendDisplay.CanvasScale._super.call(this, manager, signal);

    this.labels = [];

    Object.defineProperty(this, "units", {
        value: manager.config.display.units
    });
};

shmi.visuals.controls.TrendDisplay.CanvasScale.prototype = {
    /**
     * (Re-)renders curve on canvas.
     */
    render: function(manager, scaleComponent, canvas) {
        "use strict";

        if (!scaleComponent) {
            // this is the case early on loading/creating trend display control
            return;
        }

        var signal = this.signal,
            ctx = this.drawingContext,
            size = manager.getCanvasSize(),
            dims = scaleComponent.dimensions,
            dim = dims.signals[this.index] || {
                x: 0,
                width: 0
            },
            accuracy = Math.ceil((Math.log(signal.max - signal.min) / Math.LN10)),
            lastValue = null,
            container = this.manager.container,
            doc = container.ownerDocument,
            cfg = this.units,
            valuesToShow = {};

        accuracy = Math.pow(10, accuracy);

        // find all values to show on current scale
        shmi.visuals.controls.TrendDisplay.Scale.forEachStep(ctx, size, cfg, cfg.scale.max, function(value, relative, basicDetail, currentDetail) {
            value = Math.round(value * accuracy) / accuracy;
            var name = signal.alias || signal.name,
                upper = (relative >= 1.0),
                lower = (relative <= 0);
            if (value !== lastValue && relative >= -0.05 && relative <= 1.05) {
                valuesToShow[value] = [ctx.value2Y(value), name + (lower ? " lower value" : upper ? " upper value" : " value")];
                lastValue = value;
            }
            if (upper) {
                if (valuesToShow[value][1].indexOf("upper") === -1) {
                    valuesToShow[value][1] = name + " upper value";
                }
            }
            if (lower) {
                if (valuesToShow[value][1].indexOf("lower") === -1) {
                    valuesToShow[value][1] = name + " lower value";
                }
            }
        });

        var reusables = [];

        this.labels = this.labels.
            map(function(label) {
                var key = label.value;
                if (shmi.objectHasOwnProperty(valuesToShow, key)) {
                    // label is still in use -> just move to its new position
                    label.elem.style.top = String(valuesToShow[key][0]) + "px";
                    valuesToShow[key] = undefined;

                    return label;
                }

                reusables.push(label);

                return null;
            }).
            filter(function(i) {
                return i;
            }).
            concat(
                Object.keys(valuesToShow).sort().
                    map(function(value) {
                        var meta = valuesToShow[value],
                            ref;

                        if (meta) {
                            if (reusables.length) {
                                ref = reusables.shift();
                                ref.elem.firstChild.nodeValue = String(value);
                                ref.value = value;
                            } else {
                                ref = {
                                    value: value,
                                    elem: doc.createElement("span")
                                };

                                ref.elem.appendChild(doc.createTextNode(String(value)));

                                ref.elem.style.left = String(dim.x) + "px";
                                ref.elem.style.width = String(dim.width) + "px";

                                container.appendChild(ref.elem);
                            }
                            ref.elem.className = meta[1];
                            ref.elem.style.top = String(meta[0]) + "px";

                            return ref;
                        } else {
                            return null;
                        }
                    }).
                    filter(function(i) {
                        return i;
                    }));

        reusables.forEach(function(unused) {
            unused.elem.parentNode.removeChild(unused.elem);
        });
    },

    onDelete: function() {
        if (this.labels) {
            this.labels.forEach(function(label) {
                label.elem.parentNode.removeChild(label.elem);
            });

            this.labels = [];
        }

        this._super(this.CanvasScale, "onDelete");
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.CanvasScale, shmi.visuals.controls.TrendDisplay.CanvasCurve, "CanvasScale");

/**
 * Implements rendering of curve in embedded SVG document.
 *
 * @param {shmi.visuals.controls.TrendDisplay.AbstractRenderingManager} manager
 * @param {shmi.visuals.controls.TrendDisplay.Signal} signal
 * @extends shmi.visuals.controls.TrendDisplay.AbstractRenderer
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.SvgCurve = function SvgCurve(manager, signal) {
    "use strict";

    shmi.visuals.controls.TrendDisplay.SvgCurve._super.call(this, manager, signal);
};

shmi.visuals.controls.TrendDisplay.SvgCurve.prototype = {
    /**
     * (Re-)renders curve on canvas.
     */
    render: function(manager, data) {
        "use strict";

        if (!this.visible || !data || !data.values) {
            this.path.setAttribute("d", "");
            return;
        }

        var signal = this.signal,
            ctx = this.drawingContext,
            vp = manager.viewport,
            startTS = vp.leftRenderTime,
            endTS = vp.rightRenderTime,
            move = true,
            vIndex = signal.valueIndex,
            values = data.values;

        this.style.applyOnSvg(this.path);

        this.path.
            setAttribute("d", values.
                map(function(sample) {
                    var ts = sample[0],
                        value = sample[1][vIndex];

                    if (ts >= startTS && ts <= endTS) {
                        if (isFinite(value)) {
                        // render line chart
                            if (move) {
                                move = false;
                                return "M" + ctx.time2X(ts) + " " + ctx.value2Y(value);
                            } else {
                                return "L" + ctx.time2X(ts) + " " + ctx.value2Y(value);
                            }
                        } else if (value && shmi.objectHasOwnProperty(value, "min") &&
                            shmi.objectHasOwnProperty(value, "max")) {
                        // render candle-stick chart
                            move = true;

                            var a = parseFloat(value.min),
                                b = parseFloat(value.max),
                                x;

                            if (!isNaN(a) && !isNaN(b)) {
                                x = ctx.time2X(ts, 0);

                                a = ctx.value2Y(a, 0);
                                b = ctx.value2Y(b, 0);

                                if (a === b) {
                                    b = a + 1;
                                }

                                return "M" + x + " " + a + "L" + x + " " + b;
                            } else {
                                return null;
                            }
                        } else {
                            return null;
                        }
                    } else {
                        return null;
                    }
                }).join(""));
    },

    _createPathElement: function(canvas) {
        var path = canvas.getElementsByClassName(this.signal.alias || this.signal.name)[0];
        if (!path) {
            path = canvas.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "path");
            path.setAttribute("class", this.signal.alias || this.signal.name);

            path = canvas.appendChild(path);
        }

        return path;
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.SvgCurve, shmi.visuals.controls.TrendDisplay.AbstractRenderer, "SvgCurve");

/**
 * Implements generic API for managing UI canvas containing individual elements.
 *
 * @param {HtmlElement} container
 * @param {HtmlElement} canvas
 * @param {shmi.visuals.controls.TrendDisplay.Viewport} viewport
 * @param {function} elementClass
 * @param {shmi.visuals.controls.TrendDisplay.Configuration} configuration
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.AbstractRenderingManager = function(container, canvas, viewport, elementClass, configuration) {
    "use strict";

    /**
     * Manages notifications sent by this manager to subordinated renderers to
     * draw their content onto canvas.
     *
     * @type {shmi.visuals.controls.TrendDisplay.Messaging}
     */
    var onRender = new shmi.visuals.controls.TrendDisplay.Messaging();

    /**
     * Manages notifications sent on component containing canvas has been
     * resized requiring to update certain information of rendering engines and
     * probably requiring to re-draw all content of canvas.
     *
     * @type {shmi.visuals.controls.TrendDisplay.Messaging}
     */
    var onResize = new shmi.visuals.controls.TrendDisplay.Messaging(true);

    var controls = new shmi.visuals.controls.TrendDisplay.Controls(this, viewport, []);

    /**
     * Refers to HTML element containing canvas in document.
     *
     * @property container
     * @type {HtmlElement}
     * @readonly
     */
    Object.defineProperty(this, "container", {
        value: container || canvas
    });

    /**
     * Refers to HTML element providing actual canvas in document.
     *
     * @property canvas
     * @type {HtmlElement}
     * @readonly
     */
    Object.defineProperty(this, "canvas", {
        value: canvas
    });

    /**
     * Refers to viewport information to obey on rendering.
     *
     * @property viewport
     * @type {shmi.visuals.controls.TrendDisplay.Viewport}
     * @readonly
     */
    Object.defineProperty(this, "viewport", {
        value: viewport
    });

    /**
     * Refers to constructor of element handler for testing validity of added
     * elements.
     *
     * @property elementClass
     * @type {function}
     * @readonly
     */
    Object.defineProperty(this, "elementClass", {
        value: elementClass
    });

    /**
     * Refers to messaging channel notifying to (re-)render on canvas.
     *
     * @property onRender
     * @type {shmi.visuals.controls.TrendDisplay.Messaging}
     * @readonly
     */
    Object.defineProperty(this, "onRender", {
        value: onRender
    });

    /**
     * Refers to messaging channel notifying on resizing visible area controlled
     * by manager.
     *
     * @property onResize
     * @type {shmi.visuals.controls.TrendDisplay.Messaging}
     * @readonly
     */
    Object.defineProperty(this, "onResize", {
        value: onResize
    });

    /**
     * Refers to configuration object provided on constructing.
     *
     * @type {shmi.visuals.controls.TrendDisplay.Configuration}
     */
    Object.defineProperty(this, "config", {
        value: configuration
    });

    /**
     * Refers to all generated control objects
     *
     * @type {shmi.visuals.controls.TrendDisplay.Controls}
     */
    Object.defineProperty(this, "controls", {
        value: controls
    });

    /**
     * Caches data for rendering curves.
     *
     * @type {?shmi.visuals.controls.TrendDisplay.TrendData}
     * @protected
     */
    this._data = null;

    this._offsetX = 0;
    this._offsetY = 0;

    this._width = 100;
    this._height = 100;
};

shmi.visuals.controls.TrendDisplay.AbstractRenderingManager.prototype = {

    /**
     * Retrieves data provided on most recent update.
     *
     * @see shmi.visuals.controls.TrendDisplay.AbstractRenderingManager#update
     * @return {shmi.visuals.controls.TrendDisplay.TrendData}
     */
    getData: function() {
        "use strict";

        return this._data;
    },

    /**
     * Retrieves current size of canvas on screen.
     *
     * @note Retrieved size doesn't has to be equivalent to actual size of
     *       canvas element. Retrieved size describe visible size of canvas.
     *
     * @return {{x: int, y: int, width: int, height: int}}
     */
    getCanvasSize: function() {
        "use strict";

        return {
            x: this._offsetX,
            y: this._offsetY,
            width: this._width,
            height: this._height,
            totalWidth: this._fullWidth,
            totalHeight: this._fullHeight
        };
    },

    /**
     * Redraws canvas by notifying all listeners requesting to render.
     */
    update: function(newData, noRender) {
        "use strict";

        if (newData) {
            this._data = newData;
        }

        if (noRender) {
            return shmi.visuals.controls.TrendDisplay.Promise.resolve(false);
        }

        return this.resetLocation().
            onRender.send(this, this.getData());
    },

    resize: function(width, height, prefetchAmount, prefetchAmountY) {
        "use strict";

        width = parseInt(width);
        height = parseInt(height);

        if (width && height && (width !== this._width || height !== this._height)) {
            shmi.log("resizing canvas to " + width + " x " + height, 0);

            var self = this,
                additionalX = this._offsetX = Math.ceil(width * (prefetchAmount || 0)),
                additionalY = this._offsetY = Math.ceil(height * (prefetchAmountY || prefetchAmount || 0));

            this._width = width;
            this._height = height;

            this._fullWidth = width + 2 * additionalX;
            this._fullHeight = height + 2 * additionalY;

            this.canvas.setAttribute("width", this._fullWidth);
            this.canvas.setAttribute("height", this._fullHeight);

            this.resetLocation();

            return this.onResize.send(this, width, height).
                then(function() {
                // after resizing content must be re-rendered
                    return self.update();
                });
        } else {
            return null;
        }
    },

    resetLocation: function() {
        "use strict";

        this.container.style.left = String(-this._offsetX) + "px";
        this.container.style.top = String(-this._offsetY) + "px";

        return this;
    },

    onSlide: function(stage, absoluteDeltaX) {
        "use strict";

        this.container.style.left = String(-this._offsetX + absoluteDeltaX) + "px";
    },

    onShift: function(stage, absoluteDeltaY) {
        "use strict";

        this.container.style.top = String(-this._offsetY + absoluteDeltaY) + "px";
    },

    onDelete: function() {
        this.onRender.reset();
        this.onResize.reset();
    }
};

/**
 * Implements API for managing individual elements drawn on an HTML canvas.
 *
 * @param {HtmlElement} container
 * @param {HtmlElement} canvas
 * @param {shmi.visuals.controls.TrendDisplay.Viewport} viewport
 * @param {function} elementClass
 * @param {shmi.visuals.controls.TrendDisplay.Configuration} configuration
 * @constructor
 * @extends shmi.visuals.controls.TrendDisplay.AbstractRenderingManager
 */
shmi.visuals.controls.TrendDisplay.CanvasRenderingManager = function CanvasRenderingManager(container, canvas, viewport, elementClass, configuration, trendDisplay) {
    "use strict";

    var offscreenCanvas;

    shmi.visuals.controls.TrendDisplay.CanvasRenderingManager._super.call(this, container, canvas, viewport, elementClass, configuration, trendDisplay);

    Object.defineProperty(this, "trendDisplay", {
        value: trendDisplay,
        writable: false
    });

    /**
     * Refers to second hidden canvas element available for
     * off-screen-rendering.
     *
     * @property offscreen
     * @type HtmlElement
     */
    Object.defineProperty(this, "offscreen", {
        get: function() {
            if (!offscreenCanvas) {
                offscreenCanvas = document.createElement("canvas");
            }

            return offscreenCanvas;
        },
        set: function(value) {
            if (!value) {
                offscreenCanvas = undefined;
            }
        }
    });

    this._enableImplicitRenderingSteps();
};

shmi.visuals.controls.TrendDisplay.CanvasRenderingManager.prototype = {
    update: function(newData, noRender) {
        "use strict";

        if (newData) {
            this._data = newData;
        }

        if (noRender) {
            return shmi.visuals.controls.TrendDisplay.Promise.resolve(false);
        }

        return this.onRender.send(this, this.getData(), this.offscreen);
    },

    _enableImplicitRenderingSteps: function() {
        var self = this,
            canvas = this.canvas,
            offscreen = this.offscreen;

        this.onRender.addListener(+1000, function _initialRenderingStep() {
            offscreen.width = canvas.width;
            offscreen.height = canvas.height;

            offscreen.getContext("2d").clearRect(0, 0, offscreen.width, offscreen.height);
        });

        this.onRender.addListener(-1000, function _finalRenderingStep() {
            var dc = canvas.getContext("2d");

            // clear visible canvas
            dc.clearRect(0, 0, canvas.width, canvas.height);

            // prevent flickering by relocating visible canvas while
            // it's empty
            self.resetLocation();

            // copy pre-rendered image from off-screen to on-screen canvas
            dc.drawImage(offscreen, 0, 0);
        });
    },

    onDelete: function() {
        this.offscreen = null;
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.CanvasRenderingManager, shmi.visuals.controls.TrendDisplay.AbstractRenderingManager, "CanvasRenderingManager");

/**
 * Implements API for managing individual elements drawn on an SVG document
 * embedded in HTML.
 *
 * @param {HtmlElement} container
 * @param {HtmlElement} canvas
 * @param {shmi.visuals.controls.TrendDisplay.Viewport} viewport
 * @param {function} elementClass
 * @param {shmi.visuals.controls.TrendDisplay.Configuration} configuration
 * @constructor
 * @extends shmi.visuals.controls.TrendDisplay.AbstractRenderingManager
 */
shmi.visuals.controls.TrendDisplay.SvgRenderingManager = function SvgRenderingManager(container, canvas, viewport, elementClass, configuration) {
    "use strict";

    shmi.visuals.controls.TrendDisplay.SvgRenderingManager._super.call(this, container, canvas, viewport, elementClass, configuration);
};

shmi.visuals.controls.TrendDisplay.SvgRenderingManager.prototype = {
    resize: function(width, height, prefetchAmount) {
        "use strict";

        width = parseInt(width);
        height = parseInt(height);

        if (width && height && (width !== this._width || height !== this._height)) {
            var additional = this._offsetX = Math.ceil(width * (prefetchAmount || 0));

            this.canvas.style.left = String(-additional) + "px";
            this.canvas.setAttribute("width", width + 2 * additional);
            this.canvas.setAttribute("height", height);

            this.canvas.setAttribute("viewBox", additional + " 0 " + width + " " + height);

            this._width = width;
            this._height = height;
        }
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.SvgRenderingManager, shmi.visuals.controls.TrendDisplay.AbstractRenderingManager, "SvgRenderingManager");

/**
 * Implements drawing context providing calculations for mapping from timestamps
 * and values into abscissa and ordinate of related points on a canvas and
 * vice-versa.
 *
 * This context was designed to reduce number of required calculations while
 * keeping previously calculated values up-to-date.
 *
 * @param {shmi.visuals.controls.TrendDisplay.Signal} signal signal associated with individual drawing context
 * @param {shmi.visuals.controls.TrendDisplay.AbstractRenderingManager} manager
 * @constructor
 */

shmi.visuals.controls.TrendDisplay.DrawingContext = function(signal, manager) {
    var TD = shmi.visuals.controls.TrendDisplay,
        viewport = manager.viewport,
        cSize = manager.getCanvasSize(),
        minValue = signal.min,
        maxValue = signal.max,
        valueRange = (maxValue - minValue),
        yScale,
        nullTS,
        focusedValue,
        timeSpanPerPixelRatio,
        visibleHeightInPixel,
        valueSpanPerPixelRatio,
        centerY;

    if (minValue === maxValue) {
        shmi.log('[TrendDisplay.DrawingContext] Min/Max values not set or equal for signal ' + (signal.alias || signal.name), 3);
    }

    /**
     * @this shmi.visuals.controls.TrendDisplay.DrawingContext
     * @private
     */
    function _updateViewport() {
        yScale = viewport.zoomY;
        nullTS = viewport.leftTime;
        focusedValue = minValue + valueRange * viewport.zoomFocusY;
        timeSpanPerPixelRatio = viewport.visibleTimeSpan / cSize.width;
        valueSpanPerPixelRatio = valueRange / visibleHeightInPixel;
    }

    /**
     * @this shmi.visuals.controls.TrendDisplay.DrawingContext
     * @private
     */
    function _updateCanvasSize() {
        cSize = manager.getCanvasSize();
        visibleHeightInPixel = cSize.height;
        centerY = cSize.y + visibleHeightInPixel / 2;

        _updateViewport.call(this);
    }

    manager.onResize.addListener(_updateCanvasSize, this);
    viewport.onUpdate.addListener(100, _updateViewport, this);

    /**
     * Delivers visible canvas height of drawing context.
     *
     * @property visibleHeightInPixel
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "visibleHeightInPixel", {
        get: function() {
            return visibleHeightInPixel;
        }
    });

    /**
     * Delivers minimum value of signal drawing context is associated with.
     *
     * @property minValue
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "minValue", {
        value: signal.min
    });

    this.manager = manager;

    /**
     * Delivers maximum value of signal drawing context is associated with.
     *
     * @property minValue
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "maxValue", {
        value: signal.max
    });

    /**
     * Converts timestamp into abscissa value to use on canvas.
     *
     * @param {number} time
     * @param {int=} accuracy
     * @return {number}
     */
    this.time2X = function(time, accuracy) {
        return round((time - nullTS) / timeSpanPerPixelRatio + cSize.x, accuracy >= 0 ? accuracy : 0);
    };

    /**
     * Converts abscissa value of point on canvas into related timestamp.
     *
     * @param {number} x
     * @return {number}
     */
    this.x2Time = function(x) {
        return (x - cSize.x) * timeSpanPerPixelRatio + nullTS;
    };

    /**
     * Converts value of attached signal into ordinate value to use on canvas.
     *
     * @param {number} value
     * @param {int=} accuracy
     * @param {number=} customYScale custom scale factor to use instead of current one
     * @param {number=} customValueSpanPerPixelRatio
     * @return {number}
     */
    this.value2Y = function(value, accuracy, customYScale, customValueSpanPerPixelRatio) {
        var scaledDistanceToFocusedValue = ((value || 0) - focusedValue) * (customYScale || yScale);

        return round(centerY - (scaledDistanceToFocusedValue / (customValueSpanPerPixelRatio || valueSpanPerPixelRatio)), accuracy || 0);
    };

    /**
     * Converts ordinate value of point on canvas into value of attached signal.
     *
     * @param {number} y
     * @param {number=} customYScale custom scale factor to use instead of current one
     * @return {number}
     */
    this.y2Value = function(y, customYScale) {
        return focusedValue - (y - centerY) * valueSpanPerPixelRatio / (customYScale || yScale);
    };

    /**
     * Converts value relative to value range of attached signal into ordinate
     * value to use on canvas.
     *
     * @param {number} relative relative signal value, 0 for minimum signal value, 1.0 for maximum signal value
     * @param {int=} accuracy
     * @param {number=} customYScale custom scale factor to use instead of current one
     * @return {number}
     */
    this.relative2Y = function(relative, accuracy, customYScale) {
        return this.value2Y(relative * valueRange + minValue, accuracy, customYScale);
    };

    /**
     * Converts value relative to value range of attached signal into absolute
     * value of attached signal.
     *
     * @param {number} relative relative signal value, 0 for minimum signal value, 1.0 for maximum signal value
     * @return {number} absolute signal value
     */
    this.relative2Value = function(relative) {
        return relative * valueRange + minValue;
    };

    /**
     * Derives relative position of value related to selected ordinate in
     * relation to full value range.
     *
     * @param {int} y ordinate
     * @param {number=} customYScale vertical scale factor to use instead of current one
     * @return {number} relative position of value, 0 is for minimum value, 1 is for maximum value
     */
    this.y2Relative = function(y, customYScale) {
        var value = this.y2Value(y, customYScale);

        return (value - minValue) / valueRange;
    };

    /**
     * Calculates minimum relative focus position according to provided or
     * current vertical scale factor.
     *
     * @param {number=} customYScale
     * @return {number}
     */
    this.minFocus = function(customYScale) {
        return (this.y2Value(this.value2Y(minValue, 0, customYScale) - visibleHeightInPixel / 2, customYScale) - minValue) / valueRange;
    };

    /**
     * Calculates maximum relative focus position according to provided or
     * current vertical scale factor.
     *
     * @param {number=} customYScale
     * @return {number}
     */
    this.maxFocus = function(customYScale) {
        return (this.y2Value(this.value2Y(maxValue, 0, customYScale) + visibleHeightInPixel / 2, customYScale) - minValue) / valueRange;
    };

    /**
     * Retrieves position of vertical scaling focus (e.g. for assigning to a
     * viewport's property zoomFocusY) according to provided ordinate value of
     * current canvas.
     *
     * @note Former position is expressed as value in range 0..1 and is related
     *       to minimum and maximum value of either signal.
     *
     * @param {number} y
     * @return {number}
     */
    this.y2Focus = function(y) {
        return (this.y2Value(y) - minValue) / valueRange;
    };

    /**
     * Retrieves ordinate value of current canvas according to provided position
     * of vertical scaling focus.
     *
     * @note This position is expressed as value in range 0..1 and is related to
     *       minimum and maximum value of either signal.
     *
     * @param {number} focus
     * @return {number}
     */
    this.focus2Y = function(focus) {
        return this.value2Y(focus * valueRange + minValue);
    };

    /**
     * Calculates focusY value to use for zooming to selected scale at vertical
     * position on canvas relative to focusY value vertically centered on
     * canvas.
     *
     * @param {number} newZoomY
     * @param {int=} relativeY
     * @return {number}
     */
    this.deriveFocusY = function(newZoomY, relativeY) {
        var f1,
            f2;

        f1 = minValue + viewport.zoomFocusY * valueRange;
        f2 = f1 + (1 / viewport.zoomY - 1 / newZoomY) * valueRange / visibleHeightInPixel * relativeY;

        return TD.limit((f2 - minValue) / valueRange, this.minFocus(newZoomY), this.maxFocus(newZoomY));
    };

    /**
     * Rounds value obeying selected accuracy.
     *
     * Accuracy selects number of digits to keep after comma.
     *
     * @param {number} value value to be rounded
     * @param {int=} accuracy number of digits after comma to keep on rounding
     * @return {number} rounded value
     */
    function round(value, accuracy) {
        var scale = Math.pow(10, accuracy || 0);

        return Math.round(value * scale) / scale;
    }

    _updateCanvasSize.call(this);
};

/**
 * Implements structure used to describe currently visible part of trend.
 *
 * @param {object} configuration configuration of trend display control
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.Viewport = function Viewport(configuration) {
    "use strict";

    var leftTime,
        rightTime,
        zoom,
        minSampleTime = null,
        maxSampleTime = null,
        maxTime = configuration.maxTime,
        isLive = (configuration.mode === "live"),
        handleTime = isLive ? 0 : maxTime,
        pristine = true,
        onUpdate = new shmi.visuals.controls.TrendDisplay.Messaging(),
        self = this,
        transaction = null,
        isSingleLane = (configuration.type && (configuration.type.toLowerCase() === "singlelane" || configuration.type.toLowerCase() === "single")),
        isMultiLane = (configuration.type && (configuration.type.toLowerCase() === "multilane" || configuration.type.toLowerCase() === "multi")),
        isDetailView = configuration.view !== "" ? (configuration.view && (configuration.view.toLowerCase() === "detail")) : true,
        isDashboardView = configuration.view !== "" ? (configuration.view && (configuration.view.toLowerCase() === "dashboard")) : false;

    /*
     * initialize runtime properties of viewport
     */
    switch (configuration.mode) {
    case "now":
    case "live":
        rightTime = maxTime;
        leftTime = limitStart(rightTime - configuration.visible);
        break;

    default:
        leftTime = configuration.minTime;
        rightTime = maxTime;
    }

    zoom = {
        x: 1.0,
        y: 1.0,
        focusY: 0.5
    };

    /**
     * Checks current type to be single lane
     *
     * @note This helper variable identifies which layout to display
     *
     * @type {boolean}
     */
    Object.defineProperty(this, "isSingleLane", {
        get: function() {
            return isSingleLane;
        },
        set: function(value) {
            var v = !!value;

            if (v !== isSingleLane) {
                isSingleLane = v;
                isMultiLane = !v;

                //_onUpdated( [ "isSingleLane", "isMultiLane" ] );
            }
        }
    });

    /**
     * Checks current type to be multi lane
     *
     * @note This helper variable identifies which layout to display
     * @see
     *
     * @type {boolean}
     */
    Object.defineProperty(this, "isMultiLane", {
        get: function() {
            return isMultiLane;
        },
        set: function(value) {
            var v = !!value;

            if (v !== isMultiLane) {
                isMultiLane = v;
                isSingleLane = !v;

                //_onUpdated( [ "isSingleLane", "isMultiLane" ] );
            }
        }
    });

    /**
     * Checks current view type to be detail view
     *
     * @note This helper variable identifies which layout to display
     * @see
     *
     * @type {boolean}
     */
    Object.defineProperty(this, "isDetailView", {
        get: function() {
            return isDetailView;
        },
        set: function(value) {
            var v = !!value;

            if (v !== isDetailView) {
                isDetailView = v;
                isDashboardView = !v;

                //_onUpdated( [ "isDetailView", "isDashboardView" ] );
            }
        }
    });

    /**
     * Checks current view type to be dashboard view
     *
     * @note This helper variable identifies which layout to display
     *
     * @type {boolean}
     */
    Object.defineProperty(this, "isDashboardView", {
        get: function() {
            return isDashboardView;
        },
        set: function(value) {
            var v = !!value;

            if (v !== isDashboardView) {
                isDashboardView = v;
                isDetailView = !v;

                //_onUpdated( [ "isDetailView", "isDashboardView" ] );
            }
        }
    });

    /**
     * Marks current viewport to be pristine or dirty.
     *
     * @note This mark is set on adjusting any parameter of viewport. It must be
     *       reset explicitly by assigning any value.
     *
     * @type {boolean}
     */
    Object.defineProperty(this, "pristine", {
        get: function() {
            return pristine;
        },
        set: function() {
            pristine = true;
        }
    });

    /**
     * Provides messaging for notify on updating viewport parameters.
     *
     * @type {shmi.visuals.controls.TrendDisplay.Messaging<Viewport,string[]>}
     * @readonly
     */
    Object.defineProperty(this, "onUpdate", {
        value: onUpdate
    });

    /**
     * Provides minimum time to be ever displayed in current component.
     *
     * @readonly
     * @type {number}
     */
    Object.defineProperty(this, "minTime", {
        value: configuration.minTime
    });

    /**
     * Provides maximum time to be ever displayed in current component.
     *
     * @readonly
     * @type {number}
     */
    Object.defineProperty(this, "maxTime", {
        get: function() {
            return maxTime;
        }
    });

    /**
     * Delivers amount of curve to render beyond either end of display in
     * relation to visible amount.
     *
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "prefetch", {
        value: configuration.prefetch
    });

    /**
     * Indicates if current viewport is showing live data or not.
     *
     * @type {boolean}
     */
    Object.defineProperty(this, "isLive", {
        get: function() {
            return isLive;
        },
        set: function(value) {
            var v = !!value;

            if (v !== isLive) {
                isLive = v;

                _onUpdated(["isLive"]);
            }
        }
    });

    /**
     * Provides time of earliest available data sample.
     *
     * @property minSampleTime
     * @type {number}
     */
    Object.defineProperty(this, "minSampleTime", {
        get: function() {
            return minSampleTime || this.minTime;
        },
        set: function(value) {
            value = Number(value);

            if (!isNaN(value) && value > 0 && value < minSampleTime) {
                minSampleTime = value;
            }
        }
    });

    /**
     * Receives candidates for becoming timestamp of latest captured trend
     * value.
     *
     * @property maxSampleTime
     * @type {number}
     */
    Object.defineProperty(this, "maxSampleTime", {
        get: function() {
            return maxSampleTime;
        },
        set: function(value) {
            var v = Number(value);

            if (v > 0) {
                if (v > maxSampleTime) {
                    maxSampleTime = v;
                }

                if (v > maxTime) {
                    // extend maximum range
                    maxTime = v;

                    if (isLive) {
                        // move slice to keep showing new data at end of continuous paper
                        slideTo(this.maxLeftTime, ["maxTime"]);
                    } else {
                        _onUpdated(["maxTime"]);
                    }
                }
            }
        }
    });

    /**
     * Receives end time of any program displayed in program manager plugin.
     *
     * @note Writing timestamp into this value is extending maxTime of viewport
     *       unless maxTime was including given time before.
     *
     * @type {number}
     * @writeonly
     */
    Object.defineProperty(this, "programEndTime", {
        set: function(value) {
            var v = Number(value);

            if (v > 0) {
                if (v < maxSampleTime) {
                    v = maxSampleTime;
                }

                if (v !== maxTime) {
                    // adjust maximum range
                    maxTime = v;

                    _onUpdated(["maxTime"]);
                }
            }
        },
        get: function() {
            return null;
        }
    });

    /**
     * Provides horizontal zoom factor of component.
     *
     * This value controls zooming by choosing magnification of trend data at
     * position of time given in property handleTime. Zooming affects values of
     * properties leftTime and rightTime and is triggering broadcasts according
     * to zooming and to requiring to update position of handle as displayed on
     * screen.
     *
     * A value of 1.0 marks display of full configured range. A value of 2.0
     * marks to display half of that range while 4.0 marks to display quarter of
     * full configured range.
     *
     * @note This value is ensured to be 1.0 at least.
     *
     * @type {number}
     */
    Object.defineProperty(this, "zoomX", {
        get: function() {
            return zoom.x;
        },
        set: function(value) {
            var v = parseFloat(value);

            if (!isNaN(v) && v > 0) {
                // find minimum zoom factor by validating to zoom out for
                // showing full configured time span at most
                var min = configuration.minTime,
                    max = maxTime,
                    maxRange = (max - min),
                    newRange = Math.min(maxRange, configuration.visible / v);

                v = configuration.visible / newRange;

                if (v !== zoom.x) {
                    zoom.x = v;

                    var cursor = this.handleTime,
                        properties = ["zoomX", "handleTime"], // notify handleTime due to probably changing its visual position
                        left = cursor - (newRange / 2),
                        right = cursor + (newRange / 2);

                    if (left < min) {
                        left = min;
                        right = left + newRange;
                    } else if (right > max) {
                        right = max;
                        left = right - newRange;
                    }

                    if (leftTime !== left || rightTime !== right) {
                        leftTime = left;
                        rightTime = right;
                        properties.push("leftTime");
                    }

                    _onUpdated(properties);
                }
            }
        }
    });

    Object.defineProperty(this, "minZoomX", {
        get: function() {
            var v = configuration.visible / (maxTime - configuration.minTime),
                m = shmi.c("TREND_DISPLAY_ZOOM_SCALE_X_MIN");

            return v > m ? v : m;
        }
    });

    Object.defineProperty(this, "maxZoomX", {
        value: shmi.c("TREND_DISPLAY_ZOOM_SCALE_X_MAX")
    });

    Object.defineProperty(this, "zoomY", {
        get: function() {
            return zoom.y;
        },
        set: function(value) {
            var v = Number(value);

            if (v < this.minZoomY) {
                v = this.minZoomY;
            }

            if (v > this.maxZoomY) {
                v = this.maxZoomY;
            }

            if (v !== zoom.y) {
                zoom.y = v;

                _onUpdated(["zoomY"]);
            }
        }
    });

    Object.defineProperty(this, "minZoomY", {
        value: shmi.c("TREND_DISPLAY_ZOOM_SCALE_Y_MIN")
    });

    Object.defineProperty(this, "maxZoomY", {
        value: shmi.c("TREND_DISPLAY_ZOOM_SCALE_Y_MAX")
    });

    Object.defineProperty(this, "zoomFocusY", {
        get: function() {
            return zoom.focusY;
        },
        set: function(value) {
            var v = Number(value);
            if (v < 0.0) {
                v = 0.0;
            }

            if (v > 1.0) {
                v = 1.0;
            }

            if (v !== zoom.focusY) {
                zoom.focusY = v;

                _onUpdated(["zoomFocusY"]);
            }
        }
    });

    /**
     * Marks timestamp of handle bar or similar cursor used to select single
     * timestamp e.g. to keep centered on focusing or to display curves' values
     * in a separate snapshow display.
     *
     * @type {number}
     */
    Object.defineProperty(this, "handleTime", {
        get: function() {
            return handleTime || maxTime;
        },
        set: function(value) {
            if (setHandleTime(value)) {
                _onUpdated(["handleTime"]);
            }
        }
    });

    /**
     * Marks timestamp referring to left-most visible position of continuous
     * paper component.
     *
     * @type {number}
     */
    Object.defineProperty(this, "leftTime", {
        get: function() {
            return leftTime;
        },
        set: function(value) {
            var v = Number(value);
            if (v > 0) {
                slideTo(limitStart(limitEnd(v, true)), []);
            }
        }
    });

    /**
     * Delivers minimum time stamp of left edge of visible part of curves.
     *
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "minLeftTime", {
        value: configuration.minTime
    });

    /**
     * Delivers maximum time stamp of left edge of visible part of curves.
     *
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "maxLeftTime", {
        get: function() {
            return limitStart(maxTime - (rightTime - leftTime));
        }
    });

    /**
     * Delivers time stamp of left edge of visible part of curves.
     *
     * @type {number}
     */
    Object.defineProperty(this, "rightTime", {
        get: function() {
            return rightTime;
        },
        set: function(value) {
            if (!isNaN(value)) {
                this.leftTime = Number(value) - (rightTime - leftTime);
            }
        }
    });

    /**
     * Delivers currently visible time span.
     *
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "visibleTimeSpan", {
        get: function() {
            return rightTime - leftTime;
        }
    });

    /**
     * Delivers current time.
     *
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "now", {
        get: function() {
            return shmi.getServerTime();
        }
    });

    /**
     * Delivers time stamp of earliest sample to render on canvas.
     *
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "leftRenderTime", {
        get: function() {
            return limitStart(leftTime - configuration.prefetch * (rightTime - leftTime));
        }
    });

    /**
     * Delivers time stamp of latest sample to render on canvas.
     *
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "rightRenderTime", {
        get: function() {
            return limitEnd(rightTime + configuration.prefetch * (rightTime - leftTime));
        }
    });

    /**
     * Controls transaction collecting notifications due to several updates of
     * viewport to be sent at once on ending transaction.
     *
     * Transaction is started by assigning truthy value and is ended by
     * assigning falsy value. Multiple transactions might be nested by assigning
     * truthy value multiple times. However, equivalent number of assigments of
     * falsy values is required to end actual transaction mode.
     *
     * On reading boolean value indicates if any transaction is in progress.
     *
     * @type {boolean}
     */
    Object.defineProperty(this, "transaction", {
        get: function() {
            return !!transaction;
        },
        set: function(enabled) {
            if (enabled) {
                if (transaction) {
                    transaction.locks++;
                } else {
                    transaction = {
                        locks: 1,
                        queue: [],
                        watchdog: setTimeout(function() {
                            shmi.log("viewport transaction locked for more than 2s", 2);
                        }, 2000)
                    };
                }
            } else if (transaction) {
                if (!--transaction.locks) {
                    var queue = transaction.queue;

                    clearTimeout(transaction.watchdog); //eslint-disable-line no-undef

                    transaction = null;

                    if (queue.length > 0) {
                        onUpdate.send(this, queue);
                    }
                }
            }
        }
    });

    /**
     * Provides opportunity for accessing transaction queue listing properties
     * changed while viewport is in transaction mode.
     *
     * @note This property permits write-access for writing falsy value causing
     *       queue to be cleared. On writing array of viewport property names
     *       transaction queue is filtered to contain given properties, only.
     *
     * @property transactionQueue
     * @type {?}
     */
    Object.defineProperty(this, "transactionQueue", {
        get: function() {
            return transaction ? transaction.queue : [];
        },
        set: function(value) {
            if (value) {
                if (transaction && Array.isArray(value)) {
                    transaction.queue = transaction.queue.filter(function(p) {
                        return (value.indexOf(p) >= 0);
                    });
                }
            }
            if (!value && transaction) {
                transaction.queue = [];
            }
        }
    });

    /*
     * define some private helpers
     */

    function limitStart(value) {
        var min = configuration.minTime;

        return value < min ? min : value;
    }

    function limitEnd(value, leftEdgeLimit) {
        var max = leftEdgeLimit ? self.maxLeftTime : maxTime;

        return value > max ? max : value;
    }

    function slideTo(newLeftTime, properties) {
        // actually changing slice?
        if (newLeftTime !== leftTime) {
            // yes
            properties.push("leftTime");

            if (setHandleTime(newLeftTime + (self.handleTime - leftTime))) {
                properties.push("handleTime");
            }

            // adjust slice
            rightTime = newLeftTime + (rightTime - leftTime);
            leftTime = newLeftTime;
        }

        if (properties.length > 0) {
            _onUpdated(properties);
        }
    }

    function setHandleTime(value) {
        var v = Number(value);

        if (v > 0) {
            v = limitStart(limitEnd(v));

            if (v !== handleTime) {
                // obey case of moving handle to end of display while
                // showing live data as this is pinning handle to the end
                // of live display again
                if (v >= maxTime && isLive) {
                    if (handleTime === 0) {
                        // was pinned before, but caller doesn't know
                        // -> no actual change
                        return false;
                    }

                    v = 0;
                }

                handleTime = v;

                return true;
            }
        }

        return false;
    }

    function _onUpdated(updatedProperties) {
        if (updatedProperties.length > 0) {
            pristine = false;

            if (transaction) {
                transaction.queue = transaction.queue.concat(updatedProperties);
            } else {
                onUpdate.send(self, updatedProperties);
            }
        }
    }
};

shmi.visuals.controls.TrendDisplay.Viewport.prototype = {

    /**
     * Calculates number of pixels prefetch beyond left end of currently
     * visible viewport.
     *
     * @param {int} visibleWidth width of viewport in pixels
     * @return {int} rounded number of pixels containing curve of prefetched data
     */
    deriveLeftMaxShiftPixels: function(visibleWidth) {
        "use strict";

        return Math.round((this.leftTime - this.minLeftTime) / this.visibleTimeSpan * visibleWidth);
    },

    /**
     * Calculates number of pixels prefetch beyond left end of currently
     * visible viewport.
     *
     * @param {int} visibleWidth width of viewport in pixels
     * @return {int} rounded number of pixels containing curve of prefetched data
     */
    deriveRightMaxShiftPixels: function(visibleWidth) {
        "use strict";

        return Math.round((this.maxLeftTime - this.leftTime) / this.visibleTimeSpan * visibleWidth);
    },

    /**
     * Calculates number of pixels prefetch beyond left end of currently
     * visible viewport.
     *
     * @param {int} visibleWidth width of viewport in pixels
     * @return {int} rounded number of pixels containing curve of prefetched data
     */
    deriveLeftPrefetchPixels: function(visibleWidth) {
        "use strict";

        return Math.round((this.leftTime - this.leftRenderTime) / this.visibleTimeSpan * visibleWidth);
    },

    /**
     * Calculates number of pixels prefetch beyond right end of currently
     * visible viewport.
     *
     * @param {int} visibleWidth width of viewport in pixels
     * @return {int} rounded number of pixels containing curve of prefetched data
     */
    deriveRightPrefetchPixels: function(visibleWidth) {
        "use strict";

        return Math.round((this.rightRenderTime - this.rightTime) / this.visibleTimeSpan * visibleWidth);
    },

    onDelete: function() {
        this.onUpdate.reset();
    }

};

/**
 * Derives normalized viewport descriptor from provided set of settings.
 *
 * @param {{}} input object containing selected viewport-related settings (e.g. configuration)
 * @return {{mode:string, minTime:number, maxTime:number, visible:int, prefetch:number, type:string, view:string, lanesPerView:int, firstLabelHeight:int, handleBarOffset:int, labelHeight:int, hasVerticalScaling:bool, demo:bool}}
 */
shmi.visuals.controls.TrendDisplay.Viewport.validateDescriptor = function(input) {
    "use strict";

    var n,
        mode,
        minTime = input.tStart || "",
        maxTime = input.tEnd || "",
        visible = input.tDisplay || 1800,
        prefetch = input.prefetchPercentage || 50,
        type = input.type || "singleLane",
        view = input.view || "",

        lanesPerView = input.lanesPerView || 5,
        labelHeight = input.labelHeight || 50,
        firstLabelHeight = input.firstLabelHeight || 0,
        handleBarOffset = input.handleBarOffset || 0,
        hasVerticalScaling = input.hasVerticalScaling || true,
        demo = input.demo || false;

    /*
     * normalize and validate setting on prefetched amount
     */

    // strip off trailing "%"
    n = /^\*(.+)\s*%\s*$/.exec(prefetch);
    if (n) {
        prefetch = n[1];
    }

    // convert percent value into scaling factor
    prefetch = Number(prefetch);
    if (prefetch > 1) {
        prefetch /= 100;

        if (prefetch > 1) {
            prefetch = 1;
        }
    }

    // exclude negative values
    if (!prefetch || prefetch < 0) {
        prefetch = 0;
    }

    /*
     * process setting regarding end of time span to display at most
     */

    n = shmi.visuals.controls.TrendDisplay.Parsers.timestamp(maxTime, true);
    if (n === null) {
        shmi.log("TrendDisplay.Viewport() invalid maxTime in configuration: " + maxTime);
        n = 0;
    }

    maxTime = n;

    // derive processing mode from selected end of time span
    if (maxTime === 0) {
        mode = "latest";
    } else if (maxTime === -1) {
        mode = "live";
    } else {
        mode = "static";
    }

    // finally ensure end of time span is a proper timestamp
    if (mode !== "static") {
        maxTime = shmi.getServerTime();
    }

    /**
     * process setting regarding start if time span to display at most
     */

    n = parseInt(minTime);
    if (isFinite(n)) {
        if (n > 0) {
            minTime = n;
        } else if (n < 0) {
            minTime += maxTime;
        } else {
            shmi.log("TrendDisplay.Viewport() invalid/missing time stamp in tStart", 3);
        }
    } else {
        minTime = Date.parse(String(minTime).trim());
        if (!isNaN) {
            minTime = minTime.getTime() / 1000;
        } else {
            shmi.log("TrendDisplay.Viewport() invalid/missing time stamp in tStart", 3);
        }
    }

    // be flexible on accidentally flipping ends of time span
    if (minTime > maxTime) {
        n = minTime;
        minTime = maxTime;
        maxTime = n;
    }

    /*
     * ensure "visible" isn't exceeding time span to display at most
     */

    n = (maxTime - minTime);

    if (visible > n) {
        visible = n;
    }

    return {
        mode: mode,
        minTime: minTime,
        maxTime: maxTime,
        visible: visible,
        prefetch: prefetch,
        type: type,
        view: view,
        lanesPerView: lanesPerView,
        firstLabelHeight: firstLabelHeight,
        labelHeight: labelHeight,
        handleBarOffset: handleBarOffset,
        hasVerticalScaling: hasVerticalScaling,
        demo: demo
    };
};

shmi.visuals.controls.TrendDisplay.onConfiguration.push([90, function _onConfigureViewport(configuration, trendDisplay) {
    "use strict";

    if (configuration.invalid) {
        return;
    }
    configuration.viewport = shmi.visuals.controls.TrendDisplay.Viewport.validateDescriptor(configuration.display);
}
]);

shmi.pkg("visuals.controls.TrendDisplay");

/**
 * Implements basic behaviour of components to be part of trend display control.
 *
 * @param {HtmlElement} element DOM element detected to represent a component
 * @param {{}} config instantly available configuration
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.Component = function Component(element, config) {
    "use strict";

    var prepared = new shmi.visuals.controls.TrendDisplay.Promise();

    var onBroadcast = [];

    /**
     * Refers to HTML element of current component.
     *
     * @property element
     * @type {HtmlElement}
     */
    Object.defineProperty(this, "element", {
        value: element
    });

    /**
     * Provides messaging queue used on sending broadcasts to all components of
     * current control.
     *
     * @property onBroadcast
     * @type {Array.<ListenerDescriptor>|shmi.visuals.controls.TrendDisplay.Messaging}
     */
    Object.defineProperty(this, "onBroadcast", {
        get: function() {
            return onBroadcast;
        },
        set: function(newMessaging) {
            if (newMessaging && Array.isArray(onBroadcast)) {
                onBroadcast = newMessaging.merge(onBroadcast);
            }
        }
    });

    /**
     * Provides promise resolved when component is prepared.
     *
     * @property isPrepared
     * @type {shmi.visuals.controls.TrendDisplay.Promise}
     */
    Object.defineProperty(this, "isPrepared", {
        value: prepared
    });

    this.trendDisplay = {
        broadcast: shmi.visuals.controls.TrendDisplay.nop
    };

    this.config = this._initializeConfiguration(config || {});

    this.parseAttributes();

    this.startup();
};

shmi.visuals.controls.TrendDisplay.Component.prototype = {

    events: shmi.visuals.core.BaseControl.prototype.events.concat("discover"),

    _initializeConfiguration: function(config) {},

    /**
     * Dispatches broadcast in context of containing trend display control.
     *
     * @note This method is a NOP initially, but gets linked with related method
     *       of containing
     *
     * @param {string} name name of broadcast
     * @param {*} data detail information on broadcast
     */
    broadcast: function(name, data) {
        "use strict";

        return this.trendDisplay.broadcast(this, name, data);
    },

    /**
     * Handles event of having initialized control.
     *
     * This method is invoked after control's configuration and template have
     * been loaded. It is considered to locally prepare this component.
     *
     * @see onDiscover()
     */
    onInit: function() {
        "use strict";

        shmi.addClass(this.element, "undiscovered");

        var self = this;

        this.listen("discover", function() {
            return self.onDiscover.apply(self, arguments);
        });
    },

    /**
     * Handles event of being discovered by containing TrendDisplay control.
     *
     * TrendDisplay is expected to discover every containing component enabling
     * either discovered component to link with whole TrendDisplay control and
     * start interacting with other controls e.g. by sending or receiving
     * broadcasts.
     *
     * @param {{source:*, type:string, detail:{trendDisplay:shmi.visuals.controls.TrendDisplay}}} event
     */
    onDiscover: function(event) {
        "use strict";

        var trendDisplay = event.detail.trendDisplay;
        Object.defineProperty(this, "trendDisplay", {
            value: trendDisplay
        });

        // link component with broadcast messaging of trend display control
        this.onBroadcast = trendDisplay.onBroadcast.merge(this.onBroadcast);

        this.markDiscovered();
    },

    /**
     * Updates mark on component regarding state of being discovered by
     * containing trend display control.
     */
    markDiscovered: function() {
        "use strict";

        shmi.removeClass(this.element, "undiscovered");
        shmi.addClass(this.element, "discovered");
    },

    onDelete: function() {

    }

};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.Component, shmi.visuals.core.BaseControl);

/**
 * Controls of current lanes (buttons, selectboxes, etc.)
 *
 * @param manager
 * @param viewport
 * @param controls optional type:Array with controls of current trend display if update is called (switch between dashboard and detail view)
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.Controls = function(manager, viewport, controls) {
    "use strict";

    // check for existing controls
    var tokens = shmi.objectHasOwnProperty(controls, 'tokens') ? controls.tokens : [],
        buttons = shmi.objectHasOwnProperty(controls, 'buttons') ? controls.buttons : [],
        selectboxes = shmi.objectHasOwnProperty(controls, 'selectboxes') ? controls.selectboxes : [];

    //shmi.visuals.controls.TrendDisplay.Controls._super.call( this, manager, viewport );

    Object.defineProperty(this, "manager", {
        value: manager
    });

    Object.defineProperty(this, "viewport", {
        value: viewport
    });

    Object.defineProperty(this, "controls", {
        get: function() {
            return this._getControls();
        },
        set: function(controlsParam) {
            if (controlsParam && Array.isArray(controlsParam)) {
                this.tokens = controlsParam.tokens;
                this.buttons = controlsParam.buttons;
                this.selectboxes = controlsParam.selectboxes;
            }
        }
    });

    Object.defineProperty(this, "tokens", {
        value: tokens
    });

    Object.defineProperty(this, "buttons", {
        value: buttons
    });

    Object.defineProperty(this, "selectboxes", {
        value: selectboxes
    });
};

shmi.visuals.controls.TrendDisplay.Controls.prototype = {
    /*uiType: "trend-display-controls",
    isContainer: false,
    getClassName: function() { "use strict"; return "TrendDisplayControls"; },

    _initializeConfiguration: function( config ) {
    "use strict";

    shmi.def( config, "class-name", "trend-display-controls" );
    //shmi.def( config, "template", "default/trend-display/component/continuous" );

    return config;
    },

    onInit: function() {
    "use strict";

    var self = this;

    this._super( this.TrendDisplayControls, "onInit" );
    },*/

    /**
     * return all registered elements
     *
     * @returns {{}}
     * @private
     */
    _getControls: function() {
        var controls = {};

        controls.tokens = this.tokens;
        controls.buttons = this.buttons;
        controls.selectboxes = this.selectboxes;

        return controls;
    },

    /**
     * button to go to dashboard
     *
     * @param container
     * @param bgCanvas CanvasBackground
     * @returns {control shmi.createControl}
     */
    applyButtonCloseView: function(container, bgCanvas) {
        var manager = this.manager,
            name = 'close-view-button',
            trendDisplay = manager.trendDisplay,
            control = shmi.ctrl(trendDisplay.getName() + '.' + name);

        // already exists?
        if (!control) {
            var cfg = {
                "name": name,
                "class-name": "rh-button close",
                "icon-class": "icon icon-ui-close",
                "show-icon": true,
                "show-text": false
            };
            control = shmi.createControl("button", container, cfg, "div");
            var token = control.listen("click", function(event) {
                trendDisplay.changingView = "applyButtonCloseView";
                trendDisplay.detailView = null;
                // remove detail-view class for styling
                shmi.removeClass(manager.trendDisplay.element, 'detail-view');

                // hide detail view controls
                shmi.addClass(manager.trendDisplay.element, 'hidden');

                // show dashboard lanes and labels
                var items = manager.container.parentNode.parentNode.parentNode.querySelectorAll('.lane, .action'); //, .handle-bar .value, .handle-bar .signal
                for (var i = 0; i < items.length; ++i) {
                    var item = items[i];
                    item.className = item.className.replace(/(?:^|\s)hidden(?!\S)/g, '');
                }

                // clear scaling values
                var scalingValues = container.parentNode.parentNode.querySelector(".scaling-values");
                if (scalingValues) {
                    scalingValues.innerHTML = '';
                }

                bgCanvas._updateTrendDisplay(manager, null, "dashboard");
            });
            control.enable();

            this.tokens.push(token);
            this.buttons.push(control);
        }
        return control;
    },

    /**
     * button to go to signal detail view
     *
     * @param container
     * @param bgCanvas CanvasBackground
     * @param laneIndex
     * @param signal @todo get via controls or DOM for single lanes
     * @returns {control shmi.createControl}
     */
    applyButtonDetailView: function(container, bgCanvas, laneIndex, signal) {
        var manager = this.manager,
            name = 'detail-view-button-' + laneIndex,
            trendDisplay = manager.trendDisplay,
            control = shmi.ctrl(trendDisplay.getName() + '.' + name);

        // already exists?
        if (!control) {
            var cfg = {
                "class-name": "select-trend-button detail-view",
                "name": name,
                "icon-class": "icon icon-trends-chart-extend",
                "show-icon": true
            };
            control = shmi.createControl("button", container, cfg, "div");
            var token = control.listen("click", function(event) {
                trendDisplay.changingView = "applyButtonDetailView";
                trendDisplay.detailView = laneIndex;
                // get selected item from select box for multi lanes
                var multiLaneSelectBox = shmi.ctrl(trendDisplay.getName() + '.select-box-multi-lane-' + laneIndex);
                if (multiLaneSelectBox) {
                    signal = manager.config.trend.signals[multiLaneSelectBox.value];
                }

                // hide dashboard lanes and labels
                var items = manager.container.parentNode.parentNode.parentNode.querySelectorAll('.lane, .action'); //, .handle-bar .value, .handle-bar .signal
                for (var i = 0; i < items.length; ++i) {
                    var item = items[i];
                    item.className += " hidden";
                }

                // set detail-view class for styling
                shmi.addClass(trendDisplay.element, 'detail-view');

                // show detail view controls
                shmi.removeClass(shmi.ctrl(trendDisplay.getName() + '.detail-view-controls').element, 'hidden');

                bgCanvas._updateTrendDisplay(manager, signal, "detail");
            });
            control.enable();

            this.tokens.push(token);
            this.buttons.push(control);
        }
        return control;
    },

    /**
     * button to go to signal detail view
     *
     * @param {string} direction 'left' | 'right'
     * @param container
     * @param {string} timelinePosition 'top' | 'bottom'
     * @param {string} controlPosition 'after' | 'before'
     * @returns {control shmi.createControl}
     */
    applyButtonNavigateTimeline: function(direction, container, timelinePosition, controlPosition) {
        var manager = this.manager,
            name = 'scroll-' + timelinePosition + '-timeline-' + direction + '-' + controlPosition,
            control = shmi.ctrl(manager.trendDisplay.getName() + '.' + name),
            value = direction === 'left' ? -2 : 2;

        // already exists?
        if (!control) {
            var cfg = {
                //"uiType": "move-timeline-" + direction,

                "label": "",
                "class-name": "button arrow " + direction,
                //"config-name": "@zoom-" + direction + "-button-config",
                "name": name,
                "show-icon": true,
                "show-text": false,
                "icon-class": "icon icon-ui-move-" + direction
            };
            control = shmi.createControl("button", container, cfg, "div", controlPosition);
            var clicks = 0;
            var token = control.listen("click", function(event) {
                clicks++;
                setTimeout(function() {
                    if (clicks === 1) {
                        shmi.visuals.session.userActions["trend-display"].call(this, manager.trendDisplay.getName() + ",handleTime," + value);
                    } else if (direction === "right") {
                        // special on double click for right button
                        // go to current time (live mode)
                        shmi.visuals.session.userActions["trend-display"].call(this, manager.trendDisplay.getName() + ",toggleLiveMode,-1");
                    } else {
                        shmi.visuals.session.userActions["trend-display"].call(this, manager.trendDisplay.getName() + ",handleTime," + value);
                    }
                    clicks = 0;
                }, 200);
            });

            control.enable();

            this.tokens.push(token);
            this.buttons.push(control);
        }
        return control;
    },

    /**
     * multi lanes for selection
     *
     * @param container
     * @param bgCanvas CanvasBackground
     * @param signal
     * @param lanes
     * @returns {control shmi.createControl}
     */
    applySelectBoxLane: function(container, bgCanvas, signal, lanes) {
        var manager = this.manager,
            vp = this.viewport,
            laneIndex = signal.laneIndex,
            name = 'select-box-multi-lane-' + laneIndex,
            control = shmi.ctrl(manager.trendDisplay.getName() + '.' + name),
            lane = lanes[laneIndex];

        // already exists?
        if (!control) {
            var firstItemValue = '',
                initCall = true,
                opts = [],
                signalOpts = {};

            // create selection options of all signals with same lane index
            lane.forEach(function(s, index) {
                signalOpts = {};
                signalOpts.label = shmi.localize(s.label);
                signalOpts.value = s.valueIndex;
                opts.push(signalOpts);
                if (index === 0) {
                    firstItemValue = s.valueIndex;
                }
            });
            var cfg = {
                "label": "",
                "options": opts,
                "selected": firstItemValue, // Do not set selected
                "name": name,
                "class-name": "rh-select-box", // @todo set as global var?
                "template": "custom/rh-controls/rh-select-box"
            };
            control = shmi.createControl("select-box", container, cfg, "DIV", "after");

            var token = control.listen("change", function(event) {
                // Bugfix: do not rerender on init due to setValue() fires change with redraw event
                if (!initCall) {
                    bgCanvas._highlightTrendCurve(manager, manager.config.trend.signals[event.detail.value]);
                } else {
                    initCall = false;
                }
            });
            control.setValue(firstItemValue);
            control.enable();

            this.tokens.push(token);
            this.selectboxes.push(control);
        }
        if (vp.isMultiLane) {
            if (vp.isDetailView) {
                control.element.className += ' hidden';
            } else if (vp.isDashboardView) {
                control.element.className.replace(/(?:^|\s)hidden(?!\S)/g, '');
            }
        }
        return control;
    },

    /**
     * delete trend display controls
     * @todo: refactoring with @param name "buttons" or "all" if necessary
     * @param controls
     * @param deleteParent bool delete parent control
     */
    deleteControls: function(controls, deleteParent) {
        deleteParent = deleteParent || false;
        if (controls) {
            controls.forEach(function(item) {
                if (shmi.getControlByElement(item.element)) {
                    var parentElement = item.element.parentNode;
                    if (parentElement.getAttribute('data-ui') !== 'trend-display-timeline' && deleteParent && parentElement && item.element) { // i.e. remove action-lane element
                        shmi.deleteControl(shmi.getControlByElement(parentElement));
                    } else {
                        shmi.deleteControl(item, true);
                    }
                }
            });
        }
    },

    deleteControlButtons: function(deleteParent) {
        this.deleteControls(this.buttons, deleteParent);
        this.buttons = [];
    },

    deleteControlSelectboxes: function() {
        this.deleteControls(this.selectboxes);
        this.selectboxes = [];
    },

    deleteControlTokens: function() {
        if (this.tokens) {
            this.tokens.forEach(function(token) {
                token.unlisten();
            });
            this.tokens = [];
        }
    },

    onDelete: function() {
        this.deleteControlTokens();
        this.deleteControlButtons(true);
        this.deleteControlSelectboxes();
    }
};

//shmi.visuals.controls.TrendDisplay.derive( shmi.visuals.controls.TrendDisplay.Controls, shmi.visuals.controls.TrendDisplay.Component, "TrendDisplayControls" );

/**
 * Implements component of trend display control representing some sort of
 * "continuous paper" to draw curves on.
 *
 * @param {HtmlElement} element DOM element detected to represent a continuous paper component
 * @param {{}} configuration instantly available configuration
 * @constructor
 * @extends {shmi.visuals.controls.TrendDisplay.Component}
 */
//eslint-disable-next-line func-name-matching
shmi.visuals.controls.TrendDisplay.Continuous = function TrendDisplayContinuous(element, configuration) {
    "use strict";

    shmi.visuals.controls.TrendDisplay.Continuous._super.call(this, element, configuration);

    var viewport = null;
    var trend = null;
    var renderer = null;
    var curves = [];

    var listeners = this._initUiControl(element);

    var onSlide = new shmi.visuals.controls.TrendDisplay.Messaging(true);
    var onShift = new shmi.visuals.controls.TrendDisplay.Messaging(true);

    var onState = new shmi.visuals.controls.TrendDisplay.Messaging(true);

    /**
     * Manages viewport of continuous paper component.
     *
     * @type {?shmi.visuals.controls.TrendDisplay.Viewport}
     */
    Object.defineProperty(this, "viewport", {
        get: function() {
            return viewport || null;
        },
        set: function(config) {
            if (!config || (config.isConfigured && !config.invalid)) {
                if (viewport) {
                    viewport.onUpdate.removeListener(this._onDirtyCanvas, this);
                    viewport.onDelete();
                }

                if (config) {
                    viewport = new shmi.visuals.controls.TrendDisplay.Viewport(config.viewport);
                    viewport.onUpdate.push([this._onDirtyCanvas, this]);
                }
            }
        }
    });

    /**
     * Retrieves (visible) width of continuous paper component in pixels.
     *
     * @return {int}
     */
    Object.defineProperty(this, "width", {
        get: function() {
            return this.element.offsetWidth;
        }
    });

    /**
     * Retrieves (visible) height of continuous paper component in pixels.
     *
     * @return {int}
     */
    Object.defineProperty(this, "height", {
        get: function() {
            return this.element.offsetHeight;
        }
    });

    /**
     * Provides reference on registered listeners for processing mouse input
     * events.
     *
     * @property mouseInput
     * @type {shmi.visuals.io.MouseListener}
     * @readonly
     */
    Object.defineProperty(this, "mouseInput", {
        value: listeners.mouse
    });

    /**
     * Provides reference on registered listeners for processing touch input
     * events.
     *
     * @property touchInput
     * @type {shmi.visuals.io.TouchListener}
     * @readonly
     */
    Object.defineProperty(this, "touchInput", {
        value: listeners.touch
    });

    /**
     * Provides message queue used to notify on horizontally dragging
     * continuous.
     *
     * @note This messaging queue works synchronously and thus doesn't support
     *       promises returned for deferring message delivery. It is designed to
     *       notify simple handlers used to animate movement of all DOM elements
     *       involved in sliding trend display.
     *
     * @type {shmi.visuals.controls.TrendDisplay.Messaging}
     * @readonly
     */
    Object.defineProperty(this, "onSlide", {
        value: onSlide
    });

    /**
     * Provides message queue used to notify on vertically dragging continuous.
     *
     * @note This messaging queue works synchronously and thus doesn't support
     *       promises returned for deferring message delivery. It is designed to
     *       notify simple handlers used to animate movement of all DOM elements
     *       involved in shifting trend display.
     *
     * @type {shmi.visuals.controls.TrendDisplay.Messaging}
     * @readonly
     */
    Object.defineProperty(this, "onShift", {
        value: onShift
    });

    /**
     * Provides message queue used to notify on handling certain states or state
     * changes enabling receiving plugins to replace default behaviour.
     *
     * @note This messaging queue works synchronously and thus doesn't support
     *       promises returned for deferring message delivery.
     *
     * @type {shmi.visuals.controls.TrendDisplay.Messaging}
     * @readonly
     */
    Object.defineProperty(this, "onState", {
        value: onState
    });

    /**
     * Manages access on curves.
     *
     * @note On writing this property takes a configuration used to create
     *       curves for.
     *
     * @property curves
     * @type {Array.<shmi.visuals.controls.TrendDisplay.AbstractRenderer>}
     */
    Object.defineProperty(this, "curves", {
        get: function() {
            return curves;
        },
        set: function(config) {
            if (config.isConfigured && !config.invalid) {
                curves = this._generateCurves(config);
            }
        }
    });

    /**
     * Manages access on rendering manager.
     *
     * @note On writing this property takes a configuration used to create
     *       rendering manager for.
     *
     * @property renderer
     * @type {shmi.visuals.controls.TrendDisplay.AbstractRenderingManager}
     */
    Object.defineProperty(this, "renderer", {
        get: function() {
            return renderer;
        },
        set: function(config) {
            if (!config || (config.isConfigured && !config.invalid)) {
                if (renderer) {
                    onSlide.removeListener(renderer.onSlide, renderer);
                    onShift.removeListener(renderer.onShift, renderer);

                    renderer.onDelete();
                }

                if (config) {
                    renderer = this._generateRenderingManager(config);

                    onSlide.addListener(renderer.onSlide, renderer);
                    onShift.addListener(renderer.onShift, renderer);
                }
            }
        }
    });

    /**
     * Refers to associated trend data provider.
     *
     * @property trends
     * @type {Object.<string,shmi.visuals.controls.TrendDisplay.Trend>}
     * @readonly
     */
    Object.defineProperty(this, "trend", {
        get: function() {
            return trend;
        },
        set: function(config) {
            if (config.isConfigured && !config.invalid) {
                trend = this.trendDisplay.data.getTrendByName(config.trend.name);
            }
        }
    });
};

shmi.visuals.controls.TrendDisplay.Continuous.prototype = {
    uiType: "trend-display-continuous",
    isContainer: false,
    getClassName: function() {
        "use strict";
        return "TrendDisplayContinuous";
    },

    _initializeConfiguration: function(config) {
        "use strict";

        shmi.def(config, "class-name", "trend-display-continuous");
        shmi.def(config, "template", "default/trend-display/component/continuous");

        return config;
    },

    onInit: function() {
        "use strict";

        this._super(this.TrendDisplayContinuous, "onInit");
    },

    _interactionMode: {
        pinching: false,
        sliding: false,
        shifting: false
    },

    _initUiControl: function(element) {
        "use strict";

        var TD = shmi.visuals.controls.TrendDisplay,
            self = this,
            mode = self._interactionMode,
            zoom = {
                base: {
                    x: 0,
                    y: 0
                }, // zoom scales on starting gesture
                current: {
                    x: 0,
                    y: 0
                }, // zoom scales to be applied
                applied: {
                    x: 0,
                    y: 0
                }, // zoom scales applied previously
                focus: {
                    x: 0,
                    y: 0
                },
                throttle: null
            },
            canvas,
            handler,
            size,
            slide,
            shift;

        function _initSlide(x, vp, canvasParam, dc) {
            if (!dc) {
                return;
            }
            var left = dc.time2X(vp.leftTime, 0),
                minLeft = dc.time2X(vp.minLeftTime, 0),
                maxLeft = dc.time2X(vp.maxLeftTime, 0);

            slide = {
                start: parseInt(canvasParam.style.left) || 0
            };

            slide.end = slide.start;

            // left is negative initially, thus shifting canvas to the left out of screen
            // -> minX is limiting sliding to the _right_, but is less negative value
            slide.min = slide.start - (maxLeft - left);
            // -> maxX is limiting sliding to the _left_, but is bigger negative value
            slide.max = slide.start + (left - minLeft);
        }

        function _initShift(y, vp, canvasParam, dc, canvasSize) {
            if (!dc) {
                return;
            }
            var minValueY = dc.value2Y(dc.minValue, 0),
                maxValueY = dc.value2Y(dc.maxValue, 0);

            shift = {
                start: parseInt(canvasParam.style.top) || 0
            };

            shift.end = shift.start;

            // left is negative initially, thus shifting canvas to the left out of screen
            // -> minX is limiting sliding to the _right_, but is less negative value
            shift.min = -(minValueY - canvasSize.height);
            // -> maxX is limiting sliding to the _left_, but is bigger negative value
            shift.max = -maxValueY;
        }

        /**
         * Starts throttled repaint processed unless having started before.
         *
         * @private
         */
        function _onChangingZoom() {
            if (!zoom.throttle) {
                zoom.throttle = setTimeout(_onThrottlingZoom, 50);
            }
        }

        /**
         * Invokes repaint process unless zooming hasn't changed actually.
         *
         * @private
         */
        function _onThrottlingZoom() {
            zoom.throttle = null;

            if (zoom.current.x !== zoom.applied.x || zoom.current.y !== zoom.applied.y) {
                _applyZoom();
            }
        }

        /**
         * Updates viewport according to internally tracked changes on zooming
         * implicitly triggering repaint due to registered change listeners on
         * viewport.
         *
         * @private
         */
        function _applyZoom() {
            var vp = self.viewport;

            vp.transaction = true;

            zoom.applied.x = zoom.current.x;
            zoom.applied.y = zoom.current.y;

            vp.handleTime = zoom.focus.x;
            vp.zoomFocusY = zoom.focus.y;

            vp.zoomX = zoom.current.x;
            vp.zoomY = zoom.current.y;

            vp.transaction = false;
        }

        function _zoomY(scale, posY) {
            var vp = self.viewport,
                dc = self.getDC();
            if (!dc) {
                return;
            }

            zoom.current.y = TD.limit(scale, vp.minZoomY, vp.maxZoomY);

            if (!isNaN(posY)) {
                zoom.focus.y = dc.deriveFocusY(zoom.current.y, -(posY - self.renderer.getCanvasSize().height / 2));
            }
        }

        handler = {
            onPress: function(x, y, event) {
                if (self.trendDisplay.locked) {
                    return;
                }
                var vp = self.viewport,
                    renderer = self.renderer,
                    dc = self.getDC();

                size = renderer.getCanvasSize();
                canvas = renderer.canvas;

                zoom.base.x = zoom.current.x = zoom.applied.x = vp.zoomX;
                zoom.base.y = zoom.current.y = zoom.applied.y = vp.zoomY;

                _initSlide(x, vp, canvas, dc);
                _initShift(y, vp, canvas, dc, size);
            },

            onRelease: function(x, y, event) {
                var td = self.trendDisplay,
                    vp = self.viewport,
                    dc = self.getDC();

                if (slide && slide.end !== slide.start) {
                    vp.leftTime = dc.x2Time(dc.time2X(vp.leftTime, 0) + (slide.start - slide.end));
                }

                if (shift && shift.end !== shift.start) {
                    vp.zoomFocusY = TD.limit(dc.y2Focus(dc.focus2Y(vp.zoomFocusY) - (shift.end - shift.start)), dc.minFocus(), dc.maxFocus());
                }

                // notify on end of either phase of adjusting viewport
                if (mode.sliding) {
                    vp.transaction = false;

                    mode.sliding = false;
                    shmi.removeClass(self.element, "sliding");
                    td.broadcast(self, "slide-end");
                }

                if (mode.shifting) {
                    vp.transaction = false;

                    mode.shifting = false;
                    shmi.removeClass(self.element, "shifting");
                    td.broadcast(self, "shift-end");
                }

                if (mode.pinching) {
                    var pinchElement = shmi.getUiElement("pinch-canvas", self.element.parentNode);

                    vp.transaction = false;

                    mode.pinching = false;

                    _applyZoom();

                    shmi.removeClass(self.element, "pinching");
                    if (pinchElement) {
                        pinchElement.style.display = "none";
                    }
                    td.broadcast(self, "pinch-end");
                }
            },

            onClick: function(x, y, event) {
                if (self.trendDisplay.locked) {
                    return;
                }
                if (event.target.nodeName === 'CANVAS' && self.getDC()) {
                    var pos = self._globalToLocal(x, y);
                    self.viewport.handleTime = self.getDC().x2Time(pos.x + size.x);
                }
            },

            onDoubleClick: function(x, y, event) {},

            onDrag: function(dx, dy, event) {
                if (self.trendDisplay.locked) {
                    return;
                }
                var v;
                if (!slide) {
                    return;
                }

                // manage horizontal movements (sliding)
                v = TD.limit(slide.end + dx, slide.min, slide.max);
                if (v !== slide.end) {
                    slide.end = v;

                    if (!mode.sliding) {
                        self.viewport.transaction = true;

                        mode.sliding = true;
                        shmi.addClass(self.element, "sliding");
                        self.trendDisplay.broadcast(self, "slide-start");

                        // stop live rendering on dragging (due to disturbing drag animation)
                        self.trend.running = false;
                        self.viewport.isLive = false;
                    }

                    self.onSlide.send("move", v - slide.start);
                } else if (v === slide.start) {
                    // set live mode at right boundary end (real time)
                    self.trend.running = true;
                    self.viewport.isLive = true;
                }

                // manage vertical movements (shifting)
                v = TD.limit(shift.end + dy, shift.min, shift.max);
                if (v !== shift.end) {
                    shift.end = v;

                    if (!mode.shifting) {
                        self.viewport.transaction = true;

                        mode.shifting = true;
                        shmi.addClass(self.element, "shifting");
                        self.trendDisplay.broadcast(self, "shift-start");
                    }

                    self.onShift.send("move", v - shift.start);
                }

                event.preventDefault();
                event.stopPropagation();
            },

            onScale: function(scale, posX, posY, scaleX, scaleY, points) {
                if (self.trendDisplay.locked) {
                    return;
                }
                var pinchElement = shmi.getUiElement("pinch-canvas", self.element.parentNode);

                // disable zoom for swim lane
                if (!self.viewport.isDashboardView) {
                    if (!mode.pinching) {
                        self.viewport.transaction = true;

                        mode.pinching = true;
                        shmi.addClass(self.element, "pinching");
                        self.trendDisplay.broadcast(self, "pinch-start");

                        // keep track of focused time for zooming horizontally
                        zoom.focus.x = self.getDC().x2Time(posX + size.x);
                        if (pinchElement) {
                            pinchElement.style.display = "block";
                        }
                    }

                    if (pinchElement) {
                        pinchElement.width = pinchElement.parentNode.getBoundingClientRect().width;
                        pinchElement.height = pinchElement.parentNode.getBoundingClientRect().height;
                        var pinchStyle = getComputedStyle(pinchElement).color; //eslint-disable-line no-undef
                        var context = pinchElement.getContext("2d");
                        var p1 = points[2];
                        var p2 = points[3];
                        var rect = {
                            x: (p1.x < p2.x) ? p1.x : p2.x,
                            y: (p1.y < p2.y) ? p1.y : p2.y,
                            width: Math.abs(p1.x - p2.x),
                            height: Math.abs(p1.y - p2.y)
                        };

                        context.clearRect(0, 0, pinchElement.width, pinchElement.height);

                        context.beginPath();
                        context.strokeStyle = pinchStyle;
                        context.setLineDash([4, 2]);
                        context.rect(rect.x, rect.y, rect.width, rect.height);
                        context.stroke();
                    }

                    // zoom vertically
                    _zoomY(zoom.base.y * (scaleY + Math.sqrt(scaleY)) / 2, posY);

                    // zoom horizontally
                    zoom.current.x = zoom.base.x * (scaleX + Math.sqrt(scaleX)) / 2;

                    _onChangingZoom();
                }
            },

            onWheel: function(amount, event) {
                if (!self.trendDisplay || self.trendDisplay.locked) {
                    return;
                }
                event.preventDefault();

                var pos = self._globalToLocal(event.clientX, event.clientY),
                    vp = self.viewport,
                    newZoom;

                if (event.shiftKey) { // zoom X
                    if (amount > 0) {
                        zoom.current.x = vp.zoomX * shmi.c("TREND_DISPLAY_ZOOM_SCALE_X");
                    } else {
                        zoom.current.x = vp.zoomX / shmi.c("TREND_DISPLAY_ZOOM_SCALE_X");
                    }

                    zoom.focus.y = vp.zoomFocusY;

                    _zoomY(vp.zoomY);
                    _onChangingZoom();
                    // zoom Y
                } else if (vp.isDashboardView) {
                    // vertical scrolling instead of zoom Y for dashboard view
                    var display = self.trendDisplay.config.display;
                    if (!display.demo && event.target.nodeName === 'CANVAS') {
                        var scrollHeight = parseInt(amount / 10),
                            canvasHeight = parseInt(event.target.height, 10),
                            maxTopValue = -(canvasHeight / 4),
                            newTopValue = parseInt(event.target.style.top, 10) + scrollHeight;

                        if (newTopValue < maxTopValue) { // max top scroll
                            var lanes = event.target.parentNode.getElementsByClassName('lane'),
                                labelHeight = display.labelHeight,
                                maxBottomValue = maxTopValue - (2 * labelHeight * (lanes.length - 5));

                            if (newTopValue > maxBottomValue) { // max bottom scroll
                                // stop live rendering on scrolling (due to disturbing scroll animation)
                                self.trend.running = false;
                                self.viewport.isLive = false;

                                // scroll canvas
                                event.target.style.top = String(newTopValue) + "px";

                                // scroll lanes including labels, controls, handle bar values
                                var items = event.target.parentNode.parentNode.parentNode.querySelectorAll('.lane, .action, .handle-bar .value, .handle-bar .signal');
                                for (var i = 0; i < items.length; ++i) {
                                    var item = items[i];
                                    item.style.top = String(parseInt(item.style.top, 10) + scrollHeight) + "px";
                                }
                            }
                        }
                    }
                } else {
                    zoom.current.x = vp.zoomX;

                    if (amount > 0) {
                        newZoom = vp.zoomY * shmi.c("TREND_DISPLAY_ZOOM_SCALE_Y");
                    } else {
                        newZoom = vp.zoomY / shmi.c("TREND_DISPLAY_ZOOM_SCALE_Y");
                    }

                    _zoomY(newZoom, pos.y);
                    _onChangingZoom();
                }
            }
        };

        return {
            mouse: new shmi.visuals.io.MouseListener(element, handler),
            touch: new shmi.visuals.io.TouchListener(element, handler)
        };
    },

    /**
     * Delivers drawing context of any contained curve's renderer.
     *
     * Returned drawing context is suitable for mapping abscissa values due to
     * all curves sharing same timeline. However, it is NOT suitable for mapping
     * any ordinate value (except for renderer's zoomFocusY) due to being bound
     * to some arbitrarily chosen curve.
     *
     * @return {shmi.visuals.controls.TrendDisplay.DrawingContext|undefined}
     */
    getDC: function() {
        "use strict";

        var curve = this.curves[0];

        return curve && curve.drawingContext;
    },

    _globalToLocal: function(x, y) {
        "use strict";

        var pos = shmi.getAbsPosition(this.element),
            scroll = shmi.visuals.controls.TrendDisplay.getScrollOffset();

        return {
            x: x - pos.x + scroll.x,
            y: y - pos.y + scroll.y
        };
    },

    onEnable: function() {
        "use strict";

        this.isPrepared.then(function(self) {
            self.mouseInput.enable();
            self.touchInput.enable();

            self.onResize();

            self.enableTrendStream(true);
        });
    },

    onDisable: function() {
        "use strict";

        this.enableTrendStream(false);

        this.mouseInput.disable();
        this.touchInput.disable();
    },

    onDelete: function() {
        "use strict";
        var self = this;

        self.mouseInput.disable();
        self.touchInput.disable();

        self.viewport = null;
        self.enableTrendStream(false);

        if (self.trendDisplay && self.trendDisplay.config && self.trendDisplay.config.trend && self.trendDisplay.config.trend.signals) {
            self.trendDisplay.config.trend.signals.forEach(function(el, idx) {
                var control = shmi.ctrl("." + self.renderer.config.name + ".actions-lane-" + idx);
                if (control) {
                    shmi.deleteControl(control, true);
                }
            });
        }

        if (self.renderer && self.renderer.controls) {
            self.renderer.controls.onDelete();
        }

        self.renderer = null;

        self.onSlide.reset();
        self.onShift.reset();
        self.onState.reset();

        self._super(self.TrendDisplayContinuous, "onDelete");
    },

    onDiscover: function(event) {
        "use strict";

        var td = event.detail.trendDisplay;

        td.isPreparing = "discover-continuous";

        this._super(this.TrendDisplayContinuous, "onDiscover", event);

        // update component according to configuration of trend display control
        this._configure(td.config);

        // enable processing of broadcasts regarding remote controlling trend
        // display by user actions and trend display's virtual items
        this.onBroadcast.push([100, this._onBroadcast, this]);

        // mark this component to be discovered
        this.isPrepared.resolve(this);

        td.isPreparing = false;
    },

    _configure: function(config) {
        "use strict";

        if (!config.invalid) {
            var TD = shmi.visuals.controls.TrendDisplay,
                bgCanvas;

            // Do not Recalibrate the viewport if noRFecal is set (called by trend-select widget)
            if (!config.noRecal) {
                this.viewport = config;
            }
            this.renderer = config;
            this.trend = config;
            this.curves = config;
            this.controls = config;

            if (this.trend.gotData.isResolved()) {
                var data = this.trend.data;

                this.viewport.minSampleTime = data.startTime;
                this.viewport.maxSampleTime = data.endTime;
            }

            // install background renderer
            bgCanvas = new TD.CanvasBackground(this.renderer, this.getDC(), this.curves);
            this.renderer.onRender.push([50, bgCanvas.render, bgCanvas]);
        }
    },

    /**
     * Integrates rendering manager for controlling all visual components to be
     * (continuously) rendered onto canvas of continuous paper component.
     *
     * @private
     */
    _generateRenderingManager: function(config) {
        "use strict";

        var self = this,
            TD = shmi.visuals.controls.TrendDisplay,
            container = shmi.getUiElement("continuous-canvas-container", self.element),
            canvas = shmi.getUiElement("continuous-canvas", container || self.element);

        switch (((canvas || {}).nodeName || "").toLowerCase()) {
        case "svg":
            return new TD.SvgRenderingManager(container, canvas, self.viewport, TD.SvgCurve, config);

        case "canvas":
            return new TD.CanvasRenderingManager(container, canvas, self.viewport, TD.CanvasCurve, config, self.trendDisplay);

        default:
            shmi.log("TrendDisplay: missing canvas for rendering trend curves", 2);
            return new TD.AbstractRenderingManager(container, canvas || {
                style: {}
            }, self.viewport, function _privateClass() {}, {});
        }
    },

    /**
     * Creates curve renderers for every configured signal of current trend
     * display control and registers them with rendering manager of current
     * component.
     *
     * @return {Array|*}
     * @private
     */
    _generateCurves: function(config) {
        "use strict";

        var renderer = this.renderer,
            CurveClass = renderer.elementClass,
            curves = [];

        if (typeof config.trend.signals !== "undefined") {
            curves = config.trend.signals.
                map(function(signal, index) {
                    var curve = new CurveClass(renderer, signal);

                    curve.index = index;

                    renderer.onRender.push([curve.render, curve]);

                    return curve;
                });
        }

        return curves;
    },

    /**
     * Handles case of having resized size of trend display affecting size of
     * visible part of continuous paper.
     */
    onResize: function() {
        var element = this.element,
            renderer = this.renderer;

        if (renderer) {
            this.renderer.resize(element.offsetWidth, element.offsetHeight, this.viewport.prefetch);
        }
    },

    /**
     * Starts/stops stream of incoming trend data.
     *
     * @param {boolean} enabled
     */
    enableTrendStream: function(enabled) {
        "use strict";

        var trend = this.trend;

        if (trend) {
            if (enabled) {
                // link with rendering manager to update curves each time new data arrives
                trend.onData.addListener(this._onTrendData, this);

                // configure and enable trend provider
                this._configureTrend(trend).running = true;
            } else {
                // stop trend provider
                trend.running = false;

                // drop any previously established link
                trend.onData.removeListener(this._onTrendData, this);
            }
        }
    },

    /**
     * Handles incoming trend data on behalf of Continuous component.
     *
     * @param trend
     * @private
     */
    _onTrendData: function(trend) {
        var vp = this.viewport,
            data = trend.data;

        vp.transaction = true;

        vp.minSampleTime = data.startTime;
        vp.maxSampleTime = data.endTime;

        if (vp.isLive) {
            this._setLeftTimeInLiveMode(vp);
        }

        // notify renderer on updated data
        this.renderer.update(data, this.trendDisplay.isPreparing || vp.transactionQueue.length > 0);

        // notify all interested parties on having updated trend
        this.broadcast("trend-update", {
            trend: trend
        });

        vp.transaction = false;
    },

    /**
     * Transfers selected viewport properties to related virtual items.
     *
     * @param {Array.<string>} properties
     * @private
     */
    _updateViewportRelatedItems: function(properties) {
        var items = this.trendDisplay.items,
            viewport = this.viewport;

        if (!items) {
            //analyze and fix error on "items" sometimes being undefined unexpectedly
            return;
        }

        if (!properties.length || properties.indexOf("isLive") >= 0) {
            items.getByName("live").writeValue(viewport.isLive ? 1 : 0, true);
        }

        if (!properties.length || properties.indexOf("zoomY") >= 0) {
            items.getByName("zoomY").writeValue(viewport.zoomY * 100, true);
        }

        if (!properties.length || properties.indexOf("zoomX") >= 0) {
            items.getByName("zoomX").writeValue(viewport.zoomX * 100, true);
        }

        if (!properties.length || properties.indexOf("handleTime") >= 0) {
            items.getByName("handleTime").writeValue(viewport.handleTime, true);
        }

        if (!properties.length || properties.indexOf("leftTime") >= 0) {
            items.getByName("leftTime").writeValue(viewport.leftTime, true);
        }
    },

    /**
     * Adjusts leftTime of viewport supporting optional plugins replacing
     * default behaviour.
     *
     * @param viewport
     * @private
     */
    _setLeftTimeInLiveMode: function(viewport) {
        viewport.transaction = true;

        return this.onState.send(this, {
            mode: "live",
            viewport: viewport
        }).
            then(function(stopped) {
                if (!stopped) {
                // default action on re-enabling live mode:
                // slide viewport to show live data at right end
                    viewport.rightTime = viewport.maxSampleTime;
                }
            }).
            finally(function() {
                viewport.transaction = false;
            });
    },

    /**
     * Initially sets handleTime of viewport to move handle bar to desired
     * initial position.
     *
     * @param viewport
     * @private
     */
    _setInitialHandleTime: function(viewport) {
        var self = this;

        viewport.transaction = true;

        return this.onState.send(this, {
            mode: "handleTime",
            viewport: viewport
        }).
            then(function(stopped) {
                if (!stopped) {
                // default action on re-enabling live mode:
                // slide viewport to show live data at right end
                    if (viewport.isLive) {
                    // live mode -> set handle bar to right end of visible
                    // time span showing latest sample
                        var time = viewport.maxSampleTime;

                        // (ensure handle bar is kept in visible part of canvas)
                        var dc = self.getDC();
                        var canvas = self.renderer.getCanvasSize();
                        var x = dc.time2X(time);
                        if (x >= canvas.x + canvas.width) {
                            time = dc.x2Time(canvas.x + canvas.width - 1);
                        }

                        viewport.handleTime = time;
                    } else if (!self.trendDisplay.config.noRecal) {
                    // non-live mode -> center handle bar in visible time span
                        viewport.handleTime = (viewport.leftTime + viewport.rightTime) / 2;
                    } else {
                        // call this to update virtual items
                        self._onUpdateHandleTime(viewport.handleTime);
                    }
                }
            }).
            finally(function() {
                viewport.transaction = false;
            });
    },

    /**
     * Processes event of having changed viewport.
     *
     * @param {shmi.visuals.controls.TrendDisplay.Viewport} viewport
     * @param {string[]} properties names of viewport properties actually changed
     * @private
     */
    _onDirtyCanvas: function(viewport, properties) {
        "use strict";

        var restartingTrend = false,
            trend = this.trend,
            filtered = properties.filter(function(n) {
                return n !== "initialize";
            });

        // ensure related virtual items are up-to-date
        this._updateViewportRelatedItems(filtered);

        if (properties.indexOf("initialize") < 0) {
            // handle case of re-enabling live mode requiring some more complex fixture
            if (properties.indexOf("isLive") >= 0 && viewport.isLive) {
                this._setLeftTimeInLiveMode(viewport);

                // restart trend data stream below
                restartingTrend = true;
            }

            if (viewport.handleTime) {
                this._onUpdateHandleTime(viewport.handleTime);
            }

            if (properties.indexOf("zoomX") >= 0) {
                // stop live rendering on x-zooming (for live mode is sticking
                // display to end of curve breaking with intention of keeping
                // selected handle time centered on zooming)
                trend.running = false;
                viewport.isLive = false;
            }

            if (!viewport.transactionQueue.length) {
                this.broadcast("render");
            } // ELSE: viewport got dirty again and thus will call this method again

            // then fetch more accurate data if required
            if (restartingTrend || this._requiresFetchingData()) {
                // stop any still running trend stream
                trend.running = false;

                // reconfigure and restart stream then
                this._configureTrend(trend).running = true;
            }
        }
    },

    /**
     * Processes event of having changed time selected by "handle bar".
     *
     * @param timestamp
     * @private
     */
    _onUpdateHandleTime: function(timestamp) {
        var dc = this.getDC(),
            size = this.renderer.getCanvasSize(),
            x = dc.time2X(timestamp, 0),
            self = this,
            items = this.trendDisplay.items,
            data = this.trend.data,
            vp = this.viewport,
            curves = this.curves,
            lanes = [],
            signal;

        if (this.trendDisplay.handleDragging) {
            return;
        }

        var prog = this.trendDisplay.controls[4]; // @todo: what is this?! fix control

        // update virtual items providing values at position of handle bar (keep previous values if trend isn't including up-to-date information)
        if (timestamp >= data.startTime && timestamp <= data.endTime) {
            data = data.values;
        } else {
            data = null;
        }

        if (vp.isMultiLane) {
            Object.keys(curves).
                forEach(function(curve, index) {
                    signal = self.curves[curve].signal;
                    if (typeof lanes[signal.laneIndex] === 'undefined') {
                        lanes[signal.laneIndex] = [];
                    }
                    lanes[signal.laneIndex].push(signal);
                });
        }

        var display = this.trendDisplay.config.display,
            labelHeight = display.labelHeight,
            count = vp.isDashboardView ? (lanes.length < display.lanesPerView ? lanes.length : display.lanesPerView) : 1, // max lanes before vertical scrolling
            canvasHeight = size.height,
            allLabelHeight = labelHeight * count,
            allLaneHeight = /*( canvasHeight - allLabelHeight ) / count < 50 ? 250 :*/ canvasHeight - allLabelHeight,
            laneHeight = allLaneHeight / count;

        Object.keys(curves).
            forEach(function(curveName) {
                var innerSignal = self.curves[curveName].signal,
                    name = innerSignal.alias || innerSignal.name,
                    vIndex = innerSignal.valueIndex,
                    laneIndex = innerSignal.laneIndex || 0,
                    item = items.getByName(name, "value"),
                    handleBar = shmi.getUiElement("handle-bar", this.element),
                    isActive = true,
                    prevSample = null,
                    curSample = null,
                    value = null,
                    a,
                    b,
                    innerX;

                if (vp.isMultiLane) {
                    if (lanes[laneIndex].length > 1) {
                        var multiLaneSelectBox = shmi.ctrl(self.trendDisplay.getName() + '.select-box-multi-lane-' + laneIndex); // @todo: use trend display name manager.config.name
                        if (multiLaneSelectBox && vIndex !== multiLaneSelectBox.value) {
                            isActive = false;
                        }
                    }
                }
                var v = handleBar.parentNode.querySelector(".value.index-" + String(vIndex)),
                    s = handleBar.parentNode.querySelector(".signal.index-" + String(vIndex));

                if (isActive && data && item) {
                // find data sample next to time selected by handle bar
                    data.some(function(sample) {
                        prevSample = curSample;
                        curSample = {
                            timestamp: sample[0],
                            value: sample[1][vIndex]
                        };

                        return (sample[0] > timestamp);
                    });

                    if (curSample !== null && curSample.value !== null) {
                    // found some time next to handle bar
                    // -> write value into signal's virtual item
                        if (curSample.value && shmi.objectHasOwnProperty(curSample.value, "min") && shmi.objectHasOwnProperty(curSample.value, "max")) {
                            a = parseFloat(curSample.value.min);
                            b = parseFloat(curSample.value.max);

                            value = (a + b) / 2;
                        } else if (prevSample && prevSample.value) {
                            var distance = curSample.timestamp - prevSample.timestamp,
                                prevValue = parseFloat(prevSample.value),
                                curValue = parseFloat(curSample.value);

                            a = prevValue;
                            b = (curValue - prevValue) / distance;
                            innerX = (timestamp - prevSample.timestamp);

                            value = a + b * innerX;
                        } else {
                            value = parseFloat(curSample.value);
                        }

                        item.writeValue(value);

                        // show values of each signal next to handle bar
                        if (handleBar) {
                            var startY = canvasHeight - display.firstLabelHeight, // min point of current lane = 0%
                                floatValue = Math.round(value),
                                //difference      = Math.abs( signal.min - ( signal.max ) ), // codomain between min and max
                                percentageValue = ((Math.abs(value - innerSignal.min)) / (innerSignal.max - innerSignal.min)) * 100,
                                valueY, // one percentage in lane
                                valuePos = 10;

                            // lane calculation for multi lanes
                            if (vp.isDashboardView) {
                                startY = (labelHeight * (laneIndex)) + (laneHeight * (laneIndex + 1)) + display.labelHeight + display.handleBarOffset;
                                valuePos = (startY - laneHeight);
                            } else {
                                startY = canvasHeight - display.handleBarOffset;
                                valuePos = display.labelHeight;
                            }
                            // @todo linearberechnung -> Thomas Orban says not possible
                            valueY = startY - ((laneHeight * percentageValue) / 100);

                            // show values for timestamp at handle bar
                            if (!v && !s) {
                                var doc = handleBar.ownerDocument;

                                v = doc.createElement("div");
                                v.className = "value index-" + String(vIndex);
                                v.style.top = valuePos + "px";
                                v.innerHTML = String(floatValue);

                                s = doc.createElement("div");
                                s.className = "signal index-" + String(vIndex);
                                s.style.top = valueY + "px";

                                handleBar.appendChild(v);
                                handleBar.appendChild(s);
                            } else { // update point position and value label
                                s.style.top = valueY + "px";
                                v.style.top = valuePos + "px";
                                v.innerHTML = String(floatValue);
                            }
                        }
                    }
                } else if (v && s) { // remove signal label values
                    handleBar.removeChild(v);
                    handleBar.removeChild(s);
                }
            });

        if (prog) {
            var progData = prog.program;
            if (progData) {
                progData.steps.then(function(steps) {
                    var curStep = steps.asArray[0];
                    var curDist = Math.abs(timestamp - curStep.tStartReal);
                    steps.asArray.forEach(function(step, idx) {
                        var stepDist = Math.abs(timestamp - step.tStartReal);
                        if ((stepDist < curDist) && (timestamp >= step.tStartReal)) {
                            curStep = step;
                            curDist = stepDist;
                        } else if ((stepDist === curDist) && (timestamp > curStep.tStartReal)) {
                            curStep = step;
                        }
                    });
                    console.debug("[TrendDisplay]", "current step:", curStep);
                    curStep.cvn.forEach(function(cv, idx) {
                        items.items.pgm["CV".concat(progData.cvs[idx].valueIndex)].writeValue(cv);
                    });
                    curStep.svn.forEach(function(sv, idx) {
                        items.items.pgm["SV".concat(progData.svs[idx].valueIndex)].writeValue(sv);
                    });
                    curStep.ndo.forEach(function(doVal, idx) {
                        items.items.pgm["DO".concat(progData.dos[idx].valueIndex)].writeValue(doVal ? 1 : 0);
                    });
                });
            }
        }

        this.broadcast("move-handle", {
            time: timestamp,
            refTime: this.viewport.minTime,
            left: x,
            canvas: size
        });
    },

    /**
     * Detects if currently cached data is sufficient for rendering curve
     * viewport or not.
     *
     * @return {boolean} true if trend data needs to be fetched (again)
     * @private
     */
    _requiresFetchingData: function() {
        var vp = this.viewport,
            td = this.trend,
            unit;

        if (this._interactionMode.pinching) {
            return false;
        }

        if (td) {
            unit = this._getBestTrendUnit();
            if (unit !== td.unit) {
                return true;
            }

            return (vp.leftTime < td.startTime || (!vp.isLive && vp.rightTime > td.endTime));
        }

        return false;
    },

    /**
     * Delivers optimum aggregation unit to use on fetching data.
     *
     * This method is basically providing unit defined in configuration, but
     * increases it in case of configured unit is exceeding granularity required
     * by current display's resolution, size of canvas and zoom factor. In
     * addition it chooses some unit automatically if configuration isn't
     * explicitly selecting some fixed unit.
     *
     * @return {int} unit to use (by means of seconds between two samples)
     * @private
     */
    _getBestTrendUnit: function() {
        var vp = this.viewport,
            cfg = this.trendDisplay.config || {},
            minUnit = Math.ceil(vp.visibleTimeSpan / this.renderer.getCanvasSize().width * cfg.display.pixelsPerValue),
            unit;

        unit = cfg.trend.aggregate.unit;
        if (!unit || unit < minUnit) {
            unit = minUnit;
        }

        return unit;
    },

    /**
     * Configures provided trend according to current state of continuous paper
     * component.
     *
     * This configuration regards timestamps at either end of visible part of
     * continuous paper display as well as information on prefetching data and
     * choosing aggregation of values.
     *
     * @param {shmi.visuals.controls.TrendDisplay.Trend} trend
     * @return {shmi.visuals.controls.TrendDisplay.Trend}
     * @private
     */
    _configureTrend: function(trend) {
        "use strict";

        var vp = this.viewport,
            cfg = this.trendDisplay.config || {};

        trend.startTime = vp.leftRenderTime;
        trend.endTime = vp.isLive ? -1 : vp.rightRenderTime;
        trend.aggregate = cfg.trend.aggregate.method;
        trend.unit = this._getBestTrendUnit();
        if (cfg.trend.items.length) {
            var items = cfg.trend.items.map(function(object) {
                return object.alias;
            });
            trend.items = items;
        }

        return trend;
    },

    /**
     * @param {SHMI_TD.Broadcast} broadcast
     * @private
     */
    _onBroadcast: function(broadcast) {
        var curve,
            item,
            v,
            self = this,
            vp = this.viewport;

        switch (broadcast.type) {
        case "ui-show-curve":
            this.isPrepared.then(function(selfParam) {
                var data = broadcast.detail.value,
                    cfg = selfParam.trendDisplay.config || {};

                curve = selfParam.curves[data.curveIndex];
                if (curve) {
                    // check visibility after switching view
                    curve.visible = data.isVisible && cfg.trend.items[data.curveIndex].visible;
                    selfParam.renderer.update();

                    if (broadcast.detail.cause !== "item") {
                        // broadcast not sent due to changed item
                        // -> update related item now
                        item = selfParam.trendDisplay.items.getByName(curve.signal.alias || curve.signal.name, "visible");
                        if (item) {
                            item.writeValue(data.isVisible ? 1 : 0);
                        }
                    }
                }
            });
            break;

        case "ui-live":
            if (vp && !this.trendDisplay.isPreparing) {
                this.isPrepared.then(function(selfParam) {
                    vp.isLive = !!broadcast.detail.value;
                    if (broadcast.detail.cause !== "item") {
                        selfParam._updateViewportRelatedItems(["isLive"]);
                    }
                });
            }
            break;

        case "ui-zoomX":
            if (vp) {
                this.isPrepared.then(function(selfParam) {
                    v = broadcast.detail.value / 100;
                    vp.zoomX = (v >= vp.minZoomX) ? v : vp.minZoomX;
                    if (broadcast.detail.cause !== "item") {
                        selfParam._updateViewportRelatedItems(["zoomX"]);
                    }
                });
            }
            break;

        case "ui-zoomY":
            if (vp) {
                this.isPrepared.then(function(selfParam) {
                    v = broadcast.detail.value / 100;
                    vp.transaction = true;
                    vp.zoomY = (v >= vp.minZoomY) ? v : vp.minZoomY;
                    vp.zoomFocusY = selfParam.getDC().deriveFocusY(vp.zoomY, 0);
                    vp.transaction = false;
                    if (broadcast.detail.cause !== "item") {
                        selfParam._updateViewportRelatedItems(["zoomY"]);
                    }
                });
            }
            break;

        case "ui-handleTime":
            if (vp) {
                this.isPrepared.then(function(selfParam) {
                    v = broadcast.detail.value;
                    vp.handleTime = (v >= vp.minLeftTime) ? v : vp.minLeftTime;
                    if (broadcast.detail.cause !== "item") {
                        selfParam._updateViewportRelatedItems(["handleTime"]);
                    }
                });
            }
            break;

        case "ui-leftTime":
            if (vp) {
                this.isPrepared.then(function(selfParam) {
                    v = broadcast.detail.value;
                    vp.leftTime = (v >= vp.minLeftTime) ? v : vp.minLeftTime;
                    if (broadcast.detail.cause !== "item") {
                        selfParam._updateViewportRelatedItems(["leftTime"]);
                    }
                });
            }
            break;

        case "resize":
            this.onResize();
            break;

        case "preparation-finished":
            this.trend.gotData.
                then(function() {
                    var explicitRender;

                    vp.transaction = true;

                    if (vp.isLive) {
                        // only in live-mode
                        self._setLeftTimeInLiveMode(vp);
                    }
                    self._setInitialHandleTime(vp);
                    self._updateViewportRelatedItems([]);

                    explicitRender = !vp.transactionQueue.length;

                    vp.transaction = false;

                    if (explicitRender) {
                        self.renderer.update();
                    }
                });
            break;

        case "render":
            if (!broadcast.detail.suppress) {
                this.renderer.update();
            }
            break;

        case "configuration-changed":
            this.trendDisplay.isPreparing = "reconfigure-continuous";

            this.onDisable();
            this._configure(broadcast.detail.configuration);
            this.onEnable();

            this.trendDisplay.isPreparing = false;

            this._updateViewportRelatedItems([]);
            break;
        default:
        }
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.Continuous, shmi.visuals.controls.TrendDisplay.Component, "TrendDisplayContinuous");

/**
 * Validates configuration regarding "continuous paper" component.
 *
 * @param {Object.<string,*>} configuration qualified configuration object
 */
shmi.visuals.controls.TrendDisplay.onConfiguration.push(function _onConfigureContinuous(cfg, tdControl) {
    "use strict";

    if (!cfg.display) {
        cfg.display = {};
    }

    var ppv = parseInt(cfg.display.pixelsPerValue);
    if (isNaN(ppv)) {
        ppv = 1;
    }

    cfg.display.pixelsPerValue = ppv;
});

/**
 * Define scale factors to apply on zooming rendered curves using mouse wheel.
 */
shmi.c("TREND_DISPLAY_ZOOM_SCALE_X", 1.2);
shmi.c("TREND_DISPLAY_ZOOM_SCALE_Y", 1.2);

/**
 * Define limits for horizontal zoom factor.
 */
shmi.c("TREND_DISPLAY_ZOOM_SCALE_X_MIN", 0.00001);
shmi.c("TREND_DISPLAY_ZOOM_SCALE_X_MAX", Number.MAX_SAFE_INTEGER);

/**
 * Define limits for vertical zoom factor.
 */
shmi.c("TREND_DISPLAY_ZOOM_SCALE_Y_MIN", 1);
shmi.c("TREND_DISPLAY_ZOOM_SCALE_Y_MAX", 20);

/**
 * Define timeout for retrievel of trend-info object (trend-manager)
 */
shmi.c("TREND_DISPLAY_TRENDINFO_TIMEOUT", 10000);

/**
 * Implements specific behaviour of handle bar component used to mark single
 * time on continuous paper.
 *
 * @param {HtmlElement} element DOM element detected to represent a component
 * @param {{}} configuration instantly available configuration
 * @constructor
 */
//eslint-disable-next-line func-name-matching
shmi.visuals.controls.TrendDisplay.Handle = function TrendDisplayHandle(element, configuration) {
    "use strict";

    shmi.visuals.controls.TrendDisplay.Handle._super.call(this, element, configuration);

    var listeners = this._initUiControl(element);
    var leftTabbed = null;
    var isSet = null;

    /**
     * Provides reference on registered listeners for processing mouse input
     * events.
     *
     * @property mouseInput
     * @type {shmi.visuals.io.MouseListener}
     * @readonly
     */
    Object.defineProperty(this, "mouseInput", {
        value: listeners.mouse
    });

    /**
     * Provides reference on registered listeners for processing touch input
     * events.
     *
     * @property touchInput
     * @type {shmi.visuals.io.TouchListener}
     * @readonly
     */
    Object.defineProperty(this, "touchInput", {
        value: listeners.touch
    });

    /**
     * Marks if tab on handle bar is to be displayed left of handle bar or not.
     *
     * @property leftTabbed
     * @type {boolean}
     */
    Object.defineProperty(this, "leftTabbed", {
        get: function() {
            return leftTabbed;
        },
        set: function(value) {
            value = !!value;

            if (value !== leftTabbed) {
                leftTabbed = value;

                if (leftTabbed) {
                    shmi.addClass(element, "left-tabbed");
                } else {
                    shmi.removeClass(element, "left-tabbed");
                }
            }
        }
    });

    /**
     * Marks if handle bar has been set before.
     *
     * @property isSet
     * @type {boolean}
     */
    Object.defineProperty(this, "isSet", {
        get: function() {
            return isSet;
        },
        set: function(value) {
            if (isSet === null || value) {
                value = !!value;

                if (value !== isSet) {
                    isSet = value;

                    if (isSet) {
                        shmi.removeClass(element, "unset");
                        shmi.addClass(element, "set");
                    } else {
                        shmi.removeClass(element, "set");
                        shmi.addClass(element, "unset");
                    }
                }
            }
        }
    });
};

shmi.visuals.controls.TrendDisplay.Handle.prototype = {
    uiType: "trend-display-handle",
    isContainer: false,
    getClassName: function() {
        "use strict";
        return "TrendDisplayHandle";
    },

    _initializeConfiguration: function(config) {
        "use strict";

        shmi.def(config, "class-name", "trend-display-handle");
        shmi.def(config, "template", "default/trend-display/component/handle");

        return config;
    },

    onDiscover: function(event) {
        "use strict";

        this.isSet = false;

        Object.defineProperties(this, {
            /**
             * Refers to element representing handle bar supporting positioning
             * per CSS style property "left" with coordinates relative to
             * Continuous component.
             *
             * @property handleBar
             * @type {HtmlElement}
             * @readonly
             */
            handleBar: {
                value: shmi.getUiElement("handle-bar", this.element)
            },

            /**
             * Refers to element representing tab on handle bar showing
             * currently selected time supporting dragging of handle bar. It is
             * thus considered to be visually (and hierarchically) attached to
             * the handle bar.
             *
             * @property handleTab
             * @type {HtmlElement}
             * @readonly
             */
            handleTab: {
                value: shmi.getUiElement("handle-tab", this.element)
            },

            /**
             * Refers to element representing tab on handle bar showing
             * currently selected date supporting dragging of handle bar. It is
             * thus considered to be visually (and hierarchically) attached to
             * the handle bar.
             *
             * @property handleDate
             * @type {HtmlElement}
             * @readonly
             */
            handleDate: {
                value: shmi.getUiElement("handle-date", this.element)
            }
        });

        this.onBroadcast.push([this._onBroadcast, this]);

        this._super(this.TrendDisplayHandle, "onDiscover", event);

        var self = this,
            trendDisplay = event.detail.trendDisplay,
            continuous = trendDisplay.getComponents("trend-display-continuous")[0];

        if (continuous) {
            /**
             * Refers to continuous component current scale component is
             * considered related to.
             *
             * @property continuous
             * @type {shmi.visuals.controls.TrendDisplay.Continuous}
             * @readonly
             */
            Object.defineProperty(this, "continuous", {
                value: continuous
            });

            continuous.isPrepared.
                then(function() {
                    self.isPrepared.resolve(self);
                });
        }
    },

    onEnable: function() {
        "use strict";

        this.isPrepared.then(function(self) {
            self.mouseInput.enable();
            self.touchInput.enable();
        });
    },

    onDisable: function() {
        "use strict";

        this.mouseInput.disable();
        this.touchInput.disable();
    },

    _onBroadcast: function(broadcast) {
        var info = broadcast.detail,
            bar = this.handleBar,
            tab = this.handleTab,
            date = this.handleDate,
            cfg = this.trendDisplay.config,
            currentTime,
            currentDate,
            datestring,
            utcTime,
            x;

        switch (broadcast.type) {
        case "move-handle":
            this.isSet = true;

            x = info.left - info.canvas.x;

            bar.style.left = String(x) + "px";

            if (!info.keepTabbing) {
                this.leftTabbed = (x > info.canvas.width * 0.8);
            }

            if (date) {
                datestring = cfg['date-format'] || "$DD.$MM.$YYYY";
                utcTime = cfg["utc-time"] ? cfg["utc-time"] : false;

                if (this.trendDisplay.config["rel-time"] !== true) {
                    currentDate = Math.round(info.time);
                } else {
                    currentDate = Math.round(info.time - info.refTime);
                }

                date.textContent = shmi.requires("visuals.tools.date").
                    formatDateTime(currentDate, {
                        datestring: datestring,
                        utc: utcTime
                    });
            }

            if (tab) {
                if (this.trendDisplay.config["rel-time"] !== true) {
                    currentTime = this._formatRelativeTime(Math.round(info.time));
                } else {
                    currentTime = this._formatRelativeTime(Math.round(info.time - info.refTime));
                }
                tab.textContent = currentTime;
            }
            break;
        default:
        }
    },

    _formatRelativeTime: function(timeSpan) {
        if (this.trendDisplay.config["rel-time"] !== true) {
            var datestring = this.trendDisplay.config['time-format'] || "$HH:$mm:$ss";
            return shmi.requires("visuals.tools.date").formatDateTime(timeSpan, {
                datestring: datestring,
                utc: this.trendDisplay.config["utc-time"]
            });
        }

        var factors = [60, 60, 24, 30],
            suffix = ["", ":", ":", "d ", "m "],
            text = "",
            value,
            factor;
        while (factors.length > 2 && timeSpan > 0) {
            factor = factors.shift();
            value = timeSpan % factor;
            timeSpan = Math.floor(timeSpan / factor);

            if (factor === 60 && (factors.length > 2 || timeSpan > 0)) {
                value = String("00" + value).substr(-2);
            } else {
                value = String(value);
            }

            text = value + suffix.shift() + text;
        }

        return text;
    },

    _initUiControl: function(element) {
        "use strict";

        var TD = shmi.visuals.controls.TrendDisplay,
            self = this,
            mode = null,
            handler,
            slide,
            throttle,
            lastPosition,
            draggableAreaOffset = 200; // pixel to fire slide dragging left/right

        function _onDragHandleBar() {
            if (!throttle) {
                throttle = setTimeout(_onThrottledShowTime, 100);
            }
        }

        function _stopThrottledShowTime() {
            if (throttle) {
                clearTimeout(throttle); //eslint-disable-line no-undef
                throttle = null;
            }
        }

        function _onThrottledShowTime() {
            if (throttle) {
                // show values on handle bar
                slide.vp.handleTime = slide.dc.x2Time(slide.canvas.x + slide.bar + slide.end - slide.start);
                self.continuous._onUpdateHandleTime(slide.vp.handleTime);

                _stopThrottledShowTime();
                _showTime();
            }
        }

        function _showTime() {
            /*var v          = TD.limit( slide.current, slide.min, slide.max ),
            slideLeft  = v <= ( slide.min + draggableAreaOffset ),
            slideRight = v >= ( slide.max - draggableAreaOffset );
            var x = ( slideLeft || slideRight ) ? parseInt( self.handleBar.style.left ) : slide.canvas.x + slide.bar + slide.end - slide.start;*/
            var x = slide.canvas.x + slide.bar + slide.end - slide.start;

            // don't use more expensive broadcasting system of trend display
            // but simulate sending broadcast locally to improve performance
            // (will send full broadcast at end of dragging)
            self._onBroadcast({
                source: self,
                type: "move-handle",
                detail: {
                    time: slide.dc.x2Time(x),
                    refTime: slide.vp.minTime,
                    left: x,
                    canvas: slide.canvas,
                    keepTabbing: true
                }
            });
        }

        handler = {
            onPress: function(x, y, event) {
                var continuous = self.continuous,
                    box = continuous.element,
                    barX = parseInt(self.handleBar.style.left) || 0;

                if (!continuous.getDC()) {
                    return;
                }
                slide = {
                    box: box,
                    dc: continuous.getDC(),
                    canvas: continuous.renderer.getCanvasSize(),
                    vp: continuous.viewport,

                    bar: barX,

                    start: x,
                    end: x,
                    min: x - barX,
                    max: x + box.offsetWidth - barX - 1,
                    current: x
                };

                // save last position
                lastPosition = {
                    bar: barX,
                    x: x
                };

                event.preventDefault();
                event.stopPropagation();
            },

            onRelease: function(x, y, event) {
                var td = self.trendDisplay;

                td.handleDragging = false;

                if (slide) {
                    if (slide.end !== slide.start) {
                        _stopThrottledShowTime();

                        // trigger change of related viewport tracking current handle time
                        slide.vp.handleTime = slide.dc.x2Time(slide.canvas.x + slide.bar + slide.end - slide.start);
                    }

                    // notify on end of either phase of adjusting viewport
                    if (mode) {
                        slide.vp.transaction = false;

                        mode = null;
                        shmi.removeClass(self.element, "dragging");
                        td.broadcast(self, "drag-handle-end");
                    }

                    slide = null;
                }

                event.preventDefault();
                event.stopPropagation();
            },

            onDrag: function(dx, dy, event) {
                if (!slide) {
                    return;
                }
                slide.current += dx;

                var v = TD.limit(slide.current, slide.min, slide.max),
                    slideLeft = dx <= 0 && v <= (slide.min + draggableAreaOffset),
                    slideRight = dx >= 0 && v >= (slide.max - draggableAreaOffset),
                    display = self.trendDisplay.config.display;

                self.trendDisplay.handleDragging = true;

                if (v !== slide.end) {
                    slide.end = v;

                    if (!mode) {
                        slide.vp.transaction = true;

                        mode = true;
                        shmi.addClass(self.element, "dragging");
                        self.trendDisplay.broadcast(self, "drag-handle-start");
                    }

                    //if( slideLeft /*|| slideRight*/ && !display.demo ) {
                    //console.warn('slide.end -> drag');
                    //self.handleBar.style.left = String(slide.start - v) + "px";
                    //} else {
                    //console.warn('out of sliding area');
                    self.handleBar.style.left = String(slide.bar + slide.end - slide.start) + "px";
                    //}

                    _onDragHandleBar();
                } else if (slide !== null) { // manage horizontal movements (sliding with handle bar)
                    var startSliding = false;

                    if (slideLeft || slideRight) {
                        var barX = parseInt(self.handleBar.style.left), // handle bar position
                            moveX = (barX < 2 && barX === lastPosition.bar) ? 10 : (-(v) + slide.start); // position to slide to

                        if (slideLeft) {
                            startSliding = (slide.start >= slide.current && (moveX > lastPosition.x || (barX < 2 && barX === lastPosition.bar)));
                        } else if (slideRight) {
                            var maxEndPos = -(slide.canvas.width / 2); // real time position / end of viewable area

                            startSliding = ((-(self.continuous.element._offsetX) + v) > maxEndPos && moveX < lastPosition.x); // || slide.end >= slide.current || moveX < slide.end
                        }

                        // do nothing when sliding back to center
                        if (startSliding && !display.demo) {
                            //console.warn('Slide: '+ v +' moveX: '+ moveX +' : '+ slide.start, slide.end, slide.current, slide.min, slide.canvas.x, -(slide.canvas.x) + v , lastPosition);

                            slide.start += (dx);
                            slide.end += (dx);

                            if (!mode) {
                                slide.vp.transaction = true;

                                mode = true;
                                shmi.addClass(self.element, "dragging");
                                self.trendDisplay.broadcast(self, "drag-handle-start");

                                //slide.vp.handleTime = slide.dc.x2Time(slide.dc.time2X( slide.vp.handleTime, 0 ) + slide.canvas.x + slide.bar + slide.end - slide.start );//slide.canvas.x + slide.bar + slide.end - slide.start);
                                //self.continuous._onUpdateHandleTime(slide.vp.handleTime);

                                slide.vp.handleTime = slide.dc.x2Time(barX + slide.canvas.x + slide.bar + Number(slide.end) - slide.start);
                                self.continuous._onUpdateHandleTime(slide.vp.handleTime);

                                // stop live rendering on dragging (due to disturbing drag animation)
                                self.continuous.trend.running = false;
                                slide.vp.isLive = false;
                            }

                            // handle bar at the end of visible scaling
                            if (barX <= 20) {
                                //console.warn('--> moveX + multiplier: '+moveX);
                                moveX += 50;
                                barX = barX <= 1 ? 1 : barX;

                                // @todo: reload continuous scala with new signal values etc.
                            }

                            //console.warn('moveX: '+(moveX * multiplier)+ ' handleBar: '+parseInt( self.handleBar.style.left ) + ' -> ' + String(barX));

                            self.continuous.onSlide.send("move", moveX);

                            self.handleBar.style.left = String(barX) + "px";
                            //self.handleBar.style.left = String( slide.bar + slide.end - slide.start ) + "px";

                            _onDragHandleBar();
                        }

                        // update new position
                        lastPosition = {
                            bar: barX,
                            x: moveX
                        };
                    }
                }

                event.preventDefault();
                event.stopPropagation();
            }
        };

        return {
            mouse: new shmi.visuals.io.MouseListener(element, handler),
            touch: new shmi.visuals.io.TouchListener(element, handler)
        };
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.Handle, shmi.visuals.controls.TrendDisplay.Component, "TrendDisplayHandle");

/**
 * Implements specific behaviour of scale component displaying rulers for
 * reading curves rendered on continuous paper component.
 *
 * @param {HtmlElement} element DOM element detected to represent a component
 * @param {object} configuration instantly available configuration
 * @constructor
 */
//eslint-disable-next-line
shmi.visuals.controls.TrendDisplay.Scale = function TrendDisplayScale(element, configuration) {
    "use strict";

    /** @typedef {{x:number, width:number}} SimpleDimension */
    /** @typedef {number} CumulatedWidth */
    /** @typedef {{signals:Array.<SimpleDimension>, ruler:SimpleDimension, width:CumulatedWidth}} DimensionsSet */

    shmi.visuals.controls.TrendDisplay.Scale._super.call(this, element, configuration);

    var viewport = null;
    var renderer = null;
    var curves = null;
    var labels = null;
    var scales = null;
    var ruler = null;
    var dimensions = null;

    /**
     * Refers to viewport instance managed by associated
     * Continuous component.
     *
     * @property viewport
     * @type {shmi.visuals.controls.TrendDisplay.Viewport}
     */
    Object.defineProperty(this, "viewport", {
        get: function() {
            if (!viewport) {
                viewport = this.continuous ? this.continuous.viewport : null;
            }

            return viewport;
        },
        set: function() {
            viewport = null;
        }
    });

    /**
     * Refers to rendering manager controlling renderer engines
     * for drawing elements of scale.
     *
     * @property renderer
     * @type {SHMI_TD.AbstractRenderingManager}
     * @readonly
     */
    Object.defineProperty(this, "renderer", {
        get: function() {
            if (!renderer) {
                renderer = this._generateRenderingManager();
            }

            return renderer;
        },
        set: function() {
            if (renderer) {
                renderer.onDelete();
            }

            renderer = null;
        }
    });

    /**
     * Lists all curves incl. description of related signal and
     * incl. drawing context.
     *
     * @property curves
     * @type {Array.<SHMI_TD.AbstractRenderer>}
     */
    Object.defineProperty(this, "curves", {
        get: function() {
            var ascii = 0,
                labelLetter,
                hashedLabels = {};
            if (!curves) {
                curves = this.continuous ? this.continuous.curves.filter(function(c, index) {
                    var labelHash = "_" + c.signal.unit + "_" + c.signal.max + "_" + c.signal.min,
                        scaleVisible;
                    if (c.signal.scaleVisible) { // generate separate letter for the `visible` scale
                        labelLetter = String.fromCharCode(65 + ascii);
                        ++ascii;
                        scaleVisible = true;
                    } else if (hashedLabels[labelHash]) { // show exsisting letter for the not `visible` scale
                        labelLetter = hashedLabels[labelHash];
                        scaleVisible = false;
                    } else { // empty letter for the not `visible` scales
                        labelLetter = "";
                        hashedLabels[labelHash] = labelLetter;
                        scaleVisible = false;
                    }
                    c.signal.labelLetter = labelLetter;
                    return scaleVisible;
                }) : null;
            }

            return curves || [];
        },
        set: function() {
            curves = null;
        }
    });

    /**
     * Maps curves' names into elements used for labelling scale
     * related to either curve.
     *
     * @property labels
     * @type {Array.<HtmlElement>}
     * @readonly
     */
    Object.defineProperty(this, "labels", {
        get: function() {
            if (!labels) {
                labels = this._generateLabels();
            }

            return labels;
        },
        set: function() {
            if (labels) {
                labels.forEach(function(label) {
                    label.parentNode.removeChild(label);
                });

                labels = null;
            }
        }
    });

    /**
     * Used to check if labels have been generated already
     *
     * @property labelsReady
     * @type {boolean}
     * @readonly
     */
    Object.defineProperty(this, "labelsReady", {
        get: function() {
            return labels !== null;
        }
    });

    /**
     * Maps curves' names into modules used for drawing scale of
     * related curve.
     *
     * @type {Array.<SHMI_TD.AbstractRenderer>}
     */
    Object.defineProperty(this, "scales", {
        get: function() {
            if (!scales) {
                scales = this._generateScales();
            }

            return scales || [];
        },
        set: function() {
            if (scales) {
                scales.forEach(function(scale) {
                    scale.onDelete();
                });

                scales = null;
            }
        }
    });

    /**
     * Refers to rendering element used to draw ruler next to
     * per-signal scales.
     *
     * @type {SHMI_TD.AbstractRenderer}
     */
    Object.defineProperty(this, "ruler", {
        get: function() {
            if (!ruler) {
                ruler = this._generateRuler();
            }

            return ruler;
        },
        set: function() {
            if (ruler) {
                ruler.onDelete();

                ruler = null;
            }
        }
    });

    /**
     * Provides dimensions to use on rendering scale components.
     *
     * @note On writing any cached dimension data is dropped.
     *
     * @property dimensions
     * @type {DimensionsSet}
     */
    Object.defineProperty(this, "dimensions", {
        get: function() {
            if (!dimensions) {
                dimensions = this._getDimensions();
            }

            return dimensions;
        },
        set: function() {
            dimensions = null;
        }
    });
};

shmi.visuals.controls.TrendDisplay.Scale.prototype = {
    uiType: "trend-display-scale",
    isContainer: false,
    getClassName: function() {
        "use strict";
        return "TrendDisplayScale";
    },

    _initializeConfiguration: function(config) {
        "use strict";

        shmi.def(config, "class-name", "trend-display-scale");
        shmi.def(config, "template", "default/trend-display/component/scale");

        return config;
    },

    onDiscover: function(event) {
        "use strict";

        this._super(this.TrendDisplayScale, "onDiscover", event);

        var self = this,
            trendDisplay = event.detail.trendDisplay,
            continuous = trendDisplay.getComponents("trend-display-continuous")[0];

        if (continuous) {
            trendDisplay.isPreparing = "discover-scale";

            /**
             * Refers to continuous component current scale component is
             * considered related to.
             *
             * @property continuous
             * @type {shmi.visuals.controls.TrendDisplay.Continuous}
             * @readonly
             */
            Object.defineProperty(this, "continuous", {
                value: continuous
            });

            continuous.isPrepared.
                then(function(continuousParam) {
                    /**
                     * Refers to element contained in current component to
                     * collect all scales' labels.
                     *
                     * @property labelsContainer
                     * @type {HtmlElement}
                     * @readonly
                     */
                    Object.defineProperty(self, "labelsContainer", {
                        value: shmi.getUiElement("labels-container", self.element)
                    });

                    /**
                     * Refers to element contained in current component
                     * providing visible canvas for rendering scales onto.
                     *
                     * @property container
                     * @type {HTMLCanvasElement}
                     * @readonly
                     */
                    Object.defineProperty(self, "container", {
                        value: shmi.getUiElement("scales-canvas-container", self.element)
                    });

                    /**
                     * Refers to element contained in current component
                     * providing visible canvas for rendering scales onto.
                     *
                     * @property canvas
                     * @type {HTMLCanvasElement}
                     * @readonly
                     */
                    Object.defineProperty(self, "canvas", {
                        value: shmi.getUiElement("scales-canvas", self.container || self.element)
                    });

                    /**
                     * Refers to stub element used to get dimensions of ruler
                     * defined in CSS.
                     *
                     * @type {HtmlElement}
                     */
                    Object.defineProperty(self, "rulerLabel", {
                        value: self._generateRulerLabel()
                    });

                    // register listeners for synchronizing Scale with Continuous
                    continuousParam.onShift.push([self._onShift, self]);
                    self.onBroadcast.push([50, self._onBroadcast, self]);

                    self._configure().
                        then(function() {
                            // ... marking component to be prepared
                            self.isPrepared.resolve(self);

                            trendDisplay.isPreparing = false;
                        });
                });
        }
    },

    _configure: function() {
        var self = this,
            oldCurveCount = self._countCurveLabels();

        // drop cached values of all properties
        this.viewport = null;
        this.renderer = null;
        this.curves = null;
        this.labels = null;
        this.scales = null;
        this.ruler = null;
        this.dimensions = null;

        // read properties to trigger re-initialization of related components
        this.scales; //eslint-disable-line no-unused-expressions
        this.ruler; //eslint-disable-line no-unused-expressions

        return this.broadcast("scale-size", {
            before: oldCurveCount || 0,
            now: this.curves.filter(function(curve) {
                return curve && curve.signal && curve.signal.scaleVisible;
            }).length || 0
        }).
            then(function() {
                self.broadcast("resize");
            });
    },

    /** Counts visible curve labels @return {int} */
    _countCurveLabels: function() {
        var self = this,
            signals = self.trendDisplay.config.trend.signals,
            unitsDone = signals.filter(function(signal) {
                return signal.scaleVisible;
            });

        return unitsDone.length;
    },

    onEnable: function() {
        this.isPrepared.then(function(scale) {
            scale.onResize();
        });
    },

    /**
     * Handles case of deleting component from runtime.
     *
     * This method is distributing event into all sub components explicitly
     * created by this component. In addition in removes all DOM elements
     * created by component.
     */
    onDelete: function() {
        this.scales.
            forEach(function(scale) {
                scale.onDelete();
            });

        this.labels.
            forEach(function(label) {
                label.parentNode.removeChild(label);
            });

        this.ruler = null;

        if (this.rulerLabel && this.rulerLabel.parentNode) {
            this.rulerLabel.parentNode.removeChild(this.rulerLabel);
        }

        this.renderer = null;
    },

    /**
     * Creates rendering manager for Scale component to manage all renderers
     * involved in drawing scale.
     *
     * @return {SHMI_TD.AbstractRenderingManager}
     * @private
     */
    _generateRenderingManager: function() {
        var renderer;

        switch (((this.canvas || {}).nodeName || "").toLowerCase()) {
        case "canvas":
            renderer = new shmi.visuals.controls.TrendDisplay.CanvasRenderingManager(this.container, this.canvas, this.viewport, shmi.visuals.controls.TrendDisplay.CanvasScale, this.trendDisplay.config, this.trendDisplay);
            break;

            /*
            case "svg" :
            renderer = new shmi.visuals.controls.TrendDisplay.SvgRenderingManager( this.container, this.canvas, this.viewport, shmi.visuals.controls.TrendDisplay.SvgScale, this.trendDisplay.config );
            break;
             */

        default:
            shmi.log("TrendDisplay: missing canvas element for rendering scales", 2);
            return null;
        }

        renderer.onRender.synchronous = true;

        return renderer;
    },

    /**
     * Creates elements in DOM of Scale component for showing every configured
     * curve's label and unit.
     *
     * @return {Array.<HtmlElement>} map of curves' names into created element
     * @private
     */
    _generateLabels: function() {
        if (!this.labelsContainer) {
            return [];
        }
        var self = this,
            container = this.labelsContainer,
            rulerLabel = this.rulerLabel,
            doc = container.ownerDocument,
            curves = this.curves.
                filter(function(curve) {
                    return curve && curve.signal && curve.signal.scaleVisible;
                }).
                map(function(curve, index) {
                    var l,
                        u,
                        c,
                        s = curve.signal,
                        labelLetter = s.labelLetter,
                        textElement = doc.createElement("div"),
                        unitText = (String(s.unit)) ? "[" + String(s.unit) + "]" : "";

                    l = doc.createElement("div");
                    l.className = "label";
                    textElement.className = "inner-text";
                    textElement.textContent = labelLetter;
                    l.appendChild(textElement);

                    u = doc.createElement("div");
                    u.className = "unit";
                    u.appendChild(doc.createTextNode(shmi.localize(unitText)));

                    c = doc.createElement("div");
                    c.className = "curve index-" + String(index + 1);
                    //
                    c.appendChild(l);
                    c.appendChild(u);

                    return container.insertBefore(c, rulerLabel);
                });

        shmi.fire('labels-ready', {
            name: self.trendDisplay.getName()
        }, self.trendDisplay);
        return curves;
    },

    /**
     * Creates element in DOM of Scale component to be styled in CSS for
     * defining dimensions of ruler rendered next to per-signal scales.
     *
     * @return {HtmlElement} reference on HTML element being styled in CSS
     * @private
     */
    _generateRulerLabel: function() {
        var container = this.labelsContainer,
            doc = container.ownerDocument,
            s;

        s = doc.createElement("div");
        s.className = "ruler";
        s.appendChild(doc.createTextNode("\u00a0"));

        return container.appendChild(s);
    },

    /**
     * Creates handler for rendering ruler next to per-signal scales.
     *
     * @return {?SHMI_TD.CanvasRuler} reference on instance rendering ruler of current scale
     * @private
     */
    _generateRuler: function() {
        if (!this.curves.length) {
            return null;
        }

        var renderer = this.renderer,
            vp = this.viewport,
            Class = renderer.elementClass,
            TD = shmi.visuals.controls.TrendDisplay,
            curves = this.curves,
            anySignal = curves[0].signal,
            ruler = null;

        switch (Class) {
        case TD.CanvasScale:
            if (vp.isSingleLane) { // only for old TrendDisplay show ruler
                ruler = new TD.CanvasRuler(renderer, curves, anySignal);
            }
            break;
            /*
             * TODO implement support for SVG canvas
            case TD.SvgScale :
            ruler = new TD.SVGRuler( renderer, curves, anySignal );
            break;
             */
        default:
        }

        if (ruler) {
            renderer.onRender.push([ruler.render, ruler]);
        }

        return ruler;
    },

    /**
     * Creates modules for drawing scales on canvas of renderer and registers
     * every module to be used by renderer further on.
     *
     * @return {Array.<shmi.visuals.controls.TrendDisplay.AbstractRenderer>} map of curves' names into scale drawing modules
     * @private
     */
    _generateScales: function() {
        var renderer = this.renderer,
            Class = renderer ? renderer.elementClass : null;
        if (!Class) {
            return [];
        }
        return this.curves.
            map(function(curve, index) {
                var scale = new Class(renderer, curve.signal);

                scale.index = index;

                renderer.onRender.push([scale.render, scale]);

                return scale;
            });
    },

    /**
     * Measures dimension facts on per-signal scales and ruler element to use on
     * drawing related scale elements.
     *
     * @return {DimensionsSet}
     * @private
     */
    _getDimensions: function() {
        var labels = this.labels,
            dimensions = {
                width: 0
            },
            ruler = this.rulerLabel;

        dimensions.signals = this.curves.
            filter(function(curve) {
                return curve && curve.signal && curve.signal.scaleVisible;
            }).
            map(function(curve, index) {
                var label = labels[index],
                    width = label.offsetWidth;

                dimensions.width += width;

                return {
                    x: label.offsetLeft,
                    width: width
                };
            });

        dimensions.ruler = {
            x: ruler.offsetLeft,
            width: ruler.offsetWidth
        };

        dimensions.width += dimensions.ruler.width;

        return dimensions;
    },

    onResize: function() {
        var canvas = this.container || this.canvas,
            renderer = this.renderer,
            viewport = this.viewport,
            width,
            height;

        if (renderer && viewport) {
            // assign something to dimensions so setter is updating cache
            this.dimensions = true;

            // get cumulative width of all labels
            width = this.dimensions.width;

            // derive height available in parent component
            height = canvas.offsetParent.offsetHeight;

            renderer.resize(width, height, 0, viewport.prefetch);
            renderer.update(this);
        }
    },

    /**
     * Handles shifting scale (moving vertically).
     *
     * This method is linked to related event distributor of component
     * Continuous to move scale in sync with dragging Continuous up/down.
     *
     * @param stage
     * @param amount
     * @private
     */
    _onShift: function(stage, amount) {
        this.container.style.top = String(-this.renderer.getCanvasSize().y + amount) + "px";
    },

    /**
     * Implements specific processing of certain broadcasts.
     *
     * @param {SHMI_TD.Broadcast} broadcast
     * @private
     */
    _onBroadcast: function(broadcast) {
        var self = this,
            td,
            continuous;

        switch (broadcast.type) {
        case "preparation-finished":
        case "render":
            if (!broadcast.detail.suppress) {
                // render scale component by providing component as "data"
                this.isPrepared.then(function(scale) {
                    scale.renderer.update(scale);
                });
            }
            break;

        case "resize":
            this.onResize();
            break;

        case "configuration-changed":
            td = this.trendDisplay;
            continuous = td.getComponents("trend-display-continuous")[0];

            if (continuous && continuous.isPrepared.isResolved()) {
                td.isPreparing = "reconfigure-scale";

                this._configure().
                    then(function() {
                        return self.broadcast("resize");
                    }).
                    then(function() {
                        td.isPreparing = false;
                    });
            }
            break;
        default:
        }
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.Scale, shmi.visuals.controls.TrendDisplay.Component, "TrendDisplayScale");

shmi.visuals.controls.TrendDisplay.Scale.forEachStep = function(ctx, size, cfg, max, fn) {
    var upper = ctx.y2Relative(size.y),
        lower = ctx.y2Relative(size.y + size.height),
        units = cfg.scale.availableSteps,
        majors = cfg.scale.availableMajorSteps,
        unit = null,
        ry,
        i = -1,
        j;

    units.forEach(function(stepping, index) {
        var count = (upper - lower) / stepping;

        if (count <= max + 1) {
            unit = stepping;
            i = index;
        }
    });

    if (unit) {
        lower = _accuracy(Math.floor(ctx.y2Relative(size.totalHeight) / unit) * unit);
        upper = ctx.y2Relative(0);

        if (lower < 0.0) {
            lower = 0.0;
        }

        if (upper > 1.0) {
            upper = 1.0;
        }

        for (ry = lower; ry <= upper; ry += unit) {
            ry = _accuracy(ry);

            for (j = 0; majors[j] > units[i]; j++) {
                if (_accuracy(Math.floor(ry / majors[j]) * majors[j]) === ry) {
                    break;
                }
            }

            if (fn(ctx.relative2Value(ry), ry, i, j) === false) {
                break;
            }
        }
    }

    function _accuracy(value) {
        return Math.round(value * 10000) / 10000;
    }
};

/**
 * Validates configuration regarding scale component.
 *
 * @param {Object.<string,*>} configuration qualified configuration object
 */
shmi.visuals.controls.TrendDisplay.onConfiguration.push(function _onConfigureScale(cfg, tdControl, Parsers) {
    "use strict";

    if (cfg) {
        var display,
            units,
            scale,
            a,
            b;

        display = cfg.display = Parsers.isObject(cfg.display);
        units = display.units = Parsers.isObject(display.units);
        scale = units.scale = Parsers.isObject(units.scale);

        if (!Array.isArray(scale.availableSteps)) {
            scale.availableSteps = [1.0, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01, 0.005, 0.002, 0.001];
        }

        scale.availableSteps = Parsers.parseFilterAndSort(scale.availableSteps, Parsers.relative, true);

        if (!Array.isArray(scale.availableMajorSteps)) {
            scale.availableMajorSteps = [1.0, 0.5, 0.1, 0.05, 0.01, 0.005, 0.001];
        }

        scale.availableMajorSteps = Parsers.parseFilterAndSort(scale.availableMajorSteps, Parsers.relative, true);

        a = parseInt(scale.min);
        if (isNaN(a) || a < 1) {
            a = 1;
        }
        b = parseInt(scale.max);
        if (isNaN(b) || b < 1) {
            b = 10;
        }

        scale.min = Math.min(a, b);
        scale.max = Math.max(a, b);
    }
});

/**
 * Implements specific behaviour of timeline component providing ruler for
 * reading time of curves drawn on continuous paper component.
 *
 * @param {HtmlElement} element DOM element detected to represent a component
 * @param {{}} configuration instantly available configuration
 * @constructor
 */
//eslint-disable-next-line func-name-matching
shmi.visuals.controls.TrendDisplay.Timeline = function TrendDisplayTimeline(element, configuration) {
    "use strict";

    shmi.visuals.controls.TrendDisplay.Timeline._super.call(this, element, configuration);
};

shmi.visuals.controls.TrendDisplay.Timeline.prototype = {
    uiType: "trend-display-timeline",
    isContainer: false,
    getClassName: function() {
        "use strict";
        return "TrendDisplayTimeline";
    },

    _initializeConfiguration: function(config) {
        "use strict";

        shmi.def(config, "class-name", "trend-display-timeline");
        shmi.def(config, "template", "default/trend-display/component/timeline");

        this._labels = {};

        return config;
    },

    onDiscover: function(event) {
        "use strict";

        this._super(this.TrendDisplayTimeline, "onDiscover", event);

        /**
         * Refers to HTML element to contain all timeline elements.
         *
         * @property container
         * @type {HtmlElement}
         * @readonly
         */
        Object.defineProperty(this, "container", {
            value: shmi.getUiElement("timestamps-container", this.element)
        });

        var self = this,
            trendDisplay = event.detail.trendDisplay,
            config = trendDisplay.config,
            continuous = trendDisplay.getComponents("trend-display-continuous")[0];

        if (continuous) {
            /**
             * Refers to Continuous component this Timeline component is related to.
             *
             * @property continuous
             * @type {shmi.visuals.controls.TrendDisplay.Continuous}
             * @readonly
             */
            Object.defineProperty(this, "continuous", {
                value: continuous
            });

            continuous.isPrepared.
                then(function(continuousParam) {
                    continuousParam.onSlide.push([self._onSlide, self]);
                    self.onBroadcast.push([50, self._onBroadcast, self]);

                    self.isPrepared.resolve(self);

                    if (!config.invalid) {
                        self._updateElements(continuousParam);
                    }
                });
        }
    },

    onDelete: function() {
        this.onDeleteLabels();

        this._super(this.TrendDisplayTimeline, "onDelete");
    },

    onDeleteLabels: function() {
        var labels = this._labels;

        Object.keys(labels).
            forEach(function(name) {
                var label = labels[name];

                label.parentNode.removeChild(label);
            });

        this._labels = {};
    },

    _onSlide: function(stage, amount) {
        if (!isNaN(this._offsetX)) {
            this.container.style.left = String(-this._offsetX + amount) + "px";
        }
    },

    _reconfigure: function(cfg) {
        var continuous = this.trendDisplay.getComponents("trend-display-continuous")[0];
        if (continuous && continuous.isPrepared.isResolved()) {
            this.onDeleteLabels();
            this._updateElements(continuous);
        }
    },

    _onBroadcast: function(broadcast) {
        "use strict";

        switch (broadcast.type) {
        case "preparation-finished":
        case "render":
            if (!broadcast.detail.suppress) {
                this._updateElements(this.continuous);
            }
            break;

        case "configuration-changed":
            var cfg = broadcast.detail.configuration;
            if (!cfg.invalid) {
                this._reconfigure(cfg);
            }
            break;
        default:
        }
    },

    _updateElements: function(continuous) {
        var cfg = this.trendDisplay.config,
            vp = continuous.viewport,
            dc = continuous.getDC(),
            manager = continuous.renderer,
            controls = manager.controls,
            size = manager.getCanvasSize(),
            tsContainer = this.container,
            doc = tsContainer.ownerDocument,
            TD = shmi.visuals.controls.TrendDisplay,
            timeline = shmi.getUiElement('trend-display-timeline'),
            fullX = size.totalWidth,
            min = vp.minTime,
            max = vp.rightRenderTime - min,
            unitCfg = cfg.display.units.timeLine,
            unit = TD.Timeline.detectVisibleUnit(unitCfg.available, vp.visibleTimeSpan, unitCfg.min, unitCfg.max),
            labels = this._labels,
            ts,
            x;

        // store initial offset for use on sliding timestamp container
        this._offsetX = size.x;

        if (unit > 0) {
            // reset location and size of timestamp container
            tsContainer.style.left = String(-size.x) + "px";
            tsContainer.style.width = String(fullX) + "px";

            // show timeline control buttons to navigate
            var position = (timeline === tsContainer.parentNode.parentNode && shmi.hasClass(timeline, 'top-timeline')) ? 'top' : 'bottom';
            controls.applyButtonNavigateTimeline('left', tsContainer.parentNode, position, 'after');
            controls.applyButtonNavigateTimeline('right', tsContainer.parentNode, position, 'after');

            // vertical raster timeline scaling
            this._generateTimelineScaling(continuous, tsContainer.parentNode, position);

            // put timestamp elements into timestamp container according to current viewport
            var lastTs = 0,
                temp = {},
                date = null,
                mTime,
                mLabel,
                label;

            for (ts = Math.floor((vp.leftRenderTime - min) / unit) * unit; ts < max; ts += unit) {
                label = labels[ts];

                var rTime = (cfg["rel-time"] === true) ? ts : min + ts,
                    currentDateTime = new Date(rTime * 1000),
                    utcTime = cfg["utc-time"] ? cfg["utc-time"] : false,
                    dateTime = new Date(utcTime ? currentDateTime.valueOf() + currentDateTime.getTimezoneOffset() * 60000 : currentDateTime);

                if (label) { // update related label
                    x = dc.time2X(min + ts, 0);
                    if (x !== parseFloat(label.style.left)) {
                        label.style.left = String(x) + "px";
                    }

                    labels[ts] = null;
                } else {
                    label = doc.createElement("label");
                    label.style.left = String(dc.time2X(min + ts, 0)) + "px";

                    label.appendChild(doc.createTextNode(shmi.requires("visuals.tools.date").formatDateTime(rTime, {
                        datestring: cfg['time-format'] || "$HH:$mm:$ss",
                        utc: utcTime
                    })));
                    tsContainer.appendChild(label);
                }

                // show date at midnight
                var isTopPosition = timeline === tsContainer.parentNode.parentNode && shmi.hasClass(timeline, 'top-timeline');
                if (isTopPosition) { // show only on top
                    if (dateTime.getHours() === 0 && dateTime.getMinutes() === 0 && dateTime.getSeconds() === 0) {
                        date = doc.createElement("span");
                        date.className = "date";
                        date.appendChild(doc.createTextNode(shmi.requires("visuals.tools.date").formatDateTime(rTime, {
                            datestring: cfg["date-format"],
                            utc: utcTime
                        })));
                        label.appendChild(date);
                    } else if (ts !== 0 && lastTs !== 0 && lastTs < Math.floor(ts / 86400)) { // missed midnight?
                        dateTime.setHours(0, 0, 0, 0);
                        mTime = parseInt(dateTime.getTime() / 1000) + 86400;
                        if (!temp[mTime]) {
                            mLabel = doc.createElement("label");
                            mLabel.style.left = String(dc.time2X(mTime, 0)) + "px";

                            // show time for specific zoom
                            /*var zoomX = this.trendDisplay.items.getByName( "zoomX" ).value;
                            if(zoomX) {
                            console.warn('zoom: '+zoomX);
                            mlabel.appendChild(doc.createTextNode(shmi.requires("visuals.tools.date").formatDateTime(mTime, {
                            datestring: cfg['time-format'],
                            utc: utcTime
                            })));
                            }*/
                            tsContainer.appendChild(mLabel);

                            date = doc.createElement("span");
                            date.className = "date";
                            date.appendChild(doc.createTextNode(shmi.requires("visuals.tools.date").formatDateTime(mTime, {
                                datestring: cfg["date-format"],
                                utc: utcTime
                            })));
                            mLabel.appendChild(date);
                            temp[mTime] = mLabel;
                        }
                    }
                }

                temp[ts] = label;
                lastTs = Math.floor(ts / 86400);

                this._labels = temp;
            }
        } else {
            this._labels = [];
        }

        // remove all element not in use anymore
        Object.keys(labels).
            forEach(function(n) {
                var l = labels[n];

                if (l) {
                    tsContainer.removeChild(l);
                }
            });
    },

    /**
     * synchronized timeline scaling raster to show on separate canvas
     *
     * @param manager
     * @param container
     * @param {string} timelinePosition 'top' | 'bottom'
     * @private
     */
    _generateTimelineScaling: function(continuous, container, timelinePosition) {
        var vp = continuous.viewport,
            manager = continuous.renderer,
            ctx = continuous.getDC(),
            canvas = shmi.getUiElement("continuous-canvas", container.parentNode.parentNode),
            name = 'vertical-raster-' + timelinePosition + '-canvas',
            timelineCanvas = container.querySelector('.' + name),
            startX = timelinePosition === 'top' ? container.parentNode.scrollHeight - 1 : 0,
            endX = timelinePosition === 'top' ? startX - 7 : startX + 7;

        // create canvas if not exist
        if (!timelineCanvas) {
            timelineCanvas = document.createElement("canvas");
            timelineCanvas.className = "canvas " + name;
            timelineCanvas.height = container.parentNode.scrollHeight;
            container.appendChild(timelineCanvas);
        } else {
            timelineCanvas.width = canvas.width;

            // drawing context
            var dc = timelineCanvas.getContext("2d");

            // @todo: set global var i.e. display.verticalTimelineScalingStyle
            dc.lineWidth = 1;
            dc.strokeStyle = "rgba(255,255,255,1)"; // white
            //dc.strokeStyle = "rgba(96, 129, 26,1)"; // green
            //dc.strokeStyle = "rgba(232,58,29,1)"; // red

            dc.beginPath();

            // render vertical scaling lines on ruler
            shmi.visuals.controls.TrendDisplay.CanvasBackground.getVerticalRasterTimestamps(manager.config.display.units, vp).
                forEach(function(ts) {
                    // detect timestamps to mark
                    var n = ctx.time2X(ts, 0);

                    // draw line
                    dc.moveTo(n, startX);
                    dc.lineTo(n, endX);
                });
            dc.stroke();
        }
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.Timeline, shmi.visuals.controls.TrendDisplay.Component, "TrendDisplayTimeline");

shmi.visuals.controls.TrendDisplay.Timeline.detectVisibleUnit = function(availableUnits, visibleTimeSpan, minVisible, maxVisible) {
    var found = availableUnits.
        map(function(unit) {
            var n = visibleTimeSpan / unit;
            return (n >= minVisible && n <= maxVisible) ? unit : 0;
        }).
        filter(function(n) {
            return n;
        }).
        sort(function(l, r) {
            return r - l;
        });

    if (found.length === 0) {
        var firstMatch = null;
        availableUnits.forEach(function(curUnit) {
            if (firstMatch !== null) return;
            var compVal = visibleTimeSpan / curUnit;
            if (compVal < 1) {
                firstMatch = curUnit;
            }
            if ((compVal >= 1) && (compVal <= maxVisible)) {
                firstMatch = curUnit;
            }
        });
        return firstMatch;
    }
    return found.length ? found[Math.floor(found.length / 2)] : availableUnits[0];
};

shmi.visuals.controls.TrendDisplay.Timeline.renderTime = function(timestamp, configuration) {
    if (configuration["rel-time"] !== true) {
        var datestring = configuration['time-format'] || "$YYYY-$MM-$DD $HH:$mm";
        return shmi.requires("visuals.tools.date").formatDateTime(timestamp, {
            datestring: datestring,
            utc: configuration["utc-time"]
        });
    }

    var remainder = timestamp,
        parts = {
            year: 365 * 86400,
            month: 30 * 86400,
            week: 7 * 86400,
            day: 86400,
            hour: 3600,
            minute: 60,
            second: 1
        };

    Object.keys(parts).
        forEach(function(name) {
            var unit = parts[name];

            if (remainder >= unit) {
                parts[name] = Math.floor(remainder / unit);
                remainder -= unit * parts[name];
            } else {
                parts[name] = "";
            }
        });

    var leading = true;

    return configuration["rel-time-format"] || "$Dd $HH:$MM:$SS".replace(/\$([YNWDHMS]{1,3})([^\s$]*)/g, function(marker, type, tailor) {
        var value;

        switch (type[0]) {
        case "Y":
            value = parts.year;
            break;
        case "N":
            value = parts.month;
            break;
        case "W":
            value = parts.week;
            break;
        case "D":
            value = parts.day;
            break;
        case "H":
            value = parts.hour;
            break;
        case "M":
            value = parts.minute;
            break;
        case "S":
            value = parts.second;
            break;
        default:
            return type + tailor;
        }

        if (leading && !value) {
            return "";
        }

        leading = false;

        value = String(value);
        while (value.length < type.length) {
            value = "0" + value;
        }

        return value + tailor;
    }).trim();
};

/**
 * Validates configuration regarding timeline component.
 *
 * @param {Object.<string,*>} configuration qualified configuration object
 */
shmi.visuals.controls.TrendDisplay.onConfiguration.push(function _onConfigureTimeLine(cfg, tdControl, Parsers) {
    "use strict";

    if (cfg) {
        var display,
            units,
            timeLine,
            a,
            b;

        display = cfg.display = Parsers.isObject(cfg.display);
        units = display.units = Parsers.isObject(display.units);
        timeLine = units.timeLine = Parsers.isObject(units.timeLine);

        if (!Array.isArray(timeLine.available)) {
            timeLine.available = [5, 10, 30, 60, 120, 300, 600, 900, 1800, 3600, 4 * 3600, 6 * 3600, 12 * 3600, 86400, 7 * 86400, 31 * 86400, 365 * 86400];
        }

        timeLine.available = Parsers.parseFilterAndSort(timeLine.available, Parsers.timeSpanUnit);

        a = parseInt(timeLine.min);
        if (isNaN(a) || a < 1) {
            a = 1;
        }
        b = parseInt(timeLine.max);
        if (isNaN(b) || b < 1) {
            b = 6;
        }

        timeLine.min = Math.min(a, b);
        timeLine.max = Math.max(a, b);
    }
});

/**
 * Manages curve drawn in context of a manager representing single signal of a
 * trend.
 *
 * @param {shmi.visuals.controls.TrendDisplay.AbstractRenderingManager} manager
 * @param {shmi.visuals.controls.TrendDisplay.Program} program
 * @param {QualifiedProgramValue} programValue
 * @param {shmi.visuals.controls.TrendDisplay.Style} waitStyle
 * @extends shmi.visuals.controls.TrendDisplay.CanvasCurve
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.CanvasProgramCurve = function CanvasProgramCurve(manager, program, programValue, waitStyle) {
    "use strict";

    shmi.visuals.controls.TrendDisplay.CanvasProgramCurve._super.call(this, manager, programValue.getTrend().signal);

    /**
     * Refers to program rendered value belongs to.
     *
     * @property program
     * @type {shmi.visuals.controls.TrendDisplay.Program}
     * @readonly
     */
    Object.defineProperty(this, "program", {
        value: program
    });

    /**
     * Refers to information on program value to be rendered.
     *
     * @property programValue
     * @type {QualifiedProgramValue}
     * @readonly
     */
    Object.defineProperty(this, "programValue", {
        value: programValue
    });

    /**
     * Refers to style to use on rendering wait state on current curve.
     *
     * @property waitStyle
     * @type {shmi.visuals.controls.TrendDisplay.Style}
     * @readonly
     */
    Object.defineProperty(this, "waitStyle", {
        value: waitStyle
    });
};

shmi.visuals.controls.TrendDisplay.CanvasProgramCurve.prototype = {

    _createPathElement: function(canvas) {
        var className = "path program " + (this.programValue.isCValue ? "cv-" : this.programValue.isDoValue ? "do-" : "sv-") + (this.signal.alias || this.signal.name),
            path = canvas.getElementsByClassName(className)[0];

        if (!path) {
            path = canvas.ownerDocument.createElement("path");
            path.setAttribute("class", className);

            path = canvas.appendChild(path);
        }

        return path;
    },

    /**
     * (Re-)renders curve on canvas.
     */
    render: function(manager, data, canvas) {
        "use strict";

        var pgm = this.program,
            self = this,
            waitStyle = this.waitStyle,
            waitSymbol = waitStyle.symbol,
            required;

        if (!pgm.steps.isResolved()) {
            return null;
        }

        required = [pgm.steps];

        if (waitSymbol && waitSymbol.isResolved()) {
            required.push(waitSymbol);
        }

        return shmi.visuals.controls.TrendDisplay.Promise.all(required).
            spread(function(steps, waitSymbolParam) {
                var ctx = self.drawingContext,
                    vp = manager.viewport,
                    dc = canvas.getContext("2d"),
                    startTS = vp.leftRenderTime,
                    endTS = vp.rightRenderTime,
                    item = self.programValue,
                    isCV = item.isCValue,
                    isDo = item.isDoValue,
                    setName = isCV ? "cvn" : isDo ? "ndo" : "svn",
                    vIndex = item.index,
                    symbol = item.style.symbol,
                    symbols = [],
                    prevStepValue = NaN,
                    prevStepTSE = NaN;

                var adapter = shmi.requires("visuals.tools.item-adapter").getActiveAdapter(self.signal.alias || self.signal.name);

                // don't render fully passed parts of program
                if (!pgm.preview && startTS < pgm.tStartReal) {
                    startTS = pgm.tStartReal;
                }

                var stepsArray = steps.asArray;

                function calcLoops() {
                    var loops = [];

                    if (!stepsArray[0].loop) return; //cancel when no loop information is provided -> live-preview is active

                    stepsArray.forEach(function(step, idx) {
                        if (step.loop.count > 0) {
                            loops.push({
                                start: step.loop.t,
                                end: idx - 1,
                                loops: step.loop.count
                            });
                        }
                    });
                    stepsArray.forEach(function(step, idx) {
                        step.loopInfo = {
                            loops: 0,
                            isEnd: false,
                            reps: 0
                        };
                        loops.forEach(function(loop, jdx) {
                            if ((idx >= loop.start) && (idx <= loop.end)) {
                                step.loopInfo.loops += 1;
                                if ((idx) === loop.end) {
                                    step.loopInfo.isEnd = true;
                                    step.loopInfo.reps = loop.loops;
                                }
                            }
                        });
                    });
                }
                calcLoops();

                item.style.applyOnCanvas(dc);

                dc.beginPath();

                steps.asArray.
                    some(function(step, stepIndex, stepsArrayParam) {
                        if (stepIndex >= (stepsArrayParam.length - 1)) {
                            return false;
                        }
                        var value = step[setName][vIndex],
                            tsl,
                            tss,
                            tse,
                            xs,
                            xe,
                            ys,
                            ye,
                            l,
                            r,
                            wait;

                        if (adapter) {
                            value = adapter.outFunction(value);
                        }

                        tss = step.tStartReal;
                        tse = step.tEndReal;

                        tsl = isNaN(prevStepTSE) ? tss : prevStepTSE;

                        if (tse < tsl) tse = tsl;

                        l = tsl > startTS ? tsl : startTS;
                        r = tse < endTS ? tse : endTS;

                        if (r >= l) {
                            // part of graph is (partially) visible at the moment

                            // get left point on canvas of current step
                            xs = ctx.time2X(tss, 0);

                            //CHANGED
                            ys = ctx.value2Y(value, 0);

                            /*
                    if(stepIndex === 0) {
                    ys = ctx.value2Y( value, 0 );
                    } else {
                    ys = ctx.value2Y( prevStepValue, 0 );
                    }
                     */

                            // get right point on canvas of previous step
                            xe = isNaN(prevStepTSE) ? xs : ctx.time2X(prevStepTSE, 0);

                            wait = ((step.waitForCVIndex.icv !== 0) && (step.waitForCVIndex.icv === (vIndex + 1)) && isCV);
                            if (wait) {
                                // render styled wait symbol if wait state
                                if (step.tStart >= pgm.time) {
                                    if (waitSymbolParam) {
                                        dc.drawImage(waitSymbolParam, (xs) - 30, ys - shmi.visuals.controls.TrendDisplay.iconDimensions.height / 2);
                                    }
                                }
                            }

                            // get right point on canvas of current step
                            var nextValue = stepsArrayParam[stepIndex + 1][setName][vIndex];
                            if (adapter) {
                                nextValue = adapter.outFunction(nextValue);
                            }

                            if (isDo && (step.tStart >= pgm.time)) {
                                if (prevStepValue !== value) {
                                    var lastXe = ctx.time2X(prevStepTSE, 0);
                                    var lastYe = ctx.value2Y(prevStepValue, 0);
                                    dc.moveTo(lastXe, lastYe);
                                    dc.lineTo(xs, ys);
                                    if (symbol) {
                                        symbols.push({
                                            x: xs,
                                            y: ys
                                        });
                                    }
                                }
                            }

                            xe = ctx.time2X(tse, 0);
                            ye = isDo ? ctx.value2Y(value, 0) : ctx.value2Y(nextValue, 0);

                            if (step.tStart >= pgm.time) {
                                // render line of step
                                dc.moveTo(xs, ys);
                                dc.lineTo(xe, ye);

                                if (symbol) {
                                    if (!symbols.length || wait) {
                                        symbols.push({
                                            x: xs,
                                            y: ys
                                        });
                                    }

                                    symbols.push({
                                        x: xe,
                                        y: ye
                                    });
                                }
                            }
                        }

                        prevStepValue = value;
                        prevStepTSE = tse;

                        return (tss >= endTS);
                    });

                dc.stroke();

                //render loop gfx - START
                if (stepsArray[0].loop) {
                    dc.save();
                    dc.strokeStyle = "#9ba302";
                    dc.fillStyle = "#9ba302";
                    dc.lineWidth = 3;
                    prevStepValue = NaN;
                    prevStepTSE = NaN;

                    steps.asArray.
                        some(function(step, stepIndex) {
                            if (stepIndex >= (stepsArray.length - 1)) {
                                return false;
                            }

                            var value = step[setName][vIndex],
                                tsl,
                                tss,
                                tse,
                                xs,
                                xe,
                                l,
                                r;

                            if (adapter) {
                                value = adapter.outFunction(value);
                            }

                            tss = step.tStartReal;
                            tse = step.tEndReal;

                            tsl = isNaN(prevStepTSE) ? tss : prevStepTSE;

                            if (tse < tsl) tse = tsl;

                            l = tsl > startTS ? tsl : startTS;
                            r = tse < endTS ? tse : endTS;

                            if (r >= l) {
                                // part of graph is (partially) visible at the moment
                                // get left point on canvas of current step
                                xs = ctx.time2X(tss, 0);
                                // get right point on canvas of previous step
                                xe = isNaN(prevStepTSE) ? xs : ctx.time2X(prevStepTSE, 0);
                                // get right point on canvas of current step
                                xe = ctx.time2X(tse, 0);
                                if (step.tStart >= pgm.time) {
                                    //render loop gfx
                                    if (step.loopInfo) {
                                        var offset = 30;
                                        //var offset = 34;
                                        var fIdx = 0;
                                        var addWidth = 0;
                                        while (fIdx < step.loopInfo.loops) {
                                            dc.beginPath();
                                            dc.moveTo(xs, ctx.manager._offsetY + 6 + (fIdx * offset));
                                            dc.lineTo(xe, ctx.manager._offsetY + 6 + (fIdx * offset));
                                            dc.stroke();
                                            fIdx += 1;
                                        }
                                        if (step.loopInfo.isEnd) {
                                            if (step.loopInfo.reps >= 10) {
                                                addWidth += 8;
                                            }
                                            if (step.loopInfo.reps >= 100) {
                                                addWidth += 8;
                                            }
                                            fIdx -= 1;
                                            dc.fillRect(xe - (25 + addWidth), ctx.manager._offsetY + (fIdx * offset) + 7, 25 + addWidth, 25);
                                            dc.font = "14px sans-serif";
                                            dc.save();
                                            dc.fillStyle = "#FFFFFF";
                                            dc.fillText(step.loopInfo.reps, xe - (17 + addWidth), ctx.manager._offsetY + (fIdx * offset) + 23);
                                            dc.restore();
                                        }
                                    }
                                }
                            }
                            prevStepValue = value;
                            prevStepTSE = tse;

                            return (tss >= endTS);
                        });

                    dc.restore();
                }
                //render loop gfx - END

                if (symbol) {
                    symbol.then(function(img) {
                        symbols.forEach(function(pos) {
                            if (isFinite(pos.y)) {
                                dc.drawImage(img, pos.x - shmi.visuals.controls.TrendDisplay.iconDimensions.width / 2, pos.y - shmi.visuals.controls.TrendDisplay.iconDimensions.height / 2);
                            }
                        });
                    });
                }
            });
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.CanvasProgramCurve, shmi.visuals.controls.TrendDisplay.CanvasCurve, "CanvasProgramCurve");

//
// SVN:      $HeadURL:$
// Author:   $LastChangedBy:$
// Date:     $LastChangedDate:$
// Revision: $LastChangedRevision:$
//
// Created by User Interface Design GmbH (UID)
//eslint-disable-next-line
shmi.visuals.controls.TrendDisplay.ProgramManager = function TrendDisplayProgramManager(element, configuration) {
    "use strict";

    shmi.visuals.controls.TrendDisplay.ProgramManager._super.call(this, element, configuration);

    var viewport = null;
    var trends = null;
    var timeline = null;
    var program = null;
    var curves = null;
    var disabled = null;

    /**
     * Indicates whether program manager is disabled or not.
     *
     * @property disabled
     * @type {boolean}
     */
    Object.defineProperty(this, "disabled", {
        get: function() {
            return !!disabled;
        },
        set: function(state) {
            state = !!state;
            if (state !== disabled) {
                if (state) {
                    shmi.addClass(element, "disabled");
                } else {
                    shmi.removeClass(element, "disabled");
                }

                disabled = state;
            }
        }
    });

    /**
     * Refers to viewport instance managed by associated
     * Continuous component.
     *
     * @property viewport
     * @type {shmi.visuals.controls.TrendDisplay.Viewport}
     */
    Object.defineProperty(this, "viewport", {
        get: function() {
            if (!viewport && this.continuous) {
                viewport = this.continuous.viewport;
            }

            return viewport;
        },
        set: function() {
            viewport = null;
        }
    });

    /**
     * Lists all curves incl. description of related signal and drawing context.
     *
     * @property trends
     * @type {Array.<SHMI_TD.AbstractRenderer>}
     * @readonly
     */
    Object.defineProperty(this, "trends", {
        get: function() {
            if (!trends && this.continuous) {
                trends = this.continuous.curves;
            }

            return trends;
        },
        set: function() {
            trends = null;
        }
    });

    /**
     * Refers to renderer drawing time line of program.
     *
     * @property timeline
     * @type {shmi.visuals.controls.TrendDisplay.ProgramTimeLine}
     */
    Object.defineProperty(this, "timeline", {
        get: function() {
            if (timeline) timeline.onDelete();
            timeline = null;
            timeline = this._generateTimeLine();
            if (!timeline) {
                //timeline = this._generateTimeLine();
            }

            return timeline;
        },
        set: function() {
            if (timeline) {
                timeline.onDelete();
            }

            timeline = null;
        }
    });

    /**
     * Refers to program displayed in current trend display control.
     *
     * @property program
     * @type {shmi.visuals.controls.TrendDisplay.Program}
     */
    Object.defineProperty(this, "program", {
        get: function() {
            if (!program && !disabled) {
                program = this._generateProgram();
            }

            return program;
        },
        set: function() {
            if (program) {
                program.onDelete();
            }

            program = null;
        }
    });

    /**
     * Lists renderer engines drawing curves of program.
     *
     * @property curves
     * @type {Array}
     * @readonly
     */
    Object.defineProperty(this, "curves", {
        get: function() {
            if (!curves) {
                curves = this._generateProgramValues();
            }

            return curves;
        },
        set: function() {
            if (curves) {
                var renderer = (this.continuous || {}).renderer;

                curves.forEach(function(curve) {
                    if (renderer) {
                        renderer.onRender.removeListener([curve.render, curve]);
                    }

                    curve.onDelete();
                });
            }

            curves = null;
        }
    });
};

shmi.visuals.controls.TrendDisplay.ProgramManager.prototype = {
    uiType: "trend-display-program",
    isContainer: false,
    getClassName: function() {
        "use strict";
        return "TrendDisplayProgramManager";
    },

    _initializeConfiguration: function(config) {
        "use strict";

        shmi.def(config, "class-name", "trend-display-program");
        shmi.def(config, "template", "default/trend-display/component/program");

        return config;
    },

    onDiscover: function(event) {
        "use strict";

        this._super(this.TrendDisplayProgramManager, "onDiscover", event);

        var self = this,
            trendDisplay = event.detail.trendDisplay,
            continuous = trendDisplay.getComponents("trend-display-continuous")[0],
            waitStyle = null,
            inactiveStyle = {
                "color": "#C8C8C8",
                "width": 1,
                "label": "label"
            };

        if (continuous) {
            // semaphore has been set on processing configuration before
            //trendDisplay.isPreparing = "discover-program-manager";

            /**
             * Refers to related Continuous component.
             *
             * @property continuous
             * @type {shmi.visuals.controls.TrendDisplay.Continuous}
             */
            Object.defineProperty(self, "continuous", {
                value: continuous
            });

            /**
             * Refers to element contained in current component
             * providing visible canvas for rendering programs onto.
             *
             * @property container
             * @type {HTMLCanvasElement}
             * @readonly
             */
            Object.defineProperty(self, "container", {
                value: shmi.getUiElement("program-timeline-container", self.element)
            });

            /**
             * Refers to element contained in current component
             * providing visible canvas for rendering programs onto.
             *
             * @property canvas
             * @type {HTMLCanvasElement}
             * @readonly
             */
            Object.defineProperty(self, "canvas", {
                value: shmi.getUiElement("program-timeline", self.container || self.element)
            });

            /**
             * Refers to element contained in current component providing
             * style for rendering wait states.
             *
             * @property waitStub
             * @type {HtmlElement}
             * @readonly
             */
            Object.defineProperty(self, "waitStub", {
                value: shmi.getUiElement("program-wait", self.element)
            });

            /**
             * Refers to style to use on rendering wait states.
             *
             * @property waitStyle
             * @type {shmi.visuals.controls.TrendDisplay.Style}
             */
            Object.defineProperty(self, "waitStyle", {
                get: function() {
                    return waitStyle;
                },
                set: function(config) {
                    if (config) {
                        waitStyle = config.styleManagers.waitStyle;
                        if (waitStyle) waitStyle.symbol = "pics/system/icon_196.png";
                    }
                }
            });

            /**
             * Refers to style for inactive multi lane signal
             *
             * @property inactiveStyle
             * @type {shmi.visuals.controls.TrendDisplay.Style}
             */
            Object.defineProperty(self, "inactiveStyle", {
                get: function() {
                    return inactiveStyle;
                },
                set: function(config) {
                    if (config) {
                        inactiveStyle = config.styleManagers.inactiveStyle;
                    }
                }
            });

            continuous.isPrepared.
                then(function(continuousParam) {
                    // register listeners for synchronizing ProgramManager with Continuous
                    continuousParam.onSlide.push([self._onSlide, self]);
                    continuousParam.onState.push([self._onContinuousState, self]);
                    self.onBroadcast.push([self._onBroadcast, self]);

                    return self._configure(trendDisplay.config).
                        then(function() {
                            self.isPrepared.resolve(self);

                            self.trendDisplay.isPreparing = false;
                        });
                });
        }
    },

    onDelete: function() {
        var continuous = this.continuous;

        if (continuous && continuous.onSlide) {
            continuous.onSlide.removeListener([this._onSlide, this]);
        }
        if (continuous && continuous.onState) {
            continuous.onState.removeListener([this._onContinuousState, this]);
        }

        if (this.onBroadcast && typeof this.onBroadcast.removeListener === "function") {
            this.onBroadcast.removeListener([this._onBroadcast, this]);
        }
    },

    _configure: function(config) {
        var self = this;

        if (this.isPrepared.isResolved() && config.invalid) {
            // rejecting configuration update using invalid configuration
            return shmi.visuals.controls.TrendDisplay.Promise.resolve();
        }

        this.disabled = config.invalid || !config.program;

        // drop all previously attached elements
        this.viewport = null;
        this.trends = null;
        this.timeline = null;
        this.program = null;
        this.curves = null;
        this.waitStyle = null;
        this.controls = null;

        if (this.disabled) {
            return shmi.visuals.controls.TrendDisplay.Promise.resolve();
        }

        this.waitStyle = config;

        // trigger attaching new elements
        this.timeline; //eslint-disable-line no-unused-expressions
        this.curves; //eslint-disable-line no-unused-expressions
        this.controls; //eslint-disable-line no-unused-expressions

        return this.program.steps.
            then(function() {
                // trigger resize on having loaded program @todo required?
                return self.onResize();
            });
    },

    _generateTimeLine: function() {
        return new shmi.visuals.controls.TrendDisplay.ProgramTimeLine(this.container, this.continuous, this.program);
    },

    /**
     * Creates program instance to be managed.
     *
     * @return {shmi.visuals.controls.TrendDisplay.Program} configured program
     * @private
     */
    _generateProgram: function() {
        var self = this,
            program = new shmi.visuals.controls.TrendDisplay.Program(this, this.trendDisplay.config, this.continuous);

        program.updateMaxTime(self.continuous.viewport);

        return program;
    },

    /**
     * Creates rendering engines for drawing values on canvas of Continuous
     * component.
     *
     * @return {?Array.<shmi.visuals.controls.TrendDisplay.CanvasProgramCurve>} list of curves drawing modules
     * @private
     */
    _generateProgramValues: function() {
        var self = this,
            renderer = (this.continuous || {}).renderer,
            RequiredClass = renderer.elementClass,
            DesiredClass = shmi.visuals.controls.TrendDisplay.CanvasProgramCurve;

        if (renderer) {
            if (Object.create(DesiredClass.prototype) instanceof RequiredClass) {
                var program = this.program;

                return [].concat(
                    program.cvs.map(_create),
                    program.svs.map(_create),
                    program.dos.map(_create)).
                    map(function(curve) {
                        renderer.onRender.push([curve.render, curve]);

                        return curve;
                    });
            } else {
                shmi.log("[ProgramManager]: curve renderers not suitable for selected canvas of Continuous");
            }
        }

        return null;

        function _create(item) {
            return new DesiredClass(renderer, program, item, self.waitStyle);
        }
    },

    /**
     * Processes notification on having resized trend display.
     */
    onResize: function() {
        var timeline = this.timeline;
        if (timeline) {
            timeline.onResize();
        }
    },

    /**
     * Updates program manager component on sliding Continuous.
     *
     * @returns {*}
     * @private
     */
    _onSlide: function() {
        if (!this.disabled) {
            var timeline = this.timeline;
            if (timeline) {
                return timeline.onSlide.apply(timeline, arguments);
            }
        }
        return null;
    },

    /**
     * Handles state messages sent by Continuous component..
     *
     * @param {shmi.visuals.controls.TrendDisplay.Continuous} sender sending Continuous component
     * @param {{mode:string}} state
     * @returns {boolean} false for stopping message dispatching and preventing default
     * @private
     */
    _onContinuousState: function(sender, state) {
        var viewport = state.viewport;

        switch (state.mode) {
        case "live":
            // live-mode was (re-)enabled
            if (this.disabled) {
                return true;
            }

            viewport.leftTime = viewport.maxSampleTime - viewport.visibleTimeSpan / 2;

            // prevent default by stopping message dispatching
            return false;

        case "handleTime":
            // initial handle time has to be selected
            if (this.disabled) {
                return true;
            }

            viewport.handleTime = viewport.maxSampleTime;

            // prevent default by stopping message dispatching
            return false;
        default:
            return true;
        }
    },

    /**
     * Handles broadcasts of current trend display in favour of ProgramManager
     * component attached to it.
     *
     * @param {Broadcast} broadcast
     * @private
     */
    _onBroadcast: function(broadcast) {
        var td,
            cfg;

        switch (broadcast.type) {
        case "preparation-finished":
        case "render":
            if (!broadcast.detail.suppress) {
                if (!this.disabled) {
                    this.timeline.render();
                }
            }
            break;

        case "resize":
            if (!this.disabled) {
                this.onResize();
            }
            break;

        case "configuration-changed":
            td = this.trendDisplay;
            cfg = broadcast.detail.configuration;
            td.isPreparing = "reconfigure-program-manager";
            this._configure(cfg).
                then(function() {
                    td.isPreparing = false;
                    if (cfg.program) {
                        // code validating configuration has retained
                        // semaphore (usually to be released by
                        // onDiscovery() which isn't involved this time)
                        // -> release semaphore once more to fix this
                        td.isPreparing = false;
                    }
                });

            break;
        default:
        }
    }
};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.ProgramManager, shmi.visuals.controls.TrendDisplay.Component, "TrendDisplayProgramManager");

/**
 * Validates configuration regarding program component.
 *
 * @param {Object.<string,*>} configuration qualified configuration object
 */
shmi.visuals.controls.TrendDisplay.onConfiguration.push(function _onConfigureProgramManager(configuration, trendDisplay) {
    "use strict";
});

/**
 * Provides data of a program.
 *
 * @constructor
 */

shmi.visuals.controls.TrendDisplay.Program = function Program(component, config, continuous) {
    "use strict";

    /**
     * @typedef {{index:int,trendIndex:int,valueIndex:int,style:shmi.visuals.controls.TrendDisplay.Style,getTrend:function():shmi.visuals.controls.TrendDisplay.AbstractRenderer,isCValue:boolean}} QualifiedProgramValue
     */

    var code = null;
    var steps = null;

    var timerItem = null;

    var waiting = null;

    var cvs = [];
    var svs = [];
    var dos = [];

    this._compileItems(config, continuous, cvs, svs, dos);

    /** @typedef {{cvn:number[],svn:number[],label:string,tStart:number,length:number,waitForCVIndex:int,tStartReal:number=}} ProgramCodeStep */
    /** @typedef {{len:int,n:int,step:Array<ProgramCodeStep>}} ProgramCode */

    /**
     * Refers to ProgramManager component this program is used by.
     *
     * @property manager
     * @type {shmi.visuals.controls.TrendDisplay.ProgramManager}
     * @readonly
     */
    Object.defineProperty(this, "manager", {
        value: component
    });

    /**
     * Refers to Continuous component this program's manager is working on.
     *
     * @property continuous
     * @type {shmi.visuals.controls.TrendDisplay.Continuous}
     * @readonly
     */
    Object.defineProperty(this, "continuous", {
        value: continuous
    });

    /**
     * Promises data of selected program.
     *
     * @property code
     * @type {shmi.visuals.controls.TrendDisplay.Promise<ProgramCode>}
     */
    Object.defineProperty(this, "code", {
        get: function() {
            if (!code) {
                code = this._getOnConfiguration(config);
            }

            return code;
        },
        set: function() {
            code = null;
        }
    });

    /**
     * Delivers slot index of program as given in configuration.
     *
     * @property index
     * @type {int}
     * @readonly
     */
    Object.defineProperty(this, "index", {
        value: config.program.index
    });

    /**
     * Indicates whether program is for preview or running.
     *
     * @property preview
     * @type {boolean}
     * @readonly
     */
    Object.defineProperty(this, "preview", {
        value: config.program.preview
    });

    /**
     * Delivers timestamp tStart of program in seconds since Unix epoch
     * according to local program configuration.
     *
     * @property tStart
     * @type {number}
     * @readonly
     * TODO required?
     */
    Object.defineProperty(this, "tStart", {
        value: config.program.tStart
    });

    /**
     * Delivers program time that was current on creating current program.
     *
     * @property tStartProgram
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "tStartProgram", {
        value: this.time
    });

    /**
     * Delivers real time that was current on taking initial program time
     * available in tStartProgram.
     *
     * @property tStartReal
     * @type {number}
     * @readonly
     */
    Object.defineProperty(this, "tStartReal", {
        value: shmi.getServerTime()
    });

    /**
     * Delivers current program time.
     *
     * @note On assigning any value timer object is released.
     *
     * @property time
     * @type {number}
     */
    Object.defineProperty(this, "time", {
        get: function() {
            if (this.preview) {
                return 0;
            }

            if (!timerItem) {
                timerItem = this._bindProgramTimer(config);
            }

            return timerItem.value;
        },
        set: function() {
            if (timerItem) {
                this._unbindProgramTimer(timerItem);
                timerItem = null;
            }
        }
    });

    /**
     * Delivers program's qualified set of CVs.
     *
     * Qualification regards integration of addressed styles and related trend.
     *
     * @property cvs
     * @type {Array.<QualifiedProgramValue>}
     * @readonly
     */
    Object.defineProperty(this, "cvs", {
        value: cvs
    });

    /**
     * Delivers program's qualified set of SVs.
     *
     * Qualification regards integration of addressed styles and related trend.
     *
     * @property svs
     * @type {Array.<QualifiedProgramValue>}
     * @readonly
     */
    Object.defineProperty(this, "svs", {
        value: svs
    });

    /**
     * Delivers program's qualified set of DOs.
     *
     * Qualification regards integration of addressed styles and related trend.
     *
     * @property dos
     * @type {Array.<QualifiedProgramValue>}
     * @readonly
     */
    Object.defineProperty(this, "dos", {
        value: dos
    });

    /**
     * Indicates whether program is waiting currently or not.
     *
     * @property waiting
     * @type {boolean}
     */
    Object.defineProperty(this, "waiting", {
        get: function() {
            return !!waiting;
        },
        set: function(state) {
            if (Boolean(state) !== Boolean(waiting)) {
                if (state) {
                    var self = this;
                    if (self.preview === true) {
                        waiting = 0;
                    } else {
                        waiting = window.setInterval(function() {
                            self._whileWaiting();
                        }, 1100);
                    }
                } else {
                    window.clearInterval(waiting);
                    waiting = null;
                }
            }
        }
    });

    /**
     * Promises steps of loaded program.
     *
     * @note Steps might be qualified by injecting references to real time for
     *       start and end of either step in real time. This additional data is
     *       optional and might be considered matching program time if omitted.
     *
     * @property steps
     * @type {Promise<ProgramSteps>}
     */
    Object.defineProperty(this, "steps", {
        get: function() {
            if (!steps) {
                steps = this.code.
                    then(function(codeParam) {
                        return codeParam.step;
                    });
            }

            return steps;
        }
    });
};

shmi.visuals.controls.TrendDisplay.Program.prototype = {

    /**
     * Processes program configuration into separate qualified list of CVs, SVs and
     * DOs.
     *
     * @param config
     * @param continuous
     * @param {Array.<QualifiedProgramValue>} cvs
     * @param {Array.<QualifiedProgramValue>} svs
     * @param {Array.<QualifiedProgramValue>} dos
     * @private
     */
    _compileItems: function(config, continuous, cvs, svs, dos) {
        config.program.items.forEach(function(item) {
            var isCValue = (item.valueType === "CV"),
                isDoValue = (item.valueType === "DO"),
                list = isCValue ? cvs : isDoValue ? dos : svs;

            list.push({
                alias: item.alias,
                name: item.name,
                index: list.length,
                trendIndex: item.trendIndex,
                valueIndex: item.valueIndex,
                styleIndex: item.styleIndex,
                style: config.styleManagers[item.styleIndex] || null,
                getTrend: (function(i) {
                    return function() {
                        return continuous.curves[i];
                    };
                })(item.trendIndex),
                isCValue: isCValue,
                isDoValue: isDoValue
            });
        });
    },

    _normalizeRealTime: function(realTime) {
        var refRealTime = this.tStartReal;

        if (isNaN(realTime)) {
            realTime = shmi.getServerTime();
        }

        return refRealTime + Math.round(realTime - refRealTime);
    },

    /**
     * Promises program selected in provided configuration.
     *
     * @param cfg
     * @return {Promise.<ProgramCode>} configured program
     */
    _getOnConfiguration: function(cfg) {
        var pgm = this,
            c = this.cvs.map(_extractIndex),
            s = this.svs.map(_extractIndex),
            d = this.dos.map(_extractIndex);

        return new shmi.visuals.controls.TrendDisplay.Promise(function(done, error) {
            shmi.visuals.controls.TrendDisplay.Program.api.
                getProgram(cfg.program.index, c, s, d, function(err, code) {
                    if (err) {
                        error("error #" + err + " on fetching program #" + cfg.program.index);
                    } else {
                        var tEnd = code.len,
                            prev = null;

                        code.step = new ProgramSteps(code.step.
                            reverse().
                            map(function(step) {
                                step.length = tEnd - step.t;
                                tEnd = step.t;
                                return step;
                            }).
                            reverse().
                            map(function(step) {
                                return (prev = new ProgramStep(
                                    step.cvn, step.svn, step.ndo, step["#"], step.t,
                                    step.length, step.w, prev, step.loop));
                            }));

                        pgm._updateMaxTime(pgm.continuous.viewport, code.step);

                        if (pgm.preview) {
                            pgm.waiting = true;
                        }

                        done(code);
                    }
                });
        });

        function _extractIndex(i) {
            return i.valueIndex;
        }

        function ProgramStep(cvn, svn, ndo, label, tStart, tLength, waitForCVIndex, previousStep, loop) {
            var actualTStartReal = null;
            /** @property {ProgramStep} next */
            this.next = null;
            this.offsetTime = 0;
            this.waitTime = 0;
            if (previousStep) previousStep.next = this;
            Object.defineProperties(this, {
                /** @property {number[]} cvn */
                cvn: {
                    value: cvn
                },
                /** @property {number[]} svn */
                svn: {
                    value: svn
                },
                /** @property {boolean[]} ndo */
                ndo: {
                    value: ndo
                },
                /** @property {string} label */
                label: {
                    value: label
                },
                /** @property {number} tStart */
                tStart: {
                    value: tStart
                },
                /** @property {number} tStartReal */
                tStartReal: {
                    get: function() {
                        var doneTime,
                            setTime,
                            timeDiff,
                            remainTime,
                            completeTime;

                        if (actualTStartReal !== null) {
                            // got explicitly provided real time before
                            // -> provide that
                            return actualTStartReal;
                        }

                        if (previousStep) {
                            if (previousStep.hasTStartReal && this.tStart > pgm.time && pgm.waiting) {
                                // this is current step and it might be waiting
                                // -> don't derive its real time of start from
                                //    previous step but consider current real
                                //    time to be up-to-date start time of step

                                doneTime = (pgm.time - previousStep.tStart);
                                setTime = previousStep.tStartReal + doneTime;
                                timeDiff = shmi.getServerTime() - setTime;
                                if (timeDiff < 0) timeDiff = 0;
                                remainTime = previousStep.length - doneTime;
                                completeTime = shmi.getServerTime() + remainTime;
                                previousStep.offsetTime = timeDiff;

                                return pgm._normalizeRealTime(completeTime);
                            } else if (previousStep.hasTStartReal && this.tStart === pgm.time && pgm.waiting) {
                                doneTime = (pgm.time - previousStep.tStart);
                                setTime = previousStep.tStartReal + doneTime;
                                timeDiff = shmi.getServerTime() - setTime;
                                if (timeDiff < 0) timeDiff = 0;
                                remainTime = previousStep.length - doneTime;
                                completeTime = shmi.getServerTime() + remainTime;
                                this.waitTime = timeDiff;

                                return pgm._normalizeRealTime(completeTime);
                            } else if (previousStep.hasTStartReal && this.tStart >= pgm.time) {
                                doneTime = (pgm.time - previousStep.tStart);
                                setTime = previousStep.tStartReal + doneTime;
                                timeDiff = shmi.getServerTime() - setTime;
                                if (timeDiff < 0) timeDiff = 0;
                                remainTime = previousStep.length - doneTime;
                                completeTime = shmi.getServerTime() + remainTime;
                                previousStep.offsetTime = timeDiff;

                                return pgm._normalizeRealTime(completeTime);
                            }

                            // isn't first step of program
                            // -> deliver end of previous step as start of current one
                            //return previousStep.tStartReal + previousStep.length + previousStep.offsetTime + this.offsetTime;
                            return previousStep.tEndReal + this.waitTime;
                        }

                        if (pgm.preview) {
                            return pgm.tStartReal;
                        }

                        // it's first step in program
                        // -> calculate back assumed start time of current step
                        //    using program time as reference taken on
                        //    initializing program manager
                        var pgmSP = pgm.tStartProgram;
                        if (pgmSP === undefined) {
                            return shmi.getServerTime();
                        }
                        return pgm._normalizeRealTime(shmi.getServerTime() - (this.tStart - pgmSP));
                    },
                    set: function(timestamp) {
                        if ((actualTStartReal === null) || (timestamp === null)) {
                            actualTStartReal = timestamp;
                        }
                    }
                },
                /** @property {number} tEndReal */
                tEndReal: {
                    get: function() {
                        return this.tStartReal + this.length + this.offsetTime;
                    }
                },
                /** @property {boolean} hasTStartReal */
                hasTStartReal: {
                    get: function() {
                        return actualTStartReal !== null;
                    }
                },
                /** @property {ProgramStep} previous */
                previous: {
                    value: previousStep || null
                },
                /** @property {int} length */
                length: {
                    value: tLength
                },
                /** @property {int} waitForCVIndex */
                waitForCVIndex: {
                    value: waitForCVIndex
                },
                /** @property {function():number} waitingTime */
                waitingTime: {
                    value: function() {
                        if (!previousStep) {
                            return 0;
                        }

                        var gap = this.tStartReal - previousStep.tEndReal;
                        return gap < 0 ? 0 : gap;
                    }
                },
                loop: {
                    value: loop || null
                }
            });
        }

        /**
         * @param {Array<ProgramStep>} steps
         * @constructor
         */
        function ProgramSteps(steps) {
            var self = this,
                current = -1,
                recentProgramTime = null;

            Object.defineProperties(this, {
                asArray: {
                    value: steps
                },
                current: {
                    value: function(currentProgramTime) {
                        var index = self.currentIndex(currentProgramTime);
                        return index >= 0 ? steps[index] : null;
                    }
                },
                currentIndex: {
                    value: function(currentProgramTime) {
                        if (isNaN(currentProgramTime)) {
                            currentProgramTime = pgm.time;
                        }

                        if (currentProgramTime !== recentProgramTime || current < 0) {
                            var i,
                                l,
                                step;

                            for (i = Math.max(0, current), l = steps.length; i < l; i++) {
                                step = steps[i];

                                if (step.tStart < currentProgramTime) {
                                    // current step is running
                                    if (!step.hasTStartReal) {
                                        step.tStartReal = pgm._normalizeRealTime() - (currentProgramTime - step.tStart);
                                        console.debug("fixing real start time of step #" + i + " at " + step.tStartReal);
                                    } else if (recentProgramTime && recentProgramTime > currentProgramTime) {
                                        steps.forEach(function(st, idx) {
                                            st.tStartReal = null;
                                            st.waitTime = 0;
                                            st.offsetTime = 0;
                                        });
                                        current = -1;
                                        break;
                                    }
                                } else if (recentProgramTime && recentProgramTime > currentProgramTime) {
                                    steps.forEach(function(st, idx) {
                                        st.tStartReal = null;
                                        st.waitTime = 0;
                                        st.offsetTime = 0;
                                    });
                                    current = -1;
                                    break;
                                }

                                if (step.tStart > currentProgramTime) {
                                    break;
                                }

                                current = i;
                            }
                            // program time has change since last request or didn't request current step before
                            recentProgramTime = currentProgramTime;
                        }

                        return current;
                    }
                },
                first: {
                    value: steps[0]
                },
                last: {
                    value: steps[steps.length - 1]
                }
            });
        }
    },

    /**
     * Binds item selected in configuration to give program time.
     *
     * @note Resulting object is providing current program time in property
     *       "value".
     * @note This binding is implicitly detecting stopped program timer causing
     *       frequent invocation of local method _whiteWaiting() to update
     *       viewport and display.
     *
     * @param config
     * @returns {{name:string, id:number, value:number, setValue:function, lock:function, unlock:function, setProperties:function}}
     * @private
     */
    _bindProgramTimer: function(config) {
        var self = this,
            info = {
                name: config.program.time,
                id: 0,
                value: -1,
                timer: 0,
                setValue: function(value) {
                    var newValue = parseFloat(value) || 0;

                    if (newValue !== info.value) {
                        if (info.timer) {
                            clearTimeout(info.timer); //eslint-disable-line no-undef
                            info.timer = null;
                        }

                        info.value = newValue;
                        self.waiting = false;

                        if (self.steps.isSettled()) {
                            self.steps.
                                then(function(steps) {
                                    steps.current(newValue);
                                });
                        }

                        if (!self.preview) {
                            info.timer = setTimeout(function() {
                                self.waiting = true;
                            }, 2000);
                        }
                    }
                },
                lock: shmi.visuals.controls.TrendDisplay.nop,
                unlock: shmi.visuals.controls.TrendDisplay.nop,
                setProperties: shmi.visuals.controls.TrendDisplay.nop
            };

        info.id = shmi.visuals.session.ItemManager.subscribeItem(info.name, info);

        return info;
    },

    _unbindProgramTimer: function(info) {
        if (info.timer) {
            clearTimeout(info.timer); //eslint-disable-line no-undef
            info.timer = null;
        }

        shmi.visuals.session.ItemManager.unsubscribeItem(info.name, info.id);
    },

    _whileWaiting: function() {
        var self = this,
            viewport = this.continuous.viewport;

        this.updateMaxTime(viewport).
            then(function(currentStepTStartReal) {
                if (!viewport.isLive && !viewport.transaction && currentStepTStartReal < viewport.rightRenderTime) {
                    self.manager.broadcast("render");
                }
            });
    },

    _updateMaxTime: function(viewport, steps) {
        viewport.programEndTime = steps.last.tEndReal;
        var retVal = (steps.current() === null) ? null : steps.current().tStartReal;
        return retVal;
    },

    /**
     * Updates maxTime of viewport according to current state of program.
     *
     * @param viewport
     * @returns {Promise<number>} promises real time of starting current step
     */
    updateMaxTime: function(viewport) {
        var self = this;

        return this.steps.
            then(function(steps) {
                return self._updateMaxTime(viewport, steps);
            });
    },

    onDelete: function() {
        this.code = null;
        this.time = null;
        this.waiting = false;
    }

};

/**
 * @param {shmi.visuals.controls.TrendDisplay.Parsers} Parsers
 */
shmi.visuals.controls.TrendDisplay.onConfiguration.push([-10, function _parseProgramConfiguration(cfg, tdControl, Parsers) {
    "use strict";

    if (!cfg || typeof cfg !== "object") {
        return;
    }

    if (!cfg.program || typeof cfg.program !== "object") {
        // program is omitted, thus program manager is disabled
        return;
    }

    tdControl.isPreparing = "processing-program-manager-configuration";

    var preview = !!cfg.program.preview;

    // validate configured ID of program to display
    var programIndex = parseInt(cfg.program.index);
    if (programIndex < 0) {
        shmi.log("TrendDisplay.onConfiguration() invalid program index", 3);
    }

    // validate configured timestamp marking start time of program
    var tStart = Parsers.timestamp(cfg.program.tStart);
    if (tStart === null) {
        shmi.log("TrendDisplay.onConfiguration() invalid tStart in configuration of program", 3);
    }

    // validate configured name of item delivering program time
    var timerItemName = String(cfg.program.time || "").trim();
    if (!timerItemName.length) {
        shmi.log("TrendDisplay.onConfiguration() missing name of program timer item", 3);
    }

    // validate configured items of program to display
    var items = cfg.program.items;
    if (!Array.isArray(items) || !items.length) {
        shmi.log("TrendDisplay.onConfiguration() invalid/missing program items configuration", 3);
    }

    // validate optionally configured index of style to use on indicating wait states
    var waitStyleIndex;
    if (shmi.objectHasOwnProperty(cfg.program, "waitStyleIndex")) {
        waitStyleIndex = parseInt(cfg.program.waitStyleIndex);
        if (waitStyleIndex < 0 || waitStyleIndex >= cfg.styles.length) {
            shmi.log("TrendDisplay.onConfiguration() invalid index of wait state indicator style", 3);
        }
    } else {
        waitStyleIndex = -1;
    }

    cfg.program = {
        index: programIndex,
        preview: preview,
        tStart: tStart,
        time: timerItemName,
        items: items.map(function(item, index) {
            if (!item || typeof item !== "object") {
                shmi.log("TrendDisplay.onConfiguration() invalid/missing program item configuration @" + index, 3);
            }

            var result = {};

            // validate type of associated program value
            result.valueType = String(item.valueType || "").toUpperCase();
            switch (result.valueType) {
            case "CV":
            case "SV":
            case "DO":
                break;

            default:
                shmi.log("TrendDisplay.onConfiguration() invalid valueType of program value on program item @" + index, 3);
            }

            // validate index of associated program value
            result.valueIndex = parseInt(item.valueIndex);
            if (isNaN(result.valueIndex) || result.valueIndex < 0) {
                shmi.log("TrendDisplay.onConfiguration() invalid valueIndex of program value on program item @" + index, 3);
            }

            // validate index of associated trend signal
            result.trendIndex = parseInt(item.trendIndex);
            if (isNaN(result.trendIndex) || result.trendIndex < 0 || result.trendIndex >= cfg.trend.items.length) {
                shmi.log("TrendDisplay.onConfiguration() invalid trendIndex of trend item on program item @" + index, 3);
            }

            // validate style to use on rendering program value
            if (shmi.objectHasOwnProperty(item, "styleIndex")) {
                result.styleIndex = parseInt(item.styleIndex);
                if (isNaN(result.styleIndex) || result.styleIndex < 0 || result.styleIndex >= cfg.styles.length) {
                    shmi.log("TrendDisplay.onConfiguration() invalid styleIndex on program item @" + index, 3);
                }

                result.style = cfg.styleManagers[result.styleIndex];
            } else {
                result.style = shmi.visuals.controls.TrendDisplay.Style.defaultStyle;
            }

            return result;
        }),
        waitStyleIndex: waitStyleIndex
    };

    var Static = shmi.visuals.controls.TrendDisplay.Program;
    if (!Static.api) {
        Static.api = shmi.requires("visuals.custom.wut.pgm");
    }
}
]);

shmi.visuals.controls.TrendDisplay.Program.api = null;

//
// SVN:      $HeadURL:$
// Author:   $LastChangedBy:$
// Date:     $LastChangedDate:$
// Revision: $LastChangedRevision:$
//
// Created by User Interface Design GmbH (UID)

shmi.visuals.controls.TrendDisplay.ProgramTimeLine = function(container, continuous, program) {
    "use strict";
    container.innerHTML = "";
    /**
     * Refers to HTML element to contain elements of timeline.
     *
     * @property container
     * @type {HtmlElement}
     */
    Object.defineProperty(this, "container", {
        value: container
    });

    /**
     * Refers to Continuous component this timeline has to be synced with.
     *
     * @property continuous
     * @type {shmi.visuals.controls.TrendDisplay.Continuous}
     */
    Object.defineProperty(this, "continuous", {
        value: continuous
    });

    /**
     * Refers to program this timeline is related to.
     *
     * @property program
     * @type {shmi.visuals.controls.TrendDisplay.Program}
     */
    Object.defineProperty(this, "program", {
        value: program
    });
};

shmi.visuals.controls.TrendDisplay.ProgramTimeLine.prototype = {

    _resetLocation: function(size) {
        var style = this.container.style;

        // store initial offset for use on sliding timestamp container
        this._offsetX = size.x;

        // reset location and size of timestamp container
        style.left = String(-size.x) + "px";
        style.width = String(size.totalWidth) + "px";
    },

    render: function() {
        "use strict";

        var self = this;

        if (!this.container) {
            // template doesn't mark element to contain time line elements
            // -> don't waste time trying to drawing anything onto nothing
            return null;
        }

        return this.program.steps.
            then(function(steps) {
                var continuous = self.continuous,
                    vp = continuous.viewport,
                    dc = continuous.getDC(),
                    size = continuous.renderer.getCanvasSize(),
                    container = self.container,
                    pgm = self.program,
                    preview = pgm.preview,
                    doc = container.ownerDocument,
                    leftTime = vp.leftRenderTime,
                    rightTime = vp.rightRenderTime,
                    labels = self._labels || {},
                    temp = {};

                self._resetLocation(size);

                if (!preview && leftTime < pgm.tStartReal) {
                    // don't render steps that have been passed back when
                    // program/trenddisplay was started initially
                    leftTime = pgm.tStartReal;
                }

                // generate label elements for all steps of program instantly
                // visible or by sliding (moving continuous horizontally)
                steps.asArray.
                    some(function(step, i) {
                        var x1,
                            x2,
                            l,
                            r,
                            stepStart,
                            stepEnd,
                            label;

                        stepStart = step.previous ? step.previous.tEndReal : step.tStartReal;
                        stepEnd = step.tEndReal; //stepStart + step.length + step.offsetTime;
                        if (step.next) {
                        //stepEnd += step.next.offsetTime;
                        }

                        if (step.tStart < pgm.time) return false;

                        if (preview || stepEnd > pgm.tStartReal) {
                            l = stepStart; // > leftTime ? stepStart : leftTime;
                            r = stepEnd; // < rightTime ? stepEnd : rightTime;
                            if (r > l) {
                            // step is (partly) contained in canvas of timeline,
                            // currently

                                x1 = dc.time2X(stepStart, 0);
                                x2 = dc.time2X(stepEnd, 0);

                                label = doc.createElement("label");
                                label.appendChild(doc.createTextNode(step.label));
                                label = container.appendChild(label);

                                label.style.left = x1 + "px";
                                label.style.width = Math.round(x2 - x1) + "px";

                                temp[i] = label;
                            }
                        }

                        return (stepStart > rightTime);
                    });

                // remove all element not in use anymore
                Object.keys(labels).
                    forEach(function(id) {
                        var label = labels[id];
                        if (label) {
                            label.parentNode.removeChild(label);
                        }
                    });

                self._labels = temp;
            });
    },

    onSlide: function(stage, amount) {
        if (!isNaN(this._offsetX)) {
            this.container.style.left = String(-this._offsetX + amount) + "px";
        }
    },

    onResize: function() {
        "use strict";
    },

    onDelete: function() {
        "use strict";

        var labels = this._labels;

        Object.keys(labels || {}).
            forEach(function(key) {
                var label = labels[key];

                label.parentNode.removeChild(label);
            });
    }

};

shmi.visuals.controls.TrendDisplay.derive(shmi.visuals.controls.TrendDisplay.ProgramTimeLine, shmi.visuals.controls.TrendDisplay.AbstractRenderer, "ProgramTimeLine");

/**
 * Centrally provides configuration of current TrendDisplay control to all its
 * components.
 *
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.Configuration = function(tdControl) {
    "use strict";

    var isConfigured = false;

    Object.defineProperties(this, {
        /**
         * Refers to trend display control instance current configuration
         * belongs to.
         *
         * @property trendDisplay
         * @type {shmi.visuals.controls.TrendDisplay}
         */
        trendDisplay: {
            value: tdControl
        },

        /**
         * Indicates if current instance has been configured before.
         *
         * @note This flag is initially false and gets true on writing any value
         *       for the first time. Once set, the flag can't be cleared again.
         *
         * @property isConfigured
         * @type {boolean}
         */
        isConfigured: {
            get: function() {
                return isConfigured;
            },
            set: function() {
                isConfigured = true;
            }
        }
    });
};

shmi.visuals.controls.TrendDisplay.Configuration.prototype = {

    validate: function(source) {
        "use strict";

        var config = source || {},
            tdControl = this.trendDisplay,
            TD = shmi.visuals.controls.TrendDisplay;

        // ensure messaging for validation is set up properly
        if (Array.isArray(TD.onConfiguration)) {
            TD.onConfiguration = TD.Messaging.wrap(TD.onConfiguration);
        }

        // invoke messaging queue for validating provided configuration
        return TD.onConfiguration.send(config, tdControl, TD.Parsers).
            return(config);
    },

    /**
     * Gets invoked on changing configuration properties.
     *
     * @param {*} newValue new value of property
     * @param {string} name
     * @param {*} oldValue new value of property
     * @private
     */
    _notifyChange: function(newValue, name, oldValue) {
        this.trendDisplay.broadcast(this, "configuration-changed", {
            name: name,
            newValue: newValue,
            oldValue: oldValue,
            configuration: this
        });
    },

    read: function() {
        return shmi.visuals.controls.TrendDisplay.Object.deepCopy(this);
    },

    /**
    * Assigns configuration to current instance to provide given configuration object after
    * normalizing and validating it.
    *
    * @param {object} configuration configuration to validate and provide
    * @param {boolean} noRecal do not Recalibrate viewpoint configuration
    * @return {shmi.visuals.controls.TrendDisplay.Promise<shmi.visuals.controls.TrendDisplay.Configuration>}
    */
    update: function(configuration, noRecal) {
        "use strict";

        var target = this,
            trendDisplay = this.trendDisplay,
            TD = shmi.visuals.controls.TrendDisplay;

        configuration = shmi.visuals.controls.TrendDisplay.Object.deepCopy(configuration);
        configuration.invalid = false;
        configuration.noRecal = noRecal || false;

        return this.validate(configuration).
            catch(function(error) {
                configuration.invalid = true;

                // validation failed
                if (target.isConfigured) {
                    // isn't initial configuration
                    // -> pop up some information
                    var text = String(error.message || error || "");
                    shmi.log(text.trim(), 3);

                    // -> simply reject/ignore this update (keeping current configuration)
                    shmi.log("TrendDisplay.Configuration.update() invalid configuration rejected", 3);
                }

                // -> initial configuration is invalid
                //    -> disable control
                return configuration;
            }).
            then(function(validated) {
                var replacing = Object.isSealed(target);

                if (replacing) {
                    // configuration was injected into current target before
                    // -> can't replace its properties for being sealed
                    //    -> need to replace whole target
                    target = new TD.Configuration(trendDisplay);
                }

                // inject provided, validated and qualified configuration into
                // current object observing
                TD.Object.injectObserved(target, validated,
                    TD.fastBind(target._notifyChange, target),
                    true, ["invalid"]);

                target.isConfigured = true;

                if (replacing) {
                    // instantly send broadcast notifying on having replaced
                    // whole configuration
                    target._notifyChange(null, null, null);
                }

                return target;
            });
    }
};

shmi.visuals.controls.TrendDisplay.Data = function(trendDisplayInstance) {
    "use strict";

    var manager = shmi.visuals.session.TrendManager,
        trendNames = null;

    /**
     * Provides wrapped TrendManager instance.
     *
     * @property manager
     * @type {shmi.visuals.core.TrendManager}
     */
    Object.defineProperty(this, "manager", {
        value: manager
    });

    /**
     * Provides Access to parent TrendDisplay instance.
     *
     */
    Object.defineProperty(this, "instance", {
        get: function() {
            return trendDisplayInstance;
        }
    });

    /**
     * Promises list of available trends' names.
     *
     * @property trend
     * @type {shmi.visuals.controls.TrendDisplay.Promise<string[]>}
     */
    Object.defineProperty(this, "trends", {
        get: function() {
            if (!trendNames) {
                trendNames = new shmi.visuals.controls.TrendDisplay.Promise(function(done) {
                    manager.getTrendList(function(result) {
                        done(result);
                    });
                });
            }

            return trendNames;
        }
    });
};

shmi.visuals.controls.TrendDisplay.Data.prototype = {

    /**
     * Caches instances of trend provider in prototype of data provider to be
     * shared by all instances of the latter.
     *
     * @type {object.<string,shmi.visuals.controls.TrendDisplay.Trend>}
     * @protected
     */
    _cache: {},

    /**
     * Delivers trend provider selected by name.
     *
     * @param {string} name name of trend to provide
     * @return {shmi.visuals.controls.TrendDisplay.Trend} provider for selected trend's data
     */
    getTrendByName: function(name) {
        "use strict";

        return new shmi.visuals.controls.TrendDisplay.Trend(this.manager, name, this.instance);
    }
};

/**
 * Validates configuration on trend to show.
 *
 * @type {{priority: number, callback: Function}}
 */
shmi.visuals.controls.TrendDisplay.onConfiguration.push([100, function parseDataConfiguration(cfg, tdControl) {
    "use strict";

    if (!cfg || typeof cfg !== "object") {
        return null;
    }

    // validate configuration on trend most basically
    if (!cfg.trend || typeof cfg.trend !== "object") {
        throw new TypeError("invalid/missing trend configuration");
    }

    var trend = String(cfg.trend.name || "").trim();
    if (!trend) {
        shmi.log("TrendDisplay.onConfiguration() missing name of trend to show", 3);
    }

    // validate configuration of data aggregation support
    var aggregate = cfg.trend.aggregate;
    if (!aggregate || typeof aggregate !== "object") {
        shmi.log("TrendDisplay.onConfiguration() missing configuration on aggregation", 3);
    }

    var Parsers = shmi.visuals.controls.TrendDisplay.Parsers,
        method = Parsers.aggregation(aggregate.method || "", 1),
        unit = Parsers.timeSpanUnit(aggregate.unit || "", 0);

    if (method === null) {
        shmi.log("TrendDisplay.onConfiguration() invalid aggregation method: " + aggregate.method, 3);
    }

    if (unit === null) {
        shmi.log("TrendDisplay.onConfiguration() invalid aggregation unit: " + aggregate.unit, 3);
    }

    // validate configuration of data items support
    var items = cfg.trend.items;
    if (!items || !items.length) {
        shmi.log("TrendDisplay.onConfiguration() missing configuration on items", 2);
    }

    cfg.trend.aggregate = {
        method: method,
        unit: unit
    };

    /*
     * finally validate name of trend (done this late due to network latency)
     */
    return tdControl.data.trends.
        then(function(available) {
            if (available.indexOf(trend) < 0) {
                shmi.log("TrendDisplay.onConfiguration() selected trend is not available", 3);
            }

            cfg.trend.name = trend;
        });
}
]);

/**
 * Wraps qualified information on a single signal (also known as single
 * "item of trend").
 *
 * @note All information is provided read-only.
 *
 * @constructor
 * @property {string} name
 * @property {string} label
 * @property {string} unit
 * @property {boolean} visible marks if curve of signal is visible/available
 * @property {boolean} scaleVisible marks if scale of signal is visible/available
 * @property {number} min minimum value of signal
 * @property {number} max maximum value of signal
 * @property {int} valueIndex index of signal's value in samples streamed by trend manager
 * @property {int} itemIndex index of related item in configuration of trend items
 * @property {int} laneIndex index of related lane in configuration of multi lane items
 */
shmi.visuals.controls.TrendDisplay.Signal = function(meta) {
    "use strict";

    shmi.visuals.controls.TrendDisplay.Object.injectFrozen(this, meta, true);
};

shmi.visuals.controls.TrendDisplay.Signal.prototype = {
    dontObserve: true
};

//
// SVN:      $HeadURL:$
// Author:   $LastChangedBy:$
// Date:     $LastChangedDate:$
// Revision: $LastChangedRevision:$
//
// Created by User Interface Design GmbH (UID)

/**
 * Implements description of single style to use on rendering graphs.
 *
 * @constructor
 */
shmi.visuals.controls.TrendDisplay.Style = function Style() {
    var color = null,
        width = null,
        dash = null,
        offset = null,
        opacity = null,
        symbol = null;

    function _setColor(value) {
        var parsed = shmi.visuals.controls.TrendDisplay.Parsers.color(value);
        if (parsed) {
            color = parsed;
        } else {
            shmi.log("TrendDisplay.Style() invalid color: " + value, 3);
        }
    }

    Object.defineProperties(this, {

        black: {
            value: {
                red: 0,
                green: 0,
                blue: 0,
                opacity: 1.0,
                hasAlpha: false,
                rgba: "rgba(0,0,0,1)",
                hex: "#000000",
                hexa: "#000000FF"
            }
        },

        /**
         * @type {Color}
         * @property color
         */
        color: {
            get: function() {
                return color || this.black;
            },
            set: _setColor
        },

        /**
         * @type {Color}
         * @property colour
         */
        colour: {
            get: function() {
                return color || this.black;
            },
            set: _setColor
        },

        /**
         * Provides stroke width in pixels.
         *
         * @type {int}
         * @property width
         */
        width: {
            get: function() {
                return width || 1;
            },
            set: function(value) {
                var parsed = parseInt(value);
                if (parsed > 0) {
                    width = parsed;
                } else if (value) {
                    shmi.log("TrendDisplay.Style() invalid stroke width: " + value, 1);
                }
            }
        },

        /**
         * @type {Array.<int>}
         * @property dash
         */
        dash: {
            get: function() {
                return dash || [];
            },
            set: function(value) {
                var parsed = shmi.visuals.controls.TrendDisplay.Parsers.dash(value);
                if (parsed !== null) {
                    dash = parsed;
                } else if (value.length) {
                    shmi.log("TrendDisplay.Style() invalid dash: " + value, 1);
                }
            }
        },

        /**
         * @type {int}
         * @property offset
         */
        offset: {
            get: function() {
                return offset || 0;
            },
            set: function(value) {
                var parsed = shmi.visuals.controls.TrendDisplay.Parsers.positive(value);
                if (parsed !== null) {
                    offset = parsed;
                } else if (value !== "") {
                    shmi.log("TrendDisplay.Style() invalid offset: " + value, 1);
                }
            }
        },

        /**
         * @type {number}
         * @property opacity
         */
        opacity: {
            get: function() {
                return (color && color.hasAlpha) ? color.alpha : opacity === null ? 1.0 : opacity;
            },
            set: function(value) {
                var parsed = shmi.visuals.controls.TrendDisplay.Parsers.relative(value);
                if (parsed !== null) {
                    if (color) {
                        color.alpha = parsed;
                        color = shmi.visuals.controls.TrendDisplay.Parsers.color(color);
                    }
                    opacity = parsed;
                } else {
                    shmi.log("TrendDisplay.Style() invalid opacity: " + value, 1);
                }
            }
        },

        /**
         * @type {?shmi.visuals.controls.TrendDisplay.Promise<Image>}
         * @property symbol
         */
        symbol: {
            get: function() {
                return symbol;
            },
            set: function(value) {
                var TD = shmi.visuals.controls.TrendDisplay,
                    parsed;

                if (value instanceof TD.Promise) {
                    symbol = value;
                } else {
                    parsed = TD.Parsers.url(value);

                    if (parsed !== null) {
                        symbol = new TD.Promise();

                        var img = new Image(); //eslint-disable-line no-undef
                        img.onload = function() {
                            symbol.resolve(img);
                        };
                        img.onerror = function() {
                            symbol.reject("loading image " + parsed + " failed");
                        };
                        img.src = parsed;
                    } else {
                        shmi.log("TrendDisplay.Style() invalid symbol URL: " + value, 3);
                    }
                }
            }
        },

        /**
         * Provides this style's information as a simple object.
         *
         * @type {{color: *, width: *, opacity: *, dash: *, symbol: *}}
         * @property raw
         * @readonly
         */
        raw: {
            get: function() {
                var result = {};

                if (color !== null) {
                    result.color = color;
                }
                if (width !== null) {
                    result.width = width;
                }
                if (opacity !== null) {
                    result.opacity = opacity;
                }
                if (dash !== null) {
                    result.dash = dash;
                }
                if (offset !== null) {
                    result.offset = offset;
                }
                if (symbol !== null) {
                    result.symbol = symbol;
                }

                return result;
            }
        }

    });
};

shmi.visuals.controls.TrendDisplay.Style.prototype = {

    dontObserve: true,

    /**
     * Creates new style descriptor basing on current one being qualified by
     * provided CSS styles in case of current style isn't providing related
     * information already.
     *
     * @param {CSSStyleDefinition} css
     * @returns {shmi.visuals.controls.TrendDisplay.Style}
     */
    getQualifiedByCss: function(css) {
        var props = this._cssParserDefs,
            Parsers = shmi.visuals.controls.TrendDisplay.Parsers,
            style,
            raw;

        // make a copy of current style
        style = new shmi.visuals.controls.TrendDisplay.Style();
        raw = this.raw;

        Object.keys(raw).
            forEach(function(name) {
                style[name] = raw[name];
            });

        // check CSS on all properties missing in copied style
        Object.keys(props).
            forEach(function(target) {
                if (!shmi.objectHasOwnProperty(raw, target)) {
                    var sources = props[target];

                    sources.some(function(source) {
                        var value = Parsers.none(css[source]);
                        if (value !== null) {
                            try {
                                style[target] = value;
                                return true;
                            } catch (e) {
                                return false;
                            }
                        }
                        return false;
                    });
                }
            });

        return style;
    },

    _cssParserDefs: {
        color: ["stroke", "color"],
        width: ["stroke-width"],
        opacity: ["stroke-opacity", "opacity"],
        dash: ["stroke-dasharray"],
        offset: ["stroke-dashoffset"],
        symbol: ["background-image"]
    },

    /**
     * Applies style on provided drawing context of HTML5 canvas element.
     *
     * @param {CanvasRenderingContext2D} dc
     */
    applyOnCanvas: function(dc) {
        dc.strokeStyle = this.color.rgba;
        dc.lineWidth = this.width;
        if (typeof dc.setLineDash === "function") {
            dc.setLineDash(this.dash);
            dc.lineDashOffset = this.offset;
        }
    },

    /**
     * Applies style on provided SVG stylable element (e.g. SVGPathElement).
     *
     * @param {SVGStylable} element
     */
    applyOnSvg: function(element) {
        element.style.stroke = this.color.rgba;

        element.style["stroke-width"] = this.width;

        element.style["stroke-dasharray"] = this.dash.join(",");
        element.style["stroke-dashoffset"] = this.offset;
    },

    /**
     * Applies style on provided HTML element.
     *
     * @param {HtmlElement} element
     */
    applyOnElement: function(element) {
        element.style.color = this.color.rgba;
        //element.style.opacity = this.opacity;
    },

    /**
     * Creates detached copy of current style (suitable for individually
     * adjusting or qualifying it).
     *
     * @returns {shmi.visuals.controls.TrendDisplay.Style}
     */
    copy: function() {
        var target = new shmi.visuals.controls.TrendDisplay.Style(),
            props = this.raw;

        Object.keys(props).
            forEach(function(propName) {
                target[propName] = props[propName];
            });

        return target;
    }

};

/**
 * Declares some default style used as fallback if some configured style fails
 * validation.
 *
 * This default style is currently selecting black, solid, fully opaque stroke
 * without any symbol.
 *
 * @type {shmi.visuals.controls.TrendDisplay.Style}
 */
shmi.visuals.controls.TrendDisplay.Style.defaultStyle = new shmi.visuals.controls.TrendDisplay.Style();

/**
 * Creates style descriptor providing certain styles of selected HTML element.
 *
 * @param {HtmlElement} element
 * @returns {shmi.visuals.controls.TrendDisplay.Style}
 */
shmi.visuals.controls.TrendDisplay.Style.getOnElement = function(element) {
    var defStyle = shmi.visuals.controls.TrendDisplay.Style.defaultStyle;

    if (element) {
        return defStyle.getQualifiedByCss(window.getComputedStyle(element, null));
    }

    return defStyle;
};

/**
 * Validates mandatory configuration in group "styles".
 */
shmi.visuals.controls.TrendDisplay.onConfiguration.push([50, function _parseStyleConfiguration(cfg, tdControl) {
    if (!cfg || typeof cfg !== "object") {
        return;
    }

    if (!Array.isArray(cfg.styles)) {
        shmi.log("TrendDisplay.onConfiguration() invalid/missing style set", 3);
    }

    var Style = shmi.visuals.controls.TrendDisplay.Style;

    cfg.styleManagers = cfg.styles.map(function(style, index) {
        try {
            return styleParser(style);
        } catch (e) {
            shmi.log("TrendDisplay.onConfiguration() ignoring style due to invalid property @" + index + ": " + e.message, 1);
            return Style.defaultStyle;
        }
    });

    cfg.styleManagers.waitStyle = styleParser({
        color: "#000000",
        symbol: "pics/system/icon_196.png"
    });

    cfg.styleManagers.inactiveStyle = cfg.inactiveStyle;

    function styleParser(style) {
        var instance = new Style();

        ["color", "colour", "width", "dash", "opacity", "symbol"].
            forEach(function(propName) {
                if (shmi.objectHasOwnProperty(style, propName)) {
                    instance[propName] = style[propName];
                }
            });

        return instance;
    }
}
]);

/**
 * Provides data of a trend.
 *
 * This class isn't replacing TrendManager, but adopts trend data streamed per
 * TrendManager to comply with API of TrendDisplay's "continuous paper"
 * component and attached curve renderers.
 *
 * @param {shmi.visuals.core.TrendManager} manager
 * @param {string} trendName
 * @constructor
 */

shmi.visuals.controls.TrendDisplay.Trend = function Trend(manager, trendName, trendDisplayInstance) {
    "use strict";

    this.active = true;
    this.trendDisplay = trendDisplayInstance;

    /**
     * @typedef {{startTime:int,endTime:int,values:Array.<{}>}} shmi.visuals.controls.TrendDisplay.TrendData
     */

    /** Refers to messaging queue invoked on updating state of provider. @type {Messaging<Trend>} */
    var onUpdate = new shmi.visuals.controls.TrendDisplay.Messaging();

    /** Refers to messaging queue invoked on updating cached trend data. @type {Messaging<Trend>} */
    var onData = new shmi.visuals.controls.TrendDisplay.Messaging();

    /** Refers to messaging queue invoked on failed reception of data. @type {Messaging<Error,int>} */
    var onError = new shmi.visuals.controls.TrendDisplay.Messaging();

    /** Refers to promise resolved as soon as some data was received. @type {Promise} */
    var gotData = new shmi.visuals.controls.TrendDisplay.Promise();

    /** Marks time of earliest data to fetch and cache. @type {Number} */
    var startTime = null;

    /** Marks time of latest data to fetch and cache. @type {Number} */
    var endTime = null;

    /** Marks whether data is continuously captured or not. @type {boolean} */
    var isLive = false;

    /** Marks unit of fetched aggregated values. @type {int} */
    var unit = 0;

    /** Marks method for aggregating values. @type {int} */
    var aggregation = 1;

    /** Stores ID of data retrieval stream. @type {int} */
    var subscriptionId = null;

    /** Contains recently fetched data and related meta information. @type {shmi.visuals.controls.TrendDisplay.TrendData} */
    var cache = null;

    /** Marks if trend state hasn't been changed since last fetch of data. @type {boolean} */
    var pristine = false;

    var trendInfo = null;

    /** Item Alias/IDs of related trend group. @type {Array} */
    var items = [];

    /**
     * Promises info on items of trend.
     * Loads trendInfo object
     *
     * @property info
     * @type {Promise<object>}
     * @readonly
     */
    Object.defineProperty(this, "info", {
        get: function() {
            var Promise = shmi.visuals.controls.TrendDisplay.Promise;

            trendInfo = Promise.first(
                Promise.delay(function() {
                    shmi.log("TrendDisplay.Trend() timeout on gathering information on trend: " + trendName, 3);
                }, shmi.c("TREND_DISPLAY_TRENDINFO_TIMEOUT")),
                new Promise(function(done) {
                    var itemList = [];
                    items.forEach(function(itemInfo) {
                        itemList.push(itemInfo.alias);
                    });
                    manager.getTrendInfo(trendName, function(response) {
                        done(response);
                    }, itemList);
                }));

            return trendInfo;
        }
    });

    /**
     * Provides wrapped TrendManager instance.
     *
     * @property manager
     * @type {shmi.visuals.core.TrendManager}
     */
    Object.defineProperty(this, "manager", {
        value: manager
    });

    /**
     * Provides name of observed trend.
     *
     * @property name
     * @type {string}
     */
    Object.defineProperty(this, "name", {
        value: trendName
    });

    /**
     * Describes desired start time of slice to retrieve.
     *
     * @note This property is delivering requested start time of slice of trend
     *       to deliver. This value might be remarkably less than timestamp of
     *       first delivered sample of either signal in trend. Refer to the
     *       cache response for getting start time of actually retrieved slice
     *       of trend (e.g. per trend.data.startTime)
     *
     * @type {number}
     */
    Object.defineProperty(this, "startTime", {
        get: function() {
            return startTime || 0;
        },
        set: function(value) {
            var v = Number(value);

            if (v > 0 && v !== startTime) {
                startTime = v;

                pristine = false;
                onUpdate.send(this);
            }
        }
    });

    /**
     * Describes desired end time of slice to retrieve.
     *
     * On setting this property takes one out of
     * - a unix timestamp
     * - 0 for retrieving all most-recently captured trend data
     * - -1 for retrieving all most-recently captured trend data and keep
     *   retrieval running. This is controlling property isLive.
     * On reading this property is delivering end time of requested slice, thus
     * returning current time on requesting all most-recently captured data.
     *
     * @type {int}
     */
    Object.defineProperty(this, "endTime", {
        get: function() {
            return endTime || shmi.getServerTime();
        },
        set: function(value) {
            var v = Number(value);

            if (v > -2 && v !== endTime) {
                endTime = v;

                isLive = (endTime === -1);
                if (isLive) {
                    endTime = 0;
                }

                pristine = false;
                onUpdate.send(this);
            }
        }
    });

    /**
     * Describes method for aggregating values on retrieval.
     *
     * On setting this property is supporting fuzzy names in addition to
     * integers for selecting aggregation method so configuration parameters
     * might be passed more conveniently. On reading property is always
     * providing the internally used integer selecting unit.
     *
     * @type {int|string}
     */
    Object.defineProperty(this, "aggregate", {
        get: function() {
            return aggregation;
        },
        set: function(value) {
            var parsed = shmi.visuals.controls.TrendDisplay.Parsers.aggregation(value, 1, null);
            if (parsed !== null) {
                aggregation = parsed;

                if (aggregation && !unit) {
                    unit = 1;
                }

                pristine = false;
                onUpdate.send(this);
            }
        }
    });

    /**
     * Describes unit of aggregated values.
     *
     * On setting this property is supporting fuzzy names in addition to
     * integers for selecting unit so configuration parameters might be passed
     * more conveniently. On reading property is always providing the internally
     * used integer selecting unit.
     *
     * @type {int|string}
     */
    Object.defineProperty(this, "unit", {
        get: function() {
            return unit;
        },
        set: function(value) {
            var parsed = shmi.visuals.controls.TrendDisplay.Parsers.timeSpanUnit(value, 0, null);
            if (parsed !== null) {
                unit = parsed;

                if (aggregation && !unit) {
                    unit = 1;
                }

                pristine = false;
                onUpdate.send(this);
            }
        }
    });

    /**
     * Provides all item alias/IDs of trend group for querytrend.
     *
     * @type {Array}
     */
    Object.defineProperty(this, "items", {
        get: function() {
            return items;
        },
        set: function(value) {
            items = value;

            pristine = false;
            onUpdate.send(this);
        }
    });

    /**
     * Indicates whether trend data is captured live or not.
     *
     * @type {boolean}
     * @readonly
     */
    Object.defineProperty(this, "isLive", {
        get: function() {
            return isLive;
        }
    });

    /**
     * Indicates whether state of trend data is pristine or not.
     *
     * @note This marker is counterpart to using onUpdate messaging queue
     *       instantly notifying on changing state of provider.
     *
     * @type {boolean}
     * @readonly
     */
    Object.defineProperty(this, "isPristine", {
        get: function() {
            return pristine;
        }
    });

    /**
     * Retrieves any available set of data.
     *
     * @property data
     * @type {{}}
     * @readonly
     */
    Object.defineProperty(this, "data", {
        get: function() {
            return cache || {
                startTime: 1,
                endTime: 2,
                values: []
            };
        }
    });

    /**
     * Promises some actually fetched data.
     *
     * @note This promise is available to depend on trend providing some actual
     *       data in property "data" implying reception of some trend stream
     *       data.
     *
     * @property gotData
     * @type {shmi.visuals.controls.TrendDisplay.Promise}
     * @readonly
     */
    Object.defineProperty(this, "gotData", {
        value: gotData
    });

    /**
     * Controls and detects if data is fetched from TrendManager currently.
     *
     * @type {boolean}
     */
    Object.defineProperty(this, "running", {
        get: function() {
            return (subscriptionId !== null);
        },
        set: function(value) {
            var requested = !!value,
                actual = (subscriptionId !== null),
                self = this;

            if (requested !== actual) {
                if (actual) {
                    shmi.log("TrendDisplay: closing trend " + self.name, 0);
                    manager.closeTrend(self.name, subscriptionId);
                    subscriptionId = null;
                } else if (startTime !== null) {
                    // use local flag to keep existing cache until fetching new
                    // data for the first time
                    var dropCache = true;

                    shmi.log("TrendDisplay: opening trend " + self.name + ": [" + startTime + "," + (isLive ? -1 : endTime) + "] " + aggregation + " " + unit + " " + items, 0);
                    var loadOpen = true;
                    shmi.fire("trend-display.state", {
                        loading: true
                    }, self);
                    startTime = Math.floor(startTime / 10) * 10;

                    if (items.length > 0) {
                        subscriptionId = manager.openTrend(self.name, startTime, isLive ? -1 : endTime, function(status, data) {
                            if (loadOpen) {
                                loadOpen = false;
                                shmi.fire("trend-display.state", {
                                    loading: false
                                }, self);
                            }

                            if (!self.active) {
                                console.debug("trend-display not running, cancel callback");
                                return;
                            }

                            cache = self._collectData.call(self, dropCache ? null : cache, status, data);

                            dropCache = false;

                            if (!isLive) {
                                subscriptionId = null;
                            }

                            pristine = true;

                            onData.send(self).
                                then(function() {
                                    gotData.resolve();
                                });
                        }, unit, aggregation, items);
                    }
                }

                pristine = false;
                onUpdate.send(self);
            }
        }
    });

    /**
     * Manages listeners for event of having updated state of trend.
     *
     * @property onUpdate
     * @type {Messaging<Trend>}
     */
    Object.defineProperty(this, "onUpdate", {
        value: onUpdate
    });

    /**
     * Manages listeners for event of having updated cached trend data.
     *
     * @property onData
     * @type {Messaging<Trend>}
     */
    Object.defineProperty(this, "onData", {
        value: onData
    });

    /**
     * Manages listeners for event of having updated state of trend.
     *
     * @property onUpdate
     * @type {Messaging<Error,int>}
     */
    Object.defineProperty(this, "onError", {
        value: onError
    });
};

shmi.visuals.controls.TrendDisplay.Trend.prototype = {

    _collectData: function(cache, status, data) {
        "use strict";

        var error,
            start,
            end,
            values;

        if (status) {
            error = new Error("TrendDisplay.Trend._collectData() error status #" + status + " on retrieving trend data");
        } else if (!data || !Array.isArray(data.data)) {
            error = new TypeError("TrendDisplay.Trend._collectData() invalid or missing trend data");
        } else if (data.name !== this.name) {
            error = new TypeError("TrendDisplay.Trend._collectData() got foreign trend data, wanted: " + this.name + ", got: " + data.name);
        }

        if (error) {
            shmi.log(error.message, 3);

            this.onError.send(error, status);
        } else {
            if (!cache || !this.isLive) {
                cache = {};
            }

            start = Number(cache.startTime) || shmi.getServerTime();
            end = Number(cache.endTime) || 0;
            values = cache.values || [];

            // no trends configured
            if (values) {
                cache.values = values.concat(data.data.
                    map(function(sample) {
                        var ts = Number(sample.timestamp);

                        if (ts && isFinite(ts)) {
                            if (ts < start) {
                                start = ts;
                            }
                            if (ts > end) {
                                end = ts;
                            }

                            return [
                                ts,
                                sample.values,
                                parseInt(sample.id) === 0
                            ];
                        } else {
                            console.error("TrendDisplay: got trend data lacking valid timestamp", 2);
                            return null;
                        }
                    }).
                    filter(function(i) {
                        return i;
                    })).
                    sort(function(l, r) {
                        return l[0] - r[0];
                    });
            }

            cache.startTime = start;
            cache.endTime = start > end ? start : end;
        }

        return cache;
    }

};

/**
 * Normalizes and validates configuration options regarding selection of
 * selected trend's items.
 *
 * @note This handler is configured to run after handler of provider Data in
 *       Data.js which is validating existing group "trend" containing name of
 *       trend.
 *
 *       Furthermore this handler relies on having successfully validated set of
 *       styles before.
 */

shmi.visuals.controls.TrendDisplay.onConfiguration.push([20, function _parseTrendConfiguration(cfg, tdControl) {
    "use strict";

    if (!cfg && typeof cfg !== "object") {
        return null;
    }

    if (!Array.isArray(cfg.trend.items) || !cfg.trend.items) {
        shmi.log("TrendDisplay.onConfiguration() missing trend items in configuration", 3);
    }

    var trendName = cfg.trend.name, // trend's name was validated before (@see Data.js)
        isMultiLane = cfg.display.type && (cfg.display.type.toLowerCase() === "multilane" || cfg.display.type.toLowerCase() === "multi"),
        items,
        defaultColor,
        itemStyle,
        activeLanes = [];

    // validate selected set of items
    if (cfg.trend.items.length) {
        var localItemStyles = localStorage['itemStyles'] ? JSON.parse(localStorage['itemStyles']) : [], //eslint-disable-line no-undef
            highlightSignal = null;

        cfg.trend.items.forEach(function(item) {
            if (cfg.display.highlightSignal === item.signalIndex) {
                highlightSignal = cfg.display.highlightSignal;
            }
        });
        if (highlightSignal === null) {
            delete cfg.display.highlightSignal;
        }

        items = cfg.trend.items.map(function(item, index) {
            if (!item || typeof item !== "object") {
                shmi.log("TrendDisplay.onConfiguration() invalid/missing configuration of trend item @" + index, 3);
            }

            var result = {};

            if (shmi.objectHasOwnProperty(item, "styleIndex")) {
                result.styleIndex = parseInt(item.styleIndex);
                if (isNaN(result.styleIndex) || result.styleIndex < 0 || result.styleIndex >= cfg.styles.length) {
                    shmi.log("TrendDisplay.onConfiguration() invalid/missing styleIndex on trend item @" + index, 3);
                }

                result.style = cfg.styleManagers[result.styleIndex];

                defaultColor = cfg.styleManagers.inactiveStyle.color;
                if (isMultiLane) {
                    // highlight only first curve on lane and change all;other curves to grey
                    itemStyle = cfg.styles[item.styleIndex];
                    if (typeof activeLanes[item.laneIndex] !== 'undefined') {
                        if (!localItemStyles.length || typeof localItemStyles[item.signalIndex] === 'undefined' || localItemStyles[item.signalIndex] === null) {
                            if (itemStyle.color !== defaultColor) {
                                localItemStyles[item.signalIndex] = itemStyle.color;
                            }
                            if (localItemStyles[item.signalIndex] === null) {
                                result.style.color = itemStyle.color;
                            } else {
                                result.style.color = defaultColor;
                            }
                        } else {
                            result.style.color = defaultColor;
                        }
                    } else if ((localItemStyles[item.signalIndex] === null) || (localItemStyles[item.signalIndex] === undefined)) {
                        result.style.color = itemStyle.color;

                        activeLanes[item.laneIndex] = [];
                        activeLanes[item.laneIndex].push(item);
                    } else {
                        if (itemStyle.color !== defaultColor) {
                            localItemStyles[item.signalIndex] = itemStyle.color;
                        }
                        result.style.color = defaultColor;
                    }
                }
                if (shmi.objectHasOwnProperty(cfg.display, "highlightSignal") && cfg.display.highlightSignal !== undefined) {
                    if (highlightSignal !== null && highlightSignal !== item.signalIndex) {
                        result.style.color = defaultColor;
                    }
                }
            } else {
                result.style = shmi.visuals.controls.TrendDisplay.Style.defaultStyle;
            }

            // validate index of associated trend signal
            if (shmi.objectHasOwnProperty(item, "signalIndex")) {
                result.signalIndex = parseInt(item.signalIndex);
                if (isNaN(result.signalIndex) || result.signalIndex < 0) {
                    shmi.log("TrendDisplay.onConfiguration() invalid signalIndex on trend item @" + index, 3);
                }
            } else {
                result.signalIndex = index;
            }

            // validate index of associated lane
            if (shmi.objectHasOwnProperty(item, "laneIndex")) {
                result.laneIndex = parseInt(item.laneIndex);
                if (isNaN(result.laneIndex) || result.laneIndex < 0) {
                    shmi.log("TrendDisplay.onConfiguration() invalid laneIndex on trend item @" + index, 3);
                }
            } else {
                result.laneIndex = index;
            }

            ["alias", "label", "name", "unit"].
                forEach(function(name) {
                    if (shmi.objectHasOwnProperty(item, name)) {
                        result[name] = String(item[name]).trim();
                    }
                });

            ["visible", "scaleVisible"].
                forEach(function(name) {
                    if (shmi.objectHasOwnProperty(item, name)) {
                        result[name] = !!item[name];
                    }
                });

            return result;
        });

        if (localItemStyles.length) {
            localStorage.setItem("itemStyles", JSON.stringify(localItemStyles)); //eslint-disable-line no-undef
        }
    } else {
        return null;
    }

    // merge given trend display config (my-trend.json) object with loaded trendinfo object
    var trend = tdControl.data.getTrendByName(trendName);
    trend.items = items;
    return trend.info.then(function(infos) {
        var Signal = shmi.visuals.controls.TrendDisplay.Signal;
        cfg.trend = {
            name: trendName,
            items: items,
            aggregate: cfg.trend.aggregate,
            signals: items.map(function(item, index) {
                var info = infos.items[item.signalIndex];

                // filter items by alias
                if (item.alias !== "") {
                    info = infos.items.filter(function(object) {
                        return object.name === item.alias;
                    });
                    if (info.length) {
                        info = info[0];
                    } else {
                        shmi.log("TrendDisplay.onConfiguration() missing/invalid item.alias in trend display config (my-trend.json) or in trendInfo object @" + trendName, 3);
                    }
                }
                var customLabel = null;
                var helpers = null;

                if (!info || typeof info !== "object") {
                    shmi.log("missing/invalid info on item @" + index, 3);
                }

                helpers = shmi.pkg("visuals.custom.wut.helpers");
                if (typeof helpers.convertLabelToken === "function") {
                    customLabel = shmi.localize(helpers.convertLabelToken(info.label));
                } else {
                    //wut.helpers module not present or label could not be converted
                    customLabel = shmi.localize(info.label);
                }

                var min = parseFloat(info.min),
                    max = parseFloat(info.max);

                if (isNaN(min) || isNaN(max)) {
                    shmi.log("missing/invalid min/max value on item @" + index, 3);
                }

                return new Signal({
                    alias: String(item.alias || info.alias || ""),
                    name: String(item.name || info.name || ("item" + index)),
                    label: String(customLabel || info.label || item.name || info.name || ""),
                    unit: String(item.unit || info.unit || ""),
                    min: min,
                    max: max,
                    visible: item.visible,
                    scaleVisible: shmi.objectHasOwnProperty(item, "scaleVisible") ? item.scaleVisible : item.visible,
                    valueIndex: item.signalIndex,
                    styleIndex: item.styleIndex || 0,
                    laneIndex: item.laneIndex || 0,
                    itemIndex: index
                });
            })
        };
    });
}
]);

shmi.registerControlType("trend-display", shmi.visuals.controls.TrendDisplay, true);
shmi.registerControlType("trend-display-continuous", shmi.visuals.controls.TrendDisplay.Continuous, false);
shmi.registerControlType("trend-display-handle", shmi.visuals.controls.TrendDisplay.Handle, false);
shmi.registerControlType("trend-display-scale", shmi.visuals.controls.TrendDisplay.Scale, false);
shmi.registerControlType("trend-display-timeline", shmi.visuals.controls.TrendDisplay.Timeline, false);
shmi.registerControlType("trend-display-program", shmi.visuals.controls.TrendDisplay.ProgramManager, false);

/**
 * unfreeze object
 *
 * @param freezedObject
 * @returns {undefined}
 */
Object.unfreeze = function(freezedObject) {
    var newObject = undefined;
    if (freezedObject instanceof Array) {
        newObject = [];
        var clone = function(v) {
            newObject.push(v);
        };
        freezedObject.forEach(clone);
    } else if (freezedObject instanceof String) {
        newObject = String(freezedObject);
    } else if (typeof freezedObject === 'object') {
        newObject = {};
        for (var property in freezedObject) {
            if (typeof property === 'object') {
                newObject[property] = Object.unfreeze(freezedObject[property]);
            } else {
                newObject[property] = freezedObject[property];
            }
        }
    }
    return newObject;
};

/**
 * user action for trend-display
 *
 * @param params
 */
shmi.visuals.session.userActions['trend-display'] = function(params) {
    var parts = params.toString().split(','),
        elementName = parts[0],
        action = parts[1],
        trendDisplay = shmi.ctrl(elementName),
        value = null;

    if (trendDisplay) {
        switch (action) {
        case "addZoomX":
            // zoom factor
            value = (parts.length > 2) ? parts[2] : 100;
            addZoomX(value);
            break;
        case "setZoomX":
            // zoom factor
            value = (parts.length > 2) ? parts[2] : 100;
            setZoomX(value);
            break;
        case "handleTime":
            value = (parts.length > 2) ? parts[2] : 0;
            var interval = (parts.length > 3) ? parts[3] : '';
            setHandleTime(value, interval);
            break;
        case "timepicker":
            // get values to merge from time picker and date picker
            var //selectedDate = shmi.ctrl('.trend-display-select-date').selectedDate,
                selectedTime = shmi.ctrl('.trend-display-select-time').getValue();
            value = selectedTime * 1000;
            setHandleTime(value);
            break;
        case "toggleLiveMode":
            value = (parts.length > 2) ? parts[2] : null;
            toggleLiveMode(value);
            break;
        case "previousTimestamp":
            showTimestamp(-1);
            break;
        case "nextTimestamp":
            showTimestamp(1);
            break;
        default:
        }
    } else {
        console.error("trend-display action: TrendDisplay element '" + elementName + "' not found");
    }

    function getIM() {
        return shmi.requires("visuals.session.ItemManager");
    }

    /**
     * add value to zoom level for X
     *
     * @param level
     */
    function addZoomX(levelParam) {
        var items = trendDisplay.items.items,
            level = parseFloat(levelParam) || 100;

        if (items) {
            var zoomX = getIM().getItem(items.zoomX.name);
            if (zoomX.value < shmi.c("TREND_DISPLAY_ZOOM_SCALE_X_MIN") * 100) { //3 digit precision for zoom level
                level = shmi.c("TREND_DISPLAY_ZOOM_SCALE_X_MIN") * (100 + level);
            } else {
                level = zoomX.value * (100 + level) / 100;
            }
            zoomX.writeValue(level);
        }
    }

    /**
     * set zoom level for X
     *
     * @param level
     */
    function setZoomX(levelParam) {
        var items = trendDisplay.items.items,
            level = parseFloat(levelParam) || 100;

        if (items) {
            var zoomX = getIM().getItem(items.zoomX.name);
            console.warn('zoomX.value: ' + zoomX.value + ' level: ' + level);
            zoomX.writeValue(level);
        }
    }

    /**
     * set handle time
     *
     * @param value to add/sub or handle time as a fix date to jump to
     * @param interval optional kind of date calculation -> not in use recheck if necessary
     *        'year' | 'quarter' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second'
     */
    function setHandleTime(valueParam, intervalParam) {
        var items = trendDisplay.items.items,
            date = new Date(),
            valueF = parseInt(valueParam),
            intervalF = intervalParam || '',
            timestamp = value.toString().length === 13 ? value : date;

        if (items) {
            var liveMode = getIM().getItem(items.live.name),
                leftTime = getIM().getItem(items.leftTime.name),
                handleTime = getIM().getItem(items.handleTime.name);

            if (intervalF !== '') {
                var currentDate = date;
                if (items.leftTime.lastValue !== 0) {
                    var lastValueLength = items.leftTime.lastValue.toString().length;
                    currentDate = lastValueLength === 10 ?
                        new Date(items.leftTime.lastValue * 1000) :
                        lastValueLength === 13 ? new Date(items.leftTime.lastValue) : date;
                }
                switch (intervalF.toLowerCase()) {
                case 'year':
                    timestamp = currentDate.setFullYear(currentDate.getFullYear() + valueF);
                    break;
                case 'quarter':
                    timestamp = currentDate.setMonth(currentDate.getMonth() + (valueF * 3));
                    break;
                case 'month':
                    timestamp = currentDate.setMonth(currentDate.getMonth() + valueF);
                    break;
                case 'week':
                    timestamp = currentDate.setDate(currentDate.getDate() + (valueF * 7));
                    break;
                case 'day':
                    timestamp = currentDate.setDate(currentDate.getDate() + valueF);
                    break;
                case 'hour':
                    timestamp = currentDate.setTime(currentDate.getTime() + (valueF * 3600000));
                    break;
                case 'minute':
                    timestamp = currentDate.setTime(currentDate.getTime() + (valueF * 60000));
                    break;
                case 'second':
                    timestamp = currentDate.setTime(currentDate.getTime() + (valueF * 1000));
                    break;
                default:
                    shmi.log("[UserAction] trend-display - setTimestamp: undefined interval " + intervalF, 3);
                    break;
                }
            } else if (valueF.toString().length <= 10) {
                timestamp = (parseInt(items.leftTime.lastValue) !== 0) ? parseInt(items.leftTime.lastValue) + (valueF * 3600) : new Date(date.getTime() + (valueF * 3600)).getTime();
            }

            // correct timestamp
            timestamp = timestamp.toString().length === 13 ? parseInt(timestamp / 1000) : parseInt(timestamp);
            if (timestamp.toString().length === 10) {
                // (de)activate live mode
                if (parseInt(date.getTime() / 1000) <= timestamp) {
                    liveMode.writeValue(-1);
                } else if (trendDisplay.isActive()) {
                    liveMode.writeValue(0);
                }

                leftTime.writeValue(timestamp);

                // Bugfix: handle bar has only old values and will not display values without timeout
                setTimeout(function() {
                    handleTime.writeValue(timestamp);
                }, 250);
            } else {
                shmi.log("[UserAction] trend-display - setTimestamp: no correct value/timestamp " + timestamp, 3);
            }
        }
    }

    /**
     * toggle live mode
     */
    function toggleLiveMode(valueParam) {
        var items = trendDisplay.items.items,
            valueF = valueParam || -1;

        if (items) {
            var liveMode = getIM().getItem(items.live.name),
                handleTime = getIM().getItem(items.handleTime.name),
                compContinuous = trendDisplay.getComponents("trend-display-continuous")[0];
            // live time
            if (valueF === -1 || items.live.lastValue === 0 /* && trendDisplay.isActive()*/) {
                liveMode.writeValue(valueF);
                if (compContinuous && compContinuous.trend) {
                    compContinuous.trend.running = true;
                }
                // Bugfix: handle bar has only old values and will not display values without timeout
                setTimeout(function() {
                    var timestamp = new Date().getTime() / 1000,
                        timeOffset = trendDisplay.element.getAttribute('class').match(/(?:^|\s)rh-trend-display(?!\S)/g) ? -11 : 0, // offset due to invisible area for RH
                        newTime = parseInt(timestamp) + (timeOffset),
                        dateTime = new Date(newTime * 1000);

                    handleTime.writeValue(newTime);
                    shmi.log("[UserAction] trend-display - toggleLiveMode: " + timeOffset + " min offset due to invisible area for RH " + dateTime.getHours() + ':' + dateTime.getMinutes(), 1);
                }, 250);
            } else {
                liveMode.writeValue(0);
                if (compContinuous && compContinuous.trend) {
                    compContinuous.trend.running = false;
                }
            }
        }
    }

    /**
     * jump to timestamp (saved by customer/system)
     */
    function showTimestamp(valueParam) {
        var items = trendDisplay.items.items,
            valueF = valueParam || -1;

        // timestamps
        var timestamps = shmi.visuals.controls.TrendDisplay.CanvasBackground.getTimestamps();

        if (items && timestamps) {
            var liveMode = getIM().getItem(items.live.name),
                leftTime = getIM().getItem(items.leftTime.name),
                handleTime = getIM().getItem(items.handleTime.name),
                handleTimeDate = new Date(parseInt(handleTime.value) * 1000);

            // deactivate live mode
            liveMode.writeValue(0);

            var dates = [];
            timestamps.forEach(function(ts) {
                dates.push(new Date(parseInt(ts) * 1000));
            });

            dates.sort(function(a, b) {
                var closestPrev = Math.abs(handleTimeDate - a);
                var closestNext = Math.abs(handleTimeDate - b);
                return closestPrev - closestNext; // sort a before b when the distance is smaller
            });

            // handle time identifies which is the closest previous/next timestamp
            var timestamp = dates.filter(
                function(date) {
                    return valueF === -1 ? date - handleTimeDate < 0 : date - handleTimeDate > 0;
                });

            // timestamp available
            if (timestamp && timestamp.length) {
                timestamp = timestamp[0].getTime() / 1000;

                // jump to timestamp if it is not in current visible view
                if (valueF === -1 ? leftTime.value >= timestamp : leftTime.value <= timestamp) {
                    // check with zoom level and show more on left side
                    var offset = /*zoomX.value < 100 ? 2000 :*/ 500;
                    leftTime.writeValue(timestamp - offset);
                }
                handleTime.writeValue(timestamp);
            }
        }
    }
};

/**
 * Trend Legend
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "trend-legend",
 *     "name": null,
 *     "trend-display": null,
 *     "template": "default/trend-legend"
 * }
 *
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 * trend-display {string}: Trend display name.
 *
 */
(function() {
    'use strict';

    //variables for reference in control definition
    var className = "TrendLegend", //control name in camel-case
        uiType = "trend-legend", //control keyword (data-ui)
        isContainer = true,
        USE_LABEL = 0,
        USE_ITEM_NAME = 1;

    //example - default configuration
    var defConfig = {
        "class-name": uiType,
        "name": uiType,
        "trend-display": null,
        "template": "default/trend-legend",
        "label-type": USE_LABEL
    };

    //setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    //declare private functions - START

    /**
     * reloadTrendDisplay - update trend-display control with new configuration
     *
     * @param {object} trendControl trend-display control instance
     * @param {object} trendConfig  trend-display configuration
     * @param {object} self         local-script instance reference
     *
     * @returns {undefined}
     */
    function reloadTrendDisplay(trendControl, trendConfig, self) {
        if (self.locked) { // do not reload trend display in designer
            return;
        }

        //clear settings stored in config object by trend-display (some will cause validation to fail)
        delete trendConfig.trend.signals;
        delete trendConfig.styleManagers;
        delete trendConfig.viewport;
        delete trendConfig["config-name"];
        delete trendConfig.display.units.background;
        delete trendConfig.display.units.ruler;
        delete shmi.visuals.session.TrendManager["$$trendNames"];

        self.lock();
        //update trend-display configuration
        trendControl.config.update(trendConfig, true).then(function() {
            self.unlock();
        });
    }

    /**
     * highlightTrendSignal - highlight trend-display curve matching specified signal
     * index.
     *
     * @param {number} signalIndex curve signal index
     * @param {object} self        local-script instance reference
     *
     * @returns {boolean} `true` if setting was applied, `false` else
     */
    function highlightTrendSignal(signalIndex, self) {
        var trendControl = self.vars.trendDisplay,
            trendConfig = null;

        if (trendControl) {
            trendConfig = shmi.cloneObject(trendControl.config);
            trendConfig.display.highlightSignal = signalIndex;
            log("highlighted signal:", signalIndex);

            reloadTrendDisplay(trendControl, trendConfig, self);
            return true;
        } else {
            log("trend-display control not resolved:", self.config.trendControl);
            return false;
        }
    }

    /**
     * createEntryElement - create DIV element for single legend entry
     *
     * @param {object} data entry settings
     * @param {object} self local-script instance reference
     *
     * @returns {HTMLElement} DIV element for legend entry
     */
    function createEntryElement(data, self) {
        var entryHTML,
            entryElement = document.createElement("DIV"),
            td = self.vars.trendDisplay,
            iter = shmi.requires("visuals.tools.iterate.iterateObject"),
            defaultData = {
                "labelLetter": "?",
                "color": "#333333",
                "value": "",
                "signalLabel": "[LABEL]",
                "signalUnit": "[UNIT]",
                "signalIndex": -1000
            };

        iter(defaultData, function(val, prop) {
            if (data[prop] === undefined) {
                data[prop] = defaultData[prop];
            }
        });

        entryElement.setAttribute("data-ui", "trend-legend-entry");
        entryElement.classList.add("trend-legend-entry");
        entryElement.dataset.signalIndex = data.signalIndex;
        entryHTML =
            '<div data-ui="scale-id" class="scale-id"><div class="scale-letter">' + (data.labelLetter || "&nbsp;") + '</div></div>' +
            '<div data-ui="trend-style" class="trend-style"' + //border-style: dotted; TODO add line style here later
            ' style="border-style: solid; border-color: ' + data.color + '; border-width: 2px;"></div>' +
            '<div data-ui="trend-item-name" class="trend-item-name" title="' + data.signalLabel + '">' + data.signalLabel + '</div>' +
            '<div data-ui="trend-item-unit" class="trend-item-unit">' + shmi.localize(data.signalUnit) + '</div>' +
            '<div data-ui="trend-item-value" class="trend-item-value">' + formatItemsValue(data.value, data.signalName, self) + '</div>';

        entryElement.innerHTML = entryHTML;

        if (td && td.config.display && td.config.display.highlightSignal === data.signalIndex) {
            shmi.addClass(entryElement, "selected");
            shmi.addClass(self.element, "highlight-mode");
        }

        return entryElement;
    }

    /**
     * renderLegend - create or update current state of trend-display curves in legend
     *
     * @param {object} self local-script instance reference
     *
     * @returns {undefined}
     */
    function renderLegend(self) {
        var td, curves,
            io = shmi.requires("visuals.io"),
            im = self.imports.im,
            ihA = im.getItemHandler(),
            itemToken = null,
            signals;

        td = self.vars.trendDisplay;
        if (!td && self.config["trend-display"]) {
            td = self.vars.trendDisplay = shmi.ctrl(self.config["trend-display"]);
        }

        if (!td || !td.config || !td.config.trend) {
            console.log("TrendDisplay is not defined!");
            return;
        }

        signals = td.config.trend.signals || [];

        self.element.innerHTML = "";

        curves = td.items.items.curves;
        self.vars.curveMap = {};
        signals.forEach(function(signal) {
            var entryElement = document.createElement("DIV"),
                ml, // MouseListener
                tl, // TouchListener
                clickHandler,
                color,
                value,
                labelLetter = signal.labelLetter,
                signalName,
                curveName,
                data;

            if (curves && curves[signal] && curves[signal.name] && curves[signal.name].value) {
                signalName = signal.name;
            } else if (curves && curves[signal.alias] && curves[signal.alias].value) {
                signalName = signal.alias;
            } else {
                return;
            }
            curveName = curves[signalName].value.name;
            self.vars.curveMap[curveName] = signalName;
            value = curves[signalName].value.lastValue;

            if (td.config.styles[td.config.trend.items[signal.itemIndex].styleIndex]) {
                color = td.config.styles[td.config.trend.items[signal.itemIndex].styleIndex].color;
            } else if (td.config.trend.items[signal.itemIndex].style.color) {
                color = td.config.trend.items[signal.itemIndex].style.color;
            } else {
                console.log("Trend Item color is not defined?");
                return;
            }

            var label = "";
            if (self.config["label-type"] === USE_ITEM_NAME) {
                label = signal.alias || signal.name;
            } else { // USE_LABEL
                label = shmi.localize(signal.label);
            }
            data = {
                "labelLetter": labelLetter,
                "color": color,
                "value": value,
                "signalLabel": label,
                "signalUnit": signal.unit,
                "signalIndex": signal.valueIndex,
                "signalName": signal.name
            };
            entryElement = createEntryElement(data, self);

            self.vars.entryElements.push(entryElement);
            self.element.appendChild(entryElement);

            itemToken = im.subscribeItem(curveName, ihA);
            self.vars.tokens.push(itemToken);
            if (!self.vars.lastValueElements[curveName]) {
                self.vars.lastValueElements[curveName] = [];
            }
            self.vars.lastValueElements[curveName].push(shmi.getUiElement("trend-item-value", entryElement));

            clickHandler = {
                onClick: function(x, y, e) {
                    var selectedEls = document.getElementsByClassName("trend-legend-entry selected", self.element) || [],
                        deselectOnly = shmi.hasClass(entryElement, "selected"),
                        signalIndex = null;

                    shmi.removeClass(self.element, "highlight-mode");
                    [].forEach.call(selectedEls, function(el) {
                        shmi.removeClass(el, "selected");
                    });

                    if (!deselectOnly) {
                        shmi.addClass(entryElement, "selected");
                        signalIndex = Number(entryElement.dataset.signalIndex);
                    }

                    highlightTrendSignal(signalIndex, self);
                }
            };
            ml = new io.MouseListener(entryElement, clickHandler);
            tl = new io.TouchListener(entryElement, clickHandler);
            self.vars.listeners.push(ml, tl);
        });

        ihA.setValue = function(value, type, name) {
            if (name in self.vars.lastValueElements) {
                self.vars.lastValueElements[name].forEach(function(lastValueElement) {
                    lastValueElement.textContent = formatItemsValue(value, name, self);
                });
            }
        };

        if (!self.locked) {
            self.vars.listeners.forEach(function(l) {
                l.enable();
            });
        }
    }

    /**
     * formatItemsValue - format recorded value according to item properties.
     *
     * @param {number} value    current value
     * @param {string} itemName name of displayed item
     * @param {object} self     local-script instance reference
     *
     * @returns {string} formatted value
     */
    function formatItemsValue(value, itemName, self) {
        var nv = shmi.requires("visuals.tools.numericValues"),
            uc = shmi.visuals.tools.unitClasses,
            adapter = null,
            itemInfo = null;

        itemName = self.vars.curveMap[itemName] ? self.vars.curveMap[itemName] : itemName;

        if (self.vars.trendInfo && self.vars.trendInfo.items) {
            self.vars.trendInfo.items.forEach(function(trendItem) {
                if (trendItem.name === itemName) {
                    itemInfo = trendItem;
                }
            });
        }

        if (itemInfo) {
            if (itemInfo.unitClass) {
                adapter = uc.getSelectedAdapter(itemInfo.unitClass);
                if (adapter) {
                    value = adapter.outFunction(value);
                }
            }
            value = nv.formatOutput(value, {
                vars: {
                    valueSettings: { // vItem does not have correct "digits", "min" and "max" fields
                        min: itemInfo.min,
                        max: itemInfo.max,
                        step: 0,
                        precision: (itemInfo.digits === null) ? -1 : itemInfo.digits,
                        type: shmi.c("TYPE_FLOAT")
                    }
                }
            });
        }

        return value;
    }

    /**
     * disable - called when control is disabled
     *
     * @param {object} self local-script instance reference
     *
     * @returns {undefined}
     */
    function disable(self) {
        self.vars.tokens.forEach(function(t) {
            t.unlisten();
        });
        self.vars.tokens = [];
        self.vars.listeners.forEach(function(l) {
            l.disable();
        });
        self.vars.listeners = [];
        self.vars.entryElements = [];
        self.vars.lastValueElements = {};
        self.element.innerHTML = "";
        self.vars.trendDisplay = null;
    }

    /**
     * lock - called when control is locked
     *
     * @param {object} self  local-script instance reference
     * @param {boolean} force `true` when control should be locked until unlocked with `force`
     *
     * @returns {undefined}
     */
    function lock(self, force) {
        self.locked = true;
        self.vars.listeners.forEach(function(l) {
            l.disable();
        });
        self.controls.forEach(function(ctrl) {
            ctrl.lock(force);
        });
    }

    /**
     * unlock - called when control is unlocked
     *
     * @param {object} self  local-script instance reference
     * @param {boolean} force `true` when control should be unlocked from force-locked state
     *
     * @returns {undefined}
     */
    function unlock(self, force) {
        self.locked = false;
        self.controls.forEach(function(ctrl) {
            ctrl.unlock(force);
        });
        self.vars.listeners.forEach(function(l) {
            l.enable();
        });
    }

    /**
     * onLabelsReady - called when trend-display instance labels are generated
     *
     * @param {object} self control instance reference
     *
     * @returns {undefined}
     */
    function onLabelsReady(self) {
        renderLegend(self);
        self.vars.listeners.forEach(function(l) {
            l.enable();
        });
        if (self.locked) {
            lock(self, true);
        }
    }

    //declare private functions - END

    //definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            trendDisplay: null,
            trendInfo: null,
            curves: {},
            selectedCurve: null,
            entryElements: [],
            lastValueElements: {},
            listeners: [],
            tokens: [],
            curveMap: {}
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            /* example - add import via function call */
            qm: function() {
                return shmi.visuals.session.QueryManager;
            }
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this,
                    entryElement = createEntryElement({}, self);

                self.element.appendChild(entryElement);
            },
            /* called when control is enabled */
            onEnable: function() {
                var self = this,
                    tm = shmi.visuals.session.TrendManager;

                if (!self.config["trend-display"]) {
                    return;
                }

                shmi.onReady({
                    controls: {
                        trendControl: self.config["trend-display"]
                    },
                    items: {},
                    resources: {}
                }, function(resolved) {
                    var td = resolved.controls.trendControl,
                        listenerToken = null;

                    self.vars.trendDisplay = td;

                    if (td && td.config && td.config.trend && td.config.trend.name) {
                        tm.getTrendInfo(td.config.trend.name, function(trends) {
                            self.vars.trendInfo = trends;
                        });
                    }

                    listenerToken = shmi.listen("labels-ready", onLabelsReady.bind(null, self), {
                        "detail.name": td.getName()
                    });
                    self.vars.tokens.push(listenerToken);

                    if (td._components &&
                        td._components["trend-display-scale"] &&
                        td._components["trend-display-scale"][0] &&
                        td._components["trend-display-scale"][0].labelsReady) {
                        onLabelsReady(self);
                    }
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                var self = this;
                disable(self);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                lock(self, true);
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                unlock(self, true);
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * Trend Selector
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "test-control",
 *     "name": null,
 *     "template": "custom/test-control"
 * }
 *
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 */
(function() {
    'use strict';

    //variables for reference in control definition
    var className = "TrendSelect"; //control name in camel-case
    var uiType = "trend-select"; //control keyword (data-ui)
    var isContainer = true;

    var MAX_TREND_ITEMS = 10,
        USE_LABEL = 0,
        KEEP_ITEMS_FROM_THE_LAST_PAGE = 5; // how many items of the last page wil be shown after the page was changed

    //example - default configuration
    var defConfig = {
        "class-name": uiType,
        "name": null,
        "template": "default/trend-select",
        "label": uiType,
        "trend-display": null,
        "pager": {
            "per-page": 10000
        },
        "label-type": USE_LABEL
    };

    //setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    //declare private functions - START

    /**
     * lock - called when control is locked
     *
     * @param  {object} self  local-script instance reference
     * @param  {boolean} force `true` to lock until unlocked with `force` := `true`
     * @returns {undefined}
     */
    function lock(self, force) {
        self.locked = true;
        self.controls.forEach(function(ctrl) {
            ctrl.lock(force);
        });
    }

    /**
     * unlock - called when control is unlocked
     *
     * @param  {object} self  local-script instance reference
     * @param  {boolean} force `true` to unlock control locked with `force` := `true`
     * @returns {undefined}
     */
    function unlock(self, force) {
        self.locked = false;
        self.controls.forEach(function(ctrl) {
            ctrl.unlock(force);
        });
    }

    /**
     * enable - called when control is enabled
     *
     * @param  {object} self local-script instance reference
     * @returns {undefined}
     */
    function enable(self) {
        shmi.onReady({ // wait for trend display to get the trend info
            controls: {
                trendControl: self.config["trend-display"]
            }
        }, function(resolved) {
            if (!self.isActive()) {
                return; //cancel when control is no longer active
            }
            self.vars.trendDisplay = resolved.controls.trendControl;
            self.vars.trendName = (self.vars.trendDisplay.config.trend) ? self.vars.trendDisplay.config.trend.name : "";

            getTrendStyles(self);

            self.imports.tm.getTrendInfo(self.vars.trendName, function(td) {
                if (!self.isActive()) {
                    return; //cancel when control is no longer active
                }
                self.vars.trendInfo = td;
                log("trend info set:", td);

                if (self.vars.trendInfo && self.vars.trendInfo.items) {
                    self.vars.curDataSet = {
                        curStartPos: 0,
                        curFilter: "",
                        visibleItems: self.vars.trendInfo.items
                    };
                }

                // prepare empty option list
                prepareListElements(self);

                shmi.onReady({ // wait for the option list controls and fill them with data
                    controls: self.vars.listElementNames
                }, function(innerResolved) {
                    if (!self.isActive()) {
                        return; //cancel when control is no longer active
                    }
                    updateListElements(self.vars.trendInfo.items, self);

                    onTrendDisplayReady(self);

                    var listenerToken = shmi.listen("enable", function(evt) { // if trend display was reloaded update it
                        self.vars.trendDisplay = evt.source;
                    }, { "detail.name": self.config["trend-display"] });
                    self.vars.tokens.push(listenerToken);

                    if (self.vars.optionListAnchor) {
                        self.vars.optionListAnchor.addEventListener("scroll", self.vars.scrollListener);
                    }
                });
            });
        });
    }

    /**
     * disable - called when control is disabled
     *
     * @param  {object} self local-script instance reference
     * @returns {undefined}
     */
    function disable(self) {
        self.vars.tokens.forEach(function(t) {
            t.unlisten();
        });
        self.vars.tokens = [];
        if (self.vars.optionListAnchor) {
            self.vars.optionListAnchor.removeEventListener("scroll", self.vars.scrollListener);
        }
        self.vars.boxes = {};
        self.vars.trendDisplay = null;
    }

    /**
     * applyInputFilter - apply filter for displayed items
     *
     * @param  {object} self         local-script instance reference
     * @param  {string} filterString string from filter input
     * @returns {undefined}
     */
    function applyInputFilter(self, filterString) {
        self.vars.curDataSet = {
            curStartPos: 0,
            curFilter: filterString,
            visibleItems: []
        };

        if (self.vars.trendInfo && self.vars.trendInfo.items) {
            var visibleItems = [];

            self.vars.trendInfo.items.forEach(function(itemInfo) {
                var label = getItemLabel(self, itemInfo.name);

                if (String(label).toLowerCase().indexOf(String(filterString).toLowerCase()) !== -1) {
                    visibleItems.push(itemInfo);
                }
            });
            self.vars.curDataSet.visibleItems = visibleItems;
            updateListElements(visibleItems, self);
        } else {
            console.error(className, "trend info not loaded yet");
        }
    }

    /**
     * getItemLabel - get label of specified item
     *
     * @param {object} self control instance reference
     * @param {string} itemName item name
     *
     * @returns {string} label text for item
     */
    function getItemLabel(self, itemName) {
        var label = itemName,
            itemRef = null,
            useLabel = (self.config["label-type"] === USE_LABEL);

        if (useLabel && self.vars.trendInfo && Array.isArray(self.vars.trendInfo.items)) {
            self.vars.trendInfo.items.some(function(itemInfo) {
                if (itemInfo.name === itemName) {
                    itemRef = itemInfo;
                    return true;
                }
                return false;
            });
            if (itemRef && typeof itemRef.label === "string") {
                label = shmi.localize(itemRef.label);
            }
        }

        return label;
    }

    /**
     * getTrendStyles - get available line styles from trend-display
     *
     * @param  {object} self local-script instance reference
     * @returns {undefined}
     */
    function getTrendStyles(self) {
        var styles = [];
        if (self.vars.trendDisplay && self.vars.trendDisplay.config.styles) {
            styles = self.vars.trendDisplay.config.styles;
        }
        styles.forEach(function(style, index) {
            self.vars.colors[index] = style;
            self.vars.colors[index].value = index;
        });
    }

    /**
     * prepareListHeader - create item list header elements
     *
     * @param  {object} self local-script instance reference
     * @returns {undefined}
     */
    function prepareListHeader(self) {
        var listHeader = document.createElement("div");

        shmi.addClass(listHeader, "list-header");

        shmi.createControl("text2", listHeader, {
            "text": " ",
            "class-name": "col1 ctrl text2"
        }, 'DIV', null);
        shmi.createControl("text2", listHeader, {
            "text": "${trend-select-value}",
            "class-name": "col2 ctrl text2"
        }, 'DIV', null);
        shmi.createControl("text2", listHeader, {
            "text": "${trend-select-unit}",
            "class-name": "col3 ctrl text2"
        }, 'DIV', null);
        shmi.createControl("text2", listHeader, {
            "text": "${trend-select-graph}",
            "class-name": "col4 ctrl text2"
        }, 'DIV', null);
        shmi.createControl("text2", listHeader, {
            "text": "${trend-select-scale}",
            "class-name": "col5 ctrl text2"
        }, 'DIV', null);

        self.vars.optionList.insertAdjacentElement("afterbegin", listHeader);
    }

    /**
     * prepareListElements - create item list elements
     *
     * @param  {object} self local-script instance reference
     * @returns {undefined}
     */
    function prepareListElements(self) {
        var defaultListElementConfig = {
                "template": "default/trend-select/list-element",
                "class-name": "group list-element",
                "@select-checkbox-config": {
                    "item": ""
                },
                "@var-name-config": {
                    "_text": "[VAR_NAME]"
                },
                "@unit-config": {
                    "text": "UNIT"
                },
                "@button-config": {
                    "trendItem": null,
                    "_action": "dialog-box:.graph-style-dialog:show"
                },
                "@scale-checkbox-config": {
                }
            },
            listControl,
            perPage = self.config.pager["per-page"],
            config,
            trendInfoitems = self.vars.trendInfo.items;
        if (!trendInfoitems.length) {
            perPage = 0;
        } else if (trendInfoitems.length < perPage) {
            perPage = trendInfoitems.length;
        }

        for (var listElementIndex = 0; listElementIndex < perPage; listElementIndex++) {
            config = shmi.cloneObject(defaultListElementConfig);
            config["name"] = "list-element_" + listElementIndex;
            config["@select-checkbox-config"].name = "select-list-element";
            config["@var-name-config"].name = "name-list-element";
            config["@unit-config"].name = "unit-list-element";
            config["@button-config"].name = "button-list-element";
            config["@scale-checkbox-config"].name = "scale-list-element";
            listControl = shmi.createControl("group", self.vars.optionListAnchor, config, 'DIV', null);
            shmi.addClass(listControl.element, "hidden");

            self.vars.listElements.push(listControl);
            self.vars.listElementNames["listElement_" + listElementIndex] = self.getName() + " .list-element_" + listElementIndex;
        }
    }

    /**
     * updateListElements - Fills the list elements with the items from the `items` array
     *
     * @param {array} items array of item info objects
     * @param {object} self local-script instance reference
     *
     * @returns {undefined}
     */
    function updateListElements(items, self) {
        self.vars.trendItemsPosByName = [];
        self.vars.listElements.forEach(function(listControl, listElementIndex) {
            var nameControl = shmi.ctrl(listControl.getName() + ".name-list-element"),
                unitControl = shmi.ctrl(listControl.getName() + ".unit-list-element"),
                checkboxControl = shmi.ctrl(listControl.getName() + " .select-list-element"),
                buttonControl = shmi.ctrl(listControl.getName() + " .button-list-element"),
                lineStyleEl = shmi.getElementByName("line-style", listControl.element),
                scaleCheckboxControl = shmi.ctrl(listControl.getName() + " .scale-list-element"),
                itemInfo = items[listElementIndex] ? items[listElementIndex] : null,
                label = "",
                unit = itemInfo ? itemInfo.unit : "";
            listControl.itemName = itemInfo ? getItemName(itemInfo) : "";
            listControl.listElementIndex = listElementIndex;
            buttonControl.config.trendItem = null;

            lineStyleEl.style.borderColor = "#000000";
            lineStyleEl.style.borderWidth = "1px";

            self.vars.disableChangeListener = true;
            if (itemInfo) {
                // to find the item position by item name
                self.vars.trendItemsPosByName[getItemName(itemInfo)] = listElementIndex;

                label = getItemLabel(self, itemInfo.name);

                var itemInUse = self.vars.trendDisplayItemNames.indexOf(itemInfo.name) !== -1;
                if (checkboxControl.value !== itemInUse) {
                    checkboxControl.setValue(itemInUse);
                }

                shmi.removeClass(listControl.element, "hidden");
            } else {
                shmi.addClass(listControl.element, "hidden");
            }

            nameControl.element.title = label;
            nameControl.setValue(label);
            unitControl.setValue(shmi.localize(unit));
            self.vars.disableChangeListener = false;

            if (!self.vars.boxes[listElementIndex]) {
                self.vars.boxes[listElementIndex] = checkboxControl;

                var cbTok = checkboxControl.listen("change", function(evt) {
                    var disableChangeListener,
                        itemName = evt.source.parentContainer.itemName,
                        innerListElementIndex = evt.source.parentContainer.listElementIndex,
                        itemConfig = {};
                    if (self.vars.disableChangeListener) {
                        return; //don't handle events caused by applyTrendSettings function
                    }
                    if ((self.vars.trendDisplayItemNames.indexOf(itemName) !== -1) !== (evt.detail.value === 0)) {
                        return; // Nothing to update - trend item already added
                    }
                    self.vars.disableChangeListener = true;
                    if (evt.detail.value === 1) {
                        itemConfig.scaleVisible = false;

                        if (!addTrendItem(self, itemName, itemConfig)) {
                            disableChangeListener = self.vars.disableChangeListener;
                            self.vars.boxes[innerListElementIndex].setValue(0);
                            self.vars.disableChangeListener = disableChangeListener;
                        } else {
                            scaleCheckboxControl.setValue(itemConfig.scaleVisible);
                        }
                    } else if (!removeTrendItem(self, itemName)) {
                        disableChangeListener = self.vars.disableChangeListener;
                        self.vars.boxes[innerListElementIndex].setValue(1);
                        self.vars.disableChangeListener = disableChangeListener;
                    } else {
                        scaleCheckboxControl.setValue(0);
                    }
                    self.vars.disableChangeListener = false;
                });
                self.vars.tokens.push(cbTok);

                self.vars.tokens.push(buttonControl.listen("click", function(evt) {
                    if (!evt.source.config.trendItem) {
                        console.log("Cannot set colour. Lane is inactive.");
                        return;
                    }
                    var selectRadio = self.vars.colorSelect,
                        item = evt.source.config.trendItem,
                        itemName, titleElement;
                    if (self.vars.dialogBox) {
                        self.vars.dialogBox.show();
                        selectRadio.setValue(item.styleIndex);

                        self.vars.dialogBox.config["current-color-index"] = item.styleIndex;
                        itemName = getItemName(item);
                        self.vars.dialogBox.config["item-name"] = itemName;
                        titleElement = shmi.getUiElement("dialog-box-title", self.vars.dialogBox.element);
                        if (titleElement) {
                            titleElement.textContent = getItemLabel(self, itemName);
                        }
                    }
                }));

                var scaleCbTok = scaleCheckboxControl.listen("change", function(evt) {
                    var itemName = evt.source.parentContainer.itemName,
                        visibility = 0,
                        disableChangeListener = self.vars.disableChangeListener;
                    if (disableChangeListener) {
                        return;
                    }
                    if (evt.detail.value === 1) {
                        visibility = 1;
                    }
                    if (!setScaleVisibility(self, itemName, visibility)) {
                        self.vars.disableChangeListener = true;
                        scaleCheckboxControl.setValue(0);
                        self.vars.disableChangeListener = false;
                    }
                });
                self.vars.tokens.push(scaleCbTok);
            }
        });

        setItemsToListElements(self);
    }

    /**
     * getItemName - get string to display in list element
     *
     * @param  {object} itemInfo item info object
     * @returns {string}          string displayed in list
     */
    function getItemName(itemInfo) {
        return itemInfo.alias || itemInfo.name;
    }

    /**
     * setItemsToListElements - Adds data (checkbox, style,..) from the selected trend items to the list elements
     *
     * @param  {object} self local-script instance reference
     * @returns {undefined}
     */
    function setItemsToListElements(self) {
        var usedStyleIndices = [], color,
            items = [],
            styles = [],
            config = {};
        if (self.vars.trendDisplay && self.vars.trendDisplay.config) {
            config = self.vars.trendDisplay.config;
        }
        if (config.trend && config.trend.items) {
            items = config.trend.items;
        }
        if (config.styles) {
            styles = config.styles;
        }
        self.vars.disableChangeListener = true;
        items.forEach(function(item) {
            var itemName = getItemName(item),
                listPosition = self.vars.trendItemsPosByName[itemName],
                checkbox, button, scalecheckbox, value;
            // find the lane's color by style index
            if (styles[item.styleIndex]) {
                color = styles[item.styleIndex].color;
            }
            // paint the style element
            if (listPosition !== undefined) {
                var lineStyleEl = shmi.getElementByName("line-style", self.vars.listElements[listPosition].element);
                lineStyleEl.style.borderColor = color;
                lineStyleEl.style.borderWidth = "3px";
            }

            if (self.vars.listElements[listPosition]) {
                // set check box
                checkbox = shmi.ctrl(self.vars.listElements[listPosition].getName() + " .select-list-element");
                checkbox.setValue(1);

                // to see on click which item to use
                button = shmi.ctrl(self.vars.listElements[listPosition].getName() + " .button-list-element");
                button.config.trendItem = item;

                // set scale check box
                scalecheckbox = shmi.ctrl(self.vars.listElements[listPosition].getName() + " .scale-list-element");
                // true vs 0
                if (Boolean(item.scaleVisible) !== Boolean(scalecheckbox.getValue())) {
                    value = (item.scaleVisible) ? 1 : 0;
                    scalecheckbox.setValue(value);
                }
            }

            // remove style index from the unused list
            usedStyleIndices.push(item.styleIndex);

            self.vars.trendDisplayItemNames.push(itemName);
        });

        self.vars.disableChangeListener = false;
        for (var i = 0; i < styles.length; i++) {
            if (usedStyleIndices.indexOf(i) === -1) {
                self.vars.availableStyleIndices.push(i);
            }
        }
    }

    /**
     * onTrendDisplayReady - init & activate child controls when trend-display is ready
     *
     * @param  {object} self local-script instance reference
     * @returns {undefined}
     */
    function onTrendDisplayReady(self) {
        var changeTok;

        setItemsToListElements(self);

        changeTok = self.vars.inputField.listen("change", function(evt) {
            log("changed", evt.detail.value);
            applyInputFilter(self, evt.detail.value);
        });
        self.vars.tokens.push(changeTok);

        prepareDialogBox(self);

        self.controls.forEach(function(c) {
            c.enable();
        });

        if (self.locked) {
            lock(self, true);
        }
    }

    /**
     * reloadTrendDisplay - reload trend display with updated configuration
     *
     * @param  {object} trendConfig trend-display configuration to apply
     * @param  {object} self        local-script instance reference
     * @returns {undefined}
     */
    function reloadTrendDisplay(trendConfig, self) {
        if (self.locked) { // do not reload trend display in designer
            return;
        }

        delete trendConfig.trend.signals;
        delete trendConfig.styleManagers;
        delete trendConfig.viewport;
        delete trendConfig["config-name"];
        delete trendConfig.display.units.background;
        delete trendConfig.display.units.ruler;
        delete shmi.visuals.session.TrendManager["$$trendNames"];

        self.vars.trendDisplayItemNames = [];

        // set proper signalIndex on item info
        trendConfig.trend.items.forEach(function(item, index) {
            item.signalIndex = index;

            self.vars.trendDisplayItemNames.push(getItemName(item));
        });
        // do not recalibrate viewport
        self.vars.trendDisplay.config.update(trendConfig, true).then(function() {
            console.log("TREND_DISPLAY_UPDATE - TrendSelect", arguments);
        });
    }

    /**
     * addTrendItem - add item to list of items displayed in trend-display
     *
     * @param {object} self local-script instance reference
     * @param {string} itemName name of item to add
     * @param {object|null|undefined} config - the values you want to add to the item config
     * @returns {boolean} `true` if item was added, `false` else
     */
    function addTrendItem(self, itemName, config) {
        var iter = shmi.requires("visuals.tools.iterate.iterateObject"),
            trendConfig = null,
            nextIndex = -1,
            itemConfig = {
                "alias": itemName,
                "label": "label",
                "styleIndex": null,
                "visible": true,
                "scaleVisible": false
            },
            itemExists = false;
        if (config) {
            iter(config, function(val, prop) {
                itemConfig[prop] = val;
            });
        }
        self.vars.trendDisplay = shmi.ctrl(self.config["trend-display"]);

        if (self.vars.trendDisplay) {
            trendConfig = shmi.cloneObject(self.vars.trendDisplay.config);

            trendConfig.trend.items.forEach(function(item) {
                itemExists = (item.alias === itemName) || itemExists;
            });
            if (itemExists) {
                log("item already selected");
                return true;
            }

            nextIndex = trendConfig.trend.items.length;
            if (nextIndex >= MAX_TREND_ITEMS) {
                shmi.notify("${trend-select.notification.max-curves}", "${V_NOTIFICATION}", { VALUE: MAX_TREND_ITEMS });
                log("too many items selected");
                return false;
            }
            if (itemConfig.styleIndex === null) {
                itemConfig.styleIndex = self.vars.availableStyleIndices.shift();
            }
            itemConfig.signalIndex = nextIndex;
            itemConfig.laneIndex = itemConfig.signalIndex;
            trendConfig.trend.items.push(itemConfig);
            if (nextIndex === MAX_TREND_ITEMS) {
                iter(self.vars.boxes, function(boxControl) {
                    if (boxControl.getValue() !== 1) {
                        boxControl.lock();
                    }
                });
            }

            changeLineStyleElement(itemName, itemConfig, itemConfig.styleIndex, self);

            log("added trend item:", itemName);

            reloadTrendDisplay(trendConfig, self);
            return true;
        } else {
            log("trend-display control not resolved:", self.config["trend-display"]);
            return false;
        }
    }

    /**
     * removeTrendItem - remove item from list of items displayed in trend-display
     *
     * @param  {object} self     local-script instance reference
     * @param  {string} itemName name of item
     * @returns {boolean}          `true` if item was removed, `false` else
     */
    function removeTrendItem(self, itemName) {
        var iter = shmi.requires("visuals.tools.iterate.iterateObject"),
            trendConfig = null,
            itemIndex = -1;
        self.vars.trendDisplay = shmi.ctrl(self.config["trend-display"]);
        if (self.vars.trendDisplay) {
            trendConfig = shmi.cloneObject(self.vars.trendDisplay.config);

            if (trendConfig.trend.items.length < 2) {
                log("Last trend item:", trendConfig.trend.items);
                return false;
            }

            trendConfig.trend.items.forEach(function(itemInfo, idx) {
                if (getItemName(itemInfo) === itemName) {
                    itemIndex = idx;
                }
            });
            if (itemIndex !== -1) {
                self.vars.availableStyleIndices.push(trendConfig.trend.items[itemIndex].styleIndex);
                trendConfig.trend.items.splice(itemIndex, 1);
                iter(self.vars.boxes, function(boxControl) {
                    boxControl.unlock();
                });
                log("removed trend item:", itemName);

                changeLineStyleElement(itemName, null, null, self);

                reloadTrendDisplay(trendConfig, self);
            }
            return true;
        }
        return false;
    }

    /**
     * setScaleVisibility - set scale visibility
     *
     * @param  {object} self         local-script instance reference
     * @param  {string} itemName     name of item
     * @param  {boolean} scaleVisible visibility of scale
     * @returns {boolean}              `true` if visibility state was applied, `false` else
     */
    function setScaleVisibility(self, itemName, scaleVisible) {
        var trendConfig = null,
            itemToChange = null;
        self.vars.trendDisplay = shmi.ctrl(self.config["trend-display"]);

        if (self.vars.trendDisplay) {
            trendConfig = shmi.cloneObject(self.vars.trendDisplay.config);

            trendConfig.trend.items.some(function(item) {
                if (item.alias === itemName) {
                    itemToChange = item;
                    return true;
                }
                return false;
            });
            if (!itemToChange) {
                log("item not found");
                return false;
            }

            if (Boolean(itemToChange.scaleVisible) === Boolean(scaleVisible)) {
                log("scale visibility already set");
                return true;
            }

            itemToChange.scaleVisible = (scaleVisible === 1);

            reloadTrendDisplay(trendConfig, self);
            return true;
        } else {
            log("trend-display control not resolved:", self.config["trend-display"]);
            return false;
        }
    }

    /**
     * prepareDialogBox - create dialog for color choice
     *
     * @param  {object} self local-script instance reference
     * @returns {undefined}
     */
    function prepareDialogBox(self) {
        var config = {
            "name": "graph-style-dialog",
            "title": "${selected-trend-name}",
            "content-template": "default/trend-select/graph-style-dialog",
            "class-name": "dialog-box trend-select graph-style-dialog-box",
            "top-level": true,

            "@button-cancel-config": {
                "name": "button-cancel-config"
            },
            "@button-apply-config": {
                "name": "button-apply-config"
            }
        };
        if (!self.vars.dialogBox) {
            self.vars.dialogBox = shmi.createControl("dialog-box", self.vars.dialogBoxAnchor, config, 'DIV', null);
        }

        shmi.onActive([self.vars.dialogBox], function() {
            var anchor = shmi.getElementByName("select-radio-anchor", self.vars.dialogBox.element),
                selectReadyTok = null,
                buttonApply = shmi.ctrl(self.vars.dialogBox.getName() + " .button-apply-config"),
                buttonCancel = shmi.ctrl(self.vars.dialogBox.getName() + " .button-cancel-config");

            if (!self.vars.colorSelect) {
                self.vars.colorSelect = shmi.createControl("select-radio", anchor, {
                    "name": "color-selection",
                    "options": self.vars.colors,
                    "class-name": "select-radio no-label color-selection"
                }, 'DIV', null);
            }
            self.vars.tokens.push(buttonApply.listen("click", function(evt) {
                var btnCtrl = evt.source,
                    value, itemName, trendItem,
                    selectionCtrl = shmi.ctrl(btnCtrl.parentContainer.getName() + " .color-selection");
                itemName = btnCtrl.parentContainer.config["item-name"];
                var trendConfig = shmi.cloneObject(self.vars.trendDisplay.config);
                trendConfig.trend.items.forEach(function(item) {
                    if (item.alias === btnCtrl.parentContainer.config["item-name"]) {
                        trendItem = item;
                    }
                });

                value = Number(selectionCtrl.value);
                value = (value === null) ? -1 : value;
                if (!trendConfig.styles[value]) {
                    return;
                }
                if (selectionCtrl.value === btnCtrl.parentContainer.config["current-color-index"]) {
                    setTimeout(function() {
                        self.vars.dialogBox.hide();
                    }, 100);
                    return;
                }

                btnCtrl.parentContainer.config["current-color-index"] = value;

                changeLineStyleElement(itemName, trendItem, value, self);

                if (trendItem) {
                    trendItem.styleIndex = value;
                    trendItem.style = {};
                    reloadTrendDisplay(trendConfig, self);
                }

                addTrendItem(self, itemName, { "styleIndex": value });

                setTimeout(function() {
                    self.vars.dialogBox.hide();
                }, 100);
            }));
            self.vars.tokens.push(buttonCancel.listen("click", function(evt) {
                var btnCtrl = evt.source,
                    value,
                    selectionCtrl = shmi.ctrl(btnCtrl.parentContainer.getName() + " .color-selection");
                value = Number(btnCtrl.parentContainer.config["current-color-index"]);
                value = (value === null) ? -1 : value;
                selectionCtrl.setValue(value);
                // and close...
                self.vars.dialogBox.hide();
            }));

            selectReadyTok = self.vars.colorSelect.listen("enable", function(evt) {
                var options = shmi.getUiElements("option", self.vars.colorSelect.element),
                    option, value;
                selectReadyTok.unlisten();
                self.vars.tokens.splice(self.vars.tokens.indexOf(selectReadyTok), 1);
                for (var i = 0; i < options.length; i++) {
                    option = options[i];
                    value = option.dataset.value;
                    if (self.vars.colors[value]) {
                        option.style.backgroundColor = self.vars.colors[value].color;
                    }
                }
            });
            self.vars.tokens.push(selectReadyTok);
        });
    }

    /**
     * changeLineStyleElement - Sets style to the `line style element`
     *
     * @param {string} itemName name of item
     * @param {object|null} trendItem trend item info
     * @param {number|null} colorIndex index of line style
     * @param {object} self local-script instance reference
     *
     * @returns {undefined}
     */
    function changeLineStyleElement(itemName, trendItem, colorIndex, self) {
        var color = "#000000", borderWidth = "1px";
        var position = self.vars.trendItemsPosByName[itemName];
        var button = shmi.ctrl(self.vars.listElements[position].getName() + " .button-list-element");
        button.config.trendItem = trendItem;

        if (position !== undefined) {
            // find the lane's color by style index
            if (colorIndex !== null && self.vars.trendDisplay.config.styles[colorIndex]) {
                color = self.vars.trendDisplay.config.styles[colorIndex].color;
                borderWidth = "3px";
            }

            var lineStyleEl = shmi.getElementByName("line-style", self.vars.listElements[position].element);
            lineStyleEl.style.borderColor = color;
            lineStyleEl.style.borderWidth = borderWidth;
        }
    }

    //declare private functions - END

    //definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            dialogBox: null,
            colorSelect: null,
            disableChangeListener: null,
            inputAnchor: null,
            optionList: null,
            optionListAnchor: null,
            inputField: null,
            listElements: [],
            listElementNames: {},
            boxes: {},
            colors: [],
            availableStyleIndices: [],
            trendInfo: null,
            trendItemsPosByName: {}, // {"trendItemName": "listElementsIdx"}
            trendDisplay: null,
            trendName: null,
            tokens: [],
            scrollListener: null,
            trendDisplayItemNames: [], // names of the items used by trend display
            curDataSet: { // current set of the trend items
                curStartPos: 0,
                curFilter: "",
                visibleItems: [] // item, item
            },
            reloadTimeout: 0
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            tm: "visuals.session.TrendManager",
            /* example - add import via function call */
            qm: function() {
                return shmi.visuals.session.QueryManager;
            }
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {
                var self = this;

                self.vars.inputAnchor = shmi.getUiElement("input-anchor", self.element);
                self.vars.optionList = shmi.getUiElement("option-list", self.element);
                self.vars.optionListAnchor = shmi.getUiElement("option-list-anchor", self.element);
                self.vars.dialogBoxAnchor = shmi.getUiElement("dialog-box-anchor", self.element);

                var inputField = shmi.getUiElement("input-field", self.vars.inputAnchor);
                var config = {};
                self.vars.inputField = shmi.createControl("input-field", inputField, config, 'DIV', 'from');

                self.vars.scrollListener = function(event) {
                    var srcElement = event.target || event.srcElement,
                        keepItemsFromTheLastPage = KEEP_ITEMS_FROM_THE_LAST_PAGE,
                        perPage = self.config.pager["per-page"],
                        curDataSet = self.vars.curDataSet,
                        curLastPos = curDataSet.curStartPos + perPage,
                        itemsNum = curDataSet.visibleItems.length,
                        newStartPos = null,
                        newLastPos = null,
                        dataSet = null,
                        scrollTop = 0,
                        posEndOfThePage = ((srcElement.scrollHeight) / perPage) * (perPage - keepItemsFromTheLastPage),
                        curPerPage;

                    if (curLastPos >= itemsNum) {
                        curLastPos = itemsNum - 1;
                    }
                    curPerPage = curLastPos - curDataSet.curStartPos;

                    if ((srcElement.scrollTop === 0) && (curDataSet.curStartPos > 0)) {
                        newStartPos = curDataSet.curStartPos - (perPage - keepItemsFromTheLastPage);
                        newStartPos = (newStartPos > -1) ? newStartPos : 0;
                        newLastPos = newStartPos + perPage;
                        newLastPos = (newLastPos >= itemsNum) ? itemsNum : newLastPos;
                        dataSet = curDataSet.visibleItems.slice(newStartPos, newLastPos);

                        scrollTop = (srcElement.scrollHeight / curPerPage) * (dataSet.length - keepItemsFromTheLastPage);
                    } else if (srcElement.scrollTop > posEndOfThePage) {
                        newStartPos = curLastPos - keepItemsFromTheLastPage;
                        newLastPos = newStartPos + perPage;
                        if (newLastPos > itemsNum) {
                            newLastPos = itemsNum - 1;
                        }
                        dataSet = curDataSet.visibleItems.slice(newStartPos, newLastPos);

                        scrollTop = 1;
                    }

                    if (newStartPos !== null &&
                        newStartPos !== curDataSet.curStartPos &&
                        newLastPos !== curLastPos) {
                        curDataSet.curStartPos = newStartPos;
                        srcElement.scrollTop = scrollTop;
                        updateListElements(dataSet, self);
                    }
                };

                prepareListHeader(self);
            },
            /* called when control is enabled */
            onEnable: function() {
                var self = this;
                if (!self.config["trend-display"]) {
                    console.log("Trend-display ist not defined!");
                    return;
                }
                enable(self);
            },
            /* called when control is disabled */
            onDisable: function() {
                var self = this;
                disable(self);
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function(force) {
                var self = this;
                lock(self, force);
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function(force) {
                var self = this;
                unlock(self, force);
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * Trend Zoom, Calendar and Play/Pause
 *
 * Configuration options (default):
 *
 * {
 *     "class-name": "trend-zoom",
 *     "name": null,
 *     "trend-display": null,
 *     "template": "custom/trend-zoom"
 * }
 *
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 */
(function() {
    'use strict';

    //variables for reference in control definition
    var className = "TrendZoomModule", //control name in camel-case
        uiType = "trend-zoom", //control keyword (data-ui)
        isContainer = true;

    //example - default configuration
    var defConfig = {
        "class-name": uiType,
        "name": null,
        "trend-display": null,
        "template": "default/trend-zoom",
        "label": uiType
    };

    //setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires("visuals.tools.logging").createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    var timeUnits = {};

    //declare private functions - START

    /**
     * lock - called when control is locked
     *
     * @param  {object} self  local-script instance reference
     * @param  {boolean} force `true` to lock until unlocked with `force` := `true`
     * @return {undefined}
     */
    function lock(self, force) {
        self.controls.forEach(function(ctrl) {
            ctrl.lock(force);
        });
    }

    /**
     * unlock - called when control is unlocked
     *
     * @param  {object} self  local-script instance reference
     * @param  {boolean} force `true` to unlock control locked with `force` := `true`
     * @return {undefined}
     */
    function unlock(self, force) {
        self.controls.forEach(function(ctrl) {
            ctrl.unlock(force);
        });
    }

    /**
     * Creates the `action` string for the button control
     * @param {string} buttonName
     * @param {control} self
     *
     * @return {string} action
     */
    function generateButtonAction(buttonName, self) {
        var zooms = {
                "zoom24hButton": "percentIn24h",
                "zoom12hButton": "percentIn12h",
                "zoom8hButton": "percentIn8h",
                "zoom4hButton": "percentIn4h",
                "zoom2hButton": "percentIn2h",
                "zoom1hButton": "percentIn1h"
            },
            action;
        if (!zooms[buttonName] || !timeUnits[zooms[buttonName]]) {
            console.log("Unknown zoom or time unit for button name [" + buttonName + "].", zooms, timeUnits);
            return null;
        }
        action = {
            name: "trend-display",
            params: [
                self.config["trend-display"],
                "setZoomX",
                timeUnits[zooms[buttonName]]
            ]
        };

        return action;
    }

    /**
     * setLiveMode - active or deactivate live mode of trend-display
     *
     * @param  {number} mode `1` to activate live mode `0` to deactivate
     * @param  {object} self local-script instance reference
     * @return {undefined}
     */
    function setLiveMode(mode, self) {
        var core = shmi.requires("visuals.core"),
            actionDef = {
                name: "trend-display",
                params: [
                    self.config["trend-display"],
                    "toggleLiveMode",
                    mode
                ]
            },
            action;
        if (mode !== self.vars.liveMode) {
            action = new core.UiAction(actionDef);
            action.execute();
        }
    }

    /**
     * setLiveModeButtons - set state of buttons controlling live-mode
     *
     * @param  {object} self local-script instance reference
     * @return {undefined}
     */
    function setLiveModeButtons(self) {
        var liveMode = Number(self.vars.liveMode > 0);

        if (self.vars.controls.playButton && self.vars.controls.playButton[0]) {
            if (liveMode === 1) {
                shmi.addClass(self.vars.controls.playButton[0].element, "on");
            } else {
                shmi.removeClass(self.vars.controls.playButton[0].element, "on");
            }
        }
        if (self.vars.controls.pauseButton && self.vars.controls.pauseButton[0]) {
            if (liveMode !== 1) {
                shmi.addClass(self.vars.controls.pauseButton[0].element, "on");
            } else {
                shmi.removeClass(self.vars.controls.pauseButton[0].element, "on");
            }
        }
    }

    /**
     * initDateTimePicker - initialize date and time picker controls
     *
     * @param  {object} controls object containing control references
     * @param  {object} self     local-script instance reference
     * @return {undefined}
     */
    function initDateTimePicker(controls, self) {
        var selectDate = controls.selectDate,
            selectTime = controls.selectTime;

        self.vars.tokens.push(controls.doneButton.listen("click", function() {
            var date = selectDate.value,
                hours = selectTime.getHours(),
                minutes = selectTime.getMinutes(),
                seconds = selectTime.getSeconds(),
                dateTime = new Date(date * 1000),
                uiAction = null;

            dateTime.setHours(hours, minutes, seconds, 0);

            uiAction = new shmi.visuals.core.UiAction({
                name: "trend-display",
                params: [
                    self.config["trend-display"],
                    "handleTime",
                    dateTime.getTime()
                ]
            });
            uiAction.execute();
        }));
    }

    /**
     * create - initially create required child controls for trend-zoom
     *
     * @param  {object} self local-script instance reference
     * @return {undefined}
     */
    function create(self) {
        var selfName = self.getName(),
            elName = null,
            anchorElName = null,
            anchors = {
                "playButton": {
                    "anchorName": "play-button-anchor",
                    "controls": [
                        {
                            "ui_type": "button",
                            "cfg": {
                                "name": "play-button",
                                "template": "default/button_with_bg_pic",
                                "class-name": "icon-only no-frame play button"
                            },
                            "events": {
                                "click": "playLiveMode"
                            }
                        }
                    ]
                },
                "pauseButton": {
                    "anchorName": "pause-button-anchor",
                    "controls": [
                        {
                            "ui_type": "button",
                            "cfg": {
                                "name": "pause-button",
                                "template": "default/button_with_bg_pic",
                                "class-name": "icon-only no-frame pause button"
                            },
                            "events": {
                                "click": "pauseLiveMode"
                            }
                        }
                    ]
                },
                "zoomOutButton": {
                    "anchorName": "zoom-out-button-anchor",
                    "controls": [
                        {
                            "ui_type": "button",
                            "cfg": {
                                "name": "zoom-out-button",
                                "template": "default/button_with_bg_pic",
                                "action": {
                                    "name": "trend-display",
                                    "params": [
                                        self.config["trend-display"],
                                        "addZoomX",
                                        -25
                                    ]
                                },
                                "class-name": "ctrl button zoom-out icon-only no-frame"
                            }
                        }
                    ]
                },
                "zoomInButton": {
                    "anchorName": "zoom-in-button-anchor",
                    "controls": [
                        {
                            "ui_type": "button",
                            "cfg": {
                                "name": "zoom-in-button",
                                "template": "default/button_with_bg_pic",
                                "action": {
                                    "name": "trend-display",
                                    "params": [
                                        self.config["trend-display"],
                                        "addZoomX",
                                        25
                                    ]
                                },
                                "class-name": "ctrl button zoom-in icon-only no-frame"
                            }
                        }
                    ]
                },
                "pointInTimeArea": {
                    "anchorName": "point-in-time-area",
                    "controls": [
                        {
                            "ui_type": "button",
                            "cfg": {
                                "name": "zoom-stages-button",
                                "template": "default/button_with_bg_pic",
                                "action": {
                                    "name": "popup-menu",
                                    "params": [
                                        selfName + " .point-in-time-popup",
                                        "open"
                                    ]
                                },
                                "class-name": "ctrl button icon-only no-frame point-in-time"
                            }
                        },
                        {
                            "ui_type": "popup-menu",
                            "cfg": {
                                "name": "point-in-time-popup",
                                "content-template": "default/trend-zoom/point-in-time-popup"
                            },
                            "dateTimePicker": {
                                "selectDate": selfName + " .select-date-anchor.select-date",
                                "selectTime": selfName + " .select-time-anchor.select-time",
                                "doneButton": selfName + " .done-button-anchor.done"
                            }
                        }
                    ]
                },
                "zoomStagesArea": {
                    "anchorName": "zoom-stages-area",
                    "controls": [
                        {
                            "ui_type": "button",
                            "cfg": {
                                "name": "zoom-stages-button",
                                "show-icon": true,
                                "show-text": false,
                                "icon-src": "pics/system/controls/trend-zoom/Zeitspanne.svg",
                                "action": {
                                    "name": "popup-menu",
                                    "params": [
                                        selfName + " .zoom-stages-popup",
                                        "toggle"
                                    ]
                                },
                                "class-name": "ctrl button zoom-stage no-frame"
                            }
                        },
                        {
                            "ui_type": "popup-menu",
                            "cfg": {
                                "name": "zoom-stages-popup",
                                "class-name": "zoom-stages-popup ctrl popup-menu hidden",
                                "content-template": "default/trend-zoom/zoom-stages-popup"
                            },
                            "buttonActions": {
                                "buttons": {
                                    "zoom24hButton": selfName + ".zoom-stages-popup.zoom-stages-button-anchor.zoom-24h-button",
                                    "zoom12hButton": selfName + ".zoom-stages-popup.zoom-stages-button-anchor.zoom-12h-button",
                                    "zoom8hButton": selfName + ".zoom-stages-popup.zoom-stages-button-anchor.zoom-8h-button",
                                    "zoom4hButton": selfName + ".zoom-stages-popup.zoom-stages-button-anchor.zoom-4h-button",
                                    "zoom2hButton": selfName + ".zoom-stages-popup.zoom-stages-button-anchor.zoom-2h-button",
                                    "zoom1hButton": selfName + ".zoom-stages-popup.zoom-stages-button-anchor.zoom-1h-button"
                                }
                            }
                        }
                    ]
                }
            },
            body = document.body,
            html = document.documentElement,
            appHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);

        for (elName in anchors) {
            anchorElName = elName + "AnchorEl";
            var elConfig = anchors[elName];
            self.vars[anchorElName] = shmi.getUiElement(elConfig.anchorName, self.element);
            if (!self.vars[anchorElName]) {
                console.error(className, "Element `" + elConfig.anchorName + "` not found in template");
                continue;
            }
            self.vars.controls[elName] = [];
            elConfig.controls.forEach(function(controlData) {
                var openIntervalPopupToken = null,
                    openTimePickerToken = null,
                    control = shmi.createControl(controlData.ui_type, self.vars[anchorElName], controlData.cfg, "DIV");
                self.vars.controls[elName].push(control);

                if (controlData.buttonActions) {
                    openIntervalPopupToken = control.listen("open", function(evt) {
                        openIntervalPopupToken.unlisten();
                        self.vars.tokens.splice(self.vars.tokens.indexOf(openIntervalPopupToken), 1);
                        shmi.onReady({
                            controls: controlData.buttonActions.buttons
                        }, function(resolved) {
                            var buttonName, buttonAction,
                                viewport, secondsIn100, hoursIn100, elRect, elMenu;

                            if (resolved.controls) {
                                if (self.vars.trendDisplay && self.vars.trendDisplay.config && self.vars.trendDisplay.config.viewport) {
                                    viewport = self.vars.trendDisplay.config.viewport;
                                    secondsIn100 = viewport.visible;
                                    hoursIn100 = secondsIn100 / 3600;
                                    timeUnits = {
                                        "secondsIn100%": secondsIn100,
                                        "hoursIn100%": hoursIn100,
                                        "percentIn24h": hoursIn100 * 100 / 24,
                                        "percentIn12h": hoursIn100 * 100 / 12,
                                        "percentIn8h": hoursIn100 * 100 / 8,
                                        "percentIn4h": hoursIn100 * 100 / 4,
                                        "percentIn2h": hoursIn100 * 100 / 2,
                                        "percentIn1h": hoursIn100 * 100
                                    };
                                }
                                for (buttonName in resolved.controls) {
                                    buttonAction = generateButtonAction(buttonName, self);
                                    if (!buttonAction) {
                                        continue;
                                    }
                                    resolved.controls[buttonName].action = new shmi.visuals.core.UiAction(
                                        buttonAction,
                                        resolved.controls[buttonName]
                                    );
                                }

                                // If positioned on the bottom of the window, fix popup position
                                elMenu = shmi.getUiElement("menu-frame", resolved.controls.zoom1hButton.parentContainer.parentContainer.element);
                                elRect = elMenu.getBoundingClientRect();
                                if (appHeight < elRect.y + elMenu.scrollHeight) {
                                    var offsetBottom = self.element.getBoundingClientRect().height + 10;
                                    elMenu.style.bottom = offsetBottom + "px";
                                    // shmi.addClass(elMenu.parentElement, "menu-up");
                                } else {
                                    shmi.addClass(elMenu.parentElement, "menu-down");
                                }
                            }
                        });
                    });
                    self.vars.tokens.push(openIntervalPopupToken);
                }

                if (controlData.events) {
                    Object.keys(controlData.events).forEach(function(eventName) {
                        var action = controlData.events[eventName],
                            mode;
                        if (action === "playLiveMode" || action === "pauseLiveMode") {
                            self.vars.tokens.push(control.listen(eventName, function(evt) {
                                if (action === "playLiveMode") {
                                    mode = 1;
                                } else if (action === "pauseLiveMode") {
                                    mode = 0;
                                }
                                setLiveMode(mode, self);
                            }));
                        }
                    });
                }

                if (controlData.dateTimePicker) {
                    openTimePickerToken = control.listen("open", function(evt) {
                        openTimePickerToken.unlisten();
                        self.vars.tokens.splice(self.vars.tokens.indexOf(openTimePickerToken), 1);
                        shmi.onReady({
                            controls: controlData.dateTimePicker
                        }, function(resolved) {
                            var elMenu, elRect;
                            initDateTimePicker(resolved.controls, self);

                            // If positioned on the bottom of the window, fix popup position
                            elMenu = shmi.getUiElement("menu-frame", resolved.controls.doneButton.parentContainer.parentContainer.element);
                            elRect = elMenu.getBoundingClientRect();
                            if (appHeight < elRect.y + elMenu.scrollHeight) {
                                var offsetBottom = self.element.getBoundingClientRect().height + 10;
                                elMenu.style.bottom = offsetBottom + "px";
                            } else {
                                shmi.addClass(elMenu.parentElement, "menu-down");
                            }
                        });
                    });
                    self.vars.tokens.push(openTimePickerToken);
                }
            });
        }
    }
    //declare private functions - END

    //definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        /* schema of configuration object for validation according to json-schema v4
         * (http://json-schema.org/draft-04/schema#)
         */
        configSchema: null,
        /* instance variables */
        vars: {
            created: false,
            trendDisplay: null,
            liveMode: null,
            liveModeItem: null,
            playButtonAnchorEl: null,
            pauseButtonAnchorEl: null,
            zoomOutButtonAnchorEl: null,
            zoomInButtonAnchorEl: null,
            zoomStagesAreaEl: null,
            zoomResetButtonAnchorEl: null,
            selectDateAnchorEl: null,
            selectTimeAnchorEl: null,
            controls: {},
            tokens: []
        },
        /* imports added at runtime */
        imports: {
            /* example - add import via shmi.requires(...) */
            im: "visuals.session.ItemManager",
            /* example - add import via function call */
            qm: function() {
                return shmi.visuals.session.QueryManager;
            }
        },
        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when config-file (optional) is loaded and template (optional) is inserted into base element */
            onInit: function() {},
            /* called when control is enabled */
            onEnable: function() {
                var self = this,
                    im = self.imports.im,
                    liveItemHandler = im.getItemHandler(),
                    liveItemToken = null;

                if (!self.config["trend-display"]) {
                    console.log("config trend-display not found! Trend Zoom aborted.");
                    create(self);
                    lock(self, true);
                    return;
                }

                shmi.onReady({
                    controls: {
                        trendDisplay: self.config["trend-display"]
                    }
                }, function(resolved) {
                    self.vars.trendDisplay = resolved.controls.trendDisplay;

                    self.vars.liveModeItem = "virtual:" + self.vars.trendDisplay.getName() + ":live";
                    liveItemHandler.setValue = function(value, type, name) {
                        self.vars.liveMode = value;
                        setLiveModeButtons(self);
                    };

                    create(self);

                    liveItemToken = im.subscribeItem(self.vars.liveModeItem, liveItemHandler);
                    self.vars.tokens.push(liveItemToken);

                    if (self.locked) {
                        lock(self, true);
                    }
                });
            },
            /* called when control is disabled */
            onDisable: function() {
                var self = this,
                    iter = shmi.requires("visuals.tools.iterate.iterateObject");
                self.vars.tokens.forEach(function(t) {
                    t.unlisten();
                });
                self.vars.tokens = [];
                iter(self.vars.controls, function(val, prop) {
                    val.forEach(function(ctrl) {
                        shmi.deleteControl(ctrl, true);
                    });
                    self.vars.controls[prop] = [];
                });
                self.vars.controls = {};
                self.vars.trendDisplay = null;
            },
            /* called when control is locked - disable mouse- and touch-listeners */
            onLock: function() {
                var self = this;
                lock(self, true);
            },
            /* called when control is unlocked - enable mouse- and touch-listeners */
            onUnlock: function() {
                var self = this;
                unlock(self, true);
            },
            /* called by ItemManager when subscribed item changes and once on initial subscription */
            onSetValue: function(value, type, name) {

            },
            /** Sets min & max values and stepping of subscribed variable **/
            onSetProperties: function(min, max, step) {

            }
        }
    };

    //definition of new control extending BaseControl - END

    //generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires("visuals.tools.control-generator");
    cg.generate(definition);
})();

/**
 * WebIQ visuals control template.
 *
 * Configuration options (default):
 *
 * {
 *      'class-name': 'user-info',
 *      'name': null,
 *      'template': 'custom/controls/user-info',
 *      'label': uiType,
 *      'show-name': false,
 *      'show-group': false
 * }
 *
 * Explanation of configuration options:
 *
 * class-name {string}: Sets default css class applied on control root element
 * name {string}: Name of control set to data-name attribute
 * template {string}: Path to template file
 *
 * @version 1.1
 */
(function() {
    'use strict';

    // variables for reference in control definition
    var className = 'user-info', // control name in camel-case
        uiType = 'user-info', // control keyword (data-ui)
        isContainer = false;

    // example - default configuration
    var defConfig = {
        'class-name': 'user-info',
        'name': null,
        'template': 'custom/controls/user-info',
        'label': uiType,
        'show-name': false,
        'show-group': false
    };

    // setup module-logger
    var ENABLE_LOGGING = true,
        RECORD_LOG = false;
    var logger = shmi.requires('visuals.tools.logging').createLogger(uiType, ENABLE_LOGGING, RECORD_LOG);
    var fLog = logger.fLog,
        log = logger.log;

    // declare private functions - START
    /**
     * Displays current user
     *
     * @param  {object} self
     */
    function displayUser(self) {
        var domElName = null;

        //Abort if HTML element is missing
        if (!shmi.getUiElement('user-info-name', self.element)) {
            fLog('Dom-Element "user-info-name" not found!');
            return;
        }
        domElName = shmi.getUiElement('user-info-name', self.element);

        //Check show-name property and set to false if undefined
        if (!self.config['show-name']) {
            self.config['show-name'] = false;
        }
        //Show username or name and surname if available
        if (self.config['show-name'] === false) {
            domElName.textContent = self.vars.user.username;
        } else if (self.config['show-name'] === true) {
            if (self.vars.user.name && self.vars.user.surname) {
                domElName.textContent = self.vars.user.name + ' ' + self.vars.user.surname;
            } else if (!self.vars.user.name) {
                domElName.textContent = self.vars.user.surname;
            } else if (!self.vars.user.surname) {
                domElName.textContent = self.vars.user.name;
            } else {
                domElName.textContent = 'First Name and/or Last Name not set!';
            }
        }
    }

    /**
     * Deletes Designer-usergroups and displays extant usergroup
     *
     * @param  {object} self
     */
    function setUserGroup(self) {
        var domElGroup = null,
            primUserGrp = null,
            userGroups = self.vars.user.userGroups;
        //Abort if HTML element is missing
        if (!shmi.getUiElement('user-info-group', self.element) === undefined) {
            fLog('Dom-Element "user-info-group" not found!');
            return;
        }

        domElGroup = shmi.getUiElement('user-info-group', self.element);
        //check for 'designer default-groups' and delete from list
        // eslint-disable-next-line no-shadow
        userGroups = userGroups.filter(function(group) {
            return group.indexOf('@') !== 0;
        });
        //add default in case of no matching group
        if (userGroups[0] && !userGroups[0].includes('@')) {
            primUserGrp = userGroups[0];
        } else {
            primUserGrp = 'Default';
        }
        domElGroup.textContent = primUserGrp;
    }
    /**
     * Gets userdata (name and surname) from connect and calls setUser()
     * @param  {object} self
     */
    function getUser(self) {
        var request = shmi.requires('visuals.tools.connect').request,
            curUserName = self.vars.user.username;

        request('user.list', {}, function(response, err) {
            if (response && Array.isArray(response.users)) {
                response.users.forEach(function(usr) {
                    if (usr.username === curUserName) {
                        self.vars.user.name = usr.first_name;
                        self.vars.user.surname = usr.last_name;
                    }
                });
                displayUser(self);
            } else {
                fLog('[UserManager] could not retrieve userlist:', err.category, err.errc, err.message);
                fLog('User name and surname cannot be displayed!');
            }
        });
    }
    // declare private functions - END

    // definition of new control extending BaseControl - START
    var definition = {
        className: className,
        uiType: uiType,
        isContainer: isContainer,
        /* default configuration settings - all available options have to be initialized! */
        config: defConfig,
        configSchema: null,
        /* instance variables */
        vars: {
            user: {
                userGroups: null,
                name: null,
                surname: null,
                username: null
            }
        },
        /* imports added at runtime */
        imports: {},

        /* array of custom event types fired by this control */
        events: [],

        /* functions to extend or override the BaseControl prototype */
        prototypeExtensions: {
            /* called when control is enabled */
            onEnable: function() {
                var self = this;
                self.vars.user.userGroups = shmi.visuals.session.UserManager.currentUser.groupList;
                self.vars.user.username = shmi.visuals.session.UserManager.currentUser.name;

                getUser(self);
                //displayUser() called from getUser
                if (self.config['show-group'] === true) {
                    setUserGroup(self);
                }
            }
        }
    };

    // definition of new control extending BaseControl - END

    // generate control constructor & prototype using the control-generator tool
    var cg = shmi.requires('visuals.tools.control-generator');
    cg.generate(definition);
})();
/**
 * utility classes of Panel control
 *
 * @namespace shmi.visuals.controls.panel
 */
shmi.pkg("visuals.controls.panel");

/**
 * Creates a Panel View element
 *
 * @constructor
 * @param element - base element of the panel view
 */
shmi.visuals.controls.View = function(element, config) {
    this.element = element;
    this.config = config || {};

    this.parseAttributes();

    shmi.def(this.config, 'name', null);
    shmi.def(this.config, 'class-name', 'view inner-layout');
    shmi.def(this.config, 'index', 0);

    if (typeof this.config.index === "string") {
        this.config.index = parseInt(this.config.index);
    }

    this.parentContainer = null;
    this.controls = [];
    this.active = false;
    this.initialized = false;

    this.startup();
};

shmi.visuals.controls.View.prototype = {
    isContainer: true,
    uiType: "view",
    getClassName: function() {
        return "View";
    },
    onRegister: function(onDone) {
        var self = this;

        self.parseChildren(self.element, onDone);
    },
    onInit: function() {
        var self = this;
        self.index = self.config.index;
        shmi.removeClass(self.element, 'ctrl');
    },
    /**
     * Enables the View and child controls
     *
     */
    onEnable: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].enable();
        }
    },
    /**
     * Disables the View and child controls
     *
     */
    onDisable: function() {
        for (var i = 0; i < this.controls.length; i++) {
            this.controls[i].disable();
        }
    },
    onLock: function() {
        var self = this;
        self.controls.forEach(function(ctrl) {
            ctrl.lock();
        });
    },
    onUnlock: function() {
        var self = this;
        self.controls.forEach(function(ctrl) {
            ctrl.unlock();
        });
    },
    getIndex: function() {
        return this.index;
    },
    onAddControl: function(options, callback) {
        var self = this,
            cm = shmi.requires("visuals.tools.controller"),
            controls = [],
            initToken = null;

        if (!self.initialized) {
            throw new Error("Control not initialized yet.");
        }

        if (!Array.isArray(options)) {
            options = [options];
        }

        options.forEach(function(option) {
            var control = shmi.createControl(option.ui, self.element, option.config, "DIV", null, false);
            if (control !== null) {
                if (option.style && typeof option.style === "object") {
                    let iter = shmi.requires("visuals.tools.iterate.iterateObject");

                    iter(option.style, (value, name) => {
                        control.element.style[name] = value;
                    });
                }
                controls.push(control);
            }
        });

        initToken = shmi.waitOnInit(controls, function() {
            var idx = -1,
                errors = [],
                tm = shmi.requires("visuals.task"),
                tasks = [],
                tl = null;

            if (initToken) {
                idx = self._init_.tokens.indexOf(initToken);
                if (idx !== -1) {
                    self._init_.tokens.splice(idx, 1);
                }
                initToken.unlisten();
                initToken = null;
            }

            options.forEach(function(opt, jdx) {
                var t = null,
                    control = controls[jdx];

                if (opt.controller) {
                    cm.create(opt.controller.name, opt.controller, control.getName());
                }

                if (self.isActive()) {
                    control.enable();
                }

                if (Array.isArray(opt.children) && opt.children.length) {
                    t = tm.createTask("child control");
                    t.run = function() {
                        control.addControl(opt.children, function(childErr, childOptions) {
                            if (childErr) {
                                errors.push(childErr);
                            }
                            t.complete();
                        });
                    };
                    tasks.push(t);
                }
            });

            if (tasks.length) {
                tl = tm.createTaskList(tasks, false);
                tl.onComplete = function() {
                    callback(errors.length ? errors[0] : null, controls);
                };
                tl.run();
            } else {
                callback(errors.length ? errors[0] : null, controls);
            }
        });
        if (initToken) {
            self._init_.tokens.push(initToken);
        }
    }
};

shmi.extend(shmi.visuals.controls.View, shmi.visuals.core.BaseControl);
