写过一篇 发表于 2025-4-4 23:02:56

源码分析之Leaflet图层控制控件Control.Layers实现原理

概述

本文将先容Leaflet库中最后一个组件,即图层控制组件 Control.Layers。
源码实现

export var Layers = Control.extend({
options: {
    collapsed: true,
    position: "topright",
    autoZIndex: true,
    hideSingleBase: false,
    sortLayers: false,
    sortFunction: function (layerA, layerB, nameA, nameB) {
      return nameA < nameB ? -1 : nameB < nameA ? 1 : 0;
    },
},
initialize: function (baseLayers, overlays, options) {
    Util.setOptions(this, options);

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

    for (var i in baseLayers) {
      this._addLayer(baseLayers, i);
    }

    for (i in overlays) {
      this._addLayer(overlays, i, true);
    }
},
onAdd: function (map) {
    this._initLayout();
    this._update();

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

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

    return this._container;
},
addTo: function (map) {
    Control.prototype.addTo.call(this, map);
    return this._expandIfNotCollapsed();
},
onRemove: function () {
    this._map.off("zoomend", this._checkDisabledLayers, this);

    for (var i = 0; i < this._layers.length; i++) {
      this._layers.layer.off("add remove", this._onLayerChange, this);
    }
},
addBaseLayer: function (layer, name) {
    this._addLayer(layer, name);
    return this._map ? this._update() : this;
},
addOverlay: function (layer, name) {
    this._addLayer(layer, name, true);
    return this._map ? this._update() : this;
},
removeLayer: function (layer) {
    layer.off("add remove", this._onLayerChange, this);

    var obj = this._getLayer(Util.stamp(layer));
    if (obj) {
      this._layers.splice(this._layers.indexOf(obj), 1);
    }
    return this._map ? this._update() : this;
},
expand: function () {
    DomUtil.addClass(this._container, "leaflet-control-layers-expanded");
    this._section.style.height = null;
    var acceptableHeight =
      this._map.getSize().y - (this._container.offsetTop + 50);
    if (acceptableHeight < this._section.clientHeight) {
      DomUtil.addClass(this._section, "leaflet-control-layers-scrollbar");
      this._section.style.height = acceptableHeight + "px";
    } else {
      DomUtil.removeClass(this._section, "leaflet-control-layers-scrollbar");
    }
    this._checkDisabledLayers();
    return this;
},
collapse: function () {
    DomUtil.removeClass(this._container, "leaflet-control-layers-expanded");
    return this;
},

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

    container.setAttribute("aria-haspopup", true);

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

    var section = (this._section = DomUtil.create(
      "section",
      className + "-list"
    ));

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

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

    var link = (this._layersLink = DomUtil.create(
      "a",
      className + "-toggle",
      container
    ));
    link.href = "#";
    link.title = "Layers";
    link.setAttribute("role", "button");

    DomEvent.on(
      link,
      {
      keydown: function (e) {
          if (e.keyCode === 13) {
            this._expandSafely();
          }
      },
      click: function (e) {
          DomEvent.preventDefault(e);
          this._expandSafely();
      },
      },
      this
    );

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

    this._baseLayersList = DomUtil.create("div", className + "-base", section);
    this._separator = DomUtil.create("div", className + "-separator", section);
    this._overlaysList = DomUtil.create(
      "div",
      className + "-overlays",
      section
    );

    container.appendChild(section);
},
_getLayer: function (id) {
    for (var i = 0; i < this._layers.length; i++) {
      if (this._layers && Util.stamp(this._layers.layer) === id) {
      return this._layers;
      }
    }
},
_addLayer: function (layer, name, overlay) {
    if (this._map) {
      layer.on("add remove", this._onLayerChange, this);
    }

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

    if (this.options.sortLayers) {
      this._layers.sort(
      Util.bind(function (a, b) {
          return this.options.sortFunction(a.layer, b.layer, a.name, b.name);
      }, this)
      );
    }

    if (this.options.autoZIndex && layer.setZIndex) {
      this._lastZIndex++;
      layer.setZIndex(this._lastZIndex);
    }

    this._expandIfNotCollapsed();
},

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

    DomUtil.empty(this._baseLayersList);
    DomUtil.empty(this._overlaysList);

    this._layerControlInputs = [];
    var baseLayersPresent,
      overlaysPresent,
      i,
      obj,
      baseLayersCount = 0;

    for (i = 0; i < this._layers.length; i++) {
      obj = this._layers;
      this._addItem(obj);
      overlaysPresent = overlaysPresent || obj.overlay;
      baseLayersPresent = baseLayersPresent || !obj.overlay;
      baseLayersCount += !obj.overlay ? 1 : 0;
    }

    if (this.options.hideSingleBase) {
      baseLayersPresent = baseLayersPresent && baseLayersCount > 1;
      this._baseLayersList.style.display = baseLayersPresent ? "" : "none";
    }

    this._separator.style.display =
      overlaysPresent && baseLayersPresent ? "" : "none";

    return this;
},
_onLayerChange: function (e) {
    if (!this._handlingClick) {
      this._update();
    }

    var obj = this._getLayer(Util.stamp(e.target));
    var type = obj.overlay
      ? e.type === "add"
      ? "overlayadd"
      : "overlayremove"
      : e.type === "add"
      ? "baselayerchange"
      : null;

    if (type) {
      this._map.fire(type, obj);
    }
},
_createRadioElement: function (name, checked) {
    var radioHtml =
      '<input type="radio" class="leaflet-control-layers-selector" name="' +
      name +
      '"' +
      (checked ? ' checked="checked"' : "") +
      "/>";

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

    return radioFragment.firstChild;
},
_addItem: function (obj) {
    var label = document.createElement("label"),
      checked = this._map.hasLayer(obj.layer),
      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);

    var name = document.createElement("span");
    name.innerHTML = " " + obj.name;

    var holder = document.createElement("span");

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

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

    this._checkDisabledLayers();
    return label;
},
_onInputClick: function () {
    if (this._preventClick) {
      return;
    }

    var inputs = this._layerControlInputs,
      input,
      layer;
    var addedLayers = [],
      removedLayers = [];

    this._handlingClick = true;

    for (var i = inputs.length - 1; i >= 0; i--) {
      input = inputs;
      layer = this._getLayer(input.layerId).layer;

      if (input.checked) {
      addedLayers.push(layer);
      } else if (!input.checked) {
      removedLayers.push(layer);
      }
    }
    for (i = 0; i < removedLayers.length; i++) {
      if (this._map.hasLayer(removedLayers)) {
      this._map.removeLayer(removedLayers);
      }
    }
    for (i = 0; i < addedLayers.length; i++) {
      if (!this._map.hasLayer(addedLayers)) {
      this._map.addLayer(addedLayers);
      }
    }

    this._handlingClick = false;

    this._refocusOnMap();
},

