Leaflet源码解析系列(五):Control 对象解读

526 阅读7分钟

Control是添加在地图上的小组件,leaflet内置了图层管理器、比例尺、缩放控件和地图属性展示控件这四个。用户可以继承Control来扩展自己的小组件。

Control 基类

import {Class} from '../core/Class';
import {Map} from '../map/Map';
import * as Util from '../core/Util';
import * as DomUtil from '../dom/DomUtil';

/*
 * @class Control
 * @aka L.Control
 * @inherits Class
 *
 * L.Control 是个基类,用来继承扩展实现其他地图组件。处理了组件在地图页面上的显示位置。
 * 所有其他的组件都是从这个类扩展而来
 */

export const Control = Class.extend({
	// @section
	// @aka Control Options
	options: {
		// @option position: String = 'topright'
		// control在地图上的摆放位置 (只能在地图的一个角落). 
		// 可选'topleft','topright', 'bottomleft' or 'bottomright'
		position: 'topright'
	},

	initialize(options) {
		Util.setOptions(this, options);
	},

	/* @section
	 * 扩展了 L.Control 的类会继承以下方法:
	 *
	 * @method getPosition: string
	 * 获取 control 在地图上的位置.
	 */
	getPosition() {
		return this.options.position;
	},

	// @method setPosition(position: string): this
	// 设置 control 在地图上的位置,先移除在添加
	setPosition(position) {
		const map = this._map;

		if (map) {
			map.removeControl(this);
		}

		this.options.position = position;

		if (map) {
			map.addControl(this);
		}

		return this;
	},

	// @method getContainer: HTMLElement
	// 返回 control 对应的 HTML 元素.
	getContainer() {
		return this._container;
	},

	// @method addTo(map: Map): this
	// 将 Control 添加到地图上,会触发control内容初始化
	addTo(map) {
		this.remove();
		this._map = map;

    //  初始化control,返回control的dom元素
		const container = this._container = this.onAdd(map),
		  // 位置属性  
      pos = this.getPosition(),
		  // 对应四个角落之一的control容器  
      corner = map._controlCorners[pos];

		DomUtil.addClass(container, 'leaflet-control');

		if (pos.includes('bottom')) {
      // 如果位置在底部,则依次添加(多个control的情况,自下而上)
			corner.insertBefore(container, corner.firstChild);
		} else {
      // 顶部的话,就默认追加(自上而下)
			corner.appendChild(container);
		}

    // 注册map销毁事件,销毁的的时候移除组件
		this._map.on('unload', this.remove, this);

		return this;
	},

	// @method remove: this
	// 从地图页面中移除当前 Control.
	remove() {
		if (!this._map) {
			return this;
		}

		DomUtil.remove(this._container);

		if (this.onRemove) {
			this.onRemove(this._map);
		}

    // 取消在map上注册的事件
		this._map.off('unload', this.remove, this);
		this._map = null;

		return this;
	},

	_refocusOnMap(e) {
		// if map exists and event is not a keyboard event
		if (this._map && e && e.screenX > 0 && e.screenY > 0) {
			this._map.getContainer().focus();
		}
	}
});

export const control = function (options) {
	return new Control(options);
};

/* @section Extension methods
 * @uninheritable
 *
 * 每一个继承自 L.Control 的子类走要实现以下方法
 *
 * @method onAdd(map: Map): HTMLElement
 * 初始化 Control 内容,返回 Control的 DOM 元素,并监听相关的 map 事件。被 addTo 调用。
 *
 * @method onRemove(map: Map)
 * Optional method。移除组件时候相关的清理代码,例如删除之前 onAdd 时候绑定的监听函数,被 control.remove()调用.
 */

/* @namespace Map
 * @section Methods for Layers and Controls
 */
