2

AndroidでGPSロガー

Android2.1でGoogle Map API、メニュー画面、GPSでの位置の計測を使う方法が理解できたので作った。
GPSとって5秒おきにアップデートし、Google Mapに自分の位置を表示する。
移動に合わせて地図の中心点を動かし、移動のログを地図上に赤線で表示する。

GPS使いまくったら、電車で藤沢と日吉の間を往復する間使い続けただけでHTC Desireの電池が40%ぐらい減った。5秒間隔は狭すぎるか。

バイナリはAndroid2.1以上向け。右クリックで保存し.apkにリネームすればインストールできる

昨日テストした。日吉から藤沢方面に戻るところ。
緯度と経度のListをメモリ上に保存してあるので、線を引いて示す事ができる。
メモリ上に保存してあるだけなのでアプリを終了すると消える。でもAndroidアプリはそもそも終了しないので電源を切るまでは残る。そのうちSDカードに保存するようにしよう
R0014774.JPG


Menuボタンを押すと
  • GPSのon/off、最後に計測した場所に戻る
  • ズームをレベル18にする(18ぐらいが人間が徒歩で使う地図として調度良い)
  • 衛星写真と地図の切り替え
  • ログ線の表示on/off
  • ができる。
    GPS Tracker


    電車で日吉駅に入る所
    R0014768.JPG


    電車で二俣川あたりを通り過ぎる
    GPS Tracker



    ■GPSで位置情報を取る
    橋本商会 適当なLocationProviderから位置情報を取るに書いたうち、requestLocationUpdatesを使った。


    GPSまわりは、Android2.1と1.6以前でAPIがかなり違う。
    ネットで検索して2009年春以前の記事を見てやってたら痛い目を見た。
    出たばかりのこの本は全体的に薄く広くカバーしていて、とっかかりとして良い。ホームスクリーンウィジェットやbluetoothまで一応載っている。
    レイアウトにXMLを使わず全てJavaで書いているけど。
    Android2.1プログラミングバイブル
    布留川 英一
    ソシム
    売り上げランキング: 847


    ■メニューボタン
    Activityを継承しているクラスならそのまま@OverrideでonCreateOptionsMenuとonInputItemSelectedを実装すれば使える。
    こんな風にクラス内クラスを作っておいて
    private static class MenuId{
        private static final int START_GPS = 1;
        private static final int LAST_LOCATION = 2;
        private static final int SET_ZOOM = 3;
        private static final int SATELLITE_TOGGLE = 4;
        private static final int LOG_TOGGLE = 5;
    }


    ボタンを追加して
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
          boolean supRetVal = super.onCreateOptionsMenu(menu);
          menu.add(0, MenuId.START_GPS, 0, "Start GPS");
          menu.add(0, MenuId.LAST_LOCATION, 0, "Last Location");
          return supRetVal;
    }


    アイテムが選択された時のイベント内でどのボタンが押されたかを判定する。
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case MenuId.START_GPS:
            // 何か処理
                break;
            case MenuId.LAST_LOCATION:
            // 何か処理
                break;
        }
    }
    イベント内でitem.setTitle()を使えばボタンの文字列を変更できる。


    ■Google Mapを表示する
    mapの表示の前に準備がいる。
    1. Eclipseで[Window]→[Android SDK and AVD Manager]→updateでGoogle APIをインストールする
    2. projectのPropertiesからbuild targetを変更。Android 2.1からGoogle APIのAndroid 2.1にする
    3. 最初に作ったActivityをcom.google.maps.MapActivityの継承に変更
    4. Google Maps API Keyを取得


    Google Maps API Keyの取得に必要なMD5 fingerprintは、Macなら
    keytool -list -keystore ~/.android/debug.keystore
    して適当なパスワードを入れたら生成された。~/.androidディレクトリごと生成された。


    AndroidManifest.xmlにandroid.permission.INTERNETとcom.google.android.mapsを追加する
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.shokai.gpstracker"
          android:versionCode="1"
          android:versionName="1.0">
        <application android:icon="@drawable/icon" android:label="@string/app_name">
            <activity android:name=".GpsTracker"
                      android:label="@string/app_name"
                      android:screenOrientation="portrait">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    <uses-library android:name="com.google.android.maps" />
        </application>

    <uses-permission android:name="android.permission.INTERNET"/>
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
        <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>    

    </manifest>

    res/layout/main.xmlにAPI key取得時にでてきたサンプルコードを貼り付けると、MapViewが表示される。
    <com.google.android.maps.MapView
    android:id="@+id/mapview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:enabled="true"
    android:clickable="true"
    android:apiKey="your-api-key"
    />
    clickableやenabledをtrueにした。地図を指でつまんで動かせるようになる。


    main.xmlを書き換えたら、R.javaにいつのまにかMapViewのidが入っていた。
    import com.google.android.maps.*;
    MapView map = (MapView)findViewById(R.id.mapview);
    という風にcontrollerから扱える。


    これでとりあえず地図は表示できる。


    ■地図を操作する
    onLocationChangedでLocationオブジェクトが取れるので、そこから緯度経度が取れる。
    MapViewからMapControllerを取得して操作する。
    double lat = location.getLatitude(); // 緯度
    double lon = location.getLongitude(); // 軽度
    MapController mc = map.getController();
    GeoPoint p = new GeoPoint((int)(lat*1E6), (int)(lon*1E6));
    mc.setCenter(p);
    mc.setZoom(18);



    ■Google Map上にOverlayを表示する
    自分の位置を表示するだけなら、地図/位置情報/GPSを使うAndroidアプリを作るには (1/3) – @ITのようにMyLocationOverlayを使うといい。

    ただこの通りやるとGPSの取得間隔や精度を詳細に指定できるrequestLocationUpdateの機能とぶつかるのでそのままは使わず、onLocationChangedの中で位置を指定するようにした。


    とりあえずMyLoactionOverlayを作ってmapに配置しておくが、表示はしない
    MapView map = (MapView)findViewById(R.id.mapview);
    MyLocationOverlay myOverlay = new MyLocationOverlay(getApplicationContext(), map);
    myOverlay.onProviderEnabled(LocationManager.GPS_PROVIDER);
    map.getOverlays().add(myOverlay);

    自分アイコンを地図に表示
    public void onLocationChanged(Location location) {
        myOverlay.getMyLocation();
    }



    ログの線の描画は、Overlayを継承したLogOverlayクラスを自分で作り、draw関数の中を実装してやると作れる。drawはどうやらMapViewが描画しなおす毎に実行されるっぽい。MapView.invalidate()を呼ぶとすぐ再描画できる。
    Google MapにDrawableを配置する – Android Wiki*に東京と大阪の間を緯度経度で指定して線で結ぶ例があったので参考にした。

    LogOverlay.java
    package org.shokai.gpstracker;

    import java.util.*;
    import android.graphics.*;
    import android.graphics.Paint.*;
    import com.google.android.maps.*;

    public class LogOverlay extends Overlay {

    private Paint linePaint;
    private List<GeoPoint> points;

    public LogOverlay() {
    this.points = new ArrayList<GeoPoint>();
    this.linePaint = new Paint();
    linePaint.setARGB(255, 255, 0, 0);
    linePaint.setStrokeWidth(2);
    linePaint.setDither(true);
    linePaint.setStyle(Style.FILL);
    linePaint.setAntiAlias(true);
    linePaint.setStrokeJoin(Paint.Join.ROUND);
    linePaint.setStrokeCap(Paint.Cap.ROUND);
    }

    public void add(GeoPoint p){
    points.add(p);
    }

    @Override
    public void draw(Canvas canvas, MapView view, boolean shadow){
    if(points.size() < 2) return;
    Point p_a = new Point();
    Point p_b = new Point();
    for(int i = 0; i < points.size()-1; i++){
    view.getProjection().toPixels(points.get(i), p_a);
    view.getProjection().toPixels(points.get(i+1), p_b);
    canvas.drawLine(p_a.x, p_a.y, p_b.x, p_b.y, linePaint);
    }
    }

    public int size(){
    return points.size();
    }

    public List<GeoPoint> getPoints(){
    return this.points;
    }

    }



    ■画面を回転させない
    Androidは画面を回転させると、onDestroyイベント→onCreateイベントの順にイベントが発生して画面がまるごと描画しなおされてしまう。
    なので、画面を回転させるだけでTextViewは中身が消滅するし、Google MapにOverlayとして描画していた画像も消える。
    値や描画はいいんだけど、requestLocationUpdateの取り消しができなくなって、アプリを強制終了するまでGPSマークが点きっぱなしになったりするのがまずい。


    しょうがないので画面を回転させないようにした。


    回転させない方法としては八角研究所 : Android で再開する Java プログラミング(12) – Android ライフサイクルを考慮-手書きメモを作り込む
    に書いてある様にAndroidManifest.xmlの manifest/application/activityのattributeとして
    android:screenOrientation="portrait"
    を書いておくと縦画面専用になり、回転しなくなる。
    landscapeだと横になる。



    理想的な方法としては、回転した後に値を復元するのがある。
    画面回転時の挙動 – isherの日記が参考になる。
    onSaveInstanceState と onRestoreInstanceStateを@Overrideで実装してBundleにputString/getStringして復元する。
        @Override
        protected void onSaveInstanceState(Bundle bundle) {
            super.onSaveInstanceState(bundle);        
            bundle.putString("textViewMessage", this.textViewMessage.getText().toString() );
        }

        @Override
        protected void onRestoreInstanceState(Bundle bundle) {
            super.onRestoreInstanceState(bundle);
            this.textViewMessage.setText(bundle.getString("textViewMessage"));
        }
    key-valueでbundleに保存して復元できる。


    値の保存と復元ならこれでいいんだけど、GPSがどうしょうもない。センサーまわりのAPIの癖をつかまないと画面回転ができないアプリになっちゃって困る。


    GpsTracker.java
    package org.shokai.gpstracker;

    import android.os.Bundle;
    import com.google.android.maps.*;

    import android.content.Context;
    import android.location.*;
    import android.view.*;
    import android.widget.*;
    import java.util.*;

    public class GpsTracker extends MapActivity implements LocationListener{

    private MapView map;
    private TextView textViewMessage;
    private LocationManager lm;
    private MyLocationOverlay myOverlay;
    private final int zoom_default = 18;
    private boolean location_enalbed, log_enabled; // GPSとコンパスを動かしているかどうか、logを表示しているかどうか
    private LogOverlay logOverlay;

    private static class MenuId{
         private static final int START_GPS = 1;
         private static final int LAST_LOCATION = 2;
         private static final int SET_ZOOM = 3;
         private static final int SATELLITE_TOGGLE = 4;
         private static final int LOG_TOGGLE = 5;
    }

    public GpsTracker(){
            this.location_enalbed = false;
    }

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            this.textViewMessage = (TextView)findViewById(R.id.textViewMessage);
            lm = (LocationManager)this.getSystemService(Context.LOCATION_SERVICE);
            map = (MapView)findViewById(R.id.mapview);
            myOverlay = new MyLocationOverlay(getApplicationContext(), map);
            myOverlay.onProviderEnabled(LocationManager.GPS_PROVIDER);
            map.getOverlays().add(myOverlay);
    logOverlay = new LogOverlay();
        }

        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
          boolean supRetVal = super.onCreateOptionsMenu(menu);
          menu.add(0, MenuId.START_GPS, 0, "Start GPS");
          menu.add(0, MenuId.LAST_LOCATION, 0, "Last Location");
          menu.add(0, MenuId.SET_ZOOM, 0, "Zoom");
          menu.add(0, MenuId.SATELLITE_TOGGLE, 0, "Satellite/Map");
          menu.add(0, MenuId.LOG_TOGGLE, 0, "Show Logs");
          return supRetVal;
        }
        
        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
    case MenuId.START_GPS:
    if(!this.location_enalbed){
    lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 10, this); // 5(sec), 10(meter)
            myOverlay.enableMyLocation();
            myOverlay.enableCompass();
            message("Start GPS");
            this.location_enalbed = true;
            item.setTitle("Stop GPS");
    }
    else{
    lm.removeUpdates(this);
    myOverlay.disableCompass();
    myOverlay.disableMyLocation();
    message("Stop GPS");
    this.location_enalbed = false;
    item.setTitle("Start GPS");
    }
    break;
    case MenuId.LAST_LOCATION:
    Location loc = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
    double lat = loc.getLatitude();
    double lon = loc.getLongitude();
    message("last lat:"+Double.toString(lat) + ", lon:" + Double.toString(lon));
    this.setPosition(lat, lon, this.zoom_default);
    break;
    case MenuId.SET_ZOOM:
    MapController mc = map.getController();
    mc.setZoom(this.zoom_default);
    map.getOverlays().add(logOverlay);
    break;
    case MenuId.SATELLITE_TOGGLE:
    if(map.isSatellite()){
    map.setSatellite(false);
    }
    else{
    map.setSatellite(true);
    }
    break;
    case MenuId.LOG_TOGGLE:
    if(this.log_enabled != true){
    map.getOverlays().add(logOverlay);
    item.setTitle("Hide Logs");
    message("logs: "+Integer.toString(logOverlay.size()));
    log_enabled = true;
    }
    else{
    map.getOverlays().remove(logOverlay);
    item.setTitle("Show Logs");
    log_enabled = false;
    }
    map.invalidate(); // すぐ再描画
    break;
         }
         return true;
        }
        
        public void setPosition(double lat, double lon, int zoom){
         MapController mc = map.getController();
         GeoPoint p = new GeoPoint((int)(lat*1E6), (int)(lon*1E6));
         logOverlay.add(p);
         mc.setCenter(p);
         mc.setZoom(zoom);
         this.myOverlay.getMyLocation();
        }

        // zoomは変更せずに地図だけ動かす
        public void setPosition(double lat, double lon){
         this.setPosition(lat, lon, map.getZoomLevel());
        }
        
    public void message(String mes){
    this.textViewMessage.setText(mes);
    }

    public void onLocationChanged(Location location) {
            double lat = location.getLatitude();
            double lon = location.getLongitude();
    message("lat:"+Double.toString(lat)+", lon:"+Double.toString(lon));
    this.setPosition(lat, lon);
    }

    public void onProviderDisabled(String provider) {
    }

    public void onProviderEnabled(String provider) {
    }

    public void onStatusChanged(String provider, int status, Bundle extras) {
    }

    @Override
    protected boolean isRouteDisplayed() {
    return false;
    }

        @Override
        protected void onSaveInstanceState(Bundle bundle) {
            super.onSaveInstanceState(bundle);        
            bundle.putString("textViewMessage", this.textViewMessage.getText().toString() );
        }

        @Override
        protected void onRestoreInstanceState(Bundle bundle) {
            super.onRestoreInstanceState(bundle);
            this.textViewMessage.setText(bundle.getString("textViewMessage"));
        }
        
    }



    res/layout/main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        >

    <TextView  
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:id="@+id/textViewMessage" android:text="@string/message"/>
            
    <com.google.android.maps.MapView
    android:id="@+id/mapview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:enabled="true"
    android:clickable="true"
    android:apiKey="0y0rb2n5OYga60X7toG-HzSYQh2ICnOT27xI4LA"
    />


    </LinearLayout>


    AndroidManifest.xml

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.shokai.gpstracker"
          android:versionCode="1"
          android:versionName="1.0">
        <application android:icon="@drawable/icon" android:label="@string/app_name">
            <activity android:name=".GpsTracker"
                      android:label="@string/app_name"
                      android:screenOrientation="portrait">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    <uses-library android:name="com.google.android.maps" />
        </application>

    <uses-permission android:name="android.permission.INTERNET"/>
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
        <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>    

    </manifest>


