Thursday, December 8, 2011

Lucene On Android

嘗試性的把 Lucene 放到 Android 上面來跑,結果不是太理想,但仍是一些心得分享出來,省去後人嘗試的時間。

Lucene 要跑在 Android 上,第一個碰上的問題是,如何把 index files 傳到手機上去,在 Lucene 中,對 index 的讀取,是以目錄為單位的,所以說,無法把 index files 放在 apk 中直接讀取,一定要存放在 device or external storage 上,才能夠使用;或者是自己弄個虛擬目錄出來,不過,這會耗用過多的計憶體空間。

我是選用把 index 放在 'src/res/raw' 底下,讓他變成 apk 的一部份,省去在網路上找個空間來放置 index 的問題,當要更新 index 時,就重編個 apk 叫使用者更新就好。

在放 lucene index 時,如果你想用 compound format 的話,可以用底下的指令,把多個 index files 包裹成單一檔案 .cfs

// run REPL with 'scala -cp luke-3.4.0_1-all.jar'

import org.apache.lucene.store.FSDirectory
import org.apache.lucene.index._
import org.apache.lucene.analysis.standard._

val source = FSDirectory.open(new java.io.File("source"))
val dest = FSDirectory.open(new java.io.File("dest"))

// open source index
val reader = IndexReader.open(source)

// create writer for compound index.
val analyzer = new StandardAnalyzer(org.apache.lucene.util.Version.LUCENE_34)
val writer = new org.apache.lucene.index.IndexWriter(dir, analyzer, IndexWriter.MaxFieldLength.UNLIMITED)

// force writer always use compound index format.
writer.getMergePolicy.asInstanceOf[LogByteSizeMergePolicy].setNoCFSRatio(1.0)


// add source index to dest index.
writer.addIndexes(reader)
writer.optimized
writer.close

reader.close

接著,是把產生的 segment, segments_1, _0.cfs 拷到 src/res/raw 底下,讓這些檔案變成 .apk 的一部份。


接著,是要在第一次執行時,把這些 index 從 apk 中覆製到 SD 卡上或是機子上,這邊,我寫了個小工具來做這件事

import android.content.Context
import android.os.Environment
import android.util.Log

import com.bluetangstudio.android.disastermap.TaipeiDisasterApp.LogTag

import org.apache.commons.io.FileUtils
import org.apache.lucene.store.{FSDirectory, Directory}

import scala.collection.JavaConversions._
import java.io.File

/**
 *  Helper class that search for lucene index directories on the device. The search order is
 *  external storage first then local storage. If lucene index does not exist on device, this
 *  class will copy the index from the apk to the device storage.
 *
 * @param context  the application context
 * @param path     the root folder name of the index directory to use and to look for.
 * @param source   the source of index resource to copy if index does not exist on the device.
 *                 format: Seq[(filename, resourceId)]
 */
case class LuceneOpenHelper(context: Context, path: String, source: Seq[Tuple2[String, Int]]) {

    /**
     * create or open an Directory.
     */
    def open(): Option[Directory] = {
        val candidates = Seq(externalFolder, internalFolder).flatten

        // find the folder with index in it.
        val folder = candidates.filter(f => f.exists() && f.list().length > 0).headOption
        val withIndex = folder.orElse(
            candidates.find(
                f => {
                    // ensure folder is available.
                    f.exists() || f.mkdirs() match {
                        // folder is not accessible
                        case false => false

                        case _ => {
                            Log.d(LogTag, "Duplicating index from apk to %s...".format(f))
                            source.foreach(
                                s => {
                                    val is = context.getResources.openRawResource(s._2)
                                    try {
                                        FileUtils.copyInputStreamToFile(is, new File(f, s._1))
                                    } finally {
                                        is.close()
                                    }
                                }
                            )
                            true
                        }
                    }
                }
            )
        )

        return withIndex.map(FSDirectory.open(_))
    }

    private def externalFolder: Option[File] = {
        Environment.getExternalStorageState match {
            case Environment.MEDIA_MOUNTED => Option(context.getExternalFilesDir(path))
            case _ => None
        }
    }

    private def internalFolder: Option[File] = {
        return Option(new File(context.getFilesDir, path))
    }

}

最後,是在 *App 上加上這段

object MyApp {
    private val INDEX_DIRECTORY = "idx"

    private val INDEX_FILES = Seq(
        ("_0.cfs", R.raw.idx_0), 
        ("segments", R.raw.segments), 
        ("segments_1", R.raw.segments_1)
    )
}
class MyApp extends android.app.Application {

    import MyApp._

    private var _luceneSearcher: Option[IndexSearcher] = None

