2011年1月6日 星期四

Android 開發筆記 - 調整 GPS 座標個數,以提昇路徑繪圖效率

Android 開發教學筆記 - 透過 Google Maps API 畫出 GPS 路徑 提到關於繪製 GPS 路徑的方式之一,然而,有時路徑長度過長,如兩三千個 GPS 座標,則可能造成路徑繪圖的負擔。


在此嘗試降低 GPS 個數的方式:



  • 以連續兩點為單位,計算差距,差距小於某數值則刪去

  • 以連續三個點為單位,所建立的兩段直線的角度,若角度大過某個數值,則把中間點去除


其中前者會存在一些哪些點可刪或不可刪問題,並且去除個數有限,後者則是計算量大,刪除的效果不錯,在 2011_01_03_Schwechat_Brno.gpx 測資中,可以從不重複的一千多點縮小到兩、三百點,但仍會有畫出的路徑與道路偏移的問題,例外則是角度選擇的部份


簡單的角度實做,就是計算兩向量的 cosine 數值,將越靠近 -1 的進行刪除的動作,也就是兩向量越接近 180 度的直線。


片段程式碼:


public static void main(String a[] )
{
    double logs[] = { 25.051981, 121.522751, /* ... , */ 25.064867, 121.526154 };

    ArrayList<Double> data = new ArrayList<Double>();
    for( int i=0; i<logs.length ; ++i )
        data.add( new Double( logs[i] ) );

    System.out.println( data.size() );
    data = rebuildGSPLogs( data , 0 );
    System.out.println( data.size() );
}

//
// list = { lat1, lng1, lat2, lng2, ... };
//
private ArrayList<Double> rebuildGSPLogsByAngleCheck( ArrayList<Double> list , int at )
{
    if( at < 2 )    // begin at second point
        at = 2;
//    if( list.size() > at + 6 && at >= 0 )
    while( list.size() > at + 6 && at >= 0 )
    {
        double x1 = list.get(at).floatValue() , y1 = list.get(at+1).floatValue() , x2 = list.get(at+2).floatValue(), y2 = list.get(at+3).floatValue() , x3 = list.get(at+4).floatValue() , y3 = list.get(at+5).floatValue();
        double nx1 = x1-x2 ,nx2 = x3 - x2, ny1 = y1-y2 , ny2 = y3-y2;
        double len1 = Math.sqrt( nx1*nx1 + ny1*ny1 ) , len2 = Math.sqrt( nx2*nx2 + ny2*ny2 );
            
        if( len1 == 0 )
        {
            list.remove(at+2);    // lat
            list.remove(at+2);    // lng
        }
        else if( len2 == 0 )
        {
            list.remove(at+4);    // lat
            list.remove(at+4);    // lng
        }
        else
        {
            double checkCos = (nx1*nx2 + ny1*ny2 )/len1/len2 ;
            if( checkCos > -0.986025 )  //  >= -1 : keep all
            {
                at+=2;
            }
            else
            {
                list.remove(at+2);    // lat
                list.remove(at+2);    // lng
            }
        }
//        return rebuildGSPLogsByAngleCheck( list , at );
    }
    return list;
}


有些需要留意的點,原先把 rebuildGSPLogsByAngleCheck 寫成遞迴,在桌機上跑很正常,但移植到 Android 模擬器上,大概沒跑多久就掛了,查了錯誤訊息才得知 out of stack,所幸程式碼只需改個 2 行就改成非遞迴式。


除此之外,在繪圖方面,也可以採用一些方式提昇效率:



  • 扣除重複位置的點

  • 僅繪出可視地圖範圍內的路徑


對於前者部分,是因為就算 GPS 座標不同,在進行繪製的過程中,會先轉成對應的繪圖座標,在這樣的情境下,有些非常相近的 GPS 座標,其實是會被對應成同一個繪圖點,解法很簡單,就只要用個 Hash 就可以解掉;後者問題是在於一條路徑很長,但顯是在地圖上只是很小的一段路程,因此可以只挑出目前地圖看得見的部分進行繪製,也就是將 GPS 座標轉成繪圖座標,此時去判斷該繪圖座標的位置是不是落在目前螢幕顯示的區域,如判斷 X >= 0 && Y >= 0 && X < width && Y < height 。


然而,處理可見路徑的繪圖部分,有以下的問題:


由於兩點構成一條線,因此有可能下一個點已經超出目前顯示地圖的範圍,但剛剛畫的點離 MapView 邊框還有段距離,依據上述的條件則不會畫出連接線,導致看起來像路徑已經斷掉,這種情況也會發生在移動地圖時,前一個點已超出範圍,則會看到之前的路線斷掉。


解決方式有兩種:



  • 使用一個 diff 數值來判斷,例如原先是判斷畫點得座標必須在 0 ~ mapView.getWidth() 且 0 ~ mapView.getHeight() 範圍才顯示,現在再加上一點數值來增大範圍

    • x + diff >= 0 && x <= mapView.getWidth() && y + diff >= 0 && y <= mapView.getHeight()



  • 使用兩個 list 來處理,假設整條路徑共有 n 個點,且第 x 點代表第一個被畫在螢幕上的點,那使用一個 stack 記錄 1~ x - 1 個點,第二個 list 記錄 x+1 ~ n 個點,以輔助繪出路徑 x-1 與 x 的路徑,以及 x 與 x+1 的路徑,以此解決路線斷掉的問題。如果並沒有一個 x 點會被畫出,那只好去找尋在可視地圖中,有沒有哪條線比較接近目前的位置,有的話則把它會出來,在此使用 cosine 的計算,挑選數值最接近 -1 的,代表夾角接近 180 度


第一種解法:


int diff = 100;
boolean useMoveTo = true;
for( int i=0; i<logs.length ; i+=2 )
{
    out = getPixelXYFromGeoValue( p, logs[i], logs[i+1] );

    if( out.x + diff < 0 || out.y + diff < 0 || out.x > mapView.getWidth() + diff || out.y > mapView.getHeight() + diff )
    {
        useMoveTo = true;
    }
    else if( useMoveTo )  // i = 0
    {
        myPath.moveTo(out.x, out.y);
        useMoveTo = false;
    }
    else if( i+2 != logs.length )
    {
        myPath.lineTo(out.x, out.y);
    }
    else
    {
        myPath.setLastPoint(out.x, out.y);
    }
}


第二種:


boolean useMoveTo = true;
int pCnt = 0;
HashMap<Point,String> hashMap = new HashMap<Point,String>();
Point check = getPixelXYFromGeoValue( p, srcLogs[gps_at], srcLogs[gps_at+1]);
ArrayList<Point> UseFF = new ArrayList<Point>();
ArrayList<Point> UsePP = new ArrayList<Point>();
                
for( int i=0; i<logs.length ; i+=2 )
{
    out = getPixelXYFromGeoValue( p, logs[i], logs[i+1] );

    if( hashMap.containsKey(out) )
        continue;
    hashMap.put(out, out.x+"-"+out.y);
                    
    if( out.x < 0 || out.y < 0 || out.x > mapView.getWidth() || out.y > mapView.getHeight() )
    {
        if( pCnt < 1 )
            UsePP.add( out );
        else
            UseFF.add( out );
        continue;
    }
    else if ( pCnt < 1 )
    {
        int size = UsePP.size();
        if( size > 0 )
        {
            myPath.moveTo( UsePP.get(size-1).x, UsePP.get(size-1).y);
            useMoveTo = false;
            UsePP.clear();
        }
    }
    else
    {
        int size = UseFF.size();
        if( size > 0 )
        {
            myPath.lineTo( UseFF.get(0).x, UseFF.get(0).y);
            if( size > 1 )
            {
                myPath.moveTo( UseFF.get(size-1).x, UseFF.get(size-1).y);
            }
            UseFF.clear();
            useMoveTo = false;
        }
    }

    pCnt++;
                    
    if( useMoveTo )
    {
        myPath.moveTo(out.x, out.y);
        useMoveTo = false;
    }
    else if( i+2 != logs.length )
    {
        myPath.lineTo(out.x, out.y);
    }
    else
    {
        myPath.setLastPoint(out.x, out.y);
    }
}

