Merge pull request #2769 from adube/overviewmap

OverviewMap control
This commit is contained in:
Éric Lemoine
2014-10-09 19:33:45 +02:00
8 changed files with 816 additions and 0 deletions

View File

@@ -296,3 +296,40 @@ button.ol-full-screen-true:after {
.ol-has-tooltip [role=tooltip] {
font-family: 'Lucida Grande',Verdana,Geneva,Lucida,Arial,Helvetica,sans-serif;
}
.ol-overviewmap {
position: absolute;
left: 0.5em;
bottom: 0.5em;
}
.ol-overviewmap.ol-uncollapsible {
bottom: 0;
left: 0;
border-radius: 0 4px 0 0;
}
.ol-overviewmap .ol-overviewmap-map,
.ol-overviewmap button {
display: inline-block;
}
.ol-overviewmap .ol-overviewmap-map {
border: 1px solid #7b98bc;
height: 150px;
margin: 2px;
width: 150px;
}
.ol-overviewmap:not(.ol-collapsed) button{
bottom: 1px;
left: 2px;
position: absolute;
}
.ol-overviewmap:not(.ol-collapsed) button:hover [role=tooltip],
.ol-overviewmap.ol-collapsed .ol-overviewmap-map,
.ol-overviewmap.ol-uncollapsible button {
display: none;
}
.ol-overviewmap:not(.ol-collapsed) {
background: rgba(255,255,255,0.8);
}
.ol-overviewmap-box {
border: 2px dotted rgba(0,60,136,0.7);
}

View File

@@ -0,0 +1,77 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<link rel="stylesheet" href="../css/ol.css" type="text/css">
<link rel="stylesheet" href="../resources/bootstrap/css/bootstrap.min.css" type="text/css">
<link rel="stylesheet" href="../resources/layout.css" type="text/css">
<link rel="stylesheet" href="../resources/bootstrap/css/bootstrap-responsive.min.css" type="text/css">
<style type="text/css">
.ol-custom-overviewmap,
.ol-custom-overviewmap.ol-uncollapsible {
bottom: auto;
left: auto;
right: 0;
top: 0;
}
.ol-custom-overviewmap:not(.ol-collapsed) {
border: 1px solid black;
}
.ol-custom-overviewmap .ol-overviewmap-map {
border: none;
width: 300px;
}
.ol-custom-overviewmap .ol-overviewmap-box {
border: 2px solid red;
}
.ol-custom-overviewmap:not(.ol-collapsed) button{
bottom: auto;
left: auto;
right: 1px;
top: 1px;
}
.ol-rotate {
top: 170px;
right: 0;
}
</style>
<title>ol3 OverviewMap control with advanced customization example</title>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" href="./"><img src="../resources/logo.png"> OpenLayers 3 Examples</a>
</div>
</div>
</div>
<div class="container-fluid">
<h4 id="title">OverviewMap control, advanced</h4>
<div id="map" class="map"></div>
<div class="row-fluid">
<p id="shortdesc">Example of OverviewMap control with advanced customization.</p>
<div id="docs">
<p>See the <a href="overviewmap-custom.js" target="_blank">overviewmap-custom.js source</a> to see how this is done.</p>
<p>This example demonstrates how you can customize the overviewmap control using its supported options as well as defining custom CSS. You can also rotate the map using the shift key to see how the overview map reacts.</p>
</div>
<div id="tags">overview, overviewmap</div>
</div>
</div>
<script src="../resources/jquery.min.js" type="text/javascript"></script>
<script src="../resources/example-behaviour.js" type="text/javascript"></script>
<script src="loader.js?id=overviewmap-custom" type="text/javascript"></script>
</body>
</html>

View File

@@ -0,0 +1,43 @@
goog.require('ol.Map');
goog.require('ol.View');
goog.require('ol.control');
goog.require('ol.control.OverviewMap');
goog.require('ol.interaction');
goog.require('ol.interaction.DragRotateAndZoom');
goog.require('ol.layer.Tile');
goog.require('ol.source.OSM');
var overviewMapControl = new ol.control.OverviewMap({
// see in overviewmap-custom.html to see the custom CSS used
className: 'ol-overviewmap ol-custom-overviewmap',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM({
'url': '//{a-c}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png'
})
})
],
collapseLabel: '\u00BB',
label: '\u00AB',
collapsed: false
});
var map = new ol.Map({
controls: ol.control.defaults().extend([
overviewMapControl
]),
interactions: ol.interaction.defaults().extend([
new ol.interaction.DragRotateAndZoom()
]),
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
target: 'map',
view: new ol.View({
center: [500000, 6000000],
zoom: 7
})
});

