石澤知広

Google Maps API V3でiPhoneようゴルフナビを作りたい。

2009-08-27 01:33:00

僕は、とってもスコアは悪いですが、下手の横好きでゴルフをしています。
ちょっとお金は掛かるスポーツなのですが、広い芝生の上でボールを追いかけるのはとても気持ち良いので大好きです。

先日も、前職でお世話になった動画配信サービスを行っている会社の皆さんとゴルフコンペをしてきたのですが、参加者の中にアトラスゴルフナビイーグルビューをお持ちの方がいらっしゃいました。
これは、GPSで取得した位置情報を収録されているコース情報に重ね、いろいろな情報を提供してくれるとても優れたツールで、前々からほしいなーと思っていた代物です。
ゴルフではボールを飛ばしたい地点までの距離に応じて、僕の場合は大体20ヤード単位くらいでクラブを選択するのですが、往々にして目視での距離測定は狂ってしまいますし、打ち下ろし(グリーンが打つ場所より低い場所にある。)の場合などはかなり痛い目にあってしまいます。
ゴルフナビやイーグルビューは現在位置から目的地点までの飛距離を可視化してくれる上に、打ち出す方向を示してくれたり、打数を記録してくれる機能もあったりするのです。
実際に使っているところを見たのは実は今回のコンペが初めてだったのですが、やっぱりいいなーって思いました。なんかプレーをしていて安心感が違う印象を持ちました。
決して凄く高い!というわけでは無いのですが、結婚し最近マンションも買ったお小遣い制の僕としては、そもそも値下がりしたIntelのSSDですらまだ悩んで買えていない状況でなかなかこれを買うのには勇気がいるのです。

と、前振りが長くなってしまったのですが、ゴルフナビはまだ買えないけど、iPhoneは持っているしGoogle Maps APIもV3になったという事で(ちょっと遅いですねw)自作してみる事にしました。

必要な機能は次の3です。
・自分の今いる地点から、任意の地点までの距離を算出表示
・スタイミーなコース用に打ち出す方向を示す。
・スコアを記録してくれるとなおよし!

今回は、一番基本的な「自分の今いる地点から、任意の地点までの距離を算出表示」を実装してみたいと思います。
いきなりソースです。わざわざ解説するほどでもないのですが、追って順に解説します。




<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>ゴルフの時とかに使う用(作成中)</title>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script>
<script type="text/javascript">
<!--
var map;
var marker;
var target;
var watchId;
var lat,lng,latLng;
var infowindow;
var TARGET_FLG;
Number.prototype.toRad = function(){
return this * Math.PI / 180;
}
function initialize(){
latLng = new google.maps.LatLng(35.691955, 139.758421); //会社付近(とりあえず)
var opt = {
zoom             : 18,
center           : latLng,
scaleControl     : false,
mapTypeId        : google.maps.MapTypeId.ROADMAP
}
map = new google.maps.Map(document.getElementById("map_canvas"), opt);

marker = new google.maps.Marker({
name     : "you",
position : latLng,
draggable: true,
map      : map,
icon     : "/images/you.png",
title    :"You"
});
google.maps.event.addListener(map, 'click', function(event){
spearMarker(event.latLng);
});

google.maps.event.addListener(marker, 'click', function(){
stopUpdate();
});
watchId = navigator.geolocation.watchPosition(update);
}

function spearMarker(location){
if(target) target.set_map(null);
target = new google.maps.Marker({
position: location,
map     : map
});
TARGET_FLG = true;
attachMessage();
}

function attachMessage(){
google.maps.event.addListener(target, 'click', function(){
if(TARGET_FLG){
var y  = marker.get_position();
var t  = target.get_position();
var yd = getYard(y.lat(),y.lng(),t.lat(),y.lng());
if(infowindow) infowindow.close();
infowindow = new google.maps.InfoWindow({
content: yd + ' ヤードくらい',
size: new google.maps.Size(25,25)
});
infowindow.open(map,target);
TARGET_FLG = false;
}else{
if(infowindow) infowindow.close();
if(target) target.set_map(null);
}
});
}