if ( pCnt < 1 )
{
    int size = UsePP.size();
    if( size > 1 )
    {
        double diff = 1;
        int checkAt = -1;
        for( int i=0 ; i+1<size; ++i )
        {
            double x1 = UsePP.get(i).x - check.x ;
            double y1 = UsePP.get(i).y - check.y ;
            double x2 = UsePP.get(i+1).x - check.x ;
            double y2 = UsePP.get(i+1).y - check.y ;
            double l1 = Math.sqrt( x1*x1 + y1*y1 );
            double l2 = Math.sqrt( x2*x2 + y2*y2 );
            double checkCos = (x1*x2 + y1*y2 )/l1/l2 ;
            if( checkCos < diff )
            {
                diff = checkCos;
                checkAt = i;
            }
        }

        if( checkAt > -1 )
        {
            myPath.moveTo( UsePP.get(checkAt).x, UsePP.get(checkAt).y);
            myPath.lineTo( UsePP.get(checkAt+1).x, UsePP.get(checkAt+1).y);
        }
        UsePP.clear();
    }
}
else
{
    int size = UseFF.size();
    if( size > 0 )
    {
        myPath.lineTo( UseFF.get(0).x, UseFF.get(0).y);
        UseFF.clear();
    }
}


雖然降低 GPS 個數可以提升路徑繪圖的效率,但也因此失去精準度,特別是 Zoom Level 越高時,會看到路徑偏移很嚴重,或許還可以搭配 Zoom Level 來決定是否要做 GPS 個數的刪減囉


2011年1月5日 星期三

Android 開發筆記 - 透過 Google Maps API 畫出 GPS 路徑

mygpspath2
紅色線即為GPS logs 的路徑


想要練習把一連串的 GPS logs 畫在 Google Map 上,首先要解決的問題有兩種:



  • 哪裡找 GPS logs

  • 要怎樣在 Google Maps 上畫線


關於第一個問題,可以在 OpenStreetMap 上找到測資,有個頁面是 GPS 軌跡,在那邊可以取得別人公開的 GPS logs,此例為 2011_01_03_Schwechat_Brno.gpx,然而下載的資料格式是 xml 檔案,而測試僅需使用經緯度,因此要稍微處理一下,以 Ubuntu 系統為例:


$ wget http://www.openstreetmap.org/trace/896382/data
$ grep -P '<trkpt lat="([0-9.]+)" lon="([0-9.]+)"' 896382.gpx | awk -F '"' '{ print $2, $4; }' > /tmp/test.data


如此一來,/tmp/test.data 就是只有經緯度的資料,但是行車紀錄的過程,可能停留在某處,而導致資料重複的問題,在此可以透過 hash 的概念,再更新 awk 的使用:


$ grep -P '<trkpt lat="([0-9.]+)" lon="([0-9.]+)"' 896382.gpx | awk -F '"' '{ if(!hash[$2$4] ){ hash[$2$4] = 1; print $2,$4;  } }'


然後我又比較偷懶,想乾脆轉成 double array 來使用,故流程多了點:


$ grep -P '<trkpt lat="([0-9.]+)" lon="([0-9.]+)"' 896382.gpx | awk -F '"' 'BEGIN{out=""}{ if(!hash[$2$4] ){ hash[$2$4] = 1; if( out != "" ) out = out "," ;  out = out""$2","$4; } }END{print "double logs[]={"out"};";}' > java.log


這時候 java.log 檔案內就是一個 double array 的宣告,把他複製起來擺到程式裡吧 :P 畢竟這只是個測試,先不用開檔讀資料的建法。


接著就是建立 Android Project 囉


[Eclipse]->[File]->[New]->[Android Project]

Project name: MyGPSPath
Build Target: Google APIs/2.2/8
Application name: MyWidget
Package name: com.test.map.path
Create activity: MyGPSPath
Min SDK Version: 8


修改 AndroidManifest.xml,增加使用網路的權限以及使用 Google Maps lib


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.test.map.path"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".MyGPSPath"
                  android:label="@string/app_name">
            <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-sdk android:minSdkVersion="8" />
    <uses-permission android:name="android.permission.INTERNET" />
</manifest>


修改 layout,使用 MapView 並且填寫 Google Map API Key,另外新增一個按鈕,以供 debug 使用


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mainlayout"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >
    
    <com.google.android.maps.MapView
        android:id="@+id/mapview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:clickable="true"
        android:apiKey="GOOGLE_MAP_API_KEY"
    />
    
    <Button
        android:text="Report"
        android:id="@+id/GetReport"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true">
    </Button>
</RelativeLayout>


程式碼:


package com.test.map.path;

import java.util.List;

