スマートフォンと地図は相性が良いです。スマホは持ち歩いて使うのが基本ですし、位置情報などの情報も取得できます。
今回は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の売り機能でもあるので、ぜひ利用してください。