Webアプリを公開しよう! Chrome Web Store/Apps入門

第5回Webアプリを作ろう#2─⁠─Background Pages、Message Passing

前回は、OdometerというWebアプリの作成 を通して、Geolocation API、Notification APIの詳細を解説しました。今回は、Odometerに機能を追加する形でさらにいくつかのAPIの詳細を解説していきたいと思います。また、先日サンフランシスコで行われたGoogle I/Oで発表されたChrome Web Store関連のトピックも合わせて紹介したいと思います。

Google I/OのChrome Web Store関連トピック

アプリ内課金
Google I/Oにて、Chrome Web Storeでのアプリ内課金について発表されました。アプリ内課金の手数料は5%とのことで、ほかのプラットフォームと比べて非常に低価格になっています。これによって、Webアプリの中から電子書籍や音楽などのコンテンツを購入することができるようになります。
日本語化と各国言語へのローカライズ
Chrome Web Storeの日本語化がおこなわれ、日本語を含む41の言語にローカライズされました。これによって、Chromeの全ユーザーである1億6000万人がChrome Web Storeへアクセス可能になったとのことです。
Chromebook
Chrome OS搭載のノートブックであるChromebookが米国で6月に販売開始となるとのことです。Chrome OS上で動作するアプリは、基本的にはChrome Web StoreからWebアプリとしてダウンロードされるものになります。

Chrome Web Storeとそれを取り巻く環境はますます大きく広がっています。日本では、まだ課金の仕組みなどが提供されていないため正式なオープンはまだですが、今回の日本語化に合わせて既に日本向けのWebアプリが12個公開されています。興味のある方は是非チェックしてみてください。また、今後の日本での正式オープンに合わせて何かWebアプリを作ってみるのも面白いかもしれません。

図1 Chrome Web Store(日本語)
図1 Chrome Web Store(日本語)

追加機能の概要

ここからは、前回作成したWebアプリの追加機能について考えていきます。Odometerは、地図上で目的地を設定し、目的地までの距離が近づくとデスクトップにポップアップを表示して残りの距離を通知するというものでした。しかしながら、このままではOdometerのタブを閉じるか、ブラウザを終了した場合には残りの距離が通知されなくなってしまいます。そのため、Webアプリの任意のページをバックグラウンドで動作させるBackground Pagesの仕組みを使って、いつでも通知がおこなわれるように変更してみます。

Webアプリの構成

Webアプリを構成するファイルにbackground.htmlとbackground.jsを追加します。これらのファイルには、Webアプリのうちバックグラウンドで動作させる処理を記述します。

画像
manifest.json
{
  "name": "Odometer",
  "description": "距離計",
  "version": "0.2",
  "app": {
    "launch": {
      "local_path": "main.html"
    }
  },
  "icons": {
    "16": "icon_16.png",
    "48": "icon_48.png",
    "128": "icon_128.png"
  },
  "background_page": "background.html",
  "permissions": [
    "geolocation",
    "notifications",
    “background”
  ]
}

“background_page⁠にバックグラウンドで動作させるHTMLファイルを指定します。Webアプリをバックグラウンドで動作させるBackground Pagesの詳細については後述します。また、⁠permissions⁠⁠background⁠を指定しています。これによって、ブラウザを閉じてもChromeを明示的に終了させなければ、バックグラウンドページが動作し続けることができます。

図2 バックグラウンド動作しているOdometer
図2 バックグラウンド動作しているOdometer

Background Pages

Background Pagesは、Webアプリの任意のページをバックグラウンドで動作させるための仕組みです。タスクや状態管理など継続的に動作させたい処理や重たい計算処理、ファイルのアップロードやダウンロードなどの時間がかかる処理などを記述します。

バックグラウンドで動作させるHTMLファイルはマニフェストファイルで指定します。ここで指定したバックグラウンドページは、ひとつのWebアプリでひとつのインスタンスとなりますので、例えばWebアプリを複数のタブで開いていたとしてもバックグラウンドで動作するページはひとつです。

Hosted Appsの場合、Background Pagesの代替としてwindow.openメソッドを使って複数の任意のページをバックグラウンド動作させることのできるBackground Windowという仕組みを持っています。Background Window は、Chrome 12からwindow.openメソッドの利用のほかに⁠background_page⁠での指定が可能になります。ただし、指定できるページはHTTPSで提供されている必要があります。Background Windowについての詳細は、こちらを参照してください。

バックグラウンド動作させる処理

