GPS数据采集与OpenStreetMap编辑

简单介绍一下通过GPS采集道路数据并上传到OpenStreetMap的流程。

采集

首先你需要一个GPS数据记录器,或者叫做GPS Logger。这类产品在淘宝上可以找到很多,台湾的长天(HOLUX)是相对价廉物美的品牌。我的设备是HOLUX m1000c,下文以此为例。

m1000c的使用非常简单。开机之后,指示GPS的黄灯点亮,设备开始搜索GPS卫星。根据你所出的位置、遮挡情况以及天气情况,搜星的时间略有不同。当GPS黄灯开始闪烁时Logger即开始记录数据,默认情况下m1000c每隔5秒记录一次数据。每次重启后,Logger都会新开一条记录。

导入

对Windows用户,将Logger通过USB线与电脑连接后,可以利用随机附带的软件将数据导入为GPX格式。对Linux用户,可以利用gpsbabel导入数据:
gpsbabel -t -i m241 -f /dev/ttyACM0 -o gpx -F myfile.gpx

其中:

  • -t 表示数据为track类型
  • -i m241 输入格式为HOLUX m241,m1000c使用的是这种格式
  • -f /dev/ttyACM0 输入设备是/dev/ttyACM0
  • -o gpx 输出格式是gpx
  • -F 输出文件myfile.gpx

在一些发行版上,需要root权限访问/dev/ttyACM0,所以不要忘记将输出的文件chown给普通用户。

导入完成后可以利用下面的命令清空Logger
gpsbabel -i m241,erase_only=1 -f /dev/ttyACM0

在Windows和Linux上都可以使用Viking查看导入的数据:

  • 启动viking
  • 添加一个OSM地图图层: Layers->New Map Layer
  • 导入GPX数据: File->Append File…
  • 下载OSM地图,右键点击左侧的Map图层,Download Missing Onscreen Maps

Screenshot at 2011-10-05 09:45:20

编辑

Windows和Linux用户都可以通过Merkaartor编辑OSM数据。

启动Merkaartor,导入GPS数据: File->Import。
选择左侧的GPS图层,对需要导入的数据,右键点击Extract Drawing Layer将GPS转换为可编辑数据。
Screenshot at 2011-10-05 10:57:17

根据实际情况编辑道路数据的属性。对GPS转换的数据,选择菜单Feature->Force Upload,将其加入dirty set准备上传。点击Upload即可将数据上传到OpenStreetMap。

Using Google closure library with ClojureScript

Google closure library is shipped with ClojureScript, and could be compiled with ClojureScript into a minimized javascript file. So closure library is doubtlessly the first candidate when you are considering to use an external Javascript library in your cljs browser application.

However, different from clojure’s interoperability with Java, ClojureScript has its own characteristics when you are interoperating with JavaScript and JavaScript based libraries.

Clojure types are not fully compatible with JavaScript types
In ClojureScript, you can never treat a Clojure map as a JavaScript object although they have similar characteristics. You have to do some conversion before passing a clojure map to javascript functions. Matthew Gilliard made a sample of such conversion.

JavaScript package is not Clojure namespace
This could be a common mistake for ClojureScript newbie. Actually, JavaScript doesn’t have concept of “Package” or “Namespace”. Many JavaScript libraries(dojo, Google Closure) made enhancement on this. ClojureScript also takes advantage of this mechanism. So before you start to coding with closure, you may browse closure library API document, and find a module called goog.net which includes lots of types. Then you write this:

(ns myjs
  (:require [goog.net :as gnet]))

But compiler shows you “ERROR: JSC_MISSING_PROVIDE_ERROR. required “goog.net” namespace never
provided at … “. This is not a PATH issue. The root cause is that closure module has a lower granularity than Clojure ones. Types are often contained in their own modules. You can find closure source code in clojurescript/closure/library/closure. Modules are declare with goog.provide function. Thus, you should require this name instead of the logical module name.

(ns myjs
  (:require [goog.net.XhrIo :as gxhr]))

In addition, ClojureScript does not support ‘use’.

Just use full name for JavaScript class
For functions contains in some module, you can refer it with the clojure way:

(ns myjs
  (:require [goog.dom :as dom]))
(dom/$ "element-id")

But for classes, just use the full name and ignore the module alias.

(ns myjs
  (:require [goog.net.XhrIo :as gxhr]))
(def xhr (goog.net.XhrIo.))

These are basic tips before you start using Closure with ClojureScript. Leveraging on Google’s closure library, you can create cross-browser JavaScript application with Clojure easily.

ClojureScript Recipes

一周前左右有人说javascript是assembly language for the web, 结果一周不到clojurescript发布了。闹了半天clojure 1.3迟迟不发布是因为effort都迫不及待地转移到向javascript迁移上去了。简单地说cljs是clojure在javascript上的实现,通过cljsc可以把clojure编译成js,运行在浏览器里或是Node环境里。

