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 = '<i class="fa fa-map-marker"></i>';
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: 5000,
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 (!mainMapSize) {
return;
}
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++) {
var previousViewboxFeatureJson = this.viewboxFeatures[i];
if (previousViewboxFeatureJson !== null) {
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++) {
var previousLayerViewboxFeatureJson = viewboxFeaturesDiff[k + 1];
if (previousLayerViewboxFeatureJson === null) continue;
var previousSimplifiedJson = simplify(
previousLayerViewboxFeatureJson,
this.simplifyOptions
);
var currentLayerViewboxFeatureJson = viewboxFeaturesDiff[k];
if (currentLayerViewboxFeatureJson === null) continue;
var currentSimplifiedJson = simplify(
currentLayerViewboxFeatureJson,
this.simplifyOptions
);
if (currentSimplifiedJson === null || previousSimplifiedJson === null) continue;
var currentToPreviousFeatureDiffJson = difference(
currentSimplifiedJson,
previousSimplifiedJson
);
viewboxFeaturesDiff[k] = currentToPreviousFeatureDiffJson;
}
//Calculate and apply overlay mask to overlay
var overlayViewboxDiffJson = null;
var simplifiedOverlayMaskJson = null;
var overlayMaskJson = this.viewboxFeatures[0];
if (overlayMaskJson !== null) {
var simplifiedOverlayMaskJson = simplify(
overlayMaskJson,
this.simplifyOptions
);
}
if (simplifiedOverlayMaskJson !== null) {
overlayViewboxDiffJson = difference(
this.currentOverlayFeatureJson,
simplifiedOverlayMaskJson
);
}
var overlayViewboxDiff = null;
if (overlayViewboxDiffJson !== null) {
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();
if (overlayViewboxDiff !== null) {
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();
}
}
}