Source: mixins/ol-map-editing.js

/**
 * @desc A mixin object containing methods related to editing objects on the map.
 * @module MapEditingMixin
 */
import { Draw, Modify, Select, Snap } from "ol/interaction";
import { GeoJSON } from "ol/format";
import { noModifierKeys, primaryAction } from 'ol/events/condition';
import { getArea, getLength } from 'ol/sphere';
import Feature from "ol/Feature";
import { LineString, Point }  from "ol/geom";
import { fromLonLat } from "ol/proj";
import { tiHelp } from "@quasar/extras/themify";

export const MapEditingMixin = {
    methods: {
        /**
         * Reads custom features from the server and adds them to the editable source.
         * @returns {Promise<void>} A promise that resolves when the features are added.
         */
        async readCustomFeatures(id) {
            if (this.$store.userData == null) return;
            let ret = await this.get("User/GetCustomGeometry", {
                id: id
            });
            this.editableSource.clear();
            if (ret && ret.features) {
                let features = this.format.readFeatures(ret);
                this.reprojectFeatures(features, this.projectionForSave, this.projection);
                this.editableSource.addFeatures(features);
            }
        },      
        
        /**
         * Clears the interactions on the map.
         */
        clearInteractions() {
            this.map.removeInteraction(this.draw);
        },

        /**
         * Saves the geometry of a feature by sending it to the server.
         * @param {feature} feature - The feature to be saved.
         * @param {format} format - The format used to serialize the feature.
         * @param {projection} projection - The projection of the feature's geometry.
         * @param {projection} projectionForSave - The projection to transform the feature's geometry to before saving.
         * @returns {Promise<void>} A promise that resolves when the feature is successfully saved.
         */
        async saveFeature(feature, format, projection, projectionForSave) {
            let clonedFeature = feature.clone();
            if (format) {
                clonedFeature.getGeometry().transform(projection, projectionForSave);
            }
            format = format ?? new GeoJSON();
            let gj = format.writeFeatureObject(clonedFeature);
            let id = this.toStringIfNotNull(feature.get("custom_geometry_id"));
            let file_id = this.toStringIfNotNull(feature.get("file_id"));
            await this.post("User/UpdateCustomGeometry", {
                id: id,
                file_id: file_id,
                feature: JSON.stringify(gj)
            });
        },

        /**
         * Formats the length of a line.
         * @param {LineString} line - The line to calculate the length for.
         * @returns {string} The formatted length with unit (meters or kilometers).
         */
        formatLength (line) {
            const length = getLength(line);
            let output;
            if (length > 100) {
                output = Math.round((length / 1000) * 100) / 100 + ' km';
            } else {
                output = Math.round(length * 100) / 100 + ' m';
            }
            return output;
        },

        /**
         * Calculates the area of a polygon and formats it as a string.
         * If the area is greater than 10000, it is formatted in square kilometers (km²).
         * Otherwise, it is formatted in square meters (m²).
         * @param {Polygon} polygon - The polygon for which to calculate the area.
         * @returns {string} The formatted area string.
         */
        formatArea (polygon) {
            const area = getArea(polygon);
            let output;
            if (area > 10000) {
                output = Math.round((area / 1000000) * 100) / 100 + ' km\xB2';
            } else {
                output = Math.round(area * 100) / 100 + ' m\xB2';
            }
            return output;
        },

        /**
         * Gets the area of a given feature.
         * Only works for features with a Polygon geometry type.
         *
         * @param {feature} feature.
         */
        getFeatureArea(feature) {
            if (feature.getGeometry().getType() == "Polygon") {
                return getArea(feature.getGeometry()).toFixed(0);
            } else {
                return null;
            }
        },
        
        /**
         * Creates a new object for editing on the map.
         * 
         * @param {Object} options - The options for creating the new object.
         * @param {string} options.type - The type of object to create.
         * @param {number} options.geometry_type_id - The ID of the geometry type.
         */
        newObject(options) {
            let actionTypes = {
                'Point': 1,
                'LineString': 3,
                'Polygon': 2
            };
            this.showEditableSource = true;
            this.clearInteractions();
            this.draw = new Draw({
                type: options.type,
                source: this.editableSource,
                stopClick: true,
                condition: (e) => noModifierKeys(e) && primaryAction(e),
                style: this.selectedStyleFunction,
            });                 
            this.map.addInteraction(this.draw);
            //this.map.addInteraction(this.select);
            this.activeAction = options;
            this.draw.on('drawend', async (e) => {
                e.feature.set("geometry_type_id", actionTypes[this.activeAction.type]);
                e.feature.set("props", []);
                this.clearInteractions();
                this.selectedFeature = e.feature;
                this.draw = null;
                this.editProps();
            });
            // this.$mitt.on('escKeyPressed', () => {
            //     this.endObject();
            // });
        },

        /**
         * Ends the current object editing mode.
         */
        endObject() {
            // this.$mitt.off('escKeyPressed');
            this.clearInteractions();
            this.activeAction = null;
            if (this.select) {
                this.select.getFeatures().clear();
                this.selectCount = 0;
            }
            // this.$refs.contextmenu.hide();
        },

        /**
         * Deletes the selected object.
         * @async
         * @function deleteObject
         * @returns {Promise<void>}
         */
        async deleteObject() {
            if (await this.confirmDialog(this.$t("Delete selected object?"))) {
                await this.delete("User/DeleteCustomGeometry/" + this.selectedFeature.get("custom_geometry_id").toString());
                this.editableSource.removeFeature(this.selectedFeature);
                this.selectedFeature = null;
            };
        },

        filesUrl() {
            return [{
                url: this.axios.API.defaults.baseURL + "User/GetFile/"
                    + this.selectedFeature.get("custom_geometry_id") + "/"
                    + this.selectedFeature.get("file_id") + "/"
                    + this.selectedFeature.get("extension"),
                compass: this.selectedFeature.get("compass"),
                file_id : this.selectedFeature.get("file_id"),
            }];
        },

        compassChanged(compass, file_id) {
            this.selectedFeature.set("compass", compass);
            let features = this.editableLayer.getSource().getFeatures(); // Save the current features
            this.editableLayer.changed();
            this.editableLayer.getSource().refresh();
            this.editableLayer.getSource().addFeatures(features);
        },

        /**
         * Opens the property editor or the image viewer for the selected feature.
         * @param {Object} options - The options for editing the properties.
         */
        editProps(options) {
            if (this.selectedFeature) {
                this.infoTip.style.visibility = 'hidden';
                if (this.selectedFeature.get("extension") == ".jpg") {
                    this.$refs.imageViewerMap.show(this.filesUrl(), 0, true);
                } else {
                    this.$store.popup.component = "ol-map-props";
                    this.$store.popup.props = {
                        custom_geometry_id : this.selectedFeature.get("custom_geometry_id"),
                        geometry_type : this.selectedFeature.getGeometry().getType(),
                        title : this.$t("Location"),
                        parent : this,
                        feature : this.selectedFeature,
                        help: "Edit properties",
                        persistent: true
                    };
                    this.$store.popup.show = true;
                }
            }
        },

        addFileFeature(file) {  
            console.log("addFileFeature", file);
            let point = new Point(fromLonLat([file.lon, file.lat], this.projection));
            console.log("addFileFeature", point);
            let feature = new Feature({
                id: 'f' + file.id,
                geometry: point,
                custom_geometry_id: file.custom_geometry_id,
                file_id: file.id,
                extension: file.extension,
                compass: file.compass,
                name: file.name
            });
            feature.setId('f' + file.id);
            console.log("addFileFeature", feature);
            this.editableSource.addFeature(feature);
        }

    },
}