Ionic Native/GoogleMaps プラグイン

  • 2
    Like
  • 0
    Comment

概要

IonicNative/GoogleMapsプラグインを使用すると、Android/iOSのネイティブAPIを使ったGoogleMapのビューをアプリ内に埋め込むことが出来ます。
IonicNative/GoogleMapsプラグインは、cordova-plugin-googlemapsをTypeScriptで使えるようにしたラッパープラグインです。


テーマは『普通に使えること』

開発の目的は、Google MapsのAndroidとiOSのネイティブAPIを『普通にJavaScriptから使えるようにすること』です。

『普通』とは何でしょうか。


デモ

まずはデモを見てみましょう。
表示されている地図の上のボタンや、上に重なって表示されるサイドメニューを『普通に』操作することが出来ます。


デモの解説

少し考えてみてください。

地図はAndroidのネイティブビューです。それはHTML要素ではないです。
でもサイドメニューが重なって表示されても『普通に』操作できます。
JavaScriptのコードでは、特に何もしていません。

menu_open.png


仕組み1

この『普通に』操作できるために、このプラグインではなんと『ブラウザの背景色を透明にして、地図ビューをブラウザの下に表示』させてます。


仕組み2

そして『クリックした位置が「ブラウザ」なのか「地図」なのかを判定している』のです。


JavaScriptで擬似コードを書くと、こんなことをやっています。
↓↓↓↓↓↓↓↓ (☉▢☉)うわっ!

// クリックされた位置がHTMLかMapか、全HTML要素を調べて、イベントを渡す
var clickLayer = document.createElement("div");
clickLayer.addEventListener("touchstart", function(e) {
   var allElements = document.getElementsByTagName("*");
   var hitElements = allElements.filter(function(node) {
     return isClickedOnNode(e, node);
   });
   if (hitElements.length > 0) {
     browser.onTouchStart(e);
   } else {
     map.onTouchStart(e);
   }
});

Hello World

Google Maps APIを使うためには、Googleにプロジェクトを登録して、APIキーを発行する必要があります


プロジェクトの作成

下記URLを開き、プロジェクトを作成するか、既存のプロジェクトを選択します。
https://console.developers.google.com/projectselector/apis/library


Google Maps APIを有効にする

Google Maps Android APIGoogle Maps SDK for iOSの両方を有効にします。

Screen Shot 2017-10-27 at 3.49.29 PM.png


APIキーの作成

[Create credentials]というボタンを押します。

Screen Shot 2017-10-27 at 3.52.25 PM.png


APIキーの取得

これでAPIキーが取得できるはずです。

Screen Shot 2017-10-27 at 3.55.36 PM.png


プロジェクトの作成とプラグインのインストール

ionicコマンドを使って、プロジェクトを作成します。

$> ionic start myApp sidemenu

そのあとに@ionic-native/google-mapsプラグインをインストールします。
必ずcoregoogle-mapsプラグインは同じバージョンを使うようにしてください。

$> npm install @ionic-native/core@latest @ionic-native/google-maps@latest

最後にcordova-plugin-googlemapsプラグインをインストールします。
インストール時に先程取得したAPIキーを指定します。AndroidとiOSと同じAPIキーを指定して大丈夫です。

$> ionic cordova plugin add cordova-plugin-googlemaps --variable API_KEY_FOR_ANDROID="(APIキー)" --variable API_KEY_FOR_IOS="(APIキー)"

src/app/app.module.ts

app.module.tsファイルを少し編集します。

import { BrowserModule } from '@angular/platform-browser';
   ...
// ↓この一行を追加
import { GoogleMaps } from "@ionic-native/google-maps";

@NgModule({
  ....
  providers: [
    StatusBar,
    SplashScreen,
    GoogleMaps,  // ←この一行を追加
    {provide: ErrorHandler, useClass: IonicErrorHandler}
  ]
})
export class AppModule {}

src/app/app.components.ts

app.components.tsファイルを少し編集します。

export class MyApp {
  @ViewChild(Nav) nav: Nav;
  rootPage: any;  // ←ここを設定しない

  constructor(public platform: Platform, public statusBar: StatusBar, public splashScreen: SplashScreen) {
    this.initializeApp();
  }

  initializeApp() {
    this.platform.ready().then(() => {
      this.rootPage = HomePage;  // ←platform.ready()の後に最初に表示するページを指定する

      this.statusBar.styleDefault();
      this.splashScreen.hide();
    });
   }
}

src/pages/home/home.html

今度はページのデザインです。どこに地図を表示したいかを決めます。

