leaflet 在初始化的时候会生成根节点 DOM 元素,根节点下面是map
和control
的 DOM 元素,这些元素的默认初始位置都是浏览器左上角(0,0)
。
其中map
的 DOM 容器中是各类layer
和animated
对应的DOM元素,control
的 DOM 元素中是固定在四个角落的控件容器,里面放了具体的控件 DOM 元素
layer
的 DOM 容器中默认有tile、overlay、shadow、marker、tooltip、popup
6类图层的DOM元素,其初始位置也是浏览器左上角(0,0)
。
_container 是根节点 dom 容器,一般是用户手动指定某个id
的元素。
map、layer、control 对应的 DOM 元素都是xxx-pane
。
export const Map = Evented.extend({
// ......
// 初始化 map,传入的是 dom 元素的 id 和 map 配置项
initialize(id, options) {
// (HTMLElement or String, Object)
options = Util.setOptions(this, options);
// 在一开始就把内部变量定义好,不要等到用的时候才定义,避免在某些 edge cases 情形下发生状态不一致的问题。
this._handlers = [];
this._layers = {};
this._zoomBoundLayers = {};
this._sizeChanged = true;
// 初始化根节点 dom 容器
this._initContainer(id);
// 初始化地图和图层的 dom 节点
this._initLayout();
// 初始化 dom 事件
this._initEvents();
if (options.maxBounds) {
// 限定 map 视野范围
this.setMaxBounds(options.maxBounds);
}
if (options.zoom !== undefined) {
// 设置当前地图的缩放级别(如果传入了zoom,需要检查下zoom符合最大最小缩放级别和zoomSanp)
this._zoom = this._limitZoom(options.zoom);
}
if (options.center && options.zoom !== undefined) {
// 设置地图的初始范围
this.setView(toLatLng(options.center), options.zoom, {reset: true});
}
// 调用 map 初始化构造函数钩子,例如 control 或者 layer 都通过 Map.addInitHook 注册过 map 构造函数钩子
this.callInitHooks();
// 不在没有硬件加速功能或旧Android的浏览器上设置动画
this._zoomAnimated = DomUtil.TRANSITION && Browser.any3d && this.options.zoomAnimation;
// zoom transitions run with the same duration for all layers, so if one of transitionend events happens after starting zoom animation (冒泡到 map pane), we know that it ended globally
// 缩放过渡对所有图层的持续时间相同,因此如果在启动缩放动画(冒泡到 map pane)后其中一个 transitionend 事件发生了,我们知道它全局结束了。
if (this._zoomAnimated) {
this._createAnimProxy();
// “代理” dom 元素 transition 过度效果结束后执行 _catchTransitionEnd 方法,移除动画效果
DomEvent.on(this._proxy, DomUtil.TRANSITION_END, this._catchTransitionEnd, this);
}
// 添加配置项里的图层
this._addLayers(this.options.layers);
},
/** 初始化根节点 dom 容器 */
_initContainer(id) {
// 根据 id 获取 dom 元素
const container = (this._container = DomUtil.get(id));
if (!container) {
throw new Error('Map container not found.');
} else if (container._leaflet_id) {
// 如果已经有了 leaflet id,代表已经被初始化过一次了
throw new Error('Map container is already initialized.');
}
// 绑定鼠标滚动事件
DomEvent.on(container, 'scroll', this._onScroll, this);
// 设置 leaflet 全局唯一 id
this._containerId = Util.stamp(container);
},
/** 初始化各个子元素的 dom */
_initLayout() {
const container = this._container;
this._fadeAnimated = this.options.fadeAnimation && Browser.any3d;
// 添加 css 样式
DomUtil.addClass(container, `leaflet-container${Browser.touch ? ' leaflet-touch' : ''}${Browser.retina ? ' leaflet-retina' : ''}${Browser.safari ? ' leaflet-safari' : ''}${this._fadeAnimated ? ' leaflet-fade-anim' : ''}`);
const position = DomUtil.getStyle(container, 'position');
// 设置容器的 position 属性,方便子元素定位
if (position !== 'absolute' && position !== 'relative' && position !== 'fixed' && position !== 'sticky') {
container.style.position = 'relative';
}
this._initPanes();
if (this._initControlPos) {
this._initControlPos();
}
},
/** 创建各种图层的 dom 容器 */
_initPanes() {
const panes = (this._panes = {});
this._paneRenderers = {};
// Panes 是一个 DOM 元素,用来控制 map 的图层顺序。
// 可以使用 map.getPane 或者 map.getPanes 方法获取 panes。
// 使用 map.createPane 创建新的 panes
// 每一个地图都有以下几个 panes,她们只是 zIndex 不同
// Every map has the following default panes that differ only in zIndex.
// mapPane 放在地图的根节点 dom 元素下
this._mapPane = this.createPane('mapPane', this._container);
DomUtil.setPosition(this._mapPane, new Point(0, 0));
// @pane tilePane: HTMLElement = 200
// Pane for `GridLayer`s and `TileLayer`s
this.createPane('tilePane');
// @pane overlayPane: HTMLElement = 400
// Pane for vectors (`Path`s, like `Polyline`s and `Polygon`s), `ImageOverlay`s and `VideoOverlay`s
this.createPane('overlayPane');
// @pane shadowPane: HTMLElement = 500
// Pane for overlay shadows (e.g. `Marker` shadows)
this.createPane('shadowPane');
// @pane markerPane: HTMLElement = 600
// Pane for `Icon`s of `Marker`s
this.createPane('markerPane');
// @pane tooltipPane: HTMLElement = 650
// Pane for `Tooltip`s.
this.createPane('tooltipPane');
// @pane popupPane: HTMLElement = 700
// Pane for `Popup`s.
this.createPane('popupPane');
if (!this.options.markerZoomAnimation) {
// 如果 marker 启用了动画,添加 css 样式(marker 比较特殊,是 HTML 对象,和矢量图层的点线面不一样)
DomUtil.addClass(panes.markerPane, 'leaflet-zoom-hide');
DomUtil.addClass(panes.shadowPane, 'leaflet-zoom-hide');
}
},
/** 根据给定的名称创建一个 pane,如果没有指定父容器,则默认 pane 都是 mapPane 的子元素 */
createPane(name, container) {
const className = `leaflet-pane${name ? ` leaflet-${name.replace('Pane', '')}-pane` : ''}`,
// 默认都是在 mapPane 下
pane = DomUtil.create('div', className, container || this._mapPane);
if (name) {
this._panes[name] = pane;
}
return pane;
},
/** 获取对应的 don 容器 */
getPane(pane) {
return typeof pane === 'string' ? this._panes[pane] : pane;
},
/** 返回所有 pane 的名称 */
getPanes() {
return this._panes;
},
/** 返回当前的根节点 dom 元素 */
getContainer() {
return this._container;
},
// ......
})