2012年11月6日 星期二

[Python] 讀取 Shapefile 格式(*.dbf, *.prj, *.shp, *.shx, *.sbn, *.sbx) 以臺北市公車站牌位置圖為例

前陣子參加 2012 Yahoo! Open Hack 時,終於有空把玩台北市政府公開資料,碰到了之前一直想解卻沒空解的問題,以 臺北市公車站牌位置圖 為例,其資料格式:


$ ls bus
busstop.dbf busstop.shx 公車站牌匯入.sbx
busstop.prj 公車站牌匯入.dbf 公車站牌匯入.shp
busstop.shp 公車站牌匯入.sbn 公車站牌匯入.shx


一堆很奇妙的格式,感覺就像給你一個加密過的資料,讓人十分不解,這大概就是門外漢的滋味 Orz 回想起來,這種感觸還真像當年自己去刻圖書館 MARC 格式的心情 XD


所幸參加 Open hack 才有時間把玩,當下不知哪根筋被打通,忽然間明瞭這種 *.shp 格式就叫 Shapefile ,如此一來有關鍵字就能解了,馬上找到 pyshp - Python Shapefile Library


廢話不多,用法:


#!/usr/bin/env python
# -*- coding: utf8 -*-
import shapefile
import sys
reload(sys)
sys.setdefaultencoding('utf-8')


#sf = shapefile.Reader("bus/公車站牌匯入") # big5 & TWD67 format
sf = shapefile.Reader("bus/busstop") # utf8 & WGS84 format
#fields = sf.fields
print sf.fields 


records = sf.records()
shapeRecs = sf.shapeRecords()
print "Total: ",len(shapeRecs)
bushash = {}
for i in range(len(shapeRecs)):
   #busstop = records[i][1].decode('big5', 'ignore') # decode from big5
   busstop = records[i][1].decode('utf8', 'ignore') # decode from utf8
   if busstop in bushash:
      continue
   else:
      bushash[busstop] = ''


   print busstop
   print shapeRecs[i].shape.points[0]
   #print = FromTWD67TM2ToWGS84( shapeRecs[i].shape.points[0][0], shapeRecs[i].shape.points[0][1] )


成果:


[('DeletionFlag', 'C', 1, 0), ['SATOPID', 'N', 20, 0], ['STOPNAME', 'C', 20, 0], ['CITYNAME', 'C', 5, 0]]
Total: 5540
101國際購物中
[121.56524, 25.03415]
101購物中心
[121.565389, 25.034183]
8號水門
[121.53166335449, 25.072300254772]
一女中
[121.51507, 25.03828]
二二八和平公
[121.51303, 25.04215]
二二八紀念館
[121.513913, 25.030761]
....


眼尖的大概可以看到,第一筆有漏字,資料原始為 "101國際購物中<E5><BF>",不知是不是 pyshp 讀取錯誤還是原始資料有誤。此外,同一個站名會有多個座標點,在此僅先過濾掉,有興趣的在自行把玩吧!


其他補充:


安裝 GDAL (MacPorts: $ sudo port install gdal)
$ ogr2ogr -f geojson /tmp/output.json busstop.shp
$ enca /tmp/output.json
Unrecognized encoding
$ cat /tmp/output.json


