Angular でleaflet (OpenStreet Map) を使う

はじめに

Angular でleaflet を使う で Angularから leaflet(OpenStreet Map) を使う方法をまとめました。
今回はGeolocation API を利用して定期的に現在位置を leaflet の地図上にマッピングしていく Geo Tracker を作成してみます。

実際に動くアプリを Firebase にホストしています。
取得した位置情報に関してサーバー側に保存していませんが、ご自身の責任を以ってご利用ください。

fir-angular-showcase.web.app

geo track
geo track

leaflet で 位置情報のトラッキング

Geolocation API を利用することでブラウザだけで現在位置のトラッカーが簡単に作れます。

この記事のソースコードはGithubで公開しているので詰まったところを解説していきます。

github.com

leaflet install

leaflet の install についてはAngular でleaflet を使う を参考にしてください。

Geolocation API

現在位置を定期的に取得しsubscribeできるように geolocation.service.ts を作成します。
watchPosition という API を利用しており、位置に変化があるとその変更後の位置情報を取得できます。

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

@Injectable({
  providedIn: 'root'
})

export class GeolocationService {
  positionOptions: PositionOptions = {
    enableHighAccuracy: true,
    maximumAge: 0, // キャッシュは行わない
    timeout: 100000 // ミリ秒でタイムアウト値を指定する。結構取れないので長めに。
  };

  constructor() { }

  // https://github.com/angular/angular/issues/27597
  createWatchPosition(): Observable<Position> {
    return new Observable((observer) => {
      let watchId;

      const onSuccess: PositionCallback = (pos: Position) => {
        observer.next(pos);
      };

      const onError: PositionErrorCallback | any = (error) => {
        observer.error(error);
      };

      if ('geolocation' in navigator) {
        watchId = navigator.geolocation.watchPosition(onSuccess, onError, this.positionOptions);
      } else {
        onError('Geolocation not available');
      }

      return { unsubscribe() { navigator.geolocation.clearWatch(watchId); } };
    });
  }

}

Angular Component から利用する

位置情報が取れるまでの間には spinner を出しておくことにします。
geolocation.component.html の抜粋を掲載します。 initialize というboolフラグを用意し初期化処理が終わるまでクルクルしてもらいます。

  ~省略~
<div *ngIf="!initialize" style="width: 100%; height: 532.2px;" fxLayoutAlign="center center">
  <mat-spinner></mat-spinner>
</div>

<div class="map" #map id="map" style="width: 100%; height: 532.2px;"></div>
  ~省略~

geolocation.component.ts では先ほど作成した geolocation.service.ts の createWatchPosition から Position型でデータを取得しています。

debounceTime(1000) を設定して、データ通信頻度を調整しています。
ngAfterViewInit() から subscribeGeolocation していますが template の初期化とタイミングを合わせています。
subscribe しているので OnDestroy から unsubscribeしています。

import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
import { GeolocationService } from '../services/geolocation.service';

import { debounceTime } from 'rxjs/operators';

import * as L from 'leaflet';

  ~省略~

export class GeolocationComponent implements OnInit, AfterViewInit, OnDestroy {
  map: any;

  constructor(
    private geolocationService: GeolocationService,
  ) { }

  // ngOnInit を利用すると <div id="map"></div> が描画される前に map を参照してエラーになる場合がある
  ngAfterViewInit() {
    this.subscribeGeolocation();
  }

  // OnDestroy しないとページ遷移してもsubscribeしてしまうので必要なければDestroyした方が良い
  ngOnDestroy() {
    if (this.locationsSubscription) {
      this.locationsSubscription.unsubscribe();
    }
  }

  subscribeGeolocation() {
    this.locationsSubscription = this.geolocationService.createWatchPosition()
    .pipe(
      // debounceTimeを設定してデータ量を節約している
      debounceTime(1000)
    ).subscribe(
      (value: Position) => {
        //  value.coords の中に緯度経度とタイムスタンプが返ってくる
        this.geoLocation = {} as GeoLocations;
        this.geoLocation.lat = value.coords.latitude;
        this.geoLocation.lon = value.coords.longitude;
        this.geoLocation.timestamp = value.timestamp;

        // geoLocationArray に位置情報を格納する。見やすさを考慮して直近10点だけを利用する。
        if (this.geoLocationArray.length < 10) {
          this.geoLocationArray.push(this.geoLocation);
        } else {
          this.geoLocationArray.shift();
          this.geoLocationArray.push(this.geoLocation);
        }

        if (!this.initialize) {
          this.initMap(this.geoLocation.lat, this.geoLocation.lon);
        }

        this.setMaker();

      },
      error => {
        console.log('error:', error);
      }
    );
  }

  // 地図を初期化
  initMap(lat: number, lon: number) {
    this.map = L.map('map').setView([lat, lon], 13);
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(this.map);
    this.initialize = true;
  }

  // マーカーをプロットしpolylineでつなぐ
  setMaker() {
    const polylineArray = [];

    if (this.geoLocationArray) {
      for (let i = 0; this.geoLocationArray.length > i; i++ ) {
        const date = new Date(this.geoLocationArray[i].timestamp);

        L.marker(
          [this.geoLocationArray[i].lat, this.geoLocationArray[i].lon]
        ).bindPopup(
          '<b>Hello!!</b><br>  ' + (date.getMonth() + 1) + '/' + date.getDate()
        ).addTo(this.map);

        this.map.flyTo([this.geoLocationArray[i].lat, this.geoLocationArray[i].lon], 14, { duration: 2 });

        polylineArray.push( [this.geoLocationArray[i].lat, this.geoLocationArray[i].lon]);

        L.polyline([polylineArray], {color: '#FF0000', weight: 5, opacity: 0.6})
          .addTo(this.map);
      }
    }
  }

まとめ

Angular と leaflet で Geo Tracker を作ることができました。
この記事のソースコードはGithubで公開しています。

github.com