Angular интеграция

Интеграция CesiumJS в Angular-проект требует согласования двух архитектурных моделей: императивного WebGL-движка Cesium и реактивной компонентной системы Angular. Основная сложность заключается в управлении жизненным циклом сцены, синхронизации обновлений и корректной работе с DOM через Angular Renderer и ViewChild.

Установка базовых зависимостей:

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

Дополнительно требуется настройка статических ресурсов Cesium (workers, assets, widgets). В Angular CLI это решается через конфигурацию angular.json:

"assets": [
  "src/favicon.ico",
  "src/assets",
  {
    "glob": "**/*",
    "input": "node_modules/cesium/Build/Cesium",
    "output": "assets/cesium"
  }
]

В main.ts или в отдельном конфигурационном файле задаются базовые пути:

import * as Cesium from "cesium";

(Cesium as any).buildModuleUrl.setBaseUrl("/assets/cesium/");

Инициализация Viewer внутри Angular компонента

Основная точка интеграции — компонент, содержащий контейнер для WebGL-контекста.

<div #cesiumContainer class="cesium-container"></div>
import { Component, ElementRef, AfterViewInit, ViewChild, OnDestroy } from '@angular/core';
import * as Cesium from 'cesium';

@Component({
  selector: 'app-globe',
  templateUrl: './globe.component.html',
  styleUrls: ['./globe.component.scss']
})
export class GlobeComponent implements AfterViewInit, OnDestroy {
  @ViewChild('cesiumContainer', { static: true })
  container!: ElementRef<HTMLDivElement>;

  viewer!: Cesium.Viewer;

  ngAfterViewInit(): void {
    this.viewer = new Cesium.Viewer(this.container.nativeElement, {
      animation: false,
      timeline: false,
      geocoder: false,
      baseLayerPicker: true,
      terrainProvider: Cesium.createWorldTerrain()
    });
  }

  ngOnDestroy(): void {
    if (this.viewer && !this.viewer.isDestroyed()) {
      this.viewer.destroy();
    }
  }
}

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


Управление жизненным циклом и утечки памяти

Cesium создаёт WebGL-контекст, текстуры, буферы и воркеры. В Angular важно корректно освобождать ресурсы:

  • viewer.destroy() освобождает WebGL контекст
  • удаляются event listeners Cesium
  • очищаются таймеры и worker threads

Особое внимание требуется при использовании маршрутизации Angular. При переходе между страницами компонент уничтожается, но глобальные ссылки на viewer могут сохраняться в сервисах.


Инкапсуляция Cesium через сервис

Для масштабируемых приложений Viewer часто выносится в singleton-сервис.

import { Injectable } from '@angular/core';
import * as Cesium from 'cesium';

@Injectable({ providedIn: 'root' })
export class CesiumService {
  private viewer?: Cesium.Viewer;

  init(container: HTMLElement): Cesium.Viewer {
    if (this.viewer) {
      return this.viewer;
    }

    this.viewer = new Cesium.Viewer(container, {
      terrainProvider: Cesium.createWorldTerrain()
    });

    return this.viewer;
  }

  getViewer(): Cesium.Viewer | undefined {
    return this.viewer;
  }

  destroy(): void {
    if (this.viewer && !this.viewer.isDestroyed()) {
      this.viewer.destroy();
      this.viewer = undefined;
    }
  }
}

Такой подход позволяет разделять логику отображения и управление сценой.


Связь Cesium с реактивной моделью Angular

Cesium работает вне зоны Angular, поэтому изменения сцены не вызывают автоматического обновления шаблонов. Для синхронизации используется NgZone.

constructor(private zone: NgZone) {}

ngAfterViewInit(): void {
  this.zone.runOutsideAngular(() => {
    this.viewer = new Cesium.Viewer(this.container.nativeElement);

    this.viewer.camera.moveEnd.addEventListener(() => {
      const position = this.viewer.camera.positionWC;

      this.zone.run(() => {
        // обновление Angular состояния
        this.cameraPosition = position;
      });
    });
  });
}

Вынос Cesium-операций за пределы Angular зоны существенно снижает количество change detection циклов.


Работа с сущностями и реактивное обновление сцены

Добавление объектов в сцену часто привязывается к потокам RxJS:

import { BehaviorSubject } from 'rxjs';

private entities$ = new BehaviorSubject<Cesium.Entity[]>([]);

addPoint(lon: number, lat: number): void {
  const entity = new Cesium.Entity({
    position: Cesium.Cartesian3.fromDegrees(lon, lat),
    point: {
      pixelSize: 10,
      color: Cesium.Color.RED
    }
  });

  const current = this.entities$.value;
  this.entities$.next([...current, entity]);

  this.viewer.entities.add(entity);
}

