PMA.UI Documentation by Pathomation

view/controls/overview.js

import { Control } from "ol/control";
import { View, Map } from "ol";
import * as olExtent from "ol/extent";
import VectorLayer from "ol/layer/Vector";
import TileLayer from "ol/layer/Tile";
import VectorSource from "ol/source/Vector";
import Feature from "ol/Feature";
import Collection from "ol/Collection";
import { Polygon } from "ol/geom";
import { fromExtent } from "ol/geom/Polygon";
import { Style, Stroke, Fill } from "ol/style";
import GeoJSON from "ol/format/GeoJSON";
import union from "@turf/union";
import difference from "@turf/difference";
import simplify from "@turf/simplify";
import { unByKey } from "ol/Observable";

/**
 * Displays an interface that shows an overview and current view of slide
 * @param {object} opt_options Options to initialize the overview control
 * @param {string} opt_options.target Target DOM element to add overview control
 * @param {number} opt_options.maxResolution Maximum resolution overview control can show
 * @param {string} [opt_options.tipLabel] Label for tracking button
 * @param {boolean} [opt_options.collapsed] Whether the control starts collapsed
 * @param {boolean} [opt_options.tracking] Whether the control has tracking capability
 * @param {object} [opt_options.stateManager] The state manager to keep settings in sync
 */
export class Overview extends Control {
    constructor(opt_options) {
        let options = opt_options || {};
        let element = document.createElement("div");

        super({
            element: element,
            target: options.target,
        });

        super.render = this.updateBox.bind(this);

        this.trackingFlag = options.tracking === true;
        this.stateManager = options.stateManager ? options.stateManager : null;

        if (this.stateManager) {
            if (!this.stateManager.overview) {
                this.stateManager.overview = {};
                this.stateManager.overview.collapsed = options.collapsed
                    ? options.collapsed
                    : false;
            }

            this.collapsed_ = this.stateManager.overview.collapsed === true;
        } else {
            this.collapsed_ = options.collapsed ? options.collapsed : false;
        }

        this.collapsible_ = options.collapsible ? options.collapsible : true;

        if (!this.collapsible_) {
            this.collapsed_ = false;
        }

        var className = options.className ? options.className : "ol-overview";

        var tipLabel = options.tipLabel ? options.tipLabel : "Overview map";

        var collapseLabel = options.collapseLabel
            ? options.collapseLabel
            : "\u00BB";

        this.collapseLabel_ = document.createElement("span");
        this.collapseLabel_.innerHTML = collapseLabel;

        var label = options.label ? options.label : "\u00AB";

        this.label_ = document.createElement("span");
        this.label_.innerHTML = label;

        var activeLabel =
            this.collapsible_ && !this.collapsed_
                ? this.collapseLabel_
                : this.label_;

        var button = document.createElement("button");
        button.type = "button";
        button.title = tipLabel;
        button.appendChild(activeLabel);
        if ("ontouchstart" in document.documentElement) {
            button.addEventListener(
                "touchstart",
                this.buttonClk.bind(this),
                false
            );
        } else {
            button.addEventListener("click", this.buttonClk.bind(this), false);
        }

        this.ovmapDiv = document.createElement("div");
        this.ovmapDiv.className = "ol-overview-map";

        var cssClasses =
            className +
            " " +
            "ol-unselectable ol-control" +
            (this.collapsed_ && this.collapsible_ ? " ol-collapsed" : "") +
            (this.collapsible_ ? "" : " ol-uncollapsible");

        element.className = cssClasses;
        element.appendChild(this.ovmapDiv);
        element.appendChild(button);

        var enlargeButton = document.createElement("button");
        enlargeButton.type = "button";
        enlargeButton.title = "Enlarge";
        enlargeButton.className = "size";
        enlargeButton.innerHTML = "+";
        if ("ontouchstart" in document.documentElement) {
            enlargeButton.addEventListener(
                "touchstart",
                this.enlargeButtonClick.bind(this),
                false
            );
        } else {
            enlargeButton.addEventListener(
                "click",
                this.enlargeButtonClick.bind(this),
                false
            );
        }

        element.appendChild(enlargeButton);

        var shrinkButton = document.createElement("button");
        shrinkButton.type = "button";
        shrinkButton.title = "Shrink";
        shrinkButton.className = "size";
        shrinkButton.innerHTML = "-";
        if ("ontouchstart" in document.documentElement) {
            shrinkButton.addEventListener(
                "touchstart",
                this.shrinkButtonClick.bind(this),
                false
            );
        } else {
            shrinkButton.addEventListener(
                "click",
                this.shrinkButtonClick.bind(this),
                false
            );
        }

        element.appendChild(shrinkButton);

        var trackingButton = document.createElement("button");
        trackingButton.type = "button";
        trackingButton.title = "Enable tracking";
        trackingButton.className = "size";
        trackingButton.innerHTML = "⌕";
        if ("ontouchstart" in document.documentElement) {
            trackingButton.addEventListener(
                "touchstart",
                this.trackingButtonClick.bind(this),
                false
            );
        } else {
            trackingButton.addEventListener(
                "click",
                this.trackingButtonClick.bind(this),
                false
            );
        }

        element.appendChild(trackingButton);

        this.trackingButton = trackingButton;

        this.featureSelect = null;
        this.eventKeys = [];
    }


