Source: viewer/Viewer.js

import { cloneDeep } from "lodash";

import { UiElement } from "../ui/UiElement.js";
import { setCSSProperties } from "../utils/utils.js";
import { Filters } from "../map/filters/Filters.js";
import { LayerManager } from "../map/LayerManager.js";
import { Layer } from "../map/layer/Layer/Layer.js";
import { BASELAYERS } from "../defaults/BASELAYERS.js";
import { FileDropTarget } from "../ui/dnd/FileDropTarget.js";
import { shortcutManager } from "./shortcutsManager.js";
import * as loader from "./loader/index.js";
import * as templates from "./templates/index.js";
import { loadLayer } from "../map/utils/loadLayer.js";

import "./Viewer.css";

export { Viewer }

/**
 * Viewer class for loading a JSON configuration
 * and creating a customizable viewer from the vef modules
 * 
 * @author sjaswal <shahzeib.jaswal@awi.de>
 * @author rhess <robin.hess@awi.de>
 * 
 * @memberof vef.viewer
 */
class Viewer extends UiElement {

    /**
     * @param {HTMLElement | string} target target element to put the viewer (ID or HTMLElement)
     * @param {object | string} config config object or path to a viewer config file
     * @param {object} options additional options for the viewer class
     */
    constructor(target, config, options) {
        super(target, {
            "config_loaded": [],
            "layers_loaded": [],
            "viewer_loaded": [],
            "drag_drop_viewer_loaded": []
        });

        this.options = Object.assign({
            shortcutsEnabled: true,
            allowConfigDragDrop: false,
            configOverride: null
        }, options || {});

        this.elements = {};
        this.config = config;
        this.originalConfig = {};
        this.filters = null;
        this.activeFilters = [];
        this.template = null;
        this.layerManager = new LayerManager();
        this.baseLayers = {};

        this.getElement().classList.add("viewer");

        this.promise_ = this.loadConfig_()
        this.promise_.then(() => {
            setTimeout(() => {
                this.fire("config_loaded", this);
                this.parseConfig_();
            }, 0);
        });

        if (this.options.shortcutsEnabled) this.shortcuts = new shortcutManager(this.elements, this.options, target);
        if (this.options.allowConfigDragDrop) this._initConfigDragDrop();
    }

    /**
     * Add an element to the viewer and set the id
     * 
     * @param {string} id 
     * @param {UiElement | HTMLElement} element 
     */
    addElement(id, element) {
        this.elements[id] = element;
        if (element instanceof UiElement) {
            element.getElement().id = id;
        } else if (element instanceof HTMLElement) {
            element.id = id;
        }
    }

    /**
     * Append an element to a parent identified by id and optionally
     * update the layout config of the viewer
     * 
     * @param {string} elementId
     * @param {string} parentId
     * @param {boolean} updateLayout default is false
     */
    appendElement(elementId, parentId, updateLayout) {
        const element = this.elements[elementId];
        const parent = (parentId == "#") ? this.getElement() : this.elements[parentId];

        if (!(parent instanceof HTMLElement)) return;
        if (element instanceof UiElement) {
            element.appendTo(parent);
        } else if (element instanceof HTMLElement) {
            parent.appendChild(element);
        } else {
            return;
        }

        if (!updateLayout) return;

        let parentLayout = null;
        for (let i = 0; i < this.config.layout.length; ++i) {
            const index = this.config.layout[i].children.indexOf(elementId);
            if (index > -1) {
                this.config.layout[i].children.splice(index, 1);
                parentLayout = this.config.layout[i];
                break;
            }
        }

        if (!parentLayout) {
            parentLayout = { parent: "parentId", children: [] };
            this.config.layout.push(parentLayout);
        }

        parentLayout.children.push(elementId);
    }

    /**
     * Remove and dispose an element from the viewer and optionally update the layout
     * @param {string} elementId 
     * @param {boolean} updateLayout default is false
     */
    removeElement(elementId, updateLayout) {
        const element = this.elements[elementId];

        if (element instanceof UiElement) {
            element.dispose();
        } else if (element instanceof HTMLElement) {
            element.remove();
        } else {
            return;
        }

        if (!updateLayout) return;
        for (let i = 0; i < this.config.layout.length; ++i) {
            const index = this.config.layout[i].children.indexOf(elementId);
            if (index > -1) {
                this.config.layout[i].children.splice(index, 1);
                break;
            }
        }
    }