バックグラウンドページはHTMLファイルですので、JavaScriptだけでなく、HTMLタグを使った柔軟な処理を記述することができます。例えば<audio>要素を使って音楽を再生したり、<canvas>要素で描画した画像データをWebアプリに送ったりといった内容が可能です。

今回のOdometerへの機能追加では、特にそういった処理はないので、純粋にJavaScriptファイルのみを読み込んでいます。残りの距離の通知をバックグラウンドページから行うためには、現在地と目的地などの情報をバックグラウンドページで参照できる必要があるため、フロント側のodometer.jsからGeolocation APIの処理と各種設定周りのコードを分離し、バックグラウンドページへ配置します。地図の描画などのユーザーインターフェース部分はそのままodometer.jsに残します。

変更前のodometer.js(一部抜粋)
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;
    
}
変更後のodometer.js(一部抜粋)
/*
 * 初期表示
 */
function init(position){
    
    //地図作成
    createMap(position.coords.latitude, position.coords.longitude);
    
    //現在地情報表示
    showCurrentPosition(position);
}
background.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <script src="./js/background.js"></script>
</head>
<body>
</body>
</html>
background.js(一部抜粋)
/*
 * 現在地を取得する
 */
function getPosition(sendResponse){
    
    //現在地の位置情報取得
    navigator.geolocation.getCurrentPosition(
        
        //成功
        function(position){
            
            //現在地を保持
            currentPos.lat = position.coords.latitude;
            currentPos.lng = position.coords.longitude;
            
            //現在地を返す
            sendResponse({
                type: "position",
                position: position
            });
        },
        
        //失敗
        function(e){
            sendResponse({
                type: "error",
                error: e
            });
        },
        
        //オプション
        geoOptions
    );
}

引数やコールバックに若干見慣れない記述がありますが、詳細は後述します。

バックグラウンドページとの通信

Webアプリをフロント側のページとバックグラウンド側のページに分けたことによって、各ページ間でデータのやり取りが必要になりました。バックグラウンドページをユーティリティやデータストアなど単純な用途に使うのであれば、もっとも簡単な方法は、各ページのWindowオブジェクトを取得し、グローバルな変数やメソッドにアクセスすることです。Window.chrome以下のメソッドにはChrome独自の便利なメソッドがいくつかありますが、その中でchrome.extension.getBackgroundPage()を利用してバックグラウンドページのWindowオブジェクトを取得することができます。

odometer.js(サンプルコード)
//バックグラウンドページのWindowオブジェクトを取得
var bp = chrome.extension.getBackgroundPage();

//位置情報を取得
bp.getPosition(function(data){
    
    //地図作成と現在地情報表示
    init(data.position);
    
});

逆に、バックグラウンドページからWebアプリの各タブページのWindowオブジェクトを取得するには、chrome.extension.getViews()を利用します。

background.js(サンプルコード)
//位置情報を取得する処理など

//WebアプリのタブページのWindowオブジェクトをすべて取得
var views = chrome.extension.getViews();
for ( var i = 0, len = views.length; i < len; i++ ) {
    
    //地図作成と現在地情報を表示
    views[i].init(position);
}

ただし、この方法では直接グローバルな変数やメソッドを参照しているため結合度が強く、単純な用途であれば問題ありませんが、コールバックを多用するなどの複雑なアプリケーションではメンテナンスが困難になります。そのため、よりビューとロジックを分離させる方法として、各ぺージ間をメッセージでやり取りするMessage Passingという仕組みがあります。Odometerでは、この仕組みを利用して実装してみます。

表1 Windowオブジェクトの取得メソッド(chrome.extension )
メソッド説明
getBackgroundPage()バックグラウンドページのWindowオブジェクトを取得する
getViews()WebアプリのタブページのWindowsオブジェクトをすべて取得する

Message Passing

Message Passingとは、Webアプリのタブページやバックグラウンドページなどのページ間で通信を行うための仕組みです。通信にはメッセージ機能を用いてデータなどのやり取りが行われます。メッセージは、タブページから送信することもバックグラウンドページから送信することもできます。また、継続してメッセージを送受信するためのチャンネルを設定することや、ほかのWebアプリからのメッセージを送受信することもできます。

Odometerでは、位置情報取得や自動更新のリクエストをバックグラウンドページへ送信し、位置情報や残りの距離など各種データを受け取って表示するように変更します。

メッセージの送受信

タブページからバックグラウンドページへの送受信

単純なメッセージの送受信を記述してみます。タブページ側からバックグラウンドページへ送信する場合、chrome.extension.sendRequest()メソッドを利用します。第1引数でリクエストデータをJSON形式で渡し、第2引数でレスポンスデータを受け取るコールバックを指定します。