    getBoxSize() {
        if (this.masterMap) {
            var bottomLeft = olExtent.getBottomLeft(
                this.masterMap.getView().getProjection().getExtent()
            );
            var topRight = olExtent.getTopRight(
                this.masterMap.getView().getProjection().getExtent()
            );
            var boxWidth = Math.abs(
                (bottomLeft[0] - topRight[0]) /
                this.masterMap.getView().getMaxResolution()
            );
            var boxHeight = Math.abs(
                (topRight[1] - bottomLeft[1]) /
                this.masterMap.getView().getMaxResolution()
            );
            return [boxWidth, boxHeight];
        }

        return [0, 0];
    }

    createOverviewView() {
        let proj = this.masterMap.getView().getProjection();
        let extent = proj.getExtent();
        return new View({
            projection: proj,
            center: olExtent.getCenter(extent),
            showFullExtent: true,
            maxResolution: 1250,
            minResolution: 1,
        });
    }

    /**
    * Sets the OpenLayers map this control handles. This is automatically called by OpenLayers
    * @param {ol.Map} map 
    */
    setMap(map) {
        if (!map) {
            unByKey(this.eventKeys);
            return;
        }

        var oldMap = this.getMap();
        if (map === oldMap) {
            return;
        }

        if (oldMap) {
            this.overviewMap.setTarget(null);
        }

        super.setMap(map);
        this.masterMap = map;
        var boxSize = this.getBoxSize();

        var resolutionFactor = 1;

        var mainMapSize = map.getSize();
        if (
            boxSize[0] > mainMapSize[0] / 5 ||
            boxSize[1] > mainMapSize[1] / 5
        ) {
            resolutionFactor = 2;
        }

        boxSize[0] /= resolutionFactor;
        boxSize[1] /= resolutionFactor;

        var mainView = this.createOverviewView();

        var vectorLayer = new VectorLayer({
            source: new VectorSource({
                features: [
                    new Feature({
                        geometry: fromExtent(
                            map.getView().calculateExtent(map.getSize())
                        ),
                    }),
                ],
            }),
            style: new Style({
                stroke: new Stroke({
                    width: 2,
                    color: "rgba(0, 60, 136, 1.0)",
                }),
                fill: new Fill({
                    color: "rgba(0, 0, 0, 0.0)",
                }),
            }),
        });

        const currentOverlayFeature = new Feature({
            geometry: fromExtent(map.getView().calculateExtent(map.getSize())),
        });
        currentOverlayFeature.setStyle(
            new Style({
                fill: new Fill({
                    color: "rgba(0, 0, 0, 0.4)",
                }),
            })
        );
        var fogLayer = new VectorLayer({
            source: new VectorSource({
                features: [currentOverlayFeature],
            }),
            visible: false,
        });

        let mainLayer = map.getLayers().item(0);
        let mainLayers = map.getLayers().getArray().filter((l) => {
            return l.get("active") === true;
        });
        if (mainLayers.length > 0) {
            mainLayer = mainLayers[0];
        }
        var layerList = [new TileLayer({ source: mainLayer.getSource() })];

        layerList.push(fogLayer);
        layerList.push(vectorLayer);

        this.currentOverlayFeature = currentOverlayFeature;

        var viewboxFeatures = [null, null, null, null];
        this.viewboxFeatures = viewboxFeatures;

        var diagonal = Math.round(
            Math.sqrt(boxSize[0] * boxSize[0] + boxSize[1] * boxSize[1])
        );
        this.ovmapDiv.style.width = diagonal + "px";
        this.ovmapDiv.style.height = diagonal + "px";

        // Overview map
        this.overviewMap = new Map({
            controls: new Collection(),
            interactions: new Collection(),
            target: this.ovmapDiv,
            view: mainView,
            layers: layerList,
        });

        var format = new GeoJSON();
        this.format = format;
        var simplifyOptions = { tolerance: 0.01, highQuality: false };
        this.simplifyOptions = simplifyOptions;
        var formatOptions = {
            featureProjection: this.overviewMap.getView().getProjection(),
        };
        this.formatOptions = formatOptions;
        var currentOverlayFeatureJson = format.writeFeatureObject(
            this.currentOverlayFeature,
            formatOptions
        );
        this.currentOverlayFeatureJson = currentOverlayFeatureJson;
        var featureStyle = [];
        featureStyle.push(
            new Style({
                stroke: new Stroke({
                    width: 1,
                    color: "rgba(255, 0, 0, 1.0)",
                }),
                fill: new Fill({
                    color: "rgba(0, 0, 0, 0.3)",
                }),
            })
        );
        featureStyle.push(
            new Style({
                stroke: new Stroke({
                    width: 1,
                    color: "rgba(0, 255, 255, 1.0)",
                }),
                fill: new Fill({
                    color: "rgba(0, 0, 0, 0.2)",
                }),
            })
        );
        featureStyle.push(
            new Style({
                stroke: new Stroke({
                    width: 1,
                    color: "rgba(255, 215, 0, 1.0)",
                }),
                fill: new Fill({
                    color: "rgba(0, 0, 0, 0.1)",
                }),
            })
        );
        featureStyle.push(
            new Style({
                stroke: new Stroke({
                    width: 1,
                    color: "rgba(0, 255, 0, 1.0)",
                }),
                fill: new Fill({
                    color: "rgba(0, 0, 0, 0.0)",
                }),
            })
        );
        this.featureStyle = featureStyle;

        this.vectorLayer = vectorLayer;
        this.fogLayer = fogLayer;

        var polling = 0;
        this.polling = polling;
        var pollingRate = 10;
        this.pollingRate = pollingRate;
        var lastZoomValue = map.getView().getZoom();
        this.lastZoomValue = lastZoomValue;

        var that = this;
        let k = this.overviewMap.on(
            "singleclick",
            function (evt) {
                evt.preventDefault();
                map.getView().setCenter(evt.coordinate);
            },
            this
        );
        this.eventKeys.push(k);

        k = this.overviewMap.on(
            "pointermove",
            function (evt) {
                evt.preventDefault();
                var hit = false;

                if (evt.dragging) {
                    if (!that.featureSelect) {
                        that.featureSelect = that.overviewMap.forEachFeatureAtPixel(
                            evt.pixel,
                            function (feature, layer) {
                                hit = true;
                                return feature;
                            }
                        );
                    }

                    if (that.featureSelect) {
                        map.getView().setCenter(evt.coordinate);
                    }
                } else {
                    if (that.featureSelect) {
                        that.featureSelect = null;
                    }
                }

                // detect feature at mouse coords
                if (!hit) {
                    hit = that.overviewMap.forEachFeatureAtPixel(
                        evt.pixel,
                        function (feature, layer) {
                            return true;
                        }
                    );
                }

                var element = evt.map.getTargetElement();
                if (hit) {
                    element.style.cursor = "move";
                } else {
                    element.style.cursor = "";
                }
            },
            this
        );


        this.eventKeys.push(k);

        k = map.getView().on("change:center", this.updateBox.bind(this), this);
        this.eventKeys.push(k);
        k = map.getView().on("change:resolution", this.updateBox.bind(this), this);
        this.eventKeys.push(k);
        k = map.getView().on("change:rotation", this.updateBox.bind(this), this);
        this.eventKeys.push(k);

        this.overviewMap.updateSize();
        let extent = map.getView().getProjection().getExtent();
        this.overviewMap.getView().fit(extent, this.overviewMap.getSize());
        this.overviewMap.getView().setCenter(olExtent.getCenter(extent));
        this.updateBox();
        this.setTrackingEnabled(this.trackingFlag);
        this.masterView = this.masterMap.getView();
    }