{
"type": "FeatureCollection",

"features": [
{ "type": "Feature", "properties": { "SATOPID": 2028.0, "STOPNAME": "101å<9c><8b>é<9a><9b>è³¼ç<89>©ä¸­å¿", "CITYNAME": "å<8f>°å<8c>" }, "geometry": { "type": "Point", "coordinates": [ 121.56524, 25.03415 ] } }
,
...
]


$ ogr2ogr -f geojson /tmp/output2.json 公車站牌匯入.shp
$ enca /tmp/output2.json
Universal transformation format 8 bits; UTF-8
$ cat /tmp/output2.json


{
"type": "FeatureCollection",

"features": [
{ "type": "Feature", "properties": { "SATOPID": 2028.0, "STOPNAME": "101國際購物中??", "CITYNAME": "台??", "stopNAMEB5": null }, "geometry": { "type": "Point", "coordinates": [ 306245.046938922838308, 2769874.23026815475896 ] } }
,
...
]


2012年11月4日 星期日

常用的座標轉換筆記(TWD67, TWD97, WGS84)

台灣現在常看到的座標系統有三種,分別是 TWD67、TWD97 和 WGS84,其中 WGS84 就是網路應用上常用的 GPS (lat, lon) 格式(歐洲近幾年有在推另一個座標系統)。至於為啥會接觸到呢?實在是這幾年 ITS 的服務越來越興盛,而台灣區的資料慢慢地釋出(如 Taipei Open Data、交通部運輸研究所等),然而就開始碰到資料格式問題,那就是台灣不少資料採用的地理座標系統不是 GPS 格式。如果你不是 ITS 領域,那大概就跟我一樣從摸 Google Maps 而接觸 GPS 座標系統,例如只要在 Google Maps 上打上 25.033661, 121.564815 後,顯示的就是 Taipei 101,但從台灣區取得的地理座標系統,丟到 Google Maps 卻行不通 :P


這幾年都有接觸 ITS 應用,但一直很懶得整理導致每次都花差不多的時間去處理,所以打算來寫篇筆記


目前碰過的座標格式:



  • TWD67 (TM: 2-degree Transverse Mercator, 二度分帶)


    • 採用 1967年國際地球原子參數(Geodetic Reference System 1967,GRS67),早期還未有衛星時,透過天文觀測、三角定位量測,埔里虎子山為測量原點,而 TWD 全名為 Taiwan Datums


  • TWD97


    • 採用 1980年國際地球原子參數(Geodetic Reference System 1980, GRS80),以 GPS 衛星定位重新測量,國內是在 1997 年啟用,所以稱作 TWD97


  • WGS84


TWD67 與 TWD97 轉換:



  • 從 TWD97 轉 TWD67


    • X67=X97-807.8-A*X97-B*Y97
      Y67=Y97+248.6-A*Y97-B*X97
      A=0.00001549, B=0.000006521 


  • 從 TWD67 轉 TWD97


    • X97=X67+807.8+A*X67+B*Y67
      Y97=Y67-248.6+A*Y67+B*X67
      A=0.00001549, B=0.000006521 



TWD67 轉 WGS84 (GPS):


$ echo TWD67_X TWD67_Y | proj -I +proj=tmerc +ellps=aust_SA +lon_0=121 +x_0=250000 +k=0.9999


TWD97 轉 WGS84 (GPS):


$ echo TWD97_X TWD97_Y | proj -I +proj=tmerc +ellps=GRS80 +lon_0=121 +x_0=250000 +k=0.9999


目前測試的心得:


先把 TWD67 轉 TWD97 後,在用 TWD97 轉 WGS84 的成果跟 Google Maps 的標記就接近了不少,但仍差大概約25m的距離,也還有一條街的距離。


Python Code:


def WGS84FromTWD67TM2(x,y):
   out = {'status':False}
   lat = None
   lon = None
   
   # TWD67 to TWD97
   A = 0.00001549
   B = 0.000006521
   x = float(x)
   y = float(y)
   x = x + 807.8 + A * x + B * y
   y = y - 248.6 + A * y + B * x

   # TWD97 to WGS84
   result = os.popen('echo '+str(x)+' '+str(y)+' | proj -I +proj=tmerc +ellps=GRS80 +lon_0=121 +x_0=250000 +k=0.9999 -f "%.8f"').read().strip() # lat, lng 格式, 不必再轉換
   process = re.compile( '([0-9]+\.[0-9]+)', re.DOTALL )
   for item in process.findall(result):
      if lon == None:
         lon = float(item)
      elif lat == None:
         lat = float(item)
      else:
         break


   # result = os.popen('echo '+str(x)+' '+str(y)+' | proj -I +proj=tmerc +ellps=GRS80 +lon_0=121 +x_0=250000 +k=0.9999').read().strip() # 分度秒格式


   # 分度秒格式轉換
   #process = re.compile( "([0-9]+)d([0-9]+)'([0-9\.]+)\"E\t([0-9]+)d([0-9]+)'([0-9\.]+)", re.DOTALL )
   #for item in process.findall(result):
   #    lon = float(item[0]) + ( float(item[1]) + float(item[2])/60 )/60
   #    lat = float(item[3]) + ( float(item[4]) + float(item[5])/60 )/60
   #    break
   if lat == None or lon == None:
      return out
   out['lat'] = lat
   out['lng'] = lon
   out['status'] = True
   return out


更多 proj 資訊:


$ proj -le
MERIT a=6378137.0 rf=298.257 MERIT 1983
SGS85 a=6378136.0 rf=298.257 Soviet Geodetic System 85
GRS80 a=6378137.0 rf=298.257222101 GRS 1980(IUGG, 1980)
IAU76 a=6378140.0 rf=298.257 IAU 1976
airy a=6377563.396 b=6356256.910 Airy 1830
APL4.9 a=6378137.0. rf=298.25 Appl. Physics. 1965
NWL9D a=6378145.0. rf=298.25 Naval Weapons Lab., 1965
mod_airy a=6377340.189 b=6356034.446 Modified Airy
andrae a=6377104.43 rf=300.0 Andrae 1876 (Den., Iclnd.)
aust_SA a=6378160.0 rf=298.25 Australian Natl & S. Amer. 1969
GRS67 a=6378160.0 rf=298.2471674270 GRS 67(IUGG 1967)
bessel a=6377397.155 rf=299.1528128 Bessel 1841
bess_nam a=6377483.865 rf=299.1528128 Bessel 1841 (Namibia)
clrk66 a=6378206.4 b=6356583.8 Clarke 1866
clrk80 a=6378249.145 rf=293.4663 Clarke 1880 mod.
CPM a=6375738.7 rf=334.29 Comm. des Poids et Mesures 1799
delmbr a=6376428. rf=311.5 Delambre 1810 (Belgium)
engelis a=6378136.05 rf=298.2566 Engelis 1985
evrst30 a=6377276.345 rf=300.8017 Everest 1830
evrst48 a=6377304.063 rf=300.8017 Everest 1948
evrst56 a=6377301.243 rf=300.8017 Everest 1956
evrst69 a=6377295.664 rf=300.8017 Everest 1969
evrstSS a=6377298.556 rf=300.8017 Everest (Sabah & Sarawak)
fschr60 a=6378166. rf=298.3 Fischer (Mercury Datum) 1960
fschr60m a=6378155. rf=298.3 Modified Fischer 1960
fschr68 a=6378150. rf=298.3 Fischer 1968
helmert a=6378200. rf=298.3 Helmert 1906
hough a=6378270.0 rf=297. Hough
intl a=6378388.0 rf=297. International 1909 (Hayford)
krass a=6378245.0 rf=298.3 Krassovsky, 1942
kaula a=6378163. rf=298.24 Kaula 1961
lerch a=6378139. rf=298.257 Lerch 1979
mprts a=6397300. rf=191. Maupertius 1738
new_intl a=6378157.5 b=6356772.2 New International 1967
plessis a=6376523. b=6355863. Plessis 1817 (France)
SEasia a=6378155.0 b=6356773.3205 Southeast Asia
walbeck a=6376896.0 b=6355834.8467 Walbeck
WGS60 a=6378165.0 rf=298.3 WGS 60
WGS66 a=6378145.0 rf=298.25 WGS 66
WGS72 a=6378135.0 rf=298.26 WGS 72
WGS84 a=6378137.0 rf=298.257223563 WGS 84
sphere a=6370997.0 b=6370997.0 Normal Sphere (r=6370997)


網路資源:



2012年10月5日 星期五

Android 開發筆記 - Unable to resume activity : android.database.StaleDataException: Attempted to access a cursor after it has been closed.

以前常在操作 DBHelper 時,常常因為沒有把 mCursor.close() 而常常出現一些錯誤訊息,後來就很習慣每次取一個 mCursor 出來,用完後就執行 mCursor.close() 來當做完整的收尾,結果開始出現這種訊息並且完全不知道是自己哪一行程式出錯:


FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to resume activity {com.example/com.example.YourActivity}: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
       at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2444)
       at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2472)
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1173)
       at android.os.Handler.dispatchMessage(Handler.java:99)
       at android.os.Looper.loop(Looper.java:137)
       at android.app.ActivityThread.main(ActivityThread.java:4424)
       at java.lang.reflect.Method.invokeNative(Native Method)
       at java.lang.reflect.Method.invoke(Method.java:511)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
       at dalvik.system.NativeStart.main(Native Method)
 Caused by: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
        at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:75)
        at android.database.BulkCursorToCursorAdaptor.requery(BulkCursorToCursorAdaptor.java:144)
        at android.database.CursorWrapper.requery(CursorWrapper.java:186)
        at android.app.Activity.performRestart(Activity.java:4505)
        at android.app.ActivityThread.performRestartActivity(ActivityThread.java:2875)
        at android.app.LocalActivityManager.moveToState(LocalActivityManager.java:168)
        at android.app.LocalActivityManager.dispatchResume(LocalActivityManager.java:523)
        at android.app.ActivityGroup.onResume(ActivityGroup.java:61)
        at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1154)
        at android.app.Activity.performResume(Activity.java:4539)
        at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2434)
        ... 10 more