odometer.js(バックグラウンドページへのメッセージ送信)
/*
 * Background Pageへ現在地の取得をリクエスト
 */
chrome.extension.sendRequest({ action: 'get_position' }, function(response) {
    if ( response.type ) {
        if ( response.type == 'position' ) {
            init(response.position);
        } else if ( response.type == 'error' ) {
            onError(response.error);
        }
    } else {
        alert('予期せぬエラーです');
    }
});

Odometerでは、リクエストデータのフォーマットとしてactionというプロパティを作り、その内容によってバックグラウンドページでの処理を決定するように実装しています。ここでは、⁠get_position⁠という処理でレスポンスデータのpositionプロパティに位置情報を含めています。

background.js(メッセージ受信)
/*
 * メッセージを受信し、各処理へ振り分ける
 */
chrome.extension.onRequest.addListener(
    function(request, sender, sendResponse) {
        
        switch ( request.action ) {
        
            case 'get_position':
                getPosition(sendResponse);
                break;
            
            case 'set_destination':
                setDistination(request.lat, request.lng, sendResponse);
                break;
            
            case 'start_watch_position':
                startWatchPosition(sender, sendResponse);
                break;
            
            case 'stop_watch_position':
                stopWatchPosition(sendResponse);
                break;
            
            default:
                sendResponse({});
                break;
        }
    }
);

/*
 * 現在地を取得する
 */
function getPosition(sendResponse){
    
    //現在地の位置情報取得
    navigator.geolocation.getCurrentPosition(
        
        //成功
        function(position){
            
            //現在地を保持
            currentPos.lat = position.coords.latitude;
            currentPos.lng = position.coords.longitude;
            
            //現在地を返す
            sendResponse({
                type: "position",
                position: position
            });
        },
        
        //失敗
        function(e){
            sendResponse({
                type: "error",
                error: e
            });
        },
        
        //オプション
        geoOptions
    );
}

バックグラウンドページでメッセージを受信するには、chrome.extension.onRequestイベントを登録する必要があります。イベントのコールバックでは、第1引数にリクエストデータ、第2引数に送信オブジェクト、第3引数にレスポンスデータを送信するコールバックが渡されます。レスポンスデータを送信するコールバックは、メッセージの送受信を完了するために必ず実行する必要があります。レスポンスが何もない場合でも空のオブジェクトを返すようにしましょう。また、レスポンスデータは、JSON形式となっているため、ファンクションやDOMを返すことができないので注意してください。

バックグラウンドページからタブページへの送受信

逆にバックグラウンドページからタブページ側へメッセージを送信する場合には、chrome.tabs.sendRequest()を利用します。タブページ側は対象となるページが複数ある可能性があるため第1引数で送信先のタブIDを指定する必要があります。タブIDを指定しない場合は、現在選択中のタブへ送信されます。

background.js(タブページへのメッセージ送信)
//更新情報を送る
chrome.tabs.sendRequest(
    sender.tab.id,
    {
        action: 'refresh',
        position: position,
        distance: distance
    },
    function(response) {}
);

ここで指定しているsender.tab.idは、事前に自動更新の開始で受け取っている送信オブジェクトです。

odometer.js(メッセージの受信)
/*
 * Background Pageから現在地の自動更新があった場合に、再描画する
 */
chrome.extension.onRequest.addListener(
    function(request, sender, sendResponse) {
        
        if ( request.action && request.action == 'refresh' ) {
            
            //地図更新
            updateMap(request.position.coords.latitude, request.position.coords.longitude);
            
            //現在地情報表示
            showCurrentPosition(request.position);
            
            //目的地までの距離表示
            showDistance(request.distance);
        }
        sendResponse({});
    }
);

受け取り側のイベントは、バックグラウンドページでもタブページでも変わりません。自動更新された位置情報を画面に反映しています。

これで、Odometerのタブを閉じてもいつでも通知がおこなわれるようになりました。現状の実装では、Odometerを再表示すると通知がリセットされてしまいますが、次回以降で改善していきたいと思います。

表2 メッセージ送受信(chrome.extension)
メソッド/プロパティ説明
sendRequest(request, responseCallback)バックグラウンドページへメッセージを送信する
onRequestメッセージの受信イベント。addListenerメソッドでコールバックを登録する
表3 メッセージ送信(chrome.tab)
メソッド/プロパティ説明
sendRequest(tabId, request, responseCallback)タブページへメッセージを送信する
表4 送信者オブジェクト
メソッド/プロパティ説明
idWebアプリのID
tabタブオブジェクト(idにタブIDが格納されている)

