Source: mixins/ol-map.js

/**
 * @desc A mixin object containing common map methods
 * @module MapMixin
 */

import { View } from "ol";
import { Circle, Style, Stroke, Fill, Icon, Text } from "ol/style";
import Feature from "ol/Feature";
import { transform } from "ol/proj";
import { get as getProjection } from 'ol/proj';
//import * as Extent from 'ol/extent';
import { getCenter } from "ol/extent";
import { StatisticsMixin } from "./statistics"; 

export const MapMixin = {
    mixins: [StatisticsMixin],
    data() { 
        return {
            styleCache: {},
        }
    },
    methods: {

        /**
         * Returns the coordinates of the map based on the client coordinates.
         * @returns {Array<number>} The coordinates of the map.
         */
        coordinates() {
            return this.map.getCoordinateFromPixel([this.$store.clientCoordinates[0]/this.pixelRatio, this.$store.clientCoordinates[1]/this.pixelRatio]);
        },
            
        /**
         * Reprojects the given features from the old projection to the new projection.
         * @param {ol.Feature[]} features - The features to be reprojected.
         * @param {ol.proj.ProjectionLike} oldProjection - The old projection of the features.
         * @param {ol.proj.ProjectionLike} newProjection - The new projection to reproject the features to.
         */
        reprojectFeatures(features, oldProjection, newProjection) {
            this.$store.working = true;
            for (let feature of features) {
                feature.getGeometry().transform(oldProjection, newProjection);
            }
            this.$store.working = false;
        },

        /**
         * Sets the SRID (Spatial Reference System Identifier) for the map.
         * @param {number} val - The SRID value to set.
         */
        setSrid(val) {
            const currentView = this.map.getView();
            const currentCenter = currentView.getCenter();
            const newProjection = getProjection("EPSG:" + val);
            const newCenter = transform(currentCenter, this.projection, newProjection);
            const newView = new View({
                projection: newProjection,
                center: newCenter,
                zoom: this.zoom,
            });
            this.map.setView(newView);
            this.geolocation.setProjection(newProjection);
            this.reprojectFeatures(this.boundariesSource.getFeatures(), this.projection, newProjection);
            // this.reprojectFeatures(this.clusteredSource.getFeatures(), this.projection, newProjection);          
            this.reprojectFeatures(this.pointSource.getFeatures(), this.projection, newProjection);           
            this.reprojectFeatures(this.editableSource.getFeatures(), this.projection, newProjection);
           //this.positionFeature.getGeometry().setCoordinates(this.geolocation.getPosition());
            this.projection = newProjection;
        },

        /**
         * Show history of selected feature
         * @param {*} options 
         */
        async showHistory(options) {
            if (!this.selectedFeature) {
                // no features selected, show history for clicked point on map
                //let coords = this.coordinates();
                let coords = this.cursorXY;
                let o = {
                    x: coords[0],
                    y: coords[1],
                    srid: this.srid,
                    scaleFactor: this.selectedTitle.scale_factor,
                    sources: this.sources.map(x => ({ file: x.source, label: x.label, value: null })).sort((a, b) => a.label.localeCompare(b.label))
                };
                let res = await this.post("Gdal/GetHistory", o);
                if (res) {
                    console.log(res);
                    this.$store.chart.props = {
                        chartType: "line",
                        data: res,
                        seriesName: this.$t("Value"),
                        labelField: "label",
                        valueField: "value",
                        title: this.selectedTitle.label,
                        unit: "",
                        xScale: 'time',
                        trendLine: true
                    };
                    this.$store.chart.show = true;
                    // this.$refs.chart.showChart("line", res, "label", "value", null, this.selectedTitle.label, "", true, true)
                }
            } else {
                // show history for selected point
                let f = this.selectedFeature;
                if (f.getGeometry().getType() == "Point") {
                    let res = await this.get("Home/GetPointHistory", { pointId: f.get("point_id"), indicatorId : this.selectedIndicator.value });
                    if (res) {
                         this.$store.chart.props = {
                            chartType: "line",
                            data: res,
                            labelField: "date",
                                valueField: "value",
                            seriesField: "depth",
                            title: this.selectedIndicator.label + " - " + f.get("name"),
                            unit: this.selectedIndicator.unit,
                            xScale: 'time',
                            trendLine: true
                        };
                        this.$store.chart.show = true;
                    //     this.$refs.chart.showChart("line", res, "date", "value", "depth", this.selectedIndicator.label + " - " + f.get("name"), this.selectedIndicator.unit, true, true);
                    }
                }
            }
        },

        /**
         * Show properties of selected feature in a popup
         * @param {*} options 
         */
        async showProps(options) {
            this.$store.popup.component = "ol-map-popup";
            this.$store.popup.props = {
                title: this.selectedFeature.get("name") ?? "Feature",
                parent: this,
            };
            this.$store.popup.show = true;
        },

        /**
         * Prepares a frequency chart based on the given points.
         *
         * @param {Array} points - The array of points.
         * @param {boolean} numerical - Indicates whether the points are numerical or not.
         * @param {number} decimalsForStats - The number of decimals to round the statistics to.
         * @returns {Object} - The prepared frequency chart object.
         */
        prepareFrequencyChart(points, numerical, decimalsForStats) {
            let v;
            if (numerical) {
                v = points.map(p => this.roundTo(p.get("value"), decimalsForStats)); //.toFixed(1));
            } else {
                v = points.map(p => p.get("description"));
            }
            let f = {};
            let stat = {};
            v.forEach(v => {
                f[v] = (f[v] || 0) + 1;
            });
            if (numerical) {
                stat = this.statistics(v);
            }
            let fs = Object.keys(f).map(k => ({ label: k, value: f[k] }));
            fs.sort((a, b) => a.label - b.label);
            return {
                stat: stat,
                x: fs.map(f => f.label),
                y: fs.map(f => f.value)
            };
        },

        /**
         * Displays the frequencies of the selected indicator.
         *
         * @param {Object} options - The options for displaying the frequencies.
         * @returns {Promise<void>} - A promise that resolves when the frequencies are displayed.
         */
        async showFrequencies(options) {
            if (this.selectedIndicator) {
                let data = this.prepareFrequencyChart(this.pointFeatures, this.selectedIndicator.numerical, this.selectedIndicator.decimals_for_stats);
                this.$store.chart.props = {
                    chartType: "bar",
                    data: data,
                    stat: data.stat,
                    title: this.$t("Frequency") + " - " + this.selectedIndicator.label,
                    xyChart: true,
                    annotationAxis: "x",
                    xScale: "category",
                    feature: this.selectedFeature,
                    numerical: this.selectedIndicator.numerical,
                    unit: this.selectedIndicator.unit
                };
                this.$store.chart.show = true;
            }
        },

        /**
         * Retrieves feature statistics based on the selected feature.
         * @param {Object} options - The options for retrieving feature statistics.
         * @returns {Promise<void>} - A promise that resolves when the feature statistics are retrieved.
         */
        async showFeatureStats(options) {
            this.$store.working = true; 
            let gj = this.format.writeGeometry(this.selectedFeature.getGeometry());
            gj = JSON.parse(gj);
            gj.crs = {
                type: "name",
                properties: {
                    name: "EPSG:" + this.srid.toString()
                }
            };
            let data = await this.post("Gdal/GetAverageInGeometry", {
                file: this.selectedCatalog.source,
                geometry: JSON.stringify(gj),
                srid: this.srid.toString(),
                scale_factor: this.selectedTitle.scale_factor,
                no_data: this.selectedTitle.no_data.toString(),
            });
            this.$store.working = true;
            if (data) {
                let name = this.selectedFeature.get("name");
                if (!name && this.selectedFeature.get("props")) {
                    let nameProp = this.selectedFeature.get("props").find(p => p.name == "Name");
                    if (nameProp) name = nameProp.value;
                }
                this.$store.chart.props = {
                    chartType: "bar",
                    data: data,
                    stat: data.stat,
                    title: this.selectedTitle.label + " " + (name ?? "") + " (" + data.stat.n + " " + this.$t("points") + ")",
                    xyChart: true,
                    annotationAxis: "x",
                    xScale: "category",
                    feature: this.selectedFeature,
                    numerical: true,
                    unit: this.selectedTitle.unit
                };
                this.$store.chart.show = true;
            }
            this.$store.working = false;
        },

        /**
         * Shows the spectrum for the selected feature.
         * @async
         * @function showSpectrum
         * @returns {Promise<void>}
         */
        async showSpectrum() {
            let data = await this.get("User/GetSpectrum", {
                point_id: this.selectedFeature.get("point_id"),
                data_source_id: this.selectedDataSource.value
            });
            let ds = []; 
            for (let row of data) {
                for (let i = 0; i < row.wavelengths.length; i++) {
                    ds.push({ date: row.date, wavelength: row.wavelengths[i], value: row.values[i] });
                }
            }
            this.$store.chart.props = {
                chartType: "line",
                data: ds,
                labelField: "wavelength",
                valueField: "value",
                seriesField: "date",
                title: this.selectedIndicator.label + " - " + this.selectedFeature.get("name"),
                unit: "nm"
            };
            this.$store.chart.show = true;
            
            // this.$refs.chart.showChart("line", ds, "wavelength", "value", "date", this.selectedIndicator.label + " - " + this.selectedFeature.get("name"), 'nm', false, false);
        },

        /**
         * Rescales the given value based on the selected title's scale factor.
         * If the scale factor is a number, the value is multiplied by the scale factor.
         * If the scale factor is a formula, it is evaluated and applied to the value.
         * If the formula fails to evaluate, the original value is returned.
         * 
         * @param {number} value - The value to be rescaled.
         * @returns {number} The rescaled value.
         */
        rescale(value) {
            if (this.selectedTitle.scale_factor.indexOf('x') == -1) {
                value = (value * this.selectedTitle.scale_factor);
            } else {
                // this is a formula, so apply it to the value
                try {
                    let x = value;
                    value = eval(this.selectedTitle.scale_factor);
                } catch (e) {
                    // if the formula fails, just return the value
                }
            }
            return value;
        },

        /**
         * Creates a legend based on the provided color map.
         * @param {string} color_map - The color map in JSON format.
         */
        createLegend(color_map) {
            let a = JSON.parse(color_map);
            this.legend = [];
            for (let i = 5; i < a.length; i = i + 2) {
                a[i] = this.rescale(a[i]).toFixed(1);
            }
            for (let i = 5; i < a.length; i = i + 2) {
                let text, from, to;
                if (i == 5) {
                    text = "< " + a[i];
                    from = 0;
                    to = a[i];
                } else {
                    text = a[i - 2] + " - " + a[i];
                    from = +a[i - 2];
                    to = +a[i];
                }
                this.legend.push({ color_code : this.rgbaToHex(a[i+1]), text : text, from :from, to : to });
            }
        },

        /**
         * Animates the map view to the extent of a given source.
         *
         * @param {Source} source - The source to animate to.
         */
        animateToExtent(source) {
            let view = this.map.getView();
            let extent = source.getExtent();
            view.animate({
                zoom: view.getZoomForResolution(
                    view.getResolutionForExtent(extent, this.map.getSize()) * 1.20),
                center: getCenter(extent),
                duration: 1000,
            });
        },
    },
}