<ion-content padding>
  <h3>Ionic GoogleMaps Starter</h3>

  // ↓ <div>を追加して、map_canvasとしておく
  <div id="map_canvas">
    <button ion-button (click)="onButtonClick($event)">Start demo</button>
  </div>

</ion-content>

src/pages/home/home.scss

CSSは好きに定義できます。ただし背景色や背景画像は、全て無効化されます。
(ブラウザの下に地図を表示しているから)

page-home {
  #map_canvas {
    height: 90%;
  }
}

src/pages/home/home.ts

コードを書いてみましょう。

// ↓必要なモジュールをインポート
import { GoogleMaps, GoogleMap, GoogleMapsEvent } from '@ionic-native/google-maps';

export class HomePage {

  map: GoogleMap;  // クラス内でmapプロパティを作っておくと便利

  constructor() { }

  ionViewDidLoad() {
    this.loadMap();  // ionViewDidLoad()を待ってから地図を作成
  }


src/pages/home/home.ts

実際に地図を作成する部分はこんなコードです。簡単ですね。

  loadMap() {

    // 表示したい位置にある<div>のidを指定します
    this.map = GoogleMaps.create('map_canvas');

    // MAP_READYイベントが来るまで待ちます
    this.map.one(GoogleMapsEvent.MAP_READY).then(() => {
      console.log('地図が準備完了!');
    });
  }
}

最後に実行

Androidか、iOS端末をパソコンに接続して実行してみましょう。

$> ionic cordova run android


マーカー


マーカーの追加

地図にマーカーを追加するには、こんなコードです。
注意していただきたいのが、.then()でmarkerを受け取っていることです。
ネイティブ側にいろいろ処理をさせて、その結果、マーカーのIDを取得するので時間がかかります。
なのでthen()を使います。

this.map.addMarker({
  title: 'Ionic',
  icon: 'blue',
  animation: 'DROP',
  position: {
    lat: 43.0741904,
    lng: -89.3809802
  }
}).then((marker: Marker) => {

  marker.showInfoWindow();

});

マーカーが上から落ちてきますね。


iconプロパティ

マーカーのデザインの変更は、CSSとほぼ同じです。

  • blue, red, green, yellow... (157色が定義されています)
  • rgb(), rgba(), hsl(), hsla(), #RGB, #RGBA
  • ./assets/icon.png (jpg, gif, png)
  • http(s)://www.webserver.com/icon.png
  • cdvfile://.../icon.png
  • data:image/png;base64,iVBORw0KGgo...CC

https://github.com/mapsplugin/ionic-googlemaps-quickdemo/blob/master/src/pages/marker/marker.ts


イベント

イベントとは、「何か発生した」ということを伝える仕組みです。

予め『定義されているイベント』と『プロパティの変化を伝えるイベント』があります。

マーカーを例にして見ていきましょう。


『定義されている』イベント

マーカーのイベントは次のようなものがあります。

-MARKER_CLICK ・・・・・・ クリックされた
-MARKER_DRAG_START ・・・ マーカーのドラッグがスタート
-MARKER_DRAG ・・・・・・ ドラッグ中
-MARKER_DRAG_END ・・・・ ドロップされた
-INFO_OPEN ・・・・・・・ 情報ウィンドウが表示された
-INFO_CLOSE ・・・・・・  閉じた
-INFO_CLICK ・・・・・・  クリックされた
-INFO_LONG_CLICK ・・・  長押しされた


『プロパティの変化を伝える』イベント

このプラグインでは、addMarker(), addPolygon()などで作成されたオーバーレイは、
プロパティを持ちます。プロパティとは、「その状態を示す箱」です。
例えば、マーカーの現在位置は「position」というプロパティを参照すれば分かります。

(タンスに例えれば、マーカーがタンスで、プロパティはタンスの引き出しです)

プロパティに新しい値がセットされる時、必ず「(プロパティ名)_changed」というイベントが発生します。
これを受け取ることで、プロパティが変化したことを知ることが出来ます。

marker.addEventListener("position_changed").subscribe((params: any[]) => {
  console.log("position_changed");
});

イベントを受け取る

// 一回だけ受け取る
marker.addEventListenerOnce(GoogleMapsEvent.MARKER_CLICK).then();

// 上と同じ
marker.one(GoogleMapsEvent.MARKER_CLICK).then();
// 何回も受け取る
marker.addEventListener(GoogleMapsEvent.MARKER_CLICK).subscribe();

// 上と同じ
marker.on(GoogleMapsEvent.MARKER_CLICK).subscribe();

then()は一回だけのときに、subscribe()は複数回のときに使います。


イベントリスナーを再利用する

then()subscribe()に設定する関数を『イベントリスナー』といいます。
イベントリスナーは再利用することが出来ます。

