Пользовательские контролы в MapLibre GL JS реализуются через
интерфейс IControl, который определяет жизненный цикл
элемента управления и его взаимодействие с картой. Любой контрол — это
объект, который внедряется в DOM-контейнер карты и управляется через
стандартные методы.
Базовая идея: контрол не «рисуется» библиотекой напрямую, а создаётся разработчиком как DOM-компонент с управляемыми точками подключения.
Интерфейс включает:
onAdd(map) — вызывается при добавлении контрола на
картуonRemove() — вызывается при удалении контролаgetDefaultPosition?() — определяет позицию по
умолчаниюСтруктура базового контрола строится вокруг DOM-элемента, который
возвращается в onAdd.
class SimpleButtonControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group';
this._button = document.createElement('button');
this._button.type = 'button';
this._button.textContent = '+';
this._container.appendChild(this._button);
this._button.addEventListener('click', () => {
this._map.zoomIn();
});
return this._container;
}
onRemove() {
this._container.remove();
this._map = undefined;
}
}
Ключевой момент: MapLibre GL JS ожидает, что onAdd
вернёт DOM-узел, который будет встроен в контейнер карты.
Контролы размещаются в одной из стандартных зон:
top-lefttop-rightbottom-leftbottom-rightОпределение позиции через getDefaultPosition:
class ZoomLabelControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group';
this._label = document.createElement('div');
this._label.textContent = `Zoom: ${map.getZoom().toFixed(2)}`;
this._container.appendChild(this._label);
this._map.on('zoom', this._update.bind(this));
return this._container;
}
getDefaultPosition() {
return 'top-right';
}
_update() {
this._label.textContent = `Zoom: ${this._map.getZoom().toFixed(2)}`;
}
onRemove() {
this._map.off('zoom', this._update);
this._container.remove();
this._map = undefined;
}
}
Контрол часто подписывается на события карты:
movezoomrotateloadВажно корректно отписываться в onRemove, иначе возникают
утечки памяти.
Пример:
this._map.on('move', this._onMove);
this._map.on('zoom', this._onZoom);
Отписка:
this._map.off('move', this._onMove);
this._map.off('zoom', this._onZoom);
Контролы могут конфликтовать с перетаскиванием карты. Для предотвращения этого используется:
this._container.addEventListener('mousedown', (e) => {
e.stopPropagation();
});
Для сенсорных устройств:
this._container.addEventListener('touchstart', (e) => {
e.stopPropagation();
});
Это предотвращает захват жестов картой.
MapLibre GL JS использует стандартные классы:
maplibregl-ctrlmaplibregl-ctrl-groupmaplibregl-ctrl-iconПример CSS:
.maplibregl-ctrl.my-custom-control {
background: white;
border-radius: 6px;
padding: 6px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.my-custom-control button {
border: none;
background: transparent;
cursor: pointer;
font-size: 14px;
}
Контрол может содержать сложные UI-компоненты:
class SearchControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl';
this._input = document.createElement('input');
this._input.type = 'text';
this._input.placeholder = 'Search...';
this._container.appendChild(this._input);
this._input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
this._search(this._input.value);
}
});
return this._container;
}
_search(query) {
// пользовательская логика поиска
console.log('Searching:', query);
}
onRemove() {
this._container.remove();
this._map = undefined;
}
}
Контролы часто выступают как UI-слой над API запросами.
Пример обновления слоя карты:
async _fetchData() {
const res = await fetch('/api/points');
const data = await res.json();
this._map.getSource('points').setData(data);
}
Контрол может управлять источниками и слоями через
map.getSource() и map.addLayer().
Контрол может иметь переключатели режимов:
class ModeControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._btn = document.createElement('button');
this._btn.textContent = '3D OFF';
this._enabled = false;
this._btn.oncl ick = () => {
this._enabled = !this._enabled;
this._toggle();
};
this._container.appendChild(this._btn);
return this._container;
}
_toggle() {
this._map.easeTo({
pitch: this._enabled ? 60 : 0,
bearing: this._enabled ? -20 : 0
});
this._btn.textContent = this._enabled ? '3D ON' : '3D OFF';
}
onRemove() {
this._container.remove();
this._map = undefined;
}
}
Контролы добавляются независимо:
map.addControl(new SimpleButtonControl(), 'top-left');
map.addControl(new SearchControl(), 'top-right');
map.addControl(new ModeControl(), 'bottom-right');
Порядок добавления влияет на вертикальное расположение внутри одного угла.
Контрол может содержать вложенные компоненты:
Пример панели слоёв:
class LayersControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl layers-control';
['roads', 'buildings', 'water'].forEach(layer => {
const btn = document.createElement('button');
btn.textContent = layer;
btn.oncl ick = () => {
const visibility = map.getLayoutProperty(layer, 'visibility');
map.setLayoutProperty(
layer,
'visibility',
visibility === 'visible' ? 'none' : 'visible'
);
};
this._container.appendChild(btn);
});
return this._container;
}
onRemove() {
this._container.remove();
this._map = undefined;
}
}
В TypeScript используется интерфейс:
import type { Map, IControl } from 'maplibre-gl';
class TypedControl implements IControl {
private map?: Map;
private container!: HTMLElement;
onAdd(map: Map): HTMLElement {
this.map = map;
this.container = document.createElement('div');
return this.container;
}
onRemove(): void {
this.container.remove();
this.map = undefined;
}
getDefaultPosition?(): string {
return 'top-left';
}
}
Критические точки:
setInterval, setTimeout)Корректный onRemove обязан очищать всё:
onRemove() {
clearInterval(this._timer);
this._map.off('move', this._handler);
this._container.remove();
this._map = undefined;
}
Контролы должны поддерживать:
tabindexaria-labelkeydownПример:
this._button.setAttribute('aria-label', 'Zoom in');
this._button.tabIndex = 0;
this._button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
this._map.zoomIn();
}
});
Контролы автоматически остаются привязанными к контейнеру карты, но сложные UI могут требовать адаптации:
При росте сложности используется разделение:
Контрол становится точкой интеграции, а не монолитом логики.
Пример структуры:
control/
index.js
view.js
state.js
api.js
Такой подход критичен при построении аналитических или GIS-интерфейсов поверх MapLibre GL JS.