diff --git a/examples/popup.js b/examples/popup.js index f6c051d316..2ed85da5f5 100644 --- a/examples/popup.js +++ b/examples/popup.js @@ -29,9 +29,12 @@ closer.onclick = function() { /** * Create an overlay to anchor the popup to the map. */ -var overlay = new ol.Overlay({ - element: container -}); +var overlay = new ol.Overlay(/** @type {olx.OverlayOptions} */ ({ + element: container, + autoPanAnimation: { + duration: 250 + } +})); /** @@ -60,7 +63,7 @@ var map = new ol.Map({ /** * Add a click handler to the map to render the popup. */ -map.on('click', function(evt) { +map.on('singleclick', function(evt) { var coordinate = evt.coordinate; var hdms = ol.coordinate.toStringHDMS(ol.proj.transform( coordinate, 'EPSG:3857', 'EPSG:4326')); diff --git a/externs/olx.js b/externs/olx.js index 0b9924c79b..6705e3a3bf 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -310,7 +310,10 @@ olx.MapOptions.prototype.view; * position: (ol.Coordinate|undefined), * positioning: (ol.OverlayPositioning|string|undefined), * stopEvent: (boolean|undefined), - * insertFirst: (boolean|undefined)}} + * insertFirst: (boolean|undefined), + * autoPan: (boolean|undefined), + * autoPanAnimation: (olx.animation.PanOptions|undefined), + * autoPanMargin: (number|undefined)}} * @api stable */ olx.OverlayOptions; @@ -376,6 +379,35 @@ olx.OverlayOptions.prototype.stopEvent; olx.OverlayOptions.prototype.insertFirst; +/** + * If set to `true` the map is panned when calling `setPosition`, so that the + * overlay is entirely visible in the current viewport. + * The default is `true`. + * @type {boolean|undefined} + * @api + */ +olx.OverlayOptions.prototype.autoPan; + + +/** + * The options used to create a `ol.animation.pan` animation. This animation + * is only used when `autoPan` is enabled. By default the default options for + * `ol.animation.pan` are used. If set to `null` the panning is not animated. + * @type {olx.animation.PanOptions|undefined} + * @api + */ +olx.OverlayOptions.prototype.autoPanAnimation; + + +/** + * The margin (in pixels) between the overlay and the borders of the map when + * autopanning. The default is `20`. + * @type {number|undefined} + * @api + */ +olx.OverlayOptions.prototype.autoPanMargin; + + /** * Object literal with config options for the projection. * @typedef {{code: string, diff --git a/src/ol/dom/dom.js b/src/ol/dom/dom.js index 51c657a485..4e6da97f8d 100644 --- a/src/ol/dom/dom.js +++ b/src/ol/dom/dom.js @@ -298,3 +298,35 @@ ol.dom.transformElement2D = // content size. } }; + + +/** + * Get the current computed width for the given element including margin, + * padding and border. + * Equivalent to jQuery's `$(el).outerWidth(true)`. + * @param {!Element} element Element. + * @return {number} + */ +ol.dom.outerWidth = function(element) { + var width = element.offsetWidth; + var style = element.currentStyle || window.getComputedStyle(element); + width += parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10); + + return width; +}; + + +/** + * Get the current computed height for the given element including margin, + * padding and border. + * Equivalent to jQuery's `$(el).outerHeight(true)`. + * @param {!Element} element Element. + * @return {number} + */ +ol.dom.outerHeight = function(element) { + var height = element.offsetHeight; + var style = element.currentStyle || window.getComputedStyle(element); + height += parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10); + + return height; +}; diff --git a/src/ol/overlay.js b/src/ol/overlay.js index 97026f827b..8b18d4f845 100644 --- a/src/ol/overlay.js +++ b/src/ol/overlay.js @@ -11,6 +11,9 @@ goog.require('ol.Coordinate'); goog.require('ol.Map'); goog.require('ol.MapEventType'); goog.require('ol.Object'); +goog.require('ol.animation'); +goog.require('ol.dom'); +goog.require('ol.extent'); /** @@ -90,6 +93,26 @@ ol.Overlay = function(options) { this.element_ = goog.dom.createElement(goog.dom.TagName.DIV); this.element_.style.position = 'absolute'; + /** + * @private + * @type {boolean} + */ + this.autoPan_ = goog.isDef(options.autoPan) ? options.autoPan : true; + + /** + * @private + * @type {olx.animation.PanOptions} + */ + this.autoPanAnimation_ = goog.isDef(options.autoPanAnimation) ? + options.autoPanAnimation : /** @type {olx.animation.PanOptions} */ ({}); + + /** + * @private + * @type {number} + */ + this.autoPanMargin_ = goog.isDef(options.autoPanMargin) ? + options.autoPanMargin : 20; + /** * @private * @type {{bottom_: string, @@ -291,6 +314,9 @@ ol.Overlay.prototype.handleOffsetChanged = function() { */ ol.Overlay.prototype.handlePositionChanged = function() { this.updatePixelPosition_(); + if (goog.isDef(this.get(ol.OverlayProperty.POSITION)) && this.autoPan_) { + this.panIntoView_(); + } }; @@ -364,6 +390,89 @@ goog.exportProperty( ol.Overlay.prototype.setPosition); +/** + * Pan the map so that the overlay is entirely visible in the current viewport + * (if necessary). + * @private + */ +ol.Overlay.prototype.panIntoView_ = function() { + goog.asserts.assert(this.autoPan_); + var map = this.getMap(); + + if (!goog.isDef(map) || goog.isNull(map.getTargetElement())) { + return; + } + + var mapRect = this.getRect_(map.getTargetElement(), map.getSize()); + var element = this.getElement(); + goog.asserts.assert(!goog.isNull(element) && goog.isDef(element)); + var overlayRect = this.getRect_(element, + [ol.dom.outerWidth(element), ol.dom.outerHeight(element)]); + + var margin = this.autoPanMargin_; + if (!ol.extent.containsExtent(mapRect, overlayRect)) { + // the overlay is not completely inside the viewport, so pan the map + var offsetLeft = overlayRect[0] - mapRect[0]; + var offsetRight = mapRect[2] - overlayRect[2]; + var offsetTop = overlayRect[1] - mapRect[1]; + var offsetBottom = mapRect[3] - overlayRect[3]; + + var delta = [0, 0]; + if (offsetLeft < 0) { + // move map to the left + delta[0] = offsetLeft - margin; + } else if (offsetRight < 0) { + // move map to the right + delta[0] = Math.abs(offsetRight) + margin; + } + if (offsetTop < 0) { + // move map up + delta[1] = offsetTop - margin; + } else if (offsetBottom < 0) { + // move map down + delta[1] = Math.abs(offsetBottom) + margin; + } + + if (delta[0] !== 0 || delta[1] !== 0) { + var center = map.getView().getCenter(); + goog.asserts.assert(goog.isDef(center)); + var centerPx = map.getPixelFromCoordinate(center); + var newCenterPx = [ + centerPx[0] + delta[0], + centerPx[1] + delta[1] + ]; + + if (!goog.isNull(this.autoPanAnimation_)) { + this.autoPanAnimation_.source = center; + map.beforeRender(ol.animation.pan(this.autoPanAnimation_)); + } + map.getView().setCenter(map.getCoordinateFromPixel(newCenterPx)); + } + } +}; + + +/** + * Get the extent of an element relative to the document + * @param {Element|undefined} element The element. + * @param {ol.Size|undefined} size The size of the element. + * @return {ol.Extent} + * @private + */ +ol.Overlay.prototype.getRect_ = function(element, size) { + goog.asserts.assert(!goog.isNull(element) && goog.isDef(element)); + goog.asserts.assert(goog.isDef(size)); + + var offset = goog.style.getPageOffset(element); + return [ + offset.x, + offset.y, + offset.x + size[0], + offset.y + size[1] + ]; +}; + + /** * Set the positioning for this overlay. * @param {ol.OverlayPositioning} positioning how the overlay is