Map.include({
	// @method addControl(control: Control): this
	// Adds the given control to the map
	addControl(control) {
		control.addTo(this);
		return this;
	},

	// @method removeControl(control: Control): this
	// Removes the given control from the map
	removeControl(control) {
		control.remove();
		return this;
	},

  // Map 初始化的时候会执行
	_initControlPos() {
		const corners = this._controlCorners = {},
		    l = 'leaflet-',
      // 在map的dom元素上创建control容器元素
		    container = this._controlContainer =
		            DomUtil.create('div', `${l}control-container`, this._container);

    // 创建dom元素,设置css类
		function createCorner(vSide, hSide) {
			const className = `${l + vSide} ${l}${hSide}`;
    	// 在control容器种添加四个角落的control元素
			corners[vSide + hSide] = DomUtil.create('div', className, container);
		}

    // 分别创建四个角的dom元素
		createCorner('top', 'left');
		createCorner('top', 'right');
		createCorner('bottom', 'left');
		createCorner('bottom', 'right');
	},

	_clearControlPos() {
		for (const i in this._controlCorners) {
			DomUtil.remove(this._controlCorners[i]);
		}
		DomUtil.remove(this._controlContainer);
		delete this._controlCorners;
		delete this._controlContainer;
	}
});

Layers  图层管理控件

import { Control } from "./Control";
import * as Util from "../core/Util";
import * as DomEvent from "../dom/DomEvent";
import * as DomUtil from "../dom/DomUtil";

/*
 * @class Control.Layers
 * @aka L.Control.Layers
 * @inherits Control
 *
 *  layers control 提供了切换不同的 baseLayers 和 overlays 开关的功能。 Extends `Control`.
 *
 * @example
 *
 * ```js
 * var baseLayers = {
 * 	"Mapbox": mapbox,
 * 	"OpenStreetMap": osm
 * };
 *
 * var overlays = {
 * 	"Marker": marker,
 * 	"Roads": roadsLayer
 * };
 *
 * L.control.layers(baseLayers, overlays).addTo(map);
 * ```
 *
 * The `baseLayers` and `overlays` 参数是一个对象,图层名作为key,图层对象作为value
 *
 * ```js
 * {
 *     "<someName1>": layer1,
 *     "<someName2>": layer2
 * }
 * ```
 *
 * 图层名可以包含 html 字符串,实现自定义的效果:
 *
 * ```js
 * {"<img src='my-layer-icon' /> <span class='my-layer-item'>My Layer</span>": myLayer}
 * ```
 */

