IT评测·应用市场-qidao123.com技术社区

标题: 源码分析之Leaflet图层控制控件Control.Layers实现原理 [打印本页]

作者: 写过一篇    时间: 2025-4-4 23:02
标题: 源码分析之Leaflet图层控制控件Control.Layers实现原理
概述

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

  1. export var Layers = Control.extend({
  2.   options: {
  3.     collapsed: true,
  4.     position: "topright",
  5.     autoZIndex: true,
  6.     hideSingleBase: false,
  7.     sortLayers: false,
  8.     sortFunction: function (layerA, layerB, nameA, nameB) {
  9.       return nameA < nameB ? -1 : nameB < nameA ? 1 : 0;
  10.     },
  11.   },
  12.   initialize: function (baseLayers, overlays, options) {
  13.     Util.setOptions(this, options);
  14.     this._layerControlInputs = [];
  15.     this._layers = [];
  16.     this._lastZIndex = 0;
  17.     this._handlingClick = false;
  18.     this._preventClick = false;
  19.     for (var i in baseLayers) {
  20.       this._addLayer(baseLayers[i], i);
  21.     }
  22.     for (i in overlays) {
  23.       this._addLayer(overlays[i], i, true);
  24.     }
  25.   },
  26.   onAdd: function (map) {
  27.     this._initLayout();
  28.     this._update();
  29.     this._map = map;
  30.     map.on("zoomend", this._checkDisabledLayers, this);
  31.     for (var i = 0; i < this._layers.length; i++) {
  32.       this._layers[i].layer.on("add remove", this._onLayerChange, this);
  33.     }
  34.     return this._container;
  35.   },
  36.   addTo: function (map) {
  37.     Control.prototype.addTo.call(this, map);
  38.     return this._expandIfNotCollapsed();
  39.   },
  40.   onRemove: function () {
  41.     this._map.off("zoomend", this._checkDisabledLayers, this);
  42.     for (var i = 0; i < this._layers.length; i++) {
  43.       this._layers[i].layer.off("add remove", this._onLayerChange, this);
  44.     }
  45.   },
  46.   addBaseLayer: function (layer, name) {
  47.     this._addLayer(layer, name);
  48.     return this._map ? this._update() : this;
  49.   },
  50.   addOverlay: function (layer, name) {
  51.     this._addLayer(layer, name, true);
  52.     return this._map ? this._update() : this;
  53.   },
  54.   removeLayer: function (layer) {
  55.     layer.off("add remove", this._onLayerChange, this);
  56.     var obj = this._getLayer(Util.stamp(layer));
  57.     if (obj) {
  58.       this._layers.splice(this._layers.indexOf(obj), 1);
  59.     }
  60.     return this._map ? this._update() : this;
  61.   },
  62.   expand: function () {
  63.     DomUtil.addClass(this._container, "leaflet-control-layers-expanded");
  64.     this._section.style.height = null;
  65.     var acceptableHeight =
  66.       this._map.getSize().y - (this._container.offsetTop + 50);
  67.     if (acceptableHeight < this._section.clientHeight) {
  68.       DomUtil.addClass(this._section, "leaflet-control-layers-scrollbar");
  69.       this._section.style.height = acceptableHeight + "px";
  70.     } else {
  71.       DomUtil.removeClass(this._section, "leaflet-control-layers-scrollbar");
  72.     }
  73.     this._checkDisabledLayers();
  74.     return this;
  75.   },
  76.   collapse: function () {
  77.     DomUtil.removeClass(this._container, "leaflet-control-layers-expanded");
  78.     return this;
  79.   },
  80.   _initLayout: function () {
  81.     var className = "leaflet-control-layers",
  82.       container = (this._container = DomUtil.create("div", className)),
  83.       collapsed = this.options.collapsed;
  84.     container.setAttribute("aria-haspopup", true);
  85.     DomEvent.disableClickPropagation(container);
  86.     DomEvent.disableScrollPropagation(container);
  87.     var section = (this._section = DomUtil.create(
  88.       "section",
  89.       className + "-list"
  90.     ));
  91.     if (collapsed) {
  92.       this._map.on("click", this.collapse, this);
  93.       DomEvent.on(
  94.         container,
  95.         {
  96.           mouseenter: this._expandSafely,
  97.           mouseleave: this.collapse,
  98.         },
  99.         this
  100.       );
  101.     }
  102.     var link = (this._layersLink = DomUtil.create(
  103.       "a",
  104.       className + "-toggle",
  105.       container
  106.     ));
  107.     link.href = "#";
  108.     link.title = "Layers";
  109.     link.setAttribute("role", "button");
  110.     DomEvent.on(
  111.       link,
  112.       {
  113.         keydown: function (e) {
  114.           if (e.keyCode === 13) {
  115.             this._expandSafely();
  116.           }
  117.         },
  118.         click: function (e) {
  119.           DomEvent.preventDefault(e);
  120.           this._expandSafely();
  121.         },
  122.       },
  123.       this
  124.     );
  125.     if (!collapsed) {
  126.       this.expand();
  127.     }
  128.     this._baseLayersList = DomUtil.create("div", className + "-base", section);
  129.     this._separator = DomUtil.create("div", className + "-separator", section);
  130.     this._overlaysList = DomUtil.create(
  131.       "div",
  132.       className + "-overlays",
  133.       section
  134.     );
  135.     container.appendChild(section);
  136.   },
  137.   _getLayer: function (id) {
  138.     for (var i = 0; i < this._layers.length; i++) {
  139.       if (this._layers[i] && Util.stamp(this._layers[i].layer) === id) {
  140.         return this._layers[i];
  141.       }
  142.     }
  143.   },
  144.   _addLayer: function (layer, name, overlay) {
  145.     if (this._map) {
  146.       layer.on("add remove", this._onLayerChange, this);
  147.     }
  148.     this._layers.push({
  149.       layer: layer,
  150.       name: name,
  151.       overlay: overlay,
  152.     });
  153.     if (this.options.sortLayers) {
  154.       this._layers.sort(
  155.         Util.bind(function (a, b) {
  156.           return this.options.sortFunction(a.layer, b.layer, a.name, b.name);
  157.         }, this)
  158.       );
  159.     }
  160.     if (this.options.autoZIndex && layer.setZIndex) {
  161.       this._lastZIndex++;
  162.       layer.setZIndex(this._lastZIndex);
  163.     }
  164.     this._expandIfNotCollapsed();
  165.   },
  166.   _update: function () {
  167.     if (!this._container) {
  168.       return this;
  169.     }
  170.     DomUtil.empty(this._baseLayersList);
  171.     DomUtil.empty(this._overlaysList);
  172.     this._layerControlInputs = [];
  173.     var baseLayersPresent,
  174.       overlaysPresent,
  175.       i,
  176.       obj,
  177.       baseLayersCount = 0;
  178.     for (i = 0; i < this._layers.length; i++) {
  179.       obj = this._layers[i];
  180.       this._addItem(obj);
  181.       overlaysPresent = overlaysPresent || obj.overlay;
  182.       baseLayersPresent = baseLayersPresent || !obj.overlay;
  183.       baseLayersCount += !obj.overlay ? 1 : 0;
  184.     }
  185.     if (this.options.hideSingleBase) {
  186.       baseLayersPresent = baseLayersPresent && baseLayersCount > 1;
  187.       this._baseLayersList.style.display = baseLayersPresent ? "" : "none";
  188.     }
  189.     this._separator.style.display =
  190.       overlaysPresent && baseLayersPresent ? "" : "none";
  191.     return this;
  192.   },
  193.   _onLayerChange: function (e) {
  194.     if (!this._handlingClick) {
  195.       this._update();
  196.     }
  197.     var obj = this._getLayer(Util.stamp(e.target));
  198.     var type = obj.overlay
  199.       ? e.type === "add"
  200.         ? "overlayadd"
  201.         : "overlayremove"
  202.       : e.type === "add"
  203.       ? "baselayerchange"
  204.       : null;
  205.     if (type) {
  206.       this._map.fire(type, obj);
  207.     }
  208.   },
  209.   _createRadioElement: function (name, checked) {
  210.     var radioHtml =
  211.       '<input type="radio" class="leaflet-control-layers-selector" name="' +
  212.       name +
  213.       '"' +
  214.       (checked ? ' checked="checked"' : "") +
  215.       "/>";
  216.     var radioFragment = document.createElement("div");
  217.     radioFragment.innerHTML = radioHtml;
  218.     return radioFragment.firstChild;
  219.   },
  220.   _addItem: function (obj) {
  221.     var label = document.createElement("label"),
  222.       checked = this._map.hasLayer(obj.layer),
  223.       input;
  224.     if (obj.overlay) {
  225.       input = document.createElement("input");
  226.       input.type = "checkbox";
  227.       input.className = "leaflet-control-layers-selector";
  228.       input.defaultChecked = checked;
  229.     } else {
  230.       input = this._createRadioElement(
  231.         "leaflet-base-layers_" + Util.stamp(this),
  232.         checked
  233.       );
  234.     }
  235.     this._layerControlInputs.push(input);
  236.     input.layerId = Util.stamp(obj.layer);
  237.     DomEvent.on(input, "click", this._onInputClick, this);
  238.     var name = document.createElement("span");
  239.     name.innerHTML = " " + obj.name;
  240.     var holder = document.createElement("span");
  241.     label.appendChild(holder);
  242.     holder.appendChild(input);
  243.     holder.appendChild(name);
  244.     var container = obj.overlay ? this._overlaysList : this._baseLayersList;
  245.     container.appendChild(label);
  246.     this._checkDisabledLayers();
  247.     return label;
  248.   },
  249.   _onInputClick: function () {
  250.     if (this._preventClick) {
  251.       return;
  252.     }
  253.     var inputs = this._layerControlInputs,
  254.       input,
  255.       layer;
  256.     var addedLayers = [],
  257.       removedLayers = [];
  258.     this._handlingClick = true;
  259.     for (var i = inputs.length - 1; i >= 0; i--) {
  260.       input = inputs[i];
  261.       layer = this._getLayer(input.layerId).layer;
  262.       if (input.checked) {
  263.         addedLayers.push(layer);
  264.       } else if (!input.checked) {
  265.         removedLayers.push(layer);
  266.       }
  267.     }
  268.     for (i = 0; i < removedLayers.length; i++) {
  269.       if (this._map.hasLayer(removedLayers[i])) {
  270.         this._map.removeLayer(removedLayers[i]);
  271.       }
  272.     }
  273.     for (i = 0; i < addedLayers.length; i++) {
  274.       if (!this._map.hasLayer(addedLayers[i])) {
  275.         this._map.addLayer(addedLayers[i]);
  276.       }
  277.     }
  278.     this._handlingClick = false;
  279.     this._refocusOnMap();
  280.   },
  281.   _checkDisabledLayers: function () {
  282.     var inputs = this._layerControlInputs,
  283.       input,
  284.       layer,
  285.       zoom = this._map.getZoom();
  286.     for (var i = inputs.length - 1; i >= 0; i--) {
  287.       input = inputs[i];
  288.       layer = this._getLayer(input.layerId).layer;
  289.       input.disabled =
  290.         (layer.options.minZoom !== undefined && zoom < layer.options.minZoom) ||
  291.         (layer.options.maxZoom !== undefined && zoom > layer.options.maxZoom);
  292.     }
  293.   },
  294.   _expandIfNotCollapsed: function () {
  295.     if (this._map && !this.options.collapsed) {
  296.       this.expand();
  297.     }
  298.     return this;
  299.   },
  300.   _expandSafely: function () {
  301.     var section = this._section;
  302.     this._preventClick = true;
  303.     DomEvent.on(section, "click", DomEvent.preventDefault);
  304.     this.expand();
  305.     var that = this;
  306.     setTimeout(function () {
  307.       DomEvent.off(section, "click", DomEvent.preventDefault);
  308.       that._preventClick = false;
  309.     });
  310.   },
  311. });
  312. export var layers = function (baseLayers, overlays, options) {
  313.   return new Layers(baseLayers, overlays, options);
  314. };
