View / add a 'smoothResolutionConstraint' options

When enabled (true by default), the resolution min/max values will be applied with
a smoothing effect for a better user experience.
This commit is contained in:
Olivier Guyot
2019-01-20 20:45:49 +01:00
parent e023c144bb
commit ef6d17d817
6 changed files with 187 additions and 65 deletions

View File

@@ -123,6 +123,9 @@ import {createMinMaxResolution} from './resolutionconstraint';
* @property {boolean} [constrainResolution] If true, the view will always
* animate to the closest zoom level after an interaction; false means
* intermediary zoom levels are allowed. Default is false.
* @property {boolean} [smoothResolutionConstraint] If true, the resolution
* min/max values will be applied smoothly, i. e. allow the view to exceed slightly
* the given resolution or zoom bounds. Default is true.
* @property {import("./proj.js").ProjectionLike} [projection='EPSG:3857'] The
* projection. The default is Spherical Mercator.
* @property {number} [resolution] The initial resolution for the view. The
@@ -978,8 +981,7 @@ class View extends BaseObject {
const zoomFactor = this.resolutions_[baseLevel] / this.resolutions_[baseLevel + 1];
return this.resolutions_[baseLevel] / Math.pow(zoomFactor, clamp(zoom - baseLevel, 0, 1));
} else {
return clamp(this.maxResolution_ / Math.pow(this.zoomFactor_, zoom - this.minZoom_),
this.minResolution_, this.maxResolution_);
return this.maxResolution_ / Math.pow(this.zoomFactor_, zoom - this.minZoom_);
}
}
@@ -1203,7 +1205,7 @@ class View extends BaseObject {
}
/**
* Set the rotation for this view.
* Set the rotation for this view using an anchor.
* @param {number} rotation The rotation of the view in radians.
* @observable
* @api
@@ -1214,7 +1216,7 @@ class View extends BaseObject {
}
/**
* Zoom to a specific zoom level.
* Zoom to a specific zoom level using an anchor
* @param {number} zoom Zoom level.
* @api
*/
@@ -1263,7 +1265,7 @@ class View extends BaseObject {
* @private
*/
resolveConstraints_(opt_duration, opt_resolutionDirection) {
const duration = opt_duration || 200;
const duration = opt_duration !== undefined ? opt_duration : 200;
const direction = opt_resolutionDirection || 0;
const newRotation = this.constraints_.rotation(this.targetRotation_);
@@ -1289,7 +1291,7 @@ class View extends BaseObject {
});
}
}
/**
* Notify the View that an interaction has started.
* @api
@@ -1391,6 +1393,9 @@ export function createResolutionConstraint(options) {
const zoomFactor = options.zoomFactor !== undefined ?
options.zoomFactor : defaultZoomFactor;
const smooth =
options.smoothResolutionConstraint !== undefined ? options.smoothResolutionConstraint : true;
if (options.resolutions !== undefined) {
const resolutions = options.resolutions;
maxResolution = resolutions[minZoom];
@@ -1398,10 +1403,10 @@ export function createResolutionConstraint(options) {
resolutions[maxZoom] : resolutions[resolutions.length - 1];
if (options.constrainResolution) {
resolutionConstraint = createSnapToResolutions(resolutions,
resolutionConstraint = createSnapToResolutions(resolutions, smooth,
!options.constrainOnlyCenter && options.extent);
} else {
resolutionConstraint = createMinMaxResolution(maxResolution, minResolution,
resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, smooth,
!options.constrainOnlyCenter && options.extent);
}
} else {
@@ -1449,10 +1454,10 @@ export function createResolutionConstraint(options) {
if (options.constrainResolution) {
resolutionConstraint = createSnapToPower(
zoomFactor, maxResolution, minResolution,
zoomFactor, maxResolution, minResolution, smooth,
!options.constrainOnlyCenter && options.extent);
} else {
resolutionConstraint = createMinMaxResolution(maxResolution, minResolution,
resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, smooth,
!options.constrainOnlyCenter && options.extent);
}
}

View File

@@ -33,9 +33,9 @@ export function createExtent(extent, onlyCenter, smooth) {
const maxX = extent[2] - viewWidth / 2;
const minY = extent[1] + viewHeight / 2;
const maxY = extent[3] - viewHeight / 2;
let x = clamp(center[0], minX, maxX);
let y = clamp(center[1], minY, maxY);
let ratio = 30 * resolution;
let x = minX > maxX ? (maxX + minX) / 2 : clamp(center[0], minX, maxX);
let y = minY > maxY ? (maxY + minY) / 2 : clamp(center[1], minY, maxY);
const ratio = 30 * resolution;
// during an interaction, allow some overscroll
if (opt_isMoving && smooth) {

View File

@@ -102,13 +102,13 @@ class ZoomSlider extends Control {
* @type {number|undefined}
* @private
*/
this.previousX_;
this.startX_;
/**
* @type {number|undefined}
* @private
*/
this.previousY_;
this.startY_;
/**
* The calculated thumb size (border box plus margins). Set when initSlider_
@@ -234,9 +234,10 @@ class ZoomSlider extends Control {
*/
handleDraggerStart_(event) {
if (!this.dragging_ && event.originalEvent.target === this.element.firstElementChild) {
const element = /** @type {HTMLElement} */ (this.element.firstElementChild);
this.getMap().getView().beginInteraction();
this.previousX_ = event.clientX;
this.previousY_ = event.clientY;
this.startX_ = event.clientX - parseFloat(element.style.left);
this.startY_ = event.clientY - parseFloat(element.style.top);
this.dragging_ = true;
if (this.dragListenerKeys_.length === 0) {
@@ -260,15 +261,11 @@ class ZoomSlider extends Control {
*/
handleDraggerDrag_(event) {
if (this.dragging_) {
const element = /** @type {HTMLElement} */ (this.element.firstElementChild);
const deltaX = event.clientX - this.previousX_ + parseFloat(element.style.left);
const deltaY = event.clientY - this.previousY_ + parseFloat(element.style.top);
const deltaX = event.clientX - this.startX_;
const deltaY = event.clientY - this.startY_;
const relativePosition = this.getRelativePosition_(deltaX, deltaY);
this.currentResolution_ = this.getResolutionForPosition_(relativePosition);
this.getMap().getView().setResolution(this.currentResolution_);
this.setThumbPosition_(this.currentResolution_);
this.previousX_ = event.clientX;
this.previousY_ = event.clientY;
}
}
@@ -282,18 +279,9 @@ class ZoomSlider extends Control {
const view = this.getMap().getView();
view.endInteraction();
const zoom = view.getValidZoomLevel(
view.getZoomForResolution(this.currentResolution_));
view.animate({
zoom: zoom,
duration: this.duration_,
easing: easeOut
});
this.dragging_ = false;
this.previousX_ = undefined;
this.previousY_ = undefined;
this.startX_ = undefined;
this.startY_ = undefined;
this.dragListenerKeys_.forEach(unlistenByKey);
this.dragListenerKeys_.length = 0;
}
@@ -360,7 +348,7 @@ class ZoomSlider extends Control {
*/
getPositionForResolution_(res) {
const fn = this.getMap().getView().getValueForResolutionFunction();
return 1 - fn(res);
return clamp(1 - fn(res), 0, 1);
}
}
@@ -379,10 +367,8 @@ export function render(mapEvent) {
this.initSlider_();
}
const res = mapEvent.frameState.viewState.resolution;
if (res !== this.currentResolution_) {
this.currentResolution_ = res;
this.setThumbPosition_(res);
}
this.currentResolution_ = res;
this.setThumbPosition_(res);
}

View File

@@ -128,19 +128,12 @@ class MouseWheelZoom extends Interaction {
*/
this.trackpadDeltaPerZoom_ = 300;
/**
* The zoom factor by which scroll zooming is allowed to exceed the limits.
* @private
* @type {number}
*/
this.trackpadZoomBuffer_ = 1.5;
}
/**
* @private
*/
decrementInteractingHint_() {
endInteraction_() {
this.trackpadTimeoutId_ = undefined;
const view = this.getMap().getView();
view.endInteraction();
@@ -211,7 +204,7 @@ class MouseWheelZoom extends Interaction {
} else {
view.beginInteraction();
}
this.trackpadTimeoutId_ = setTimeout(this.decrementInteractingHint_.bind(this), this.trackpadEventGap_);
this.trackpadTimeoutId_ = setTimeout(this.endInteraction_.bind(this), this.trackpadEventGap_);
view.adjustZoom(-delta / this.trackpadDeltaPerZoom_, this.lastAnchor_);
this.startTime_ = now;
return false;

View File

@@ -11,24 +11,45 @@ import {getHeight, getWidth} from './extent';
*/
/**
* Returns a modified resolution taking into acocunt the viewport size and maximum
* allowed extent.
* @param {number} resolution Resolution
* @param {import("./extent.js").Extent=} maxExtent Maximum allowed extent.
* @param {import("./size.js").Size} viewportSize Viewport size.
* @return {number} Capped resolution.
*/
function getCappedResolution(resolution, maxExtent, viewportSize) {
function getViewportClampedResolution(resolution, maxExtent, viewportSize) {
const xResolution = getWidth(maxExtent) / viewportSize[0];
const yResolution = getHeight(maxExtent) / viewportSize[1];
return Math.min(resolution, Math.min(xResolution, yResolution));
}
/**
* Returns a modified resolution to be between maxResolution and minResolution while
* still allowing the value to be slightly out of bounds.
* @param {number} resolution Resolution.
* @param {number} maxResolution Max resolution.
* @param {number} minResolution Min resolution.
* @return {number} Smoothed resolution.
*/
function getSmoothClampedResolution(resolution, maxResolution, minResolution) {
let result = Math.min(resolution, maxResolution);
result *= Math.log(Math.max(1, resolution / maxResolution)) * 0.1 + 1;
if (minResolution) {
result = Math.max(result, minResolution);
result /= Math.log(Math.max(1, minResolution / resolution)) * 0.1 + 1;
}
return result;
}
/**
* @param {Array<number>} resolutions Resolutions.
* @param {boolean=} opt_smooth If true, the view will be able to slightly exceed resolution limits. Default: true.
* @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent.
* @return {Type} Zoom function.
*/
export function createSnapToResolutions(resolutions, opt_maxExtent) {
export function createSnapToResolutions(resolutions, opt_smooth, opt_maxExtent) {
return (
/**
* @param {number|undefined} resolution Resolution.
@@ -39,16 +60,23 @@ export function createSnapToResolutions(resolutions, opt_maxExtent) {
*/
function(resolution, direction, size, opt_isMoving) {
if (resolution !== undefined) {
const cappedRes = opt_maxExtent ? getCappedResolution(resolution, opt_maxExtent, size) : resolution;
const maxResolution = resolutions[0];
const minResolution = resolutions[resolutions.length - 1];
const cappedMaxRes = opt_maxExtent ?
getViewportClampedResolution(maxResolution, opt_maxExtent, size) :
maxResolution;
// during interacting or animating, allow intermediary values
if (opt_isMoving) {
const maxResolution = resolutions[0];
const minResolution = resolutions[resolutions.length - 1];
return clamp(cappedRes, minResolution, maxResolution);
const smooth = opt_smooth !== undefined ? opt_smooth : true;
if (!smooth) {
return clamp(resolution, minResolution, cappedMaxRes);
}
return getSmoothClampedResolution(resolution, cappedMaxRes, minResolution);
}
let z = Math.floor(linearFindNearest(resolutions, cappedRes, direction));
const capped = Math.min(cappedMaxRes, resolution);
const z = Math.floor(linearFindNearest(resolutions, capped, direction));
return resolutions[z];
} else {
return undefined;
@@ -62,10 +90,11 @@ export function createSnapToResolutions(resolutions, opt_maxExtent) {
* @param {number} power Power.
* @param {number} maxResolution Maximum resolution.
* @param {number=} opt_minResolution Minimum resolution.
* @param {boolean=} opt_smooth If true, the view will be able to slightly exceed resolution limits. Default: true.
* @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent.
* @return {Type} Zoom function.
*/
export function createSnapToPower(power, maxResolution, opt_minResolution, opt_maxExtent) {
export function createSnapToPower(power, maxResolution, opt_minResolution, opt_smooth, opt_maxExtent) {
return (
/**
* @param {number|undefined} resolution Resolution.
@@ -76,20 +105,26 @@ export function createSnapToPower(power, maxResolution, opt_minResolution, opt_m
*/
function(resolution, direction, size, opt_isMoving) {
if (resolution !== undefined) {
const cappedRes = opt_maxExtent ? getCappedResolution(resolution, opt_maxExtent, size) : resolution;
const cappedMaxRes = opt_maxExtent ?
getViewportClampedResolution(maxResolution, opt_maxExtent, size) :
maxResolution;
const minResolution = opt_minResolution !== undefined ? opt_minResolution : 0;
// during interacting or animating, allow intermediary values
if (opt_isMoving) {
return opt_minResolution !== undefined ? Math.max(opt_minResolution, cappedRes) : cappedRes;
const smooth = opt_smooth !== undefined ? opt_smooth : true;
if (!smooth) {
return clamp(resolution, minResolution, cappedMaxRes);
}
return getSmoothClampedResolution(resolution, cappedMaxRes, minResolution);
}
const offset = -direction * (0.5 - 1e-9) + 0.5;
const capped = Math.min(cappedMaxRes, resolution);
const zoomLevel = Math.floor(
Math.log(maxResolution / cappedRes) / Math.log(power) + offset);
Math.log(maxResolution / capped) / Math.log(power) + offset);
let newResolution = maxResolution / Math.pow(power, zoomLevel);
return opt_minResolution !== undefined ?
clamp(newResolution, opt_minResolution, maxResolution) :
Math.min(maxResolution, newResolution);
return clamp(newResolution, minResolution, cappedMaxRes);
} else {
return undefined;
}
@@ -99,10 +134,11 @@ export function createSnapToPower(power, maxResolution, opt_minResolution, opt_m
/**
* @param {number} maxResolution Max resolution.
* @param {number} minResolution Min resolution.
* @param {boolean=} opt_smooth If true, the view will be able to slightly exceed resolution limits. Default: true.
* @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent.
* @return {Type} Zoom function.
*/
export function createMinMaxResolution(maxResolution, minResolution, opt_maxExtent) {
export function createMinMaxResolution(maxResolution, minResolution, opt_smooth, opt_maxExtent) {
return (
/**
* @param {number|undefined} resolution Resolution.
@@ -113,8 +149,15 @@ export function createMinMaxResolution(maxResolution, minResolution, opt_maxExte
*/
function(resolution, direction, size, opt_isMoving) {
if (resolution !== undefined) {
const cappedRes = opt_maxExtent ? getCappedResolution(resolution, opt_maxExtent, size) : resolution;
return clamp(cappedRes, minResolution, maxResolution);
const cappedMaxRes = opt_maxExtent ?
getViewportClampedResolution(maxResolution, opt_maxExtent, size) :
maxResolution;
const smooth = opt_smooth !== undefined ? opt_smooth : true;
if (!smooth || !opt_isMoving) {
return clamp(resolution, minResolution, cappedMaxRes);
}
return getSmoothClampedResolution(resolution, cappedMaxRes, minResolution);
} else {
return undefined;
}