前回までの連載で、
Webアプリの概要
今回作成するWebアプリは、
また、
 
Webアプリの構成
Webアプリを構成するファイルを以下のように配置します。
 
{
  "name": "Odometer",
  "description": "距離計",
  "version": "0.1",
  "app": {
    "launch": {
      "local_path": "main.html"
    }
  },
  "icons": {
    "16": "icon_16.png",
    "48": "icon_48.png",
    "128": "icon_128.png"
  },
  "permissions": [
    "geolocation",
    "notifications"
  ]
}Packaged Appとして作成するため
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Odometer</title>
    <link rel="stylesheet" href="./css/odometer.css">
    <script src="http://maps.google.com/maps/api/js?sensor=true"></script>
    <script src="./js/odometer.js"></script>
</head>
<body>
<div id="map-container">
    <div id="map"></div>
</div>
<div id="data-container">
    <form id="search">
        <input type="text" id="address" placeholder="住所など">
        <input type="submit" value="検索">
    </form>
    <p>地図をクリックして目的地を設定してください</p>
    <section id="distance-container">
        <h1>- 目的地までの距離 - </h1>
        
        <p style="">約 <span id="distance">0</span> km<br>
        <input type="button" value="自動更新開始" id="auto-update"></p>
        
    </section>
    
    <section>
        <h1>- 目的地 -</h1>
        <table>
            <tr>
                <td width="80px">緯度</td>
                <td width="200px" id="dest-latitude"></td>
            </tr>
            <tr>
                <td>経度</td>
                <td id="dest-longitude"></td>
            </tr>
        </table>
        <input type="button" value="目的地を表示" id="move-dest">
    </section>
    
    <section>
        <h1>- 現在地 -</h1>
        <table>
            <tr>
                <td width="80px">緯度</td>
                <td width="200px" id="latitude"></td>
            </tr>
            <tr>
                <td>経度</td>
                <td id="longitude"></td>
            </tr>
            <tr>
                <td>精度</td>
                <td id="accuracy"></td>
            </tr>
            <tr>
                <td>移動方向</td>
                <td id="heading"></td>
            </tr>
            <tr>
                <td>移動速度</td>
                <td id="speed"></td>
            </tr>
            <tr>
                <td>日時</td>
                <td id="timestamp"></td>
            </tr>
        </table>
        <input type="button" value="現在地を表示" id="move-current">
    </section>
</div>
</body>
</html>Geolocation API
Geolocation APIは、
位置情報の取得
まずは、
//オプション
var geoOptions = {
    enableHighAccuracy: true,   //高精度要求
    timeout: 6000,          //タイムアウト(ミリ秒)
    maximumAge: 0       //キャッシュ有効期限(ミリ秒)
}
if (navigator.geolocation) {
    
    //現在地の位置情報取得
    navigator.geolocation.getCurrentPosition(
        init,       //成功時コールバック
        onError,    //失敗時コールバック
        geoOptions  //オプション
    );
} else {
    return;
}現在の位置情報を取得する場合は、
/*
 * 初期表示
 */
function init(position){
    
    //地図作成
    createMap(position.coords.latitude, position.coords.longitude);
    
    //現在地情報表示
    showCurrentPosition(position);
    
    //現在地を保持
    currentPos.lat = position.coords.latitude;
    currentPos.lng = position.coords.longitude;
    
}
/*
 * エラーコールバック
 */
function onError(e) {
    alert(e.message + '(' + e.code + ')');
}
/*
 * 現在地情報表示
 */
function showCurrentPosition(position){
    
    //現在地を表示
    //緯度
    document.getElementById('latitude').textContent = 
        position.coords.latitude;
    
    //経度
    document.getElementById('longitude').textContent = 
        position.coords.longitude;
    
    //精度
    document.getElementById('accuracy').textContent = 
        position.coords.accuracy;
    
    //移動方向
    document.getElementById('heading').textContent = 
        position.coords.heading;
    
    //移動速度
    document.getElementById('speed').textContent = 
        position.coords.speed;
    
    //取得日時
    var dt = new Date(position.timestamp);
    document.getElementById('timestamp').textContent =
        dt.getFullYear() + '年' + (dt.getMonth()+1) + '月' + dt.getDate() + '日' + 
        dt.getHours() + '時' + dt.getMinutes() + '分' + dt.getSeconds() + '秒';
}initメソッド内では、
| プロパティ | 説明 | 
|---|---|
| coords. | 緯度 | 
| coords. | 経度 | 
| coords. | 精度 | 
| coords. | 高度 ※標高ではないことに注意 | 
| coords. | 高度の精度 | 
| coords. | 移動方向 | 
| coords. | 移動速度 | 
| timestamp | 取得日時 ※本来の仕様上はDate型を返す | 
目的地の設定と距離の算出
目的地の設定は、
/*
 * 目的地情報表示
 */
