Установка библиотеки выполняется стандартным способом через 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();
}
}
}
Критически важно использовать 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-маркеры и геометрические слои.
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%;
}
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'
}
});
В 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);
});
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'
MapLibre использует WebGL, поэтому важно учитывать:
map.remove() при уничтожении
компонентаПри серверном рендеринге карта должна инициализироваться только на клиенте:
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 требует учёта нескольких факторов:
setDatarequestIdleCallback для тяжёлых
операцийПример отключения вращения:
this.map.dragRotate.disable();
this.map.touchZoomRotate.disableRotation();
В больших системах MapLibre интегрируется через:
Пример фасада:
@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'
});