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

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

NCMBとMonaca、Mapbox、国土地理院APIを使った地図メモアプリ(その2:地図の表示とデータ登録)

f:id:mbaasdevrel:20210904214400j:plain

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

今回はMonacaとNCMB、さらにMapboxや国土地理院APIを使って地図メモアプリを作ります。前回は画面の仕様と、NCMBとMapboxの初期化を行いましたので、今回は地図の表示とデータ登録までを作成します。

index.htmlについて

www/index.html では、タブバーを読み込みます。2つのタブがあり、左側が地図表示、右側がメモのリスト表示になります。

<div id="app">
  <!-- Views/Tabs container -->
  <div class="views tabs safe-areas">
    <div class="toolbar toolbar-bottom tabbar-labels">
      <div class="toolbar-inner">
        <a href="#view-home" class="tab-link tab-link-active">
          <i class="icon material-icons">map</i>
          <span class="tabbar-label">地図</span>
        </a>
        <a href="#view-list" class="tab-link">
          <i class="icon material-icons">list</i>
          <span class="tabbar-label">メモ</span>
        </a>
      </div>
    </div>
    
    <!-- Your main view/tab, should have "view-main" class. It also has "tab-active" class -->
    <div id="view-home" class="view view-main view-init tab tab-active" data-url="/map">
    </div>
    <div id="view-list" class="view view-init tab" data-name="List" data-url="/list">
    </div>
  </div>
</div>

地図画面について

f:id:mbaasdevrel:20210904214331j:plain

まず地図画面 www/pages/map.html について解説します。HTMLは地図を前面に表示するのでシンプルです。

<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner sliding">
        <div class="title">マップ</div>
      </div>
    </div>
    <div class="page-content">
      <div id="map"></div>
    </div>
  </div>
</template>
<style>
  #map {
      width: 100%;
      height: 100%;
  }
</style>

JavaScriptの実装

まず各メソッドで共通するデータを準備します。

export default async function (props, {$f7, $f7router, $on, $store }) {
  // Mapboxのアクセストークンをセット
  mapboxgl.accessToken = mapboxAccessToken;
  // MapBoxオブジェクト
  let map;
  // 現在位置をストア(アプリ共通変数)に入れる
  $store.dispatch('setCoords', await getLocation());

  // 省略
  return $render;
}

getLocation 関数は js/app.js 内に定義しています。位置情報APIがコールバック形式なので、Promiseベースにしているだけです。

// 位置情報を取得するAPIをコールバックからPromiseにする
const getLocation = () => {
  return new Promise((res, rej) => {
    navigator.geolocation.getCurrentPosition(res, rej);
  });
}

次に画面の準備ができた段階で呼ばれるイベントにてMapboxの表示と、地図のクリック時のイベント、地図をドラッグし終わった時のイベントを設定します。地図を動かし終わった時には、その位置情報をストアに保存しておきます。こうすることで、一覧表示の際にも同じ位置情報を使ってメモを検索できるようになります。

// DOMが初期化完了した際に呼ばれるイベント
$on('pageBeforeIn', async (e, page) => {
  // ストア(アプリ共通変数)に入った位置情報(現在位置)を取得
  const { longitude, latitude } = $store.state.coords;

  // MapBoxを用意
  map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v11',
    center: [longitude, latitude], // 現在位置を中心に地図を表示
    zoom: 14
  });

  // 地図を動かし終わった時のイベント
  map.on('moveend', async e => {
    // 地図の中心点を取得
    const center = map.getCenter();
    // ストア(アプリ共通変数)に入れる用のオブジェクトを作成
    const coords = {latitude: center.lat, longitude: center.lng};
    // ストア(アプリ共通変数)に保存
    $store.dispatch('setCoords', { coords } );
  });

  // 地図をクリックした場合
  map.on('click', async e => {
    // ポップアップを立てる
    addPopup(e.lngLat.lng, e.lngLat.lat);
  });
});

地図をクリックした際にはポップアップを表示します。このポップアップをタップすると、その位置情報を使ってメモ書きを行うフォームを開きます。

// ポップアップを立てる処理
const addPopup = (lng, lat) => {
  // ポップアップを立てる
  new mapboxgl.Popup()
    .setLngLat([lng, lat])
    .setHTML('<div class="addMemo">ここにメモ</div>')
    .setMaxWidth('300px')
    .addTo(map);
  // ポップアップをタップした際のイベント
  $f7.$el.find('.addMemo').on('click', e => {
    // 追加用フォームへ移動する(位置情報を次の画面に送る)
    $f7router.navigate('/add', {
      props: { lng, lat }
    });
  });
}

