源码分析之Openlayers中ZoomSlider滑块缩放控件
概述
ZoomSlider
滑块缩放控件就是Zoom
缩放控件的异形体,通过滑块的拖动或者点击滑槽,实现地图的缩放;另外其他方式控制地图缩放时,也会引起滑块在滑槽中的位置改变;即ZoomSlider
滑块缩放控件会监听地图的缩放级别,当级别发生改变时,也会触发ZoomSlider
中注册的事件,从而改变滑块的相对位置。
本文主要介绍 Openlayers 中ZoomSlider
滑块缩放控件的源码实现和核心逻辑分析。
源码分析
ZoomSlider
源码实现
ZoomSlider
类控件继承于Control
类,关于Control
类,可以参考这篇文章源码分析之Openlayers中的控件篇Control基类介绍。
ZoomSlider
类的源码如下:
class ZoomSlider extends Control {
constructor(options) {
options = options ? options : {};
super({
target: options.target,
element: document.createElement("div"),
render: options.render,
});
this.dragListenerKeys_ = [];
this.currentResolution_ = undefined;
this.direction_ = Direction.VERTICAL;
this.dragging_;
this.heightLimit_ = 0;
this.widthLimit_ = 0;
this.startX_;
this.startY_;
this.thumbSize_ = null;
this.sliderInitialized_ = false;
this.duration_ = options.duration !== undefined ? options.duration : 200;
const className =
options.className !== undefined ? options.className : "ol-zoomslider";
const thumbElement = document.createElement("button");
thumbElement.setAttribute("type", "button");
thumbElement.className = className + "-thumb " + CLASS_UNSELECTABLE;
const containerElement = this.element;
containerElement.className =
className + " " + CLASS_UNSELECTABLE + " " + CLASS_CONTROL;
containerElement.appendChild(thumbElement);
containerElement.addEventListener(
PointerEventType.POINTERDOWN,
this.handleDraggerStart_.bind(this),
false
);
containerElement.addEventListener(
PointerEventType.POINTERMOVE,
this.handleDraggerDrag_.bind(this),
false
);
containerElement.addEventListener(
PointerEventType.POINTERUP,
this.handleDraggerEnd_.bind(this),
false
);
containerElement.addEventListener(
EventType.CLICK,
this.handleContainerClick_.bind(this),
false
);
thumbElement.addEventListener(EventType.CLICK, stopPropagation, false);
}
setMap(map) {
super.setMap(map);
if (map) {
map.render();
}
}
initSlider_() {
const container = this.element;
let containerWidth = container.offsetWidth;
let containerHeight = container.offsetHeight;
if (containerWidth === 0 && containerHeight === 0) {
return (this.sliderInitialized_ = false);
}
const containerStyle = getComputedStyle(container);
containerWidth -=
parseFloat(containerStyle["paddingRight"]) +
parseFloat(containerStyle["paddingLeft"]);
containerHeight -=
parseFloat(containerStyle["paddingTop"]) +
parseFloat(containerStyle["paddingBottom"]);
const thumb = /** @type {HTMLElement} */ (container.firstElementChild);
const thumbStyle = getComputedStyle(thumb);
const thumbWidth =
thumb.offsetWidth +
parseFloat(thumbStyle["marginRight"]) +
parseFloat(thumbStyle["marginLeft"]);
const thumbHeight =
thumb.offsetHeight +
parseFloat(thumbStyle["marginTop"]) +
parseFloat(thumbStyle["marginBottom"]);
this.thumbSize_ = [thumbWidth, thumbHeight];
if (containerWidth > containerHeight) {
this.direction_ = Direction.HORIZONTAL;
this.widthLimit_ = containerWidth - thumbWidth;
} else {
this.direction_ = Direction.VERTICAL;
this.heightLimit_ = containerHeight - thumbHeight;
}
return (this.sliderInitialized_ = true);
}
handleContainerClick_(event) {
const view = this.getMap().getView();
const relativePosition = this.getRelativePosition_(
event.offsetX - this.thumbSize_[0] / 2,
event.offsetY - this.thumbSize_[1] / 2
);
const resolution = this.getResolutionForPosition_(relativePosition);
const zoom = view.getConstrainedZoom(view.getZoomForResolution(resolution));
view.animateInternal({
zoom: zoom,
duration: this.duration_,
easing: easeOut,
});
}
handleDraggerStart_(event) {
if (!this.dragging_ && event.target === this.element.firstElementChild) {
const element = /** @type {HTMLElement} */ (
this.element.firstElementChild
);
this.getMap().getView().beginInteraction();
this.startX_ = event.clientX - parseFloat(element.style.left);
this.startY_ = event.clientY - parseFloat(element.style.top);
this.dragging_ = true;
if (this.dragListenerKeys_.length === 0) {
const drag = this.handleDraggerDrag_;
const end = this.handleDraggerEnd_;
const doc = this.getMap().getOwnerDocument();
this.dragListenerKeys_.push(
listen(doc, PointerEventType.POINTERMOVE, drag, this),
listen(doc, PointerEventType.POINTERUP, end, this)
);
}
}
}
handleDraggerDrag_(event) {
if (this.dragging_) {
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_);
}
}
handleDraggerEnd_(event) {
if (this.dragging_) {
const view = this.getMap().getView();
view.endInteraction();
this.dragging_ = false;
this.startX_ = undefined;
this.startY_ = undefined;
this.dragListenerKeys_.forEach(unlistenByKey);
this.dragListenerKeys_.length = 0;
}
}
setThumbPosition_(res) {
const position = this.getPositionForResolution_(res);
const thumb = /** @type {HTMLElement} */ (this.element.firstElementChild);
if (this.direction_ == Direction.HORIZONTAL) {
thumb.style.left = this.widthLimit_ * position + "px";
} else {
thumb.style.top = this.heightLimit_ * position + "px";
}
}
getRelativePosition_(x, y) {
let amount;
if (this.direction_ === Direction.HORIZONTAL) {
amount = x / this.widthLimit_;
} else {
amount = y / this.heightLimit_;
}
return clamp(amount, 0, 1);
}
getResolutionForPosition_(position) {
const fn = this.getMap().getView().getResolutionForValueFunction();
return fn(1 - position);
}
getPositionForResolution_(res) {
const fn = this.getMap().getView().getValueForResolutionFunction();
return clamp(1 - fn(res), 0, 1);
}
render(mapEvent) {
if (!mapEvent.frameState) {
return;
}
if (!this.sliderInitialized_ && !this.initSlider_()) {
return;
}
const res = mapEvent.frameState.viewState.resolution;
this.currentResolution_ = res;
this.setThumbPosition_(res);
}
}
ZoomSlider
构造函数
ZoomSlider
构造函数接受的参数对象options
除了包含常规的控件属性render
、target
和className
外,还有个属性duration
,不传的话,该属性值默认为200
毫秒,表示地图视图动画的持续时长。
ZoomSlider
构造函数除了创建控件元素外,还给控件元素添加了几个监听事件,如下:
//鼠标按键按下时触发,pointerdown相当于mousedown
containerElement.addEventListener(
PointerEventType.POINTERDOWN,
this.handleDraggerStart_.bind(this),
false
);
//鼠标按键移动时触发,pointermove相当于mousemove
containerElement.addEventListener(
PointerEventType.POINTERMOVE,
this.handleDraggerDrag_.bind(this),
false
);
//鼠标按键抬起时触发,pointerup相当于mouseup
containerElement.addEventListener(
PointerEventType.POINTERUP,
this.handleDraggerEnd_.bind(this),
false
);
ZoomSlider
主要方法
-
setMap
方法:这个方法就是调用父类的setMap
方法,然后判断,若map
存在,则调用map.render
,这个操作着实有点多余,因为父类中也有这个逻辑. -
initSlider_
方法:滑动缩放控件可以是水平方向也可以是垂直方向,这个方法就是初始化滑动控件的显示,确保滑块滑动时始终在滑槽内. -
render
方法:render
方法主要用于更新滑块的位置,当地图的postrender
类型触发时,会执行这个函数;获取当前地图视图状态的分辨率,调用setThumbPosition
设置滑块的位置. -
getPositionForResolution_
方法:获取给定的分辨率下,滑块的相对位置 -
getResolutionForPosition_
方法:通过滑块的相对位置,计算出相对应的分辨率 -
getRelativePosition_
方法:通过x
和y
计算出相对位置,该值在[0,1]
之间 -
setThumbPosition_
方法:该方法作用就是用于设置滑块的相对位置,通过当前地图视图的分辨率计算出滑块的相对偏移值,然后设置其left
或top
属性值 -
handleDraggerEnd_
方法
在构造函数中初始化了一个全局变量this.dragging_
,用来标识当前滑块是否处于拖动状态;当鼠标停止拖动抬起时,会触发该方法;该方法内部会先判断,若this.dragging_
为true
,则表明前一刻的鼠标是拖动状态,会先结束地图视图的交互,然后重置一些状态变量this.dragging_
,this.startX_
和this.startY_
,最后清除一些在拖动开始时注册的监听;否则不是,不执行任何逻辑.
handleDraggerDrag_
方法
handleDraggerDrag_
方法会在鼠标拖动滑块时调用,同样地,会先判断,若this.dragging_
为true
,则计算出滑块的相对偏移值,然后根据偏移值调用this.getRelativePosition
获取相对的位置偏移量,再通过this.getResolutionForPosition_
得出当前得分辨率,最后调用地图视图的setResolution
设置地图的分辨率,这就实现了拖动滑块时地图实时进行缩放动作的效果.
handleDraggerStart_
方法
handleDraggerStart_
方法就是在滑块拖动时进行一些初始化操作,设置一些状态量,以及调用beginInteraction
开始交互,还会给地图容器注册一些鼠标移动和抬起的监听,这在触屏设备有用.
handleContainerClick_
方法
ZoomSlider
滑块缩放控件除了拖动滑块可以实现地图的缩放,还可以通过点击滑槽实现地图的缩放.后者的功能就是handleContainerClick_
方法提供的.该方法内部就是先获取点击位置的坐标,然后通过该坐标计算出相对位置,再通过相对位置调用this.getResolutionForPosition
计算出相对分辨率,然后调用view.getZoomForResolution
获取缩放级别,最后调用view.animateInternal
设置地图的缩放级别,这和滑块拖动缩放的最后调用的方法不同,这种会有动画效果.
总结
本文主要介绍了 Openlayers 中ZoomSlider
滑块缩放控件的实现,主要是滑块在滑槽中的相对位置对应着当前地图的分辨率在分辨率区间的映射关系,这一关系可以基于view
通过计算所得.