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

View File

@@ -11,6 +11,11 @@ docs: >
view's <code>centerOn</code> method is used to position a coordinate (Lausanne)
at a specific pixel location (the center of the black box).
<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"
---
<div class="mapcontainer">

54
examples/view-padding.css Normal file
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);
}

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
examples/view-padding.js Normal file
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
);

View File

@@ -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<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;
/**
* 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
* @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<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_,
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;

View File

@@ -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<number>=): (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<number>=} 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

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 () {
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 () {