前回Javascriptでコンパス機能を実装して、Android、iPhoneの背面が向いている方角が数値(0~360:0が北)で分かるようになりました。
今回は地図上で向きを視覚化する処理をざっくり検証してみようと思います。
目次
実装上の課題
課題1:Web地図用Javascriptライブラリの選定
GoogleMapsはAPIキーを取るのが面倒、OpenLayersは今回の検証には高機能過ぎ、ゼンリンさんのAPIは有料。
今回は無料で手軽に使えるLeaflet.jsを使うことにしました。
課題2:背景地図用GISサーバの選定
leaflet.jsのサンプルで使われているOpenStreetMapを使用します。
PostgreSQLにPostGIS拡張を入れてOpenStreetMapデータを投入して自分だけのGISサーバを作るとめっちゃ楽しいのですが、時間もコストも掛かるので今回はインターネット上のOpenStreetMapデータで地図を表示します。
(GeoServer + PostgreSQL + PostGISでオレオレGISサーバを作る記事もその内書きたいと思ってます。shapeデータのDB保存でHDDめちゃ喰いますけどね・・・)
課題3:現在地の取得
移動に追従して地図上のマーカーを移動させたいのでgeolocation.watchPosition()を使います。
課題4:向きの表現
CSSを定義して、GoogleMapsっぽくビームで表現してみます。
#beam {
position: fixed;
left: 0;
top: 0;
z-index: 1000;
opacity: 0.3;
height: 0;
width: 20px;
margin: 10px auto;
border-top: 40px solid #11b;
border-left: 30px solid transparent;
border-right: 30px solid transparent;
transform-origin: bottom;
}
このCSS矩形に対して、前回の記事で求められた0~360の値をrotete関数で設定してあげることで、向いている方向に回転させます。
let beam = document.querySelector("#beam");
beam.style.transform = "rotate(" + degrees + "deg)";
処理するタイミングはDeviceOrientationイベントにすることでリアルタイムに向いている方向が分かるようにします。
実装開始
課題がクリアになったので例のごとく一枚っぺらのindex.htmlに実装。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin=""
/>
<script
src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"
integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew=="
crossorigin=""
></script>
<style>
body {
margin: 0px;
padding: 0px;
}
#mapid {
height: 100vh;
}
#beam {
position: fixed;
left: 0;
top: 0;
z-index: 1000;
opacity: 0.3;
height: 0;
width: 20px;
margin: 10px auto;
border-top: 40px solid #11b;
border-left: 30px solid transparent;
border-right: 30px solid transparent;
transform-origin: bottom;
}
</style>
</head>
<body>
<div id="mapid"></div>
<input
id="permitGeolocation"
type="button"
value="iOSでDeviceOrientationを許可する"
style="position: fixed; top: 10px; left: 100px; z-index: 1000"
onclick="permitGeolocation()"
/>
<input
type="text"
id="os"
style="position: fixed; top: 40px; left: 100px; z-index: 1000"
/>
<input
type="text"
id="degree"
style="position: fixed; top: 70px; left: 100px; z-index: 1000"
/>
<input
type="text"
id="iPhone"
style="position: fixed; top: 100px; left: 100px; z-index: 1000"
/>
<input
type="text"
id="accuracy"
style="position: fixed; top: 130px; left: 100px; z-index: 1000"
/>
<div id="heading"></div>
<div id="beam"></div>
<script type="text/javascript">
let os;
if (
navigator.userAgent.indexOf("iPhone") > 0 ||
navigator.userAgent.indexOf("iPad") > 0 ||
navigator.userAgent.indexOf("iPod") > 0
) {
os = "iphone";
console.log("iPhone");
} else if (navigator.userAgent.indexOf("Android") > 0) {
os = "android";
console.log("Android");
} else {
os = "pc";
console.log("PC");
}
document.querySelector("#os").value = os;
let map;
let human;
window.addEventListener("DOMContentLoaded", init);
if (os == "iphone") {
window.addEventListener(
"deviceorientation",
detectDirection,
true
);
} else if (os == "android") {
window.addEventListener(
"deviceorientationabsolute",
detectDirection,
true
);
}
// DOM初期化
function init() {
// 初回に現在地緯度経度を取得
navigator.geolocation.getCurrentPosition(
initMap,
err => {
alert(err.message);
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
}
// Map初期化
function initMap(initPos) {
// #mapidにOSMタイルマップをレンダリング
map = L.map("mapid").setView(
[initPos.coords.latitude, initPos.coords.longitude],
17
);
L.tileLayer(
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
{
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}
).addTo(map);
// 現在地緯度経度を継続取得
let watchId = navigator.geolocation.watchPosition(
pos => {
moveMapFollowingHuman(
pos.coords.latitude,
pos.coords.longitude,
pos.coords.heading
);
},
err => {
window.alert(err.message);
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
}
// 現在地変更ハンドラ
function moveMapFollowingHuman(latitude, longitude, heading) {
// 現在地circle描画を削除
if (human) {
map.removeLayer(human);
}
// 現在地circle描画
human = L.circle([latitude, longitude], {
color: "blue",
fillColor: "#30f",
fillOpacity: 0.5,
radius: 10
}).addTo(map);
human._path.id = "human";
// 現在地を示すエレメントの画面位置座標を取得
var clientRect = human._path.getBoundingClientRect();
// 画面の左端から、要素の左端までの距離
var x = clientRect.left;
// 画面の上端から、要素の上端までの距離
var y = clientRect.top;
let beam = document.querySelector("#beam");
let h = beam.clientHeight;
let w = beam.clientWidth;
beam.style.top = y - 40 + "px";
beam.style.left = x - 30 + "px";
}
function detectDirection(e) {
let absolute = event.absolute;
let alpha = event.alpha;
let beta = event.beta;
let gamma = event.gamma;
let degrees;
if (os == "iphone") {
degrees = e.webkitCompassHeading;
} else {
degrees = compassHeading(alpha, beta, gamma);
}
document.querySelector("#degree").value = degrees;
let beam = document.querySelector("#beam");
beam.style.transform = "rotate(" + degrees + "deg)";
let iPhone = document.querySelector("#iPhone");
iPhone.value = e.webkitCompassHeading;
let accuracy = document.querySelector("#accuracy");
accuracy.value = e.webkitCompassAccuracy;
}
function compassHeading(alpha, beta, gamma) {
var degtorad = Math.PI / 180; // Degree-to-Radian conversion
var _x = beta ? beta * degtorad : 0; // beta value
var _y = gamma ? gamma * degtorad : 0; // gamma value
var _z = alpha ? alpha * degtorad : 0; // alpha value
var cX = Math.cos(_x);
var cY = Math.cos(_y);
var cZ = Math.cos(_z);
var sX = Math.sin(_x);
var sY = Math.sin(_y);
var sZ = Math.sin(_z);
// Calculate Vx and Vy components
var Vx = -cZ * sY - sZ * sX * cY;
var Vy = -sZ * sY + cZ * sX * cY;
// Calculate compass heading
var compassHeading = Math.atan(Vx / Vy);
// Convert compass heading to use whole unit circle
if (Vy < 0) {
compassHeading += Math.PI;
} else if (Vx < 0) {
compassHeading += 2 * Math.PI;
}
return compassHeading * (180 / Math.PI); // Compass Heading (in degrees)
}
function permitGeolocation() {
DeviceOrientationEvent.requestPermission()
.then(response => {
if (response === "granted") {
window.addEventListener(
"deviceorientation",
detectDirection
);
}
})
.catch(console.error);
}
</script>
</body>
</html>
動作確認
HTTPSサーバにindex.htmlを配備して、iPhone8(iOS13)+ Safariで動作確認。スマホを剥けた側にビームが向くようになりました。Android + Chromeでも同じ挙動です。
まとめ
DeviceOrientaitonイベントを使い、方角が0~360の範囲で分かりさえすれば、地図上で向きを表現することはそれほど難しくない事が分かりました。
やっぱり見た目に動きがあるとテンション上がりますね。
現在地はLeafletのcircle関数で作り、ビームはCSSで作ったので、地図のズームで表示位置がおかしくなるとかの問題点もあるサンプルですが、そこはまぁ・・・実際に実戦投入する際に考えればいいかな、といった感じです(適当人間)