[Scala] 打造地址反查經緯度的簡易函式庫。

話說 Android 裡的 Googple Maps API 沒有地址反查經緯度的功能,有點小不方便,於是我就自己弄一個簡單的 Scala 函式庫出來了。

這裡簡單介紹一下做法,首先一開始先來定義我們的使用者端介面。在這邊,由於 Google Maps 在查詢地址時返回的是一個列表,所以我們的函式庫也是傳回一個 List。

另外,像很多國家都有『南港』這個地名,為了縮小範圍,我們也定了另一個介面,可以指定查詢的國家。

整體使用的感覺如下:

val sinica: List[GeoPlacemark]  = new GeoService ("中央研究院") // 預設查詢
val nangang: List[GeoPlacemark] = new GeoService ("南港", "tw") // 指定國家

println (sinica.placemarkList)  // 取得『中央研究院』的相關地理資料
println (nangang.placemarkList) // 取得『南港』的地理資料

在上面的程式裡,GeoPlacemark 的部份就是該地址的地理資訊,我們希望這個物件存放下列幾像資訊。

  • 原始查詢字串
  • Google Maps 幫我們解析出的詳細地址
  • 範圍精確度
  • 經度
  • 緯度

另外,我們也希望能夠有方便的函式可以幫我們把經度和緯度轉換成 GeoPoint 用的 microdegree。

由這邊可以看出來,將 GeoPlacemark 設計成 immutable 是比較合理的,因為你不應該手動更改任何查詢回來的經緯度結果。此外,在這邊我們需要的都只是單純的取得物件相關的資料,沒有什麼複雜的動作。

這種資料結構,在 Scala 裡最適合使用 case class 來實作了,我們實作出的程式碼如下。

case class GeoPlacemark (val query: String, val address: String, 
                         val accuracy: Int, val longitude: Double,
                         val latitude: Double)
{
    val longitudeE6 = (longitude * 1E6) toInt
    val latitudeE6  = (latitude  * 1E6) toInt
}

在這段程式碼裡,我們宣告了一個 GeoPlacemark 的 case class,建構子傳入的參數剛好就是上面清單列出來的資料。至於 longitudeE6 和 latitudeE6 因為可以直接從傳入的經緯度推算,所以不用放在建構子中,使用 val 變數而不用 def 定義,是因為這樣這兩個計算只會在建立物件時執行一次而已。

別懷疑,就真的只有這樣而已。剩下的 Scala 都幫你搞定了,包括各個變數的 getter 以及相關的 toString() 和 hashCode() 這類東西,而且還可以用在 Pattern Matching 上!

接著來看我們的 GeoService 類別吧!這邊的重點其實不多,只有幾個而已。

首先,我們要從一個網址取回資料時,只要用 Scala.io.Source 就可以了。

val url = "http://maps.google.com/maps/geo?q=%s&output=xml&gl=%s".
          format(query, locale)

val source = Source.fromURL (url, "utf-8")
val iter   = for (line <- source.getLines) yield line
val xml    = XML.loadString (iter.mkString)

裡面的 iter 會是一個 Iterator,每次一行,可以使用 iter.mkString 轉成字串,接著再用 scala.xml.XML.loadString 把他轉成 XML 資料結構,夠簡單吧?連 HTTP Connection 都不用建立了。如果只是單純的要抓網頁資料,像是 RSS/ATOM 的話,用內建的 io.Source 類別就很足夠了。

接著要檢查回傳值,如果 Google Maps 成功反解地址的話,會傳回 200。

statusCode = (xml \ "Response" \ "Status" \ "code").text.toInt

// code 200 means OK
if (statusCode != 200) {
    throw new Exception ("Query Failed")
}

val placemark   = xml \ "Response" \ "Placemark"
val addressList = for (node <- placemark) yield 
                      createGeoPlacemark (node)

上面的程式碼裡,我們利用類似 XPath 的方式,取得 Status 下的 code 節點的內容,接著轉成整數,如果不是 200 代表查詢失敗。成功的話,把每一個 Placemark 節點丟到 createGeoPlacemark 裡,建立一個 GeoPlacemark 物件的 List。