41
examples/overviewmap.html Normal file
View File

@@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<link rel="stylesheet" href="../css/ol.css" type="text/css">
<link rel="stylesheet" href="../resources/bootstrap/css/bootstrap.min.css" type="text/css">
<link rel="stylesheet" href="../resources/layout.css" type="text/css">
<link rel="stylesheet" href="../resources/bootstrap/css/bootstrap-responsive.min.css" type="text/css">
<title>ol3 OverviewMap control example</title>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" href="./"><img src="../resources/logo.png"> OpenLayers 3 Examples</a>
</div>
</div>
</div>
<div class="container-fluid">
<h4 id="title">OverviewMap control</h4>
<div id="map" class="map"></div>
<div class="row-fluid">
<p id="shortdesc">Example of OverviewMap control.</p>
<div id="docs">
<p>See the <a href="overviewmap.js" target="_blank">overviewmap.js source</a> to see how this is done.</p>
</div>
<div id="tags">overview, overviewmap</div>
</div>
</div>
<script src="../resources/jquery.min.js" type="text/javascript"></script>
<script src="../resources/example-behaviour.js" type="text/javascript"></script>
<script src="loader.js?id=overviewmap" type="text/javascript"></script>
</body>
</html>

22
examples/overviewmap.js Normal file
View File

@@ -0,0 +1,22 @@
goog.require('ol.Map');
goog.require('ol.View');
goog.require('ol.control');
goog.require('ol.control.OverviewMap');
goog.require('ol.layer.Tile');
goog.require('ol.source.OSM');
var map = new ol.Map({
controls: ol.control.defaults().extend([
new ol.control.OverviewMap()
]),
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
target: 'map',
view: new ol.View({
center: [500000, 6000000],
zoom: 7
})
});

View File

