Merge pull request #11755 from ahocevar/view-padding

Add padding option for View
This commit is contained in:
Andreas Hocevar
2020-11-24 09:10:57 +01:00
committed by GitHub
7 changed files with 340 additions and 30 deletions
+5
View File
@@ -11,6 +11,11 @@ docs: >
view's <code>centerOn</code> method is used to position a coordinate (Lausanne) view's <code>centerOn</code> method is used to position a coordinate (Lausanne)
at a specific pixel location (the center of the black box). at a specific pixel location (the center of the black box).
<p>Use <code>Alt+Shift+Drag</code> to rotate the map.</p> <p>Use <code>Alt+Shift+Drag</code> to rotate the map.</p>
<p><b>Note:</b> 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 <a href="view-padding.html">view-padding.html</a> example.
</p>
tags: "center, rotation, openstreetmap" tags: "center, rotation, openstreetmap"
--- ---
<div class="mapcontainer"> <div class="mapcontainer">
+54
View File
@@ -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);
}
+23
View File
@@ -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.
<p>Use <code>Alt+Shift+Drag</code> to rotate the map.</p>
tags: "center, padding, view, shift"
---
<div class="mapcontainer">
<div id="map" class="map"></div>
<div class="padding-top"></div>
<div class="padding-left"></div>
<div class="padding-right"></div>
<div class="padding-bottom"></div>
</div>
<button id="zoomtoswitzerland">Zoom to Switzerland</button>
<button id="centerlausanne">Center on Lausanne</button>
+73
View File
@@ -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<import("../src/ol/geom/SimpleGeometry.js").default>} */
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
);
+125 -24
View File
@@ -175,6 +175,10 @@ import {fromExtent as polygonFromExtent} from './geom/Polygon.js';
* level used to calculate the initial resolution for the view. * level used to calculate the initial resolution for the view.
* @property {number} [zoomFactor=2] The zoom factor used to compute the * @property {number} [zoomFactor=2] The zoom factor used to compute the
* corresponding resolution. * corresponding resolution.
* @property {!Array<number>} [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; 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<number>|undefined}
* @api
*/
this.padding = options.padding;
/** /**
* @private * @private
* @type {number} * @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 * @param {import("./size.js").Size=} opt_size Box pixel size. If not provided,
* first map that uses this view will be used. * the map's last known viewport size will be used.
* @return {import("./extent.js").Extent} Extent. * @return {import("./extent.js").Extent} Extent.
*/ */
calculateExtentInternal(opt_size) { 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. * @return {State} View state.
*/ */
getState() { getState() {
const center = /** @type {import("./coordinate.js").Coordinate} */ (this.getCenterInternal());
const projection = this.getProjection(); const projection = this.getProjection();
const resolution = /** @type {number} */ (this.getResolution()); const resolution = /** @type {number} */ (this.getResolution());
const rotation = this.getRotation(); 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 { return {
center: center.slice(0), center: center.slice(0),
projection: projection !== undefined ? projection : null, projection: projection !== undefined ? projection : null,
@@ -1196,8 +1240,6 @@ class View extends BaseObject {
* @api * @api
*/ */
fit(geometryOrExtent, opt_options) { fit(geometryOrExtent, opt_options) {
const options = assign({size: this.getViewportSize_()}, opt_options || {});
/** @type {import("./geom/SimpleGeometry.js").default} */ /** @type {import("./geom/SimpleGeometry.js").default} */
let geometry; let geometry;
assert( 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 || {}; const options = opt_options || {};
let size = options.size; let size = options.size;
if (!size) { if (!size) {
size = this.getViewportSize_(); size = this.getViewportSizeMinusPadding_();
} }
const padding = const padding =
options.padding !== undefined ? options.padding : [0, 0, 0, 0]; 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. * @param {import("./pixel.js").Pixel} position Position on the view to center on.
*/ */
centerOnInternal(coordinate, size, position) { centerOnInternal(coordinate, size, position) {
// calculate rotated position this.setCenterInternal(
const rotation = this.getRotation(); calculateCenterOn(
const cosAngle = Math.cos(-rotation); coordinate,
let sinAngle = Math.sin(-rotation); size,
let rotX = coordinate[0] * cosAngle - coordinate[1] * sinAngle; position,
let rotY = coordinate[1] * cosAngle + coordinate[0] * sinAngle; this.getResolution(),
const resolution = this.getResolution(); this.getRotation()
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 * Calculates the shift between map and viewport center.
const centerX = rotX * cosAngle - rotY * sinAngle; * @param {import("./coordinate.js").Coordinate} center Center.
const centerY = rotY * cosAngle + rotX * sinAngle; * @param {number} resolution Resolution.
* @param {number} rotation Rotation.
this.setCenterInternal([centerX, centerY]); * @param {import("./size.js").Size} size Size.
* @return {Array<number>|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_, this.targetCenter_,
newResolution, newResolution,
size, size,
isMoving isMoving,
this.calculateCenterShift(
this.targetCenter_,
newResolution,
newRotation,
size
)
); );
if (this.get(ViewProperty.ROTATION) !== newRotation) { if (this.get(ViewProperty.ROTATION) !== newRotation) {
@@ -1595,7 +1664,14 @@ class View extends BaseObject {
const newCenter = this.constraints_.center( const newCenter = this.constraints_.center(
this.targetCenter_, this.targetCenter_,
newResolution, newResolution,
size size,
false,
this.calculateCenterShift(
this.targetCenter_,
newResolution,
newRotation,
size
)
); );
if (duration === 0 && !this.cancelAnchor_) { if (duration === 0 && !this.cancelAnchor_) {
@@ -1936,4 +2012,29 @@ export function isNoopAnimation(animation) {
return true; 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; export default View;
+9 -6
View File
@@ -4,7 +4,7 @@
import {clamp} from './math.js'; 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<number>=): (import("./coordinate.js").Coordinate|undefined)} Type
*/ */
/** /**
@@ -21,16 +21,19 @@ export function createExtent(extent, onlyCenter, smooth) {
* @param {number} resolution Resolution. * @param {number} resolution Resolution.
* @param {import("./size.js").Size} size Viewport size; unused if `onlyCenter` was specified. * @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 {boolean=} opt_isMoving True if an interaction or animation is in progress.
* @param {Array<number>=} opt_centerShift Shift between map center and viewport center.
* @return {import("./coordinate.js").Coordinate|undefined} Center. * @return {import("./coordinate.js").Coordinate|undefined} Center.
*/ */
function (center, resolution, size, opt_isMoving) { function (center, resolution, size, opt_isMoving, opt_centerShift) {
if (center) { if (center) {
const viewWidth = onlyCenter ? 0 : size[0] * resolution; const viewWidth = onlyCenter ? 0 : size[0] * resolution;
const viewHeight = onlyCenter ? 0 : size[1] * resolution; const viewHeight = onlyCenter ? 0 : size[1] * resolution;
let minX = extent[0] + viewWidth / 2; const shiftX = opt_centerShift ? opt_centerShift[0] : 0;
let maxX = extent[2] - viewWidth / 2; const shiftY = opt_centerShift ? opt_centerShift[1] : 0;
let minY = extent[1] + viewHeight / 2; let minX = extent[0] + viewWidth / 2 + shiftX;
let maxY = extent[3] - viewHeight / 2; 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 // 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 // end up inverted (min > max); this has to be accounted for
+51
View File
@@ -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 () { describe('fit', function () {
const originalRequestAnimationFrame = window.requestAnimationFrame; const originalRequestAnimationFrame = window.requestAnimationFrame;
const originalCancelAnimationFrame = window.cancelAnimationFrame; const originalCancelAnimationFrame = window.cancelAnimationFrame;
@@ -1776,6 +1802,14 @@ describe('ol.View', function () {
expect(view.getCenter()[0]).to.be(1500); expect(view.getCenter()[0]).to.be(1500);
expect(view.getCenter()[1]).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 () { it('fits correctly to the extent when a view extent is configured', function () {
view.options_.extent = [1500, 0, 2500, 10000]; view.options_.extent = [1500, 0, 2500, 10000];
view.applyOptions_(view.options_); view.applyOptions_(view.options_);
@@ -2157,6 +2191,23 @@ describe('ol.View', function () {
expect(center[1]).to.roughlyEqual(0, 1e-10); 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 () { describe('does not start unexpected animations during interaction', function () {