追到最後,終於發現問題的所在點了,因為我的 mCursor 是從 Activity.managedQuery 產生的,在 Activity.managedQuery 有提到:


Warning: Do not call close() on a cursor obtained using this method, because the activity will do that for you at the appropriate time. However, if you call stopManagingCursor(Cursor) on a cursor from a managed query, the system will not automatically close the cursor and, in that case, you must call close().


這應該就是不懂 Android 架構的關係 Orz 果真懂一點不如不懂 XDD


2012年10月4日 星期四

GPS 座標轉換 NE 常用格式與度分秒

找旅遊資料之餘,發現官方單位給的資料都是類似 N25 02 1.25 E121 33 53.01 這種座標型態,查了一下資料才知道這是度分秒單位。


可以把上述那串丟進 Google Maps 一樣可以通啦。


例如台北 101 GPS 座標為 25.033681,121.564726:


25.033681:


25 度


02 分 = 0.33681 * 60 的整數位 = 2.02086


1.25 秒 = 0.02086 * 60 = 1.2516


121.564726:


121 度


33 分 = 0.564726 * 60 的整數位 = 33.88356


53.01 秒 = 0.88356 * 60 = 53.0136


同理,要從分度秒變成常用的 GPS 座標:


25 度 02 分 1.25 秒:


25 + ( 2 + 1.25 / 60 ) / 60 = 25.0336806


121 度 33 分 53.01 秒:


121 + ( 33 + 53.01 / 60 ) / 60 = 121.564725


後來,大學同學的前東家在做導航機,就說這就像 10進位跟 60 進位的單位轉換囉 :D


[OSX] Adium - 因為 Switchboard 發生錯誤,所以訊息無法送出 @ Mac OS X 10.8

adium

網路上還滿常看到 MAC 上使用 MSN 問題的,在 Mac OS X 上頭,雖然微軟有出 Messenger for Mac,但是我還是滿常碰到 Switchboard 的問題。此問題本身應該是傳訊雙方的網路狀態,例如公司有擋 msn 、防火牆等等,則會常出現這類的問題。

網路上的解法很多是調整 msn 設定,如 "透過HTTP連線" 、 "允許直接連線" 、"連線埠" 等等,這些都可以在微軟的官網也能查到對應的資訊 Windows Live Messenger 使用的網路連接埠與 URL

然而,怎樣設定也解決不了問題 Orz 連改用 Messenger for Mac 也都一樣,最後就換回好用的 Adium ,暫時的解法是…一旦出現 "因為 Switchboard 發生錯誤,所以訊息無法送出" 時,就重新登入,一登入完時可以正常發送訊息。只是每次重新登入也很麻煩,最後想到一招可以 reset Switchboard 了,那就是封鎖對方後,再解除封鎖,也能短暫解除這種問題囉。

Updated: 現在 Skype 6.0 for Mac 已經可以支援 MSN 登入囉!用 Skype 後沒有在碰到這種問題了!