2011年1月24日 星期一

Django 與 Ubuntu 10.04 server 使用筆記

記得 2009 年年底,我也曾接觸 Django 一陣子,但久而沒碰,等於砍掉重練。最近被派去管一台機器,順手筆記一下到底安裝了啥東西。大學時期,每個人都搶著管機器,想說要有個經驗,碩班後,大概越來不想管機器,而工作之後呢?管機器根本就是個吃力不討好的事,做得好沒人稱讚也不會列入考績,做差會被抱怨啊 XD 所以啊,工作後能不管機器還是不要管,甚至一些案子都忘了規劃管機器的人力費用,也有可能沒規畫架設機器的時間。說完題外話 XD 來回顧一下以前的筆記:



以前我比較熟 FreeBSD,現在?大概沒有一項熟,工作上大概都用 Ubuntu ,目前是用 Ubuntu 10.04 64-bit server 。


光碟安裝完系統後,系統更新:


$ sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
$ sudo vim /etc/apt/source
:%s/us.archive/tw.archive
$ sudo apt-get update


安裝 denyhosts ,可用來阻擋惡意 try 帳密的 IP


$ sudo apt-get install denyhosts
$ sudo touch /etc/hosts.deny
$ sudo touch /etc/hosts.allow
$ sudo /etc/init.d/denyhosts restart


也可以把自己常用的 IP 寫在 /etc/hosts.allow 以防止不小心擋掉自己


sshd: MyIP1, MyIP2, MyIP3


在新增網卡後的設定,此為 VirtualBox 上安裝 eth1 網卡的例子


查看網卡有沒安裝好


$ dmesg | grep eth


若看不到可以移掉下述檔案並重開機偵測


$ sudo mv /etc/udev/rules.d/70-persistent-et.rules /etc/udev/rules.d/70-persistent-et.rules.old 


若 ifconfig 看不到但 dmesg 可以查看到,那就要設定  /etc/network/interfaces ,此例為設定 DHCP 方式


$ sudo vim /etc/network/interfaces
auto eth1
iface eth1 inet dhcp


$ sudo /etc/init.d/networking restart


安裝 MySQL、Apache Web Server、PHP


$ sudo apt-get install mysql-server
$ sudo apt-get install apache2
$ sudo apt-get install php5


設定 Apache 相關


啟用 https 其及 ssl 模組


$ sudo a2enmod ssl
$ sudo a2ensite default-ssl


啟用某些設定檔,如 /etc/apache2/site-available/default_setting


$ sudo a2ensite default_setting


設定完重新啟動


$ sudo /etc/init.d/apache restart


安裝 X-Window,有些環境一開始只可以本機端登入,有些 Web service 測試就需要先安裝一下 X-Window 啦,不然只能用 telnet localhost port 跟 GET / 測試,還滿辛苦的,也可以安裝 lynx 這種工具來使用


$ sudo apt-get ubuntu-desktop


設定開機預設不要進 X-Window (目前還待測試,有點怪,機器跑起來還必須透過 ALT+ F1, ... 切換到其他的來使用)


$ vim /etc/init/gdm.conf 
stop on runlevel [0216]
$ sudo service gdm stop


簡易 MySQL 語法,建立帳號、設定資料庫權限


$ mysql -u root -p
mysql> CREATE DATABASES mydb;
mysql> CREATE USER 'user'@'localhost' IDENTIFIED BY 'password';
mysql> GRANT ALL ON mydb.* TO 'user'@'localhost';
mysql> FLUSH PRIVILEGES;


$ mysql -u user -p
mysql> USE mydb;
mysql> CREATE TABLE my_table ( int x , int y );
mysql> INSERT INTO my_table (x,y) values( 1, 2 );
mysql> DESC my_table;
mysql> SELECT * FROM my_table;
mysql> DELETE FROM my_table WHERE x=1;
mysql> DROP TABLE my_table;


設定 utf8 相關


$ vim /etc/mysql/my.cnf

[client]
default-character-set = utf8

[mysqld]
default-character-set=utf8
init_connect='SET NAMES utf8'