marker1.on(GoogleMapsEvent.MARKER_CLICK).subscribe(this.onMarkerClick);
marker2.on(GoogleMapsEvent.MARKER_CLICK).subscribe(this.onMarkerClick);
marker3.on(GoogleMapsEvent.MARKER_CLICK).subscribe(this.onMarkerClick);
this.onMarkerClick(params:any[]) {
  console.log(params);
}

イベントリスナーに渡される情報

イベントリスナーには、「誰が」という情報と、「それに付随する情報」が渡されます。
例えば、MARKER_CLICKイベントの場合は、「マーカーの位置」と「マーカーのそのもの」です。

渡されるマーカーを使って、さらに操作することも出来ます。

this.onMarkerClick(params:any[]) {
  let position: LatLng = <LatLng>params[0];
  let marker: Marker = <Marker>params[1];
  marker.setTitle(position.toString());
  marker.showInfoWindow();
}

イベントの受け取りを解除する

// 特定のイベントリスナーだけ解除
marker.off(GoogleMapsEvent.MARKER_CLICK, this.onMarkerClick);

// MARKER_CLICKイベントに紐付いているイベントリスナー全てを解除
marker.off(GoogleMapsEvent.MARKER_CLICK);

// 全部解除
marker.off();

オレオレイベントを作る

コードを綺麗に分離するために、イベントを自分で作ることも出来ます。
このときに一緒にデータを渡すことも可能です。

marker1.on("oreoreEvent1").subscribe(this.oreoreEvent);
marker2.on("oreoreEvent2").subscribe(this.oreoreEvent);

marker1.trigger("oreoreEvent1", GoogleMapsAnimation.DROP);
marker2.trigger("oreoreEvent2", GoogleMapsAnimation.BOUNDS);
this.oreoreEvent2(params:any[]) {
  let marker: Marker = <Marker>params[0];
  let animation: String = params[1];
  marker.setAnimation(GoogleMapsAnimation.BOUNDS);
}

イベントのデモ

this.map.addMarker({
  position: { lat: 43.0741804, lng: -89.381 },
  title: "A",
  disableAutoPan: true
}).then(this.onMarkerAdded);

this.map.addMarker({
  position: { lat: 43.0741804, lng: -89.382 },
  title: "B",
  disableAutoPan: true
}).then(this.onMarkerAdded);

onMarkerAdded(marker: Marker) {
  marker.one(GoogleMapsEvent.MARKER_CLICK).then(() => {
    alert("Marker" + marker.getTitle() + " is clicked");
  });
}


その他できること

簡単にこのプラグインを使ってできることを見ていきましょう。


ポリライン

ポリラインの頂点の位置の配列を渡すだけです。

let AIR_PORTS = [
  HND_AIR_PORT,  HNL_AIR_PORT, SFO_AIR_PORT
];

this.map.addPolyline({
  points: AIR_PORTS,
  color: '#AA00FF',
  width: 10,
  geodesic: true,  // 地球の丸みに併せてカーブして描画
  clickable: true  // clickable = falseがデフォルト
}).then((polyline: Polyline) => {
  polyline.on(GoogleMapsEvent.POLYLINE_CLICK).subscribe(this.onPolylineClick);
});


ポリゴンの追加

ポリラインの外側の頂点の位置の配列を渡すだけです。
穴の空いたポリラインを作ることも出来ます。

let GORYOKAKU_POINTS: ILatLng[] = [
  {lat: 41.79883, lng: 140.75675},
  {lat: 41.799240000000005, lng: 140.75875000000002},
  {lat: 41.797650000000004, lng: 140.75905},
  
  {lat: 41.79909000000001, lng: 140.75465}
];