目前clojurescript还没有正式的发布版本,需要从github签出开发版本。

  1. cljs是在Oracle JDK上开发的,引用了sun.org.mozilla.javascript.internal.Context,这个类在OpenJDK里叫做sun.org.mozilla.javascript.Context。所以没有办法,暂时只能在Oracle JDK上用clojurescript。
  2. cljs自带的所有脚本,启动jvm时,heap size都是开2G的。可怜我所有的内存才2.5G,还是分布在两台电脑上(#@&*……@¥@!)。不过我手动把它改为512M后cljsc还是依然可以正常运行的。
  3. cljs与javascript的互操作是最麻烦的部分,一般情况cljs通过(js*)这个form来访问javascript数据和对象,比如访问document:(js* “document”)。进而访问getElementById时,即(.getElementById (js* “document”) “some-id”)。
  4. cljs访问js对象时,通过(aget)而不是(.属性名),例如(aget rage “ups”)编译后为rage.ups,如果(.ups rage)就会被当作函数调用。不过奇怪的是如果(.title rage)依然会被编译成rage.title。这种不一致的情况在对.url也存在。当然用aget是可以获得一致的结果的(aget在clojure里是根据索引取java数组的form,在cljs里可以支持js object了)。
  5. 当访问无参数的js对象时,cljs与clojure有不同。例如在clojure里(.toString date),而在cljs里,这样写编译的结果是date.toString,注意没有括号,直接访问这个function对象了。所以在cljs里,正确的写法有些变化(. date (toString))。
  6. 创建js对象,可以通过(js-obj)这个form
  7. 修改dom属性,需要用(set!)这个form,如 (set! (.src img) “http://xxx”)。如果把cljs用在网页里,类似这样的操作比比皆是,这和clojure大不相同。
  8. cljs里没有STM,暂时也不支持ref。可以直接用def定义变量,随意地访问和修改,js环境是单线程环境。
  9. 对于要对外访问的方法,在声明时加上^:export可以让编译器不修改方法的名字。

用cljs开发有趣归有趣,调试还是很困难的,建议开发的时候就写好打log的代码(js* “console.log”),因为目前cljsc编译的速度也不快,反复地修改代价还是挺高的。当然,为了cool,以上都不是问题。

使用defrecord与defprotocol的注意事项

简单地说,protocol是clojure中的接口,record是clojure中的数据类型。

可以通过这样的code定义一个protocol

(defprotoco DummyProtocol
  "doc string..."
  (method-one [self x] "doc string..."))

需要注意的是,protocol里所有方法的第一个参数都是self/this参数(类似python),从第二个开始才是调用时传入的参数。如果方法要重载呢?

(defprotocol DummyProtocol
  "doc string..."
  (method-one [self x] [self x y] "doc string")
)

Apress的 Practical Clojure 书里的例子,给重载的参数表加上了括号,这样会导致编译错误(注记)。

定义一个record实现protocol

(defrecord DummyRecord [a b c]
  DummyProtocol
  (method-one [self x] (+ a x))
  (method-one [self x y] (+ a x y)))

Practical Clojure里关于这部分的代码,又丢掉了self参数(注记)。

最后还有一个问题,如果直接use你的ns,你会发现调用record时出现:
java.lang.IllegalArgumentException: Unable to resolve classname: DummyRecord

怎么回事,不是都use了吗?原因是record被编译成了java对象,所以引用时要用java对象的引用方式,import之。

Staged Event-Driven Architecture

按照传统的编写应用程序的思路,当server接到请求,包装完成之后,分配到线程池中交给一个线程完成,返回。Java的servlet容器就是这么设计的,这么多年大部分的应用程序也是在这种模式下编写的。当任务的代价比较大,比如多次和数据库交互,并且可能和后端其他服务通信,这是线程被任务占用的时间就相对较长。如果请求的并发量很大,容器的线程池耗尽,新的请求就无法被处理,导致并发性无法提高,吞吐量也无法提高。

实际上在大部分应用程序里,所谓代价很高的任务,往往都是I/O Bound。涉及网络通信,磁盘读写,线程被迫wait,CPU却是空闲的。所以,I/O Bound的程序理论上都还有优化的空间。于是有了这种Staged Event-Driven Architecture,即SEDA。

SEDA的思路是将原先由一个线程完成的任务,分割为相对独立的多个阶段。每个阶段由专用的一组线程负责执行,阶段之间用过队列交互。又是上次提到的老话:If you cannot split it, you cannot scale it.

例如,我们的业务逻辑是读取请求,操作数据库,与后端通信,操作数据库,写回结果。如果采用NIO的方式,网络通信可以认为是非阻塞的。而目前与数据库的交互,通常还是阻塞式的风格。这样这五个阶段分别是非阻塞、阻塞、非阻塞、阻塞、非阻塞。组塞操作容易成为性能瓶颈,CPU时间用于等待,占用率不高,所以对组塞操作可以分配相对多的线程;非阻塞操作速度较快,为了避免快速的切换导致sy升高,采用和CPU核数一样多的线程即可。

这样,原本一个线程反复等待的阻塞操作,变成了部分线程等待的同时,其他线程仍然在处理自己的业务,有效使用CPU。利用的多核的优势,提高了CPU的占用,即提高了系统的处理能力。

而对外部接口来说,当读取数据的线程在读取完毕之后,将任务dispatch到相应队列即返回,前端可以保证很高的处理速度,并发性也可以保证。任务被积压在阻塞操作的队列中,而消费阻塞操作的线程要多于提供者,在一定程度上也保证了处理速度。再退一步说,当阻塞操作的速度却是无法消费大量任务时,前端可以根据队列的大小判断当前系统的load,拒绝服务。

当然,采用这种方式,只有在并发量提高到一定程度,并发成为系统瓶颈时才能体现价值。就单个操作而言,由于队列的传递,他的latency一定是有所上升的。

关于SEDA,可以参考Matt Welsh的相关论文:The Staged Event-Driven Architecture for Highly-Concurrent Server Applications。