Методы жизненного цикла оверлея

В Google Maps JavaScript API оверлеями называются объекты, отображаемые поверх карты и связанные с её системой координат. К ним относятся как встроенные элементы (маркеры, полигоны, полилинии), так и пользовательские оверлеи, создаваемые через наследование от класса OverlayView.

Для управления пользовательскими оверлеями API предоставляет набор специальных методов жизненного цикла. Эти методы автоматически вызываются картографическим движком при добавлении, обновлении и удалении объекта с карты.

Основные методы жизненного цикла:

  • onAdd()
  • draw()
  • onRemove()

Дополнительно важную роль играют методы:

  • setMap()
  • getMap()
  • getPanes()
  • getProjection()

Жизненный цикл пользовательского оверлея выглядит следующим образом:

Создание объекта
        ↓
setMap(map)
        ↓
onAdd()
        ↓
draw()
        ↓
draw() при изменениях карты
        ↓
setMap(null)
        ↓
onRemove()

Базовая архитектура OverlayView

Все пользовательские оверлеи наследуются от класса google.maps.OverlayView.

Пример каркаса класса:

class CustomOverlay extends google.maps.OverlayView {

    constructor(position) {
        super();
        this.position = position;
        this.div = null;
    }

    onAdd() {
    }

    draw() {
    }

    onRemove() {
    }
}

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

const overlay = new CustomOverlay({
    lat: 55.751244,
    lng: 37.618423
});

overlay.setMap(map);

В этот момент запускается жизненный цикл оверлея.


Метод onAdd()

Назначение

Метод onAdd() вызывается один раз после добавления оверлея на карту.

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

onAdd() {
    this.div = document.createElement("div");

    this.div.style.position = "absolute";
    this.div.innerHTML = "Мой оверлей";

    const panes = this.getPanes();

    panes.overlayLayer.appendChild(this.div);
}

Когда вызывается

Вызов происходит после выполнения:

overlay.setMap(map);

Однако вызов осуществляется не мгновенно из пользовательского кода, а внутренним механизмом API после регистрации объекта в карте.

Последовательность:

setMap(map)
      ↓
создание связей с картой
      ↓
onAdd()
      ↓
draw()

Получение панелей карты

Внутри onAdd() доступен метод:

this.getPanes()

Он возвращает объект с контейнерами отображения.

Пример:

const panes = this.getPanes();

Доступные панели:

panes.mapPane
panes.overlayLayer
panes.overlayMouseTarget
panes.floatPane
panes.markerLayer

Каждая панель имеет собственный уровень отображения.


Типичная логика метода

Чаще всего в onAdd() выполняются:

  • создание DOM-элемента;
  • назначение классов;
  • настройка обработчиков событий;
  • добавление элемента в нужную панель.

Пример:

onAdd() {

    this.div = document.createElement("div");

    this.div.className = "custom-label";

    this.div.addEventListener("click", () => {
        console.log("Клик по оверлею");
    });

    this.getPanes()
        .overlayMouseTarget
        .appendChild(this.div);
}

Метод draw()

Назначение

Метод draw() отвечает за позиционирование и обновление отображения оверлея.

Он вызывается:

  • после onAdd();
  • при изменении масштаба карты;
  • при перемещении карты;
  • при смене проекции;
  • при обновлении области просмотра.

Пример:

draw() {

    const projection = this.getProjection();

    const point = projection.fromLatLngToDivPixel(
        new google.maps.LatLng(
            this.position.lat,
            this.position.lng
        )
    );

    this.div.style.left = point.x + "px";
    this.div.style.top = point.y + "px";
}

Получение проекции карты

Ключевым элементом метода является объект проекции.

Получение:

const projection = this.getProjection();

Он позволяет преобразовывать координаты.

Например:

projection.fromLatLngToDivPixel()

или

projection.fromDivPixelToLatLng()

Преобразование координат

Координаты карты:

{
    lat: 55.751244,
    lng: 37.618423
}

преобразуются в экранные координаты:

const pixel = projection.fromLatLngToDivPixel(
    new google.maps.LatLng(
        55.751244,
        37.618423
    )
);

Результат:

{
    x: 350,
    y: 240
}

После этого можно позиционировать DOM-элемент:

this.div.style.left = `${pixel.x}px`;
this.div.style.top = `${pixel.y}px`;

Повторные вызовы draw()

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

Например:

Пользователь тянет карту
        ↓
draw()
draw()
draw()
draw()
draw()

Поэтому внутри него не рекомендуется:

  • создавать DOM-элементы;
  • выполнять тяжёлые вычисления;
  • обращаться к серверу;
  • создавать большое количество объектов.

Неправильно:

draw() {

    const div = document.createElement("div");

    document.body.appendChild(div);
}

Правильно:

draw() {

    const point = this.getProjection()
        .fromLatLngToDivPixel(this.position);

    this.div.style.left = point.x + "px";
    this.div.style.top = point.y + "px";
}

Управление видимостью

Внутри draw() часто реализуется скрытие элементов, выходящих за пределы области просмотра.

Пример:

draw() {

    const projection = this.getProjection();

    const point =
        projection.fromLatLngToDivPixel(this.position);

    if (!point) {
        this.div.style.display = "none";
        return;
    }

    this.div.style.display = "block";

    this.div.style.left = point.x + "px";
    this.div.style.top = point.y + "px";
}