メッセージチャンネルの設定

Odometerでは実装していませんが、Message Passingでは継続してメッセージを送受信するためのチャンネルを設定することもできます。ここでは、チャンネルの設定方法について簡単に解説しておきたいと思います。チャンネルの設定には、chrome .extension.connect()メソッドやchrome.tab.connect()メソッドを利用します。メソッドの戻り値としてポートオブジェクトが返るので、ポートオブジェクトのpostMessage()メソッドでメッセージを送信し、onMessageイベントを登録してメッセージを受信します。

また、chrome.extension.onRequestExternalやchrome.extension.onConnectExternalなどのイベントを利用することでWebアプリ間の通信を行うこともできます。詳細は、こちらを参照してください。

タブページ
//チャンネル設定の要求
var port = chrome.extension.connect({name: "geolocation"});

//メッセージを送信
port.postMessage({acton: "get_position"});

//メッセージを受信
port.onMessage.addListener(function(message) {
    
    //受信したメッセージを処理
    
});
バックグラウンドページ
//チャンネル設定の受付
chrome.extension.onConnect.addListener(function(port) {
    
    //メッセージを受信
    port.onMessage.addListener(function(msg) {
        
        //受信したメッセージを処理
    });
});
表5 チャンネル設定(chrome.extension)
メソッド/プロパティ説明
connect (connectInfo)チャンネルを設定する
onConnectチャンネルの設定イベント。addListenerメソッドでコールバックを登録する
表6 メッセージ送信(chrome.tab)
メソッド/プロパティ説明
connect(tabId, connectInfo)チャンネルを設定する
表7 ポートオブジェクト
メソッド/プロパティ説明
name名前
onDisconnectチャンネルの切断イベント
onMessageメッセージの受信イベント
postMessage(message)メッセージの送信
sender送信者オブジェクト

まとめ

今回は、Odometerにバックグラウンド通知の機能を追加しつつBackground Pages、Message Passingの仕組みの詳細を解説しました。次回は、引き続きこのOdometerに機能を追加していく形で各種APIを解説していきたいと思います。

crxファイルはzipファイルですので、右クリックからのダウンロード後に拡張子をzipに変えていただければ中身を参照できます。

また、本連載で取り上げているChrome Web Store/Appsは、⁠Chrome API Developers JP」という開発者コミュニティでも議論されていますので、興味のある方は是非覗いてみてください。

参考

odometer.js
document.addEventListener('DOMContentLoaded', function(){
    
    //新しく立ち上げた際に前回の自動更新を停止する(リカバリは未対応)
    chrome.extension.sendRequest({ action: 'stop_watch_position' }, function(response) {});
    
    /*
     * Background Pageへ現在地の取得をリクエスト
     */
    chrome.extension.sendRequest({ action: 'get_position' }, function(response) {
        if ( response.type ) {
            if ( response.type == 'position' ) {
                init(response.position);
            } else if ( response.type == 'error' ) {
                onError(response.error);
            }
        } else {
            alert('予期せぬエラーです');
        }
    });
    
    /*
     * 初期表示
     */
    function init(position){
        
        //地図作成
        createMap(position.coords.latitude, position.coords.longitude);
        
        //現在地情報表示
        showCurrentPosition(position);
    }
    
    /*
     * エラーコールバック
     */
    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(distance){
        
        //単位をkmに変換して表示
        document.getElementById('distance').textContent = distance;
    }
    
    
    /*
     * Background Pageへ自動更新の開始/終了をリクエスト
     */
    document.getElementById('auto-update').addEventListener('click', function(){
        var autoUpdateButton = this;
        if ( autoUpdateButton.value == '自動更新開始') {
            
            chrome.extension.sendRequest({ action: 'start_watch_position' }, function(response){
                
                //目的地が設定されていない場合、メッセージが返る
                if ( response.type && response.type == 'message') {
                    alert(response.message);
                } else {
                    autoUpdateButton.value = '自動更新停止';
                }
            });
            
        
        } else {
            chrome.extension.sendRequest({ action: 'stop_watch_position' }, function(response){
                autoUpdateButton.value = '自動更新開始';
            });
        }
    });
    
    /*
     * Background Pageから現在地の自動更新があった場合に、再描画する
     */
    chrome.extension.onRequest.addListener(
        function(request, sender, sendResponse) {
            
            if ( request.action && request.action == 'refresh' ) {
                
                //地図更新
                updateMap(request.position.coords.latitude, request.position.coords.longitude);
                
                //現在地情報表示
                showCurrentPosition(request.position);
                
                //目的地までの距離表示
                showDistance(request.distance);
            }
            sendResponse({});
        }
    );
    
    
    
    /*
     * 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());
            
            /*
             * Background Pageで目的地を設定
             */
            chrome.extension.sendRequest(
                
                //リクエストデータ
                {
                    action: 'set_destination',
                    lat: event.latLng.lat(),
                    lng: event.latLng.lng()
                },
                
                //レスポンスコールバック
                function(response) {
                    if ( response.type && response.type == 'distance' ) {
                        
                        //目的地までの距離を表示
                        showDistance(response.distance);
                        
                    } else {
                        alert('目的地の設定に失敗しました');
                    }
                }
            );
            
        });
        
        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);