    updateBox() {
        if (!this.masterMap) {
            return;
        }

        if (!this.masterMap.isRendered() || !this.overviewMap.isRendered()) {
            return;
        }

        // if(this.masterView !== this.masterMap.getView()){
        //     return;
        // }

        var masterView = this.masterMap.getView();
        var overviewView = this.overviewMap.getView();

        var rotation = masterView.getRotation();
        overviewView.setRotation(rotation);

        var masterMapSize = this.masterMap.getSize();
        var resolution = masterView.getResolution();

        var halfWidth = masterMapSize[0] * resolution * 0.5;
        var halfHeight = masterMapSize[1] * resolution * 0.5;

        var center = masterView.getCenter();
        var x1 = center[0] - halfWidth;
        var x2 = center[0] + halfWidth;
        var y1 = center[1] - halfHeight;
        var y2 = center[1] + halfHeight;

        var viewportExtent = [x1, y1, x2, y2];

        // constrain the viewport box to the overview window box
        var masterMapExtent = masterView.getProjection().getExtent();
        var bottomLeft = olExtent.getBottomLeft(masterMapExtent);
        var topRight = olExtent.getTopRight(masterMapExtent);
        var projCenter = olExtent.getCenter(masterMapExtent);
        var boxWidth = Math.abs(bottomLeft[0] - topRight[0]);
        var boxHeight = Math.abs(topRight[1] - bottomLeft[1]);
        var halfDiagonal =
            Math.sqrt(boxWidth * boxWidth + boxHeight * boxHeight) * 0.5;
        var overviewExtent = [
            projCenter[0] - halfDiagonal,
            projCenter[1] - halfDiagonal,
            projCenter[0] + halfDiagonal,
            projCenter[1] + halfDiagonal,
        ];

        var finalExtent = olExtent.getIntersection(
            viewportExtent,
            overviewExtent
        );

        var poly = new Polygon([
            [
                [finalExtent[0], finalExtent[1]],
                [finalExtent[2], finalExtent[1]],
                [finalExtent[2], finalExtent[3]],
                [finalExtent[0], finalExtent[3]],
                [finalExtent[0], finalExtent[1]],
            ],
        ]);
        poly.rotate(rotation, poly.getInteriorPoint().getFirstCoordinate());
        this.vectorLayer.getSource().getFeatures()[0].setGeometry(poly);

        if (this.trackingFlag) {
            //Available different zoom levels
            var zoomIndex;
            switch (true) {
                case Math.floor(masterView.getZoom()) < 3:
                    return;
                case Math.floor(masterView.getZoom()) < 4:
                    zoomIndex = 1;
                    break;
                case Math.floor(masterView.getZoom()) < 6:
                    zoomIndex = 2;
                    break;
                case Math.floor(masterView.getZoom()) < 7:
                    zoomIndex = 3;
                    break;
                default:
                    zoomIndex = 4;
                    break;
            }

            //Current view polygon
            var currentViewboxFeature = new Feature({
                geometry: poly,
            });
            var currentViewboxFeatureJson = this.format.writeFeatureObject(
                currentViewboxFeature,
                this.formatOptions
            );

            if (
                this.polling !== 0 &&
                this.polling < this.pollingRate &&
                this.lastZoomValue === masterView.getZoom()
            ) {
                this.polling++;
                return;
            }

            this.polling = 1;
            this.lastZoomValue = masterView.getZoom();

            //Add current poly to all zoom levels
            for (var i = 0; i < zoomIndex; i++) {
                if (this.viewboxFeatures[i] !== null) {
                    var previousViewboxFeatureJson = this.viewboxFeatures[i];
                    var currentToPreviousFeatureUnionJson = union(
                        previousViewboxFeatureJson,
                        currentViewboxFeatureJson
                    );
                    this.viewboxFeatures[i] = currentToPreviousFeatureUnionJson;
                } else {
                    this.viewboxFeatures[i] = currentViewboxFeatureJson;
                }
            }

            //Substract previous layer
            var viewboxFeaturesDiff = Array.from(this.viewboxFeatures);
            for (var k = 0; k < viewboxFeaturesDiff.length - 1; k++) {
                if (
                    viewboxFeaturesDiff[k + 1] === null ||
                    viewboxFeaturesDiff[k] === null
                ) {
                    continue;
                }
                var previousLayerViewboxFeatureJson =
                    viewboxFeaturesDiff[k + 1];
                var previousSimplifiedJson = simplify(
                    previousLayerViewboxFeatureJson,
                    this.simplifyOptions
                );
                var currentLayerViewboxFeatureJson = viewboxFeaturesDiff[k];
                var currentSimplifiedJson = simplify(
                    currentLayerViewboxFeatureJson,
                    this.simplifyOptions
                );
                var currentToPreviousFeatureDiffJson = difference(
                    currentSimplifiedJson,
                    previousSimplifiedJson
                );
                viewboxFeaturesDiff[k] = currentToPreviousFeatureDiffJson;
            }

            //Calculate and apply overlay mask to overlay
            var overlayViewboxDiffJson;
            var overlayMaskJson = this.viewboxFeatures[0];
            var simplifiedOverlayMaskJson = simplify(
                overlayMaskJson,
                this.simplifyOptions
            );
            overlayViewboxDiffJson = difference(
                this.currentOverlayFeatureJson,
                simplifiedOverlayMaskJson
            );
            var overlayViewboxDiff = this.format.readFeature(
                overlayViewboxDiffJson,
                this.formatOptions
            );
            overlayViewboxDiff.setStyle(
                new Style({
                    fill: new Fill({
                        color: "rgba(0, 0, 0, 0.4)",
                    }),
                })
            );

            //Remove all features and add new
            var currentFeatures = Array.from(
                this.fogLayer.getSource().getFeatures()
            );
            this.fogLayer.getSource().clear();
            this.fogLayer.getSource().addFeature(overlayViewboxDiff);
            for (var j = 0; j < viewboxFeaturesDiff.length; j++) {
                var featureJson = viewboxFeaturesDiff[j];
                if (featureJson !== null) {
                    var feature = this.format.readFeature(
                        featureJson,
                        this.formatOptions
                    );
                    feature.setStyle(this.featureStyle[j]);
                    this.fogLayer.getSource().addFeature(feature);
                } else {
                    if (currentFeatures[j + 1]) {
                        this.fogLayer
                            .getSource()
                            .addFeature(currentFeatures[j + 1]);
                    }
                }
            }
        }
    }