export const Layers = Control.extend({
  // @section
  // @aka Control.Layers options
  options: {
    // @option collapsed: Boolean = true
    // If `true`, 默认折叠为一个图标,鼠标悬浮、点击或其他事件触发时候会展开.
    collapsed: true,
    position: "topright",

    // @option autoZIndex: Boolean = true
    // If `true`, the control will assign zIndexes in increasing order to all of its layers so that the order is preserved when 切换图层开关.
    autoZIndex: true,

    // @option hideSingleBase: Boolean = false
    // If `true`, 如果只有一个 baseLayer,这个将被隐藏.
    hideSingleBase: false,

    // @option sortLayers: Boolean = false
    // 是否对 layer 排序. When `false`, layers 将保持添加时候的顺序
    sortLayers: false,

    // @option sortFunction: Function = *
    // A [compare function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)
    // 当 `sortLayers` 为 `true`,用来进行图层排序,接收图层对象和图层名称作为参数.
    // 默认按照图层名的字母顺序进行排序.
    sortFunction(layerA, layerB, nameA, nameB) {
      return nameA < nameB ? -1 : nameB < nameA ? 1 : 0;
    }
  },

  /** 初始化,接收三个参数,默认分了两类
   * @param baseLayers 基础底图,图层间单选,Object,图层名作为key(key可以包含 html 字符串,实现自定义的效果),图层对象作为value
   * @param overlays 叠加图层,图层间可以复选,Object,图层名作为key(key可以包含 html 字符串,实现自定义的效果),图层对象作为value
   * @param options 配置项
   */
  initialize(baseLayers, overlays, options) {
    Util.setOptions(this, options);

    this._layerControlInputs = [];
    this._layers = [];
    this._lastZIndex = 0;
    this._handlingClick = false;

    // 依次将 baselayer 加到 control
    for (const i in baseLayers) {
      this._addLayer(baseLayers[i], i);
    }

    //  依次将 overlays 加到 control
    for (const i in overlays) {
      this._addLayer(overlays[i], i, true);
    }
  },

  /** 生成 dom 容器,绑定事件 */
  onAdd(map) {
    this._initLayout();
    this._update();

    this._map = map;
    map.on("zoomend", this._checkDisabledLayers, this);

    for (let i = 0; i < this._layers.length; i++) {
      this._layers[i].layer.on("add remove", this._onLayerChange, this);
    }

    return this._container;
  },

  /** 添加到map */
  addTo(map) {
    Control.prototype.addTo.call(this, map);
    // 在图层控件插入DOM后触发展开,使它有一个实际的高度。
    return this._expandIfNotCollapsed();
  },

  /** 移除的时候解绑事件 */
  onRemove() {
    this._map.off("zoomend", this._checkDisabledLayers, this);

    for (let i = 0; i < this._layers.length; i++) {
      this._layers[i].layer.off("add remove", this._onLayerChange, this);
    }
  },

  // @method addBaseLayer(layer: Layer, name: String): this
  // 添加基础图层 baseLayer (radio button entry)  .
  addBaseLayer(layer, name) {
    this._addLayer(layer, name);
    return this._map ? this._update() : this;
  },

  // @method addOverlay(layer: Layer, name: String): this
  // 添加叠加层 overlay (checkbox entry)
  addOverlay(layer, name) {
    this._addLayer(layer, name, true);
    return this._map ? this._update() : this;
  },

  // @method removeLayer(layer: Layer): this
  // 移除一个图层.
  removeLayer(layer) {
    layer.off("add remove", this._onLayerChange, this);

    // 确保图层在control中
    const obj = this._getLayer(Util.stamp(layer));
    if (obj) {
      this._layers.splice(this._layers.indexOf(obj), 1);
    }
    // 更新control和map
    return this._map ? this._update() : this;
  },

  // @method expand(): this
  // 展开 layer control
  expand() {
    // 添加 css 类
    this._container.classList.add("leaflet-control-layers-expanded");
    this._section.style.height = null;
    const acceptableHeight = this._map.getSize().y - (this._container.offsetTop + 50);
    if (acceptableHeight < this._section.clientHeight) {
      this._section.classList.add("leaflet-control-layers-scrollbar");
      this._section.style.height = `${acceptableHeight}px`;
    } else {
      this._section.classList.remove("leaflet-control-layers-scrollbar");
    }
    this._checkDisabledLayers();
    return this;
  },

  // @method collapse(): this
  // 折叠 layer control.
  collapse() {
    this._container.classList.remove("leaflet-control-layers-expanded");
    return this;
  },

  _initLayout() {
    const className = "leaflet-control-layers",
      container = (this._container = DomUtil.create("div", className)),
      collapsed = this.options.collapsed;

    // makes this work on IE touch devices by stopping it from firing a mouseout event when the touch is released
    container.setAttribute("aria-haspopup", true);

    DomEvent.disableClickPropagation(container);
    DomEvent.disableScrollPropagation(container);

    const section = (this._section = DomUtil.create("fieldset", `${className}-list`));

    if (collapsed) {
      this._map.on("click", this.collapse, this);

      DomEvent.on(
        container,
        {
          mouseenter: this._expandSafely,
          mouseleave: this.collapse
        },
        this
      );
    }

    // 折叠按钮
    const link = (this._layersLink = DomUtil.create("a", `${className}-toggle`, container));
    link.href = "#";
    link.title = "Layers";
    link.setAttribute("role", "button");

    // 折叠按钮事件
    DomEvent.on(
      link,
      {
        keydown(e) {
          if (e.keyCode === 13) {
            this._expandSafely();
          }
        },
        // Certain screen readers intercept the key event and instead send a click event
        click(e) {
          DomEvent.preventDefault(e);
          this._expandSafely();
        }
      },
      this
    );

    if (!collapsed) {
      this.expand();
    }

    // 基础底图 baselayer 容器
    this._baseLayersList = DomUtil.create("div", `${className}-base`, section);
    // 分割线
    this._separator = DomUtil.create("div", `${className}-separator`, section);
    // 叠加层 overlays 容器
    this._overlaysList = DomUtil.create("div", `${className}-overlays`, section);

    // 将 section 加到 layer control 容器
    container.appendChild(section);
  },

  // 根据唯一id获取layer
  _getLayer(id) {
    for (let i = 0; i < this._layers.length; i++) {
      if (this._layers[i] && Util.stamp(this._layers[i].layer) === id) {
        return this._layers[i];
      }
    }
  },

  _addLayer(layer, name, overlay) {
    if (this._map) {
      layer.on("add remove", this._onLayerChange, this);
    }

    this._layers.push({
      layer,
      name,
      overlay
    });

    // 排序
    if (this.options.sortLayers) {
      this._layers.sort((a, b) => this.options.sortFunction(a.layer, b.layer, a.name, b.name));
    }

    // 设置 layer 的 index
    if (this.options.autoZIndex && layer.setZIndex) {
      this._lastZIndex++;
      layer.setZIndex(this._lastZIndex);
    }

    this._expandIfNotCollapsed();
  },

  _update() {
    if (!this._container) {
      return this;
    }

		// 基础底图 baselayer 容器
    this._baseLayersList.replaceChildren();
		// 叠加层 overlays 容器
    this._overlaysList.replaceChildren();

    this._layerControlInputs = [];
    let baseLayersPresent, // 是否有 baseLayer
      overlaysPresent, // 是否有 overlay
      i,
      obj,
      baseLayersCount = 0;

    // 依次遍历,添加图层 item
    for (i = 0; i < this._layers.length; i++) {
      obj = this._layers[i];
      this._addItem(obj);
      overlaysPresent = overlaysPresent || obj.overlay; // 是否有 overlay
      baseLayersPresent = baseLayersPresent || !obj.overlay; // 是否有 baseLayer
      baseLayersCount += !obj.overlay ? 1 : 0;
    }

    // 如果只有一个的话,隐藏 baseLayer 
    if (this.options.hideSingleBase) {
      baseLayersPresent = baseLayersPresent && baseLayersCount > 1;
      this._baseLayersList.style.display = baseLayersPresent ? "" : "none";
    }

		// 如果 baseLayer 和 overlay 都存在,才显示分割线
    this._separator.style.display = overlaysPresent && baseLayersPresent ? "" : "none";

    return this;
  },

  /** 图层变化,触发底图的图层加载和移除事件 */
  _onLayerChange(e) {
    if (!this._handlingClick) {
      this._update();
    }

    const obj = this._getLayer(Util.stamp(e.target));

    // @namespace Map
    // @section Layer events
    // @event baselayerchange: LayersControlEvent
    // Fired when the base layer is changed through the [layers control](#control-layers).
    // @event overlayadd: LayersControlEvent
    // Fired when an overlay is selected through the [layers control](#control-layers).
    // @event overlayremove: LayersControlEvent
    // Fired when an overlay is deselected through the [layers control](#control-layers).
    // @namespace Control.Layers
    const type = obj.overlay ? (e.type === "add" ? "overlayadd" : "overlayremove") : e.type === "add" ? "baselayerchange" : null;

    if (type) {
      this._map.fire(type, obj);
    }
  },

  // IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see https://stackoverflow.com/a/119079)
  _createRadioElement(name, checked) {
    const radioHtml = `<input type="radio" class="leaflet-control-layers-selector" name="${name}"${checked ? ' checked="checked"' : ""}/>`;

    const radioFragment = document.createElement("div");
    radioFragment.innerHTML = radioHtml;

    return radioFragment.firstChild;
  },

	/** 创建 layer item 对应的 dom 元素,绑定事件 */
  _addItem(obj) {
    const label = document.createElement("label"),
      checked = this._map.hasLayer(obj.layer);
    let input;

    if (obj.overlay) {
      input = document.createElement("input");
      input.type = "checkbox";
      input.className = "leaflet-control-layers-selector";
      input.defaultChecked = checked;
    } else {
      input = this._createRadioElement(`leaflet-base-layers_${Util.stamp(this)}`, checked);
    }

    this._layerControlInputs.push(input);
    input.layerId = Util.stamp(obj.layer);

    DomEvent.on(input, "click", this._onInputClick, this);

    const name = document.createElement("span");
    name.innerHTML = ` ${obj.name}`;

    // Helps from preventing layer control flicker when checkboxes are disabled
    // https://github.com/Leaflet/Leaflet/issues/2771
    const holder = document.createElement("span");

    label.appendChild(holder);
    holder.appendChild(input);
    holder.appendChild(name);

    const container = obj.overlay ? this._overlaysList : this._baseLayersList;
    container.appendChild(label);

    this._checkDisabledLayers();
    return label;
  },

	/** 图层 item 点击事件 */
  _onInputClick() {
    const inputs = this._layerControlInputs,
      addedLayers = [],
      removedLayers = [];
    let input, layer;

    this._handlingClick = true;

		// 遍历,把开着的、关闭了的图层存起来
    for (let i = inputs.length - 1; i >= 0; i--) {
      input = inputs[i];
      layer = this._getLayer(input.layerId).layer;

      if (input.checked) {
        addedLayers.push(layer);
      } else if (!input.checked) {
        removedLayers.push(layer);
      }
    }

    // Bugfix issue 2318: Should remove all old layers before readding new ones
		// 移除图层
    for (let i = 0; i < removedLayers.length; i++) {
      if (this._map.hasLayer(removedLayers[i])) {
        this._map.removeLayer(removedLayers[i]);
      }
    }
		// 添加图层
    for (let i = 0; i < addedLayers.length; i++) {
      if (!this._map.hasLayer(addedLayers[i])) {
        this._map.addLayer(addedLayers[i]);
      }
    }

    this._handlingClick = false;

    this._refocusOnMap();
  },

  _checkDisabledLayers() {
    const inputs = this._layerControlInputs,
      zoom = this._map.getZoom();
    let input, layer;

    for (let i = inputs.length - 1; i >= 0; i--) {
      input = inputs[i];
      layer = this._getLayer(input.layerId).layer;
      input.disabled = (layer.options.minZoom !== undefined && zoom < layer.options.minZoom) || (layer.options.maxZoom !== undefined && zoom > layer.options.maxZoom);
    }
  },

  _expandIfNotCollapsed() {
    if (this._map && !this.options.collapsed) {
      this.expand();
    }
    return this;
  },

  _expandSafely() {
    const section = this._section;
    DomEvent.on(section, "click", DomEvent.preventDefault);
    this.expand();
    setTimeout(() => {
      DomEvent.off(section, "click", DomEvent.preventDefault);
    });
  }
});