    def luceneSearcher: Option[IndexSearcher] = {
        if (_luceneSearcher.isEmpty) {
            Log.d(LogTag, "Initializing new IndexSearcher...")
            _luceneSearcher = LuceneOpenHelper(this, INDEX_DIRECTORY, INDEX_FILES).open().map(new IndexSearcher(_))
        }
        _luceneSearcher
    }
   
    override def onLowMemory() {
        _luceneSearcher.foreach(s => s.close())
        _luceneSearcher = None
    }
}

這樣一來,就能在 Android 上面跑 lucene-core 了.

Thursday, November 24, 2011

Analytics: Hadoop V.S. DBMS

最近兩周,我開始參加 GTUG 的活動,在活動間,有人問起,為什麼我公司選用DBMS(MySQL)來做資料分析,而不用 Hadoop。在這邊,我把跟公司的DB怪才同事的討論結果,稍為簡單敘述一下。

首先,要先理解到的一點是,在 MapReduce 及 Hadoop 出現之前,Data Mining的工具及技術,已經在DBMS上建構了有30年了,相關的技術也發產的很成熟且有許多專書論述。

ETL(Extract, Transform, Load)是這個領域的關鍵字,用 ETL 去找下去,會找到許多的書籍講怎麼做資料分析
  • Extract: 如何在雜亂的原始資料中,找出有用的訊號,並把這些訊號歸檔
    例如:把 Apache 的 access.log 轉換成一對對的 (timestamp, session)
  • Transform: 如何把整理好的訊號,轉換成更好用的中介查詢表格
    例如:把前面的 (timestamp, session) 轉換成以小時為單位的存取次數 (time-slice, count)
  • Load: 把整理好的資料,存到資料庫中。
透過這個轉換過的Fact Table,我們可以把很昂貴的『每日訪問總數查尋』從
# pseudo code 應該是錯的,不過你知道我在表達什麼就好了 :P

SELECT COUNT(UNIQUE session) FROM http_access
  GROUP BY DATE_FORMAT(timestamp,'%Y %m %d')
變成
SELECT SUM(count) FROM access_counts
  GROUP BY DATE_FORMAT(time_slice,'%Y %m %d')

假設,你的原始資料有一整年這麼多,總共有四千萬筆(40M),那麼,前者就是把四千萬筆的資料讀出來,然後過濾加總,而後者,由於資料匯入時已先處理過了,所以要處理的只有24 * 365筆資料;而這兩者間所花費的時間差易,會隨著查尋次數而產生重大差異。 

試問,若是每次你老闆跟你要資料時,你要跟他說,要等半小時才可以,還是說等我五分鐘? 至於前面的半小時是怎麼算出來的呢?

由於DB運算,瓶頸多是卡在硬碟存取上,在前面的例子中,40M筆資料,假設一筆是1K bytes,那麼,總資料量是 40GB ,從硬碟循序讀取 40GB 出來,所花的時間是 40GB / 40MB/s(硬碟速度) = 17分鐘

MapReduce & DBMS


MapReduce是把資料分拆運算,然後再加總的技術;但是,MapReduce的概念對 RDBMS 並不是什麼新奇的事,在這份IBM的文件中,便清楚的敘述到,DBMS會對 SQL 指令做編譯,DBMS內部就會依資料型態、INDEX檔、CPU Core的數量等,去做最佳化,這些,是Hadoop不會幫你做的。

既然DBMS那麼好那麼,為什麼會有 MapReduce 等技術的出現呢,那是因為,DBMS可以幫你scale up,但是最終,還是會碰上硬體的極限,那麼,這時只有 scale out ,才能解決這問題。 但是 scale out 的程式難度,總是比 scale up 困難,而且,一個不好的分析模式,並不會因為跑的快10倍,就變得比較好;因此, ETL 的概念,在使用 MapReduce 時仍是可以應用的,


講到這邊,什麼時候該用 Hadoop ,什麼時候該選用 DBMS ,應該很清楚了
  1. 首先,就是要先看你要找的分析訊號是什麼,定好分析的步驟及查詢表格
  2. 再來,看你的資料量會有多大,做些簡單的計算,看看是不是能在合理的時間內跑完
  3. 最後,看你這些報表倒底是有多常被取用,若是,這是內部的報表,只要一個月能出一次就好,那麼,Hadoop可能就大才小用;若是,你是要做產品,像Google Analytics做到即時大量分析,那麼你可能就需要用到Hadoop

    過去我在 Medio 服務時,公司有幫美國幾大電信商分析 App 的購買行為,由於,這些是每個月才需產生一次的報表,而且,資料的訊號有待我們的 data expert 去尋找,因此,我們的資料專家是把資料倒到 MicroStrategy 再去下指令分析的,並沒有用到 Hadoop

