Goalist Developers Blog

angular-google-mapsでカスタムオーバーレイする

こんにちわ、 ゴーリスト開発のモリツグです。

AdobeがFlashのサポートを2020年に終了することを発表しましたね!
全国に50人はいるであろう私を含むFlex愛好家はついにこの時が来たかという気持ちだとおもいます。
Angularはそんな悲しみに暮れるFlex愛好家にはピッタリなのでお勧めです。

前回はAngular4でangular-google-mapsを使うという内容を書いたのですが、更にカスタムオーバーレイを行う必要に迫られた場合の解決方法を残しておこうと思います。

はじめに

カスタムオーバーレイとはこんな感じにGoogleMap上に画像などの任意のDOM要素を配置できる機能です。 JavaScriptで使う方法は公式説明に詳しくあるのですが、angular-google-mapsで使うにはどうしたらいいのか。そもそも出来るのか、というのが今回の内容になります。

手順

とりあえず先人の知識を探りますこのリンクがものすごく参考になりました。というかほぼこれで解決します。
前述のリンクは純粋にTypeScriptでカスタムオーバーレイする場合です。
私が調べた限りではangular-google-mapsはカスタムオーバーレイを何とかしてくれる方法を提供してくれていません。
したがって今回はイメージ的にはangular-google-mapsのAgmMapクラスが保持しているであろうネイティブなGoogleMapのインスタンスを引っ張りだし、先人の知識を利用する形になります。(正確にはGoogleMapsAPIWrapperというServiceが保持している)
このような方法をとるのでAngularのComponentをそのまま突っ込むようなことはできず、ネイティブなDOMの追加になります。
AgmMapにはmapReadyというイベントが用意されており、AgmMapの初期化が完了するとネイティブなGoogleMapのインスタンスをイベントの引数としてemitしてくれます。この飛んできたGoogleMapに対して先人の知識を適用すれば完了です。

ソースコードと注意点

plunkerのデモはこちらになります。
なんか緑っぽい拡大縮小されない異質な地図が表示されていると思います。 drawメソッドの中で計算すれば拡大縮小時にオーバーレイされた画像も良い感じに描画されます。
htmlとTypeScriptの関係個所だけを念のため貼っておきます。

<agm-map (省略) (mapReady)="onMapReady($event)">(省略)</agm-map>
  public onMapReady(nativeMap:GoogleMap):void {

    class USGSOverlay extends google.maps.OverlayView  { // user inner class to avoid error of 'google is not defined'
      //export class USGSOverlay extends google.maps.OverlayView { // exportするとここのgoogleがgoogle is not definedにつかまる
      image_;
      map_;
      div_;

      constructor(map) {
        super(); //  we need to call super

        this.image_  = 'https://developers.google.com/maps/documentation/' +
          'javascript/examples/full/images/talkeetna.png';
        this.map_ = map;

        // Define a property to hold the image's div. We'll
        // actually create this div upon receipt of the onAdd()
        // method so we'll leave it null for now.
        this.div_ = null;

        // Explicitly call setMap on this overlay.
        this.setMap(map);
      }
      /**
       * onAdd is called when the map's panes are ready and the overlay has been
       * added to the map.
       */
      onAdd(){
        const div = document.createElement('div');
        div.style.borderStyle = 'none';
        div.style.borderWidth = '0px';
        div.style.position = 'absolute';

        // Create the img element and attach it to the div.
        const img = document.createElement('img');
        img.src = this.image_;
        img.style.width = '100%';
        img.style.height = '100%';
        img.style.position = 'absolute';
        div.appendChild(img);

        this.div_ = div;

        // Add the element to the "overlayLayer" pane.
        const panes = this.getPanes();
        panes.overlayLayer.appendChild(div);
      };

      draw(){ // 今回は緯度経度を位置に変換することはせずいったん直接指定で地図を表示する
        // We use the south-west and north-east
        // coordinates of the overlay to peg it to the correct position and size.
        // To do this, we need to retrieve the projection from the overlay.
        const overlayProjection = this.getProjection();

        // Retrieve the south-west and north-east coordinates of this overlay
        // in LatLngs and convert them to pixel coordinates.
        // We'll use these coordinates to resize the div.
        //  const sw = overlayProjection.fromLatLngToDivPixel(this.bounds_.getSouthWest());
        //  const ne = overlayProjection.fromLatLngToDivPixel(this.bounds_.getNorthEast());
        const sw = {x:100, y:200};
        const ne = {x:200, y:100};


        // Resize the image's div to fit the indicated dimensions.
        const div = this.div_;
        div.style.left = sw.x + 'px';
        div.style.top = ne.y + 'px';
        div.style.width = (ne.x - sw.x) + 'px';
        div.style.height = (sw.y - ne.y) + 'px';
      };

      // The onRemove() method will be called automatically from the API if
      // we ever set the overlay's map property to 'null'.
      onRemove(){
        this.div_.parentNode.removeChild(this.div_);
        this.div_ = null;
      };
    }
    this.overlay = new USGSOverlay(nativeMap);
  }

以下のクラスはonMapReadyメソッドでやっているようにインナークラスにしないと動きません。

class USGSOverlay extends google.maps.OverlayView {
  //省略
}

多分タイミング的な問題だと思うのですが、exportして使おうとすると実行時にgoogle is not definedといわれて怒られます。 色々やってみましたが全然解決できなかったので凄腕の方が解決してくれるのを待つことにします。
また今回はとりあえずカスタムオーバーレイを使うまでを目的としたので緯度経度から描画位置への変換はせず決め打ちです。参考リンクのソースではその辺の変換もやっているようです。

まとめ

angular-google-mapsでカスタムオーバーレイする場合の手順を解説してみました。
ほとんどstackoverflowのコピーじゃないかという意見もありますが、全くもってその通りです。私が紹介したのは「AgmMapのmapReadyがネイティブなGoogleMapのインスタンスを引数にemitされる」という部分だけです。
他力本願でComponentをカスタムオーバーレイに突っ込めるような修正がangular-google-mapsに入ることを期待しています。