this.map.addPolygon({
  'points':GORYOKAKU_POINTS,
  'strokeColor' : '#AA00FF', 
  'fillColor' : '#00FFAA',
  'strokeWidth': 10
}.then((polygon: Polygon) => {
  ...
});


サークル

サークルの中心位置と、そこからの半径(m)を指定するだけです。

let center: ILatLng = {"lat": 32, "lng": -97};
let radius = 300;  // radius (meter)

this.map.addCircle({
  'center': center,
  'radius': radius,
  'strokeColor' : '#AA00FF',
  'strokeWidth': 5,
  'fillColor' : '#00880055'
}).then((circle: Circle) => {
  marker.on('position_changed').subscribe((params: any) => {
    let newValue: ILatLng = <ILatLng>params[1];
    let newRadius: number = 
        this.spherical.computeDistanceBetween(center, newValue);
    circle.setRadius(newRadius);
  });
});


グラウンドオーバーレイ

地図の上の特定の地域にだけ画像を貼り付けたい場合、グラウンドオーバーレイを使用します。

this.map.one(GoogleMapsEvent.MAP_READY).then(() => {
  return this.map.addGroundOverlay({
    'url': 'assets/imgs/newark_nj_1922.jpg',
    'bounds': bounds,
    'opacity': 0.5,
    'clickable': true  // default = false
  });
}).then((groundOverlay: GroundOverlay) => {

  // Catch the GROUND_OVERLAY_CLICK event
  groundOverlay.on(GoogleMapsEvent.GROUND_OVERLAY_CLICK).subscribe(() => {
    groundOverlay.setImage('assets/imgs/newark_nj_1922_2.jpg');
  });

});


タイルオーバーレイ

地図そのものを変えたいときは、タイルオーバーレイを使います。

this.map.addTileOverlay({
  getTile: (x: number, y: number, zoom: number) => {
    return "http://tile.stamen.com/watercolor/" +
            zoom + "/" + x + "/" + y + ".jpg";
  },

  // draw the debug information on tiles
  debug: false,

  opacity: 1.0
});


HtmlInfoWindow

HtmlInfoWindowは、情報ウィンドウ内にHtmlを表示することができます。
YoutubeでもCanvasでも何でも表示可能です。

let htmlInfoWindow = new HtmlInfoWindow();
let frame: HTMLElement = document.createElement('div');
frame.innerHTML = [
  '<h3>Hearst Castle</h3>',
  '<img src="assets/imgs/hearst_castle.jpg">'
].join("");
frame.getElementsByTagName("img")[0].addEventListener("click", () => {
  htmlInfoWindow.setBackgroundColor('red');
});
htmlInfoWindow.setContent(frame, {width: "280px", height: "330px"});

this.map.addMarker({
  position: {lat: 35.685208, lng: -121.168225},
  draggable: true,
  disableAutoPan: true
}).then((marker: Marker) => {
  marker.on(GoogleMapsEvent.MARKER_CLICK).subscribe(() => {
    htmlInfoWindow.open(marker);
  });
});


マーカークラスター

地図上に数百箇所もマーカーを表示するのは、ユーザーとして非常に使い勝手が悪いです。
そうゆうときはマーカークラスターをオススメします。
ズームインするときに表示される赤い枠は、オプションで消すことが可能です。

this.map.addMarkerCluster({
  markers: data,
  icons: [
    {
      min: 3, max: 9,
      url: "./assets/markercluster/small.png",
      label: { color: "white" }
    },
    {
      min: 10,
      url: "./assets/markercluster/large.png",
      label: { color: "white" }
    }
  ]
}).then((markerCluster: MarkerCluster) => {

  markerCluster.on(GoogleMapsEvent.MARKER_CLICK).subscribe((params) => {
    let marker: Marker = params[1];
    marker.setTitle(marker.get("name"));
    marker.setSnippet(marker.get("address"));
    marker.showInfoWindow();
  });

});


ジオコーディング(1)

場所の名称や住所から緯度経度に変換することを『ジオコーディング』といいます。

// 住所 -> 緯度経度
this.geocoder.geocode({
  "address": this.search_address
})
.then((results: GeocoderResult[]) => {
  console.log(results);

  return this.map1.addMarker({
    'position': results[0].position,
    'title':  JSON.stringify(results[0].position)
  })
})
.then(...)


ジオコーディング(2)

このプラグインでは、バッチリクエスト(まとめて処理)もサポートしています。

this.geocoder.geocode({
  // US Capital cities
  "address": [
    "Montgomery, AL, USA", "Juneau, AK, USA", ...
    "Madison, WI, USA", "Cheyenne, Wyoming, USA"
  ]
})
.then((mvcArray: BaseArrayClass<GeocoderResult[]>) => {

});

アメリカの50箇所(50州の州都名)をジオコーディングしている例です。
50箇所のジオコーディングを1.9秒で完了させています。


最後に

ここで紹介したのは、まだまだ一部に過ぎず、本当はもっと『普通に』すごいです。
もしGoogle Maps JavaScript API v3で地図を作ったことがある人なら、大概のことは可能です。
地理的な計算関数もある程度サポートしています。

もし良かったら使ってみてください。

※この記事はionic Los Angeles勉強会で発表したスライドを元に、日本語に作り変えて、加筆修正したものです。元記事はこちら。
https://docs.google.com/presentation/d/e/2PACX-1vScoho1ensbR4qCI9AIuQN55BZVvK73pAjI7sumDvW3CrxxHnrmpXWUjx2-8CpFibqU1EjLKCRhuthJ/pub?start=false&loop=false&delayms=3000