Google Maps API V3 と Gears Geolocation API 使って Android のブラウザで現在位置情報を取得する | クレコ では、Androidのブラウザで現在位置情報取得してTwitterにポストするというJavaScriptを作りました。
今回は、それのiPhone版です。要 iPhone OS 3.0以上なので、まだアップデートされてない方は本日のアップデートを楽しみに待ちましょう。
iPhone OS 3.0 のブラウザ Mobile Safari でGPS現在位置情報が取得できるようになった
Mobile Safari の W3C Geolocation API 対応
ついに iPhone Safari ブラウザから位置情報を取得できるようになります – Cirius Lab. ブログ から知ったのですが、iPhone OS 3.0 に標準搭載されているブラウザ Mobile Safari が W3C の Geolocation API に対応して、ブラウザから JavaScript を用いることによって、GPSの現在位置情報を取得することが可能になりました。
これによって、いままでネイティブアプリでしかできなかったことが、簡単に敷居なく実装することが可能です。
W3CのGeolocation API って?
Geolocation API Specification
W3Cで標準化を進めている、位置情報を取得するためのAPI使用のことです。
位置情報を取得するメソッドには、getCurrentPosition() と watchPosition() があり、前者は現在の位置情報を取得するメソッド、後者は連続した位置情報を取得するメソッドとなっています。
つまり、従来の携帯電話ではできなかった、連続した位置情報を扱うGPSトラッキングなどもJavaScriptを記述するだけで実現できるのです!
実際に現在位置情報をTwitterへポストするJavaScriptを作ってみた
JavaScript ソース
<html> <head> <meta name="viewport" content="initial-scale=1.0, user-scalable=no" /> <meta http-equiv="content-type" content="text/html; charset=UTF-8"/> <title>GeoPo : Mobile Safari</title> <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script> <script type="text/javascript"> /* * GeoPo Encode in JavaScript * @author : Shintaro Inagaki * @param location (Object) * @return geopo (String) */ function geopoEncode(location){ // 64characters (number + big and small letter + hyphen + underscore) var chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"; var geopo = new String(); var lat = parseFloat(location.lat); // Parse as float var lng = parseFloat(location.lng); // Parse as float var scale = parseInt(location.scale); // Parse as int // Change a degree measure to a decimal number lat = (lat + 90) / 180 * Math.pow(8, 10); lng = (lng + 180) / 360 * Math.pow(8, 10); // Compute a GeoPo code from head and concatenate for(var i = 0; i < scale; i++) { geopo = geopo + chars.substr(Math.floor(lat / Math.pow(8, 9 - i) % 8) + Math.floor(lng / Math.pow(8, 9 - i) % 8) * 8, 1); } return geopo; } var lat; var lng; var latLng; var geocoder = new google.maps.Geocoder(); var map; var infowindow = new google.maps.InfoWindow(); var marker; var watchId; function update(position) { lat = position.coords.latitude; lng = position.coords.longitude; latLng = new google.maps.LatLng(lat,lng); if(!map){ var options = { zoom: 15, center: latLng, mapTypeId: google.maps.MapTypeId.ROADMAP, scaleControl: true, } map = new google.maps.Map(document.getElementById("map_canvas"), options); } else{ map.set_center(latLng); } if(!marker){ marker = new google.maps.Marker({ position: latLng, map: map, title: "現在地", }); google.maps.event.addListener(marker, 'click', function() { stopUpdate() geocoding(); }); google.maps.event.addListener(infowindow, 'closeclick', function() { startUpdate() }); } else{ marker.set_position(latLng); } } function geocoding() { // geopo var location = new Object(); location.lat = lat; location.lng = lng; location.scale = 7; var geopo = geopoEncode(location); geocoder.geocode({'latLng': latLng}, function(results, status) { if (status == google.maps.GeocoderStatus.OK) { var geocodeAddress; for(i=1; i<results.length; i++){ if (results[i].types.length >= 2 && results[i].types[1] == "political") { if(results[i].formatted_address.indexOf("日本") != -1){ geocodeAddress = results[i].formatted_address.substring(2); }else{ geocodeAddress = results[i].formatted_address; } break; } } if(geocodeAddress){ infowindow.set_content('<strong>現在地:</strong><br /><span style="font-size:80%">' + geocodeAddress + '</span><hr /><a href="http://twitter.com/home?status=' + encodeURIComponent(' L:' + geocodeAddress + ' http://geopo.at/' + geopo) + '" target="twitter"><img src="icon_twitter.gif" width="14" height="16" border="0" align="bottom" hspace="5" />TwitterにPOSTする</a>'); infowindow.open(map, marker); } else { alert("現在地が取得できませんでした><"); } } else { alert("Geocoder failed due to: " + status); } }); } function startUpdate() { watchId = navigator.geolocation.watchPosition(update); } function stopUpdate() { navigator.geolocation.clearWatch(watchId); } startUpdate(); </script> </head> <body style="margin:0px; padding:0px;"> <div id="map_canvas" style="width: 100%; height: 100%;"></div> </body> </html>
前回の数倍丁寧に解説
前回のAndroid版では説明が省略されすぎて、説明になってなかった反省を踏まえて丁寧に解説。
HTML
前回と同様、Google Maps API V3 を使います。
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
これは、iPhoneのためのmeta要素です。地図上でピンチイン、ピンチアウトを使った拡大縮小できるように、user-scalableはnoにして、HTMLでの拡大縮小はできなくしてます。
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script>
Google Maps API V3のJavaScript読み込みですが、位置情報を取得できるデバイスではsensorパラメータをtrueにします。
<body style="margin:0px; padding:0px;"> <div id="map_canvas" style="width: 100%; height: 100%;"></div> </body>
divタグで地図を表示する領域を作成、めいっぱいにするため、縦横100%にし、bodyタグでは余白をなしにするCSS。
うん、丁寧な説明ですね。
GeoPoエンコードライブラリ
ジオポ – 位置情報を短縮して使いやすく
GeoPo(ジオポ)というのは、私が作成したWebサービスで。
ジオポは位置情報を表す緯度・経度を短縮したURLに変換し、受け取る側のブラウザに合わせた地図を表示するウェブサービスで、会員登録なしに誰でも無料で利用できます。
という、Twitterに位置情報を付加したいときに使うといいよ!的なサービス。詳しくは、ジオポの特徴 や 開発者向け情報 を見てね。よかったら、実装してください。
ソース上では、
/* * GeoPo Encode in JavaScript * @author : Shintaro Inagaki * @param location (Object) * @return geopo (String) */ function geopoEncode(location){ // 64characters (number + big and small letter + hyphen + underscore) var chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"; var geopo = new String(); var lat = parseFloat(location.lat); // Parse as float var lng = parseFloat(location.lng); // Parse as float var scale = parseInt(location.scale); // Parse as int // Change a degree measure to a decimal number lat = (lat + 90) / 180 * Math.pow(8, 10); lng = (lng + 180) / 360 * Math.pow(8, 10); // Compute a GeoPo code from head and concatenate for(var i = 0; i < scale; i++) { geopo = geopo + chars.substr(Math.floor(lat / Math.pow(8, 9 - i) % 8) + Math.floor(lng / Math.pow(8, 9 - i) % 8) * 8, 1); } return geopo; }
という GeoPoエンコード ライブラリ部分と、
// geopo var location = new Object(); location.lat = lat; location.lng = lng; location.scale = 7; var geopo = geopoEncode(location);
呼び出し部分。
lat:緯度、lng:経度、scale:縮尺を渡して、geopoというジオポコードの文字列が返ってくる。
位置情報の取得制御
W3CのGeolocation APIを使って、現在位置情報を取得する。
function startUpdate() { watchId = navigator.geolocation.watchPosition(update); } function stopUpdate() { navigator.geolocation.clearWatch(watchId); } startUpdate();
ここでは、watchPosition() を使う。というのも、iPhoneでは getCurrentPosition() がうまく動かなかった。また、最初の1回だけの位置情報取得では位置情報の精度がかなり荒いので、連続して取得し精度が高くなった段階で、その位置情報を採用するという形にしたかったというのもある。
ここで、startUpdate() と stopUpdate() とわざわざ関数にしているのは、後々に取得動作の制御したいためです。
位置情報の加工と地図の表示
位置情報が取得できたときに呼ばれる update() では、位置情報を加工して、地図を表示するようにします。
var lat; var lng; var latLng; var geocoder = new google.maps.Geocoder(); var map; var infowindow = new google.maps.InfoWindow(); var marker; var watchId; function update(position) { lat = position.coords.latitude; lng = position.coords.longitude; latLng = new google.maps.LatLng(lat,lng); if(!map){ var options = { zoom: 15, center: latLng, mapTypeId: google.maps.MapTypeId.ROADMAP, scaleControl: true, } map = new google.maps.Map(document.getElementById("map_canvas"), options); } else{ map.set_center(latLng); } if(!marker){ marker = new google.maps.Marker({ position: latLng, map: map, title: "現在地", }); google.maps.event.addListener(marker, 'click', function() { stopUpdate() geocoding(); }); google.maps.event.addListener(infowindow, 'closeclick', function() { startUpdate() }); } else{ marker.set_position(latLng); } }
position.coords.latitude には緯度、 position.coords.longitude には経度
interface Coordinates { readonly attribute double latitude; readonly attribute double longitude; readonly attribute double altitude; readonly attribute double accuracy; readonly attribute double altitudeAccuracy; readonly attribute double heading; readonly attribute double speed; };
他にも、altitude:高度、accuracy:水平方向の精度、altitudeAccuracy:垂直方向の精度、heading:方向、speed:速度 といった属性があります。iPhone 3G S では、コンパスによって方向なども取得できるかもしれません。iPhone 3Gでは、方向には null値、速度には 0 が入るようです。
map や marker は、初回はインスタンスを生成し、2回目以降の呼び出しでは地図は中心地への移動、マーカーは位置情報取得値への書き換えを行います。
marker の初回で、イベントリスナーを作成。上のイベントは、マーカーをクリックしたら位置情報取得動作を止めて情報ウィンドウ( geocoding() で現在位置の住所やTwitterへポストするアンカーを記載)の表示、下のイベントは、情報ウィンドウを閉じたら位置情報取得動作の再開。
ここで、位置情報の取得動作を操作してるのには次の理由があります。
- 情報ウィンドウが表示されるときに、位置情報が更新されて地図表示が移動されたりすると使い勝手が悪い。
- 位置情報の取得のたびに情報ウィンドウの中身を更新すると大変(逆ジオコーディングとか)。
情報ウィンドウの更新(逆ジオコーディング)
現在位置の住所取得(逆ジオコーディング)とジオポコードの作成、Twitterへポストするためのアンカーを情報ウィンドウの中に書き込むための処理を geocoding() でやります。
function geocoding() { // geopo var location = new Object(); location.lat = lat; location.lng = lng; location.scale = 7; var geopo = geopoEncode(location); geocoder.geocode({'latLng': latLng}, function(results, status) { if (status == google.maps.GeocoderStatus.OK) { var geocodeAddress; for(i=1; i<results.length; i++){ if (results[i].types.length >= 2 && results[i].types[1] == "political") { if(results[i].formatted_address.indexOf("日本") != -1){ geocodeAddress = results[i].formatted_address.substring(2); }else{ geocodeAddress = results[i].formatted_address; } break; } } if(geocodeAddress){ infowindow.set_content('<strong>現在地:</strong><br /><span style="font-size:80%">' + geocodeAddress + '</span><hr /><a href="http://twitter.com/home?status=' + encodeURIComponent(' L:' + geocodeAddress + ' http://geopo.at/' + geopo) + '" target="twitter"><img src="icon_twitter.gif" width="14" height="16" border="0" align="bottom" hspace="5" />TwitterにPOSTする</a>'); infowindow.open(map, marker); } else { alert("現在地が取得できませんでした><"); } } else { alert("Geocoder failed due to: " + status); } }); }
前回のAndroid版とほぼ同じです(多少変えたってこと)。
逆ジオコーディングした結果で、郵便番号や道路名ってのをフィルタリングし、formatted_address には「日本」という文字が入るのが気にくわないので、「日本」を取り除いてやる。という内容。
実装して iPhone OS 3.0 で動作させてみる
実際に動作させてみる。
GeoPo : Mobile Safari @inagaki.co.uk に作ったものをおいたので、自由に試してみてください。ソースコードも自由に使ってください。
iPhoneでの位置情報利用の確認
初回のみSafariでの位置情報利用と、ドメイン単位での位置情報利用の確認があります。
#これ、ドメイン単位の位置情報利用確認ってリセットする方法あるんですかね?
地図と情報ウィンドウの表示
ブックマークなどからURLを読み出せば、そのまま現在位置を取得して地図を表示するという一連の動作ができます。
そして、この地図はスマートフォンに最適化された Google Maps API V3 で作成していますので、まるでネイティブアプリのような感覚でシームレスなスクロールが可能。
マーカーをクリックすることで、情報ウィンドウを表示。このときに、位置情報取得動作をストップさせて、逆ジオコーディングを行い住所を取得。
住所表示の下にある、アンカーをクリックすることで別ウィンドウが立ち上がりTwitterへ現在地情報のついたポストが行えます。
情報ウィンドウの右上の×印をクリックすれば、情報ウィンドウは非表示になり、位置情報取得取得動作を再開させます。現在地が移動した場合はリロードしなくても、情報ウィンドウの開閉だけで位置情報取得のコントロールができます。
Twitterへ現在地情報の投稿
アンカーをクリックするとTwitterの投稿のために新しいウィンドウが立ち上がります。このときに、Twitterへログインされてない場合は、ログイン画面となるので、ログインを行ってください。
また、iPhoneからTwitterを使用する場合、デフォルトではモバイル版の画面表示となっています。しかし、モバイル版の画面表示ですと、アンカーをクリックした際に伝える住所やジオポURLが反映されないので、ページ下部にある「スタンダード版で見る」をクリックし、スタンダード版でTwitterを表示させてください。
#これ、回避方法を知ってる方いましたら教えてください
スタンダード版では、いまなにしてる?のテキストエリアの中に、住所とジオポURLが反映されている状態となります。そこから、メッセージを加えたり、ジオポURLの縮尺精度変更操作を行うことができます。
感想、まとまらないまとめ
連続した位置情報が取得できるのは熱いですね!!でも、iPhoneも熱くなります ><
位置情報を連続取得させるのは非常にバッテリーを消費します、ほどほどにしましょう。
今までGPSロガーなどがないとGPSトラッキングできなかったのですが、それがJavaScriptだけで実現できるのは魅力的です。私もiPhone単体でできるGPSトラッキングサービスを早速作りたいと思っていますよ。
また、すでにあるGPSトラッキングサービスでも簡単(上のソースを数行いじるだけ)に実現できるので、今ココなう!(β) などの人気のあるサービスが対応すれば一気に対応端末が増えるようになるんじゃないでしょうか?位置情報を使うことの面白さがいろんな人に伝わってくれるとうれしいですね。