diff --git a/examples/center.html b/examples/center.html index 11eae3ce7d..a981db28da 100644 --- a/examples/center.html +++ b/examples/center.html @@ -11,6 +11,11 @@ docs: > 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.

+

Note: This example does not shift the view center. So the zoom + controls and rotating the map will still use the center of the viewport as anchor. + To shift the whole view based on a padding, use the `padding` option on the view, + as shown in the view-padding.html example. +

tags: "center, rotation, openstreetmap" ---
diff --git a/examples/view-padding.css b/examples/view-padding.css new file mode 100644 index 0000000000..76bd8f5f7f --- /dev/null +++ b/examples/view-padding.css @@ -0,0 +1,54 @@ +.mapcontainer { + position: relative; + margin-bottom: 20px; +} +.map { + width: 1000px; + height: 600px; +} +.map .ol-zoom { + top: 178px; + left: 158px; +} +.map .ol-rotate { + top: 178px; + right: 58px; +} +.map .ol-attribution, +.map .ol-attribution.ol-uncollapsible { + bottom: 30px; + right: 50px; +} +.padding-top { + position: absolute; + top: 0; + left: 0px; + width: 1000px; + height: 170px; + background: rgba(255, 255, 255, 0.5); +} +.padding-left { + position: absolute; + top: 170px; + left: 0; + width: 150px; + height: 400px; + background: rgba(255, 255, 255, 0.5); +} +.padding-right { + position: absolute; + top: 170px; + left: 950px; + width: 50px; + height: 400px; + background: rgba(255, 255, 255, 0.5); +} +.padding-bottom { + position: absolute; + top: 570px; + left: 0px; + width: 1000px; + height: 30px; + background: rgba(255, 255, 255, 0.5); +} + diff --git a/examples/view-padding.html b/examples/view-padding.html new file mode 100644 index 0000000000..35e30cc9f3 --- /dev/null +++ b/examples/view-padding.html @@ -0,0 +1,23 @@ +--- +layout: example.html +title: View Padding +shortdesc: This example demonstrates the use of the view's padding option. +docs: > + This example demonstrates how a map's view can be configured to accommodate + for viewport space covered by other elements. + If the map viewport is partially covered with other content (overlays) along + its edges, the `padding` option allows to shift the center of the viewport away from + that content. The shifted viewport center will also be the anchor for zooming in and + out with the Zoom controls, and for rotating. +

Use Alt+Shift+Drag to rotate the map.

