Angular интеграция

Установка библиотеки выполняется стандартным способом через npm:

npm install maplibre-gl

Дополнительно требуется типизация:

npm install --save-dev @types/maplibre-gl

Подключение стилей обязательно, так как без них карта не отобразится корректно:

// angular.json
"styles": [
  "node_modules/maplibre-gl/dist/maplibre-gl.css",
  "src/styles.css"
]

Ключевой момент интеграции — корректная работа с DOM, поскольку MapLibre напрямую взаимодействует с HTML-элементом контейнера карты.


Создание компонента карты

Типовая архитектура в Angular предполагает изоляцию карты в отдельный компонент.

import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import maplibregl from 'maplibre-gl';

@Component({
  selector: 'app-map',
  template: `<div #mapContainer class="map-container"></div>`,
  styleUrls: ['./map.component.css']
})
export class MapComponent implements AfterViewInit, OnDestroy {
  @ViewChild('mapContainer', { static: false }) mapContainer!: ElementRef<HTMLDivElement>;

  map!: maplibregl.Map;

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

  private initializeMap(): void {
    this.map = new maplibregl.Map({
      container: this.mapContainer.nativeElement,
      style: 'https://demotiles.maplibre.org/style.json',
      center: [37.6173, 55.7558],
      zoom: 10
    });
  }

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

Работа с жизненным циклом Angular

Критически важно использовать AfterViewInit, поскольку DOM-элемент контейнера должен существовать до инициализации карты.

Использование OnDestroy предотвращает утечки памяти, освобождая ресурсы WebGL-контекста.


Стили контейнера карты

Без явных размеров контейнера карта не будет отображена.

.map-container {
  width: 100%;
  height: 500px;
}

При использовании flex-layout или grid необходимо гарантировать ненулевую высоту родительских элементов.


Интеграция через сервис

Для масштабируемых приложений логика карты выносится в сервис.

import { Injectable } from '@angular/core';
import maplibregl from 'maplibre-gl';

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

  createMap(container: HTMLElement): maplibregl.Map {
    this.map = new maplibregl.Map({
      container,
      style: 'https://demotiles.maplibre.org/style.json',
      center: [0, 0],
      zoom: 2
    });

    return this.map;
  }

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

Компонент становится тонким слоем представления:

ngAfterViewInit(): void {
  this.map = this.mapService.createMap(this.mapContainer.nativeElement);
}

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

MapLibre поддерживает DOM-маркеры и геометрические слои.

DOM-маркер

const marker = new maplibregl.Marker()
  .setLngLat([37.6173, 55.7558])
  .addTo(this.map);

Кастомный маркер

const el = document.createElement('div');
el.className = 'custom-marker';

new maplibregl.Marker(el)
  .setLngLat([37.62, 55.75])
  .addTo(this.map);
.custom-marker {
  width: 20px;
  height: 20px;
  background: red;
  border-radius: 50%;
}

Работа с источниками данных (Sources)

MapLibre использует архитектуру источников и слоёв.

this.map.on('load', () => {
  this.map.addSource('points', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          geometry: {
            type: 'Point',
            coordinates: [37.6173, 55.7558]
          },
          properties: {}
        }
      ]
    }
  });
});

Добавление слоя:

this.map.addLayer({
  id: 'points-layer',
  type: 'circle',
  source: 'points',
  paint: {
    'circle-radius': 6,
    'circle-color': '#1978c8'
  }
});

Реактивная интеграция с RxJS

В Angular часто требуется динамическое обновление данных карты.

this.dataService.points$.subscribe(points => {
  const source = this.map.getSource('points') as maplibregl.GeoJSONSource;
  source.setData({
    type: 'FeatureCollection',
    features: points
  });
});

Ключевая особенность — избегание полной перерисовки карты, обновляется только источник данных.


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

Типовые операции:

Перемещение карты

this.map.flyTo({
  center: [30.5, 50.5],
  zoom: 12
});

Установка масштаба

this.map.setZoom(8);

Поворот

this.map.rotateTo(45);

Обработка событий

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

this.map.on('click', (e) => {
  console.log(e.lngLat);
});

Подписка на движение карты:

this.map.on('move', () => {
  const center = this.map.getCenter();
  console.log(center);
});

Интеграция с Angular Change Detection

MapLibre работает вне зоны Angular, поэтому возможны ситуации, когда UI не обновляется.

Решение — использование NgZone:

import { NgZone } from '@angular/core';

constructor(private ngZone: NgZone) {}
this.map.on('click', (e) => {
  this.ngZone.run(() => {
    this.selectedPoint = e.lngLat;
  });
});

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


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

MapLibre поддерживает JSON-стили.

this.map = new maplibregl.Map({
  container: this.mapContainer.nativeElement,
  style: {
    version: 8,
    sources: {},
    layers: []
  }
});

Также можно подключать внешние стили:

style: 'https://tiles.example.com/style.json'

Работа с WebGL ограничениями

MapLibre использует WebGL, поэтому важно учитывать:

  • ограничение контекста браузера
  • утечки при множественной инициализации
  • необходимость map.remove() при уничтожении компонента
  • невозможность SSR-рендеринга без условной загрузки

Условная загрузка в SSR (Angular Universal)

При серверном рендеринге карта должна инициализироваться только на клиенте:

if (typeof window !== 'undefined') {
  this.initializeMap();
}

Либо через isPlatformBrowser:

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

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

Динамическая смена стилей

this.map.setStyle('https://new-style-url.com/style.json');

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

this.map.on('styledata', () => {
  this.addSourcesAndLayers();
});

Работа с изображениями и иконками

this.map.loadImage('/assets/icon.png', (error, image) => {
  if (image && !this.map.hasImage('custom-icon')) {
    this.map.addImage('custom-icon', image);
  }
});

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

this.map.addLayer({
  id: 'symbols',
  type: 'symbol',
  source: 'points',
  layout: {
    'icon-image': 'custom-icon'
  }
});

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

Практика работы с Angular и MapLibre требует учёта нескольких факторов:

  • минимизация вызовов setData
  • использование requestIdleCallback для тяжёлых операций
  • разделение слоёв по источникам
  • отключение ненужных анимаций

Пример отключения вращения:

this.map.dragRotate.disable();
this.map.touchZoomRotate.disableRotation();

Архитектура крупных приложений

В больших системах MapLibre интегрируется через:

  • MapFacade (уровень абстракции)
  • MapService (управление состоянием)
  • Feature modules (слои карты)
  • Store (NgRx или Signals)

Пример фасада:

@Injectable({ providedIn: 'root' })
export class MapFacade {
  constructor(private mapService: MapService) {}

  moveToUserLocation(coords: [number, number]) {
    this.mapService.getMap().flyTo({
      center: coords,
      zoom: 14
    });
  }
}

Обработка большого количества объектов

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

this.map.addSource('clusters', {
  type: 'geojson',
  data: geojson,
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50
});

Слои кластеров:

this.map.addLayer({
  id: 'clusters-layer',
  type: 'circle',
  source: 'clusters'
});