Angular

Архитектура Angular предполагает строгую компонентную модель, основанную на жизненном цикле компонентов, реактивности и управлении изменениями через zone.js. Leaflet, напротив, работает как императивная библиотека для работы с картами, напрямую манипулирующая DOM-элементами. Это создаёт характерную точку соприкосновения двух подходов: декларативного (Angular) и императивного (Leaflet).

Ключевой принцип интеграции заключается в изоляции Leaflet-карты внутри Angular-компонента с контролируемым доступом через сервисы и lifecycle hooks.


Установка и базовая конфигурация

Подключение Leaflet в Angular-проект требует установки самой библиотеки и типов:

npm install leaflet
npm install --save-dev @types/leaflet

Дополнительно в Angular проекте необходимо подключить стили Leaflet, так как без них карта не будет корректно отображаться:

"styles": [
  "node_modules/leaflet/dist/leaflet.css",
  "src/styles.css"
]

Либо через styles.scss:

@import "leaflet/dist/leaflet.css";

Базовый компонент карты

Структура Angular-компонента для Leaflet обычно включает:

  • ссылку на DOM-элемент карты
  • инициализацию карты в ngAfterViewInit
  • хранение экземпляра карты
  • управление слоями и объектами
import { Component, AfterViewInit, ElementRef, ViewChild } from '@angular/core';
import * as L from 'leaflet';

@Component({
  selector: 'app-map',
  template: `<div #map class="map-container"></div>`,
  styles: [`
    .map-container {
      height: 500px;
      width: 100%;
    }
  `]
})
export class MapComponent implements AfterViewInit {
  @ViewChild('map', { static: false }) mapElement!: ElementRef;

  private map!: L.Map;

  ngAfterViewInit(): void {
    this.initMap();
  }

  private initMap(): void {
    this.map = L.map(this.mapElement.nativeElement).setView([51.505, -0.09], 13);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: '&copy; OpenStreetMap contributors'
    }).addTo(this.map);
  }
}

Жизненный цикл Angular и Leaflet

Критический момент интеграции — выбор правильного lifecycle hook.

ngAfterViewInit

Используется для инициализации карты, так как DOM уже доступен.

ngOnDestroy

Обязателен для предотвращения утечек памяти:

ngOnDestroy(): void {
  if (this.map) {
    this.map.remove();
  }
}

Leaflet создаёт слушатели событий и DOM-слои, которые не очищаются автоматически Angular.


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

Leaflet оперирует слоями (layers), что идеально ложится на компонентную модель Angular.

Добавление маркера

const marker = L.marker([51.5, -0.09]).addTo(this.map);
marker.bindPopup('Точка интереса');

Динамическое управление маркерами

Часто используется хранение маркеров в массиве:

private markers: L.Marker[] = [];

addMarker(lat: number, lng: number): void {
  const marker = L.marker([lat, lng]).addTo(this.map);
  this.markers.push(marker);
}

Очистка слоя

clearMarkers(): void {
  this.markers.forEach(m => m.remove());
  this.markers = [];
}

Реактивная архитектура с сервисами

В Angular корректная практика — вынос логики карты в сервис, а компонент оставлять представлением.

MapService

import { Injectable } from '@angular/core';
import * as L from 'leaflet';

@Injectable({ providedIn: 'root' })
export class MapService {
  private map!: L.Map;

  initMap(container: HTMLElement): L.Map {
    this.map = L.map(container).setView([51.505, -0.09], 13);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: 'OpenStreetMap'
    }).addTo(this.map);

    return this.map;
  }

  getMap(): L.Map {
    return this.map;
  }
}

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

constructor(private mapService: MapService) {}

ngAfterViewInit(): void {
  this.mapService.initMap(this.mapElement.nativeElement);
}

Работа с изменениями данных (Change Detection)

Angular использует механизм change detection, который может конфликтовать с частыми обновлениями карты.

Проблема проявляется при:

  • анимации маркеров
  • потоковом обновлении координат
  • частом обновлении слоёв

Решение через NgZone

constructor(private ngZone: NgZone) {}

this.ngZone.runOutsideAngular(() => {
  this.map.on('move', () => {
    console.log('map moved');
  });
});

Это предотвращает лишние циклы обнаружения изменений.


Кастомные иконки

Leaflet позволяет задавать кастомные маркеры, что важно для Angular UI.

const icon = L.icon({
  iconUrl: 'assets/marker.png',
  iconSize: [25, 41],
  iconAnchor: [12, 41]
});

L.marker([51.5, -0.09], { icon }).addTo(this.map);

При использовании Angular CLI важно учитывать путь assets, который копируется в build.


