源碼分析之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], i);}for (i in overlays) {this._addLayer(overlays[i], 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[i].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[i].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[i] && Util.stamp(this._layers[i].layer) === id) {return this._layers[i];}}},_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[i];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[i];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[i])) {this._map.removeLayer(removedLayers[i]);}}for (i = 0; i < addedLayers.length; i++) {if (!this._map.hasLayer(addedLayers[i])) {this._map.addLayer(addedLayers[i]);}}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[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: 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-indexhideSingleBase: 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], i);for (i in overlays) this._addLayer(overlays[i], 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-Indexif (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 / overlayremoveconst 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);
}

設計亮點

  1. 響應式設計

    • 自動根據地圖縮放級別禁用不符合條件的圖層選項
    • 展開時動態計算最大高度避免溢出視口
  2. 可擴展性

    • 支持通過 sortFunction 自定義圖層排序規則
    • 允許通過 autoZIndex 自動管理圖層疊加順序
  3. 無障礙支持

    • 使用 aria-haspopup 標記控件
    • 支持鍵盤操作(通過回車鍵展開)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/75830.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/75830.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/75830.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Element 使用 textarea 內容實現高度自適應

在 ElInput 組件的 type"textarea" 模式下&#xff0c;你可以使用 autosize 屬性來實現內容高度自適應。當沒有內容時默認顯示 3 行&#xff0c;當有內容時根據內容動態調整高度。 代碼&#xff1a; <el-form-item v-if"item.type textarea" :rules&…

Java技術生態前沿洞察:虛擬線程引領并發革命,框架創新賦能云原生時代

Java技術生態正迎來新一輪變革浪潮。虛擬線程的落地成為高并發編程范式轉折點&#xff0c;其極低資源開銷特性在電商秒殺場景中展現出3倍吞吐量提升&#xff0c;徹底改寫傳統線程模型性能邊界。Spring Boot 3.2原生支持虛擬線程&#xff0c;結合Observation API與HTTP客戶端優化…

leetcode每日一題:替換子串得到平衡字符串

引言 今天的每日一題原題是1863. 找出所有子集的異或總和再求和&#xff0c;比較水&#xff0c;直接對于集合中的每一個元素&#xff0c;都有取或者不取2種情況&#xff0c;直接遞歸進去求和即可。更換成前幾天遇到的更有意思的一題來寫這個每日一題。 題目 有一個只含有 Q,…

node-modules-inspector 可視化node_modules

1、node_modules 每個vue的項目都有很多的依賴&#xff0c;有的是dev的&#xff0c;有的是生產的。 2、使用命令pnpx node-modules-inspector pnpx node-modules-inspector 3、node_modules可視化 4、在線體驗 Node Modules Inspector 5、github地址 https://github.com/a…

【零基礎入門unity游戲開發——動畫篇】unity舊動畫系統Animation組件的使用

考慮到每個人基礎可能不一樣&#xff0c;且并不是所有人都有同時做2D、3D開發的需求&#xff0c;所以我把 【零基礎入門unity游戲開發】 分為成了C#篇、unity通用篇、unity3D篇、unity2D篇。 【C#篇】&#xff1a;主要講解C#的基礎語法&#xff0c;包括變量、數據類型、運算符、…

Linux網絡:數據鏈路層以太網

目錄 認識數據鏈路層關于以太網1. 基本概念2. 以太網幀格式3. MAC vs IP 認識數據鏈路層 數據鏈路層 位于物理層和網絡層之間&#xff0c;其作用是將源自物理層來的數據可靠地傳輸到相鄰節點的目標主機的網絡層&#xff0c;主要通過物理介質(如以太網&#xff0c;Wi-Fi等)將數…

SpringMVC與SpringCloud的區別

SpringMVC與SpringCloud的核心區別 功能定位 ? SpringMVC&#xff1a; 基于Spring框架的Web層開發模塊&#xff0c;采用MVC&#xff08;Model-View-Controller&#xff09;模式&#xff0c;專注于處理HTTP請求、路由分發&#xff08;如DispatcherServlet&#xff09;和視圖…

使用MATIO庫寫入MATLAB結構體(struct)數據的示例程序

使用MATIO庫寫入MATLAB結構體(struct)數據的示例程序 MATIO是一個用于讀寫MATLAB數據文件(.mat)的開源C庫。下面是一個完整的示例程序&#xff0c;展示如何使用MATIO庫創建一個包含結構體數據的MAT文件。 示例程序 #include <stdio.h> #include <stdlib.h> #inc…

SSE與Streamable HTTP的區別:協議與技術實現的深度對比

引言 在現代Web開發中&#xff0c;實時數據傳輸是許多應用的核心需求&#xff0c;從聊天應用到股票市場更新&#xff0c;從游戲服務器到AI模型通信。為了滿足這一需求&#xff0c;各種技術應運而生&#xff0c;其中Server-Sent Events (SSE)和Streamable HTTP是兩種重要的實時…

