Angular+Openweathermap+Leafret.js+MongoDBで天気予報WEBアプリを作る ⑤サービスが持つプロパティの更新を検知する

#この記事でやること
　前回、[都市の登録]→[地図上にマーカー描画]の処理を実装したが、
　非同期の登録処理が終わる前に[画面遷移]→[マーカー描画]が走ることで、
　**一度別画面に遷移しないとマーカーが表示されない**状態になってしまった。

　※過去記事
　　[①Angular環境構築とひな形の生成](https://qiita.com/ryo_1050/items/71f0d5dd56b121c64b84)
　　[②コンポーネントとサービスの追加(登録画面)](https://qiita.com/ryo_1050/items/5ecf0ffa4c1b4d8cf940)
　　[③OpenweathermapのAPIを叩く](https://qiita.com/ryo_1050/items/578903a80653c92aa4cf)
　　[④Leafletで地図を表示する](https://qiita.com/ryo_1050/items/663eba44afe7ee6d95a1)

　今回はサービスの保持しているプロパティの更新を検知する仕組みを導入し、
　更新処理が終わったタイミングでマーカー描画を行う仕組みに変更する

##参考にした記事
[[Angular] サービスを使用してデータをコンポーネント間で共有する](https://qiita.com/ksh-fthr/items/e43dd37bff2e51e95a59)
※めっちゃ助かりました。ありがとうございます。


##Subjectを利用した更新通知を実装

　今回やりたいことは、「サービスが持つプロパティの変更」を「コンポーネント(ts)で検知して後続処理を行う」なので、
　任意のタイミングで他のコンポーネントにデータを送受信できる`Subject`を使って更新メッセージのやり取りを行う

###サービスの修正
　まずはサービス側に以下の修正を行う
　　・subjectのインポート
　　・更新メッセージを格納(通知)する領域を追加
　　・更新を通知する処理を実装

```typescript:src/app/city/city.service.ts
 :
/* 追加*/
import { Subject } from 'rxjs';
 :
 :
export class CityService {

  cities: City[];

  /* 通知の仕組み追加 */
  // notice$を更新するとsubscribeしている個所が発火する
  private subject = new Subject<string>();
  public notice$ = this.subject.asObservable();

  public addCity(city: City): void {
    var id: number;
    id = this.cities.length + 1;
    /* idを設定 */
    city.id = id;

    /* OpenweathermapのAPIを呼び出す */
    var apiEndPoint: string = this.environment.baseUrl
    + 'weather?q=' + city.name
    + '&appid=' + this.environment.appId;

    this.service.sendHttpRequest(apiEndPoint)
    .subscribe(res => {
      var weather = res.weather[0].description;
      // 天気を設定
      city.weather = weather;

      // 緯度経度を設定
      var coord = res.coord;
      city.location = {lat:coord.lat, lon:coord.lon};

      // 配列に追加
      this.cities.push(city);

      /* 登録処理完了時に更新メッセージ領域の文字列を更新*/
      this.updateNotice('city pushued')

      }, err => {
        console.log(err);
      }, () => {
      });
  }

  /* 更新メッセージを反映させる処理 */
  private updateNotice(update: string) {
    console.log('data update!');
    this.subject.next(update);
  }
}
```

これで登録処理`addCity`が完了したタイミングで通知を飛ばすことができる。

###コンポーネントの修正(subscribeする)
続いて`dashboard.component.ts`側に監視する仕組み`subscribe`を追加する

```typescript:src/app/dashboard/dashboard.component.ts
 :
/* 追加 */
import { Subscription } from 'rxjs';
 :
 :
export class DashboardComponent implements OnInit {
  cities: City[];
  /* 追加 */
  private subscription: Subscription;
 :
 :
  ngOnInit() {
    this.cities = this.cityService.getCities();

    /* 地図を生成 */
    this.leafletService.createMap();

    /* サービス側プロパティの更新(都市の追加)を検知 */
    this.subscription = this.cityService.notice$.subscribe(
      msg => {
        console.log('Data Updated Detection.');
        // マーカーを描画する
        this.addMarker();
      });
  }

  private addMarker() {
    /* 登録されている都市ごとにマーカーを作成*/
    this.cities.map(city => {
      this.leafletService.addMarker(city.name, city.weather, city.location.lat, city.location.lon);
    });
  }
```
###結果
　登録完了を待ってマーカー描画処理を行うことができた！

　登録処理の実行　　　　　→
　　　　↓　　　　　　　　　　↓
　画面遷移&マップ描画　　Openweather問い合わせ
　　　　↓　　　　　　　　　　↓
　　　　↓　　　　　　　　登録完了を通知(city.service内で更新メッセージ)
　　　　↓　　　　　　　　　　↓
　登録完了を検知　　　　　　↲
　　　　↓　　　　　　　　　
　マーカー描画

**画面**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315491/1202e21e-7100-16f6-6271-50dfd2b1b6aa.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315491/f69ae3ee-49de-36df-f298-42703ffac21f.png)


##(ついで①)マーカー画像をOpenweathermapのアイコンに変更
　Openweatherで取得した気象情報の中に`icon`があることに気づいたので、ついでに実装する

###`city.ts`クラスの修正
　アイコン画像URLを格納する領域を追加する

```typescript:src/app/city/city.ts
export class City {
    id: number;       /* 連番 */
    name: string;     /* 都市名 */
    weather: string;  /* 天気を取得して格納 */

    /* 緯度経度を格納するプロパティ*/
    location: 
      {lat: number, lon: number}

    /* アイコンURL */
    icon: string;
  }
```

###登録処理`city.service.ts`の修正
　APIレスポンスからアイコンURLを取得する処理を追加

```typescript:src/app/city/city.service.ts
 :
 :
  public addCity(city: City): void {
 :
 :
    /* OpenweathermapのAPIを呼び出す */
    var apiEndPoint: string = this.environment.baseUrl
    + 'weather?q=' + city.name
    + '&appid=' + this.environment.appId;

    this.service.sendHttpRequest(apiEndPoint)
    .subscribe(res => {
      var weather = res.weather[0];
      // 天気を設定
      city.weather = weather.description;

      // 緯度経度を設定
      var coord = res.coord;
      city.location = {lat:coord.lat, lon:coord.lon};

      // アイコンURLを取得
      city.icon = 'http://openweathermap.org/img/wn/' + 
weather.icon + '@2x.png'

      // 配列に追加
      this.cities.push(city);
      this.updateNotice('city pushued:' + city.name)
 :
 :
```

###マーカー描画処理の変更
　`leaflet.service.ts`のマーカー描画処理を変更し、Openweatherのアイコンを利用する

```typescript:src/app/common/leaflet.service.ts

import { Injectable } from '@angular/core';
import * as L from 'leaflet';

@Injectable({
  providedIn: 'root'
})
export class LeafletService {

  map:any;
  constructor() { }

  createMap() {
    this.map = L.map('map').setView([33.584261,130.403789], 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);
  }

  addMarker(name: string, weather: string, lat: number, lon: number, icon: string) {
    /* icon情報を追加 */
    var iconOpt = L.icon({
      iconUrl: icon,
      iconRetinaUrl: icon,
      iconSize: [75, 75],
      iconAnchor: [25, 25],
      popupAnchor: [0, 0],
    });
    // マーカー作成
    L.marker([lat, lon], { icon: iconOpt } )
    .bindPopup(name + ':\n' + weather)
    .addTo(this.map);
  }
}
```

###コンポーネントの修正
　マーカー描画処理にアイコンURLを渡すように変更

```typescript:src/app/dashboard/dashboard.component.ts
 :
 :
 :
  private addMarker() {
    /* 登録されている都市ごとにマーカーを作成*/
    this.cities.map(city => {
      this.leafletService.addMarker(city.name, city.weather, city.location.lat, city.location.lon, city.icon);
    });
  }
 :
```

**画面**
Openstreetmapのアイコンに変更できた！
![2019-10-13_15h07_26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315491/4f563614-81dc-a496-c8cd-bfe3a54b9bf5.png)

※よくみると曇りのアイコンになっている(赤枠部)

##(ついで②)すべてのマーカーが収まるように地図を動かす
マーカーの数が増えた場合にすべてが収まるように変更してみる
マーカーを配列として管理し、`getBounds()`すればいい感じになるようだ

###`leaflet.service.ts`クラスの修正
　これまではマーカーは作りっぱなし（mapにaddToするだけ)だったため、
　管理用の配列に格納し、`getBouds()`してmapに`fitBouds()`する

```src/app/common/leaflet.service.ts
import { Injectable } from '@angular/core';
import * as L from 'leaflet';

@Injectable({
  providedIn: 'root'
})
export class LeafletService {
  map:any;
  /* マーカー管理用プロパティ追加 */
  markers:L.FeatureGroup;

  constructor() {
    /* 初期化 */
    this.markers = new L.FeatureGroup();
   }

  createMap() {
    this.map = L.map('map').setView([33.584261,130.403789], 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);
  }

  addMarker(name: string, weather: string, lat: number, lon: number, icon: string) {
    // icon情報
    var iconOpt = L.icon({
      iconUrl: icon,
      iconRetinaUrl: icon,
      iconSize: [75, 75],
      iconAnchor: [25, 25],
      popupAnchor: [0, 0],
    });

    // マーカー作成
    this.markers.addLayer(
    L.marker( [lat, lon], { icon: iconOpt } ).bindPopup(name + ':\n' + weather)
    )

    this.markers.addTo(this.map);
    // マーカー追加後にフィット処理を呼び出す
    this.changeBounds();
  }

  changeBounds() {
    // ズームサイズが大きすぎる場合は下げる
    this.map.fitBounds(this.markers.getBounds());
    console.log(this.map.getZoom());
    if (this.map.getZoom() > 15) {
      this.map.setZoom(13);
    }
  }
}
```

###画面
いい感じになった
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315491/df7fe3e7-62f4-f05a-daf0-9856a7762917.png)

##次回の予定
　登録した都市をどこにも保存しておらず、F5で消えてしまうので
　次はMongoDBを用意して格納してみる


　※関連記事
　　[①Angular環境構築とひな形の生成](https://qiita.com/ryo_1050/items/71f0d5dd56b121c64b84)
　　[②コンポーネントとサービスの追加(登録画面)](https://qiita.com/ryo_1050/items/5ecf0ffa4c1b4d8cf940)
　　[③OpenweathermapのAPIを叩く](https://qiita.com/ryo_1050/items/578903a80653c92aa4cf)
　　[④Leafletで地図を表示する](https://qiita.com/ryo_1050/items/663eba44afe7ee6d95a1)