@@ -1037,6 +1037,78 @@ olx.control.MousePositionOptions.prototype.target;
olx.control.MousePositionOptions.prototype.undefinedHTML;
/**
* @typedef {{collapsed: (boolean|undefined),
* collapseLabel: (string|undefined),
* collapsible: (boolean|undefined),
* label: (string|undefined),
* layers: (Array.<ol.layer.Layer>|ol.Collection|undefined),
* target: (Element|undefined),
* tipLabel: (string|undefined)}}
* @api
*/
olx.control.OverviewMapOptions;
/**
* Whether the control should start collapsed or not (expanded).
* Default to `true`.
* @type {boolean|undefined}
* @api
*/
olx.control.OverviewMapOptions.prototype.collapsed;
/**
* Text label to use for the expanded overviewmap button. Default is `«`
* @type {string|undefined}
* @api
*/
olx.control.OverviewMapOptions.prototype.collapseLabel;
/**
* Whether the control can be collapsed or not. Default to `true`.
* @type {boolean|undefined}
* @api
*/
olx.control.OverviewMapOptions.prototype.collapsible;
/**
* Text label to use for the collapsed overviewmap button. Default is `»`
* @type {string|undefined}
* @api
*/
olx.control.OverviewMapOptions.prototype.label;
/**
* Layers for the overview map. If not set, then all main map layers are used
* instead.
* @type {!Array.<ol.layer.Layer>|!ol.Collection|undefined}
* @api
*/
olx.control.OverviewMapOptions.prototype.layers;
/**
* Specify a target if you want the control to be rendered outside of the map's
* viewport.
* @type {Element|undefined}
* @api
*/
olx.control.OverviewMapOptions.prototype.target;
/**
* Text label to use for the button tip. Default is `Overview map`
* @type {string|undefined}
* @api
*/
olx.control.OverviewMapOptions.prototype.tipLabel;
/**
* @typedef {{className: (string|undefined),
* minWidth: (number|undefined),

View File

@@ -0,0 +1,510 @@
goog.provide('ol.control.OverviewMap');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classlist');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.math.Size');
goog.require('goog.style');
goog.require('ol.Collection');
goog.require('ol.Map');
goog.require('ol.MapEventType');
goog.require('ol.Object');
goog.require('ol.Overlay');
goog.require('ol.OverlayPositioning');
goog.require('ol.View');
goog.require('ol.control.Control');
goog.require('ol.coordinate');
goog.require('ol.css');
goog.require('ol.extent');
goog.require('ol.pointer.PointerEventHandler');
/**
* Create a new control with a map acting as an overview map for an other
* defined map.
* @constructor
* @extends {ol.control.Control}
* @param {olx.control.OverviewMapOptions=} opt_options OverviewMap options.
* @api
*/
ol.control.OverviewMap = function(opt_options) {
var options = goog.isDef(opt_options) ? opt_options : {};
/**
* @type {boolean}
* @private
*/
this.collapsed_ = goog.isDef(options.collapsed) ? options.collapsed : true;
/**
* @private
* @type {boolean}
*/
this.collapsible_ = goog.isDef(options.collapsible) ?
options.collapsible : true;
if (!this.collapsible_) {
this.collapsed_ = false;
}
var className = goog.isDef(options.className) ?
options.className : 'ol-overviewmap';
var tipLabel = goog.isDef(options.tipLabel) ?
options.tipLabel : 'Overview map';
var tip = goog.dom.createDom(goog.dom.TagName.SPAN, {
'role' : 'tooltip'
}, tipLabel);
/**
* @private
* @type {string}
*/
this.collapseLabel_ = goog.isDef(options.collapseLabel) ?
options.collapseLabel : '\u00AB';
/**
* @private
* @type {string}
*/
this.label_ = goog.isDef(options.label) ? options.label : '\u00BB';
var label = goog.dom.createDom(goog.dom.TagName.SPAN, {},
(this.collapsible_ && !this.collapsed_) ?
this.collapseLabel_ : this.label_);
/**
* @private
* @type {Element}
*/
this.labelSpan_ = label;
var button = goog.dom.createDom(goog.dom.TagName.BUTTON, {
'class': 'ol-has-tooltip',
'type': 'button'
}, this.labelSpan_);
goog.dom.appendChild(button, tip);
var buttonHandler = new ol.pointer.PointerEventHandler(button);
this.registerDisposable(buttonHandler);
goog.events.listen(buttonHandler, ol.pointer.EventType.POINTERUP,
this.handlePointerUp_, false, this);
goog.events.listen(button, goog.events.EventType.CLICK,
this.handleClick_, false, this);
goog.events.listen(button, [
goog.events.EventType.MOUSEOUT,
goog.events.EventType.FOCUSOUT
], function() {
this.blur();
}, false);
var ovmapDiv = goog.dom.createDom(goog.dom.TagName.DIV, 'ol-overviewmap-map');
/**
* @type {ol.Map}
* @private
*/
this.ovmap_ = new ol.Map({
controls: new ol.Collection(),
interactions: new ol.Collection(),
target: ovmapDiv
});
var ovmap = this.ovmap_;
if (goog.isDef(options.layers)) {
options.layers.forEach(
/**
* @param {ol.layer.Layer} layer Layer.
*/
function(layer) {
ovmap.addLayer(layer);
}, this);
}
var box = goog.dom.createDom(goog.dom.TagName.DIV, 'ol-overviewmap-box');
/**
* @type {ol.Overlay}
* @private
*/
this.boxOverlay_ = new ol.Overlay({
position: [0, 0],
positioning: ol.OverlayPositioning.BOTTOM_LEFT,
element: box
});
this.ovmap_.addOverlay(this.boxOverlay_);
var element = goog.dom.createDom(goog.dom.TagName.DIV, {
'class': className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
ol.css.CLASS_CONTROL +
(this.collapsed_ && this.collapsible_ ? ' ol-collapsed' : '') +
(this.collapsible_ ? '' : ' ol-uncollapsible')
}, ovmapDiv, button);
goog.base(this, {
element: element,
target: options.target
});
};
goog.inherits(ol.control.OverviewMap, ol.control.Control);
/**
* @inheritDoc
* @api
*/
ol.control.OverviewMap.prototype.setMap = function(map) {
var currentMap = this.getMap();
if (goog.isNull(map) && !goog.isNull(currentMap)) {
goog.events.unlisten(
currentMap, ol.Object.getChangeEventType(ol.MapProperty.VIEW),
this.handleViewChanged_, false, this);
}
goog.base(this, 'setMap', map);
if (!goog.isNull(map)) {
// if no layers were set for the overviewmap map, then bind with
// those in the main map
if (this.ovmap_.getLayers().getLength() === 0) {
this.ovmap_.bindTo(ol.MapProperty.LAYERGROUP, map);
}
// bind current map view, or any new one
this.bindView_();
goog.events.listen(
map, ol.Object.getChangeEventType(ol.MapProperty.VIEW),
this.handleViewChanged_, false, this);
this.ovmap_.updateSize();
this.resetExtent_();
}
};
/**
* Bind some actions to the main map view.
* @private
*/
ol.control.OverviewMap.prototype.bindView_ = function() {
var map = this.getMap();
var view = map.getView();
// if the map does not have a view, we can't act upon it
if (goog.isNull(view)) {
return;
}
// FIXME - the overviewmap view rotation currently follows the one used
// by the main map view. We could support box rotation instead. The choice
// between the 2 modes would be made in a single option
this.ovmap_.getView().bindTo(ol.ViewProperty.ROTATION, view);
};
/**
* Function called on each map render. Executes in a requestAnimationFrame
* callback. Manage the extent of the overview map accordingly,
* then update the overview map box.
* @param {goog.events.Event} event Event.
*/
ol.control.OverviewMap.prototype.handleMapPostrender = function(event) {
this.validateExtent_();
this.updateBox_();
};
/**
* Called on main map view changed.
* @param {goog.events.Event} event Event.
* @private
*/
ol.control.OverviewMap.prototype.handleViewChanged_ = function(event) {
this.bindView_();
};
/**
* Reset the overview map extent if the box size (width or
* height) is less than the size of the overview map size times minRatio
* or is greater than the size of the overview size times maxRatio.
*
* If the map extent was not reset, the box size can fits in the defined
* ratio sizes. This method then checks if is contained inside the overview
* map current extent. If not, recenter the overview map to the current
* main map center location.
* @private
*/
ol.control.OverviewMap.prototype.validateExtent_ = function() {
var map = this.getMap();
var ovmap = this.ovmap_;
if (!map.isRendered() || !ovmap.isRendered()) {
return;
}
var mapSize = map.getSize();
goog.asserts.assertArray(mapSize);
var view = map.getView();
goog.asserts.assert(goog.isDef(view));
var extent = view.calculateExtent(mapSize);
var ovmapSize = ovmap.getSize();
goog.asserts.assertArray(ovmapSize);
var ovview = ovmap.getView();
goog.asserts.assert(goog.isDef(ovview));
var ovextent = ovview.calculateExtent(ovmapSize);
var topLeftPixel =
ovmap.getPixelFromCoordinate(ol.extent.getTopLeft(extent));
var bottomRightPixel =
ovmap.getPixelFromCoordinate(ol.extent.getBottomRight(extent));
var boxSize = new goog.math.Size(
Math.abs(topLeftPixel[0] - bottomRightPixel[0]),
Math.abs(topLeftPixel[1] - bottomRightPixel[1]));
var ovmapWidth = ovmapSize[0];
var ovmapHeight = ovmapSize[1];
if (boxSize.width < ovmapWidth * ol.OVERVIEWMAP_MIN_RATIO ||
boxSize.height < ovmapHeight * ol.OVERVIEWMAP_MIN_RATIO ||
boxSize.width > ovmapWidth * ol.OVERVIEWMAP_MAX_RATIO ||
boxSize.height > ovmapHeight * ol.OVERVIEWMAP_MAX_RATIO) {
this.resetExtent_();
} else if (!ol.extent.containsExtent(ovextent, extent)) {
this.recenter_();
}
};
/**
* Reset the overview map extent to half calculated min and max ratio times
* the extent of the main map.
* @private
*/
ol.control.OverviewMap.prototype.resetExtent_ = function() {
if (ol.OVERVIEWMAP_MAX_RATIO === 0 || ol.OVERVIEWMAP_MIN_RATIO === 0) {
return;
}
var map = this.getMap();
var ovmap = this.ovmap_;
var mapSize = map.getSize();
goog.asserts.assertArray(mapSize);
var view = map.getView();
goog.asserts.assert(goog.isDef(view));
var extent = view.calculateExtent(mapSize);
var ovmapSize = ovmap.getSize();
goog.asserts.assertArray(ovmapSize);
var ovview = ovmap.getView();
goog.asserts.assert(goog.isDef(ovview));
// get how many times the current map overview could hold different
// box sizes using the min and max ratio, pick the step in the middle used
// to calculate the extent from the main map to set it to the overview map,
var steps = Math.log(
ol.OVERVIEWMAP_MAX_RATIO / ol.OVERVIEWMAP_MIN_RATIO) / Math.LN2;
var ratio = 1 / (Math.pow(2, steps / 2) * ol.OVERVIEWMAP_MIN_RATIO);
ol.extent.scaleFromCenter(extent, ratio);
ovview.fitExtent(extent, ovmapSize);
};
/**
* Set the center of the overview map to the map center without changing its
* resolution.
* @private
*/
ol.control.OverviewMap.prototype.recenter_ = function() {
var map = this.getMap();
var ovmap = this.ovmap_;
var view = map.getView();
goog.asserts.assert(goog.isDef(view));
var ovview = ovmap.getView();
goog.asserts.assert(goog.isDef(ovview));
ovview.setCenter(view.getCenter());
};
/**
* Update the box using the main map extent
* @private
*/
ol.control.OverviewMap.prototype.updateBox_ = function() {
var map = this.getMap();
var ovmap = this.ovmap_;
if (!map.isRendered() || !ovmap.isRendered()) {
return;
}
var mapSize = map.getSize();
goog.asserts.assertArray(mapSize);
var view = map.getView();
goog.asserts.assert(goog.isDef(view));
var ovview = ovmap.getView();
goog.asserts.assert(goog.isDef(ovview));
var ovmapSize = ovmap.getSize();
goog.asserts.assertArray(ovmapSize);
var rotation = view.getRotation();
goog.asserts.assert(goog.isDef(rotation));
var overlay = this.boxOverlay_;
var box = this.boxOverlay_.getElement();
var extent = view.calculateExtent(mapSize);
var ovresolution = ovview.getResolution();
var bottomLeft = ol.extent.getBottomLeft(extent);
var topRight = ol.extent.getTopRight(extent);
// set position using bottom left coordinates
var rotateBottomLeft = this.calculateCoordinateRotate_(rotation, bottomLeft);
overlay.setPosition(rotateBottomLeft);
// set box size calculated from map extent size and overview map resolution
if (goog.isDefAndNotNull(box)) {
var boxWidth = Math.abs((bottomLeft[0] - topRight[0]) / ovresolution);
var boxHeight = Math.abs((topRight[1] - bottomLeft[1]) / ovresolution);
goog.style.setBorderBoxSize(box, new goog.math.Size(
boxWidth, boxHeight));
}
};
/**
* @param {number} rotation Target rotation.
* @param {ol.Coordinate} coordinate Coordinate.
* @return {ol.Coordinate|undefined} Coordinate for rotation and center anchor.
* @private
*/
ol.control.OverviewMap.prototype.calculateCoordinateRotate_ = function(
rotation, coordinate) {
var coordinateRotate;
var map = this.getMap();
var view = map.getView();
goog.asserts.assert(goog.isDef(view));
var currentCenter = view.getCenter();
if (goog.isDef(currentCenter)) {
coordinateRotate = [
coordinate[0] - currentCenter[0],
coordinate[1] - currentCenter[1]
];
ol.coordinate.rotate(coordinateRotate, rotation);
ol.coordinate.add(coordinateRotate, currentCenter);
}
return coordinateRotate;
};
/**
* @param {goog.events.BrowserEvent} event The event to handle
* @private
*/
ol.control.OverviewMap.prototype.handleClick_ = function(event) {
if (event.screenX !== 0 && event.screenY !== 0) {
return;
}
this.handleToggle_();
};
/**
* @param {ol.pointer.PointerEvent} pointerEvent The event to handle
* @private
*/
ol.control.OverviewMap.prototype.handlePointerUp_ = function(pointerEvent) {
pointerEvent.browserEvent.preventDefault();
this.handleToggle_();
};
/**
* @private
*/
ol.control.OverviewMap.prototype.handleToggle_ = function() {
goog.dom.classlist.toggle(this.element, 'ol-collapsed');
goog.dom.setTextContent(this.labelSpan_,
(this.collapsed_) ? this.collapseLabel_ : this.label_);
this.collapsed_ = !this.collapsed_;
// manage overview map if it had not been rendered before and control
// is expanded
var ovmap = this.ovmap_;
if (!this.collapsed_ && !ovmap.isRendered()) {
ovmap.updateSize();
this.resetExtent_();
goog.events.listenOnce(ovmap, ol.MapEventType.POSTRENDER,
function(event) {
this.updateBox_();
},
false, this);
}
};
/**
* @return {boolean} True if the widget is collapsible.
* @api stable
*/
ol.control.OverviewMap.prototype.getCollapsible = function() {
return this.collapsible_;
};
/**
* @param {boolean} collapsible True if the widget is collapsible.
* @api stable
*/
ol.control.OverviewMap.prototype.setCollapsible = function(collapsible) {
if (this.collapsible_ === collapsible) {
return;
}
this.collapsible_ = collapsible;
goog.dom.classlist.toggle(this.element, 'ol-uncollapsible');
if (!collapsible && this.collapsed_) {
this.handleToggle_();
}
};
/**
* @param {boolean} collapsed True if the widget is collapsed.
* @api stable
*/
ol.control.OverviewMap.prototype.setCollapsed = function(collapsed) {
if (!this.collapsible_ || this.collapsed_ === collapsed) {
return;
}
this.handleToggle_();
};
/**
* @return {boolean} True if the widget is collapsed.
* @api stable
*/
ol.control.OverviewMap.prototype.getCollapsed = function() {
return this.collapsed_;
};

View File

@@ -187,6 +187,20 @@ ol.MOUSEWHEELZOOM_MAXDELTA = 1;
ol.MOUSEWHEELZOOM_TIMEOUT_DURATION = 80;
/**
* @define {number} Maximum width and/or height extent ratio that determines
* when the overview map should be zoomed out.
*/
ol.OVERVIEWMAP_MAX_RATIO = 0.75;
/**
* @define {number} Minimum width and/or height extent ratio that determines
* when the overview map should be zoomed in.
*/
ol.OVERVIEWMAP_MIN_RATIO = 0.1;
/**
* @define {number} Rotate animation duration.
*/