Tuesday, November 22, 2011

Source code of my Scala on Android Project.

把前面幾回講的用Scala寫的Android程式,放上 bitbucket 了,有興趣的可以自己去下載來看。

https://bitbucket.org/bluetang/android-taipei-disaster

Wednesday, November 16, 2011

My experience with Scala on Android

用Scala開發Android,是個滿有趣的經驗,大致上來因為我切入的時間點較晚,所以大部份的問題已經被前人所解決,就用 https://github.com/jberkel/android-plugin 把專案用 sbt 開好後,就可以開始寫 android.

我用的 IDE 是 intellij 11 EAP,用 sbt-idea 把 idea project 設好後,把預設的 asset pa th 從 .idea_module 改成 src/main ,就可以開始開發了。


在開發上,除了前一回碰上的proguard問題外,我還碰上另一個比較嚴重的問題-不能在Scala裡寫 AsyncTask

這問題跟SI-3622 SI-3494有關,看來是在 Scala 2.8解掉的問題,2.9又跑回來了,我這邊看到的狀況是

override protected def doInBackground(params: Params*): Result

會被Scala compiler專換成
public Result doInBackground(Seq params)
    public Result doInBackground(Params[] params)


而底下的code,則是scala compiler會吐出 overides nothing.
override protected def doInBackground(params: Array[Params]): Result

不管怎樣,都跟Android要求的protected Result doInBackground(Params[] params)不同,所以在runtime時會跑出NoSuchMethodError.

解決方法是在 java 裡寫個 bridge interface ,幫 scala compiler 搞不定的東西,在這個 interface 裡定意好

public abstract class SAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {

    protected abstract Result doInBackground(Seq<Params> params);

    @Override
    protected Result doInBackground(Params... paramses) {
        return doInBackground(JavaConversions.asScalaBuffer(Arrays.asList(paramses)));
    }
}

Tuesday, November 15, 2011

Data Modeling With Jackson Json and Scala - Proguard

關於Proguard,官方的網頁是這麼自述的:
ProGuard is a free Java class file shrinker, optimizer, obfuscator, and preverifier. It detects and removes unused classes, fields, methods, and attributes. It optimizes bytecode and removes unused instructions. It renames the remaining classes, fields, and methods using short meaningless names. Finally, it preverifies the processed code for Java 6 or for Java Micro Edition.

在Mobile App開發的時候,多會用proguard把沒有用到的程式碼濾除,並把變數名稱跟函式名稱用更精簡的字串來取代,這樣編出來的程式會更小,以我用Scala開發的Android程式,使用的的函式庫大小超過10MB,但是用proguard編出來的class.min.jar只有1.7MB,8.xMB的scala-runtime.jar一大票沒用到的功能都被移掉了。

這麼好的功具當然也有他的問題存在,proguard是使用靜態分析的方式,去追縱看有那些程式碼會被執行到,有那些程式碼是不會被碰到的可以被移除的;但由於Jackson是使用Reflection的方式去取得物件的屬性及是用reflection的方式去生成物件,因此,這些行為並不會被proguard偵測到,反而是被認為是dead code而被移除掉。

另外proguard會把變數名稱改寫,這也是跟jackson不相容的地方,當getXxxx被改寫成gY,jackson自然無法知道這是Xxxx的getter,因此若是要在proguard的環境下使用jackson,obfuscator是要被關掉的。

底下這邊,是我在台北積水地圖內,用的proguard設定檔