function update(position){
lat = position.coords.latitude;
lng = position.coords.longitude;
latLng = new google.maps.LatLng(lat, lng);
map.set_center(latLng);
marker.set_position(latLng);
}

function stopUpdate(){
navigator.geolocation.clearWatch(watchId);
}

function getYard(lat1, lon1, lat2, lon2) {
  var R = 6371;
  var YD = 1.0936133;
  var dLat = (lat2-lat1).toRad();
  var dLon = (lon2-lon1).toRad();
  lat1 = lat1.toRad(), lat2 = lat2.toRad();

  var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
          Math.cos(lat1) * Math.cos(lat2) * 
          Math.sin(dLon/2) * Math.sin(dLon/2);
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  var d = R * c;
  return (d * 1000 * YD).toFixed(1);
}

// -->
</script>


<div id="map_canvas" style="width: 100%; height: 100%;"></div>



こんな感じです。

まず、Google Maps API V3の読み込みをします。現時点では、http://maps.gstatic.com/intl/ja_ALL/mapfiles/api-3/12a/main.js というバージョンでした。

<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script>

V2と違って、APIキーがいらないので、すっきりする感じですね。sensorパラメータは必須パラメータなのですが、GPSからなどのセンサー??位置情報を使いますかという指定です。使うのでtrueにします。

function initialize(){
latLng = new google.maps.LatLng(35.691955, 139.758421); //会社付近(とりあえず)
var opt = {
zoom             : 18,
center           : latLng,
scaleControl     : false,
mapTypeId        : google.maps.MapTypeId.ROADMAP
}
map = new google.maps.Map(document.getElementById("map_canvas"), opt);

marker = new google.maps.Marker({
name     : "you",
position : latLng,
draggable: true,
map      : map,
icon     : "/images/you.png",
title    :"You"
});
google.maps.event.addListener(map, 'click', function(event){
spearMarker(event.latLng);
});

google.maps.event.addListener(marker, 'click', function(){
stopUpdate();
});
watchId = navigator.geolocation.watchPosition(update);
}

次に、onloadで呼ばれる、initialize処理です。ここでは、弊社オフィスがある付近の経度、緯度を初期値で渡しLatLngオブジェクトを作り、続いて地図本体のMapオブジェクト、自分の位置を示すMarkerオブジェクトを作っています。そして、MapオブジェクトとMarkerオブジェクトがクリックされた時のイベントハンドラの登録もしています。Mapオブジェクトがクリックされた際には新たなMarkerを作り、Markerオブジェクトがクリックされた際には、後述する位置監視の停止を行っています。
Mapオブジェクトのオプションでzoomを18にしているのは、テストで小金井カントリークラブにMarkerを置いたときちょうどよさそうだったからです。最後にwatchPositionを呼び出して、継続した位置情報の取得を行わせています。引数にしているのはコールバック関数名です。

function spearMarker(location){
if(target) target.set_map(null);
target = new google.maps.Marker({
position: location,
map     : map
});
TARGET_FLG = true;
attachMessage();
}

続いて、Mapオブジェクトのクリックイベントで呼ばれるこの関数は、ターゲットとなるMarkerの作成を行っています。作成したMarkerがクリックされた時や、Map上の別の場所をクリックされた際にMarkerを移動するように、TARGET_FLGというフラグを設けています。

function attachMessage(){
google.maps.event.addListener(target, 'click', function(){
if(TARGET_FLG){
var y  = marker.get_position();
var t  = target.get_position();
var yd = getYard(y.lat(),y.lng(),t.lat(),y.lng());
if(infowindow) infowindow.close();
infowindow = new google.maps.InfoWindow({
content: yd + ' ヤードくらい',
size: new google.maps.Size(25,25)
});
infowindow.open(map,target);
TARGET_FLG = false;
}else{
if(infowindow) infowindow.close();
if(target) target.set_map(null);
}
});
}