// @factory L.control.layers(baselayers?: Object, overlays?: Object, options?: Control.Layers options)
// Creates a layers control with the given layers. Base layers will be switched with radio buttons, while overlays will be switched with checkboxes. Note that all base layers should be passed in the base layers object, but only one should be added to the map during map instantiation.
export const layers = function (baseLayers, overlays, options) {
  return new Layers(baseLayers, overlays, options);
};

Zoom 缩放控件

import { Control } from "./Control";
import { Map } from "../map/Map";
import * as DomUtil from "../dom/DomUtil";
import * as DomEvent from "../dom/DomEvent";

/*
 * @class Control.Zoom
 * @aka L.Control.Zoom
 * @inherits Control
 *
 * 放大缩小控件,有zoom in 和 zoom out 两个按钮。默认会添加到地图中,除非设置 zoomControl 为 false
 */

export const Zoom = Control.extend({
  // @section
  // @aka Control.Zoom options
  options: {
    // @option position: String = 'topleft' 默认是 topleft
    // control的位置,可选值是 'topleft', 'topright', 'bottomleft' or 'bottomright'
    position: "topleft",

    // @option zoomInText: String = '<span aria-hidden="true">+</span>'
    // 放大按钮的 html 内容,默认是个加号
    zoomInText: '<span aria-hidden="true">+</span>',

    // @option zoomInTitle: String = 'Zoom in'
    // 放大按钮的提示文字 默认是 Zoom in
    zoomInTitle: "Zoom in",

    // @option zoomOutText: String = '<span aria-hidden="true">&#x2212;</span>'
    // 缩小按钮的 html 内容,默认是个减号
    zoomOutText: '<span aria-hidden="true">&#x2212;</span>',

    // @option zoomOutTitle: String = 'Zoom out'
    // 放大按钮的提示文字 默认是 Zoom out
    zoomOutTitle: "Zoom out"
  },

  /** 生成 control dom 容器 */
  onAdd(map) {
    const zoomName = "leaflet-control-zoom",
      container = DomUtil.create("div", `${zoomName} leaflet-bar`),
      options = this.options;

    // 创建按钮,绑定事件
    this._zoomInButton = this._createButton(options.zoomInText, options.zoomInTitle, `${zoomName}-in`, container, this._zoomIn);
    this._zoomOutButton = this._createButton(options.zoomOutText, options.zoomOutTitle, `${zoomName}-out`, container, this._zoomOut);

    this._updateDisabled();
    map.on("zoomend zoomlevelschange", this._updateDisabled, this);

    return container;
  },

  /** 解绑事件 */
  onRemove(map) {
    map.off("zoomend zoomlevelschange", this._updateDisabled, this);
  },

  /** 停用 */
  disable() {
    this._disabled = true;
    this._updateDisabled();
    return this;
  },

  /** 启用 */
  enable() {
    this._disabled = false;
    this._updateDisabled();
    return this;
  },

  /**放大地图 */
  _zoomIn(e) {
    if (!this._disabled && this._map._zoom < this._map.getMaxZoom()) {
      this._map.zoomIn(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1));
    }
  },

  /** 缩小地图 */
  _zoomOut(e) {
    if (!this._disabled && this._map._zoom > this._map.getMinZoom()) {
      this._map.zoomOut(this._map.options.zoomDelta * (e.shiftKey ? 3 : 1));
    }
  },

  /** 创建放大缩小按钮 a 元素 */
  _createButton(html, title, className, container, fn) {
    const link = DomUtil.create("a", className, container);
    link.innerHTML = html;
    link.href = "#";
    link.title = title;

    /*
     * Will force screen readers like VoiceOver to read this as "Zoom in - button"
     */
    link.setAttribute("role", "button");
    link.setAttribute("aria-label", title);

    DomEvent.disableClickPropagation(link);
    DomEvent.on(link, "click", DomEvent.stop);
    DomEvent.on(link, "click", fn, this);
    DomEvent.on(link, "click", this._refocusOnMap, this);

    return link;
  },

	/** 监测放大缩小按钮是否可用 */
  _updateDisabled() {
    const map = this._map,
      className = "leaflet-disabled";

    DomUtil.removeClass(this._zoomInButton, className);
    DomUtil.removeClass(this._zoomOutButton, className);
    this._zoomInButton.setAttribute("aria-disabled", "false"); // html无障碍属性
    this._zoomOutButton.setAttribute("aria-disabled", "false"); // html无障碍属性

    if (this._disabled || map._zoom === map.getMinZoom()) {
			// 小于等于最小zoom时候,缩小按钮不可用
      DomUtil.addClass(this._zoomOutButton, className);
      this._zoomOutButton.setAttribute("aria-disabled", "true"); // html无障碍属性
    }
    if (this._disabled || map._zoom === map.getMaxZoom()) {
			// 大于等于最大zoom时候,放大按钮不可用
      DomUtil.addClass(this._zoomInButton, className);
      this._zoomInButton.setAttribute("aria-disabled", "true"); // html无障碍属性
    }
  }
});