proguardOption in Android :=
      ("-dontoptimize -dontpreverify -dontobfuscate"  // shrinking only
          :: "-dontskipnonpubliclibraryclassmembers"  // keep Jackson's internal classes
          :: "-dontskipnonpubliclibraryclasses"       // keep Jackson's internal classes
          :: "-keepattributes *Annotation*."          // keep Jackson Json Annotations.
          :: "-keep class org.codehaus.jackson.**"
          :: "-keep class com.bluetangstudio.android.model.**"
          :: "-keep class com.bluetangstudio.searchcloud.client.**"
          :: """-keep class com.bluetangstudio.searchcloud.client.** {
                 (...);
                 public static ** valueOf(...);
             }"""
          :: """-keep class com.bluetangstudio.** {
                 void set*(***);
                 void set*(int, ***);

                 boolean is*();
                 boolean is*(int);

                 *** get*();
                 *** get*(int);
             }"""
          :: """-keep class * implements android.os.Parcelable {
                 public static final android.os.Parcelable$Creator *;
             }"""
          :: """-keepclassmembers class * {
                 ** MODULE$;
             }"""
          :: "-keep public class org.xml.sax.EntityResolver"
          :: "-keep public class scala.Either"
          :: "-keep public class scala.Function1"
          :: "-keep public class scala.Function2"
          :: "-keep public class scala.Tuple2"
          :: "-keep public class scala.collection.Iterable"
          :: "-keep public class scala.PartialFunction"
          :: "-keep public class scala.collection.Seq"
          :: "-keep public class scala.collection.TraversableOnce"
          :: "-keep public class scala.collection.generic.CanBuildFrom"
          :: "-keep public class scala.collection.immutable.Map"
          :: "-keep public class scala.collection.immutable.List"
          :: "-keep public class scala.collection.mutable.StringBuilder"
          :: "-keep public class scala.Predef$$less$colon$less"
          :: "-keep public class scala.math.Numeric"
          :: "-keep public class scala.math.Ordering"
          :: "-keep public class scala.reflect.ClassManifest"
          :: "-keep public class scala.runtime.IntRef"
          :: "-keep public class scala.runtime.BooleanRef"
          :: "-keep public class scala.runtime.AbstractFunction1"
          :: "-keep public class * extends android.app.Activity"
          :: "-keep public class * extends android.app.Application"
          :: "-keep public class * extends android.app.Service"
          :: "-keep public class * extends android.appwidget.AppWidgetProvider"
          :: "-keep public class * extends android.content.BroadcastReceiver"
          :: "-keep public class * extends android.content.BroadcastReceiver"
          :: "-keep public class * extends android.app.backup.BackupAgentHelper"
          :: "-keep public class * extends android.content.ContentProvider"
          :: "-keep public class * extends android.view.View"
          :: "-keep public class * extends android.preference.Preference"
          :: Nil
      ) mkString " "

Data Modeling With Jackson Json and Scala - lazy val

Scala中有個好用的功能是 lazy val - lazy initialized variable ,然而,Scala在實作這功能時,是會加入一個
public volatile int bitmap$0;

若是這欄位沒有被從生成的 json 中替除的話,那麼,讀回來的 scala object 中,所有的lazy val,都會仍是 null ,不會被初始化。

這個問題,只有做好unit-testing可以避免,因為這個欄位是 public 的,所以不需要setter or constructor, Jackson 就可以直接把值設定進去,只用unit-test仔細檢查生成的,確定沒有預料之外的欄位被寫入到 json 中,才可以免去後來在 runtime 發生不可預料的問題。

至於怎麼去除這欄位呢,只要在class上標上@JsonIgnoreProperties({"bitmap$0"})即可

Data Modeling With Jackson Json and Scala - Polymorphic Types

關於 Jackson Json 對多型的支援,原開發者已經有一篇 blog 很清楚的解釋該怎麼做,因此,我只寫下在 scala 上面要注意的眉角。

首先,我先寫下理想中,Jackson在Scala上該怎麼使用,在來談,為什麼不能這麼用
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(Array(
     new JsonSubTypes.Type(name = "flood", value = classOf[Flood]),
     new JsonSubTypes.Type(name = "mountainslide", value = classOf[MountainSlide])
)) 
trait Disaster {}

case class Flood @JsonCreator()(
    @BeanProperty @JsonProperty("event") event: String,
    @BeanProperty @JsonProperty("address") address: String
) extends Disaster

case class MountainSlide @JsonCreator()(
    @BeanProperty @JsonProperty("event") event: String,
    @BeanProperty @JsonProperty("landmark") landmark: String
) extends Disaster


在 scala 2.8.0 上面,我碰上的問題是不能在annotation內使用 java enum 如: use = JsonTypeInfo.Id.NAME

在 scala 2.9.1 上,上面的問題消失了,不過碰上另一個問題,當一個 annotation 上面有兩個 java enum,scala compiler 及 java compiler 都不會吐出錯誤,但是annotations從bytecode中消失了,SI-5165中紀錄了這問題。

不管怎樣,在現在這時間,若是你想用Jackson + Scala來處理多型,那麼,最頂層類別,一定要用Java來寫,才不會碰上上述兩個問題。

Data Modeling With Jackson Json and Scala

我用Scala有一年半左右,第一次看brianhsu介紹 case class ,就對 Scala 的簡潔著迷,當然,經過了一年半後,許多的面紗揭開後,Scala在與Java函式庫整合的小問題就跑出來了,接下來,我會寫寫怎麼用Jackson Json及Scala來設計一個Json WebService API的Model Objects.

case class


Scala的case class在經過Scala compiler轉換後,就會生成一個POJO,例如這樣一個簡單的case class
case class Disaster(event: String)

