ニフクラ mobile backend(mBaaS)お役立ちブログ

スマホアプリ開発にニフクラ mobile backend(mBaaS)。アプリ開発に役立つ情報をおとどけ!

Monaca × NCMBで位置情報検索アプリを作る(その3:位置情報検索と地図表示)

f:id:mbaasdevrel:20210706222035j:plain

NCMBにはいくつかの機能がありますが、アプリと親和性の高い機能としてはプッシュ通知と位置情報機能が挙げられます。デバイスから位置情報を取得し、それをマッピングしたり、自分の今いる場所に近い情報を得たりする際にも位置情報検索が利用できます。

今回はMonacaとNCMBで位置情報検索を行うアプリを作成してみます。前回は位置情報データのインポートを実装しました。今回はそのデータを位置情報検索を使って取得し、地図上に表示します。

地図画面について

f:id:mbaasdevrel:20210706221954j:plain

地図画面 pages/map.html はMapboxを表示する画面です。HTMLはMapbox用に地図用のDOMを配置します。

<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner sliding">
        <div class="title">マップ</div>
        <div class="right">
          <a href="#" @click=${clear}><i class="f7-icons">trash</i></a>
        </div>
        </div>
    </div>
    <div class="page-content">
      <div id="map"></div>
    </div>
  </div>
</template>
<style>
  #map {
      width: 100%;
      height: 100%;
  }
</style>

次にJavaScriptでは、各関数で共有する変数を定義しておきます。

<script>
  export default async function (props, {$f7, $f7router, $on, $onBeforeMount, $onMounted, $onBeforeUnmount, $onUnmounted }) {
    // タップしたマーカーが入る
    const markers = [];
    // 駅のマーカーが入る
    const stationsMarkers = [];
    // MapBoxオブジェクト
    let map;
    // 省略
  }
</script>

画面表示時にMapboxを初期化

map.html を表示した際にMapboxを初期化します。その際にはアクセストークンが必要なので、これを js/app.js にて定義しておきます。

const mapboxAccessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';

そして画面表示時のイベントで初期化処理を行います。

// 画面表示前に実行されるイベント
$on('pageBeforeIn', async (e, page) => {
  // MapBoxを用意
  mapboxgl.accessToken = mapboxAccessToken;
  map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v11',
    center: [139.7454329, 35.6585805], // 東京タワーの位置情報
    zoom: 14
  });
  // 省略
});

初期化に続けて、地図をタップした際のイベントを定義します。これはマーカーを立てる処理に加えて、1つのマーカーであれば距離検索、2つのマーカーであれば範囲検索を行う関数を実行します。

// 地図をクリックした場合
map.on('click', async e => {
  addMarker(e.lngLat.lng, e.lngLat.lat);
  if (markers.length === 1) {
    // マーカーを中心に5km以内の駅をリストアップ
    searchMapPoint();
  } else {
    // 2つのマーカーに含まれる駅をリストアップ
    searchMapSquare();
  }
});

マーカーの追加処理

マーカーの追加処理 addMarker は次の通りです。ここではMapboxのAPIを実行するだけです。すでに2つのマーカーが立っている場合は、1つ目のマーカーを削除します。

// マーカーを立てる処理
const addMarker = (lng, lat) => {
  // すでに2つのマーカーが立っている場合は、前のものを削除する
  if (markers.length == 2) {
    markers[0].remove();
    markers.shift();
  }
  // マーカーを立てる
  const marker = new mapboxgl.Marker()
    .setLngLat([lng, lat])
    .addTo(map);
  markers.push(marker);
}

距離検索

f:id:mbaasdevrel:20210706222019j:plain

マーカーが一つだった場合には距離検索を実行します。マーカーを中心として、半径2km以内にある駅を検索します。 これは withinKilometers メソッドで実行し、最後の2という数字が2kmを意味しています。

