Source: mixins/ol-map-styles.js

/**
 * @desc A mixin object containing map styles for the OpenLayers map.
 * @module MapStylesMixin
 */
import { Style, Stroke, Fill, Circle, RegularShape, Text, Icon } from 'ol/style';
import { LineString, Point } from 'ol/geom';

const labelFont = '14px Calibri,sans-serif';

export const MapStylesMixin = {
    data() {
        return {

            shapeStyle: new Style({
                stroke: new Stroke({
                    color: '#3399CC',
                    width: 1,
                }),
                fill: new Fill({
                    color: 'rgba(255,255,255, 0.5)',
                }),
            }),

            selectStyle: new Style({
                zIndex: 1000,
                stroke: new Stroke({
                    color: '#3399CC',
                    width: 3,
                }),
                fill: new Fill({
                    color: 'rgba(255,255,255, 0.3)',
                }),
                image: new Circle({
                    fill: new Fill({
                        color: '#3399CC'
                    }),
                }),
            }),

            positionStyle: new Style({
                image: new Icon({
                    anchor: [0.5, 0.5],
                    anchorXUnits: 'fraction',
                    anchorYUnits: 'fraction',
                    src: 'my_location.svg'
                }),
            }),

            labelStyleYellow: new Style({
                zIndex: 2000,
                text: new Text({
                    font: labelFont,
                    fill: new Fill({
                        color: "yellow"
                    }),
                    textBaseline: 'middle',
                    offsetY: 0
                }),
            }),

            labelStyleBlack: new Style({
                zIndex: 2000,
                text: new Text({
                    font: labelFont,
                    fill: new Fill({
                        color: "black"
                    }),
                    textBaseline: 'middle',
                    offsetY: 0
                }),
            }),
        }
    },
    methods: {
         /**
         * Returns the style for a boundary feature.
         * @param {Feature} feature - The boundary feature.
         * @returns {Style} The style for the boundary feature.
         */
        getBoundaryStyle(feature) {           
            let color = feature.get('color_code') ?? '#f0f0f0';
            let style = this.styleCache["B" + color];
            if (!style) {
                style = new Style({
                    stroke: new Stroke({
                        color: '#000000',
                    }),
                    fill: new Fill({
                        color: color,
                    }),
                });
                this.styleCache["B"+color] = style;
            }
            return style;
        },

        /**
         * Returns the style for a given point based on whether it is clustered or not.
         *
         * @param {Feature} feature - The feature for which to get the style.
         * @param {boolean} clustered - Indicates whether the feature is clustered or not.
         * @param {number} zoom - The current zoom level of the map.
         * @returns {Style} The style for the feature.
         */
        getPointStyle(feature, clustered, zoom) {
            let style;
            let radius = Math.round(zoom*2/3);
            if (!clustered) {
                let color = feature.get('color_code') ?? '#f0f0f0';
                style = this.styleCache["P"+color+radius];
                if (!style) {
                    style = new Style({
                        image: new Circle({
                            radius: radius,
                            stroke: new Stroke({
                                color: '#707070',
                            }),
                            fill: new Fill({
                                color: color,
                            }),
                        }),
                    });
                    this.styleCache["P"+color+radius] = style;
                }
            } else {
                const size = feature.get('features').length;
                style = this.styleCache["C" + size];
                if (!style) {
                    style = new Style({
                        image: new Circle({
                            radius: 7,
                            stroke: new Stroke({
                                color: '#fff',
                            }),
                            fill: new Fill({
                                color: '#3399CC',
                            }),
                        }),
                        text: new Text({
                        text: size.toString(),
                            fill: new Fill({
                                color: '#fff',
                            }),
                        }),
                    });
                    this.styleCache["C" + size] = style;
                }
            }
            return style;
        },
        
        /**
         * Returns the style for a given observation point based on zoom.
         *
         * @param {Feature} feature - The feature for which to get the style.
         * @param {number} zoom - The current zoom level of the map.
         * @returns {Style} The style for the feature.
         */
        getObservationPointStyle(feature, zoom) {
            let style;
            //if (feature.get('extension') == '.jpg') {
            if (feature.getId() && feature.getId().startsWith("f")) {  // it is an image
                let compass = feature.get('compass');
                if (compass == null) {
                    compass = 0;
                }
                style = this.styleCache[".jpg" + compass];
                if (!style) {
                    style = new Style({
                        image: new Icon({
                            anchor: [0.5, 0.8],
                            anchorXUnits: 'fraction',
                            anchorYUnits: 'fraction',
                            src: 'viewpoint.svg',
                            scale: 2,
                            rotateWithView: true,
                            rotation: Math.PI * compass / 180,
                        }),
                    });
                    this.styleCache[".jpg" + compass] = style;
                }
            } else {
                let radius = Math.round(zoom * 2 / 3);
                style = this.styleCache["Pred" + radius];
                if (!style) {
                    style = new Style({
                        image: new Circle({
                            radius: radius,
                            stroke: new Stroke({
                                color: '#707070',
                            }),
                            fill: new Fill({
                                color: "red",
                            }),
                        }),
                    });
                    this.styleCache["Pred" + radius] = style;
                }
            }
            return style;
        },

        /**
         * Generates an array of styles for a given feature.
         * @param {Feature} feature - The feature to generate styles for.
         * @returns {Array<Style>} - An array of styles for the feature.
         */
        selectedStyleFunction(feature) {
            let styles = [];
            styles.push(this.selectStyle);
            let geometry = feature.getGeometry();
            let type = geometry.getType();
            let point, label, line, style, text, segment, centerPoint, pointCoords;
            if (type == "Polygon") {
                line = new LineString(geometry.getCoordinates()[0]);
                centerPoint = geometry.getInteriorPoint();
                label = this.formatArea(geometry);
                style = this.selectedBaseLayer.value == 1 ? this.labelStyleBlack.clone() : this.labelStyleYellow.clone();
                if (line.getCoordinates().length > 3) style.getText().setText(label);
                style.setGeometry(centerPoint);
                styles.push(style);
            }
            if (type != "Point") {
                if (!line) line = geometry;
                if (line.forEachSegment) {
                    let self = this;
                    line.forEachSegment(function (a, b) {
                        segment = new LineString([a, b]);
                        label = self.formatLength(segment);
                        style = self.selectedBaseLayer.value == 1 ? self.labelStyleBlack.clone() : self.labelStyleYellow.clone();
                        pointCoords = segment.getCoordinateAt(0.5);
                        if (type == "Polygon") {
                            let pixel = self.map.getPixelFromCoordinate(pointCoords); 
                            let unitVectorAB = self.unitVector(centerPoint.getCoordinates(), pointCoords);
                            let wh = self.getTextDimensions(label, labelFont);
                            let pixelOffset = [unitVectorAB[0] * (wh[0] / 2 + 2), -unitVectorAB[1] * (wh[1] / 2 + 2)];
                            pixel[0] += pixelOffset[0];
                            pixel[1] += pixelOffset[1];
                            pointCoords = self.map.getCoordinateFromPixel(pixel);
                        }
                        point = new Point(pointCoords);
                        style.setGeometry(point);
                        style.getText().setText(label);
                        styles.push(style);
                    });
                }    
            }
            return styles;
        },

        /**
         * Get the dimensions of a given text string.
         * @param {*} text 
         * @returns [width, height]
         */
        getTextDimensions(text, font) {
            var span = document.createElement('span');
            span.style.font = font;
            span.textContent = text;
            document.body.appendChild(span);
            var width = span.offsetWidth;
            var height = span.offsetHeight;
            document.body.removeChild(span);
            return [width, height];
        },

        /**
         * Calculates the unit vector from point a to point b.
         * @param {number[]} a - The coordinates of point a.
         * @param {number[]} b - The coordinates of point b.
         * @returns {number[]} The unit vector from point a to point b.
         */
        unitVector(a, b) {
            // Calculate the vector from a to b
            var vectorAB = [b[0] - a[0], b[1] - a[1]];
            // Calculate the length of the vector
            var lengthAB = Math.sqrt(vectorAB[0] * vectorAB[0] + vectorAB[1] * vectorAB[1]);
            // Normalize the vector (make it a unit vector)
            if (lengthAB == 0) return [0, 0]; // Prevent division by zero (if a and b are the same point
            else return [vectorAB[0] / lengthAB, vectorAB[1] / lengthAB];         
        },

        /**
         * Extends a line segment from point a to point b by a specified distance.
         * @param {number[]} a - The coordinates of point a.
         * @param {number[]} b - The coordinates of point b.
         * @param {number} distance - The distance by which to extend the line segment.
         * @returns {number[]} The coordinates of the extended point c.
         */
        extendLine(a, b, distance) {
            var unitVectorAB = this.unitVector(a, b);
            var c = [b[0] + unitVectorAB[0] * distance, b[1] + unitVectorAB[1] * distance];
            return c;
        },
    }
}