會被轉換成如下的Java Code
public class Disaster
    implements ScalaObject, Product, Serializable
{
    public String event()
    {
        return event;
    }
    public int hashCode()
    {
        return ScalaRunTime$.MODULE$._hashCode(this);
    }
    public String toString()
    {
        return ScalaRunTime$.MODULE$._toString(this);
    }
    public boolean equals(Object obj)
    {
        //removed
    }
    public Disaster(String event)
    {
        this.event = event;
        super();
        scala.Product.class.$init$(this);
    }
    private final String event;
}

這個POJO跟一般POJO的不同處在於
  1. immutable object,沒有辦法去更改這物件的屬性
  2. getter的命名不是Java通用的 getXxxx() 而是 xxxx()

這在一般使用上,不是問題,反而是優點,然而,當於 Java binding framework 如 Jackson Json 或 JAXB 要做整合時,就變成問題了,原因在於,多數的 java binding frameworks 在將資料轉回Java Object時,多是呼叫沒代參數的default constructor,然後再呼叫setter(s)把物件的屬性傳進去。

另一個問題則是,多數的java binding frameworks在偵側那些屬性是serializable時,要麻是用java annotation一個個屬性去標,要不然就是用 refection 去找 getXxxx()

而好死不死的plain scala case class沒有辦法滿足上面兩個條件,因此,我們要想辦法繞過這兩個問題。第二個問題比較好解,只要在屬性上標上 @scala.reflect.BeanProperty ,Scala compiler就會自動幫你把你的屬性加上 getXxxx() 及 setXxxx(...)。

case class Disaster(@BeanProperty event: String)

第一的問題,在Jackson Json的解法是用 @JsonCreator 去指定不代變數default constructor的替代品,然而,在這邊我們須要把每一個屬性的名稱,用 @JsonProperty("xxx") 標在 constructor 的參數上,這是因為,Java compiler在編譯時,會把 parameter 的名稱擦去,如 public Disaster(String event) 在經過編譯後的 bytecode 中只剩下 public Disaster(String p1),因此我們須要用java annotation來強制幫參數取個名稱,這樣,當Jackson在把 Json String 轉成Object時,才知道該把那個json object的屬性對到java object的那個屬性之上。

case class Disaster @JsonCreator()(
    @BeanProperty @JsonProperty("event") event: String
)




當我們的Scala case class已經是Jackson Json Serializable後,我們要怎麼樣把scala case class object轉成json string再轉回來呢,請看底下的例子。

val disaster = new Disaster("88 typhoon")
val mapper = new org.codehaus.jackson.map.ObjectMapper()

val json = mapper.writeValueAsString(disaster)
assert(json === """{"event": "88typhon"}"""}
  