function showDestinationPosition(lat, lng) {
    
    //目的地を表示
    document.getElementById('dest-latitude').textContent = lat;
    document.getElementById('dest-longitude').textContent = lng;
}
/*
 * 目的地までの距離表示
 */
function showDistance(currentPos, destPos){
    
    //単位をkmに変換して表示
    document.getElementById('distance').textContent =
        Math.round(getDistance(currentPos.lat, currentPos.lng, destPos.lat, destPos.lng)) / 1000;
}
/*
 * 2点間距離計算(m)
 */
function getDistance(lat, lng, dLat, dLng){
    
    //緯度1度あたり111km、経度1度あたり91kmの概算
    var h = Math.abs(dLat - lat) * 111000;
    var v = Math.abs(dLng - lng) * 91000;
    
    return Math.sqrt(Math.pow(h, 2) + Math.pow(v, 2));
}目的地までの距離の算出については、
位置情報の自動更新
次に、
/*
 * 自動更新
 */
var watchId = 0;    //自動更新停止用のID
document.getElementById('auto-update').addEventListener('click', function(){
    
    if ( watchId == 0) {
        
        if ( destPos.lat == 0 && destPos.lng == 0 ) {
            alert('目的地が設定されていません');
            return;
        }
        
        
        //自動更新を開始する
        watchId = navigator.geolocation.watchPosition(function(position){
            
            //地図更新
            updateMap(position.coords.latitude, position.coords.longitude);
            
            //現在地情報表示
            showCurrentPosition(position);
            
            //目的地までの距離表示
            currentPos.lat = position.coords.latitude;
            currentPos.lng = position.coords.longitude;
            showDistance(currentPos, destPos);
            
        }, null, geoOptions);
        
        this.value = '自動更新停止';
        
    } else {
        
        //以前の自動更新を停止する
        navigator.geolocation.clearWatch(watchId);
        watchId = 0;
        this.value = '自動更新開始';
    }
    
}, false);実際の動きですが、
ここまでで、
| メソッド | 説明 | 
|---|---|
| getCurrentPosition(onSuccessCallback, onErrorCallback, options) | 位置情報を取得する | 
| watchPosition(onSuccessCallback, onErrorCallback, options) | 位置情報を継続的に取得する。戻り値として、 | 
| clearWatch(watchId) | 位置情報の自動更新を停止する | 
| オプション | 説明 | 
|---|---|
| enableHighAccuracy | 高精度を要求する | 
| timeout | タイムアウト | 
| maximumAge | キャッシュ有効期限 | 
Notification API
Odometerに新たな機能として、
Notification APIは、
デスクトップへの通知
まずは、
/*
 * デスクトップへ通知
 */
