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:
Olivier Guyot
2019-01-14 13:36:20 +01:00
parent e52fab636c
commit 1f379a06a4
8 changed files with 128 additions and 28 deletions

View File

@@ -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};

View File

@@ -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;

View File

@@ -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<number>} 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);

View File

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