IT评测·应用市场-qidao123.com技术社区
标题:
源码分析之Leaflet图层控制控件Control.Layers实现原理
[打印本页]
作者:
写过一篇
时间:
2025-4-4 23:02
标题:
源码分析之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-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], 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-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企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/)
Powered by Discuz! X3.4