如果一開始不是 UTF-8 的話,又跑了服務一陣子,那大概就親自去改


$ mysql -u user -p 
mysql> status
--------------
mysql  Ver 14.14 Distrib 5.1.41, for debian-linux-gnu (x86_64) using readline 6.1

Connection id:          252
SSL:                    Not in use
Current pager:          stdout
Using outfile:          ''
Using delimiter:        ;
Server version:         5.1.41-3ubuntu12.9 (Ubuntu)
Protocol version:       10
Connection:             Localhost via UNIX socket
Client characterset:    utf8
Server characterset:    utf8
UNIX socket:            /var/run/mysqld/mysqld.sock
Uptime:                 21 hours 38 min 58 sec


Threads: 3  Questions: 5376  Slow queries: 0  Opens: 267  Flush tables: 1  Open tables: 63  Queries per second avg: 0.68
--------------
mysql> ALTER DATABASE mydb CHARACTER SET utf8 COLLATE utf8_general_ci;
mysql> USE mydb;
mysql> SHOW CREATE TABLE my_table;
可查看最後的編碼是否為 DEFAULT CHARSET=utf8
mysql> ALTER TABLE my_table CHARACTER SET utf8 COLLATE utf8_general_ci ;


安裝 Django 相關


$ sudo apt-get install libapache2-mod-wsgi python-mysqldb python-setuptool python-django


建立一個 Project


$ cd ~/
$ django-admin startproject myproj
$ cd ~/myproj
$ django-admin startapp myapp



設定 myproj 的 settings.py


DATABASE_ENGINE = 'mysql' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
DATABASE_NAME = 'mydb' # Or path to database file if using sqlite3.
DATABASE_USER = 'user' # Not used with sqlite3.
DATABASE_PASSWORD = 'password' # Not used with sqlite3.
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = ''  # Set to empty string for default. Not used with sqlite3.

TIME_ZONE = 'Etc/GMT-8'

INSTALLED_APPS = (
    'myproj.myapp',
)

SYS_TEMP_DIR = '/tmp'


設定 myproj 的 urls.py


from django.conf.urls.defaults import *

urlpatterns = patterns('',
    # Example:
    (r'^myapp', include('myproj.myapp.urls')),
)


設定 myapp 的 model.py


from django.db import models

import datetime

# Create your models here.
class MyTable(models.Model):
        mypath = models.CharField(max_length=255,primary_key=True)
        mydate = models.DateTimeField(default=datetime.date.today,auto_now_add = True)
        myscore = models.IntegerField()


設定 myapp 的 views.py


# -*- coding:utf-8 -*-
from django.shortcuts import render_to_response
from django.template import RequestContext
from django import http

from myproj.myapp.models import MyTable

import simplejson
import sys, os, os.path
from time import gmtime, strftime, localtime

from myproj.settings import SYS_TEMP_DIR

def MyTest(request):
        _result = {}

        _name = request.POST.get('name','anonymous') or request.GET.get('name','anonymous')

        #my_time = gmtime()
        my_time = localtime()
        my_date = strftime( "%Y-%m-%d" , my_time )

        save_dir = SYS_TEMP_DIR
        save_file = ''

        if save_dir is None:
                _result['status']='1'
                _result['message']='Internal Error'
                result=simplejson.dumps(_result)
                return http.HttpResponse(str(result))

        if request.FILES and request.FILES['my_file'] :
                upload_file = request.FILES['my_file']
                save_file = str( _name  ) + str( strftime( "_%y_%m_%d_%H_%M_%S" , my_date ) )
                save_path = os.path.join( save_dir , save_file )

                try:   
                        destination = open( save_path , 'wb+' )
                        for chunk in upload_file.chunks():
                                destination.write(chunk)
                        destination.close()
                except:
                      _result['status']='1'
                      _result['message']='Internal Error'
                      result=simplejson.dumps(_result)
                      return http.HttpResponse(str(result))
        else:  
                return render_to_response('my_upload.html' , { 'test':'hello world' } )

        try:   
                db_object = MyTable(mypath=save_file)
                db_object.mydate = my_time
                db_object.save()
        except:
                _result['status']='1'
                _result['message']='Internal Error'
                result=simplejson.dumps(_result)
                return http.HttpResponse(str(result))

        _result['status']='0'
        result=simplejson.dumps(_result)
        return http.HttpResponse(str(result))