0

GoogleEarth+GPS+DigitalCompass

せっかく方位センサ買ったので、さくっとGpsWalkingしてみた。

■本日の素材

s.h.log: Arduinoで方位センサ RDCM-802 を試す

s.h.log: C#でGoogleEarthを動かす

s.h.log: C#用クラス: シリアル接続GPSのラッパーをver up

■技術的なこと

結構モジュールが揃っていたので楽だった。

Read more

2

Arduino – 偽GPS

GPS BC-337と同じ挙動をする偽GPSをArduinoで作った。

なんでかというと、GPSの位置取得に時間がかかってしょうがないから。

曇っていたり屋内だと位置を取れないし、調子が悪いと屋外でも位置取得に1~5分ぐらいかかる事がある。しかもBC-337は仮想シリアルポートとして認識されるけどCFカード式なので、仮想シリアルポートに接続している間しか給電されない。

つまり、デバッグするたびに1~5分ぐらい待たされる

2回目で嫌になったのでArduinoで同じ挙動を再現した。

SourceCode(Arduino 0005 Alpha)

大したモノでもなく、4800bpsでGPSと同じプロトコルで文字列を垂れ流すだけの偽GPS

前のGpsConnectorの記事のサンプルプロジェクトのGpsTestの方は、偽GPSを使用して作った。

GpsTest

Read more