Source: map/layer/StaLayer/StaLayer.js

import { cloneDeep } from "lodash";
import { GeoJSONLayer } from "../GeoJSONLayer/GeoJSONLayer.js";
import { NETWORK_LOADING_ERROR } from "../../utils/messageTemplates.js";
import { STABuilder } from "../../filters/builders/STABuilder.js";
import { StaLayerSchema } from "./StaLayer.schema.js";
import { FilterBuilder } from "../../filters/index.js";

/**
 * STA Layer Implementation
 * 
 * @author rhess <robin.hess@awi.de>
 * 
 * @memberof vef.map.layer
 */
class StaLayer extends GeoJSONLayer {

    /**
     * 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);

        this.setSchema_(StaLayerSchema.getSchema());

        // overwrite geoJson getter/setter, so the geojson is not exported when saving the viewer
        Object.defineProperty(this, "geoJSON", {
            get() { return this.defaults.geoJSON; },
            set(val) { this.defaults.geoJSON = val; },
            configurable: true
        });

        this.loadThings_();
    }

    /**
     * 
     * @param {string} baseUrl 
     * @param {number} max 
     * @param {object} params 
     * @returns {object[]} values 
     */
    async fetch_(baseUrl, max, params) {
        this.fire("layer_loading", this);
        const values = [];


        Object.assign(params, {
            $count: "false"
        })

        let json = {};
        do {
            let url = json?.["@iot.nextLink"];

            if (!url) {
                let query = "";

                const addParam = (key, value) => {
                    if (value.length == 0) return;
                    query += ((query.length) ? "&" : "?") + key + "=" + value;
                }

                for (let key in params) {
                    if (Array.isArray(params[key])) {
                        for (let i = 0; i < params[key].length; ++i) {
                            addParam(key, params[key][i])
                        }
                    } else {
                        addParam(key, params[key])
                    }
                }
                url = baseUrl + query;
            }

            try {
                const response = await fetch(url);
                json = await response.json();
            } catch (e) {
                this.addMessage(NETWORK_LOADING_ERROR);
                break;
            }

            for (let i = 0; i < json.value.length; ++i) {
                values.push(json.value[i]);
            }
        } while (("@iot.nextLink" in json) && (!max || (values.length < max)))

        this.fire("layer_loaded", this);
        return values;
    }

    /**
     * @private
     */
    async loadThings_() {
        const geoJson = {
            type: "FeatureCollection",
            features: []
        };
        const params = {
            $expand: "Locations",
            $top: "1000"
        }
        if (this.filter) { // add the STA specific filter params to the params
            Object.assign(params, {
                $filter: this.filter
            })
        }

        // load things applying the params
        const values = await this.fetch_(this.url + "Things", null, params);

        for (let i = 0; i < values.length; ++i) {
            const value = values[i];
            if (!value?.Locations?.length) continue;
            geoJson.features.push({
                "type": "Feature",
                "geometry": cloneDeep(value.Locations[0].location),
                "properties": {
                    name: value?.name,
                    description: value?.description,
                    selfLink: value?.['@iot.selfLink'],
                    images: value?.properties?.images,
                    sourceLink: value?.properties?.['jsonld.id'] || "",
                    datastreams: null,
                    sensors: []
                }
            })
        }

        this.setData(geoJson);
    }

    async applyFilter() {
        // create a copy of the filter to allow editing the filter without changing the original filter
        const filters = cloneDeep(this.filter);

        // If no column is defined for the bounding box filter, use a default column
        for (let i = 0; i < filters.length; ++i) {
            if (!filters[i].column && (filters[i].type === "geometry")) {
                filters[i].column = "Locations/location";
            }
        }

        const harmonizedFilter = FilterBuilder.harmonizeFilter(filters);
        const sta_filter = STABuilder.getFilterParams(harmonizedFilter);
        this.filter = sta_filter["sta_filter"];
        await this.loadThings_();
    }

    /**
     * Return all datastreams for the given thing.
     * @param {*} thingUrl 
     * @returns {Promise}
     */
    async getDatastreams(thingUrl) {
        return await this.fetch_(`${thingUrl}/Datastreams`, null, {
            $expand: [
                "Sensor($select=@iot.id,name)",
                "ObservedProperty($select=@iot.id,name)"
            ],
            $top: "1000"
        });
    }

    /**
     * Return the observations for an observations url of a selected datastream
     * @param {*} observationsUrl 
     * @returns {Promise}
     */
    async getObservations(observationsUrl) {
        return await this.fetch_(observationsUrl, 1000, {
            $select: "resultTime,phenomenonTime,result",
            $orderby: "phenomenonTime desc, resultTime desc",
            $top: "1000"
        });
    }

    /**
     * Get Info of the last clicked features, requests datastreams for each clicked thing
     * @override
     * @returns {Promise} feature info
     */
    async getFeatureInfo() {
        const info = this.activeLayer_.getClickedFeatures();

        if (info?.data?.features) {
            for (let i = 0; i < info.data.features.length; ++i) {
                const properties = info.data.features[i]?.properties;
                if (!(Array.isArray(properties?.datastreams)) && properties?.selfLink) {
                    properties.datastreams = await this.getDatastreams(properties.selfLink);
                }
            }
        }

        return info;
    }

    /**
     * @param {string} attributeField
     * @returns {string[]} array of unique values
     */
    async getUniqueValues(attributeField) {
        if (!attributeField.includes('/')) {
            throw new Error("Malformed attribute field definition: " + attributeField);
        }
        let slashIndex = attributeField.indexOf('/');
        let staEntity = attributeField.substring(0, slashIndex);
        let staAttributePath = attributeField.substring(slashIndex + 1);
        let json = await this.fetch_(this.url + staEntity, 1000, {
            $select: staAttributePath
        });
        let values = json.map(item => this.getJSONValue(item, staAttributePath));
        return [...new Set(values)];
    }

    /**
     * @param {*} obj 
     * @param {*} path 
     * @returns value at the given path in the given JSON object or else undefined
     */
    getJSONValue = (obj, path) =>
        path.split('/').reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj);

    /**
     * Returns an array of possible attribute names for the checkbox filter with the following format convention:
     * 
     * "<STA Entity>/<path to the attribute>"
     * 
     * Examples: 'Sensors/properties/isVariantOf/name', 'Datastreams/unitOfMeasurement/symbol', 'ObservedProperties/name', 'Thing/name'
     * 
     * @returns {string[]} array of attribute names
     */
    getAttributeNames() {
        return ['Sensors/properties/isVariantOf/name', 'Datastreams/unitOfMeasurement/symbol', 'ObservedProperties/name', 'Things/name'];
    }

}

export { StaLayer };