Angular директивы

Архитектура взаимодействия HERE Technologies JavaScript API с Angular требует разделения ответственности между DOM-уровнем и логикой карты. Основной инструмент такого разделения — директивы Angular, позволяющие привязывать поведение карты к элементам шаблона без нарушения компонентной структуры приложения.

Карта HERE Maps строится вокруг объекта H.Map, который требует привязки к DOM-узлу. В Angular это приводит к необходимости получения ссылки на элемент через ElementRef и управления жизненным циклом через хуки OnInit и OnDestroy.


Базовая директива и инициализация карты

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

import { Directive, ElementRef, Input, OnInit, OnDestroy } from '@angular/core';

declare const H: any;

@Directive({
  selector: '[hereMap]'
})
export class HereMapDirective implements OnInit, OnDestroy {
  @Input() apiKey!: string;
  @Input() center: { lat: number; lng: number } = { lat: 0, lng: 0 };
  @Input() zoom: number = 10;

  private platform: any;
  private map?: any;

  constructor(private el: ElementRef) {}

  ngOnInit(): void {
    this.platform = new H.service.Platform({
      apikey: this.apiKey
    });

    const defaultLayers = this.platform.createDefaultLayers();

    this.map = new H.Map(
      this.el.nativeElement,
      defaultLayers.vector.normal.map,
      {
        center: this.center,
        zoom: this.zoom,
        pixelRatio: window.devicePixelRatio || 1
      }
    );

    const beh * avior = new H.mapevents.Behavior(
      new H.mapevents.MapEvents(this.map)
    );

    H.ui.UI.createDefault(this.map, defaultLayers);
  }

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

Ключевым моментом является привязка this.el.nativeElement к экземпляру карты. Это позволяет использовать директиву как декларативный контейнер.


Декларативное использование в шаблоне

Директива превращает HTML-элемент в полноценный контейнер карты:

<div
  hereMap
  [apiKey]="HERE_API_KEY"
  [center]="{ lat: 49.988, lng: 36.232 }"
  [zoom]="12"
  class="map-container"
></div>

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


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

Angular позволяет динамически изменять параметры карты через @Input, однако сама карта не реагирует автоматически на изменения. Требуется ручная синхронизация через ngOnChanges.

import { SimpleChanges, OnChanges } from '@angular/core';

export class HereMapDirective implements OnInit, OnDestroy, OnChanges {
  @Input() center!: { lat: number; lng: number };
  @Input() zoom!: number;

  private map: any;

  ngOnChanges(changes: SimpleChanges): void {
    if (!this.map) return;

    if (changes['center'] && this.center) {
      this.map.setCenter(this.center);
    }

    if (changes['zoom'] && this.zoom !== undefined) {
      this.map.setZoom(this.zoom);
    }
  }
}

Таким образом директива выступает адаптером между реактивной моделью Angular и императивным API HERE Maps.


Добавление маркеров через директивный слой

Работа с объектами на карте в HERE Maps осуществляется через H.map.Marker. Инкапсуляция этой логики в директиве позволяет управлять маркерами декларативно.

@Input() markers: { lat: number; lng: number }[] = [];

private uiMarkers: any[] = [];

private renderMarkers(): void {
  if (!this.map) return;

  this.uiMarkers.forEach(m => this.map.removeObject(m));
  this.uiMarkers = [];

  this.markers.forEach(point => {
    const marker = new H.map.Marker(point);
    this.map.addObject(marker);
    this.uiMarkers.push(marker);
  });
}

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


Работа с событиями карты

События карты интегрируются через систему H.mapevents. В Angular их удобно преобразовывать в EventEmitter, чтобы пробрасывать наружу.

import { Output, EventEmitter } from '@angular/core';

@Output() mapClicked = new EventEmitter<{ lat: number; lng: number }>();

private attachEvents(): void {
  this.map.addEventListener('tap', (evt: any) => {
    const coord = this.map.screenToGeo(
      evt.currentPointer.viewportX,
      evt.currentPointer.viewportY
    );

    this.mapClicked.emit({
      lat: coord.lat,
      lng: coord.lng
    });
  });
}

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


Изоляция логики через сервис карты

При усложнении логики директива начинает зависеть от сервисного слоя, который управляет платформой и общими ресурсами HERE Maps.

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

declare const H: any;

@Injectable({ providedIn: 'root' })
export class HereMapService {
  createPlatform(apiKey: string): any {
    return new H.service.Platform({ apikey: apiKey });
  }

  createMap(platform: any, element: HTMLElement, config: any): any {
    const layers = platform.createDefaultLayers();

    return new H.Map(element, layers.vector.normal.map, config);
  }
}

Директива при этом перестаёт отвечать за создание инфраструктуры и концентрируется на DOM-уровне.


Работа с жизненным циклом и утечками памяти

HERE Maps API требует явного освобождения ресурсов. В Angular это критично из-за повторного создания компонентов.

ngOnDestroy(): void {
  if (this.map) {
    this.map.removeObjects(this.map.getObjects());
    this.map.dispose();
  }
}

Игнорирование очистки приводит к накоплению WebGL-контекстов и утечкам памяти при навигации по маршрутам приложения.


Интеграция с реактивными потоками RxJS

В Angular часто требуется синхронизация карты с потоками данных. Директива может подписываться на Observable координат:

@Input() center$?: any;

private subscription: any;

ngOnInit(): void {
  if (this.center$) {
    this.subscription = this.center$.subscribe((coord: any) => {
      if (this.map) {
        this.map.setCenter(coord);
      }
    });
  }
}

ngOnDestroy(): void {
  if (this.subscription) {
    this.subscription.unsubscribe();
  }
}

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


Масштабирование директивы до системы управления картами

При расширении функциональности директива превращается в ядро управления картографическим слоем:

  • управление слоями (tile layers, traffic layer)
  • управление объектами (маршруты, полигоны)
  • синхронизация с роутингом Angular
  • кэширование состояния карты при навигации

Каждый слой добавляется как отдельная абстракция поверх H.Map, что позволяет сохранять предсказуемость поведения при росте сложности интерфейса.


Обработка resize и адаптивность

Карта HERE Maps не всегда автоматически реагирует на изменение размеров контейнера. В Angular это решается через ResizeObserver:

private resizeObserver?: ResizeObserver;

private initResizeObserver(): void {
  this.resizeObserver = new ResizeObserver(() => {
    if (this.map) {
      this.map.getViewPort().resize();
    }
  });

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

ngOnDestroy(): void {
  this.resizeObserver?.disconnect();
}

Это обеспечивает корректное отображение карты при изменении layout.


Привязка нескольких экземпляров карт

Директивная модель Angular позволяет использовать несколько карт на одной странице. В этом случае важно изолировать платформу или переиспользовать её:

const sharedPlatform = new H.service.Platform({ apikey: this.apiKey });

this.map = new H.Map(
  element,
  sharedPlatform.createDefaultLayers().vector.normal.map,
  config
);

Переиспользование платформы снижает накладные расходы на инициализацию и ускоряет рендеринг.


Гибридная модель управления: директива + компонент

При усложнении интерфейса директива используется только для инициализации карты, а логика переносится в компонент:

  • директива управляет H.Map
  • компонент управляет бизнес-логикой
  • сервис управляет данными и состоянием

Такая архитектура обеспечивает предсказуемую масштабируемость при работе с HERE Technologies API внутри больших Angular-приложений.