val copy = mapper.readValue[Disaster](json, classOf[[Disaster])
assert(copy === disaster)

這邊我們會碰上Scala較Java語法上比較沒有那麼漂亮的地方,在Java裡,若是函式支援generic 且又有個Class的參數,那麼,我們不用把傳兩次進去這個函式,如

Disaster copy = mapper.readValue(json, Disaster.class)

因此在Scala的這邊,我會寫個JsonSerializer來把 scala 這邊的語法弄漂亮些

object JsonSerializer {
  private val mapper = new ObjectMapper

  def fromJson[T <: AnyRef](jsonString: String)(implicit m: Manifest[T]): T = {
    mapper.readValue(jsonString, m.erasure).asInstanceOf[T];
  }
}
// readValue the scala way.
val disaster = JsonSerializer.fromJson[Disaster](json)

最後,@JsonCreator不一定是要標在constructor上面,他也可以標在static method上面,這樣一來,可以讓你做到scala constructor無法做到的一些處理

case class Disaster(
    @BeanProperty event: String, 
    location: Option[GeoPoint]) {
  
  /** helper method for jackson json */
  def getLocation = location.getOrElse(null)
}

object Disaster {

  @JsonCreator
  def newInstance(
    @Property("event") event: String,
    @Property("location") location: GeoPoint): Disaster = {
    
    val locationOption = if (location == null) {
       None
    } else {
       Some(location)
    } 
    return new Disaster(event, locationOption)
  }
}
下一回,我們會看,如何讓Jackson Json能夠對多型的物件做處理

台北市政府公開資料(三)- 台北積水地圖

花了三個禮拜,學習怎麼開發Android程式,在沒買開發機的狀況下,在模擬器上完成了我的第一個Android App - 台北積水地圖







資料來源一樣是從台北市政府所來,原始資料檔可從『臺北市歷年積水紀錄圖』下載。我把資料檔的座標系轉換(TW67 -> WSG84)後,就塞到跟前一回一樣的後端去做處理,剩下的就都是前端的工作了。

在前端UI開發上,依以前開發的習慣,先把wireframe(假UI)畫好才動工,找了找一些畫UI的工具,最後買了keynotopia出的工具,這傢伙也是個奇人,開start-up不到三小時就開始獲利;在畫假UI時就發現,傳統上我們用點來代表一個物件,在畫地圖(羞~)時,並不能合理的表現許多地理圖型。

然而,若是我們要把地理圖型直接畫在地圖上讓使用者直接點開,那麼,我們又要處理重疊的狀況,這在淹水地圖上就特別麻煩,因為常淹水的區塊會互相重疊多次,或像納利颱風,一次淹掉了一大塊的區塊,在處理點選時,我們要怎麼知道使用者點選的是那一層的資料?

而在顯示上,要怎麼處理重疊的區塊,讓使用者知道,某個地點曾淹水多次?一個選項是用顏色來做,重疊的部份用加深的顏色來顯式;最後,我們還是回歸傳統的點座標的方式,來表示淹水的區域,等使用者點開後,才用色塊畫出實際淹水的地區。等下次改版時,等我對Android 2D再熟一些時,或許會嘗試用顏色的方式來做呈現。


接下來,我會在有空的時候,再開發一個程式,把台北市的土地分區(住一、住二、商用...)資料,跟土地的地號資料,做成個台北都更地圖,若各位有類似的需求,請與我聯絡

Monday, October 17, 2011

台北市政府公開資料(二)- 按地理位址搜尋資料

多數的搜尋技術,在處理地理位址時,只處理地點的資料,例如餐廳、百貨公司等小地理區塊等,但是這些地點搜詢技術,並不適合用在處理大區塊範圍的資料,如行政區及學區等。

如多數外國網站用的坐標轉行政區的GeoNames,他在搜尋某個座標目前所屬的行政區時,是計算座標點與附近行政區中心的距離來猜測,但是行政區往往不是正圓型的,行政區的劃分多是依自然環境(如河川)來切割的,所以,既始某個座標離行政區甲較近,但是,在實務上則是被劃分到乙行政區去的;類似的例子還有學區等

另一類無法用點去描述 的,是線型的資料,如登山步道、腳踏車道,當我們登山時,我們不一定要從起點開始爬,而是可以從中間點開始加入,因此,當我們在找附近所有的登山步道時,該是計算所有線型資料與目前座標點的最短距離,而非是算起點與目前座標點的距離。

依據了這套需求,我在我公司的產品上增加了對地理區塊搜尋的功能,讓用戶能對點(Point)、線(LineString),多邊型(Polygon)等資料,做搜尋。

底下的連結,是搜詢台北101大樓所在的行政區,大家可以玩玩看


{"results": [{"id": 302,"properties": {"AREA": 522872.00372,"NEW": 6300200001,"FULL": "臺北市信義區西村里","PERF_ID": 63002,"COUN_ID": "6300200","CPID": "63002","CPTID": "6300200","CPTVID": "6300200001","NPID": 63002,"NPTID": 6300200,"NPTVID": 6300200001,"PNAME": "臺北市","TNAME": "信義區","VNAME": "西村里","PTVNAME": "臺北市信義區西村里","PTNAME": "臺北市信義區","TVNAME": "信義區西村里","TM2_MAX_X": 306528.415,"TM2_MAX_Y": 2770435.21,"TM2_MIN_X": 305428.311,"TM2_MIN_Y": 2769739.57,"MAX_X": 0,"MAX_Y": 0,"MIN_X": 0,"MIN_Y": 0}}],"metadata": {"searchQuery": {"queries": [],"constraints": [{"@type": "location","field": "geometry","location": "25.033611,121.565","radius": 1}],"pagination": {"start": 0,"count": 1}},"start": 0,"count": 1,"total": 1}}

Thursday, October 6, 2011

轉換台北市政府公開資料的格式

在台北市政府公開的資料中,有一大部份是用shapefile的格式開放的,如何把這些資料轉換出來,變成是一個問題,在這邊把我怎麼把TWD67座標的shapefile,轉換成WSG84的 GeoJSON格式。

首先,請參考前文把里介圖在QGIS中打開。

再來按這邊講的,把TWD67的定義加入到 QGIS 中

接著,選Layer Property -> Set Layer CRS -> TWD 67

然後,指定專案用的CRS定意,FIle -> Project Property -> CRS -> WSG 84

最後,是把Layer另存新檔,Layer -> Save As -> File Format: GeoJSON, Encoding: UTF-8

這樣,就可以把shapefile的內容匯出來,變成像下述的格式

{ "type": "Feature", "id": 0, "properties": { "AREA": 16326625.055040, "NEW": 6301200042.000000, "FULL": "臺北市北投區湖田里", "PERF_ID": 63012.000000, "COUN_ID": "6301200", "CPID": "63012", "CPTID": "6301200", "CPTVID": "6301200042", "NPID": 63012.000000, "NPTID": 6301200.000000, "NPTVID": 6301200042.000000, "PNAME": "臺北市", "TNAME": "北投區", "VNAME": "湖田里", "PTVNAME": "臺北市北投區湖田里", "PTNAME": "臺北市北投區", "TVNAME": "北投區湖田里", "TM2_MAX_X": 306973.245000, "TM2_MAX_Y": 2789383.300000, "TM2_MIN_X": 301490.096000, "TM2_MIN_Y": 2783263.350000, "MAX_X": 0.000000, "MAX_Y": 0.000000, "MIN_X": 0.000000, "MIN_Y": 0.000000 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 121.522589, 25.174993 ], [ 121.522587, 25.174995 ], [ 121.522557, 25.175033 ], [ 121.522408, 25.175170 ], [ 121.522317, 25.175324 ], [ 121.522314, 25.175367 ], [ 121.522323, 25.175569 ], [ 121.522315, 25.175789 ], [ 121.522322, 25.175950 ], [ 121.522270, 25.176082 ], [ 121.522158, 25.176302 ], [ 121.522043, 25.176548 ], [ 121.522002, 25.176598 ], [ 121.521997, 25.176648 ], [ 121.522278, 25.177273 ], [ 121.522359, 25.177825 ], [ 121.522384, 25.178200 ], [ 121.522338, 25.178332 ], [ 121.522127, 25.178615 ], [ 121.521821, 25.178986 ], [ 121.521780, 25.179026 ], [ 121.521691, 25.179134 ], [ 121.522349, 25.179671 ], [ 121.522623, 25.179892 ], [ 121.522810, 25.179997 ], [ 121.522949, 25.180089 ], [ 121.523101, 25.180244 ], [ 121.523337, 25.180539 ], [ 121.523425, 25.180655 ], [ 121.523550, 25.180683 ], [ 121.523641, 25.180779 ], [ 121.523652, 25.180779 ], [ 121.523816, 25.1809



Tuesday, September 6, 2011

如何在Mac OSX上,讀取台北市政府放出來的地圖資料

台北市政府把一些地圖資訊,用shapefile的格式放出來了,玩了兩天,總算把這檔案開了起來。為了造福後人,就把過程寫下來了

1. 安裝QGIS
2. 下載里界圖,並解壓縮

開起QGIS,從選單上選 Layer -> Add Vector Layer -> Encoding = BIG5, Dataset = 解開的檔案



一開始開啟的檔案,並沒有把文字資料一起顯示出來,所以,接著我們要把這資料打開來。從Layers中點選『台北市里界圖』並選Labels,Display labels勾起來,然後Field選PTVAME,然後按OK



這樣,文字資料就跑出來了



接下來的兩週,我會把Geometric search的功能,加到我公司的產品中,讓你直接能後匯入多邊型的地理資料,並且能夠做一些簡單的搜尋功能

Sunday, September 4, 2011

關於北市府開放資料授權方式的一些想法

在某些人士運作之下,中華民國政府及台北市政府,示範性值的,開放了一些資料出來,開放出來的資料及品質,大家自己去看就好,我懶得去說了。

我比較有意見的是,這些開放資料的授權方式。目前北市府網站已故障,找不到合約。跟據我的印象,第二條是寫『資料的使用必須要就有公益性的部份,提供免費版』 很明顯,這個條約是個蠢左派公務員定的,當大家都是喝空氣過活當神仙。

公益性,是個很好打嘴砲的東西,但是,什麼叫公益性質呢?簡單說來就是,『當一個人把自己的勞力或財產,無償供應其它人使用』就變成有公益性;例如,我無償打掃公司外路口的走道,叫有公益性,我無償把土地借出來讓大家開會,也叫有公益性,問題在於,若是那一天我開始收費,同一個行為就變成無公益性了。

不知你有看出來這授權問題了嗎?只要有人跳出來說,某程式很多人用不應該收費,那開發商馬上要變成做白工,這種授權方式,會有認真的開發商想跳下去嗎?當然是沒有。

所以我說,這授權方式是蠢左派定的,再者,公益性跟營利並不起衝突阿;若是有衝突,那,負責收集這些資料的公務員,可不可以也不支薪來做事啊。

Thursday, September 1, 2011

Having a good build system is like living in the heaven, and having a bad.....

Having a good build system is like living in the heaven, and having a bad build system is like living in the hell. I am just tired of dealing with all kind of build system. In short, all build systems are either suck or sucks more.

Yesterday, I spent 4 hours to help a coworker to deal with library projects in Android. It wasn't a pleasure experience. Today, I spent another 4 hours to figure out how to configure an integration-test phase in SBT.

Just to save you some time, here is how to do it with SBT's quick configuration DSL, build.sbt.

import sbt._

{
    lazy val IntegrationTest = config("it") extend(Test) extend(Provided) extend(Optional)
    lazy val itSettings: Seq[Project.Setting[_]] =
        inConfig(IntegrationTest)(Defaults.testSettings) ++
        Seq(
             ivyConfigurations += IntegrationTest
        )
    seq(itSettings : _*)
}

That is all you need. The first line in the block defines a new configuration, it. The 'it' configuration extends all the dependencies from 'Test'. And the fifth line exposes this configuration to ivy and sbt.

Thursday, June 30, 2011

為什麼Apple辦得到,Google辦不到

講一下我理解中,為什麼Apple 辦得到(合乎中華民國消保法)Google 辦不到的原因。

要知道,在AppStore & Android Market上交易,可不是只有Apple/Google與消費者,交易的過程中,還有軟體開發商這單位。

Google面對的不是只有消費者這塊,Google可是還有跟千千萬萬個軟體開發商,簽有合約,Google若是片面為了服合中華民國的法規,而在台灣進行七日鑑賞期,那可是違反了Google與軟體開發商所簽屬的合約。這一頭另一頭,可是比台北市政府還大多的EA等軟體商。

今天,Google把整個Android Market下架,仍是不脫他 Don't Be Evil的口號。

既然今天Google沒有辦法,在法規上及技術上,快速的處理這問題,那麼,把Android Market下架,是唯一能合法處理這問題的方式。只有待Google修改過Market程式,以及與眾多軟體商,解釋這狀況,讓軟體商自由選擇是否要在這特殊市場上上架。

在這天來臨之前,這是唯一,可以在不違反眾多法規及合約下的解決方案。

至於北市府葉大官人,對Google在合法的範圍內,用他意料之外的方式,處理這問題;而腦羞成怒,對Google合法的作為,開罰一百萬。

我只能說,法律是天龍人在玩的。

至於 Apple為什麼可以,因為 Apple跟軟體商簽的合約本就有30天退費條款阿




推 coldlian:xbox live跟psn要是被北市府告 不知道會如何應付喔?       07/01 03:16
→ coldlian:會為了台灣而修改現行於全球的規則嗎?                    07/01 03:17

這又要談到另一個商業上問題了 Android Market 與其它 Market 的不同。

整個 Android平台,當初是為了讓各別電信商,能有自我掌控的權力,所以沒有推一個一統的 Market ,這樣的好處是,電信商會推 Android比較用力。

至於 Apple那大一統的平台,讓電信商無利可途,所以在美國,也是持到iPhone上市的四年後,才有第二家電信商開始賣 iPhone

Apple/MSFT/SONY 為了賣軟硬體,一定會進全力要求開發商,符合當地法規的要求。

但是 Android Market 只是 Google 的示範平台,所以要解決法規問題,讓電信商去自建Market處理吧

對於北市府開罰Google Android Market的一些感想

對於一個新興、發展中的行業,開明的政府,應該採取延後立法的動作;在一個運作正常,還未出現嚴重問題的市場,政府不該強硬將現有的法規,不經與業界及消費者團體討論資詢,就強加以規範,只能說郝軍頭上行下效。垃圾袋政策如此,App Market也如此。

今天Google被罰,跟本是天龍人間的自我感覺良好。月初市議員要求市府處理iPhone App Store上 350,000的 App中,一隻無法運作的程式的問題。

就為了這 1/350,000  的問題程式,葉大官人,就這麼巧妙(恐怖)的善用行政解釋的權力,在短短的幾日內,就把數個發展中的數位產業,輕輕鬆鬆的就納入了現有的消保法去管理。


這之間的過程,比立法院立法還簡便,立法程序好歹會有行政單位、幾個立委助理看過法案再送表決;今天,一個擁有極大權力的行政官員,明知法已不適用,卻仍執意擴大解釋消保法適用範圍。在部落格上,未經見面,就以上到下的指導口氣告訴商家

『我希望Google能意識到自己犯了多大的錯誤—不是對臺北市政府,而是對全世界信賴Google跟Android系統的消費者。』

只能說,好大的官威。大官人,請你了解,你的一言一行,對我們這些小民,有多長遠的影響好嗎。

ref:
1. 蘋果黑心APP再不退錢 今將被重罰
2.從處罰Google一百萬談起

葉狀師表示:『也有網友質疑,軟體不應該適用郵購買賣七日鑑賞期的規定。我必須同意這是一個非常有爭議的議題,但是,目前法律並沒有將軟體買賣排除消費者保護法七日鑑賞期之適用,所以Google不能自己變成立法者,自己決定適不適用消保法的規定—台灣的業者不能,Google也不能。』