    buttonClk(event) {
        if (event) {
            event.preventDefault();
            event.stopPropagation();
        }

        if (
            (" " + this.element.className + " ").indexOf(" ol-collapsed ") > -1
        ) {
            this.element.className = this.element.className.replace(
                /ol-collapsed/g,
                ""
            );
        } else {
            this.element.className += " ol-collapsed";
        }

        if (this.collapsed_) {
            this.label_.parentNode.replaceChild(
                this.collapseLabel_,
                this.label_
            );
        } else {
            this.collapseLabel_.parentNode.replaceChild(
                this.label_,
                this.collapseLabel_
            );
        }

        this.collapsed_ = !this.collapsed_;
        if (this.stateManager) {
            this.stateManager.overview.collapsed = this.collapsed_;
        }

        this.overviewMap.updateSize();
    }

    /**
     * Changes overview control size by factor
     * @param {number} factor Factor to change overview's size
     */
    changeOverviewSize(factor) {
        var boxSize = [this.ovmapDiv.clientWidth, this.ovmapDiv.clientHeight];
        boxSize[0] /= factor;
        boxSize[1] /= factor;

        var diagonal = Math.round(
            Math.sqrt(boxSize[0] * boxSize[0] + boxSize[1] * boxSize[1])
        );
        if (diagonal < 60 || diagonal > 600) {
            return;
        }

        this.ovmapDiv.style.width = boxSize[0] + "px";
        this.ovmapDiv.style.height = boxSize[1] + "px";

        this.overviewMap.updateSize();
        this.overviewMap
            .getView()
            .fit(
                this.overviewMap.getView().getProjection().getExtent(),
                this.overviewMap.getSize()
            );
    }