【Easylive】視頻在線人數統計系統實現詳解 WebSocket 及其在在線人數統計中的應用

【Easylive】項目常見問題解答&#xff08;自用&持續更新中…&#xff09; 匯總版 視頻在線人數統計系統實現詳解 1. 系統架構概述 您實現的是一個基于Redis的視頻在線人數統計系統&#xff0c;主要包含以下組件&#xff1a; 心跳上報接口&#xff1a;客戶端定期調用以…

Linux 高級命令與常見操作:文本處理、系統管理與網絡調試

下面是一份針對已經熟悉 Linux 基礎命令的用戶所整理的「高級命令與常見操作」筆記&#xff0c;涵蓋文本處理、系統管理、網絡調試與其他常用的進階技巧。請你審核下面筆記&#xff0c;檢查是否有過時的內容&#xff0c;如有請進行替換&#xff0c;確保其符合現代化需求&#x…

使用MFC ActiveX開發KingScada控件(OCX)

最近有個需求&#xff0c;要在KingScada上面開發一個控件。 原來是用的WinCC&#xff0c;WinCC本身是支持調用.net控件&#xff0c;就是winform控件的&#xff0c;winform控件開發簡單&#xff0c;相對功能也更豐富。奈何WinCC不是國產的。 話說KingScada&#xff0c;國產組態軟…

QScrollArea 內部滾動條 QSS 樣式失效問題及解決方案

在使用 Qt 進行 UI 開發時,我們經常希望通過 QSS(Qt Style Sheets)自定義控件的外觀,比如為 QScrollArea 的內部滾動條設置特定的樣式。然而,有開發者遇到了這樣的問題:在 UI 設計器中預覽 QSS 顯示效果正常,但程序運行時卻顯示為系統默認樣式。經過反復測試和調試,最終…

使用OpenSceneGraph生成3D數據格式文件

OpenSceneGraph (OSG) 提供了多種方式來生成和導出3D數據格式文件。以下是詳細的生成方法和示例代碼&#xff1a; 一、基本文件生成方法 1. 使用osgDB::writeNodeFile函數 這是最直接的生成方式&#xff0c;支持多種格式&#xff1a; #include <osgDB/WriteFile>osg:…

JMeter接口性能測試從入門到精通

前言&#xff1a; 本文主要介紹了如何利用jmter進行接口的性能測試 1.在測試計劃中添加線程組 1.1.線程組界面中元素含義 如果點擊循環次數為永遠&#xff1a; 2.添加HTTP取樣器 2.1.填寫登錄接口的各個參數 2.2.在線程組下面增加查看結果樹 請求成功的情況&#xff1a; 請求…

C++抽卡模擬器

近日在學校無聊&#xff0c;寫了個抽卡模擬器供大家娛樂。 代碼實現以下功能&#xff1a;抽卡界面&#xff0c;抽卡判定、動畫播放、存檔。 1.抽卡界面及判定 技術有限&#xff0c;不可能做的和原神一樣精致。代碼如下&#xff08;注&#xff1a;這不是完整代碼&#xff0c;…

詳解相機的內參和外參,以及內外參的標定方法

1 四個坐標系 要想深入搞清楚相機的內參和外參含義&#xff0c; 首先得清楚以下4個坐標系的定義&#xff1a; 世界坐標系&#xff1a; 名字看著很唬人&#xff0c; 其實沒什么大不了的&#xff0c; 這個就是你自己定義的某一個坐標系。 比如&#xff0c; 你把房間的某一個點定…

學透Spring Boot — 011. 一篇文章學會Spring Test

系列文章目錄 這是學透Spring Boot的第11篇文章。更多系列文章請關注 CSDN postnull 用戶的專欄 文章目錄 系列文章目錄Spring Test的依賴Spring Test的核心功能SpringBootTest 加載Spring上下文依賴注入有問題時Spring配置有問題時 WebMvcTest 測試Web層&#xff08;Controll…

Mysql 數據庫編程技術01

一、數據庫基礎 1.1 認識數據庫 為什么學習數據庫 瞬時數據&#xff1a;比如內存中的數據&#xff0c;是不能永久保存的。持久化數據&#xff1a;比如持久化至數據庫中或者文檔中&#xff0c;能夠長久保存。 數據庫是“按照數據結構來組織、存儲和管理數據的倉庫”。是一個長…

新一代AI架構實踐:數字大腦AI+智能調度MCP+領域執行APP的黃金金字塔體系

新一代AI架構實踐&#xff1a;數字大腦智能調度領域執行的黃金金字塔體系 一、架構本質的三層穿透性認知 1.1 核心范式轉變&#xff08;CPS理論升級&#xff09; 傳統算法架構&#xff1a;數據驅動 → 特征工程 → 模型訓練 → 業務應用 新一代AI架構&#xff1a;物理規律建…