background.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <script src="./js/background.js"></script>
</head>
<body>
</body>
</html>
background.js
//現在地
var currentPos = {
    lat: 0,
    lng: 0
}

//目的地
var destPos = {
    lat: 0,
    lng: 0
}

//通知済みの距離を保持
var notified = {};


//オプション
var geoOptions = {
    enableHighAccuracy: true,       //高精度要求
    timeout: 6000,              //タイムアウト(ミリ秒)
    maximumAge: 0               //キャッシュ有効期限(ミリ秒)
}

/*
 * メッセージを受信し、各処理へ振り分ける
 */
chrome.extension.onRequest.addListener(
    function(request, sender, sendResponse) {
        
        switch ( request.action ) {
        
            case 'get_position':
                getPosition(sendResponse);
                break;
            
            case 'set_destination':
                setDistination(request.lat, request.lng, sendResponse);
                break;
            
            case 'start_watch_position':
                startWatchPosition(sender, sendResponse);
                break;
            
            case 'stop_watch_position':
                stopWatchPosition(sendResponse);
                break;
            
            default:
                sendResponse({});
                break;
        }
    }
);

/*
 * 現在地を取得する
 */
function getPosition(sendResponse){
    
    //現在地の位置情報取得
    navigator.geolocation.getCurrentPosition(
        
        //成功
        function(position){
            
            //現在地を保持
            currentPos.lat = position.coords.latitude;
            currentPos.lng = position.coords.longitude;
            
            //現在地を返す
            sendResponse({
                type: "position",
                position: position
            });
        },
        
        //失敗
        function(e){
            sendResponse({
                type: "error",
                error: e
            });
        },
        
        //オプション
        geoOptions
    );
}

/*
 * 目的地を設定する
 */
function setDistination(lat, lng, sendResponse){
    
    //目的地を保持
    destPos.lat = lat;
    destPos.lng = lng;
    
    //通知済みの距離をリセット
    notified = {};
    
    //目的地までの距離を返す
    sendResponse({
        type: "distance",
        distance: getDistance(currentPos.lat, currentPos.lng, destPos.lat, destPos.lng)
    });
}

/*
 * 自動更新を開始する
 */
var watchId = 0;    //自動更新停止用のID
function startWatchPosition(sender, sendResponse){
    
    if ( watchId != 0 ) {
        
        //以前の自動更新を停止する
        navigator.geolocation.clearWatch(watchId);
        watchId = 0;
    }
    
    if ( destPos.lat == 0 && destPos.lng == 0 ) {
        sendResponse({
            type: "message",
            message: "目的地を設定してください"
        });
        return;
    }
        
    //通知済みの距離をリセット
    notified = {};
    
    //自動更新を開始する
    watchId = navigator.geolocation.watchPosition(function(position){
        
        //現在地を保持
        currentPos.lat = position.coords.latitude;
        currentPos.lng = position.coords.longitude;
        
        //デスクトップに通知
        //目的地までの距離を取得してkm単位に変換
        var distance = getDistance(currentPos.lat, currentPos.lng, destPos.lat, destPos.lng);
        
        //距離によってしきい値を変える
        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;
        }
        
        //更新情報を送る
        chrome.tabs.sendRequest(
            sender.tab.id,
            {
                action: 'refresh',
                position: position,
                distance: distance
            },
            function(response) {}
        );
        
    }, null, geoOptions);
    
    sendResponse({});
}

/*
 * 自動更新を停止する
 */
function stopWatchPosition(sendResponse){
    
    navigator.geolocation.clearWatch(watchId);
    watchId = 0;
    sendResponse({});
}


/*
 * 2点間距離計算(km)
 */
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.round(Math.sqrt(Math.pow(h, 2) + Math.pow(v, 2))) / 1000;
}

/*
 * デスクトップへ通知
 */
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();
    }
}

おすすめ記事

記事・ニュース一覧