UNPKG

@geoext/geoext

Version:

GeoExt is Open Source and enables building desktop-like GIS applications through the web. It is a JavaScript framework that combines the GIS functionality of OpenLayers with the user interface savvy of the ExtJS library provided by Sencha.

785 lines (714 loc) 22.8 kB
/* 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; }, });