設定 myapp 的 urls.py


from django.conf.urls.defaults import *
urlpatterns = patterns('',
        (r'^upload/', 'myproj.myapp.views.MyTest'),
)


建立 templates


$ cd ~/myproj/myapp
$ mkdir templates
$ vim my_upload.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
        <head>
                <meta http-equiv="content-type" content="text/html; charset=utf-8" />
                <title>GPX Upload</title>
        </head>
        <body>
{% if test %}
                <h1>{{ test }}</h1>
{% end if %}
                <form action="." enctype="multipart/form-data" method="post">
                        <input type="file" name="my_file" />
                        <button type="submit">Upload</button>
                </form>
        </body>
</html>



建立 myproj 之 myapp 相關的 db table


$ cd ~/myproj
$ python manage.py syncdb



測試,用瀏覽器連 http://MyIP:8000/myapp/upload/


$ cd ~/myproj
$ python manage.py runserver MyIP:8000



重置 myapp 的 db table


$ cd ~/myproj
$ python manage.py reset myapp


最後,也可以使用 apache 的 mod-wsgi 模組


$ cd ~/myproj
$ mkdir apache
$ vim django.wsgi

import os, sys
sys.path.append('/home/user/myproj')
os.environ['DJANGO_SETTINGS_MODULE'] = 'myproj.settings'
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()


設定 apache 檔


$ vim /etc/apache2/site-available/default

WSGIScriptAlias /
/home/user/myproj/apache/django.wsgi
# WSGIDaemonProcess demo_com user=www-data group=www-data processes=1 threads=10
# WSGIProcessGroup demo_com


$ sudo /etc/init.d/apache restart


2011年1月23日 星期日

買了!Uptech TV7130電視卡

IMG_0109


前幾個禮拜,常常跟博班 Adios 大神閒聊電視的事情,沒想到我也買了電視卡了。宿舍本來就有提供第四台,但住了一年多並沒有吸引我去買電視卡/電視盒的念頭,但最後我還是敗了。買電視卡的主因是因為前陣子申請了壹電視的網樂通,原先以為他可以當電視盒來用,沒想到他只輸出 AV 端子 Orz 盡管東西非常慢才來,在等待的過程中,我也回信說要退掉,但耐心的客服始終說服我,最後也就等到網樂通啦!算是比第一批申請的人,卻慢了快三個禮拜吧!


TV7130 產品應用
來源:TV7130產品型錄


接著來說說 TV7130 電視卡吧!這是我第一次使用電視卡,更早之前算是使用過電視盒。電視卡的缺點是需要插在主機上面,並佔用一個插槽,並且要看電視就必須開機。我買的這張是使用 PCI 插槽,在高階一點的電視卡是用 PCI-E 的。再加上現在主機板大多設計為 M-ATX 的板子,為了縮小面積,所提供的 PCI 插槽就少了不少,像我這張主機板是 GA-MA785GPMT-UD2H 的,就只有兩個 PCI 插槽。一個用在網路卡,另一個就用在這張電視卡,剛好都插滿了。而使用電視卡的優點,那就是可以邊用電腦邊看電視,換句話說,可以把看電視弄成小視窗擺在螢幕上,而不用像電視盒那樣佔了一個螢幕資源,除此之外,也可以錄製電視節目喔。但真正吸引得我的主因,就是可以把邊寫程式邊看電視啦 cc 以前常用廣播當作背景音樂,現在大概可以用電視當背景音樂吧 XD


TV7130 配備
來源:TV7130 繁體中文使用手冊第二版


