Source: map/layer/WMSLayer/WMSLayer.js

import { isEqual } from "lodash";
import { ServiceLayer } from "../ServiceLayer/ServiceLayer.js";
import { colorScaleToWmsParams, copyColorScale } from "../../../ui/color/Utils.js";
import { WMSLayerProxyLeaflet } from "./WMSLayer.proxy.leaflet.js";
import { FeatureInfoHelper } from "../../utils/FeatureInfoHelper.js";
import { WMSLayerProxy } from "./WMSLayer.proxy.js";
import { POPUP_NETWORK_ERROR, POPUP_PARSING_ERROR } from "../../utils/messageTemplates.js";
import { WMSLayerSchema } from "./WMSLayer.schema.js";

export { WMSLayer }

/**
 * WMS Layer Implementation
 * 
 * @author rhess <robin.hess@awi.de>
 * @author sjaswal <shahzeib.jaswal@awi.de>
 * @memberof vef.map.layer
 */
class WMSLayer extends ServiceLayer {

    /**
     * Set all properties for the layer based on the options object.
     * 
     * @param {object} config 
     * @param {object} cache 
     * @param {string} id 
     */
    constructor(config, cache, id) {
        // calling parent constructor
        super(config, cache, id);

        // init default values and define getters and setters
        this.setSchema_(WMSLayerSchema.getSchema());

        // internal properties
        this.zIndex_ = 0;
        this.filterParams_ = {};
        this.featureInfoHelper_ = new FeatureInfoHelper();
        this.activeLayer_ = new WMSLayerProxy();

        // framework specific map implementations
        this.layerProxies_.leaflet = WMSLayerProxyLeaflet;

        this.featureInfoHelper_.on("loading_error", () => this.addMessage(POPUP_NETWORK_ERROR));
        this.featureInfoHelper_.on("parsing_error", () => this.addMessage(POPUP_PARSING_ERROR));
        this.featureInfoHelper_.on("loaded", () => {
            this.removeMessage(POPUP_NETWORK_ERROR);
            this.removeMessage(POPUP_PARSING_ERROR);
        });
    }

    /**
     * create a map layer based on the service options
     * 
     * @param {string} type e.g. "leaflet"
     * @private
     */
    createMapLayer_(type, options) {
        return super.createMapLayer_(type, {
            url: this.serviceUrl,
            layers: this.getRequestName(),
            version: this.serviceVersion,
            attribution: this.attribution,
            format: this.format
        });
    }

    /**
     * Helper method to update the mapLayer according to
     * the current state. Might be necessary after switching the map type.
     * calls internal setters to apply options to the active mapLayer
     * 
     * @private
     */
    updateMapLayer_() {
        if (Number.isFinite(this.opacity)) this.activeLayer_.setOpacity(this.opacity);
        if (Number.isFinite(this.zIndex_)) this.activeLayer_.setZIndex(this.zIndex_);
        this.setProjection(this.crs_);
        this.activeLayer_.setFormat(this.format);

        // set the color scale or a style if defined
        if (this.colorScale) {
            this.setColorScale(this.colorScale, true);
        } else if (this.queryParams.styles) {
            this.setStyle(this.queryParams.styles, true);
        } else if ((this.serviceSoftware == "rasdaman") && !this.getStyle() && (Object.keys(this.availableStyles).length > 0)) {
            // apply first style for rasdaman, because the default style is not returned automatically be the service
            this.setStyle(Object.keys(this.availableStyles)[0], true);
        } else {
            this.setStyle("", true);
        }

        // apply filter, static filter and static query parameters
        this.applyFilter(true);

        // reload was intentionally prevented in the setters to call it in the end
        this.reload();
    }

