Source: specific/mixins/ol-map.js

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

import { View } from "ol";
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 "@/common/mixins/statistics"; 
import { date } from "quasar";

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.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() {
            let f = this.selectedFeature;
            if (!f.get("point_id")) {   // show history for selected point on tiff layer
                let coords = this.selectedFeature.getGeometry().getCoordinates();
                let pixel = this.map.getPixelFromCoordinate(coords);
                let r = this.getActiveTiffLayer(pixel);
                let o = {
                    x: coords[0],
                    y: coords[1],
                    srid: this.srid,
                    scaleFactor: r.selectedTitle.scale_factor,
                    decimalsForStats: r.selectedTitle.decimals,
                    sources: r.selectedTitle.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.initPopup({component : "chart-popup", maximized: true,
                        chartType: "line",
                        data: res,
                        labelField: "label",
                        valueField: "value",
                        title: r.selectedTitle.label,
                        unit: r.selectedTitle.unit,
                        xScale: 'category',
                        trendLine: true,
                        pointRadius: 3
                    });
                }
            } else {  // show history for selected point
                if (f.getGeometry().getType() == "Point") {
                    let res = await this.get("User/GetPointHistory", { pointId: f.get("point_id"), indicatorId : this.selectedIndicator.value });
                    if (res) {
                         this.initPopup({component : "chart-popup", maximized: true,
                            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,
                            pointRadius: 3
                        });
                    }
                }
            }
        },

        /**
         * Show properties of selected feature in a popup
         * @param {*} options 
         */
        async showProps(options) {
            this.initPopup({component : "ol-map-popup", title : this.selectedFeature.get("name") ?? this.selectedFeature.get("Name") ?? "Feature"});   
        },

        /**
         * 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.initPopup({component : "chart-popup", maximized: true,
                    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
                });
            }
        },

        /**
         * 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(compare) {
            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/CalcStatisticsForGeometry", {
                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(),
            });

            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;

                }
                if (!name) name = this.selectedFeature.get("Name");
                if (compare) {

                    let id = this.selectedFeature.get("key");
                    let geometry_type_id = this.selectedFeature.get("geometry_type_id") ?? 3;
                
                    let thisData = { id: id, indicator_id : this.selectedCatalog.indicator_id, name: name, label: name, ...data.stat, x: data.x, y: data.y };
                    thisData.parent_id = id;
                    thisData.geometry_type_id = geometry_type_id;

                    this.$store.globalValues.indicator_id_val = this.selectedTitle.label;

                    let otherData = await this.get("Table/GetTable", {
                        dbFunction: "data.get_intersecting_geometries",
                        json: true,
                        pars: JSON.stringify({
                            id: id,
                            geometry_type_id: geometry_type_id,
                            indicator_id : this.selectedCatalog.indicator_id,
                        })
                    });
                    otherData = [thisData, ...otherData];

                    let action = this.getProps("/Compare");
                    action.path = "/Compare";  // see that user has permission to this path
                    action.data = otherData;
                    action.title = "Comparison of " + name + " with intersecting geometries for " + this.selectedTitle.label;

                    this.initPopup(action);

                } else {
                
                    this.initPopup({
                        component: "chart-popup", maximized: true,
                        chartType: "bar",
                        data: data,
                        stat: data.stat,
                        title: this.selectedTitle.label + " " + (name ?? "") + " (" + data.stat.n + " " + this.$t("points") + ")",
                        xyChart: true,
                        seriesName: this.$t("Frequency"),
                        annotationAxis: "x",
                        xScale: "category",
                        feature: this.selectedFeature,
                        numerical: true,
                        unit: this.selectedTitle.unit,
                        labelField: this.selectedTitle.label,
                    });
                }
            }
            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/${this.selectedDataSource[0].value}/${this.selectedFeature.get("point_id")}/${date.formatDate(this.selectedFeature.get("date"), "YYYY-MM-DD")}`);
            this.initPopup({component : "chart-popup", maximized: true,
                chartType: "line",
                data: data,
                labelField: "wavelength",
                valueField: "value",
                //seriesField: "date",
                title: this.selectedIndicator.label + " - " + this.selectedFeature.get("name"),
                unit: "nm"
            });
        },

        /**
         * 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, scale_factor) {
            if (scale_factor.indexOf('x') == -1) {
                value = (value / scale_factor);
            } else {
                // this is a formula, so apply it to the value
                try {
                    let x = value;
                    value = eval(scale_factor);
                } catch (e) {
                    // if the formula fails, just return the value
                }
            }
            return value;
        },

        updateColorMap(selectedTitle, tiffLegend) {
            let color_map = "";
            if (selectedTitle.numerical)  {
                color_map = selectedTitle.color_map;
                // color_map = '["interpolate", ["linear"], ["band", 1]';
                // color_map += ", 0, [255, 255, 255, 0]";
                // let prev = 0;
                // for (let i = 0; i < tiffLegend.length; i++) {
                //     if (tiffLegend[i].active) {
                //         color_map += ", " + tiffLegend[i].original + ", [" + this.hexToRgbaValues(tiffLegend[i].color_code) + "]";
                //     } else
                // }
                // color_map += ", 9999, [0, 0, 0, 0]]"; 
            } else {
                color_map = '["match", ["band", 1]';
                for (let i = 0; i < tiffLegend.length; i++) {
                    if (tiffLegend[i].active) {
                        color_map += ", " + tiffLegend[i].from + ", [" + this.hexToRgbaValues(tiffLegend[i].color_code) + "]";
                    }
                };
                if (color_map == '["match", ["band", 1]') {
                    color_map += ", 0, [255, 255, 255, 0]";
                }
                color_map += ", [255, 255, 255, 0]]";
            }
            selectedTitle.tiffLayer.setStyle({
                color: JSON.parse(color_map)
            }); 

            console.log("colorMap", color_map);
        },

        /**
         * Creates a legend based on the provided color map.
         * @param {string} color_map - The color map in JSON format.
         */
        createLegend(selectedTitle, tiffLegend) {
            selectedTitle.allActive = true;
            if (selectedTitle.description) {
                tiffLegend.length = 0;
                let a = selectedTitle.description;
                Object.keys(a).forEach( key => {
                    tiffLegend.push({ color_code: a[key].color_code, text: a[key].name, from: key, to: key, active: true });
                });
                tiffLegend.sort((a, b) => a.text - b.text);
                console.log("tiffLegend", tiffLegend);
            } else if (selectedTitle.color_map > "") {
                let a = JSON.parse(selectedTitle.color_map);
                tiffLegend.length = 0;
                // 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.tiffLegend.push({ color_code : this.rgbaToHex(a[i+1]), text : text, from :from, to : to });
                // }
                let start = 5;
                if (a[0] == "match") start = 4;
                let end = a.length - 2;
                if (start == 4) end = a.length - 3;
                for (let i = start; i < end; i = i + 2) {
                    let original = a[i];
                    a[i] = this.rescale(a[i], selectedTitle.scale_factor).toFixed(2);
                    let text = a[i];
                    let from = +a[i];
                    let to = null;
                    tiffLegend.push({ color_code: this.rgbaToHex(a[i+1]), text: text, from: from, to: to, active: true, original: original });
                }
            }
            this.updateColorMap(selectedTitle, tiffLegend);
        },

        /**
         * Animates the map view to the extent of a given source.
         *
         * @param {Source} source - The source to animate to.
         */
        animateToExtent(source) {
            if (source == null) {
                if (this.pointSource && this.pointSource.getFeatures().length > 0) {
                    source = this.pointSource;
                } else if (this.shapeSource && this.shapeSource.getFeatures().length > 0) {
                    source = this.shapeSource;
                } else {
                    source = this.editableSource;
                    let count = 0;         
                    for (let f of this.editableSource.getFeatures()) {
                        //if (f.getId().startsWith('g')) {
                            count++;
                        //}
                    }
                    if (count > 0) {
                        let f = source.getFeatures()[0];
                        if (count == 1 && f.getGeometry().getType() === 'Point') {
                            this.map.getView().setCenter(f.getGeometry().getCoordinates());
                            return;
                        }
                    }
                }
            }

            let view = this.map.getView();
            let extent = source.getExtent();
            let zoom = view.getZoomForResolution(view.getResolutionForExtent(extent, this.map.getSize()) * 1.20);
            if (zoom > 16) zoom = 16;
            view.animate({
                zoom: zoom,
                center: getCenter(extent),
                duration: 1000,
            });
        },
    },
}