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:
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user