import com.google.android.maps.*;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MyGPSPath extends MapActivity {

    StringBuilder debugOut;
    MapView mapView;

    // 從 openstreetmap 取出的 gps logs, 在此僅擺上頭尾兩點
    double logs[]={48.138050017878413,16.481179967522621,/* ... , */ 49.179430995136499,16.558635039255023}; 

    @Override
    protected boolean isRouteDisplayed() {
        return false;
    }
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        debugOut = new StringBuilder();
        
        if( ( mapView = (MapView) findViewById(R.id.mapview) ) != null )
        {
            mapView.setBuiltInZoomControls(true);
            mapView.setTraffic(true);
        }
        
        Button b;
        if( ( b = (Button) findViewById(R.id.GetReport) ) != null )
        {
            b.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    debugOut.append("logs length:"+logs.length+"\nbegin at:"+logs[0]+","+logs[1]+"\n");
                    AlertDialog alertDialog = new AlertDialog.Builder(MyGPSPath.this).create();
                    alertDialog.setTitle("debug");
                    alertDialog.setMessage(debugOut);
                    alertDialog.setButton("OK", new DialogInterface.OnClickListener(){
                        public void onClick(DialogInterface dialog, int which)
                        {
                            dialog.cancel();
                        }
                    } );
                    alertDialog.show();
                }
            });
        }
        
        List<com.google.android.maps.Overlay> ol = mapView.getOverlays();
        ol.clear();
        ol.add(new MyMapOverlay());
        mapView.invalidate();
        
        MapController mapController = mapView.getController();
        if( mapController != null )
        {
            mapController.animateTo(new GeoPoint( (int) (logs[0]* 1000000) , (int)(logs[1]* 1000000) ) );
            mapController.setZoom(8);
        }
    }
    
    class MyMapOverlay extends com.google.android.maps.Overlay
    {
        private Point getPixelXYFromGeoValue( Projection p , double lat, double lng )
        {
            GeoPoint in = new GeoPoint((int) (lat * 1000000) , (int) (lng * 1000000) );
            Point out = new Point();
            p.toPixels(in, out);
            return out;
        }
        @Override
        public boolean draw(Canvas canvas, MapView mapView,boolean shadow, long when)
        {
            super.draw(canvas, mapView, shadow);
            
            //if( !shadow )
            {
                Projection p = mapView.getProjection();
                Point out;
                Path myPath = new Path();
                
                for( int i=0; i<logs.length ; i+=2 )
                {
                    out = getPixelXYFromGeoValue( p, logs[i], logs[i+1] );
                    if( i == 0 )
                        myPath.moveTo(out.x, out.y);
                    else if( i+2 != logs.length )
                        myPath.lineTo(out.x, out.y);
                    else
                        myPath.setLastPoint(out.x, out.y);
                }
                
                Paint myPaint = new Paint();
                myPaint.setColor(Color.RED);
                myPaint.setStyle(Paint.Style.STROKE);
                myPaint.setStrokeWidth(10);
                myPaint.setAlpha(70);
               
                canvas.drawPath(myPath, myPaint);
            }
            return true;
        }
    }
}


成果:


mygpspath
紅色線則是這條 GPS logs 所繪製出來的路徑


mygpspathdebug
按下右上角的按鈕,可以顯示一些訊息,如點的個數和起點位置,另外也可以擺一些 debug 訊息也不錯


其他部份,還有很多待改善的地方,例如 GPS logs 過長時,移動地圖所造成的重繪之效率問題


[Unix] 以 line 為單位,去除重複的資料

有一堆 GPS 資料想要使用,但在紀錄資料時,有可能某處停留較久,因此會有重複資料的問題。解決方式可以寫簡單的程式,但我一直想不出有哪個洽當的 Unix 指令可以用,就先挑 awk 來使用。


資料格式:


25.064903,121.526657,
25.064903,121.526657,
25.064903,121.526657,
25.064898,121.526573,
25.064898,121.526573,
25.064898,121.526573,
25.064890,121.526489,
25.064890,121.526489,
25.064886,121.526436,
25.064886,121.526436,


輸出目標:


25.064903,121.526657,
25.064898,121.526573,
25.064890,121.526489,
25.064886,121.526436,


指令:


$ awk '{ if( !a[$0] ){ print $0 ; a[$0] = 1; } }' in.txt


使用簡單的 hash 概念。但比較重要的是指令少用,紀錄一下,以節省以後的時間花費。


感謝許青蛙補充!若不在意順序還可以用 sort -u 就好


感謝 fin 補充,對於連續的資料可以採用 uniq 來處理就行啦,所以在這個情境下就是連續性的刪除,可以用:


$ cat in.txt | uniq


2011年1月3日 星期一

中華電信 免費、短期日租型 3G 上網

之前想說放特休時申請短期的 3G 上網,但因為不能報帳的關係,再加上好像也不是必須的,因此最後沒用到。這次恰好碰到國小同學在問寒假返家想申請短期的行動上網,順便筆記一下,以免每次要找資訊都要重新找一遍。


有時候會希望可以 3G 上網,例如老家沒有網路,再加上自己用得是傳統手機,也沒啥上網功能,甚至有的 sim 卡還在走 2G 網路,殊不知使用手機 2G 上網(稱作 GPRS),不小心下載 10MB 資料量可就要費2457元!為此中華電信也都有做一種保護機制,像 GPRS 最高收費 4000 元,而 3G 上網好像最高收費是 1500 還 2000 ,我這邊沒找到資訊,可以打電話去問客服。為此,也有人為了避免自己或家人不小心用手機上網,一種是把 sim 卡的上網功能關閉,等到要想要使用時才打去客服請她們打開。另一個議題則是 sim 卡到底要不要換成 3G 的,新辦手機得大部分應該都會是 3G 的(或 3.5G的?這些我都沒研究),但很久以前辦的手機應該就會是 2G 的。有人說從 2G 調成 3G,有些通話費會變貴,細節要在驗證,而另一種問題則是手機會因為環境的關係,手機的使用模式會不停地在 2G 或 3G 網路切換,照成手機電力浪費,在此也不多談。另外一提的,想從 2G 轉成 3G 要去櫃檯辦理,要換 sim 卡,通常要等 1 ~ 2 天才會啟用。


回到主題,目前中華電信有提供 3G 上網的試用方案:3G行動上網免費試用



  • 免費 72 小時使用3G上網

  • 押金 2000 元

  • 可借行動網卡


試用的好處一來是免費,二來是可以測試使用環境,如住家或租屋處,是否適用於 3G 上網,因為並不是任何地方都適用的,這跟基地台、住家附近屏蔽等有關。缺點是只有限定特定幾家的店可以申請。


另一種則是直接用 3G 日租型方案:3G行動上網日租型



  • 3天 250 元

  • 7天 450 元


這是不限流量的費用,以七天來說,一天平均約65元,細節可從上述連結過去,點選"費率說明"。另外,看來是要自備行動網卡。缺點是別家的有比較便宜,像威寶有以日計算,一天39元喔,但還是要留意,使用 3G 上網還是跟使用環境有關。


雖然現在智慧手機當道,很多人一開始辦手機時就被強迫要辦 3G 網路的方案,例如吃到飽一個月 750 元,但對於我這類型的,身處於 wifi 環境再加上常常宅在家,對於 3G 網路的使用可有可無,偶爾去租短期的使用,其實還滿適合的,除非每個週末都要出去旅遊或是到處爬爬走的人,吃到飽方案可能也適合,另外,也有一些人租用一個月 200、400 或 650 元的月租方案,然後用超過的量,最多會只收一個定額,依序最高1500、1100 和 950 等,也是另一種使用考量喔,細節請參考:3G行動上網


其他參考資訊:



2011年1月2日 星期日

2011年~新年快樂!








這是電腦模擬的煙火,跟現場還是有不少落差 XD


不一會兒,一年又過了。在 2010 年的最後一天,跟隨著天龍人的腳步,在清大的竹客站,等車等了兩小時,第一次看到排隊的人潮,已經不是 S 型可以形容的,然後車子發動到台北又是兩個半小時,整整快要五個小時,才踏入目的地。在台北街頭看著人潮的,發現台北人真的十足地很會打扮,而底心瞧見的是錢潮!可惜得我還沒任何概念可以付諸於行動。


隨後,又跟著腳步到101附近等著倒數,也體會了什麼是水洩不通,在人群中擠壓打轉,但還不錯,瞧著 101 會有那種念頭,總要去 101 一次,無論是在那邊工作還是瞧瞧風景。


散場之後,散步回家,也算是第一次經過國父紀念堂,看到許多還在玩煙火的年輕人,以及不少外國人和相機的蹤影,另外,螢光幕上的阿妹正賣力的吆喝著。


沒想到 2011 年就這樣的開跑了,隔天一早收到了一位實驗室學長的紅色炸彈!希望今年可以過得比去年更加充實,將自己想做的事好好地實現。


祝大家新年快樂!