attachMessageという名の関数なのですが、TARGET_FLGをみてターゲット用Markerを消す処理までやってます(うーん。名前と機能が一致していない)。ここでは後述するgetYard関数を呼んで距離を取得しています。Google Maps API V3ではまだdistanceFromが実装されいません。その為、今時点だと2点間の距離は自前で計算してやらなくてはいけない感じです。Ver3.12aでは.Hh()関数の定義以下でどうやらHaversine公式に近いような事をやろうとしているように見えるので(間違っていたらごめんなさいー)ひょっとしたら、distanceFrom(もしくは変わるもの)の復活は間近かかもしれませんねー

function update(position){
lat = position.coords.latitude;
lng = position.coords.longitude;
latLng = new google.maps.LatLng(lat, lng);
map.set_center(latLng);
marker.set_position(latLng);
}

これは、watchPosition関数のコールバックで呼ばれる関数です。watchPositionで取得した経度・緯度をMapとMarkerに更新しています。

function stopUpdate(){
navigator.geolocation.clearWatch(watchId);
}

この関数はwatchPositionを止めるものです。自分のMarkerをクリックした際に呼ばれるようにしました。watchPositionを止めないと、ちらちらちらちらMarkerが動いてしまうので、ゴルフ場での利用を想定するとある程度自分のいる近い位置に来たら、止めちゃってもいいかなと思いまし。ちなみに上記で書き忘れましたが自分の位置を表すMarkerはオプションで「draggable: true」を指定しているのでドラックして動かす事が可能です。2点間の距離計算はターゲット用のMarkerをクリックした時点で計算しているので、自分Markerを動かしちゃっても再計算されます。

function getYard(lat1, lon1, lat2, lon2) {
  var R = 6371;
  var YD = 1.0936133;
  var dLat = (lat2-lat1).toRad();
  var dLon = (lon2-lon1).toRad();
  lat1 = lat1.toRad(), lat2 = lat2.toRad();

  var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
          Math.cos(lat1) * Math.cos(lat2) * 
          Math.sin(dLon/2) * Math.sin(dLon/2);
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  var d = R * c;
  return (d * 1000 * YD).toFixed(1);
}

はい。最後にこれがHaversine公式という奴です。詳細はGoogle先生に聞いてください!!実際僕も数学が超得意というわけではないのでちょっと難しすぎてよく分かりませんw。

本当は、2点間に線を引きたかったのですが、これまた、現時点のバージョンだとpolylineが使えないので保留にしました!
そんなこんなで出来上がったのが、こちらです。
FireFox3.5なら見れるはずです。iPhoneで見るとこんな感じです。

ちょっとiPhoneで近所を歩いてみた感じだと、正直なところいまいちな感じです。。なんかやっぱり自前の距離計算が微妙な感じです。
そして、Google Maps API V3は基本的に動きが早くてステキです!後はもう少し機能が増える(polylineは是非欲しい。)とうれしいなと思いました。
(gmaps-api-issuesで沢山リクエストが出ているのでそのうち機能UPしますかねー)
まだ、ゴルフ場で使っていないのでどの程度の使い物になるのか分からないのですが今度ラウンドするときは必ず使ってみようと思います。
どなたか直近でゴルフ行かれる予定がある方で且つiPhoneかAndroidをお持ちの方はもしよかったら使ってみてフィードバックください!
でも、問題はゴルフ場でSoftbankの3G回線が使えるかどうかですね。。w

次回
・スタイミーなコース用に打ち出す方向を示す。
・スコアを記録してくれるとなおよし!
の対応をしてまた報告いたします。それでは。

※このエントリは ブロガーにより投稿されたものです。朝日インタラクティブ および ZDNet Japan編集部の見解・意向を示すものではありません。
  • 新着記事
  • 特集
  • ブログ