function notify(title, message){
    
    //マニフェストファイルへの記述で許可されている
    if ( webkitNotifications.checkPermission() == 0 ) {
        
        var popup = webkitNotifications.createNotification('icon_48.png', title, message);
        popup.ondisplay = function(){
            setTimeout(function(){
                popup.cancel();
            }, 5000);
        };
        popup.show();
        
    } else {
        
        //デスクトップへの通知許可を要求する
        webkitNotifications.requestPermission();
    }
}webkitNotifications.
もし、
デスクトップへの通知用のポップアップは、
そのほかにも、
//通知済みの距離を保持
var notified = {};
~省略~
//自動更新を開始する
watchId = navigator.geolocation.watchPosition(function(position){
    
    //地図更新
    updateMap(position.coords.latitude, position.coords.longitude);
    
    //現在地情報表示
    showCurrentPosition(position);
        
        //目的地までの距離表示
        currentPos.lat = position.coords.latitude;
        currentPos.lng = position.coords.longitude;
        showDistance(currentPos, destPos);
    
    //デスクトップに通知
    //目的地までの距離を取得してkm単位に変換
    var distance = Math.round(getDistance(currentPos.lat, currentPos.lng, destPos.lat, destPos.lng)) / 1000;
    
    //距離によってしきい値を変える
    var threshold = 0;
    if ( distance < 1 ) {
        
        //1km未満は、200mごとに通知
        threshold = 0.2;
        
    } else if (distance < 10 ) {
        
        //10km未満は、1kmごとに通知
        threshold = 1;
        
    } else {
        
        //10km以上は、10kmごとに通知
        threshold = 10;
    }
    
    //一度通知した距離は再通知しない
    var notifiedKey = Math.floor(distance / threshold) * threshold;
    if ( !notified[notifiedKey]  ) {
        notify('目的地までの距離', '約 ' + distance + ' km');
        notified[notifiedKey] = true;
    }
    
    
}, null, geoOptions);位置情報が更新されるたびに通知されるのは冗長ですので、
 
これで、
| メソッド | 説明 | 
|---|---|
| checkPermission() | デスクトップへの通知に対する許可、 | 
| requestPermission() | デスクトップへの通知の許可を求める | 
| createNotification(iconURL, title, message) | 通知用のポップアップを作成する | 
| createHTMLNotification(htmlURL) | 通知用のポップアップをHTMLから作成する | 
| メソッド | 説明 | 
|---|---|
| show() | デスクトップへ通知する | 
| cancel() | デスクトップへの通知をキャンセルする | 
| onclick | 通知ポップアップのクリックイベントのコールバック | 
| onclose | 通知ポップアップの閉じるイベントのコールバック | 
| ondisplay | 通知ポップアップの表示イベントのコールバック ※ドラフト仕様では、 | 
| onerror | エラー時のコールバック | 
まとめ
今回は、
- Odometer(crxファイル) [14. 6KB] 
- ※
- crxファイルはzipファイルですので、 - 右クリックからのダウンロード後に拡張子をzipに変えていただければ中身を参照できます 
また、
参考
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Odometer</title>
    <link rel="stylesheet" href="./css/odometer.css">
    <script src="http://maps.google.com/maps/api/js?sensor=true"></script>
    <script src="./js/odometer.js"></script>
</head>
<body>
<div id="map-container">
    <div id="map"></div>
</div>
<div id="data-container">
    <form id="search">
        <input type="text" id="address" placeholder="住所など">
        <input type="submit" value="検索">
    </form>
    <p>地図をクリックして目的地を設定してください</p>
    <section id="distance-container">
        <h1>- 目的地までの距離 - </h1>
        
        <p style="">約 <span id="distance">0</span> km<br>
        <input type="button" value="自動更新開始" id="auto-update"></p>
        
    </section>
    
    <section>
        <h1>- 目的地 -</h1>
        <table>
            <tr>
                <td width="80px">緯度</td>
                <td width="200px" id="dest-latitude"></td>
            </tr>
            <tr>
                <td>経度</td>
                <td id="dest-longitude"></td>
            </tr>
        </table>
        <input type="button" value="目的地を表示" id="move-dest">
    </section>
    
    <section>
        <h1>- 現在地 -</h1>
        <table>
            <tr>
                <td width="80px">緯度</td>
                <td width="200px" id="latitude"></td>
            </tr>
            <tr>
                <td>経度</td>
                <td id="longitude"></td>
            </tr>
            <tr>
                <td>精度</td>
                <td id="accuracy"></td>
            </tr>
            <tr>
                <td>移動方向</td>
                <td id="heading"></td>
            </tr>
            <tr>
                <td>移動速度</td>
                <td id="speed"></td>
            </tr>
            <tr>
                <td>日時</td>
                <td id="timestamp"></td>
            </tr>
        </table>
        <input type="button" value="現在地を表示" id="move-current">
    </section>
