MonacaとNCMBで地図メモアプリを作る

スマートフォンと地図は相性が良いです。スマホは持ち歩いて使うのが基本ですし、位置情報などの情報も取得できます。

今回はNCMBとMonacaを使って地図上にメモできる地図メモアプリを作ります。地図はOpenStreetMapのものを利用し、タップした場所にメモと写真を残しておけるアプリです。

まず最初の記事では画面の説明とSDKの導入までを進めます。

## コードについて

今回のコードは[monaca-map-note-handson](https://github.com/NCMBMania/monaca-map-note-handson) にアップロードしてあります。実装時の参考にしてください。

## 利用技術について

今回は次のような組み合わせになっています。OpenLayersは地図ライブラリです。国土地理院APIは位置情報を住所に変換する、逆ジオコーディングに利用しています。

- Monaca
- Framework7
- OpenLayers
- 国土地理院API

## 仕様について

地図はOpenLayersを使い、OpenStreetMapを表示します。

## 利用する機能について

地図メモアプリで利用するNCMBの機能は次の通りです。

- データストア
  - データ登録
  - データ取得
- ファイルストア
  - アップロード
  - ダウンロード

## 画面について

今回は以下の3つの画面があります。

### www/pages/map.html

![1357 localhost - 0712180616.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f47acbde-9f97-02d2-a30b-7b92e3503ed5.jpeg)


OpenLayerを使い、OpenStreetMapを表示します。デフォルトは東京タワーの位置情報としていますので、位置情報取得のダイアログは使いません。

### www/pages/note.html

![1356 localhost - 0712180608.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/2df23d87-25de-6283-29a5-df3e6451f0c3.jpeg)


地図上のタップされた場所にメモおよび画像を記録するための画面です。

### www/pages/list.html

![1359 localhost - 0712180638.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/d9340aea-d5bd-8727-baa9-8f926842ef5b.jpeg)


地図画面で表示しているメモを一覧表示する画面です。

### SDKのインストール

今回はMonacaのJS/CSSコンポーネントの追加と削除より、NCMBを追加します。アプリのテンプレートはFramework7のJavaScript版（VueやReactではなく）を選択しています。

## NCMBのAPIキーを取得

[mBaaSでサーバー開発不要！ | ニフクラ mobile backend](https://mbaas.nifcloud.com/)にてアプリを作成し、アプリケーションキーとクライアントキーを作成します。

## js/config.jsonの作成

js/config.jsonを作成し、その中に先ほど取得したNCMBのAPIキーを設定します。内容は次のようになります。

```json
{
	"applicationKey": "YOUR_APPLICATION_KEY",
	"clientKey": "YOUR_CLIENT_KEY"
}
```

## 初期化

初期化は `js/app.js` にて行います。config.jsonを読み込む関係上、非同期処理内にて行います。cordovaの有無（アプリまたはプレビューの違いを検知）によって初期化時のイベント処理を変えています。

```jsx
// 記述済み
// NCMBの初期化を行う
// window.cordovaが存在する場合は'deviceready'、それ以外の場合は'DOMContentLoaded'イベントが発火した時に実行
const event = window.cordova ? 'deviceready' : 'DOMContentLoaded';
document.addEventListener(event, async (e) => {
  // この中に処理を書きます
});
```

config.jsonの内容を読み込んで、NCMBとFramework7の初期化を行います。

```jsx
// 記述あり

// 設定ファイルを非同期で取得
const config = await (await fetch('./js/config.json')).json();
// NCMBのインスタンスを作成
window.ncmb = new NCMB(config.applicationKey, config.clientKey);
// 以降は記述済み
// Framework7のインスタンスを作成
window.app = new Framework7({
  name: 'My App', // アプリ名
  theme: 'auto', // テーマの自動検出
  el: '#app', // アプリのルート要素
  store: store, // 状態管理ストア
  routes: routes, // ルーティング設定
});
```

これでNCMBの初期化が完了します。

## ルーティング設定

今回は地図画面、ノート画面、リスト画面の3画面になります。これを `js/routes.js` に定義します。

```jsx
// 記述済み
// ルーティングの設定を行う配列
const routes = [
  {
    // pathはルーティングのパスを指定します。
    // '/'の場合はルート（ホーム）を意味します。
    path: '/',
    // urlはそのパスが指定された時にロードされるHTMLファイルの場所を指定します。
    url: './index.html',
  },
  {
    // '/map/'というパスが指定された時にロードされるコンポーネントのURL
    path: '/map/',
    componentUrl: './pages/map.html',
  },
  {
    // '/list/'というパスが指定された時にロードされるコンポーネントのURL
    path: '/list/',
    componentUrl: './pages/list.html',
  },
  {
    // '/note/:lat/:lng/'というパスが指定された時にロードされるコンポーネントのURL
    // この場合、:latと:lngはパラメータとなり、動的に値が入ります。
    path: '/note/:lat/:lng/',
    componentUrl: './pages/note.html',
  },
  // どのルートにもマッチしない場合（404）のルート定義
  // ルーティングの一番最後に配置しなければなりません（全てのルートがマッチしない場合にこのルートが使用されるため）
  {
    path: '(.*)',
    url: './pages/404.html',
  },
];
```

## ストア

ストアはアプリ全体で利用する変数です。今回は取得したメモ一覧と、現在の中心座標の2つをストアで管理します。

```jsx
// 記述済み
// Framework7のcreateStore関数を使ってストアを作成します
const createStore = Framework7.createStore;

// ストアを作成し、初期状態としてnotesとcoordsを設定します
const store = createStore({
  // stateはアプリケーションの状態を保存します
  state: {
    // notesはメモのリストを保存します。初期状態は空配列です
    notes: [],
    // coordsは現在の座標を保存します。初期状態は東京タワーの座標です
    coords: {
      lat: 35.6585805,
      lng: 139.7454329,
    },
  },
  // gettersはstateから特定のデータを取得するための関数を定義します
  getters: {
    // notesのゲッター。stateからnotesを取得します
    notes({ state }) {
      return state.notes;
    },
    // coordsのゲッター。stateからcoordsを取得します
    coords({ state }) {
      return state.coords;
    },
  },
  // actionsはstateを更新するための関数を定義します
  actions: {
    // coordsを更新するためのアクション
    setCoords({ state }, coords) {
      state.coords = coords;
    },
    // 新たなメモを追加するためのアクション
    addNote({ state }, note) {
      state.notes = [...state.notes, note];
    },
    // 複数のメモを一度に追加するためのアクション
    resetNote({ state }) {
      state.notes = [];
    },
  },
})
```

## 地図を表示する

ここからはNCMBは関係なく、OpenLayerの話になります。まず必要なライブラリを `www/index.html` にて読み込みます。

```html
<!-- 記述済み -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v7.3.0/ol.css">
<script src="https://cdn.jsdelivr.net/npm/ol@v7.3.0/dist/ol.js"></script>
```

### www/pages/map.htmlの処理

では画面を作成します。地図を表示するのは `#map` になります。

```html
<!-- 記述済み -->
<template>
  <!--
  この部分はHTMLテンプレートで、地図を表示するための<div>タグを定義しています。
  -->
  <div class="page" data-name="map">
    <div id="map"></div>
  </div>
</template>
<style>
  /* 
  この部分はCSSスタイルシートで、地図の表示スタイルを定義しています。
  地図の表示エリアを画面全体に広げています。
  */
  #map {
    width: 100%;
    height: 100%;
  }
</style>
```

ここからはJavaScriptのコードです。全体は以下のコードで囲みます（Framework7のお作法です）。

```html
<!-- 記述済み -->
<script>
  /* 
  この部分はJavaScriptで、地図の操作や表示に関する詳細な設定を行っています。
  */
  export default (props, { $f7route, $store, $on, $f7router }) => {
		// ここに実装していきます
    return $render; // 最終的にレンダリングを行います。
  };
</script>
```

ページがマウントされた際に地図を初期化する `initMap` 関数を呼び出します。

```js
// 記述済み
// ページが表示されたときに地図を初期化します。
$on('page:mounted', initMap);
```

### initMapの実装

`initMap` ではOpenLayerを利用して地図を表示します。前半はOpenLayerの初期化設定で、後半では地図をタップした際のイベント設定と、中心が変わったときのストア更新処理を行っています。

```jsx
// 記述済み
// 地図を初期化する関数を定義します。
const initMap = () => {
	// マーカーを表示するためのレイヤーを作成します。
	const markerLayer = new ol.layer.Vector({
		source: new ol.source.Vector(),
	});

	// 地図を作成し、地図とマーカーのレイヤーを設定します。また、初期の中心座標とズームレベルを設定します。
	map = new ol.Map({
		target: 'map',
		layers: [
				new ol.layer.Tile({
				source: new ol.source.OSM(),
			}),
			markerLayer,
		],
		view: new ol.View({
			center: ol.proj.fromLonLat([ lng, lat]), // 東京タワーの位置情報
			zoom: 14,
		})
	});
	
	// 地図をクリックしたときのイベントを設定します。
	map.on('click', mapClick);

	// 地図の中心が変わったときのイベントを設定します。
	const view = map.getView();
	view.on('change:center', (e) => {
		const [lng, lat] = ol.proj.toLonLat(view.getCenter());
		$store.dispatch('setCoords', { lat, lng });
	});
};
```

### 地図をタップした際の処理

地図をタップした際には `mapClick` 関数が呼ばれます。この関数では、地図上のマーカーがタップされたのか、それ以外の部分なのかを判定して処理分けしています。

```jsx
// 記述済み
// 地図をクリックしたときの関数を定義します。クリックした場所にマーカーを追加したり、ツールチップを表示したりします。
const mapClick = (e) => {
	// クリックした場所のピクセル情報を取得します。
	const pixel = map.getEventPixel(e.originalEvent);

	// ピクセル情報から、マーカーが存在するかどうかを判定します。
	const target = map.forEachFeatureAtPixel(pixel, function(feature, layer) {
		return feature;
	});

	// クリックした場所にマーカーがあれば、そのマーカーに関連するツールチップを表示します。
	if (target) {
		return showTooltip(e.coordinate, target.note);
	}

	// マーカーがなく、すでに表示されているツールチップがあれば、そのツールチップを非表示にします。
	if ($('.marker').length > 0) {
		return hideTooltip();
	}

	// マーカーがなく、ツールチップも表示されていない場合、新たなマーカーを追加します。
	const [ lng, lat ] = ol.proj.toLonLat(e.coordinate);
	const path = `/note/${lat}/${lng}/`;
	$f7router.navigate(path);
};
```

上記関数での `/note/${lat}/${lng}/` がノート画面への遷移になります。

## ノート画面での表示

ノート画面 `pages/note.html` は以下のようにフォームを表示しています。

```html
<!-- 記述済み -->
<template>
  <!-- ページ全体のレイアウトを定義する部分 -->
  <div class="page" data-name="product">
    
    <!-- ナビゲーションバーを定義する部分 -->
    <div class="navbar">
      <!-- ナビゲーションバーの背景を定義する部分 -->
      <div class="navbar-bg"></div>
      <!-- ナビゲーションバーの内側を定義する部分 -->
      <div class="navbar-inner sliding">
        <!-- ナビゲーションバーの左側を定義する部分。ここでは戻るボタンが設置されている -->
        <div class="left">
          <a href="#" class="link back">
            <i class="icon icon-back"></i>
            <span class="if-not-md">戻る</span>
          </a>
        </div>
        <!-- ナビゲーションバーのタイトルを定義する部分。ここでは新規メモと表示されている -->
        <div class="title">新規メモ</div>
      </div>
    </div>
    
    <!-- ページの主要なコンテンツを定義する部分 -->
    <div class="page-content">
      <!-- メモのタイトルを定義する部分。アドレス付近のメモと表示される -->
      <div class="block-title">${page.address}付近のメモ</div>
      <!-- メモの内容を入力する部分を定義するフォーム -->
      <div class="block block-strong">
        <!-- メモと画像を入力するためのフォーム -->
        <form class="note list" @submit="${submit}">
          <ul>
            <!-- メモ入力部分 -->
            <li>
              <div class="item-content item-input">
                <div class="item-inner">
                  <div class="item-title item-label">メモ</div>
                  <div class="item-input-wrap">
                    <textarea class="resizable"></textarea>
                  </div>
                </div>
              </div>
            </li>
            <!-- 画像選択部分 -->
            <li>
              <div class="item-content item-input">
                <div class="item-inner">
                  <div class="item-title item-label">画像</div>
                  <div class="item-input-wrap">
                    <!-- すでに選択されている画像があれば表示、なければ画像選択アイコンを表示 -->
                    ${page.image ?
                      $h`<img src="${page.image}" width="100%"  @click="${select}" />`
                      :
                      $h`<i class="f7-icons" @click="${select}">photo</i>`
                    }
                    <span class="image-wrap">
                      <!-- 画像ファイル選択のためのinput要素。デフォルトでは表示されていません -->
                      <input type="file" name="file" accept="image/*" @change="${change}" />
                    </span>
                  </div>
                </div>
              </div>
            </li>
            <!-- 保存ボタン -->
            <button class="button col" @click=${submit}>保存</button>
          </ul>
        </form>
      </div>
    </div>
  </div>
</template>

<!-- スタイルシート。画像ファイル選択のinput要素を非表示に設定しています -->
<style>
  .image-wrap {
    display: none;
  }
</style>
```

### JavaScriptの処理

まずJavaScriptはFramework7の作法に沿って定義します。位置情報である `lat` および `lng` は前の画面から渡されたデータです。

```html
<!-- 記述済み -->
<!-- スクリプト部分。ページの動作を定義しています -->
<script>
  // JavaScriptのコード。ここでページの動作を定義します
  export default (props, { $f7router, $f7route, $store, $on, $update }) => {
    // 座標情報を取得します
    const { lat, lng } = props;
    // pageオブジェクトを定義します。ここに入力情報が保存されます
    const page = {
      lat: parseFloat(lat),
      lng: parseFloat(lng),
      image: undefined,
    };

		// この中に処理を書きます

    // 描画を行います
    return $render;
  };
</script>
```

#### 逆ジオコーディングの実行

画面が表示（初期化）された際に、国土地理院APIを使って位置情報を住所に変換します。

```js
// 記述済み
// ページが初期化されたときに呼ばれる関数を定義します
$on('pageInit', async () => {
	// 逆ジオコーディングで座標から住所情報を取得します
	page.address = await reverseGeoCoding(lat, lng);
	// 描画を更新します
	$update();
});
```

`reverseGeoCoding` 関数は以下のようになります。 `js/app.js` に定義しています。

```js
// 記述済み
// 緯度と経度から地名を取得する関数
// https://memo.appri.me/programming/gsi-geocoding-api 参照
const reverseGeoCoding = async (lat, lng) => {
  const url = `https://mreversegeocoder.gsi.go.jp/reverse-geocoder/LonLatToAddress?lat=${lat}&lon=${lng}`;
  const res = await fetch(url);
  const json = await res.json();
  const params = GSI.MUNI_ARRAY[json.results.muniCd].split(',');
  return `${params[1]}${params[3]}${json.results.lv01Nm}`;
}
```

なお、 `GSI` という変数は国土地理院の用意している住所一覧です。これは `www/index.html` にて読み込みます。

```html
<!-- 記述済み -->
<!-- 初期化 -->
<script>
const GSI = {};
</script>
<script src="https://maps.gsi.go.jp/js/muni.js"></script>
```

### 写真選択時の処理

写真を選択する際には、まずカメラアイコンをタップします。これは非表示になっているファイル選択ダイアログをクリックするものです。

```js
// 記述済み
// 画像選択部分がクリックされたときに呼ばれる関数を定義します
const select = () => {
	// 画像選択のinput要素をクリックします
	$('form.note input[type=file]').click();
};
```

そして写真が選択されたら、プレビューを表示します。

```js
// 記述済み
// 画像が選択されたときに呼ばれる関数を定義します
const change = (e) => {
	// 選択されたファイルを取得します
	const file = e.target.files[0];
	// 選択された画像のURLを取得します
	page.image = URL.createObjectURL(file);
	// 描画を更新します
	$update();
}
```

なお、この時のURLは `blob://` となっており、デフォルトのCSP（コンテンツセキュリティポリシー）では表示できません。 `www/index.html` に以下を追加します。

```html
<!-- 記述済み -->
<!-- 末尾に blob: を追加 -->
<meta http-equiv="Content-Security-Policy" content="default-src * 'self' 'unsafe-inline' 'unsafe-eval' data: gap: content: blob:">
```

### ノートを保存

入力が終わったら、保存処理を実行します。選択されている位置情報はNCMBの位置情報オブジェクトに変換します。そして住所、メモと一緒に保存します。

```js
// 記述してください

// 保存ボタンが押されたときに呼ばれる関数を定義します
const submit = async (e) => {
	// フォームのデフォルトの送信動作をキャンセルします
	e.preventDefault();
	// Noteオブジェクトを作成します
	const Note = ncmb.DataStore('Note');
	const note = new Note;
	// ジオポイントを作成します
	const geo = new ncmb.GeoPoint(page.lat, page.lng);
	// メモに座標、住所、テキストをセットします
	note
		.set('geo', geo)
		.set('address', page.address)
		.set('text', $('form.note textarea').val());
	// 画像が選択されていれば画像もセットします
	if (page.image) {
		// 選択された画像のデータをBlob形式に変換します
		const blob = await toBlob(page.image);
		// ファイル名を一意な名前に変換します
		const ext = blob.type.split('/')[1];
		const name = Math.random().toString(32).substring(2);
		const fileName = `${name}.${ext}`;
		// ファイルをアップロードします
		await ncmb.File.upload(fileName, blob);
		// ファイル名をメモにセットします
		note.set('image', fileName);
	}
	// メモを保存します
	await note.save();
	// 前のページに戻ります
	$f7router.back();
}
```

`toBlob` は選択されている画像データ（blob://形式）をBlobに戻す関数です。

```js
// 記述済み
// 画像のURLをBlob形式に変換する関数を定義します
const toBlob = async (uri) => {
	// 画像のURLからデータを取得します
	const res = await fetch(uri);
	// データをBlob形式に変換します
	const blob = await res.blob();
	return blob;
};
```

これでメモの保存処理が完成しました。

## 地図上へのマーカー表示

この処理は `www/map.html` にて実装します。地図が表示された際に、NCMBからメモ一覧を取得します。

```js
// 記述済み
// ページが表示された後にノートをロードし、マーカーを追加します。
$on('page:afterin', async () => {
	// ノートをロードします。
	const notes = await loadNotes();
	// ストア内のノートをリセットします。
	$store.dispatch('resetNote');
	// 取得したノートを使って、マーカーを追加します。
	notes.forEach(addMarker);
});
```

`loadNotes` 関数は現在の中心点を利用して、周囲3キロにあるメモ一覧を取得する関数です。 `withinKilometers` メソッドにて、距離と位置情報を指定して絞り込み条件としています。

```js
// 記述してください

// 中心座標の周辺3km以内のノートを取得する関数を定義します。
const loadNotes = async () => {
	const Note = ncmb.DataStore('Note');
	const center = map.getView().getCenter();
	const [lng, lat] = ol.proj.toLonLat(center);
	const geo = new ncmb.GeoPoint(lat, lng);
	return Note
		.withinKilometers("geo", geo, 3)
		.fetchAll();
};
```

### マーカーを表示する

マーカー表示の処理 `addMarker` はOpenLayerでの実装になります。繰り返し画像をダウンロードすることないようノートオブジェクトの `blob` プロパティにダウンロードしたデータ `file` を追加しています。また、そのノートオブジェクト自体、マーカーの `note` プロパティに追加しています。

```js
// 記述してください

// ノートに基づいてマーカーを追加する関数を定義します。
const addMarker = async (note) => {
	// ノートの座標を取得します。
	const geo = note.get('geo');
	const coordinate = ol.proj.fromLonLat([geo.longitude, geo.latitude]);

	// ノートの画像をダウンロードします。
	const file = await ncmb.File.download(note.get('image'), 'blob');
	note.blob = file;
	$store.dispatch('addNote', note);

	// 画像のサイズを調整します。
	const image = await resizeImage(file, 40);
	const src = URL.createObjectURL(image);

	// マーカーのスタイルを設定します。
	const markerStyle = new ol.style.Style({
		image: new ol.style.Icon({
			anchor: [0, 0],
			size: [40, 40],
			src,
		}),
	});

	// マーカーを作成し、地図に追加します。
	const marker = new ol.Feature({
		geometry: new ol.geom.Point(coordinate),
	});
	marker.note = note;
	marker.setStyle(markerStyle);
	map.getLayers().getArray()[1].getSource().addFeature(marker);
}
```

OpenLayerではマーカーをCanvasタグ上に表示します。そのため、画像サイズを img タグのように柔軟に指定はできません。そこで `resizeImage` を用意して画像のリサイズを実行しています。 `resizeImage` 関数は `js/app.js` に定義しています。

```js
// 記述済み
// 画像のリサイズを行う関数
const resizeImage = async (blob, maxWidth) => {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      const ratio = img.width / img.height;
      canvas.width = maxWidth;
      canvas.height = maxWidth / ratio;
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      canvas.toBlob((blob) => resolve(blob), 'image/jpeg');
    };
    img.src = URL.createObjectURL(blob);
  });
};
```

### マーカーをタップした際の処理

マーカーをタップした際の処理は、地図をタップしたときの処理の分岐です。

```js
// 記述済み
// 地図をクリックしたときのイベントを設定します。
map.on('click', mapClick);
```

`mapClick` 関数は以下の通りです（紹介済み）。

```js
// 記述済み
// 地図をクリックしたときの関数を定義します。クリックした場所にマーカーを追加したり、ツールチップを表示したりします。
const mapClick = (e) => {
	// クリックした場所のピクセル情報を取得します。
	const pixel = map.getEventPixel(e.originalEvent);

	// ピクセル情報から、マーカーが存在するかどうかを判定します。
	const target = map.forEachFeatureAtPixel(pixel, function(feature, layer) {
		return feature;
	});

	// クリックした場所にマーカーがあれば、そのマーカーに関連するツールチップを表示します。
	if (target) {
		return showTooltip(e.coordinate, target.note);
	}

	// マーカーがなく、すでに表示されているツールチップがあれば、そのツールチップを非表示にします。
	if ($('.marker').length > 0) {
		return hideTooltip();
	}

	// マーカーがなく、ツールチップも表示されていない場合、新たなマーカーを追加します。
	const [ lng, lat ] = ol.proj.toLonLat(e.coordinate);
	const path = `/note/${lat}/${lng}/`;
	$f7router.navigate(path);
};
```

マーカーがタップされた場合には `showTooltip` が呼ばれるので、ここでノートオブジェクトの内容に基づいてツールチップを表示します。

```js
// 記述してください

// ツールチップを表示する関数を定義します。
const showTooltip = async (coordinate, note) => {
	hideTooltip(); // 既存のツールチップを非表示にします。

	// ツールチップに表示する画像を調整します。
	const image = await resizeImage(note.blob, 200);

	// ツールチップの要素を作成し、内容を設定します。
	const tooltip = document.createElement('div');
	tooltip.className = 'marker';
	tooltip.innerHTML = `
	<strong>${note.get('address')}付近のメモ</strong>
	<p>${note.get('text')}</p>
	<div>
		<img src="${URL.createObjectURL(image)}" width="100%" />
	</div>
	`;

	// ツールチップを地図に追加します。
	document.querySelector('#view-map').appendChild(tooltip);

	// ツールチップの位置を設定します。
	const tooltipOverlay = new ol.Overlay({
		element: tooltip,
		offset: [-20, -20],
	});
	map.addOverlay(tooltipOverlay);
	tooltipOverlay.setPosition(coordinate);
};
```

すでにツールチップが表示されている場合には `hideTooltip` を呼んで消しています。

```js
// 記述済み
// ツールチップを非表示にする関数を定義します。
const hideTooltip = () => {
	const tooltip = document.querySelector('.marker');
	if (tooltip) {
		tooltip.parentNode.removeChild(tooltip);
	}
};
```

## メモの一覧表示

一覧画面 `pages/list.html` ではストアに入っているメモデータを一覧表示します。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 class="list media-list">
        <!-- メモの一覧を表示するためのリスト部分を定義 -->
        <ul>
          <!-- 各メモをリストアイテムとして表示する部分を定義 -->
          <!-- この部分では、JavaScriptのmap関数を使用してnotes.value内の各noteに対して以下のHTMLを生成しています -->
          ${ notes.value.map((note) => $h`
            <!-- 各メモを表現するリストアイテム部分 -->
            <li>
              <!-- メモの内容を表示する部分 -->
              <div class="item-content">
                <!-- もしメモに添付された画像(blob)が存在するならば、それを表示する -->
                ${ note.blob ?
                  $h`<div class="item-media"><img src="${URL.createObjectURL(note.blob)}" width="44" /></div>` :
                  ''
                }
                <!-- メモの内部情報を表示する部分 -->
                <div class="item-inner">
                  <!-- メモのタイトル部分と、メモの距離情報部分を表示する -->
                  <div class="item-title-row">
                    <!-- メモのタイトル部分 -->
                    <div class="item-title">${note.text}</div>
                    <!-- メモの距離情報部分 -->
                    <div class="item-after">${distance(note.geo.latitude, note.geo.longitude, coords.value.lat, coords.value.lng)}m</div>
                  </div>
                </div>
              </div>
            </li>
          `)}
        </ul>
      </div>
    </div>
  </div>
</template>
```

JavaScriptは以下の通りです。ストアからデータを取得して、それを一覧表示しています。

```html
<!-- 記述済み -->
<script>
  // このスクリプトはページの動作を定義します。
  // Framework7の仕組みを使って、propsという引数を通じて親コンポーネントからデータを受け取り、
  // $storeオブジェクトを利用しています。
  export default async function (props, {$store }) {
    // ストアからメモ(notes)と座標(coords)を取得します
    const { notes, coords } = $store.getters;
    // この関数の最後で、テンプレート部分をレンダリングします
    return $render;
  }
</script>
```

HTMLで利用している `distance` 関数は2つの位置情報から距離を出す関数です。 `js/app.js` にて定義しています。

```js
// 記述済み
// 2点間の距離を求める関数（メートル単位で返す）
// https://qiita.com/kawanet/items/a2e111b17b8eb5ac859a 参照
const R = Math.PI / 180;
const distance = (lat1, lng1, lat2, lng2) => {
  lat1 *= R;
  lng1 *= R;
  lat2 *= R;
  lng2 *= R;
  return parseInt(6371 * Math.acos(Math.cos(lat1) * Math.cos(lat2) * Math.cos(lng2 - lng1) + Math.sin(lat1) * Math.sin(lat2)) * 1000);
};
```

## 今回利用したNCMBの機能

この地図メモアプリでは、NCMBの以下の機能を利用しました。

- データストア
  - データ登録
  - データ取得
- ファイルストア
  - アップロード
  - ダウンロード

NCMBには他にも認証、スクリプト、プッシュ通知などの機能があります。ぜひそれらの機能も利用してください。

## まとめ

今回はOpenLayerとMonacaを組み合わせて、位置情報を利用したメモアプリを作成しました。位置情報検索はNCMBの売り機能でもあるので、ぜひ利用してください。

[ドキュメント : 開発者向けドキュメント | ニフクラ mobile backend](https://mbaas.nifcloud.com/doc/current/#/JavaScript)