Add padding option for View

This commit is contained in:
Andreas Hocevar
2020-11-22 10:18:48 +01:00
parent 9d255be87c
commit 0e2d17b9c6
6 changed files with 264 additions and 8 deletions

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
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. * 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,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. * @return {State} View state.
*/ */
getState() { 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 projection = this.getProjection();
const resolution = /** @type {number} */ (this.getResolution()); const resolution = /** @type {number} */ (this.getResolution());
const rotation = this.getRotation(); const rotation = this.getRotation();
@@ -1196,8 +1236,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 +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 || {}; 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,6 +1370,18 @@ 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) {
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 // calculate rotated position
const rotation = this.getRotation(); const rotation = this.getRotation();
const cosAngle = Math.cos(-rotation); const cosAngle = Math.cos(-rotation);
@@ -1347,7 +1397,7 @@ class View extends BaseObject {
const centerX = rotX * cosAngle - rotY * sinAngle; const centerX = rotX * cosAngle - rotY * sinAngle;
const centerY = rotY * cosAngle + rotX * sinAngle; const centerY = rotY * cosAngle + rotX * sinAngle;
this.setCenterInternal([centerX, centerY]); return [centerX, centerY];
} }
/** /**

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