复制代码
核心结构

  1. export var Layers = Control.extend({...});
  2. export var layers = function (...) { return new Layers(...) };
复制代码


配置项 (options)

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



初始化 (initialize)

  1. initialize: function (baseLayers, overlays, options) {
  2.   Util.setOptions(this, options);
  3.   this._layerControlInputs = [];  // 存储输入控件
  4.   this._layers = [];             // 存储图层信息
  5.   this._lastZIndex = 0;          // 自动 Z-Index 计数器
  6.   // 添加初始图层
  7.   for (var i in baseLayers) this._addLayer(baseLayers[i], i);
  8.   for (i in overlays) this._addLayer(overlays[i], i, true);
  9. }
复制代码
参数说明



生命周期方法

onAdd(map)

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

  1. onRemove: function () {
  2.   this._map.off("zoomend", this._checkDisabledLayers, this);
  3.   // 解绑图层事件
  4.   this._layers.forEach(layer => layer.layer.off("add remove", this._onLayerChange, this));
  5. }
复制代码

图层管理 API

添加/移除图层

  1. addBaseLayer(layer, name); // 添加基础图层
  2. addOverlay(layer, name); // 添加覆盖层
  3. removeLayer(layer); // 移除指定图层
复制代码
核心逻辑方法

  1. _addLayer(layer, name, overlay) {
  2.   // 处理排序、自动 Z-Index
  3.   if (this.options.sortLayers) this._layers.sort(...);
  4.   if (this.options.autoZIndex) layer.setZIndex(++this._lastZIndex);
  5. }