Работа с геоданными (GeoJSON)

Leaflet поддерживает GeoJSON как основной формат описания геометрии.

const geoJsonFeature = {
  type: 'Feature',
  geometry: {
    type: 'Polygon',
    coordinates: [[
      [-0.1, 51.5],
      [-0.12, 51.5],
      [-0.12, 51.52],
      [-0.1, 51.52],
      [-0.1, 51.5]
    ]]
  }
};

L.geoJSON(geoJsonFeature).addTo(this.map);

GeoJSON часто используется совместно с Angular HttpClient:

this.http.get('/api/geo-data').subscribe(data => {
  L.geoJSON(data).addTo(this.map);
});

Интерактивные события

Leaflet предоставляет богатую систему событий:

  • click
  • zoom
  • move
  • mouseover

Пример обработки клика:

this.map.on('click', (e: L.LeafletMouseEvent) => {
  L.marker([e.latlng.lat, e.latlng.lng]).addTo(this.map);
});

При интеграции с Angular важно учитывать контекст зоны выполнения.


Оптимизация производительности

При большом количестве объектов на карте возникают проблемы:

1. Кластеризация маркеров

Используется библиотека leaflet.markercluster:

npm install leaflet.markercluster

2. Lazy rendering слоёв

Слои добавляются только при необходимости:

if (zoomLevel > 10) {
  layer.addTo(this.map);
}

3. Отключение лишних обновлений Angular

ChangeDetectionStrategy.OnPush

SSR и Angular Universal

Leaflet зависит от window и document, поэтому при серверном рендеринге возникают ошибки.

Решение — проверка платформы:

import { isPlatformBrowser } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';

constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

ngAfterViewInit(): void {
  if (isPlatformBrowser(this.platformId)) {
    this.initMap();
  }
}

Директивный подход

Вместо компонента можно использовать директиву для переиспользования карты:

@Directive({
  selector: '[appLeafletMap]'
})
export class LeafletMapDirective implements AfterViewInit {
  constructor(private el: ElementRef) {}

  ngAfterViewInit(): void {
    const map = L.map(this.el.nativeElement).setView([51.5, -0.09], 13);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
  }
}

Такой подход позволяет внедрять карту в любой контейнер.


Двусторонняя синхронизация состояния

Часто требуется синхронизировать состояние карты с Angular формами или store (NgRx).

Пример:

this.map.on('zoomend', () => {
  this.zoomLevel = this.map.getZoom();
});

И наоборот:

setZoom(level: number): void {
  this.map.setZoom(level);
}

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

Существуют обёртки, упрощающие интеграцию:

  • ngx-leaflet
  • angular-leaflet-directive (устаревшая)

Они добавляют декларативный стиль:

<div
  leaflet
  [leafletOptions]="options"
  [leafletLayers]="layers">
</div>

Однако они ограничивают гибкость чистого Leaflet API.


Архитектурные паттерны интеграции

1. Thin Component + Map Service

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

2. Feature-based maps

Каждая карта — отдельный feature module.

3. Event-driven map layer

Слои управляются через RxJS streams.


Работа с потоками данных RxJS

Leaflet хорошо сочетается с Observables:

this.location$.subscribe(loc => {
  this.map.setView([loc.lat, loc.lng]);
});

Для оптимизации:

this.location$
  .pipe(debounceTime(300))
  .subscribe(loc => this.updateMap(loc));

Управление памятью и утечки

Основные источники утечек:

  • незакрытые event listeners
  • не удалённые layers
  • подписки RxJS

Правильное удаление:

ngOnDestroy(): void {
  this.map.off();
  this.map.remove();
}

Работа с тайлами и кастомными источниками

Leaflet позволяет подключать любые tile servers:

L.tileLayer('https://tiles.example.com/{z}/{x}/{y}.png', {
  maxZoom: 18
}).addTo(this.map);

Также поддерживаются:

  • Mapbox
  • Stadia Maps
  • self-hosted tile servers

Обработка сложных визуализаций

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

  • heatmaps (через плагины)
  • choropleth maps
  • route visualization

Пример линии маршрута:

const route = L.polyline([
  [51.5, -0.1],
  [51.51, -0.11],
  [51.52, -0.12]
]).addTo(this.map);

Интеграция с формами Angular

Пример синхронизации координат:

this.form.valueChanges.subscribe(val => {
  this.map.setView([val.lat, val.lng]);
});

И обратная связь:

this.map.on('moveend', () => {
  const center = this.map.getCenter();
  this.form.patchValue({
    lat: center.lat,
    lng: center.lng
  });
});