f:id:mbaasdevrel:20210904214400j:plain

入力フォームについて

f:id:mbaasdevrel:20210904214417j:plain

地図でポップアップをクリックすると www/pages/form.html が表示されます。これは位置情報に紐付けたメモを入力する画面です。

<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner sliding">
        <div class="left">
          <a href="#" @click=${() => $f7router.back()} class="link">
            <i class="f7-icons">chevron_left</i> 戻る
          </a>
        </div>
        <div class="title">メモ</div>
      </div>
    </div>
    <div class="page-content">
      <div class="block">
        <form id="form">
          <div class="row">
            <div class="col-100">
              <div class="list">
                <ul>
                  <li class="item-content item-input">
                    <div class="item-inner">
                      <div class="item-title item-label">メモ</div>
                      <div class="item-input-wrap">
                        <textarea name="body" class="resizable"></textarea>
                      </div>
                    </div>
                  </li>
                  <li class="item-content item-input">
                    <div class="item-inner">
                      <div class="item-title item-label">住所</div>
                      <div class="item-input-wrap">
                        ${addresses.join('')}
                      </div>
                    </div>
                  </li>
                </ul>
              </div>
              <input type="hidden" name="address" value="${addresses.join('')}" />
              <input type="hidden" name="address1" value="${addresses[0]}" />
              <input type="hidden" name="address2" value="${addresses[1]}" />
              <input type="hidden" name="address3" value="${addresses[2]}" />
              <button class="button col" @click=${save}>保存</button>
            </div>
          </div>
        </form>
      </div>
      <ul id="log">
      </ul>
    </div>
  </div>
</template>

画面が表示されるタイミングで国土地理院APIを利用して住所情報を取得しています。

export default async (props, {$f7, $f7router, $on }) => {
  // 前画面から送られてきた位置情報を取得
  const { lng, lat } = props;

  // 位置情報から住所に変換する関数
  const getAddress = async ({lng, lat}) => {
    // 国土地理院APIを利用
    const url = `https://mreversegeocoder.gsi.go.jp/reverse-geocoder/LonLatToAddress?lat=${lat}&lon=${lng}`;
    // APIを実行      
    const res = await app.request.json(url);
    // 結果を取得
    const { muniCd, lv01Nm } = res.data.results;
    // 都道府県や市区町村を取得(js/muni.jsにて定義)
    const add = GSI.MUNI_ARRAY[muniCd].split(',');
    // 結果を返却(決め打ち)
    return [add[1], add[3], lv01Nm];
  }

  // 位置情報から住所を取得
  const addresses = await getAddress({lng, lat});

  // 省略
  return $render;
}

addresses には ["東京都", "港区", "六本木一丁目"] のような形で住所情報が入っています。

保存処理について

メモを入力したら、保存ボタンをタップします。この時実行されるのがsave関数になります。入力内容をオブジェクト化し( serializeForm 関数は js/app.js にて定義しています)、NCMBのデータストア(クラウドデータベース)に保存します。位置情報は NCMB の位置情報オブジェクトとして保存します。

// 入力された内容でデータストアに保存する関数
const save = async (e) => {
  e.preventDefault();
  // 入力内容をオブジェクト化する(js/app.jsにて定義)
  const params = serializeForm('form#form');
  // メモクラス(DBでいうテーブル相当)を準備
  const Memo = ncmb.DataStore('Memo');
  // メモクラスのインスタンス(DBでいう行相当)を作成
  const memo = new Memo;
  // 入力内容を反映
  for (const key in params) {
    memo.set(key, params[key]);
  }
  // 位置情報オブジェクトを作成
  const geo = new ncmb.GeoPoint(lat, lng);
  // 保存
  await memo
    .set('geo', geo)
    .save();
  // 前の画面に戻る
  $f7router.back();
}

これでデータの保存が完了です。

コード

今回のコードはNCMBMania/Monaca_MapMemo: Monacaを使った地図メモアプリですにアップロードしてあります。実装時の参考にしてください。

まとめ

今回は地図の表示とデータ登録まで行いました。次回はデータの一覧表示と、地図上での表示を行います。

NCMBは位置情報を扱った検索が簡単にできるので、地図を使ったアプリ開発時にぜひ役立ててください。

中津川 篤司

中津川 篤司

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