0
0

More than 1 year has passed since last update.

MonacaとNCMBで地図メモアプリを作る(その3: マーカー表示と一覧表示)

Posted at

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

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

前回は地図表示とデータの登録を実装しましたので、今回は地図上へのマーカー表示と一覧表示処理を実装していきます。

コードについて

今回のコードはmap-note-monaca にアップロードしてあります。実装時の参考にしてください。

地図上へのマーカー表示

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

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

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

// 中心座標の周辺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 プロパティに追加しています。

// ノートに基づいてマーカーを追加する関数を定義します。
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 に定義しています。

// 画像のリサイズを行う関数
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);
  });
};

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

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

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

mapClick 関数は以下の通りです(紹介済み)。

// 地図をクリックしたときの関数を定義します。クリックした場所にマーカーを追加したり、ツールチップを表示したりします。
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 が呼ばれるので、ここでノートオブジェクトの内容に基づいてツールチップを表示します。

// ツールチップを表示する関数を定義します。
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 を呼んで消しています。

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

メモの一覧表示

一覧画面 pages/list.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は以下の通りです。ストアからデータを取得して、それを一覧表示しています。

<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 にて定義しています。

// 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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0