复制代码

DOM 与交互

控件布局 (_initLayout)

  1. _initLayout: function () {
  2.   // 创建 DOM 结构
  3.   this._container = DomUtil.create("div", "leaflet-control-layers");
  4.   this._section = DomUtil.create("section", "leaflet-control-layers-list");
  5.   // 折叠/展开交互逻辑
  6.   if (this.options.collapsed) {
  7.     this._map.on("click", this.collapse);
  8.     DomEvent.on(container, { mouseenter: this._expandSafely, mouseleave: this.collapse });
  9.   }
  10. }
复制代码
更新逻辑 (_update)

  1. _update: function () {
  2.   // 清空并重新渲染所有选项
  3.   DomUtil.empty(this._baseLayersList);
  4.   DomUtil.empty(this._overlaysList);
  5.   this._layers.forEach(layer => this._addItem(layer));
  6. }
复制代码

变乱处理

输入控件点击 (_onInputClick)

  1. _onInputClick: function () {
  2.   // 处理图层显隐切换
  3.   const addedLayers = [], removedLayers = [];
  4.   this._layerControlInputs.forEach(input => {
  5.     const layer = this._getLayer(input.layerId).layer;
  6.     input.checked ? addedLayers.push(layer) : removedLayers.push(layer);
  7.   });
  8.   // 更新地图图层
  9.   removedLayers.forEach(layer => this._map.removeLayer(layer));
  10.   addedLayers.forEach(layer => this._map.addLayer(layer));
  11. }
复制代码
图层状态变革 (_onLayerChange)

  1. _onLayerChange: function (e) {
  2.   // 触发 Leaflet 事件:baselayerchange / overlayadd / overlayremove
  3.   const obj = this._getLayer(Util.stamp(e.target));
  4.   const eventType = obj.overlay ?
  5.     (e.type === "add" ? "overlayadd" : "overlayremove") :
  6.     (e.type === "add" ? "baselayerchange" : null);
  7.   if (eventType) this._map.fire(eventType, obj);
  8. }
复制代码

辅助功能

动态禁用图层 (_checkDisabledLayers)

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

  1. _expandSafely: function () {
  2.   // 临时阻止点击事件防止误操作
  3.   this._preventClick = true;
  4.   DomEvent.on(this._section, "click", DomEvent.preventDefault);
  5.   setTimeout(() => {
  6.     DomEvent.off(this._section, "click", DomEvent.preventDefault);
  7.     this._preventClick = false;
  8.   }, 0);
  9. }
复制代码

计划亮点


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/) Powered by Discuz! X3.4