Maplibre 源码阅读
事件处理
相关类:
HandlerManager
: Handler
的管理类,负责所有的交互系统的注册、启用/禁用、Handler
冲突处理,统一 DOM 事件捕获并将事件路由到各个 Handler
,合并处理结果,集中管理交互状态。
MapMouseEvent
: 鼠标事件封装类,内部通过 map.unproject
计算像素坐标对应的经纬度,并存储了事件的上下文信息,以及事件的 _defaultPrevented
阻止默认事件的状态
初始化:
- 在 map 实例中初始化
HandlerManager
,HandlerManager
初始化各个事件处理Handler
(如:MapEventHandler
负责 dom 事件处理,BoxZoomHandler
处理框选缩放) HandlerManager
注册 DOM 监听器,根据监听document
还是map
的容器分别绑定handleWindowEvent
和handleEvent
事件。
事件注册:
- 通过
map.on
函数统一事件注册入口 全局事件直接通过super.on
注册到map
实例继承的Evented
抽象类中, 对于指定layerId
的侦听器,通过_createDelegatedListener
创建一个委托闭包用于封装相关状态(mousein
)和layerIds.filter()
的逻辑,并将闭包存储到实例维护的_delegatedListeners
对象中,然后再将闭包中封装后回调函数通过super.on
注册到的事件总线中
事件分发:
- 捕捉到事件后,由
handleEvent
集中处理事件分发到各个Handler
并合并结果。handleWindowEvent
捕获document
事件后在事件的type
中添加Windows
后缀转发给handleEvent
进行后续处理 handleEvent
通过一系列的条件判定事件由哪个Handler
处理 以点击事件为例,点击过程会先调用MapEventHandler
的mousedown
函数并记录下点击位置的坐标; 然后会调用MapEventHandler
的click
函数,此时会判定point
位置和之前记录的位置的偏移是否超过容差范围,如果超过则视为拖拽退出当前处理函数, 没超过则正常触发点击事件,调用map.fire(new MapMouseEvent(e.type, this._map, e))
调用地图实例中注册的回调函数,并创建MapMouseEvent
作为事件参数
总结:
交互系统的难度主要体现在如何设计一套完备正交的交互类型判定流程,代码实现上是一个非常“脏”的工作,需要写大量的 if
判断处理各种边界条件和冲突
浏览器的原始事件对象是不足够支撑复杂的交互需求的,需要在此之上封装满足自己业务需求的事件对象
坐标系统
背景知识:
[MDN] WebGL model view projection
名词解释:
- 裁剪空间: 一个特殊坐标空间,中心点位于 (0, 0, 0),角落范围在 (-1, -1, -1) 到 (1, 1, 1) 之间,2 个单位宽的立方体。该剪裁空间被压缩到一个二维空间并栅格化为图像。
MDN 裁剪空间 - FOV: FOV (Field Of View 视场角) 指相机通过镜头能够捕捉到的场景范围的角度大小。它决定了相机成像的广度,通常以角度为单位表示(如水平、垂直或对角线方向);通常我们说 FOV 指的是垂直FOV
相关类:
TransformHelper
: 管理和地图变换的所有状态信息,如: zoom
, pitch
, tileSzie
等,并在 set*
方法中实现了状态验证和约束机制。
同时,将计算所需的逻辑通过注册回调的方式交给具体的投影实现(如 MercatorTransform
)这样可以避免在每个投影系统中重复实现状态管理逻辑
MercatorTransform
: 通过 TransformHelper
管理所有变换状态,这个类专注于投影计算逻辑,实现多种坐标系之间的双向转换,如:屏幕坐标和墨卡托坐标的转换,屏幕坐标和**地理坐标(包含地形高度的墨卡托坐标)**的转换等
这里为了方便表述投影空间直接表述为了墨卡托坐标,实际上地图有非常的多的投影空间,不过 maplibre 中目前只支持了墨卡托投影
MercatorCoveringTilesDetailsProvider
: 墨卡托投影系统的瓦片覆盖计算服务核心类
MercatorCameraHelper
: 在墨卡托投影系统中提供相机控制和动画功能的核心类
关键函数:
-
MercatorTransform._calcMatrices
计算渲染用到的矩阵会计算并保存七个变换相关的矩阵,分别是:
_projectionMatrix
- 叠加offset
的标准透视投影矩阵_mercatorMatrix
- 在_projectionMatrix
上叠加相机变换,并缩放大,墨卡托坐标到裁剪空间矩阵_pixelMatrix
- 世界坐标到屏幕像素矩阵_viewProjMatrix
- 视图投影矩阵_pixelMatrix3D
- 3D世界坐标到屏幕矩阵_fogMatrix
- 雾效计算矩阵_alignedProjMatrix
- 像素对齐投影矩阵- 计算过程:
- 获取地图中心点在世界坐标系中的位置
(x, y)
- 计算
_pixelPerMeter
像素/米 - 通过
_calculateNearFarZIfNeeded
计算出透视投影的近/远裁剪平面 - 使用
mat4.perspective(m, this.fovInRadians, aspectRatio, nearZ, farZ)
创建标准的 透视投影矩阵 ,计算投影矩阵的逆矩阵,应用视角中心偏移(处理地图padding),保存投影矩阵 - 在投影矩阵的基础上叠加相机变换矩阵,得到相机空间矩阵
- 再通过
mat4.scale([], m, [worldSize, worldSize, worldSize])
,并保存为_mercatorMatrix
矩阵,可以用于将墨卡托空间坐标([0, 0]nw, [1, 1]se)转换为裁剪空间坐标 - 在视图空间矩阵的基础上叠加裁剪空间到像素空间的矩阵
TransformHelper.clipSpaceToPixelsMatrix
得到pixelMatrix
- 将相机移动到地形上方(如果有启用地形),保存
_viewProjMatrix
和_invViewProjMatrix
- 创建 fog 矩阵,用于计算雾化强度,和透视矩阵的区别在于近裁剪面设置在地图中心。 这里有一个特性是透视矩阵的深度并非线性分布的,越近的部分计算精度会越高
- 计算像素对齐矩阵并保存为
_alignedProjMatrix
- 翻转像素矩阵保存为
_pixelMatrixInverse
- 清理之前的缓存矩阵
- 获取地图中心点在世界坐标系中的位置
- 计算过程:
初始化:
- 在
map
实例中初始化MercatorTransform
实例 MercatorTransform
初始化TransformHelper
实例,并将calcMatrices
回调注册到TransformHelper
中 初始化MercatorCoveringTilesDetailsProvider
类map
实例初始化MercatorTransform
的状态map
调用jumpTo
通过MercatorCameraHelper.handleJumpToCenterZoom()
方法初始化视图handleJumpToCenterZoom
中会调用ITransform.setCenter()
方法,进一步调用MercatorTransform._calcMatrices
初始化变换矩阵
点击事件流程:
-
捕获流程前文已写这里省略,直接从
HandlerManager.handleEvent()
调用开始 -
通过调用
MapEventHandler.click()
创建MapMouseEvent()
事件对象 -
MapMouseEvent()
通过map.unproject(point)
将像素坐标转化为lngLat
-
map.unproject
调用ITransform.screenPointToLocation(point, this.terrain)
将像素坐标转换为lnglat
,这里的ITransform
接口的实现是MercatorTransform
目前maplibre
只支持墨卡托投影,转换过程中也考虑了地形因素,如果启用了地形则会使用地形实例中的terrain.pointCoordinate(point)
来获取坐标 -
否则调用
MercatorTransform.screenPointToMercatorCoordinateAtZ(point, mercatorZ)
对坐标进行转换,这个函数接受两个参数像素坐标和墨卡托坐标中的Z
值(世界中的海拔高度)来确定点击的具体位置,原理如图:
-
-
事件对象创建完成后通过
this.map.fire()
方法将事件对象传递给各个监听器