安裝這張電視卡並非難事,插進主機板,安裝一下驅動程式重開機後,接上 Cable 線就算搞定。然後我一直搞不懂為啥他多了一條線,也就是上圖左下角的那條 Phone Jack to Phone Jack ,最後,我才發現原來裝了電視卡後,使用時螢幕上雖然可以看到影像(第一次使用別忘了要去電台掃描),但始終沒有聲音,原來這就是那條線的目的,他就是用來把電視卡輸出的聲音導入音效卡的輸入端,當然,也可以另外接喇叭吧。我使用的就是導回音效卡的輸入端,並且在電視卡軟體->[設定]->[裝置]->[播放控制]->[Rear Blue In]


這樣的使用缺點就是電視的聲音大小聲,跟主機上使用的聲音大小聲會綁再一起,例如我想要把電視的聲音關小一點,把 Skype 的聲音放大一點,那這樣是不太行的!整體上但勉強都還堪用囉!


對於電視卡算是多了點了解,最驚訝的是音效的輸出設計,只提供從電視卡輸出,這大概也跟自身產品有關吧,未來可以在找一個喇叭接著用,就可以改善這種困擾吧。


USB 無線網卡 - SAPIDO AU-4912 超迷你無線N

usb網卡


上週無意間發現這款 USB 無線網卡,就在 Plurk 上詢問有無使用經驗,倒是讓我大學同學也敗了這款,他還比我早買 XD 同學買的主因是自己筆電網卡很不穩定,所以需要新購一張網卡,最後則是被這款體積小的給吸引了,只是筆電 USB 孔很少,要留意啊。這是一款跟 10 元硬幣大小差不多大的 USB 無線網卡,我買他的主因有兩個,一個是讓主機可以透過無線網路上網,第二個則是它可當 Soft AP 囉。其中 Soft AP 就是透過軟體讓無線網卡變成無線基地台啦,雖然說原理都差不多,但並不是每張無線網卡的製造商都有提供 Soft AP 的程式。


我在新竹 NOVA 邊逛了會,發現還不少家在賣 USB 無線網卡,體積跟隨身碟差不多大的比較多,跟這張一樣大的比較少,另外,價格也都不便宜,都要 500 上下,我在順發有看到一款體積稍大的,但賣 369 元的無線網卡,但最後還是花多一點點錢試試這款小體積的網卡。可以多留一下 spec ,像這張網卡是 150 Mbps ,我看到有得賣得比較貴是 300 Mbps 囉,只是速度快慢是否影響很大,那就很難說了!我覺得 150 Mbps 在某些使用情境已經非常夠用了。


usage


使用上很簡便,若只是當一般無線網卡,那可能插上主機後,在 Win 7 x64 甚至連驅動程式都不用安裝也可以用。但如果要當 Soft AP 的話,那必須安裝軟體,安裝完後,請將 [模式] 調成 [基地台] ,接著再選擇要分享的網路以及加設定密連線的密碼,拉到此頁面下面,按下 OK 後即可完工!我用 iPod Touch 4 測試可以正常使用網路。


逛了一下內附的 CD ,發現裡頭也有 Linux 跟 Mac 的 driver ,看起來還不錯吧。有興趣的可以再去官網看看,至少短時間我還測不到啥問題。未來應該會把它當成旅行用的隨身 3C 產品 XD


2011年1月18日 星期二

心的方向!Go~








昨天到台北參加聚餐,往高鐵站的路上,被後照鏡上火紅的夕陽閃了許久,讓我哼起了這首歌。有點久沒騎車到高鐵站,除了騎過頭外,沒想到連高鐵前面常常一排排違規停車的機車群,忽然都沒看到,害我一時不知要停在哪。


最近還是有非技術類的心情想要記下來,但每每寫到一半就被我 cancel 掉,實在是三五句提到公司福利不然就政治八卦等,這樣的行為讓我對自己感到失落。去年底開始警惕自己不要談論這類話題,希望 2011 年有好的開始。盡管背負的壓力還沒減輕,但開始比較不會只為錢工作了。今年的工作壓力理當比去年大,計畫趕不上變化,但還好,除了每週唸不熟領域的 paper 外,剩下的應該都還能應付吧?


