Расширение базовых классов

В Leaflet архитектура расширения основана на прототипном наследовании и системе классов, реализованной поверх JavaScript-конструкций. Основная идея заключается в том, что почти любой базовый компонент библиотеки может быть расширен через extend, сохраняя при этом поведение родительского класса и добавляя собственную логику.

Исторически в Leaflet использовалась система L.Class, обеспечивающая единый подход к созданию расширяемых сущностей. Каждый класс создаётся через вызов:

const MyClass = L.Class.extend({
    initialize: function (options) {
        this._options = options;
    }
});

Метод extend формирует новый конструктор, наследующий прототип родительского класса. Внутри такого объекта можно переопределять методы и добавлять новые свойства.

Ключевые элементы механизма:

  • initialize — аналог конструктора
  • extend — создание наследника
  • superclass — доступ к родительскому прототипу
  • options — единая система конфигурации

Расширение базовых сущностей L.Layer

Наиболее часто расширяемым классом выступает L.Layer. Любой визуальный элемент карты (маркер, слой, canvas-оверлей) наследуется от него.

const CustomLayer = L.Layer.extend({
    initialize: function (options) {
        L.Util.setOptions(this, options);
    },

    onAdd: function (map) {
        this._map = map;
        this._container = L.DomUtil.create('div', 'custom-layer');

        map.getPanes().overlayPane.appendChild(this._container);
        this._update();
    },

    onRemove: function () {
        L.DomUtil.remove(this._container);
        this._map = null;
    },

    _update: function () {
        if (!this._map) return;
        const latLng = this._map.getCenter();
        this._container.style.transform =
            `translate(${latLng.lat}px, ${latLng.lng}px)`;
    }
});

Методы onAdd и onRemove являются обязательными для корректной интеграции слоя с жизненным циклом карты.

Использование L.Util.setOptions

Функция L.Util.setOptions служит для объединения пользовательских параметров с дефолтными значениями класса:

const MyLayer = L.Layer.extend({
    options: {
        padding: 10,
        visible: true
    },

    initialize: function (options) {
        L.Util.setOptions(this, options);
    }
});

Такой подход обеспечивает единообразное управление конфигурацией и предотвращает дублирование логики.

Переопределение методов и вызов родительской логики

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

const MyMarker = L.Marker.extend({
    initialize: function (latlng, options) {
        L.Marker.prototype.initialize.call(this, latlng, options);
        this._customState = true;
    }
});

Подобная конструкция обеспечивает контроль над цепочкой вызовов без нарушения внутренней логики Leaflet.

Расширение L.Control

Контролы представляют элементы интерфейса карты. Их расширение требует реализации методов onAdd и onRemove.

const CustomControl = L.Control.extend({
    options: {
        position: 'topright'
    },

    onAdd: function (map) {
        const container = L.DomUtil.create('div', 'custom-control');
        container.innerHTML = 'Control';

        L.DomEvent.disableClickPropagation(container);
        return container;
    }
});

Особенность контролов заключается в том, что они не привязаны к географическим координатам и взаимодействуют исключительно с DOM.

Работа с событиями через L.Evented

Современная архитектура Leaflet использует L.Evented, предоставляющий систему событий.

const MyObject = L.Evented.extend({
    triggerUpdate: function () {
        this.fire('update', { time: Date.now() });
    }
});

Подписка на события реализуется стандартным образом:

obj.on('update', function (e) {
    console.log(e.time);
});

Создание собственных слоёв отображения

Расширение базовых классов позволяет создавать сложные визуальные сущности, включая canvas-слои:

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

        map.getPanes().overlayPane.appendChild(this._canvas);
        this._reset();
    },

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

    draw: function () {
        const ctx = this._ctx;
        ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);

        const center = this._map.latLngToContainerPoint(this._map.getCenter());
        ctx.fillRect(center.x, center.y, 10, 10);
    }
});

Здесь важна синхронизация географических координат с пиксельной системой отображения.

Расширение L.TileLayer

Класс тайлового слоя часто используется для подключения кастомных источников картографических данных:

const CustomTileLayer = L.TileLayer.extend({
    getTileUrl: function (coords) {
        return `https://tiles.example.com/${coords.z}/${coords.x}/${coords.y}.png`;
    }
});

Переопределение getTileUrl позволяет интегрировать любые тайловые сервисы без изменения ядра карты.

Использование миксинов

Leaflet поддерживает механизм миксинов для повторного использования функциональности:

const ZoomMixin = {
    zoomIn: function () {
        this._map.setZoom(this._map.getZoom() + 1);
    }
};

const MyControl = L.Control.extend({
    includes: ZoomMixin
});

Миксины особенно полезны для разделения логики между несколькими классами без жёсткого наследования.

Инкапсуляция и соглашения о коде

При расширении базовых классов применяется ряд соглашений:

  • Приватные методы обозначаются префиксом _
  • Публичные методы формируют API класса
  • Свойства конфигурации выносятся в options
  • DOM-элементы хранятся в полях вида _container, _canvas
  • Географическое состояние хранится отдельно от визуального

Такая структура снижает связанность компонентов и упрощает сопровождение.

Расширение GeoJSON-слоёв

GeoJSON-слои позволяют переопределять логику создания объектов:

const CustomGeoJSON = L.GeoJSON.extend({
    pointToLayer: function (feature, latlng) {
        return L.circleMarker(latlng, {
            radius: 8
        });
    },

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

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

Управление жизненным циклом расширённых классов

Каждый расширённый компонент должен корректно взаимодействовать с картой через стандартные точки входа:

  • инициализация через initialize
  • добавление через onAdd
  • удаление через onRemove
  • обновление через пользовательские методы

Несоблюдение этих контрактов приводит к утечкам памяти и некорректному отображению слоёв.

Работа с DOM внутри расширений

При создании кастомных элементов используется API L.DomUtil:

const el = L.DomUtil.create('div', 'custom-class');
L.DomUtil.addClass(el, 'active');
L.DomUtil.removeClass(el, 'hidden');

Для предотвращения нежелательного взаимодействия с картой применяется:

L.DomEvent.disableClickPropagation(el);

Это критично для интерактивных контролов и панелей управления.

Композиция расширяемых компонентов

Сложные системы строятся через комбинацию нескольких расширений:

  • слой данных (L.Layer)
  • визуализация (Canvas / SVG)
  • управление (L.Control)
  • события (L.Evented)

Такой подход формирует модульную архитектуру, где каждый компонент отвечает за строго определённую область ответственности.