Такой подход позволяет синхронизировать состояние сцены с состоянием приложения.


Масштабирование сцен и оптимизация рендеринга

Cesium чувствителен к количеству объектов и частоте обновлений. В Angular-приложениях типичные проблемы возникают при:

  • частом триггере change detection
  • массовом обновлении entities
  • постоянных пересозданиях Viewer

Оптимизация включает:

  • использование runOutsideAngular
  • батчинг изменений entities
  • применение Primitive вместо Entity для больших наборов данных
  • отключение неиспользуемых UI компонентов Cesium

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

При работе с маршрутизацией важно предотвращать повторную инициализацию WebGL:

{
  path: 'globe',
  component: GlobeComponent,
  runGuardsAndResolvers: 'always'
}

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


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

Cesium требует ручного уведомления о resize контейнера:

ngAfterViewInit(): void {
  this.viewer = new Cesium.Viewer(this.container.nativeElement);

  const resizeObserver = new ResizeObserver(() => {
    this.viewer.resize();
  });

  resizeObserver.observe(this.container.nativeElement);
}

Без этого Canvas может некорректно масштабироваться при изменении layout Angular.


Работа с тайлами и слоями

Cesium поддерживает различные источники карт:

this.viewer.imageryLayers.addImageryProvider(
  new Cesium.OpenStreetMapImageryProvider({
    url: 'https://a.tile.openstreetmap.org/'
  })
);

Добавление слоёв должно выполняться после полной инициализации Viewer, иначе возможно неконсистентное состояние слоя.


Асинхронная загрузка и lazy loading модулей

Cesium имеет значительный вес, поэтому часто применяется динамический импорт:

async ngAfterViewInit(): Promise<void> {
  const Cesium = await import('cesium');

  this.viewer = new Cesium.Viewer(this.container.nativeElement);
}

В связке с Angular lazy modules это снижает начальный размер бандла и ускоряет первичную загрузку приложения.


SSR и Angular Universal ограничения

Cesium зависит от WebGL и window, поэтому при серверном рендеринге требуется защита:

if (typeof window !== 'undefined') {
  this.viewer = new Cesium.Viewer(container);
}

На сервере Viewer не создаётся, а клиентская гидрация Angular должна учитывать отсутствие сцены.


Управление камерой через Angular API слой

Абстракция управления камерой часто выделяется в отдельный сервис:

setCameraHome(): void {
  this.viewer.camera.flyHome(1.5);
}

flyToCoordinates(lon: number, lat: number): void {
  this.viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(lon, lat, 15000)
  });
}

Это разделяет UI-логику Angular и низкоуровневые операции Cesium.


Обработка событий сцены

Cesium предоставляет богатую систему событий, которая интегрируется через Angular сервисы:

this.viewer.screenSpaceEventHandler.setInputAction((movement: any) => {
  const picked = this.viewer.scene.pick(movement.position);

  if (picked) {
    // обработка выбора объекта
  }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

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


Проблемы синхронизации состояния

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

  • состоянием Angular store
  • состоянием Cesium scene graph

Решение заключается в введении промежуточного слоя:

  • сервис состояния сцены
  • RxJS streams для событий камеры и entities
  • односторонняя синхронизация (Angular → Cesium или Cesium → Angular)

Двунаправленная синхронизация требует строгого контроля, чтобы избежать циклических обновлений.


Работа с большими данными и геопространственными слоями

При отображении миллионов точек стандартные Entity становятся неэффективными. Используются:

  • PointPrimitiveCollection
  • CustomDataSource
  • 3D Tiles

Интеграция в Angular требует минимизации перерисовок:

const collection = this.viewer.scene.primitives.add(
  new Cesium.PointPrimitiveCollection()
);

collection.add({
  position: Cesium.Cartesian3.fromDegrees(30, 50),
  color: Cesium.Color.YELLOW,
  pixelSize: 5
});

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

Контейнер Cesium должен занимать весь доступный viewport:

.cesium-container {
  width: 100%;
  height: 100vh;
  position: relative;
  overflow: hidden;
}

Angular layout напрямую влияет на размер WebGL canvas, поэтому любые flex/grid изменения должны учитывать пересчёт размеров сцены.


Типизация и работа с TypeScript

Cesium имеет частичную типизацию, поэтому часто требуется расширение типов:

declare module 'cesium' {
  interface Viewer {
    customState?: any;
  }
}

Это позволяет интегрировать Cesium в строгую архитектуру Angular без потери типобезопасности.