// @namespace Map
// @section Control options
// @option zoomControl: Boolean = true
// Whether a [zoom control](#control-zoom) is added to the map by default.
Map.mergeOptions({
  zoomControl: true
});

Map.addInitHook(function () {
  if (this.options.zoomControl) {
    // @section Controls
    // @property zoomControl: Control.Zoom
    // The default zoom control (only available if the
    // [`zoomControl` option](#map-zoomcontrol) was `true` when creating the map).
    this.zoomControl = new Zoom();
    this.addControl(this.zoomControl);
  }
});

// @namespace Control.Zoom
// @factory L.control.zoom(options: Control.Zoom options)
// Creates a zoom control
export const zoom = function (options) {
  return new Zoom(options);
};

Scale  比例尺控件

import {Control} from './Control';
import * as DomUtil from '../dom/DomUtil';

/*
 * @class Control.Scale
 * @aka L.Control.Scale
 * @inherits Control
 *
 * 比例尺 control,展示了当前地图中心点的比例尺,单位是 metric (m/km) 或者 imperial (mi/ft)  
 *
 * @example
 *
 * ```js
 * L.control.scale().addTo(map);
 * ```
 */

export const Scale = Control.extend({
  // @section
  // @aka Control.Scale options
  options: {
    // @option position: String = 'bottomleft',默认是 bottomleft
    // control的位置,可选值是 'topleft', 'topright', 'bottomleft' or 'bottomright'
    position: "bottomleft",

    // @option maxWidth: Number = 100
    // control的最大像素宽度,实际渲染出来的宽度会被动态地设置为整数距离对应的像素宽度 (e.g. 100, 200, 500).
    // 刻度会向下取整成 1 2 3 5 10 的整十倍数,规定比例尺只显示这几个数值
    maxWidth: 100,

    // @option metric: Boolean = True 默认是true
    // 是否显示米制刻度线 (m/km).
    metric: true,

    // @option imperial: Boolean = True
    // 是否显示英制刻度线 (mi/ft).
    imperial: true

    // @option updateWhenIdle: Boolean = false
    // If `true`, the control is updated on [`moveend`](#map-moveend), otherwise it's always up-to-date (updated on [`move`](#map-move)).
  },

  /** 生成 dom 元素容器 */
  onAdd(map) {
    const className = "leaflet-control-scale",
      container = DomUtil.create("div", className),
      options = this.options;

    this._addScales(options, `${className}-line`, container);

    // 注册map相关事件
    map.on(options.updateWhenIdle ? "moveend" : "move", this._update, this);
    map.whenReady(this._update, this);

    return container;
  },

  // 解绑事件
  onRemove(map) {
    map.off(this.options.updateWhenIdle ? "moveend" : "move", this._update, this);
  },

  /** 生成比例尺 dom 元素 */
  _addScales(options, className, container) {
    if (options.metric) {
      this._mScale = DomUtil.create("div", className, container);
    }
    if (options.imperial) {
      this._iScale = DomUtil.create("div", className, container);
    }
  },

  /** 跟新比例尺dom元素 */
  _update() {
    const map = this._map,
      y = map.getSize().y / 2;
    // 计算两个像素点(地图垂直居中处,水平距离为maxWidth)对应的空间距离,单位是米
    const maxMeters = map.distance(map.containerPointToLatLng([0, y]), map.containerPointToLatLng([this.options.maxWidth, y]));

    this._updateScales(maxMeters);
  },

  /** 更新米制刻度还是英制刻度 */
  _updateScales(maxMeters) {
    if (this.options.metric && maxMeters) {
      this._updateMetric(maxMeters);
    }
    if (this.options.imperial && maxMeters) {
      this._updateImperial(maxMeters);
    }
  },

  /** 更新米制刻度 */
  _updateMetric(maxMeters) {
    const meters = this._getRoundNum(maxMeters),
      // 大于1000,单位变为 km
      label = meters < 1000 ? `${meters} m` : `${meters / 1000} km`;

    this._updateScale(this._mScale, label, meters / maxMeters);
  },

  /** 更新英制刻度 */
  _updateImperial(maxMeters) {
    const maxFeet = maxMeters * 3.2808399;
    let maxMiles, miles, feet;

    if (maxFeet > 5280) {
      maxMiles = maxFeet / 5280;
      miles = this._getRoundNum(maxMiles);
      this._updateScale(this._iScale, `${miles} mi`, miles / maxMiles);
    } else {
      feet = this._getRoundNum(maxFeet);
      this._updateScale(this._iScale, `${feet} ft`, feet / maxFeet);
    }
  },

  /** 更新比例尺的宽度和刻度文字 */
  _updateScale(scale, text, ratio) {
		// 计算处正式距离对应的dom元素宽度
    scale.style.width = `${Math.round(this.options.maxWidth * ratio)}px`;
    scale.innerHTML = text;
  },

  /** 向下取整成 1 2 3 5 10 的整十倍数,规定比例尺只显示这几个数值 */
  _getRoundNum(num) {
    // 向下取整,获取长度(长度最小是1),减去一(最小是0),然后求10的n次方
    // 结果最小是1,最大是10的你、次方
    const pow10 = Math.pow(10, `${Math.floor(num)}`.length - 1);
    let d = num / pow10;

    // 依次判断 d 与 1 2 3 5 10 的大小,选取最大值
    d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1;

    // 结果是 1 2 3 5 10 的整十倍
    return pow10 * d;
  }
});


