Source: map/LayerManager.js

import { WMSLayer } from "./layer/WMSLayer/WMSLayer.js";
import { WFSLayer } from "./layer/WFSLayer/WFSLayer.js";
import { Layer } from "./layer/Layer/Layer.js";
import { GeoJSONLayer } from "./layer/GeoJSONLayer/GeoJSONLayer.js";
import { EventObject } from "../events/EventObject.js";
import { OpusLayer } from "./layer/OpusLayer/OpusLayer.js";
import { IndexBinder } from "./importer/binder/IndexBinder.js";
import { FilterLayer } from "./layer/FilterLayer/FilterLayer.js";
import { OgcBinder } from "./importer/binder/OgcBinder.js";
import { MeasurementsLayer } from "./layer/MeasurementsLayer/MeasurementsLayer.js";
import { loadLayer } from "./utils/loadLayer.js";
import { RELOAD_METADATA_ERROR } from "./utils/messageTemplates.js";
import { PromiseQueue } from "../media/data/PromiseQueue.js";

export { LayerManager };

/**
 * LayerManager class for getting and handling layers
 * 
 * @author sjaswal <shahzeib.jaswal@awi.de>
 * @author awalter <andreas.walter@awi.de>
 * @author rhess <robin.hess@awi.de>
 * 
 * @memberof vef.map
 */
class LayerManager extends EventObject {

    /**
     * Creates an empty LayerManager instance to which Layers can be added
     * using a maps-project configuration or manually using the
     * method addLayer.
     */
    constructor() {
        super({
            "layermanager_add_layer": [],
            "layermanager_remove_layer": [],
            "layermanager_filterlayer_change": [],
            "layermanager_layer_mouseover": [],
            "layermanager_layer_mouseout": [],
            "layermanager_toggle_selection": []
        });

        this.layers_ = {};
        this.filterTimeout_ = null;
        this.reloadQueue_ = new PromiseQueue();
    }

    /**
     * Add layer to the LayerManager using a syntax that is 
     * compatible to the Layer's constructor.
     * 
     * @param {object[]} layers layer configs
     * @param {object[]} cache cached layer configs
     * 
     * @returns {Promise} resolved layers
     */
    loadLayers(layers, cache) {
        return new Promise((resolve) => {
            this.resolveCatalogLayers_(layers).then(catalogLayers => {
                const servicesToReload = {
                    "wms": [],
                    "wfs": []
                };
                for (let id in layers) {
                    let inCatalog = false;
                    let cachedLayer = (id in cache) ? cache[id] : {};
                    if (("catalogId" in layers[id]) && (layers[id].catalogId in catalogLayers)) {
                        cachedLayer = catalogLayers[layers[id].catalogId];
                        inCatalog = true;
                    }
                    const layer = loadLayer(layers[id], cachedLayer, id);
                    this.addLayer(layer);

                    // Collect layers that could not be updated from the catalog
                    if (("catalogId" in layers[id]) && !inCatalog && ["wms", "wfs"].includes(layer.type)) {
                        if (!servicesToReload[layer.type].includes(layer.serviceUrl)) {
                            servicesToReload[layer.type].push(layer.serviceUrl)
                        }
                    }
                }
                // reload service metadata of layers that cannot be loaded from the catalog
                for (let type in servicesToReload) {
                    for (let url of servicesToReload[type]) {
                        this.reloadQueue_.enqueue(() => this.reloadServiceMetadata(url, type));
                    }
                }
                resolve();
            });
        });
    }

    /**
     * Resolves catalog layers by their IDs.
     *
     * This function takes an object of layers, extracts the catalog IDs from the layers,
     * and fetches the corresponding layers from the catalog using the IndexBinder.
     *
     * @param {Object} layers - An object containing layer information.
     * @returns {Promise<Object>} A promise that resolves to an object containing the fetched layers.
     */
    async resolveCatalogLayers_(layers) {
        const catalogIds = [];

        for (let id in layers) {
            if ("catalogId" in layers[id]) {
                catalogIds.push(layers[id].catalogId);
            }
        }

        if (catalogIds.length == 0) {
            return {};
        } else {
            try {
                const layers = await new IndexBinder().fetchLayersByIds(catalogIds);
                return layers;
            } catch (e) {
                console.warn(e);
                return {};
            }
        }
    }

