diff --git a/examples/center.html b/examples/center.html new file mode 100644 index 0000000000..20eff037c4 --- /dev/null +++ b/examples/center.html @@ -0,0 +1,135 @@ + + + + + + + + + + + + Advanced View Positioning example + + + + + +
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+ (best fit),
+ (respect resolution constraint).
+ (nearest),
+ (with min resolution),
+ +
+
+ +
+ +
+

Advanced View Positioning example

+

This example demonstrates how a map's view can be + adjusted so a geometry or coordinate is positioned at a specific + pixel location. The map above has top, right, bottom, and left + padding applied inside the viewport. The view's fitGeometry method + is used to fit a geometry in the view with the same padding. The + view's centerOn method is used to position a coordinate (Lausanne) + at a specific pixel location (the center of the black box).

+
+

Use Alt+Shift+drag to rotate the map.

+

See the center.js source to see how this is done.

+
+
center, rotation, openstreetmap
+
+ +
+ +
+ + + + + + + diff --git a/examples/center.js b/examples/center.js new file mode 100644 index 0000000000..23c2770885 --- /dev/null +++ b/examples/center.js @@ -0,0 +1,128 @@ +goog.require('ol.Map'); +goog.require('ol.View2D'); +goog.require('ol.geom.Point'); +goog.require('ol.geom.SimpleGeometry'); +goog.require('ol.layer.Tile'); +goog.require('ol.layer.Vector'); +goog.require('ol.source.GeoJSON'); +goog.require('ol.source.OSM'); +goog.require('ol.style.Circle'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); + +var source = new ol.source.GeoJSON({ + projection: 'EPSG:3857', + url: 'data/geojson/switzerland.geojson' +}); +var style = new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.6)' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3', + width: 1 + }), + image: new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.6)' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3', + width: 1 + }) + }) +}); +var vectorLayer = new ol.layer.Vector({ + source: source, + style: style +}); +var view = new ol.View2D({ + center: [0, 0], + zoom: 1 +}); +var map = new ol.Map({ + layers: [ + new ol.layer.Tile({ + source: new ol.source.OSM() + }), + vectorLayer + ], + renderer: 'canvas', + target: 'map', + view: view +}); + +var zoomtoswitzerlandbest = document.getElementById('zoomtoswitzerlandbest'); +zoomtoswitzerlandbest.addEventListener('click', function() { + var feature = source.getFeatures()[0]; + var polygon = /** @type {ol.geom.SimpleGeometry} */ (feature.getGeometry()); + var size = /** @type {ol.Size} */ (map.getSize()); + view.fitGeometry( + polygon, + size, + { + padding: [170, 50, 30, 150], + constrainResolution: false + } + ); +}, false); + +var zoomtoswitzerlandconstrained = + document.getElementById('zoomtoswitzerlandconstrained'); +zoomtoswitzerlandconstrained.addEventListener('click', function() { + var feature = source.getFeatures()[0]; + var polygon = /** @type {ol.geom.SimpleGeometry} */ (feature.getGeometry()); + var size = /** @type {ol.Size} */ (map.getSize()); + view.fitGeometry( + polygon, + size, + { + padding: [170, 50, 30, 150] + } + ); +}, false); + +var zoomtoswitzerlandnearest = + document.getElementById('zoomtoswitzerlandnearest'); +zoomtoswitzerlandnearest.addEventListener('click', function() { + var feature = source.getFeatures()[0]; + var polygon = /** @type {ol.geom.SimpleGeometry} */ (feature.getGeometry()); + var size = /** @type {ol.Size} */ (map.getSize()); + view.fitGeometry( + polygon, + size, + { + padding: [170, 50, 30, 150], + nearest: true + } + ); +}, false); + +var zoomtolausanne = document.getElementById('zoomtolausanne'); +zoomtolausanne.addEventListener('click', function() { + var feature = source.getFeatures()[1]; + var point = /** @type {ol.geom.SimpleGeometry} */ (feature.getGeometry()); + var size = /** @type {ol.Size} */ (map.getSize()); + view.fitGeometry( + point, + size, + { + padding: [170, 50, 30, 150], + minResolution: 50 + } + ); +}, false); + +var centerlausanne = document.getElementById('centerlausanne'); +centerlausanne.addEventListener('click', function() { + var feature = source.getFeatures()[1]; + var point = /** @type {ol.geom.Point} */ (feature.getGeometry()); + var size = /** @type {ol.Size} */ (map.getSize()); + view.centerOn( + point.getCoordinates(), + size, + [570, 500] + ); +}, false); diff --git a/examples/data/geojson/switzerland.geojson b/examples/data/geojson/switzerland.geojson new file mode 100644 index 0000000000..eea0851bfd --- /dev/null +++ b/examples/data/geojson/switzerland.geojson @@ -0,0 +1,4 @@ +{"type":"FeatureCollection","features":[ +{"type":"Feature","id":"CHE","properties":{"name":"Switzerland"},"geometry":{"type":"Polygon","coordinates":[[[9.594226,47.525058],[9.632932,47.347601],[9.47997,47.10281],[9.932448,46.920728],[10.442701,46.893546],[10.363378,46.483571],[9.922837,46.314899],[9.182882,46.440215],[8.966306,46.036932],[8.489952,46.005151],[8.31663,46.163642],[7.755992,45.82449],[7.273851,45.776948],[6.843593,45.991147],[6.5001,46.429673],[6.022609,46.27299],[6.037389,46.725779],[6.768714,47.287708],[6.736571,47.541801],[7.192202,47.449766],[7.466759,47.620582],[8.317301,47.61358],[8.522612,47.830828],[9.594226,47.525058]]]}}, +{"type":"Feature","id":"LSNE","properties":{"name":"Lausanne"},"geometry":{"type":"Point","coordinates":[6.6339863,46.5193823]}} +]} diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 2ce516d266..710a7d924e 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -1074,3 +1074,12 @@ * @property {!Array.} resolutions Resolutions. * @todo stability experimental */ + +/** + * @typedef {Object} olx.View2D.fitGeometryOptions + * @property {!Array.} padding Padding (in pixels) to be cleared inside the view. Values in the array are top, right, bottom and left padding. Default is `[0, 0, 0, 0]`. + * @property {boolean|undefined} constrainResolution Constrain the resolution. Default is `true`. + * @property {boolean|undefined} nearest Get the nearest extent. Default is `false`. + * @property {number|undefined} minResolution Minimum resolution that we zoom to. Default is `0`. + * @todo stability experimental + */ diff --git a/src/ol/view2d.exports b/src/ol/view2d.exports index 85318dd373..01d44dd7b5 100644 --- a/src/ol/view2d.exports +++ b/src/ol/view2d.exports @@ -3,6 +3,8 @@ @exportProperty ol.View2D.prototype.constrainResolution @exportProperty ol.View2D.prototype.constrainRotation @exportProperty ol.View2D.prototype.fitExtent +@exportProperty ol.View2D.prototype.fitGeometry +@exportProperty ol.View2D.prototype.centerOn @exportProperty ol.View2D.prototype.getView2D @exportProperty ol.View2D.prototype.getZoom @exportProperty ol.View2D.prototype.setZoom diff --git a/src/ol/view2d.js b/src/ol/view2d.js index c11764adaa..0276506464 100644 --- a/src/ol/view2d.js +++ b/src/ol/view2d.js @@ -430,6 +430,101 @@ ol.View2D.prototype.fitExtent = function(extent, size) { }; +/** + * Fit the given geometry based on the given map size and border. + * Take care on the map angle. + * @param {ol.geom.SimpleGeometry} geometry Geometry. + * @param {ol.Size} size Box pixel size. + * @param {olx.View2D.fitGeometryOptions=} opt_options Options. + * @todo stability experimental + */ +ol.View2D.prototype.fitGeometry = function(geometry, size, opt_options) { + var options = goog.isDef(opt_options) ? opt_options : {}; + + var padding = goog.isDef(options.padding) ? options.padding : [0, 0, 0, 0]; + var constrainResolution = goog.isDef(options.constrainResolution) ? + options.constrainResolution : true; + var nearest = goog.isDef(options.nearest) ? options.nearest : false; + var minResolution = goog.isDef(options.minResolution) ? + options.minResolution : 0; + var coords = geometry.getFlatCoordinates(); + + // calculate rotated extent + var rotation = this.getRotation(); + goog.asserts.assert(goog.isDef(rotation)); + var cosAngle = Math.cos(-rotation); + var sinAngle = Math.sin(-rotation); + var minRotX = +Infinity; + var minRotY = +Infinity; + var maxRotX = -Infinity; + var maxRotY = -Infinity; + var stride = geometry.getStride(); + for (var i = 0, ii = coords.length; i < ii; i += stride) { + var rotX = coords[i] * cosAngle - coords[i + 1] * sinAngle; + var rotY = coords[i] * sinAngle + coords[i + 1] * cosAngle; + minRotX = Math.min(minRotX, rotX); + minRotY = Math.min(minRotY, rotY); + maxRotX = Math.max(maxRotX, rotX); + maxRotY = Math.max(maxRotY, rotY); + } + + // calculate resolution + var resolution = this.getResolutionForExtent( + [minRotX, minRotY, maxRotX, maxRotY], + [size[0] - padding[1] - padding[3], size[1] - padding[0] - padding[2]]); + resolution = isNaN(resolution) ? minResolution : + Math.max(resolution, minResolution); + if (constrainResolution) { + var constrainedResolution = this.constrainResolution(resolution, 0, 0); + if (!nearest && constrainedResolution < resolution) { + constrainedResolution = this.constrainResolution( + constrainedResolution, -1, 0); + } + resolution = constrainedResolution; + } + this.setResolution(resolution); + + // calculate center + sinAngle = -sinAngle; // go back to original rotation + var centerRotX = (minRotX + maxRotX) / 2; + var centerRotY = (minRotY + maxRotY) / 2; + centerRotX += (padding[1] - padding[3]) / 2 * resolution; + centerRotY += (padding[0] - padding[2]) / 2 * resolution; + var centerX = centerRotX * cosAngle - centerRotY * sinAngle; + var centerY = centerRotY * cosAngle + centerRotX * sinAngle; + + this.setCenter([centerX, centerY]); +}; + + +/** + * Center on coordinate and view position. + * Take care on the map angle. + * @param {ol.Coordinate} coordinate Coordinate. + * @param {ol.Size} size Box pixel size. + * @param {ol.Pixel} position Position on the view to center on. + * @todo stability experimental + */ +ol.View2D.prototype.centerOn = function(coordinate, size, position) { + // calculate rotated position + var rotation = this.getRotation(); + var cosAngle = Math.cos(-rotation); + var sinAngle = Math.sin(-rotation); + var rotX = coordinate[0] * cosAngle - coordinate[1] * sinAngle; + var rotY = coordinate[1] * cosAngle + coordinate[0] * sinAngle; + var resolution = this.getResolution(); + rotX += (size[0] / 2 - position[0]) * resolution; + rotY += (position[1] - size[1] / 2) * resolution; + + // go back to original angle + sinAngle = -sinAngle; // go back to original rotation + var centerX = rotX * cosAngle - rotY * sinAngle; + var centerY = rotY * cosAngle + rotX * sinAngle; + + this.setCenter([centerX, centerY]); +}; + + /** * @return {boolean} Is defined. */ diff --git a/test/spec/ol/view2d.test.js b/test/spec/ol/view2d.test.js index 64c192c962..0e51e0d3ac 100644 --- a/test/spec/ol/view2d.test.js +++ b/test/spec/ol/view2d.test.js @@ -102,6 +102,106 @@ describe('ol.View2D', function() { expect(view.getZoom()).to.be(undefined); }); }); + + describe('fitGeometry', function() { + var view; + beforeEach(function() { + view = new ol.View2D({ + resolutions: [200, 100, 50, 20, 10, 5, 2, 1] + }); + }); + it('fit correctly to the geometry', function() { + view.fitGeometry( + new ol.geom.LineString([[6000, 46000], [6000, 47100], [7000, 46000]]), + [200, 200], + { + padding: [100, 0, 0, 100], + constrainResolution: false + } + ); + expect(view.getResolution()).to.be(11); + expect(view.getCenter()[0]).to.be(5950); + expect(view.getCenter()[1]).to.be(47100); + + view.fitGeometry( + new ol.geom.LineString([[6000, 46000], [6000, 47100], [7000, 46000]]), + [200, 200], + { + padding: [100, 0, 0, 100] + } + ); + expect(view.getResolution()).to.be(20); + expect(view.getCenter()[0]).to.be(5500); + expect(view.getCenter()[1]).to.be(47550); + + view.fitGeometry( + new ol.geom.LineString([[6000, 46000], [6000, 47100], [7000, 46000]]), + [200, 200], + { + padding: [100, 0, 0, 100], + nearest: true + } + ); + expect(view.getResolution()).to.be(10); + expect(view.getCenter()[0]).to.be(6000); + expect(view.getCenter()[1]).to.be(47050); + + view.fitGeometry( + new ol.geom.Point([6000, 46000]), + [200, 200], + { + padding: [100, 0, 0, 100], + minResolution: 2 + } + ); + expect(view.getResolution()).to.be(2); + expect(view.getCenter()[0]).to.be(5900); + expect(view.getCenter()[1]).to.be(46100); + + view.setRotation(Math.PI / 4); + view.fitGeometry( + new ol.geom.LineString([[6000, 46000], [6000, 47100], [7000, 46000]]), + [200, 200], + { + padding: [100, 0, 0, 100], + constrainResolution: false + } + ); + expect(view.getResolution()).to.be(14.849242404917458); + expect(view.getCenter()[0]).to.be(5200.000000000011); + expect(view.getCenter()[1]).to.be(46300); + }); + }); + + describe('centerOn', function() { + var view; + beforeEach(function() { + view = new ol.View2D({ + resolutions: [200, 100, 50, 20, 10, 5, 2, 1] + }); + }); + it('fit correctly to the coordinates', function() { + view.setResolution(10); + view.centerOn( + [6000, 46000], + [400, 400], + [300, 300] + ); + expect(view.getCenter()[0]).to.be(5000); + expect(view.getCenter()[1]).to.be(47000); + + view.setRotation(Math.PI / 4); + view.centerOn( + [6000, 46000], + [400, 400], + [300, 300] + ); + expect(view.getCenter()[0]).to.be(4585.78643762691); + expect(view.getCenter()[1]).to.be(46000); + }); + }); }); goog.require('ol.View2D'); +goog.require('ol.geom.LineString'); +goog.require('ol.geom.Point');