// @factory L.control.scale(options?: Control.Scale options)
// Creates an scale control with the given options.
export const scale = function (options) {
	return new Scale(options);
};

Attribution 右下角文本信息

import {Control} from './Control';
import {Map} from '../map/Map';
import * as Util from '../core/Util';
import * as DomEvent from '../dom/DomEvent';
import * as DomUtil from '../dom/DomUtil';
import Browser from '../core/Browser';

const ukrainianFlag = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="12" height="8" viewBox="0 0 12 8" class="leaflet-attribution-flag"><path fill="#4C7BE1" d="M0 0h12v4H0z"/><path fill="#FFD500" d="M0 4h12v3H0z"/><path fill="#E0BC00" d="M0 7h12v1H0z"/></svg>';


/*
 * @class Control.Attribution
 * @aka L.Control.Attribution
 * @inherits Control
 *
 * 这个控件可以用小一点的字体在map上展示介绍信息,默认会在地图上加载,除非手动设置 attributionControl 为 false,会自动调用 getAttribution 从 layer 中获取 attribution 信息。
 */

export const Attribution = Control.extend({
  // @section
  // @aka Control.Attribution options
  options: {
    // @option position: String = 'bottomright',默认是 bottomright
    // control的位置,可选值是 'topleft', 'topright', 'bottomleft' or 'bottomright'
    position: "bottomright",

    // @option prefix: String|false = 'Leaflet'
    // 下面的文字会作为前缀,不想要的话就把 prefix 设置为 false
    prefix: `<a href="https://leafletjs.com" title="A JavaScript library for interactive maps">${Browser.inlineSvg ? `${ukrainianFlag} ` : ""}Leaflet</a>`
  },

  initialize(options) {
    Util.setOptions(this, options);
		// 存放属性信息的对象,key就是属性内容,值是属性出现的次数
    this._attributions = {};
  },

	// 初始化 Control 内容
  onAdd(map) {
    map.attributionControl = this;
    this._container = DomUtil.create("div", "leaflet-control-attribution");
    DomEvent.disableClickPropagation(this._container);

    // TODO ugly, refactor
    for (const i in map._layers) {
      if (map._layers[i].getAttribution) {
        this.addAttribution(map._layers[i].getAttribution());
      }
    }

    this._update();

		// 注册事件,添加图层的时候,自动更新 attribution
    map.on("layeradd", this._addAttribution, this);

    return this._container;
  },

  onRemove(map) {
    map.off("layeradd", this._addAttribution, this);
  },

  _addAttribution(ev) {
    if (ev.layer.getAttribution) {
      this.addAttribution(ev.layer.getAttribution());
      ev.layer.once(
        "remove",
        function () {
          this.removeAttribution(ev.layer.getAttribution());
        },
        this
      );
    }
  },

  // @method setPrefix(prefix: String|false): this
  // The HTML text shown before the attributions. Pass `false` to disable.
  setPrefix(prefix) {
    this.options.prefix = prefix;
    this._update();
    return this;
  },

  // @method addAttribution(text: String): this
  // 把文字信息添加到 attribution 中
  addAttribution(text) {
    if (!text) {
      return this;
    }
    // 把文字存储起来,并记录相同文字出现的次数
    if (!this._attributions[text]) {
      this._attributions[text] = 0;
    }
    this._attributions[text]++;
    
		// 统一更新,生成dom
    this._update();

    return this;
  },

  // @method removeAttribution(text: String): this
  // 移除 attribution 文字
  removeAttribution(text) {
    if (!text) {
      return this;
    }

    if (this._attributions[text]) {
      this._attributions[text]--;
      this._update();
    }

    return this;
  },

	 /** 生成 dom 元素 */
  _update() {
    if (!this._map) {
      return;
    }

		// 属性信息文本
    const attribs = [];

    for (const i in this._attributions) {
      if (this._attributions[i]) {
        attribs.push(i);
      }
    }

    const prefixAndAttribs = [];

    if (this.options.prefix) {
      prefixAndAttribs.push(this.options.prefix);
    }
    if (attribs.length) {
			// 转成逗号分割的字符串
      prefixAndAttribs.push(attribs.join(", "));
    }

    this._container.innerHTML = prefixAndAttribs.join(' <span aria-hidden="true">|</span> ');
  }
});

// @namespace Map
// @section Control options
// @option attributionControl: Boolean = true
// 合并 attributionControl 配置项,默认是 true.
Map.mergeOptions({
	attributionControl: true
});

Map.addInitHook(function () {
	if (this.options.attributionControl) {
		new Attribution().addTo(this);
	}
});

// @namespace Control.Attribution
// @factory L.control.attribution(options: Control.Attribution options)
// Creates an attribution control.
export const attribution = function (options) {
	return new Attribution(options);
};