From 1f379a06a4671b8aaf21ec6985240d9bac52387e Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 14 Jan 2019 13:36:20 +0100 Subject: [PATCH] View / add support for viewport extent constraint This introduces a breaking change: The `extent` view option now constrains the whole viewport and not just the view center. The option `constrainOnlyCenter` was added to keep the previous behaviour. Constraining the whole viewport and not only the view center means that the center and resolution constraints must be applied with a knowledge of the viewport size. --- examples/extent-constrained.html | 9 ++++ examples/extent-constrained.js | 25 +++++++++ src/ol/View.js | 53 ++++++++++++++++---- src/ol/centerconstraint.js | 19 ++++--- src/ol/resolutionconstraint.js | 43 ++++++++++++---- src/ol/rotationconstraint.js | 3 +- test/spec/ol/interaction/interaction.test.js | 1 + test/spec/ol/view.test.js | 3 +- 8 files changed, 128 insertions(+), 28 deletions(-) create mode 100644 examples/extent-constrained.html create mode 100644 examples/extent-constrained.js diff --git a/examples/extent-constrained.html b/examples/extent-constrained.html new file mode 100644 index 0000000000..7c228d6c1b --- /dev/null +++ b/examples/extent-constrained.html @@ -0,0 +1,9 @@ +--- +layout: example.html +title: Constrained Extent +shortdesc: Example of a view with a constrained extent. +docs: > + This map has a view that is constrained in an extent. This is done using the `extent` view option. Please note that specifying `restrictOnlyCenter: true` would only apply the extent restriction to the view center. +tags: "view, extent, constrain, restrict" +--- +
diff --git a/examples/extent-constrained.js b/examples/extent-constrained.js new file mode 100644 index 0000000000..8fc6e68c77 --- /dev/null +++ b/examples/extent-constrained.js @@ -0,0 +1,25 @@ +import Map from '../src/ol/Map.js'; +import View from '../src/ol/View.js'; +import TileLayer from '../src/ol/layer/Tile.js'; +import OSM from '../src/ol/source/OSM.js'; +import {defaults as defaultControls} from '../src/ol/control/util'; +import ZoomSlider from '../src/ol/control/ZoomSlider'; + +const view = new View({ + center: [328627.563458, 5921296.662223], + zoom: 8, + extent: [-572513.341856, 5211017.966314, + 916327.095083, 6636950.728974] +}); + +new Map({ + layers: [ + new TileLayer({ + source: new OSM() + }) + ], + keyboardEventTarget: document, + target: 'map', + view: view, + controls: defaultControls().extend([new ZoomSlider()]) +}); diff --git a/src/ol/View.js b/src/ol/View.js index ee0cbb1eb4..4c03566bdc 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -92,7 +92,9 @@ import Units from './proj/Units.js'; * used. The `constrainRotation` option has no effect if `enableRotation` is * `false`. * @property {import("./extent.js").Extent} [extent] The extent that constrains the - * center, in other words, center cannot be set outside this extent. + * view, in other words, nothing outside of this extent can be visible on the map + * @property {boolean} [constrainOnlyCenter] If true, the extent + * constraint will only apply to the center and not the whole view. * @property {number} [maxResolution] The maximum resolution used to determine * the resolution constraint. It is used together with `minResolution` (or * `maxZoom`) and `zoomFactor`. If unspecified it is calculated in such a way @@ -473,8 +475,7 @@ class View extends BaseObject { if (options.zoom !== undefined) { animation.sourceResolution = resolution; - animation.targetResolution = this.constrainResolution( - this.maxResolution_, options.zoom - this.minZoom_, 0); + animation.targetResolution = this.getResolutionForZoom(options.zoom); resolution = animation.targetResolution; } else if (options.resolution) { animation.sourceResolution = resolution; @@ -675,7 +676,15 @@ class View extends BaseObject { constrainResolution(resolution, opt_delta, opt_direction) { const delta = opt_delta || 0; const direction = opt_direction || 0; - return this.constraints_.resolution(resolution, delta, direction); + + const size = this.getSizeFromViewport_(); + const rotation = this.getRotation() || 0; + const rotatedSize = [ + Math.abs(size[0] * Math.cos(rotation)) + Math.abs(size[1] * Math.sin(rotation)), + Math.abs(size[0] * Math.sin(rotation)) + Math.abs(size[1] * Math.cos(rotation)) + ]; + + return this.constraints_.resolution(resolution, delta, direction, rotatedSize); } /** @@ -1167,15 +1176,28 @@ class View extends BaseObject { /** * Recompute rotation/resolution/center based on target values. + * Note: we have to compute rotation first, then resolution and center considering that + * parameters can influence one another in case a view extent constraint is present. * @param {boolean=} opt_doNotCancelAnims Do not cancel animations. * @param {boolean=} opt_forceMoving Apply constraints as if the view is moving. * @private */ applyParameters_(opt_doNotCancelAnims, opt_forceMoving) { const isMoving = this.getAnimating() || this.getInteracting() || opt_forceMoving; + + // compute rotation const newRotation = this.constraints_.rotation(this.targetRotation_, 0, isMoving); - const newResolution = this.constraints_.resolution(this.targetResolution_, 0, 0, isMoving); - const newCenter = this.constraints_.center(this.targetCenter_, isMoving); + + // compute viewport size with rotation + const size = this.getSizeFromViewport_(); + const rotation = this.getRotation() || 0; + const rotatedSize = [ + Math.abs(size[0] * Math.cos(rotation)) + Math.abs(size[1] * Math.sin(rotation)), + Math.abs(size[0] * Math.sin(rotation)) + Math.abs(size[1] * Math.cos(rotation)) + ]; + + const newResolution = this.constraints_.resolution(this.targetResolution_, 0, 0, rotatedSize, isMoving); + const newCenter = this.constraints_.center(this.targetCenter_, newResolution, rotatedSize, isMoving); this.set(ViewProperty.ROTATION, newRotation); this.set(ViewProperty.RESOLUTION, newResolution); @@ -1215,8 +1237,16 @@ class View extends BaseObject { const direction = opt_direction || 0; const currentRes = this.getResolution(); const currentZoom = this.getZoom(); + + const size = this.getSizeFromViewport_(); + const rotation = this.getRotation() || 0; + const rotatedSize = [ + Math.abs(size[0] * Math.cos(rotation)) + Math.abs(size[1] * Math.sin(rotation)), + Math.abs(size[0] * Math.sin(rotation)) + Math.abs(size[1] * Math.cos(rotation)) + ]; + return this.getZoomForResolution( - this.constraints_.resolution(currentRes, targetZoom - currentZoom, direction)); + this.constraints_.resolution(currentRes, targetZoom - currentZoom, direction, rotatedSize)); } } @@ -1238,7 +1268,7 @@ function animationCallback(callback, returnValue) { */ export function createCenterConstraint(options) { if (options.extent !== undefined) { - return createExtent(options.extent); + return createExtent(options.extent, options.constrainOnlyCenter); } else { return centerNone; } @@ -1274,8 +1304,8 @@ export function createResolutionConstraint(options) { maxResolution = resolutions[minZoom]; minResolution = resolutions[maxZoom] !== undefined ? resolutions[maxZoom] : resolutions[resolutions.length - 1]; - resolutionConstraint = createSnapToResolutions( - resolutions); + resolutionConstraint = createSnapToResolutions(resolutions, + !options.constrainOnlyCenter && options.extent); } else { // calculate the default min and max resolution const projection = createProjection(options.projection, 'EPSG:3857'); @@ -1320,7 +1350,8 @@ export function createResolutionConstraint(options) { minResolution = maxResolution / Math.pow(zoomFactor, maxZoom - minZoom); resolutionConstraint = createSnapToPower( - zoomFactor, maxResolution, maxZoom - minZoom); + zoomFactor, maxResolution, maxZoom - minZoom, + !options.constrainOnlyCenter && options.extent); } return {constraint: resolutionConstraint, maxResolution: maxResolution, minResolution: minResolution, minZoom: minZoom, zoomFactor: zoomFactor}; diff --git a/src/ol/centerconstraint.js b/src/ol/centerconstraint.js index 4c4fcfe23f..eda8973ddd 100644 --- a/src/ol/centerconstraint.js +++ b/src/ol/centerconstraint.js @@ -5,25 +5,32 @@ import {clamp} from './math.js'; /** - * @typedef {function((import("./coordinate.js").Coordinate|undefined)): (import("./coordinate.js").Coordinate|undefined)} Type + * @typedef {function((import("./coordinate.js").Coordinate|undefined), number, import("./size.js").Size, boolean=): (import("./coordinate.js").Coordinate|undefined)} Type */ /** * @param {import("./extent.js").Extent} extent Extent. + * @param {boolean} onlyCenter If true, the constraint will only apply to the view center. * @return {Type} The constraint. */ -export function createExtent(extent) { +export function createExtent(extent, onlyCenter) { return ( /** - * @param {import("./coordinate.js").Coordinate=} center Center. + * @param {import("./coordinate.js").Coordinate|undefined} center Center. + * @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. * @return {import("./coordinate.js").Coordinate|undefined} Center. */ - function(center, opt_isMoving) { + function(center, resolution, size, opt_isMoving) { if (center) { + let viewWidth = onlyCenter ? 0 : size[0] * resolution; + let viewHeight = onlyCenter ? 0 : size[1] * resolution; + return [ - clamp(center[0], extent[0], extent[2]), - clamp(center[1], extent[1], extent[3]) + clamp(center[0], extent[0] + viewWidth / 2, extent[2] - viewWidth / 2), + clamp(center[1], extent[1] + viewHeight / 2, extent[3] - viewHeight / 2) ]; } else { return undefined; diff --git a/src/ol/resolutionconstraint.js b/src/ol/resolutionconstraint.js index 9a7d713888..d007b81dc9 100644 --- a/src/ol/resolutionconstraint.js +++ b/src/ol/resolutionconstraint.js @@ -3,34 +3,47 @@ */ import {linearFindNearest} from './array.js'; import {clamp} from './math.js'; +import {getHeight, getWidth} from './extent'; /** - * @typedef {function((number|undefined), number, number): (number|undefined)} Type + * @typedef {function((number|undefined), number, number, import("./size.js").Size, boolean=): (number|undefined)} Type */ /** * @param {Array} resolutions Resolutions. + * @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent. * @return {Type} Zoom function. */ -export function createSnapToResolutions(resolutions) { +export function createSnapToResolutions(resolutions, opt_maxExtent) { return ( /** * @param {number|undefined} resolution Resolution. * @param {number} delta Delta. * @param {number} direction Direction. + * @param {import("./size.js").Size} size Viewport size. + * @param {boolean=} opt_isMoving True if an interaction or animation is in progress. * @return {number|undefined} Resolution. */ - function(resolution, delta, direction, opt_isMoving) { + function(resolution, delta, direction, size, opt_isMoving) { if (resolution !== undefined) { + let cappedRes = resolution; + + // apply constraint related to max extent + if (opt_maxExtent) { + const xResolution = getWidth(opt_maxExtent) / size[0]; + const yResolution = getHeight(opt_maxExtent) / size[1]; + cappedRes = Math.min(cappedRes, Math.min(xResolution, yResolution)); + } + // during interacting or animating, allow intermediary values if (opt_isMoving) { // TODO: actually take delta and direction into account - return resolution; + return Math.min(resolution, cappedRes); } - let z = linearFindNearest(resolutions, resolution, direction); + let z = linearFindNearest(resolutions, cappedRes, direction); z = clamp(z + delta, 0, resolutions.length - 1); const index = Math.floor(z); if (z != index && index < resolutions.length - 1) { @@ -51,27 +64,39 @@ export function createSnapToResolutions(resolutions) { * @param {number} power Power. * @param {number} maxResolution Maximum resolution. * @param {number=} opt_maxLevel Maximum level. + * @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent. * @return {Type} Zoom function. */ -export function createSnapToPower(power, maxResolution, opt_maxLevel) { +export function createSnapToPower(power, maxResolution, opt_maxLevel, opt_maxExtent) { return ( /** * @param {number|undefined} resolution Resolution. * @param {number} delta Delta. * @param {number} direction Direction. + * @param {import("./size.js").Size} size Viewport size. + * @param {boolean=} opt_isMoving True if an interaction or animation is in progress. * @return {number|undefined} Resolution. */ - function(resolution, delta, direction, opt_isMoving) { + function(resolution, delta, direction, size, opt_isMoving) { if (resolution !== undefined) { + let cappedRes = resolution; + + // apply constraint related to max extent + if (opt_maxExtent) { + const xResolution = getWidth(opt_maxExtent) / size[0]; + const yResolution = getHeight(opt_maxExtent) / size[1]; + cappedRes = Math.min(cappedRes, Math.min(xResolution, yResolution)); + } + // during interacting or animating, allow intermediary values if (opt_isMoving) { // TODO: actually take delta and direction into account - return resolution; + return Math.min(resolution, cappedRes); } const offset = -direction / 2 + 0.5; const oldLevel = Math.floor( - Math.log(maxResolution / resolution) / Math.log(power) + offset); + Math.log(maxResolution / cappedRes) / Math.log(power) + offset); let newLevel = Math.max(oldLevel + delta, 0); if (opt_maxLevel !== undefined) { newLevel = Math.min(newLevel, opt_maxLevel); diff --git a/src/ol/rotationconstraint.js b/src/ol/rotationconstraint.js index 5183f28572..1efcc7f734 100644 --- a/src/ol/rotationconstraint.js +++ b/src/ol/rotationconstraint.js @@ -5,7 +5,7 @@ import {toRadians} from './math.js'; /** - * @typedef {function((number|undefined), number): (number|undefined)} Type + * @typedef {function((number|undefined), number, boolean=): (number|undefined)} Type */ @@ -47,6 +47,7 @@ export function createSnapToN(n) { /** * @param {number|undefined} rotation Rotation. * @param {number} delta Delta. + * @param {boolean=} opt_isMoving True if an interaction or animation is in progress. * @return {number|undefined} Rotation. */ function(rotation, delta, opt_isMoving) { diff --git a/test/spec/ol/interaction/interaction.test.js b/test/spec/ol/interaction/interaction.test.js index 000a575eda..3d2d04e91a 100644 --- a/test/spec/ol/interaction/interaction.test.js +++ b/test/spec/ol/interaction/interaction.test.js @@ -132,6 +132,7 @@ describe('ol.interaction.Interaction', function() { const view = new View({ center: [0, 0], extent: [-2.5, -2.5, 2.5, 2.5], + constrainOnlyCenter: true, resolution: 1, resolutions: [4, 2, 1, 0.5, 0.25] }); diff --git a/test/spec/ol/view.test.js b/test/spec/ol/view.test.js index 453ca5d623..88b18e4718 100644 --- a/test/spec/ol/view.test.js +++ b/test/spec/ol/view.test.js @@ -45,7 +45,8 @@ describe('ol.View', function() { describe('with extent option', function() { it('gives a correct center constraint function', function() { const options = { - extent: [0, 0, 1, 1] + extent: [0, 0, 1, 1], + constrainOnlyCenter: true }; const fn = createCenterConstraint(options); expect(fn([0, 0])).to.eql([0, 0]);