對於 2010 的感觸,除了提不得的暗黑八卦外,大概是對資源管控有所感觸。例如一天 24 小時,某件事做多了,另一件事就必然做少了。有時也不得不跟別人搶資源,例如宿舍樓下的烘乾機啊,或是火車坐位上永遠只有 3 個扶手。也曾跟大神吃飯,當時探問他對 Android Market 或 iOS App store 有無興趣投入,得到的回應當然是有,但有更重要的事要擺在前頭 -- 陪孩子成長,這時我也才比較會回過頭思考一些事情,投入熱門的產業追求的目標是什麼?有沒有比這些更重要的目標呢?很佩服一些人,像青蛙學弟,可以大膽地嘗試一些新東西,不要只為錢工作,也讓我反省一下,該抽空弄弄點更有意義的東西。


最近對未來有些新的方向了,願能持之以恆,離目標越來越近。


2011年1月6日 星期四

Android 開發筆記 - 模擬與繪製 GPS 移動路線

mygpspathwithmarker
上頭紅色的是繪製的路徑,藍色點則是模擬時,顯示的 GPS 座標位置,將隨著時間變動。這是根據此篇 Android 開發教學筆記 - 透過 Google Maps API 畫出 GPS 路徑Android 開發教學筆記 - 調整 GPS 座標個數,以提昇路徑繪圖效率 的筆記所做的延伸練習。


給定一段 GPS 紀錄的路徑,經過個數的縮減壓縮並在地圖上完成路徑的繪製,接著想要了解原始 GPS 移動與繪製的路徑差異有多大,所以就有了這個練習。依照原始 GPS 座標資訊,畫製一個 marker 隨著時間擺放到指定的 GPS 位置,這樣的效果除了能確認原始 GPS 路線與壓縮後的路徑位置的差異,也可以了解實際收集到的 GPS 座標是否出現在 Google Maps 繪製的地圖道路。


實做上在 class MyGPSPath 裡,新增並且改變一些 member 宣告方式:


Handler jobs;
final double srcLogs[]={ 48.138050017878413, 16.481179967522621 /* ,  ... */ };
double logs[] = {};
int gps_at;


其中 jobs 用來更新畫面,srcLogs用來紀錄原始的不重複的 GPS 資訊,而 logs 會是壓縮過後的 GPS 個數,而 gps_at 則是等會模擬時,用來紀錄走到那個 GPS 點。


接著在 onCreate 裡,初始化 gps_at, jobs, logs 變數,以及調整 overlay :


gps_at = 0;
jobs = new Handler();

ArrayList<Double> data = new ArrayList<Double>();
for( int i=0; i<srcLogs.length ; ++i )
    data.add( new Double( srcLogs[i] ) );
data = rebuildGSPLogsByAngleCheck( data , 0 );
        
logs = new double[data.size()];
for( int i=0; i<data.size() ; ++i)
    logs[i] = data.get(i).floatValue();

List<com.google.android.maps.Overlay> ol = mapView.getOverlays();
ol.clear();
ol.add(new MyPathOverlay());

MapController mapController = mapView.getController();
if( mapController != null )
{
    mapController.setCenter(new GeoPoint( (int) (srcLogs[0]* 1000000) , (int)(srcLogs[1]* 1000000) ) );
    mapController.setZoom(15);
}

new Thread( new Runnable(){
    public void run()
    {
        gps_at = 2;
        while( gps_at < srcLogs.length )
        {
            try{
                jobs.post( new Runnable(){
                    public void run(){
                        List<com.google.android.maps.Overlay> ol = mapView.getOverlays();
                        while( ol.size() > 1 )
                            ol.remove(1);
                        ol.add(new MyMarkerOverlay( srcLogs[gps_at] , srcLogs[gps_at+1]));
                        
                        GeoPoint in = new GeoPoint((int) (srcLogs[gps_at] * 1000000) , (int) (srcLogs[gps_at+1] * 1000000) );
                        Point out = new Point();
                        mapView.getProjection().toPixels(in, out);
                        int diff = 100;
                        if( out.x < diff || out.y < diff || out.x > mapView.getWidth() - diff || out.y > mapView.getHeight() - diff )
                        {
                            MapController mc = mapView.getController();
                            mc.animateTo(new GeoPoint( (int)(srcLogs[gps_at]*1000000) , (int)(srcLogs[gps_at+1]*1000000 )) );
                        }
                    }
                });
                Thread.sleep(500);
            }
            catch(Exception e)
            {
            }
            gps_at +=2 ;
        }
    }
}).start();


