Leaflet源码解析系列(六-3):Map 对象解读——初始化 Dom 容器

312 阅读3分钟

leaflet 在初始化的时候会生成根节点 DOM 元素,根节点下面是mapcontrol的 DOM 元素,这些元素的默认初始位置都是浏览器左上角(0,0)

其中map的 DOM 容器中是各类layeranimated对应的DOM元素,control的 DOM 元素中是固定在四个角落的控件容器,里面放了具体的控件 DOM 元素

layer的 DOM 容器中默认有tile、overlay、shadow、marker、tooltip、popup6类图层的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;
	},
  // ......
})