Пользовательские слои

В экосистеме Leaflet пользовательские слои представляют собой расширяемый механизм, позволяющий интегрировать произвольные визуальные и интерактивные элементы в карту поверх базовой тайловой подложки. Архитектура слоёв построена вокруг абстракции L.Layer, которая определяет единый жизненный цикл: добавление на карту, обновление состояния и удаление из DOM или Canvas.

Каждый слой в Leaflet наследуется от базового класса L.Layer и реализует два ключевых метода:

  • onAdd(map) — вызывается при добавлении слоя на карту
  • onRemove(map) — вызывается при удалении слоя

Дополнительно может использоваться метод getPane() для выбора контейнера рендеринга.

L.CustomLayer = L.Layer.extend({
    onAdd: function (map) {
        this._map = map;

        this._container = L.DomUtil.create('div', 'custom-layer');
        map.getPanes().overlayPane.appendChild(this._container);

        this._update();
        map.on('zoom viewreset move', this._update, this);
    },

    onRemove: function (map) {
        L.DomUtil.remove(this._container);
        map.off('zoom viewreset move', this._update, this);
    },

    _update: function () {
        // логика перерисовки слоя
    }
});

Такой подход позволяет интегрировать любые DOM- или Canvas-основанные визуализации.

Панели (panes) и порядок отрисовки

Система панелей определяет, в каком слое DOM будет размещён пользовательский элемент. Основные панели:

  • tilePane — тайлы
  • overlayPane — векторные слои
  • markerPane — маркеры
  • popupPane — всплывающие окна

Пользовательский слой чаще всего добавляется в overlayPane, что обеспечивает корректное наложение поверх тайлов, но ниже маркеров.

this._container = L.DomUtil.create('canvas', 'custom-canvas-layer');
map.getPanes().overlayPane.appendChild(this._container);

При необходимости можно создавать собственные панели:

map.createPane('customPane');
map.getPane('customPane').style.zIndex = 650;

Координатная привязка слоя

Для синхронизации слоя с картой используется преобразование географических координат в пиксельные через методы latLngToLayerPoint и layerPointToLatLng.

_update: function () {
    const topLeft = this._map.containerPointToLayerPoint([0, 0]);
    L.DomUtil.setPosition(this._container, topLeft);

    this._draw();
},

_draw: function () {
    const point = this._map.latLngToLayerPoint([55.75, 37.61]);

    this._ctx.fillStyle = 'red';
    this._ctx.beginPath();
    this._ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
    this._ctx.fill();
}

Этот механизм обеспечивает корректную работу при масштабировании и перемещении карты.

Canvas-слои

Canvas-слои применяются для визуализации большого количества геометрии. Они обеспечивают высокую производительность по сравнению с DOM-элементами.

L.CanvasLayer = L.Layer.extend({
    onAdd: function (map) {
        this._canvas = L.DomUtil.create('canvas', 'canvas-layer');
        this._ctx = this._canvas.getContext('2d');

        map.getPanes().overlayPane.appendChild(this._canvas);
        this._resize();
        map.on('resize zoom move', this._redraw, this);
    },

    _resize: function () {
        const size = this._map.getSize();
        this._canvas.width = size.x;
        this._canvas.height = size.y;
    },

    _redraw: function () {
        this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
    }
});

Canvas-слои часто применяются для тепловых карт, плотностей и динамических визуализаций.

SVG-слои и векторная графика

SVG-слои удобны для интерактивных векторных объектов, так как позволяют работать с DOM-событиями.

L.SVGLayer = L.Layer.extend({
    onAdd: function (map) {
        this._svg = L.SVG.create('svg');
        map.getPanes().overlayPane.appendChild(this._svg);

        this._group = L.SVG.create('g');
        this._svg.appendChild(this._group);

        map.on('zoom move', this._update, this);
    }
});

SVG предпочтителен при небольшом количестве объектов, где важна интерактивность и стилизация через CSS.

Интеграция с GeoJSON слоями

Механизм L.GeoJSON позволяет расширять поведение слоёв через параметры:

  • pointToLayer — кастомизация точечных объектов
  • style — стилизация геометрии
  • onEachFeature — обработка событий
L.geoJSON(data, {
    pointToLayer: function (feature, latlng) {
        return L.circleMarker(latlng, {
            radius: 6,
            color: 'blue'
        });
    },

    onEachFeature: function (feature, layer) {
        layer.on('click', function () {
            layer.bindPopup(feature.properties.name);
        });
    }
});

Этот механизм фактически является высокоуровневой формой пользовательских слоёв.

ImageOverlay и VideoOverlay как частные случаи

Пользовательские слои часто строятся поверх встроенных реализаций:

  • L.ImageOverlay — наложение изображения на координатный прямоугольник
  • L.VideoOverlay — привязка видео к географической области
const overlay = L.imageOverlay('map.png', [
    [55.7, 37.5],
    [55.8, 37.7]
]).addTo(map);

Эти слои уже содержат внутреннюю реализацию синхронизации координат и обновления.

Управление состоянием слоя

Слой может реагировать на события карты:

  • zoomstart, zoomend
  • move, moveend
  • resize
this._map.on({
    zoom: this._update,
    move: this._update,
    resize: this._resize
}, this);

Это позволяет реализовывать динамическое обновление данных и адаптивную отрисовку.

Производительность пользовательских слоёв

Ключевые аспекты оптимизации:

  • минимизация DOM-операций при move и zoom
  • использование requestAnimationFrame для перерисовки
  • кеширование координатных преобразований
  • использование Canvas вместо DOM при большом количестве объектов
  • ограничение частоты обработчиков событий
_update: function () {
    if (this._animating) return;
    this._animating = true;

    requestAnimationFrame(() => {
        this._animating = false;
        this._render();
    });
}

Расширение через композицию слоёв

Пользовательские слои часто строятся как композиции:

  • базовый контейнерный слой
  • набор подслоёв
  • отдельные интерактивные элементы
const CompositeLayer = L.Layer.extend({
    onAdd: function (map) {
        this._subLayer = new L.CustomLayer();
        map.addLayer(this._subLayer);
    }
});

Такой подход позволяет разделять ответственность и упрощать поддержку сложных визуализаций.