    /**
     * parse the json config to load the viewer
     * @private
     */
    parseConfig_() {
        this.originalConfig = cloneDeep(this.config);

        // assign config override
        if (typeof this.options.configOverride == "object") {
            Object.assign(this.config, this.options.configOverride);
        }

        // convert template config to regular viewer config
        if (this.config.template && (this.config.template in templates)) {
            this.template = this.config.template;
            this.config = templates[this.config.template].getViewerConfig(this.config);
        }

        // copy config object
        const config = cloneDeep(this.config);

        // css theme
        if (config.theme) setCSSProperties(config.theme);

        // load baseLayers
        const baseLayers = Object.assign({}, BASELAYERS);
        if (config.baseLayers) Object.assign(baseLayers, config.baseLayers);
        if (Array.isArray(baseLayers.excluded)) {
            baseLayers.excluded.forEach(layer => {
                if (layer in baseLayers) delete baseLayers[layer];
            });
        }

        for (let name in baseLayers) {
            if (name == "excluded") continue;
            this.baseLayers[name] = {};
            for (let key in baseLayers[name]) {
                const baseLayer = baseLayers[name][key];
                this.baseLayers[name][key] = (typeof baseLayer == "object")
                    ? loadLayer(baseLayer, {})
                    : baseLayer;
                // apply key as title, to allow identifying the baselayer in the map
                if (typeof baseLayer == "object") this.baseLayers[name][key].title = name;
            }
        }

        // load layers
        this.layerManager.loadLayers(config.layers || {}, config.cache || {}).then(() => {
            this.fire("layers_loaded", this);

            // parse filters
            if (config.filters) {
                this.filters = new Filters(this.layerManager);

                this.filters.on("remove", id => this.removeElement(id));
                this.filters.on("add", e => {
                    this.elements[e.id] = e.filter;
                });

                this.filters.addFilters(config.filters);

                const setFilter = layer => {
                    if ((layer instanceof Layer) && !layer.filterDisabled) {
                        // Filter out active filters that exclude this layer
                        const applicableFilters = this.activeFilters.filter(filter => 
                            !filter.excludedLayers.includes(layer.uniqueId)
                        );
                        // add the filter but only apply it if the layer is active
                        layer.setFilter(applicableFilters);
                        if (layer.active) layer.applyFilter();
                    }
                };

                this.layerManager.on("layermanager_add_layer", layer => setFilter(layer));
                this.filters.on("change", () => {
                    this.activeFilters = this.filters.getActiveFilters();
                    this.layerManager.forEach(layer => setFilter(layer));
                })

                // trigger change event to apply filters to all layers
                this.filters.fire("change", this.filters);
            }

            // load elements
            for (let i = 0; i < config.elements.length; ++i) {
                const element = config.elements[i];
                if (element.type in loader) {
                    loader[element.type](element, this);
                }
            }

            // parse layout
            for (let i = 0; i < config.layout.length; ++i) {
                const layout = config.layout[i];
                for (let j = 0; j < layout.children.length; ++j) {
                    this.appendElement(layout.children[j], layout.parent);
                }
            }

            this.fire("viewer_loaded", this);
        });
    }

    /**
     * load the config-file from the given path
     * @private
     */
    loadConfig_() {
        return new Promise((resolve, reject) => {
            if (typeof this.config == "string") {
                fetch(this.config)
                    .then(response => response.json())
                    .then(json => {
                        this.config = json;
                        resolve(this);
                    })
                    .catch(e => reject(e));
            } else if (typeof this.config == "object") {
                resolve(this)
            } else {
                reject("invalid viewer config");
            }
        });
    }

    /**
     * Gets all instances of LayerTree and
     * returns all used layers in an array
     * 
     * @private
     * @param {object[]} elements element configs
     * @returns {object[]} layer configs
     */
    getUsedLayers_(elements) {
        const layers = {};
        const cache = {};

        for (let i in elements) {
            if (elements[i].type.toLowerCase() == "layertree") {
                const structure = elements[i].structure;
                for (let parent of structure) {
                    for (let child of parent.children || []) {
                        if (!(child in layers)) {
                            const layer = this.layerManager.getLayerById(child);
                            if (layer) {
                                layers[child] = layer.getConfig();

                                const cacheElement = layer.getCache();
                                if (Object.keys(cacheElement).length > 0) {
                                    cache[child] = cacheElement;
                                }
                            }
                        }
                    }
                }
            }
        }

        return {
            cache: cache,
            layers: layers
        };
    }

    /**
     * Clear all viewer elements
     * @returns {Promise}
     * 
     * @override
     */
    dispose() {
        super.dispose();
        return new Promise((resolve, reject) => {
            this.promise_.finally(() => {
                for (let i in this.elements) {
                    const element = this.elements[i];
                    if (element instanceof UiElement) {
                        element.dispose();
                    } else if (element instanceof HTMLElement) {
                        element.remove();
                    }
                }
                this.layerManager.dispose();
                resolve();
            });
        });
    }

    /**
     * Get the JSON config of the current viewer state
     * 
     * @param {boolean} useTemplate convert generic config to template config (if template was used) default is false
     * @returns {object} viewer config
     */
    getConfig(useTemplate = false) {
        let config = cloneDeep(this.config);

        // parse element options
        const elementOptions = {};
        for (let i in this.elements) {
            if (typeof this.elements[i].getOptions == "function") {
                elementOptions[i] = this.elements[i].getOptions();
            }
        }

        // merge element options
        for (let i = 0; i < config.elements.length; ++i) {
            const id = config.elements[i].id;
            if (id in elementOptions) {
                config.elements[i] = Object.assign(config.elements[i], elementOptions[id]);
            }
        }

        // add layers
        const layers = this.getUsedLayers_(config.elements);;
        config.layers = layers.layers;
        config.cache = layers.cache;

        // re-write filter options
        config.filters = [];
        for (let id in this.filters.filters) {
            const filterOptions = this.filters.filters[id].getOptions();
            filterOptions.id = id;
            config.filters.push(filterOptions);
        }

        // convert generic viewer config to template config
        if (useTemplate && this.template && (this.template in templates)) {
            config = templates[this.template].getTemplateConfig(config, this.originalConfig);
        }

        return config;
    }

    _initConfigDragDrop() {
        new FileDropTarget(this.getElement()).on("drop", (files) => {
            console.log(files);
            if (files.length == 0) return;
            // only check first file
            const file = files[0];
            if (file.name.endsWith("viewer.json")) {
                const reader = new FileReader();
                reader.onload = e => {
                    const viewer = new Viewer(null, JSON.parse(e.target.result), this.options)
                    this.fire("drag_drop_viewer_loaded", viewer);
                }
                reader.readAsText(file);
            }

        });
    }
}