Метод onRemove()

Назначение

Метод onRemove() вызывается при удалении оверлея с карты.

Обычно используется для очистки ресурсов.

Пример:

onRemove() {

    if (this.div) {

        this.div.remove();

        this.div = null;
    }
}

Когда вызывается

Метод запускается после:

overlay.setMap(null);

или при уничтожении карты.

Схема:

setMap(null)
       ↓
onRemove()
       ↓
удаление DOM-узлов

Освобождение памяти

В onRemove() рекомендуется:

  • удалять DOM-элементы;
  • удалять обработчики событий;
  • обнулять ссылки;
  • очищать коллекции.

Пример:

onRemove() {

    if (this.clickHandler) {

        this.div.removeEventListener(
            "click",
            this.clickHandler
        );

        this.clickHandler = null;
    }

    if (this.div) {

        this.div.remove();

        this.div = null;
    }
}

Метод setMap()

Назначение

Метод управляет привязкой оверлея к карте.

Добавление:

overlay.setMap(map);

Удаление:

overlay.setMap(null);

Внутреннее поведение

При добавлении:

setMap(map)
       ↓
onAdd()
       ↓
draw()

При удалении:

setMap(null)
       ↓
onRemove()

Таким образом, setMap() является точкой входа и выхода жизненного цикла.


Метод getMap()

Метод возвращает текущую карту.

const map = overlay.getMap();

Проверка наличия карты:

if (overlay.getMap()) {
    console.log("Оверлей активен");
}

После удаления:

overlay.setMap(null);

console.log(overlay.getMap());

Результат:

null

Взаимодействие методов жизненного цикла

Рассмотрим полный пример.

class LabelOverlay extends google.maps.OverlayView {

    constructor(position, text) {
        super();

        this.position = position;
        this.text = text;
        this.div = null;
    }

    onAdd() {

        this.div = document.createElement("div");

        this.div.className = "label";

        this.div.innerText = this.text;

        this.getPanes()
            .overlayMouseTarget
            .appendChild(this.div);
    }

    draw() {

        const projection =
            this.getProjection();

        const pixel =
            projection.fromLatLngToDivPixel(
                this.position
            );

        this.div.style.left =
            pixel.x + "px";

        this.div.style.top =
            pixel.y + "px";
    }

    onRemove() {

        if (this.div) {

            this.div.remove();

            this.div = null;
        }
    }
}

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

const label =
    new LabelOverlay(
        { lat: 55.751244, lng: 37.618423 },
        "Москва"
    );

label.setMap(map);

Последовательность вызовов:

constructor()
       ↓
setMap(map)
       ↓
onAdd()
       ↓
draw()
       ↓
draw()
       ↓
draw()
       ↓
setMap(null)
       ↓
onRemove()

Повторное добавление оверлея

После удаления объект можно снова использовать.

overlay.setMap(null);

Позже:

overlay.setMap(map);

Жизненный цикл будет запущен заново:

onAdd()
    ↓
draw()

Поэтому код в onAdd() должен корректно создавать все необходимые ресурсы каждый раз при повторном подключении.


Асинхронные операции и жизненный цикл

Иногда оверлей загружает данные асинхронно.

Пример:

async onAdd() {

    const response =
        await fetch("/data.json");

    const data =
        await response.json();

    this.div =
        document.createElement("div");

    this.div.textContent =
        data.title;

    this.getPanes()
        .overlayLayer
        .appendChild(this.div);
}

Следует учитывать возможность удаления объекта до завершения запроса.

Безопасный вариант:

async onAdd() {

    const response =
        await fetch("/data.json");

    if (!this.getMap()) {
        return;
    }

    const data =
        await response.json();

    this.div =
        document.createElement("div");

    this.div.textContent =
        data.title;

    this.getPanes()
        .overlayLayer
        .appendChild(this.div);
}

Частые ошибки при работе с жизненным циклом

Создание элементов внутри draw()

Неверно:

draw() {

    const div =
        document.createElement("div");

    document.body.appendChild(div);
}

Каждый вызов создаёт новый элемент.

Правильно:

onAdd() {

    this.div =
        document.createElement("div");
}

Отсутствие очистки в onRemove()

Неверно:

onRemove() {
}

DOM-элементы остаются в памяти.

Правильно:

onRemove() {

    this.div.remove();

    this.div = null;
}

Использование getProjection() до onAdd()

Неверно:

constructor() {

    const projection =
        this.getProjection();
}

Проекция ещё недоступна.

Правильно:

draw() {

    const projection =
        this.getProjection();
}

Выполнение тяжёлых операций в draw()

Неверно:

draw() {

    for (let i = 0; i < 1000000; i++) {
    }
}

Это ухудшает плавность карты.

Правильно:

draw() {

    this.div.style.left =
        this.pixelX + "px";

    this.div.style.top =
        this.pixelY + "px";
}

Рекомендации по организации кода

Метод onAdd()

  • создание DOM;
  • регистрация событий;
  • добавление в панели карты.

Метод draw()

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

Метод onRemove()

  • удаление DOM;
  • очистка обработчиков;
  • освобождение памяти.

Такое разделение обязанностей делает пользовательские оверлеи предсказуемыми, производительными и полностью совместимыми с внутренним механизмом рендеринга Google Maps JavaScript API.