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