_checkDisabledLayers: function () {
    var inputs = this._layerControlInputs,
      input,
      layer,
      zoom = this._map.getZoom();

    for (var i = inputs.length - 1; i >= 0; i--) {
      input = inputs;
      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: function () {
    if (this._map && !this.options.collapsed) {
      this.expand();
    }
    return this;
},

_expandSafely: function () {
    var section = this._section;
    this._preventClick = true;
    DomEvent.on(section, "click", DomEvent.preventDefault);
    this.expand();
    var that = this;
    setTimeout(function () {
      DomEvent.off(section, "click", DomEvent.preventDefault);
      that._preventClick = false;
    });
},
});

export var layers = function (baseLayers, overlays, options) {
return new Layers(baseLayers, overlays, options);
};
核心结构

export var Layers = Control.extend({...});
export var layers = function (...) { return new Layers(...) };


[*]继续自 Leaflet 的 Control 基类,实现图层控制功能。
[*]提供工厂函数 layers() 简化实例化操作。
配置项 (options)

options: {
collapsed: true,          // 默认折叠
position: "topright",   // 控件位置
autoZIndex: true,         // 自动管理图层 z-index
hideSingleBase: false,    // 是否隐藏单一基础图层
sortLayers: false,      // 是否排序图层
sortFunction: (a, b) => { ... } // 自定义排序函数
}
关键配置说明



[*]autoZIndex
自动为新图层分配递增的 z-index,确保叠加次序精确。
[*]sortLayers
启用后按 sortFunction 排序图层(默认按名称字母排序)。
[*]hideSingleBase
当仅有一个底子图层时隐藏其选项地区。
初始化 (initialize)

initialize: function (baseLayers, overlays, options) {
Util.setOptions(this, options);
this._layerControlInputs = [];// 存储输入控件
this._layers = [];             // 存储图层信息
this._lastZIndex = 0;          // 自动 Z-Index 计数器

// 添加初始图层
for (var i in baseLayers) this._addLayer(baseLayers, i);
for (i in overlays) this._addLayer(overlays, i, true);
}
参数说明