// 1つのマーカーを中心に、2km以内の駅を検索する処理
const searchMapPoint = async () => {
  // すでにある駅のマーカーを削除
  clearStations();
  // 1つ目のマーカーの位置情報を取得
  const {lng, lat} = markers[0]._lngLat;
  // NCMBの位置情報オブジェクト作成
  const geo = new ncmb.GeoPoint(lat, lng);
  // 検索するクラス(DBでいうテーブル相当)
  const Station = ncmb.DataStore('Station');
  // 検索条件を指定
  const stations = await Station
    .withinKilometers('geo', geo, 2) // 2km以内のデータを検索
    .limit(50)
    .fetchAll();
  // 検索結果をマーカーとして地図上に表示
  addStationMarkers(stations);
}

検索結果で取得できた駅の一覧を addStationMarkers にて地図上に描画します。これはマーカーを立てる時と同様、MapboxのAPIを実行しているだけです。

// 検索結果の駅一覧を地図上に表示
const addStationMarkers = (stations) => {
  for (const station of stations) {
    const marker = new mapboxgl.Marker({color: '#f00'})
      .setLngLat([station.geo.longitude, station.geo.latitude])
      .addTo(map);
    stationsMarkers.push(marker)
  }
}

検索実行前には、すでにある駅のマーカーを消す clearStations 関数を実行しています。

// 駅のマーカーを削除する処理
const clearStations = () => {
  for (const marker of stationsMarkers) {
    marker.remove();
  }
  stationsMarkers.splice(0, stationsMarkers.length);
}

範囲を検索する

f:id:mbaasdevrel:20210706222035j:plain

マーカーが二つだった場合には、2つのマーカー内にある駅を検索して地図上に描画します。こちらは withinSquare メソッドを2つのGeoPointオブジェクトとともに実行するのがポイントです。

// 2つのマーカーに含まれている駅を検索する処理
const searchMapSquare = async () => {
  // すでにある駅のマーカーを削除
  clearStations();
  // マーカーをNCMBの位置情報オブジェクトにする
  const geos = [];
  for (const marker of markers) {
    const {lng, lat} = marker._lngLat;
    geos.push(new ncmb.GeoPoint(lat, lng));
  }
  // 検索するクラス(DBでいうテーブル相当)
  const Station = ncmb.DataStore('Station');
  // 検索条件を指定
  const stations = await Station
    .withinSquare('geo', geos[0], geos[1]) // 2つの位置情報で検索
    .limit(50)
    .fetchAll();
  // 検索結果をマーカーとして地図上に表示
  addStationMarkers(stations);
}

すでにマーカーが2つ立っている場合、1つに戻すことができないので、右上にゴミ箱アイコンを配置し、そのイベントとしてマーカーを削除する clear 関数を実行しています。

// マーカーを削除する
const clear = () => {
  for (const marker of markers) {
    marker.remove();
  }
  markers.splice(0, 2);
  // 駅のマーカーも削除
  clearStations();
}

コードについて

今回のコードは以下のリポジトリにて公開しています。ライセンスはMIT Licenseになりますので、自由にご利用ください。

NCMBMania/monaca_map_app: Monacaで位置情報検索を利用したデモアプリです。

まとめ

これで地図を用いた位置情報検索アプリの完成です。今回は元データを用意しましたが、実際のアプリでは自分たちで好きに追加できるようにしても良いでしょう。データをあらかじめ登録する方式であれば、つくばの公園 on the App Storeのようなアプリを作る際にも利用できるはずです。

中津川 篤司

中津川 篤司

NCMBエヴァンジェリスト。プログラマ、エンジニアとしていくつかの企業で働き、28歳のときに独立。 2004年、まだ情報が少なかったオープンソースソフトの技術ブログ「MOONGIFT」を開設し、毎日情報を発信している。2013年に法人化、ビジネスとエンジニアを結ぶDXエージェンシー「DevRel」活動をスタート。