+tags: "center, padding, view, shift" +--- +
+
+
+
+
+
+
+ + diff --git a/examples/view-padding.js b/examples/view-padding.js new file mode 100644 index 0000000000..ee5b7936d1 --- /dev/null +++ b/examples/view-padding.js @@ -0,0 +1,73 @@ +import GeoJSON from '../src/ol/format/GeoJSON.js'; +import Map from '../src/ol/Map.js'; +import View from '../src/ol/View.js'; +import {Circle as CircleStyle, Fill, Stroke, Style} from '../src/ol/style.js'; +import {OSM, Vector as VectorSource} from '../src/ol/source.js'; +import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; +import {fromLonLat} from '../src/ol/proj.js'; + +/** @type {VectorSource} */ +const source = new VectorSource({ + url: 'data/geojson/switzerland.geojson', + format: new GeoJSON(), +}); +const style = new Style({ + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.6)', + }), + stroke: new Stroke({ + color: '#319FD3', + width: 1, + }), + image: new CircleStyle({ + radius: 5, + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.6)', + }), + stroke: new Stroke({ + color: '#319FD3', + width: 1, + }), + }), +}); +const vectorLayer = new VectorLayer({ + source: source, + style: style, +}); +const view = new View({ + center: fromLonLat([6.6339863, 46.5193823]), + padding: [170, 50, 30, 150], + zoom: 6, +}); +const map = new Map({ + layers: [ + new TileLayer({ + source: new OSM(), + }), + vectorLayer, + ], + target: 'map', + view: view, +}); + +const zoomtoswitzerland = document.getElementById('zoomtoswitzerland'); +zoomtoswitzerland.addEventListener( + 'click', + function () { + const feature = source.getFeatures()[0]; + const polygon = feature.getGeometry(); + view.fit(polygon); + }, + false +); + +const centerlausanne = document.getElementById('centerlausanne'); +centerlausanne.addEventListener( + 'click', + function () { + const feature = source.getFeatures()[1]; + const point = feature.getGeometry(); + view.setCenter(point.getCoordinates()); + }, + false +); diff --git a/src/ol/View.js b/src/ol/View.js index ffc75f2bd6..726474e9de 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -175,6 +175,10 @@ import {fromExtent as polygonFromExtent} from './geom/Polygon.js'; * level used to calculate the initial resolution for the view. * @property {number} [zoomFactor=2] The zoom factor used to compute the * corresponding resolution. + * @property {!Array} [padding=[0, 0, 0, 0]] Padding (in css pixels). + * If the map viewport is partially covered with other content (overlays) along + * its edges, this setting allows to shift the center of the viewport away from + * that content. The order of the values is top, right, bottom, left. */ /** @@ -394,6 +398,17 @@ class View extends BaseObject { */ this.resolutions_ = options.resolutions; + /** + * Padding (in css pixels). + * If the map viewport is partially covered with other content (overlays) along + * its edges, this setting allows to shift the center of the viewport away from that + * content. The order of the values in the array is top, right, bottom, left. + * The default is no padding, which is equivalent to `[0, 0, 0, 0]`. + * @type {Array|undefined} + * @api + */ + this.padding = options.padding; + /** * @private * @type {number} @@ -901,8 +916,8 @@ class View extends BaseObject { } /** - * @param {import("./size.js").Size=} opt_size Box pixel size. If not provided, the size of the - * first map that uses this view will be used. + * @param {import("./size.js").Size=} opt_size Box pixel size. If not provided, + * the map's last known viewport size will be used. * @return {import("./extent.js").Extent} Extent. */ calculateExtentInternal(opt_size) { @@ -1098,11 +1113,36 @@ class View extends BaseObject { ); } + /** + * Returns the size of the viewport minus padding. + * @private + * @return {import("./size.js").Size} Viewport size reduced by the padding. + */ + getViewportSizeMinusPadding_() { + let size = this.getViewportSize_(); + const padding = this.padding; + if (padding) { + size = [ + size[0] - padding[1] - padding[3], + size[1] - padding[0] - padding[2], + ]; + } + return size; + } + /** * @return {State} View state. */ getState() { - const center = /** @type {import("./coordinate.js").Coordinate} */ (this.getCenterInternal()); + let center = /** @type {import("./coordinate.js").Coordinate} */ (this.getCenterInternal()); + const padding = this.padding; + if (padding) { + const reducedSize = this.getViewportSizeMinusPadding_(); + center = this.calculateShiftedCenter(center, this.getViewportSize_(), [ + reducedSize[0] / 2 + padding[3], + reducedSize[1] / 2 + padding[0], + ]); + } const projection = this.getProjection(); const resolution = /** @type {number} */ (this.getResolution()); const rotation = this.getRotation(); @@ -1196,8 +1236,6 @@ class View extends BaseObject { * @api */ fit(geometryOrExtent, opt_options) { - const options = assign({size: this.getViewportSize_()}, opt_options || {}); - /** @type {import("./geom/SimpleGeometry.js").default} */ let geometry; assert( @@ -1228,7 +1266,7 @@ class View extends BaseObject { } } - this.fitInternal(geometry, options); + this.fitInternal(geometry, opt_options); } /** @@ -1239,7 +1277,7 @@ class View extends BaseObject { const options = opt_options || {}; let size = options.size; if (!size) { - size = this.getViewportSize_(); + size = this.getViewportSizeMinusPadding_(); } const padding = options.padding !== undefined ? options.padding : [0, 0, 0, 0]; @@ -1332,6 +1370,18 @@ class View extends BaseObject { * @param {import("./pixel.js").Pixel} position Position on the view to center on. */ centerOnInternal(coordinate, size, position) { + this.setCenterInternal( + this.calculateShiftedCenter(coordinate, size, position) + ); + } + + /** + * @param {import("./coordinate.js").Coordinate} coordinate Coordinate. + * @param {import("./size.js").Size} size Box pixel size. + * @param {import("./pixel.js").Pixel} position Position on the view to center on. + * @return {import("./coordinate.js").Coordinate} Shifted center. + */ + calculateShiftedCenter(coordinate, size, position) { // calculate rotated position const rotation = this.getRotation(); const cosAngle = Math.cos(-rotation); @@ -1347,7 +1397,7 @@ class View extends BaseObject { const centerX = rotX * cosAngle - rotY * sinAngle; const centerY = rotY * cosAngle + rotX * sinAngle; - this.setCenterInternal([centerX, centerY]); + return [centerX, centerY]; } /** diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index ae24f773a4..ea8d2852ee 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -1660,6 +1660,32 @@ describe('ol.View', function () { }); }); + describe('#getViewportSizeMinusPadding_()', function () { + let map, target; + beforeEach(function () { + target = document.createElement('div'); + target.style.width = '200px'; + target.style.height = '150px'; + document.body.appendChild(target); + map = new Map({ + target: target, + }); + }); + afterEach(function () { + map.setTarget(null); + document.body.removeChild(target); + }); + it('same as getViewportSize_ when no padding is set', function () { + const size = map.getView().getViewportSizeMinusPadding_(); + expect(size).to.eql(map.getView().getViewportSize_()); + }); + it('correctly updates when the padding is changed', function () { + map.getView().padding = [1, 2, 3, 4]; + const size = map.getView().getViewportSizeMinusPadding_(); + expect(size).to.eql([194, 146]); + }); + }); + describe('fit', function () { const originalRequestAnimationFrame = window.requestAnimationFrame; const originalCancelAnimationFrame = window.cancelAnimationFrame; @@ -1776,6 +1802,14 @@ describe('ol.View', function () { expect(view.getCenter()[0]).to.be(1500); expect(view.getCenter()[1]).to.be(1500); }); + it('fits correctly to the extent when a padding is configured', function () { + view.padding = [100, 0, 0, 100]; + view.setViewportSize([200, 200]); + view.fit([1000, 1000, 2000, 2000]); + expect(view.getResolution()).to.be(10); + expect(view.getCenter()[0]).to.be(1500); + expect(view.getCenter()[1]).to.be(1500); + }); it('fits correctly to the extent when a view extent is configured', function () { view.options_.extent = [1500, 0, 2500, 10000]; view.applyOptions_(view.options_); @@ -2157,6 +2191,23 @@ describe('ol.View', function () { expect(center[1]).to.roughlyEqual(0, 1e-10); }); }); + + describe('#getState', function () { + let view; + beforeEach(function () { + view = new View({ + center: [0, 0], + resolutions: [1], + zoom: 0, + }); + view.setViewportSize([100, 100]); + }); + it('Correctly shifts the viewport center when a padding is set', function () { + view.padding = [50, 0, 0, 50]; + const state = view.getState(); + expect(state.center).to.eql([-25, 25]); + }); + }); }); describe('does not start unexpected animations during interaction', function () {