    /**
     * Changes overview control size by pixel size
     * @param {number} sizePx Pixel size to change overview's size
     */
    changeOverviewSizePx(sizePx) {
        if (sizePx < 60 || sizePx > 600) {
            return;
        }

        var boxSize = this.getBoxSize();
        var diagonal = Math.round(
            Math.sqrt(boxSize[0] * boxSize[0] + boxSize[1] * boxSize[1])
        );

        var newFactor = diagonal / sizePx;
        this.ovmapDiv.style.width = sizePx + "px";
        this.ovmapDiv.style.height = sizePx + "px";

        if (this.overviewMap) {
            // this.overviewMap.setView(this.createOverviewView(newFactor));
            this.overviewMap.updateSize();
            this.overviewMap
                .getView()
                .fit(this.masterMap.getView().getProjection().getExtent());
        }
    }

    enlargeButtonClick(event) {
        if (event) {
            event.preventDefault();
            event.stopPropagation();
        }

        this.changeOverviewSize(0.75);
    }

    shrinkButtonClick(event) {
        if (event) {
            event.preventDefault();
            event.stopPropagation();
        }

        this.changeOverviewSize(1.25);
    }

    trackingButtonClick(event) {
        if (event) {
            event.preventDefault();
            event.stopPropagation();
        }

        this.setTrackingEnabled(!this.trackingFlag);
    }

    /**
     * Enables or disables tracking
     * @param {boolean} enabled True to enable tracking, otherwise false
     */
    setTrackingEnabled(enabled) {
        this.trackingFlag = enabled;

        if (this.trackingFlag) {
            this.trackingButton.classList.add("selected");
            this.polling = 0;
            this.fogLayer.setVisible(true);
            this.updateBox();
        } else {
            this.trackingButton.classList.remove("selected");
            this.fogLayer.setVisible(false);
        }

        this.trackingButton.blur();
        this.trackingButton.hideFocus = true;
        this.trackingButton.style.outline = "none";
    }

    /**
     * Gets the tracking state of the control
     * @return {boolean} True if the control is currently tracking
     */
    getTrackingEnabled() {
        return this.trackingFlag;
    }

    /**
     * Gets the collapsed state of the control
     * @return {boolean} True if the control is currently collapsed
     */
    getCollapsed() {
        return (
            (" " + this.element.className + " ").indexOf(" ol-collapsed ") > -1
        );
    }

    /**
     * Sets the collapsed state of the control
     * @param {boolean} collapsed True to collapse the control, otherwise false
     */
    setCollapsed(collapsed) {
        if (this.getCollapsed() != collapsed) {
            this.buttonClk();
        }
    }
}