private def createGeoPlacemark (node: Node) = 
{
    val address    = (node \ "address").text
    val accuracy   = (node \ "AddressDetails" \ "@Accuracy").text
    val coordinate = (node \ "Point").text.split (",").toList
    val List (longitude, latitude, _) = coordinate

    GeoPlacemark (query, address, accuracy.toInt, 
                  longitude.toDouble, latitude.toDouble)
}

createGeoPlacemark 沒啥特別的,就是取得 XML 的內容,解析後把他丟給 GeoPlacemark 的建構子,產生 GeoPlacemark 物件而已。

結合上述所有的東西,我們的 GeoService 物件就出來啦。

class GeoService (val query: String, val locale: String)
{
    lazy val placemarkList: List[GeoPlacemark] = doQuery ()

    private var statusCode   = 0
    private var errorMessage = ""

    def this (query: String) = this (query, "")
    def error = (statusCode, errorMessage)

    private def createGeoPlacemark (node: Node, query: String) = 
    {
        val address    = (node \ "address").text
        val accuracy   = (node \ "AddressDetails" \ "@Accuracy").text
        val coordinate = (node \ "Point").text.split (",").toList
        val List (longitude, latitude, _) = coordinate

        GeoPlacemark (query, address, accuracy.toInt, 
                      longitude.toDouble, latitude.toDouble)
    }

    private def doQuery () =
    {
        try {
            val url = "http://maps.google.com/maps/geo?q=%s&output=xml&gl=%s".
                      format(query, locale)

            val source = Source.fromURL (url, "utf-8")
            val iter   = for (line <- source.getLines) yield line
            val xml    = XML.loadString (iter.mkString)

            statusCode = (xml \ "Response" \ "Status" \ "code").text.toInt

            // code 200 means OK
            if (statusCode != 200) {
                throw new Exception ("Query Faild")
            }

            val placemark   = xml \ "Response" \ "Placemark"
            val addressList = for (node <- placemark) yield 
                                  createGeoPlacemark (node, query)

            addressList.toList
        } catch {
            case e => errorMessage = e.getMessage
                      Nil
        }
    }
}

這樣我們就完成了一個可以用地址來反查經緯度的函式庫了,夠簡單吧?最後,我們發現 doQuery 看起來有點複雜,其實可以把抓網頁轉成 XML 的部份再寫成另一個 loadXMLFromGoogleMaps 函式,所以來重構一下唄!

class GeoService (val query: String, val locale: String)
{
    lazy val placemarkList: List[GeoPlacemark] = doQuery ()

    private var statusCode   = 0
    private var errorMessage = ""

    def this (query: String) = this (query, "")
    def error = (statusCode, errorMessage)

    private def createGeoPlacemark (node: Node) = 
    {
        val address    = (node \ "address").text
        val accuracy   = (node \ "AddressDetails" \ "@Accuracy").text
        val coordinate = (node \ "Point").text.split (",").toList
        val List (longitude, latitude, _) = coordinate

        GeoPlacemark (query, address, accuracy.toInt, 
                      longitude.toDouble, latitude.toDouble)
    }

    private def loadXMLFromGoogleMaps = {
        val url = "http://maps.google.com/maps/geo?q=%s&output=xml&gl=%s".
                  format(query, locale)

        val source = Source.fromURL (url, "utf-8")
        val iter   = for (line <- source.getLines) yield line
        val xml    = XML.loadString (iter.mkString)

        val statusCode = (xml \ "Response" \ "Status" \ "code").text.toInt

        (xml, statusCode)
    }

    private def doQuery () =
    {
        try {
            val (xml, statusCode) = loadXMLFromGoogleMaps

            // code 200 means OK
            if (statusCode != 200) {
                this.statusCode = statusCode
                throw new Exception ("Query Faild")
            }

            val placemark   = xml \ "Response" \ "Placemark"
            val addressList = for (node <- placemark) yield 
                                  createGeoPlacemark (node)

            addressList.toList
        } catch {
            case e => errorMessage = e.getMessage
                      Nil
        }
    }
}

如何?看起來清楚多了吧?

回響