[*]baseLayers: 底子图层对象(互斥,如地图类型切换)
[*]overlays: 覆盖层对象(可叠加,如标记层)
生命周期方法

onAdd(map)

onAdd: function (map) {
this._initLayout();   // 初始化 DOM 结构
this._update();         // 渲染图层选项
this._map = map;
map.on("zoomend", this._checkDisabledLayers, this); // 监听缩放事件
// 绑定图层变化事件
this._layers.forEach(layer => layer.layer.on("add remove", this._onLayerChange, this));
}
onRemove()

onRemove: function () {
this._map.off("zoomend", this._checkDisabledLayers, this);
// 解绑图层事件
this._layers.forEach(layer => layer.layer.off("add remove", this._onLayerChange, this));
}
图层管理 API

添加/移除图层

addBaseLayer(layer, name); // 添加基础图层
addOverlay(layer, name); // 添加覆盖层
removeLayer(layer); // 移除指定图层
核心逻辑方法

_addLayer(layer, name, overlay) {
// 处理排序、自动 Z-Index
if (this.options.sortLayers) this._layers.sort(...);
if (this.options.autoZIndex) layer.setZIndex(++this._lastZIndex);
}
DOM 与交互

控件布局 (_initLayout)

_initLayout: function () {
// 创建 DOM 结构
this._container = DomUtil.create("div", "leaflet-control-layers");
this._section = DomUtil.create("section", "leaflet-control-layers-list");

// 折叠/展开交互逻辑
if (this.options.collapsed) {
    this._map.on("click", this.collapse);
    DomEvent.on(container, { mouseenter: this._expandSafely, mouseleave: this.collapse });
}
}
更新逻辑 (_update)

_update: function () {
// 清空并重新渲染所有选项
DomUtil.empty(this._baseLayersList);
DomUtil.empty(this._overlaysList);
this._layers.forEach(layer => this._addItem(layer));
}
变乱处理

输入控件点击 (_onInputClick)

_onInputClick: function () {
// 处理图层显隐切换
const addedLayers = [], removedLayers = [];
this._layerControlInputs.forEach(input => {
    const layer = this._getLayer(input.layerId).layer;
    input.checked ? addedLayers.push(layer) : removedLayers.push(layer);
});
// 更新地图图层
removedLayers.forEach(layer => this._map.removeLayer(layer));
addedLayers.forEach(layer => this._map.addLayer(layer));
}
图层状态变革 (_onLayerChange)

_onLayerChange: function (e) {
// 触发 Leaflet 事件:baselayerchange / overlayadd / overlayremove
const obj = this._getLayer(Util.stamp(e.target));
const eventType = obj.overlay ?
    (e.type === "add" ? "overlayadd" : "overlayremove") :
    (e.type === "add" ? "baselayerchange" : null);
if (eventType) this._map.fire(eventType, obj);
}
辅助功能

动态禁用图层 (_checkDisabledLayers)

_checkDisabledLayers: function () {
// 根据当前缩放级别禁用不符合条件的图层
const zoom = this._map.getZoom();
this._layerControlInputs.forEach(input => {
    const layer = this._getLayer(input.layerId).layer;
    input.disabled =
      (layer.options.minZoom !== undefined && zoom < layer.options.minZoom) ||
      (layer.options.maxZoom !== undefined && zoom > layer.options.maxZoom);
});
}
安全睁开逻辑 (_expandSafely)

_expandSafely: function () {
// 临时阻止点击事件防止误操作
this._preventClick = true;
DomEvent.on(this._section, "click", DomEvent.preventDefault);
setTimeout(() => {
    DomEvent.off(this._section, "click", DomEvent.preventDefault);
    this._preventClick = false;
}, 0);
}
计划亮点


[*] 相应式计划

[*]自动根据地图缩放级别禁用不符合条件的图层选项
[*]睁开时动态盘算最大高度避免溢出视口

[*] 可扩展性

[*]支持通过 sortFunction 自定义图层排序规则
[*]允许通过 autoZIndex 自动管理图层叠加次序

[*] 无障碍支持

[*]使用 aria-haspopup 标记控件
[*]支持键盘操作(通过回车键睁开)


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 源码分析之Leaflet图层控制控件Control.Layers实现原理