上頭的 MyPathOverlay 則是之前例子中的 MyMapOverlay,在此僅改變成較有意義的名稱,而 onCreate 最後面使用到的 Thread,就是用來模擬 GPS 變化的過程,每 0.5 秒更動 gps_at 時,則重新擺放 MyMarkerOverlay 的位置,並且稍微計算一下,當 Marker 太靠近可視地圖的邊框時,自動調整地圖位置。


另外,還需實做 MyMarkerOverlay,用來標記 GPS 移動:


class MyMarkerOverlay extends com.google.android.maps.Overlay
{
    double at_lat,at_lng;
    MyMarkerOverlay( double _lat, double _lng )
    {
        super();
        at_lat = _lat;
        at_lng = _lng;
    }
    @Override
    public boolean draw(Canvas canvas, MapView mapView,boolean shadow, long when)
    {
        super.draw(canvas, mapView, shadow);
            
        if( !shadow )
        {
            GeoPoint in = new GeoPoint((int) (at_lat * 1000000) , (int) (at_lng * 1000000) );
            Point out = new Point();
            mapView.getProjection().toPixels(in, out);
                
            int r = 6;
            Paint p = new Paint();
                
            p.setColor(Color.BLUE);
            p.setAntiAlias(true);
            RectF oval=new RectF( out.x - r, out.y - r, out.x + r, out.y + r);

            canvas.drawOval(oval, p);
        }
        return true;
    }
}


也可以實做 ItemizedOverlay<OverlayItem> 的方式,好處是用法接近 Web API 的使用,把你想要新增的點,加到下面的 list 即可:


class MyItemMarkerOverlay extends com.google.android.maps.ItemizedOverlay<OverlayItem>
{
    private ArrayList<OverlayItem> myOverlays = new ArrayList<OverlayItem>();
    private Context myContext;
    public MyItemMarkerOverlay(Drawable defaultMarker, Context context)
    {
        super(boundCenterBottom(defaultMarker));
        myContext = context;
    }
    @Override
    protected OverlayItem createItem(int i)
    {
        return myOverlays.get(i);
    }
    @Override
    public int size()
    {
        return myOverlays.size();
    }
    @Override
    protected boolean onTap(int index)
    {
        OverlayItem item = myOverlays.get(index);
        //AlertDialog dialog = new AlertDialog.Builder(MyGPSPath.this).create();
        AlertDialog dialog = new AlertDialog.Builder(myContext).create();
        dialog.setTitle(item.getTitle());
        dialog.setMessage(item.getSnippet());
        dialog.setButton("OK", new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialog, int which)
            {
                dialog.cancel();
            }
        } );
        dialog.show();
        return true;
    }
    public void addOverlay(OverlayItem overlay)
    {
        myOverlays.add(overlay);
        populate();
    }
}


用法:


List<com.google.android.maps.Overlay> olay = mapView.getOverlays();
Drawable drawable = this.getResources().getDrawable(R.drawable.icon);
MyItemMarkerOverlay markersOverlay = new MyItemMarkerOverlay( drawable, this ); // new MyItemMarkerOverlay( drawable, MyPGSPath.this );

markersOverlay.addOverlay(
    new OverlayItem(
        new GeoPoint((int) (srcLogs[gps_at] * 1000000) , (int) (srcLogs[gps_at+1] * 1000000)),
        "Title",
        "Snippet"
    )
);

olay.add(markersOverlay);


此練習的 GPS 變化,也可以透過 DDMS 來處理,例如定時 send 一些 GPS 座標等,但我還沒嘗試,看來下個練習方向可以用用看 DDMS 囉。


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


祝大家新年快樂!