    /**
     * Add an existing Layer to the LayerManager
     * 
     * @param {Object} layer
     */
    addLayer(layer) {
        if (!(layer instanceof Layer)) return;

        this.layers_[layer.uniqueId] = layer;

        layer.on("layer_request_remove", () => this.fire("layermanager_remove_layer", layer));

        if (layer instanceof FilterLayer) {
            this.initFilterLayer_(layer);
        } else if ((layer instanceof MeasurementsLayer) || (layer instanceof OpusLayer) || (layer instanceof GeoJSONLayer) || (layer instanceof WFSLayer)) {
            layer.on("layer_mouseover", e => this.fire("layermanager_layer_mouseover", e));
            layer.on("layer_mouseout", e => this.fire("layermanager_layer_mouseout", e));
        }

        this.fire("layermanager_add_layer", layer);
    }

    /**
     * Method that returns a layer from LayerManager based on UniqueId provided
     * 
     * @param {string} id 
     */
    getLayerById(id) {
        if (this.layers_.hasOwnProperty(id)) {
            return this.layers_[id];
        }
    }

    /**
     * Reloads the metadata of all layers from the given service identified by url and type
     * @param {string} url 
     * @param {string} type 
     */
    reloadServiceMetadata(url, type) {
        this.forEach(layer => {
            if ((url == layer.serviceUrl) && (type == layer.type)) {
                layer.fire("layer_loading", layer);
            }
        });

        return OgcBinder.getCapabilities(null, url, type).then(data => {
            this.forEach(layer => {
                if ((url == layer.serviceUrl) && (type == layer.type)) {
                    for (let i = 0; i < data.length; ++i) {
                        for (let id in data[i].layers) {
                            if ((data[i].layers[id].name == layer.cache.name) || (data[i].layers[id].name == layer.name)) {
                                layer.cache = data[i].layers[id].cache;
                                if ("catalogId" in layer.cache) layer.config.catalogId = layer.cache.catalogId;
                                layer.removeMessage(RELOAD_METADATA_ERROR);
                                layer.fire("layer_update", layer);
                            }
                        }
                    }
                }
            });
        }).catch(() => {
            this.forEach(layer => {
                if ((url == layer.serviceUrl) && (type == layer.type)) {
                    layer.addMessage(RELOAD_METADATA_ERROR);
                }
            });
        }).finally(() => {
            this.forEach(layer => {
                if ((url == layer.serviceUrl) && (type == layer.type)) {
                    layer.fire("layer_loaded", layer);
                }
            });
        });
    }

    /**
     * Reloads the metadata of all layers
     * @param {string} url 
     * @param {string} type 
     */
    reloadAllServiceMetadata() {
        const reloaded = { wms: [], wfs: [] };
        this.forEach(layer => {
            if (layer instanceof WMSLayer || layer instanceof WFSLayer) {
                const type = layer.type.toLowerCase();
                if (!reloaded[type].includes(layer.serviceUrl)) {
                    reloaded[type].push(layer.serviceUrl);
                    this.reloadServiceMetadata(layer.serviceUrl, type);
                }
            }
        });
    }

    /**
     * Iterate over each layer inside the LayerManager
     * 
     * @param {function} callback
     */
    forEach(callback) {
        for (let i in this.layers_) {
            callback(this.layers_[i]);
        }
    }

    /**
     * Clear LayerManager and dispose layers
     */
    dispose() {
        this.forEach(l => l.dispose());
        this.layers_ = {};
    }

    /**
     * @private
     */
    initFilterLayer_(layer) {
        const applyFilter = () => {
            if (this.filterTimeout_) clearTimeout(this.filterTimeout_);
            this.filterTimeout_ = setTimeout(() => {
                this.fire("layermanager_filterlayer_change", this);
                this.filterTimeout_ = null;
            }, 100);
        };
        layer.on("layer_select", applyFilter);
        layer.on("layer_deselect", applyFilter);
    }

    /**
     * Get selected FilterLayers
     * @returns {object[]} filters
     */
    getActiveFilter() {
        const mapping = {};
        const filters = [];

        this.forEach(layer => {
            if ((layer instanceof FilterLayer) && layer.active) {
                if (!(layer.targetLayer in mapping)) mapping[layer.targetLayer] = {};
                if (!(layer.column in mapping[layer.targetLayer])) {
                    const excluded = Object.keys(this.layers_);
                    const index = excluded.indexOf(layer.targetLayer);
                    if (index > -1) excluded.splice(index, 1);
                    const f = {
                        excludedLayers: excluded,
                        column: layer.column,
                        values: [layer.values]
                    };
                    mapping[layer.targetLayer][layer.column] = f;
                    filters.push(f);
                } else {
                    const f = mapping[layer.targetLayer][layer.column];
                    f.values.push(layer.values);
                }
            }
        })

        return filters;
    }
}