話說 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 } } }
如何?看起來清楚多了吧?
回響