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..a870152ac3 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,14 +1113,43 @@ class View extends BaseObject {
);
}
+ /**
+ * Returns the size of the viewport minus padding.
+ * @private
+ * @param {number=} opt_rotation Take into account the rotation of the viewport when giving the size
+ * @return {import("./size.js").Size} Viewport size reduced by the padding.
+ */
+ getViewportSizeMinusPadding_(opt_rotation) {
+ let size = this.getViewportSize_(opt_rotation);
+ 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());
const projection = this.getProjection();
const resolution = /** @type {number} */ (this.getResolution());
const rotation = this.getRotation();
+ let center = /** @type {import("./coordinate.js").Coordinate} */ (this.getCenterInternal());
+ const padding = this.padding;
+ if (padding) {
+ const reducedSize = this.getViewportSizeMinusPadding_();
+ center = calculateCenterOn(
+ center,
+ this.getViewportSize_(),
+ [reducedSize[0] / 2 + padding[3], reducedSize[1] / 2 + padding[0]],
+ resolution,
+ rotation
+ );
+ }
return {
center: center.slice(0),
projection: projection !== undefined ? projection : null,
@@ -1196,8 +1240,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 +1270,7 @@ class View extends BaseObject {
}
}
- this.fitInternal(geometry, options);
+ this.fitInternal(geometry, opt_options);
}
/**
@@ -1239,7 +1281,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,22 +1374,43 @@ class View extends BaseObject {
* @param {import("./pixel.js").Pixel} position Position on the view to center on.
*/
centerOnInternal(coordinate, size, position) {
- // calculate rotated position
- const rotation = this.getRotation();
- const cosAngle = Math.cos(-rotation);
- let sinAngle = Math.sin(-rotation);
- let rotX = coordinate[0] * cosAngle - coordinate[1] * sinAngle;
- let rotY = coordinate[1] * cosAngle + coordinate[0] * sinAngle;
- const resolution = this.getResolution();
- rotX += (size[0] / 2 - position[0]) * resolution;
- rotY += (position[1] - size[1] / 2) * resolution;
+ this.setCenterInternal(
+ calculateCenterOn(
+ coordinate,
+ size,
+ position,
+ this.getResolution(),
+ this.getRotation()
+ )
+ );
+ }
- // go back to original angle
- sinAngle = -sinAngle; // go back to original rotation
- const centerX = rotX * cosAngle - rotY * sinAngle;
- const centerY = rotY * cosAngle + rotX * sinAngle;
-
- this.setCenterInternal([centerX, centerY]);
+ /**
+ * Calculates the shift between map and viewport center.
+ * @param {import("./coordinate.js").Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {import("./size.js").Size} size Size.
+ * @return {Array|undefined} Center shift.
+ */
+ calculateCenterShift(center, resolution, rotation, size) {
+ let centerShift;
+ const padding = this.padding;
+ if (padding && center) {
+ const reducedSize = this.getViewportSizeMinusPadding_(-rotation);
+ const shiftedCenter = calculateCenterOn(
+ center,
+ size,
+ [reducedSize[0] / 2 + padding[3], reducedSize[1] / 2 + padding[0]],
+ resolution,
+ rotation
+ );
+ centerShift = [
+ center[0] - shiftedCenter[0],
+ center[1] - shiftedCenter[1],
+ ];
+ }
+ return centerShift;
}
/**
@@ -1550,7 +1613,13 @@ class View extends BaseObject {
this.targetCenter_,
newResolution,
size,
- isMoving
+ isMoving,
+ this.calculateCenterShift(
+ this.targetCenter_,
+ newResolution,
+ newRotation,
+ size
+ )
);
if (this.get(ViewProperty.ROTATION) !== newRotation) {
@@ -1595,7 +1664,14 @@ class View extends BaseObject {
const newCenter = this.constraints_.center(
this.targetCenter_,
newResolution,
- size
+ size,
+ false,
+ this.calculateCenterShift(
+ this.targetCenter_,
+ newResolution,
+ newRotation,
+ size
+ )
);
if (duration === 0 && !this.cancelAnchor_) {
@@ -1936,4 +2012,29 @@ export function isNoopAnimation(animation) {
return true;
}
+/**
+ * @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.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @return {import("./coordinate.js").Coordinate} Shifted center.
+ */
+function calculateCenterOn(coordinate, size, position, resolution, rotation) {
+ // calculate rotated position
+ const cosAngle = Math.cos(-rotation);
+ let sinAngle = Math.sin(-rotation);
+ let rotX = coordinate[0] * cosAngle - coordinate[1] * sinAngle;
+ let rotY = coordinate[1] * cosAngle + coordinate[0] * sinAngle;
+ 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
+ const centerX = rotX * cosAngle - rotY * sinAngle;
+ const centerY = rotY * cosAngle + rotX * sinAngle;
+
+ return [centerX, centerY];
+}
+
export default View;
diff --git a/src/ol/centerconstraint.js b/src/ol/centerconstraint.js
index b024106275..1131fc9c27 100644
--- a/src/ol/centerconstraint.js
+++ b/src/ol/centerconstraint.js
@@ -4,7 +4,7 @@
import {clamp} from './math.js';
/**
- * @typedef {function((import("./coordinate.js").Coordinate|undefined), number, import("./size.js").Size, boolean=): (import("./coordinate.js").Coordinate|undefined)} Type
+ * @typedef {function((import("./coordinate.js").Coordinate|undefined), number, import("./size.js").Size, boolean=, Array=): (import("./coordinate.js").Coordinate|undefined)} Type
*/
/**
@@ -21,16 +21,19 @@ export function createExtent(extent, onlyCenter, smooth) {
* @param {number} resolution Resolution.
* @param {import("./size.js").Size} size Viewport size; unused if `onlyCenter` was specified.
* @param {boolean=} opt_isMoving True if an interaction or animation is in progress.
+ * @param {Array=} opt_centerShift Shift between map center and viewport center.
* @return {import("./coordinate.js").Coordinate|undefined} Center.
*/
- function (center, resolution, size, opt_isMoving) {
+ function (center, resolution, size, opt_isMoving, opt_centerShift) {
if (center) {
const viewWidth = onlyCenter ? 0 : size[0] * resolution;
const viewHeight = onlyCenter ? 0 : size[1] * resolution;
- let minX = extent[0] + viewWidth / 2;
- let maxX = extent[2] - viewWidth / 2;
- let minY = extent[1] + viewHeight / 2;
- let maxY = extent[3] - viewHeight / 2;
+ const shiftX = opt_centerShift ? opt_centerShift[0] : 0;
+ const shiftY = opt_centerShift ? opt_centerShift[1] : 0;
+ let minX = extent[0] + viewWidth / 2 + shiftX;
+ let maxX = extent[2] - viewWidth / 2 + shiftX;
+ let minY = extent[1] + viewHeight / 2 + shiftY;
+ let maxY = extent[3] - viewHeight / 2 + shiftY;
// note: when zooming out of bounds, min and max values for x and y may
// end up inverted (min > max); this has to be accounted for
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 () {