Source: map/layer/Layer/Layer.js

import { EventObject } from "../../../events/EventObject.js";
import { generateUniqueId } from "../../../utils/utils.js";
import { resolveComplexTemplate } from "../../../utils/template/resolveComplexTemplate.js";
import { enhanceHTML } from "../../../utils/template/enhanceHTML.js";
import { LayerProxy } from "./Layer.proxy.js";
import { NETWORK_LOADING_ERROR } from "../../utils/messageTemplates.js";
import LayerMetadata from "./Layer.metadata.md?raw";
import { LayerSchema } from "./Layer.schema.js";

export { Layer }

/**
 * Abstract Layer class
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.map.layer
 */
class Layer extends EventObject {

    /**
     * Set all properties for the layer based on the options object.
     * 
     * @param {object} config 
     * @param {object} cache 
     * @param {string} id 
     */
    constructor(config, cache, id) {
        super({
            "layer_select": [],
            "layer_deselect": [],
            "layer_update": [],
            "layer_request_remove": [],
            "layer_loading": [],
            "layer_loaded": [],
            "layer_message": [],
            "layer_click": [],
            "layer_mouseover": [],
            "layer_mouseout": []
        });

        this.uniqueId = id || generateUniqueId();

        // layer metadata containers
        this.config = Object.assign({}, config || {});
        this.cache = Object.assign({}, cache || {});
        this.defaults = {};
        this.schema = {};

        // initialize the default properties
        this.setSchema_(LayerSchema.getSchema());

        // apply the catalogId to the config, if it is only defined in the cache
        if ("catalogId" in this.cache) {
            this.config.catalogId = this.cache.catalogId;
        }

        // internal properties
        this.messages_ = [];
        this.popupEvents_ = [];
        this.projectionMatches_ = true;
        this.filterMatches_ = true;

        // active map layer state
        this.crs_ = (this.availableCrs.length > 0) ? this.availableCrs[0] : "";
        this.mapLayers_ = {};
        this.activeLayer_ = new LayerProxy();

        // framework specific map implementations
        this.layerProxies_ = {};
    }

    get deactivated() {
        return (!this.projectionMatches_ || !this.filterMatches_);
    }

    set deactivated(val) {
        console.log("not implemented, only the calculated getter is relevant");
    }
    
    /**
     * Helper method to get a property from the config, cache or default container
     * 
     * @param {string} name 
     * @private
     * @returns {any} property
     */
    getProperty_(name) {
        if (name in this.config) return this.config[name];
        if (name in this.cache) return this.cache[name];
        if (name in this.defaults) return this.defaults[name];
        return undefined;
    }

    /**
     * Register the getter and setter for a property and add default
     * values for properties based on the JSON Schema
     * @param {object} schema json schema
     * @private
     */
    setSchema_(schema) {
        this.schema = schema;
        for (let name in schema.properties) {
            if (name in this) delete this[name];
            this.defaults[name] = schema.properties[name].default;
            Object.defineProperty(this, name, {
                get() { return this.getProperty_(name); },
                set(val) { this.config[name] = val; },
                configurable: true
            });
        }

        // set layer type based on default for all content objects
        const type = this.defaults.type;
        this.config.type = type;
        this.cache.type = type;
    }

    /**
     * Register the getter and setter for a property and add default values for properties
     * in the exported config object. Existing values are not overwritten
     * 
     * @private
     * @param {object} config 
     */
    addConfig_(config) {
        for (let name in config) {
            if (name in this) delete this[name];
            if (!(name in this.config)) this.config[name] = config[name];
            Object.defineProperty(this, name, {
                get() { return this.config[name]; },
                set(val) { this.config[name] = val; },
                configurable: true
            });
        }
    }

    getConfig_(container) {
        container = JSON.parse(JSON.stringify(container));

        for (let key in container) {
            if (
                (container[key] === null) ||
                (((typeof container[key] == "string") || Array.isArray(container[key])) && (container[key].length == 0)) ||
                ((typeof container[key] == "object") && (Object.keys(container[key]).length == 0))

            ) delete container[key];
        }

        // remove deprecated keys
        if ("settings" in container) delete container.settings;
        if ("filter" in container) delete container.filter;
        // remove default keys
        if (container.active === false) delete container.active;
        if (container.expanded === false) delete container.expanded;

        return container;
    }

    /**
     * create a map layer based on the service options
     * 
     * @param {string} type e.g. "leaflet"
     * @private
     */
    createMapLayer_(type, options) {
        if (type in this.layerProxies_) {
            const proxy = new this.layerProxies_[type](options || {});

            let error = false;
            proxy.on("layerproxy_loading", () => {
                error = false;
                this.fire("layer_loading");
            });
            proxy.on("layerproxy_loaded", () => {
                this.fire("layer_loaded");
                if (!error) this.removeMessage(NETWORK_LOADING_ERROR);
            });
            proxy.on('layerproxy_error', () => {
                error = true;
                this.addMessage(NETWORK_LOADING_ERROR);
            });
            this.mapLayers_[type] = proxy;

            return proxy;
        } else {
            return null;
        }
    }

