源码分析之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.mzph.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;物理规律建…