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.
This commit is contained in:
@@ -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"
|
||||||
|
---
|
||||||
|
<div id="map" class="map"></div>
|
||||||
@@ -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()])
|
||||||
|
});
|
||||||
+42
-11
@@ -92,7 +92,9 @@ import Units from './proj/Units.js';
|
|||||||
* used. The `constrainRotation` option has no effect if `enableRotation` is
|
* used. The `constrainRotation` option has no effect if `enableRotation` is
|
||||||
* `false`.
|
* `false`.
|
||||||
* @property {import("./extent.js").Extent} [extent] The extent that constrains the
|
* @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
|
* @property {number} [maxResolution] The maximum resolution used to determine
|
||||||
* the resolution constraint. It is used together with `minResolution` (or
|
* the resolution constraint. It is used together with `minResolution` (or
|
||||||
* `maxZoom`) and `zoomFactor`. If unspecified it is calculated in such a way
|
* `maxZoom`) and `zoomFactor`. If unspecified it is calculated in such a way
|
||||||
@@ -473,8 +475,7 @@ class View extends BaseObject {
|
|||||||
|
|
||||||
if (options.zoom !== undefined) {
|
if (options.zoom !== undefined) {
|
||||||
animation.sourceResolution = resolution;
|
animation.sourceResolution = resolution;
|
||||||
animation.targetResolution = this.constrainResolution(
|
animation.targetResolution = this.getResolutionForZoom(options.zoom);
|
||||||
this.maxResolution_, options.zoom - this.minZoom_, 0);
|
|
||||||
resolution = animation.targetResolution;
|
resolution = animation.targetResolution;
|
||||||
} else if (options.resolution) {
|
} else if (options.resolution) {
|
||||||
animation.sourceResolution = resolution;
|
animation.sourceResolution = resolution;
|
||||||
@@ -675,7 +676,15 @@ class View extends BaseObject {
|
|||||||
constrainResolution(resolution, opt_delta, opt_direction) {
|
constrainResolution(resolution, opt_delta, opt_direction) {
|
||||||
const delta = opt_delta || 0;
|
const delta = opt_delta || 0;
|
||||||
const direction = opt_direction || 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.
|
* 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_doNotCancelAnims Do not cancel animations.
|
||||||
* @param {boolean=} opt_forceMoving Apply constraints as if the view is moving.
|
* @param {boolean=} opt_forceMoving Apply constraints as if the view is moving.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
applyParameters_(opt_doNotCancelAnims, opt_forceMoving) {
|
applyParameters_(opt_doNotCancelAnims, opt_forceMoving) {
|
||||||
const isMoving = this.getAnimating() || this.getInteracting() || opt_forceMoving;
|
const isMoving = this.getAnimating() || this.getInteracting() || opt_forceMoving;
|
||||||
|
|
||||||
|
// compute rotation
|
||||||
const newRotation = this.constraints_.rotation(this.targetRotation_, 0, isMoving);
|
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.ROTATION, newRotation);
|
||||||
this.set(ViewProperty.RESOLUTION, newResolution);
|
this.set(ViewProperty.RESOLUTION, newResolution);
|
||||||
@@ -1215,8 +1237,16 @@ class View extends BaseObject {
|
|||||||
const direction = opt_direction || 0;
|
const direction = opt_direction || 0;
|
||||||
const currentRes = this.getResolution();
|
const currentRes = this.getResolution();
|
||||||
const currentZoom = this.getZoom();
|
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(
|
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) {
|
export function createCenterConstraint(options) {
|
||||||
if (options.extent !== undefined) {
|
if (options.extent !== undefined) {
|
||||||
return createExtent(options.extent);
|
return createExtent(options.extent, options.constrainOnlyCenter);
|
||||||
} else {
|
} else {
|
||||||
return centerNone;
|
return centerNone;
|
||||||
}
|
}
|
||||||
@@ -1274,8 +1304,8 @@ export function createResolutionConstraint(options) {
|
|||||||
maxResolution = resolutions[minZoom];
|
maxResolution = resolutions[minZoom];
|
||||||
minResolution = resolutions[maxZoom] !== undefined ?
|
minResolution = resolutions[maxZoom] !== undefined ?
|
||||||
resolutions[maxZoom] : resolutions[resolutions.length - 1];
|
resolutions[maxZoom] : resolutions[resolutions.length - 1];
|
||||||
resolutionConstraint = createSnapToResolutions(
|
resolutionConstraint = createSnapToResolutions(resolutions,
|
||||||
resolutions);
|
!options.constrainOnlyCenter && options.extent);
|
||||||
} else {
|
} else {
|
||||||
// calculate the default min and max resolution
|
// calculate the default min and max resolution
|
||||||
const projection = createProjection(options.projection, 'EPSG:3857');
|
const projection = createProjection(options.projection, 'EPSG:3857');
|
||||||
@@ -1320,7 +1350,8 @@ export function createResolutionConstraint(options) {
|
|||||||
minResolution = maxResolution / Math.pow(zoomFactor, maxZoom - minZoom);
|
minResolution = maxResolution / Math.pow(zoomFactor, maxZoom - minZoom);
|
||||||
|
|
||||||
resolutionConstraint = createSnapToPower(
|
resolutionConstraint = createSnapToPower(
|
||||||
zoomFactor, maxResolution, maxZoom - minZoom);
|
zoomFactor, maxResolution, maxZoom - minZoom,
|
||||||
|
!options.constrainOnlyCenter && options.extent);
|
||||||
}
|
}
|
||||||
return {constraint: resolutionConstraint, maxResolution: maxResolution,
|
return {constraint: resolutionConstraint, maxResolution: maxResolution,
|
||||||
minResolution: minResolution, minZoom: minZoom, zoomFactor: zoomFactor};
|
minResolution: minResolution, minZoom: minZoom, zoomFactor: zoomFactor};
|
||||||
|
|||||||
@@ -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 {import("./extent.js").Extent} extent Extent.
|
||||||
|
* @param {boolean} onlyCenter If true, the constraint will only apply to the view center.
|
||||||
* @return {Type} The constraint.
|
* @return {Type} The constraint.
|
||||||
*/
|
*/
|
||||||
export function createExtent(extent) {
|
export function createExtent(extent, onlyCenter) {
|
||||||
return (
|
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.
|
* @return {import("./coordinate.js").Coordinate|undefined} Center.
|
||||||
*/
|
*/
|
||||||
function(center, opt_isMoving) {
|
function(center, resolution, size, opt_isMoving) {
|
||||||
if (center) {
|
if (center) {
|
||||||
|
let viewWidth = onlyCenter ? 0 : size[0] * resolution;
|
||||||
|
let viewHeight = onlyCenter ? 0 : size[1] * resolution;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
clamp(center[0], extent[0], extent[2]),
|
clamp(center[0], extent[0] + viewWidth / 2, extent[2] - viewWidth / 2),
|
||||||
clamp(center[1], extent[1], extent[3])
|
clamp(center[1], extent[1] + viewHeight / 2, extent[3] - viewHeight / 2)
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -3,34 +3,47 @@
|
|||||||
*/
|
*/
|
||||||
import {linearFindNearest} from './array.js';
|
import {linearFindNearest} from './array.js';
|
||||||
import {clamp} from './math.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<number>} resolutions Resolutions.
|
* @param {Array<number>} resolutions Resolutions.
|
||||||
|
* @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent.
|
||||||
* @return {Type} Zoom function.
|
* @return {Type} Zoom function.
|
||||||
*/
|
*/
|
||||||
export function createSnapToResolutions(resolutions) {
|
export function createSnapToResolutions(resolutions, opt_maxExtent) {
|
||||||
return (
|
return (
|
||||||
/**
|
/**
|
||||||
* @param {number|undefined} resolution Resolution.
|
* @param {number|undefined} resolution Resolution.
|
||||||
* @param {number} delta Delta.
|
* @param {number} delta Delta.
|
||||||
* @param {number} direction Direction.
|
* @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.
|
* @return {number|undefined} Resolution.
|
||||||
*/
|
*/
|
||||||
function(resolution, delta, direction, opt_isMoving) {
|
function(resolution, delta, direction, size, opt_isMoving) {
|
||||||
if (resolution !== undefined) {
|
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
|
// during interacting or animating, allow intermediary values
|
||||||
if (opt_isMoving) {
|
if (opt_isMoving) {
|
||||||
// TODO: actually take delta and direction into account
|
// 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);
|
z = clamp(z + delta, 0, resolutions.length - 1);
|
||||||
const index = Math.floor(z);
|
const index = Math.floor(z);
|
||||||
if (z != index && index < resolutions.length - 1) {
|
if (z != index && index < resolutions.length - 1) {
|
||||||
@@ -51,27 +64,39 @@ export function createSnapToResolutions(resolutions) {
|
|||||||
* @param {number} power Power.
|
* @param {number} power Power.
|
||||||
* @param {number} maxResolution Maximum resolution.
|
* @param {number} maxResolution Maximum resolution.
|
||||||
* @param {number=} opt_maxLevel Maximum level.
|
* @param {number=} opt_maxLevel Maximum level.
|
||||||
|
* @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent.
|
||||||
* @return {Type} Zoom function.
|
* @return {Type} Zoom function.
|
||||||
*/
|
*/
|
||||||
export function createSnapToPower(power, maxResolution, opt_maxLevel) {
|
export function createSnapToPower(power, maxResolution, opt_maxLevel, opt_maxExtent) {
|
||||||
return (
|
return (
|
||||||
/**
|
/**
|
||||||
* @param {number|undefined} resolution Resolution.
|
* @param {number|undefined} resolution Resolution.
|
||||||
* @param {number} delta Delta.
|
* @param {number} delta Delta.
|
||||||
* @param {number} direction Direction.
|
* @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.
|
* @return {number|undefined} Resolution.
|
||||||
*/
|
*/
|
||||||
function(resolution, delta, direction, opt_isMoving) {
|
function(resolution, delta, direction, size, opt_isMoving) {
|
||||||
if (resolution !== undefined) {
|
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
|
// during interacting or animating, allow intermediary values
|
||||||
if (opt_isMoving) {
|
if (opt_isMoving) {
|
||||||
// TODO: actually take delta and direction into account
|
// TODO: actually take delta and direction into account
|
||||||
return resolution;
|
return Math.min(resolution, cappedRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
const offset = -direction / 2 + 0.5;
|
const offset = -direction / 2 + 0.5;
|
||||||
const oldLevel = Math.floor(
|
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);
|
let newLevel = Math.max(oldLevel + delta, 0);
|
||||||
if (opt_maxLevel !== undefined) {
|
if (opt_maxLevel !== undefined) {
|
||||||
newLevel = Math.min(newLevel, opt_maxLevel);
|
newLevel = Math.min(newLevel, opt_maxLevel);
|
||||||
|
|||||||
@@ -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|undefined} rotation Rotation.
|
||||||
* @param {number} delta Delta.
|
* @param {number} delta Delta.
|
||||||
|
* @param {boolean=} opt_isMoving True if an interaction or animation is in progress.
|
||||||
* @return {number|undefined} Rotation.
|
* @return {number|undefined} Rotation.
|
||||||
*/
|
*/
|
||||||
function(rotation, delta, opt_isMoving) {
|
function(rotation, delta, opt_isMoving) {
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ describe('ol.interaction.Interaction', function() {
|
|||||||
const view = new View({
|
const view = new View({
|
||||||
center: [0, 0],
|
center: [0, 0],
|
||||||
extent: [-2.5, -2.5, 2.5, 2.5],
|
extent: [-2.5, -2.5, 2.5, 2.5],
|
||||||
|
constrainOnlyCenter: true,
|
||||||
resolution: 1,
|
resolution: 1,
|
||||||
resolutions: [4, 2, 1, 0.5, 0.25]
|
resolutions: [4, 2, 1, 0.5, 0.25]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ describe('ol.View', function() {
|
|||||||
describe('with extent option', function() {
|
describe('with extent option', function() {
|
||||||
it('gives a correct center constraint function', function() {
|
it('gives a correct center constraint function', function() {
|
||||||
const options = {
|
const options = {
|
||||||
extent: [0, 0, 1, 1]
|
extent: [0, 0, 1, 1],
|
||||||
|
constrainOnlyCenter: true
|
||||||
};
|
};
|
||||||
const fn = createCenterConstraint(options);
|
const fn = createCenterConstraint(options);
|
||||||
expect(fn([0, 0])).to.eql([0, 0]);
|
expect(fn([0, 0])).to.eql([0, 0]);
|
||||||
|
|||||||
Reference in New Issue
Block a user