    /**
     * Helper method to update the mapLayer according to
     * the current state. Might be necessary after switching the map type
     * 
     * @private
     */
    updateMapLayer_() { }

    getConfig() {
        const config = this.getConfig_(this.config);
        if (("catalogId" in config) && ("type" in config)) delete config.type;
        return config;
    }

    getCache() {
        return this.getConfig_(this.cache);
    }

    /**
     * Print the Metadata from this layer as html.
     */
    printMetadata(layer) {
        const container = resolveComplexTemplate(LayerMetadata, {
            layer: layer || this,
            datasetDescription: this?.dataFrame?.description || {},
            sensorMetadata: this?.sensorMetadata || {},
        }, {
            markdown: true,
            postProcess: true
        });
        enhanceHTML(container, {});
        return container;
    }

    /**
     * get the events that can be triggered by buttons in a popup
     */
    getPopupEvents() {
        return this.popupEvents_.slice();
    }

    /**
     * add an event for the buttons in the popup
     */
    addPopupEvent(title, callback) {
        if ((typeof title == "string") && (typeof callback == "function")) {
            this.popupEvents_.push({
                title: title,
                callback: callback
            });
        }
    }

    /**
     * returns the map layer proxy for the
     * specific map implementation
     * 
     * @param {string} type 
     * @returns {object} map layer for framework
     */
    getLayerProxy(type) {
        return (type in this.mapLayers_)
            ? this.mapLayers_[type]
            : this.createMapLayer_(type);
    }

    /**
     * enables the map layer for the
     * specific map implementation
     * 
     * @param {string} type 
     */
    enable(type) {
        if (type in this.mapLayers_) {
            this.active = true;
            this.activeLayer_ = this.mapLayers_[type];
            this.updateMapLayer_(type);
            return true;
        }

        return false;
    }

    /**
     * Unset the active state of the layer
     */
    disable() {
        this.active = false;
    }

    /**
     * Returns the Legend Graphic for this layer.
     * May depend on current color scale.
     * 
     * @returns html <img>-tag (null or undefined if there is no legend graphic)
     */
    getLegendGraphic() { }

    /**
     * Set Filter without applying it
     *  
     * @param {object} config generic filter config 
     */
    setFilter(config) {
        this.filter = config;
    }

    /**
     * Apply the defined filter. Uses generic
     * filter syntax and transforms it internally
     */
    applyFilter() { }

    /**
     * Set the style of the layer.
     * 
     * @param {string} styleName style name
     * @returns {boolean} returns true if successful
     */
    setStyle(styleName) { }

    /**
     * Get the active style of the layer.
     */
    getStyle() { }

    /**
     * Get the active colorscale for the layer
     */
    getColorScale() { }

    /**
     * Set the colorscale for the layer
     * 
     * @param colorScale 
     */
    setColorScale(colorScale) { }

    /**
     * Dispose the Layer
     */
    dispose() { }

    /**
     * Get the transparency value
     * 
     * @returns {number} number between 0 and 1
     */
    getOpacity() {
        return this.opacity;
    }

    /**
     * Set the transparency between 0 and 1
     * @param {number} opacity style name
     */
    setOpacity(opacity) {
        this.opacity = opacity;
        this.activeLayer_.setOpacity(opacity);
    }

    /**
     * Reload the Layer on the map
     */
    reload() {
        this.activeLayer_.reload();
    }

    /**
     * Switch the projection for the visualization
     * of the layer. CRS hast to be included in availableCRS.
     * 
     * @param {string} crs target crs code
     */
    setProjection(crs) {
        crs = crs.toUpperCase();
        if (this.availableCrs.includes(crs)) {
            this.crs_ = crs;
            this.activeLayer_.setProjection(crs);
            this.projectionMatches_ = true;
        } else {
            this.projectionMatches_ = false;
        }
        return this.projectionMatches_;
    }

    /**
     * Get the bounding box as an object
     * 
     * @returns {object} { min: {x, y}, max: {x, y}, crs: "CRS:84" }
     */
    getBounds() {
        return {
            min: { y: -90, x: -180 },
            max: { y: 90, x: 180 },
            crs: "CRS:84"
        }
    }

    /**
     * Get all active messages
     * 
     * @returns {object[]} messages
     */
    getMessages() {
        // implementation of layer-specific messages needs to be done in child classes
        return [...this.messages_];
    }

    /**
     * Add a message to the layer
     * @param {object} message 
     */
    addMessage(message) {
        if (this.messages_.indexOf(message) > -1) return;
        this.messages_.push(message);
        this.fire("layer_message", this.getMessages());
    }

    /**
     * Remove a message from the layer
     * @param {object} message 
     */
    removeMessage(message) {
        const index = this.messages_.indexOf(message);
        if (index > -1) {
            this.messages_.splice(index, 1);
            this.fire("layer_message", this.getMessages());
        }
    }

    /**
     * @param {string} attributeField 
     * @returns {string[]} array of unique values
     */
    async getUniqueValues(attributeField) {
        return [];
    }

    /**
     * @returns {string[]} array of attribute names
     */
    getAttributeNames() {
        return Object.keys(this.attributeFields);
    }

}