</div>
</body>
</html>#map-container, #data-container {
    float: left;
}
#data-container {
    padding: 10px;
}
#map {
    width: 600px;
    height: 500px;
}
h1 {
    font-size: 1em;
    
}
#distance-container p {
    font-size: 2em;
    margin-top: 5px;
}document.addEventListener('DOMContentLoaded', function(){
    
    //現在地
    var currentPos = {
        lat: 0,
        lng: 0
    }
    
    //目的地
    var destPos = {
        lat: 0,
        lng: 0
    }
    
    //通知済みの距離を保持
    var notified = {};
    
    //オプション
    var geoOptions = {
        enableHighAccuracy: true,   //高精度要求
        timeout: 6000,              //タイムアウト(ミリ秒)
        maximumAge: 0               //キャッシュ有効期限(ミリ秒)
    }
    
    if (navigator.geolocation) {
        
        //現在地の位置情報取得
        navigator.geolocation.getCurrentPosition(
            init,       //成功時コールバック
            onError,    //失敗時コールバック
            geoOptions  //オプション
        );
    } else {
        return;
    }
    
    /*
     * 初期表示
     */
    function init(position){
        
        //地図作成
        createMap(position.coords.latitude, position.coords.longitude);
        
        //現在地情報表示
        showCurrentPosition(position);
        
        //現在地を保持
        currentPos.lat = position.coords.latitude;
        currentPos.lng = position.coords.longitude;
        
    }
    
    /*
     * エラーコールバック
     */
    function onError(e) {
        alert(e.message + '(' + e.code + ')');
    }
    
    /*
     * 現在地情報表示
     */
    function showCurrentPosition(position){
        
        //現在地を表示
        //緯度
        document.getElementById('latitude').textContent = 
            position.coords.latitude;
        
        //経度
        document.getElementById('longitude').textContent = 
            position.coords.longitude;
        
        //精度
        document.getElementById('accuracy').textContent = 
            position.coords.accuracy;
        
        //移動方向
        document.getElementById('heading').textContent = 
            position.coords.heading;
        
        //移動速度
        document.getElementById('speed').textContent = 
            position.coords.speed;
        
        //取得日時
        var dt = new Date(position.timestamp);
        document.getElementById('timestamp').textContent =
            dt.getFullYear() + '年' + (dt.getMonth()+1) + '月' + dt.getDate() + '日' + 
            dt.getHours() + '時' + dt.getMinutes() + '分' + dt.getSeconds() + '秒';
    }
    
    /*
     * 目的地情報表示
     */
    function showDestinationPosition(lat, lng) {
        
        //目的地を表示
        document.getElementById('dest-latitude').textContent = lat;
        document.getElementById('dest-longitude').textContent = lng;
    }
    
    /*
     * 目的地までの距離表示
     */
    function showDistance(currentPos, destPos){
        
        //単位をkmに変換して表示
        document.getElementById('distance').textContent =
            Math.round(getDistance(currentPos.lat, currentPos.lng, destPos.lat, destPos.lng)) / 1000;
    }
    
    /*
     * 2点間距離計算(m)
     */
    function getDistance(lat, lng, dLat, dLng){
        
        //緯度1度あたり111km、経度1度あたり91kmの概算
        var h = Math.abs(dLat - lat) * 111000;
        var v = Math.abs(dLng - lng) * 91000;
        
        return Math.sqrt(Math.pow(h, 2) + Math.pow(v, 2));
    }
    
    /*
     * 自動更新
     */
    var watchId = 0;    //自動更新停止用のID
    document.getElementById('auto-update').addEventListener('click', function(){
        
        if ( watchId == 0) {
            
            if ( destPos.lat == 0 && destPos.lng == 0 ) {
                alert('目的地が設定されていません');
                return;
            }
            
            //通知済みの距離をリセット
            notified = {};
            
            //自動更新を開始する
            watchId = navigator.geolocation.watchPosition(function(position){
                
                //地図更新
                updateMap(position.coords.latitude, position.coords.longitude);
                
                //現在地情報表示
                showCurrentPosition(position);
                
                //目的地までの距離表示
                currentPos.lat = position.coords.latitude;
                currentPos.lng = position.coords.longitude;
                showDistance(currentPos, destPos);
                
                //デスクトップに通知
                //目的地までの距離を取得してkm単位に変換
                var distance = Math.round(getDistance(currentPos.lat, currentPos.lng, destPos.lat, destPos.lng)) / 1000;
                
                //距離によってしきい値を変える
                var threshold = 0;
                if ( distance < 1 ) {
                    
                    //1km未満は、200mごとに通知
                    threshold = 0.2;
                    
                } else if (distance < 10 ) {
                    
                    //10km未満は、1kmごとに通知
                    threshold = 1;
                    
                } else {
                    
                    //10km以上は、10kmごとに通知
                    threshold = 10;
                }
                
                //一度通知した距離は再通知しない
                var notifiedKey = Math.floor(distance / threshold) * threshold;
                if ( !notified[notifiedKey]  ) {
                    notify('目的地までの距離', '約 ' + distance + ' km');
                    notified[notifiedKey] = true;
                }
                
                
            }, null, geoOptions);
            
            this.value = '自動更新停止';
            
        } else {
            
            //以前の自動更新を停止する
            navigator.geolocation.clearWatch(watchId);
            watchId = 0;
            this.value = '自動更新開始';
        }
        
    }, false);
    
    /*
     * デスクトップへ通知
     */
    function notify(title, message){
        
        //マニフェストファイルへの記述で許可されている
        if ( webkitNotifications.checkPermission() == 0 ) {
            
            var popup = webkitNotifications.createNotification('icon_48.png', title, message);
            popup.ondisplay = function(){
                setTimeout(function(){
                    popup.cancel();
                }, 5000);
            };
            popup.show();
            
        } else {
            
            //デスクトップへの通知許可を要求する
            webkitNotifications.requestPermission();
        }
    }
    
    
    /*
     * Google Maps
     */
    var map,
        marker;
    var mapOptions = {
        zoom: 13,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    };
    
    /*
     * 地図作成
     */
    function createMap(lat, lng) {
        
        //地図作成
        var infowindow = new google.maps.InfoWindow(),
            latLng = new google.maps.LatLng(lat, lng);
        map = new google.maps.Map(document.getElementById("map"), mapOptions);
        
        //マーカー作成
        marker = new google.maps.Marker(
            {
                title: '現在地',
                position: latLng,
                map: map
            }
        );
        
        /*
         * クリックで目的地設定
         */
        var destMarker = null;
        google.maps.event.addListener(map, "click", function(event){
            if ( destMarker ) {
                destMarker.setMap(null);
            }
            destMarker = new google.maps.Marker(
                {
                    title: '目的地',
                    position: event.latLng,
                    map: map
                }
            );
            
            //目的地情報表示
            showDestinationPosition(event.latLng.lat(), event.latLng.lng());
            //目的地までの距離表示
            destPos.lat = event.latLng.lat();
            destPos.lng = event.latLng.lng();
            showDistance(currentPos, destPos);
            
            //通知済みの距離をリセット
            notified = {};
        });
        
        map.setCenter(latLng);
        infowindow.open(map);
    }
    
    /*
     * 地図更新
     */
    function updateMap(lat, lng) {
        
        var latLng = new google.maps.LatLng(lat, lng);
        
        //マーカー作成
        if ( marker ) {
            marker.setMap(null);
        }
        marker = new google.maps.Marker(
            {
                title: '現在地',
                position: latLng,
                map: map
            }
        );
        map.setCenter(latLng);
    }
    
    /*
     * 住所検索
     */
    var geocoder = new google.maps.Geocoder();
    document.getElementById('search').addEventListener('submit', function(event){
        
        //デフォルトのsubmit動作をキャンセル
        event.preventDefault();
        
        var addr = document.getElementById('address').value;
        if ( !addr ) {
            return;
        }
        
        //Geocoding APIで住所から座標を取得する
        geocoder.geocode({ 'address': addr}, function(results, status) {
            if (status == google.maps.GeocoderStatus.OK) {
                
                //最初の候補を表示する
                map.setCenter(results[0].geometry.location);
            } else {
                alert('検索できませんでした');
            }
        });
        
    }, false);
    
    /*
     * 目的地表示
     */
    document.getElementById('move-dest').addEventListener('click', function(){
        map.setCenter(new google.maps.LatLng(destPos.lat, destPos.lng));
    }, false);
    
    /*
     * 現在地表示
     */
    document.getElementById('move-current').addEventListener('click', function(){
        map.setCenter(new google.maps.LatLng(currentPos.lat, currentPos.lng));
    }, false);
    
}, false);