    /**
     * 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() {
        let url = this.legendUrl || `${this.serviceUrl}?REQUEST=GetLegendGraphic`;
        const urlLowerCase = url.toLowerCase();

        const parts = url.split("?");
        if (urlLowerCase.includes("request=getlegendgraphic") && (parts.length == 2)) {
            const params = parts[1].split("&");
            const style = this.getStyle();

            // set current style if the predefined style is empty.
            // cannot be done with "addParam" because "style=" already exists as an empty property
            for (let i = 0; i < params.length; ++i) {
                if (params[i].toLowerCase() == "style=") {
                    params[i] += encodeURIComponent(style);
                }
            }

            // only add param if it does not exist
            const addParam = (key, value) => {
                if (value && !urlLowerCase.includes(key.toLowerCase() + "=")) {
                    params.push(key + "=" + encodeURIComponent(value));
                }
            };

            addParam("SERVICE", "WMS");
            addParam("LAYER", this.getRequestName());
            addParam("STYLE", style);
            addParam("ENV", this.queryParams.env);
            addParam("TRANSPARENT", "true");
            addParam("FORMAT", this.format);
            addParam("VERSION", this.serviceVersion);
            addParam("LEGEND_OPTIONS", "forceLabels:on;fontSize:16;fontAntiAliasing:true;fontName:PT Sans");

            url = parts[0] + ((params.length) ? ("?" + params.join("&")) : "");
        }

        const image = document.createElement("img");
        image.src = url;

        return image;
    }

    /**
     * Switches the projection for the visualization
     * of the layer. CRS hast to be included in availableCRS
     * 
     * @param {string} crs target crs code
     * @returns {boolean} returns true if successful
     */
    setProjection(crs) {
        const success = super.setProjection(crs);
        if (success) {
            this.activeLayer_.setLayers(this.getRequestName());
        }
        return success;
    }

    /**
     * Set the style of the layer. Does not check if a style exists in availableStyles (non-listed styles are allowed)
     * 
     * @param {string} styleName style name
     * @param {boolean} noReload prevents reload if true
     */
    setStyle(styleName, noReload) {
        // remove "env" from colorscale settings
        if (typeof this.queryParams.env == "string") delete this.queryParams.env;

        if (styleName) {
            this.queryParams.styles = styleName;
        } else if ("styles" in this.queryParams) {
            delete this.queryParams.styles;
        }

        this.colorScale = null;

        if (!noReload) this.reload();
    }

    /**
     * Get the active style of the layer.
     */
    getStyle() {
        return ("styles" in this.queryParams) ? this.queryParams.styles : "";
    }

    /**
     * Set the colorscale for the layer
     * 
     * @param colorScale 
     * @param {boolean} noReload prevents reload if true
     */
    setColorScale(colorScale, noReload) {
        this.colorScale = copyColorScale(colorScale);

        const params = colorScaleToWmsParams(colorScale);
        Object.assign(this.queryParams, params);

        if (!noReload) this.reload();
    }

    /**
     * Get the active colorscale for the layer
     */
    getColorScale() {
        return (this.colorScale) ? copyColorScale(this.colorScale) : null;
    }

    getQueryParams_() {
        return Object.assign({}, this.queryParams, this.filterParams_);
    }

    /**
     * Reload the Layer on the map
     */
    reload() {
        const params = this.getQueryParams_();

        this.activeLayer_.setParams(params);
        this.activeLayer_.reload();
    }

    /**
     * Apply a filter on the layer. Uses WMS filter string
     * 
     * @param {boolean} noReload prevents reload if true
     */
    applyFilter(noReload) {
        const params = (this.filter) ? this.createFilterParams_(this.filter) : {};
        const changed = !isEqual(params, this.filterParams_);
        this.filterParams_ = params;
        if (!noReload && changed) this.reload();
    }

    /**
     * Get feature info for a specific location.
     * e.g. for displaying it in a map-popup
     * 
     * @param {object} options getFeatureInfo params from map 
     * @returns {Promise} feature info promise
     */
    getFeatureInfo(options) {
        const helper = this.featureInfoHelper_;
        return (this.serviceSoftware.toLowerCase() in helper)
            ? helper[this.serviceSoftware.toLowerCase()](this, options)
            : helper.geoserver(this, options)
            ;
    }

    /**
     * Set the z-index of a layer
     * @param {number} index 
     */
    setZIndex(index) {
        this.zIndex_ = index;
        this.activeLayer_.setZIndex(index);
    }

    /**
     * Get the bounding box as an object
     * 
     * @returns {object} { min: {x, y}, max: {x, y}, crs: "CRS:84" }
     */
    getBounds() {
        return {
            min: {
                x: (this.minX < this.maxX) ? this.minX : this.maxX,
                y: (this.minY < this.maxY) ? this.minY : this.maxY

            },
            max: {
                x: (this.minX > this.maxX) ? this.minX : this.maxX,
                y: (this.minY > this.maxY) ? this.minY : this.maxY
            },
            crs: "CRS:84"
        }
    }

    setFormat(format) {
        this.format = format;
        this.activeLayer_.setFormat(format);
        this.activeLayer_.reload();
    }

}