/* Copyright (c) 2015-present The Open Source Geospatial Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* An GeoExt.component.OverviewMap displays an overview map of a parent map.
* You can use this component as any other Ext.Component, e.g give it as an item
* to a panel.
*
* Example:
*
* @example preview
* var olMap = new ol.Map({
* layers: [
* new ol.layer.Tile({
* source: new ol.source.OSM()
* })
* ],
* view: new ol.View({
* center: ol.proj.fromLonLat([-8.751278, 40.611368]),
* zoom: 12,
* rotation: -Math.PI / 6
* })
* });
* var mapComponent = Ext.create('GeoExt.component.Map', {
* map: olMap
* });
* var mapPanel = Ext.create('Ext.panel.Panel', {
* title: 'Map',
* region: 'center',
* layout: 'fit',
* items: mapComponent
* });
* var overviewMapPanel = Ext.create('Ext.panel.Panel', {
* title: 'OverviewMap',
* region: 'west',
* layout: 'fit',
* width: 160,
* // create the overview by passing the ol.Map:
* items: Ext.create('GeoExt.component.OverviewMap', {
* parentMap: olMap
* })
* });
* Ext.create('Ext.panel.Panel', {
* height: 300,
* layout: 'border',
* items: [mapPanel, overviewMapPanel],
* renderTo: Ext.getBody()
* });
*
* @class GeoExt.component.OverviewMap
*/
Ext.define('GeoExt.component.OverviewMap', {
extend: 'Ext.Component',
alias: [
'widget.gx_overview',
'widget.gx_overviewmap',
'widget.gx_component_overviewmap',
],
requires: ['GeoExt.util.Version', 'GeoExt.util.Layer'],
mixins: ['GeoExt.mixin.SymbolCheck'],
// <debug>
symbols: [
// For ol4 support we can no longer require this symbols:
// 'ol.animation.pan',
// 'ol.Map#beforeRender',
'ol.Collection',
'ol.Feature',
'ol.Feature#setGeometry',
'ol.Feature#setStyle',
'ol.geom.Point',
'ol.geom.Point#getCoordinates',
'ol.geom.Point#setCoordinates',
'ol.geom.Polygon',
'ol.geom.Polygon#getCoordinates',
'ol.geom.Polygon#setCoordinates',
'ol.interaction.Translate',
'ol.layer.Image', // we should get rid of this requirement
'ol.layer.Tile', // we should get rid of this requirement
'ol.layer.Vector',
'ol.layer.Vector#getSource',
'ol.Map',
'ol.Map#addLayer',
'ol.Map#getView',
'ol.Map#on',
'ol.Map#updateSize',
'ol.Map#un',
'ol.source.Vector',
'ol.source.Vector#addFeatures',
'ol.View',
'ol.View#calculateExtent',
'ol.View#getCenter',
'ol.View#getProjection',
'ol.View#getRotation',
'ol.View#getZoom',
'ol.View#on',
'ol.View#set',
'ol.View#setCenter',
'ol.View#un',
],
// </debug>
inheritableStatics: {
/**
* Returns an object with geometries representing the extent of the
* passed map and the top left point.
*
* @param {ol.Map} map The map to the extent and top left corner
* geometries from.
* @return {Object} An object with keys `extent` and `topLeft`.
*/
getVisibleExtentGeometries: function (map) {
const mapSize = map && map.getSize();
const w = mapSize && mapSize[0];
const h = mapSize && mapSize[1];
if (!mapSize || isNaN(w) || isNaN(h)) {
return;
}
const pixels = [
[0, 0],
[w, 0],
[w, h],
[0, h],
[0, 0],
];
const extentCoords = [];
Ext.each(pixels, function (pixel) {
const coord = map.getCoordinateFromPixel(pixel);
if (coord === null) {
return false;
}
extentCoords.push(coord);
});
if (extentCoords.length !== 5) {
return;
}
const geom = new ol.geom.Polygon([extentCoords]);
const anchor = new ol.geom.Point(extentCoords[0]);
return {
extent: geom,
topLeft: anchor,
};
},
},
config: {
/**
* The style for the anchor feature which indicates the upper-left
* corner of the overview rectangle.
*
* @cfg {ol.style.Style} anchorStyle
*/
anchorStyle: null,
/**
* The style for the overview rectangle.
*
* @cfg {ol.style.Style} boxStyle
*/
boxStyle: null,
/**
* An `Array` of `ol.layer.Base`. It needs to have own layers
* specified, it cannot use layers of the parent map.
*
* @cfg {Array}
*/
layers: [],
/**
* The magnification is the relationship in which the resolution of the
* overviewmaps view is bigger then resolution of the parentMaps view.
*
* @cfg {number} magnification
*/
magnification: 5,
/**
* A configured map or a configuration object for the map constructor.
*
* **This is the overviewMap itself.**
*
* @cfg {ol.Map/Object} map
*/
map: null,
/**
* A configured map or a configuration object for the map constructor.
*
* **This should be the map the overviewMap is bound to.**
*
* @cfg {ol.Map} parentMap
*/
parentMap: null,
/**
* Shall a click on the overview map recenter the parent map?
*
* @cfg {boolean} recenterOnClick Whether we shall recenter the parent
* map on a click on the overview map or not.
*/
recenterOnClick: true,
/**
* Shall the extent box on the overview map be draggable to recenter the
* parent map?
*
* @cfg {boolean} enableBoxDrag Whether we shall make the box feature of
* the overview map draggable. When dragging ends, the parent map
* is recentered.
*/
enableBoxDrag: true,
/**
* Duration time in milliseconds of the panning animation when we
* recenter the map after a click on the overview or after dragging of
* the extent box ends. Only has effect if either or both of the
* configs #recenterOnClick or #enableBoxDrag are `true`.
*
* @cfg {number} recenterDuration Amount of milliseconds for panning
* the parent map to the clicked location or the new center of the
* box feature.
*/
recenterDuration: 500,
},
/**
* The `ol.Feature` that represents the extent of the parent map.
*
* @type {ol.Feature}
* @private
*/
boxFeature: null,
/**
* The `ol.Feature` that represents the top left corner 0f the parent map.
*
* @type {ol.Feature}
* @private
*/
anchorFeature: null,
/**
* The `ol.layer.Vector` displaying the extent geometry of the parent map.
*
* @private
*/
extentLayer: null,
/**
* The `ol.interaction.Translate` that we might have created (depending on
* the setting of the #enableBoxDrag configuration).
*
* @private
*/
dragInteraction: null,
/**
* Whether we already rendered an ol.Map in this component. Will be
* updated in #onResize, after the first rendering happened.
*
* @property {boolean} mapRendered Map already rendered flag
* @private
*/
mapRendered: false,
/**
* The constructor of the OverviewMap component.
*/
constructor: function () {
this.initOverviewFeatures();
this.callParent(arguments);
},
/**
* Initializes the GeoExt.component.OverviewMap.
*/
initComponent: function () {
const me = this;
if (!me.getParentMap()) {
Ext.Error.raise('No parentMap defined for overviewMap');
} else if (!(me.getParentMap() instanceof ol.Map)) {
Ext.Error.raise('parentMap is not an instance of ol.Map');
}
me.initOverviewMap();
me.on('beforedestroy', me.onBeforeDestroy, me);
me.on('resize', me.onResize, me);
me.on('afterrender', me.updateBox, me);
me.callParent();
},
/**
* Creates the ol instances we need: two features for the box and the
* anchor, and a vector layer with empty source to hold the features.
*
* @private
*/
initOverviewFeatures: function () {
const me = this;
me.boxFeature = new ol.Feature();
me.anchorFeature = new ol.Feature();
me.extentLayer = new ol.layer.Vector({
source: new ol.source.Vector(),
});
},
/**
* Initializes the #map from the configuration and the #parentMap.
*
* @private
*/
initOverviewMap: function () {
const me = this;
const parentMap = me.getParentMap();
const ovMap = me.getMap();
me.getLayers().push(me.extentLayer);
if (!ovMap) {
const parentView = parentMap.getView();
const olMap = new ol.Map({
controls: new ol.Collection(),
interactions: new ol.Collection(),
view: new ol.View({
center: parentView.getCenter(),
zoom: parentView.getZoom(),
projection: parentView.getProjection(),
}),
});
me.setMap(olMap);
} else if (ovMap.getView() && !ovMap.getView().getCenter()) {
// OL expects (any) center here.
// otherwise problems as described in
// https://github.com/geoext/geoext/issues/707 can occur
ovMap.getView().setCenter([0, 0]);
}
GeoExt.util.Layer.cascadeLayers(
parentMap.getLayerGroup(),
function (layer) {
if (me.getLayers().indexOf(layer) > -1) {
throw new Error(
'OverviewMap cannot use layers of the ' +
'parent map. (Since ol v6.0.0 maps cannot share ' +
'layers anymore)',
);
}
},
);
Ext.each(me.getLayers(), function (layer) {
me.getMap().addLayer(layer);
});
// Set the OverviewMaps center or resolution, on property changed
// in parentMap.
parentMap
.getView()
.on('propertychange', me.onParentViewPropChange.bind(me));
// Update the box after rendering a new frame of the parentMap.
me.enableBoxUpdate();
// Initially set the center and resolution of the overviewMap.
me.setOverviewMapProperty('center');
me.setOverviewMapProperty('resolution');
me.extentLayer.getSource().addFeatures([me.boxFeature, me.anchorFeature]);
},
/**
* Enable everything we need to be able to drag the extent box on the
* overview map, and to properly handle drag events (e.g. recenter on
* finished dragging).
*/
setupDragBehaviour: function () {
const me = this;
const dragInteraction = new ol.interaction.Translate({
features: new ol.Collection([me.boxFeature]),
});
me.getMap().addInteraction(dragInteraction);
dragInteraction.setActive(true);
// disable the box update during the translation
// because it interferes when dragging the feature
dragInteraction.on('translatestart', me.disableBoxUpdate.bind(me));
dragInteraction.on('translating', me.repositionAnchorFeature.bind(me));
dragInteraction.on('translateend', me.recenterParentFromBox.bind(me));
dragInteraction.on('translateend', me.enableBoxUpdate.bind(me));
me.dragInteraction = dragInteraction;
},
/**
* Disables the update of the box by unbinding the updateBox function
* from the postrender event of the parent map.
*/
disableBoxUpdate: function () {
const me = this;
const parentMap = me.getParentMap();
if (parentMap) {
parentMap.un('postrender', me.updateBox, me);
}
},
/**
* Enables the update of the box by binding the updateBox function
* to the postrender event of the parent map.
*/
enableBoxUpdate: function () {
const me = this;
const parentMap = me.getParentMap();
if (parentMap) {
parentMap.on('postrender', me.updateBox.bind(me));
}
},
/**
* Disable / destroy everything we need to be able to drag the extent box on
* the overview map. Unregisters any events we might have added and removes
* the `ol.interaction.Translate`.
*/
destroyDragBehaviour: function () {
const me = this;
const dragInteraction = me.dragInteraction;
if (!dragInteraction) {
return;
}
dragInteraction.setActive(false);
me.getMap().removeInteraction(dragInteraction);
dragInteraction.un('translatestart', me.disableBoxUpdate, me);
dragInteraction.un('translating', me.repositionAnchorFeature, me);
dragInteraction.un('translateend', me.recenterParentFromBox, me);
dragInteraction.un('translateend', me.enableBoxUpdate, me);
me.dragInteraction = null;
},
/**
* Repositions the #anchorFeature during dragging sequences of the box.
* Called while the #boxFeature is being dragged.
*/
repositionAnchorFeature: function () {
const me = this;
const boxCoords = me.boxFeature.getGeometry().getCoordinates();
const topLeftCoord = boxCoords[0][0];
const newAnchorGeom = new ol.geom.Point(topLeftCoord);
me.anchorFeature.setGeometry(newAnchorGeom);
},
/**
* Recenters the #parentMap to the center of the extent of the #boxFeature.
* Called when dragging of the #boxFeature ends.
*/
recenterParentFromBox: function () {
const me = this;
const parentMap = me.getParentMap();
const parentView = parentMap.getView();
const parentProjection = parentView.getProjection();
const overviewMap = me.getMap();
const overviewView = overviewMap.getView();
const overviewProjection = overviewView.getProjection();
const currentMapCenter = parentView.getCenter();
const boxExtent = me.boxFeature.getGeometry().getExtent();
let boxCenter = ol.extent.getCenter(boxExtent);
// transform if necessary
if (!ol.proj.equivalent(parentProjection, overviewProjection)) {
boxCenter = ol.proj.transform(
boxCenter,
overviewProjection,
parentProjection,
);
}
// Check for backwards compatibility
if (GeoExt.util.Version.isOl3()) {
const panAnimation = ol.animation.pan({
duration: me.getRecenterDuration(),
source: currentMapCenter,
});
parentMap.beforeRender(panAnimation);
parentView.setCenter(boxCenter);
} else {
parentView.animate({
center: boxCenter,
});
}
},
/**
* Called when a property of the parent maps view changes.
*
* @param {ol.ObjectEvent} evt The event emitted by the `ol.Object`.
* @private
*/
onParentViewPropChange: function (evt) {
if (evt.key === 'center' || evt.key === 'resolution') {
this.setOverviewMapProperty(evt.key);
}
},
/**
* Handler for the click event of the overview map. Recenters the parent
* map to the clicked location.
*
* @param {ol.MapBrowserEvent} evt The click event on the map.
* @private
*/
overviewMapClicked: function (evt) {
const me = this;
const parentMap = me.getParentMap();
const parentView = parentMap.getView();
const parentProjection = parentView.getProjection();
const currentMapCenter = parentView.getCenter();
const overviewMap = me.getMap();
const overviewView = overviewMap.getView();
const overviewProjection = overviewView.getProjection();
let newCenter = evt.coordinate;
// transform if necessary
if (!ol.proj.equivalent(parentProjection, overviewProjection)) {
newCenter = ol.proj.transform(
newCenter,
overviewProjection,
parentProjection,
);
}
// Check for backwards compatibility
if (GeoExt.util.Version.isOl3()) {
const panAnimation = ol.animation.pan({
duration: me.getRecenterDuration(),
source: currentMapCenter,
});
parentMap.beforeRender(panAnimation);
parentView.setCenter(newCenter);
} else {
parentView.animate({
center: newCenter,
});
}
},
/**
* Updates the Geometry of the extentLayer.
*/
updateBox: function () {
const me = this;
const parentMap = me.getParentMap();
const extentGeometries = me.self.getVisibleExtentGeometries(parentMap);
if (!extentGeometries) {
return;
}
const geom = extentGeometries.extent;
const anchor = extentGeometries.topLeft;
const parentMapProjection = parentMap.getView().getProjection();
const overviewProjection = me.getMap().getView().getProjection();
// transform if necessary
if (!ol.proj.equivalent(parentMapProjection, overviewProjection)) {
geom.transform(parentMapProjection, overviewProjection);
anchor.transform(parentMapProjection, overviewProjection);
}
me.boxFeature.setGeometry(geom);
me.anchorFeature.setGeometry(anchor);
},
/**
* Set an OverviewMap property (center or resolution).
*
* @param {string} key The name of the property, either `'center'` or
* `'resolution'`
*/
setOverviewMapProperty: function (key) {
const me = this;
const parentView = me.getParentMap().getView();
const parentProjection = parentView.getProjection();
const overviewView = me.getMap().getView();
const overviewProjection = overviewView.getProjection();
let overviewCenter = parentView.getCenter();
if (key === 'center') {
// transform if necessary
if (!ol.proj.equivalent(parentProjection, overviewProjection)) {
overviewCenter = ol.proj.transform(
overviewCenter,
parentProjection,
overviewProjection,
);
}
overviewView.set('center', overviewCenter);
}
if (key === 'resolution') {
if (ol.proj.equivalent(parentProjection, overviewProjection)) {
overviewView.set(
'resolution',
me.getMagnification() * parentView.getResolution(),
);
} else if (me.mapRendered === true) {
const parentExtent = parentView.calculateExtent(
me.getParentMap().getSize(),
);
const parentExtentProjected = ol.proj.transformExtent(
parentExtent,
parentProjection,
overviewProjection,
);
// call fit to assure that resolutions are available on
// overviewView
overviewView.fit(parentExtentProjected);
overviewView.set(
'resolution',
me.getMagnification() * overviewView.getResolution(),
);
}
// Do nothing when parent and overview projections are not
// equivalent and mapRendered is false as me.getMap().getSize()
// would not be reliable here.
// Note: As soon as mapRendered will be set to true (in onResize())
// setOverviewMapProperty('resolution') will be called explicitly
}
},
/**
* The applier for the #recenterOnClick configuration. Takes care of
* initially registering an appropriate eventhandler and also unregistering
* if the property changes.
*
* @param {boolean} shallRecenter The value for #recenterOnClick that was
* set.
* @return {boolean} The value for #recenterOnClick that was passed.
*/
applyRecenterOnClick: function (shallRecenter) {
const me = this;
const map = me.getMap();
if (!map) {
me.addListener(
'afterrender',
function () {
// set the property again, and re-trigger the 'apply…'-sequence
me.setRecenterOnClick(shallRecenter);
},
me,
{single: true},
);
return shallRecenter;
}
if (shallRecenter) {
map.on('click', me.overviewMapClicked.bind(me));
} else {
map.un('click', me.overviewMapClicked.bind(me));
}
return shallRecenter;
},
/**
* The applier for the #enableBoxDrag configuration. Takes care of initially
* setting up an interaction if desired or destroying when dragging is not
* wanted.
*
* @param {boolean} shallEnableBoxDrag The value for #enableBoxDrag that was
* set.
* @return {boolean} The value for #enableBoxDrag that was passed.
*/
applyEnableBoxDrag: function (shallEnableBoxDrag) {
const me = this;
const map = me.getMap();
if (!map) {
me.addListener(
'afterrender',
function () {
// set the property again, and re-trigger the 'apply…'-sequence
me.setEnableBoxDrag(shallEnableBoxDrag);
},
me,
{single: true},
);
return shallEnableBoxDrag;
}
if (shallEnableBoxDrag) {
me.setupDragBehaviour();
} else {
me.destroyDragBehaviour();
}
return shallEnableBoxDrag;
},
/**
* Cleanup any listeners we may have bound.
*/
onBeforeDestroy: function () {
const me = this;
const map = me.getMap();
const parentMap = me.getParentMap();
const parentView = parentMap && parentMap.getView();
if (map) {
// unbind recenter listener, if any
map.un('click', me.overviewMapClicked, me);
}
me.destroyDragBehaviour();
if (parentMap) {
// unbind parent listeners
me.disableBoxUpdate();
parentView.un('propertychange', me.onParentViewPropChange, me);
}
},
/**
* Update the size of the ol.Map onResize.
*
* TODO can we reuse the mapcomponent code? Perhaps even for this complete
* class???
* @private
*/
onResize: function () {
// Get the corresponding view of the controller (the mapPanel).
const me = this;
const div = me.getEl().dom;
const map = me.getMap();
if (!me.mapRendered) {
map.setTarget(div);
me.mapRendered = true;
// explicit call to assure that magnification mechanism will also
// work initially if projections of parent and overview are
// not equal
me.setOverviewMapProperty('resolution');
} else {
me.getMap().updateSize();
}
},
/**
* The applier for the anchor style.
*
* @param {ol.Style} style The new style for the anchor feature that was
* set.
* @return {ol.Style} The new style for the anchor feature.
*/
applyAnchorStyle: function (style) {
this.anchorFeature.setStyle(style);
return style;
},
/**
* The applier for the box style.
*
* @param {ol.Style} style The new style for the box feature that was set.
* @return {ol.Style} The new style for the box feature.
*/
applyBoxStyle: function (style) {
this.boxFeature.setStyle(style);
return style;
},
});