可視化你的足跡 - 服務器端
可視化你的足跡
數據可視化可以讓讀者以一種輕松的方式來消費數據,人類大腦在處理圖形的速度是處理文本的66,000
倍,這也是人們常常說的一圖勝千言
。在本文中,我們通過將日常中很容易收集到的數據,通過一系列的處理,并最終展現在地圖上。這僅僅是GIS的一個很簡單場景,但是我們可以看到,當空間數據和地圖結合在一起時,可以在可視化上得到很好的效果,讀者可以很容易從中獲取信息。
我們在本文中會制作一個這樣的地圖,圖中灰色的線是城市中的道路,小六邊形表示照片拍攝地。顏色表示當時當地拍攝照片的密度,紅色表示密集,黃色為稀疏。可以看到,我的活動區域主要集中在左下角,那是公司所在地和我的住處,:)
要展現數據,首先需要采集數據,不過這些已經在日常生活中被不自覺的被記錄下來了:
數據來源
如果你開啟了iPhone相機中的定位功能,拍照的時候,iPhone會自動把當前的地理信息寫入到圖片的元數據中,這樣我們就可以使用這些數據來做進一步的分析了。
我在去年學習OpenLayers的時候已經玩過一些簡單的足跡可視化,另外還有一篇全球地震信息的可視化,但是僅僅是展示矢量信息,并沒有深入,而且都是一些前端的JavaScript的代碼。最近又在重新整理之前的GIS知識,重新把這個作為例子來練手。當然,這次會涉及一些地圖編輯,空間計算的內容。
我的照片一般都通過Mac自帶的Photos管理(前身iPhoto),手機里照片會定期同步上去。老版本的iPhoto用的是XML文件來存儲照片的EXIF數據,新的Photos的實現里,數據被存儲在了好幾個SQLite數據庫文件中,不過問題不大,我們只需要寫一點Ruby代碼就可以將數據轉化為標準格式,這里使用GeoJSON,GeoJSON既可以方便人類閱讀,也可以很方便的導入到PostGIS或者直接在客戶端展現。
實現步驟
我們現在要繪制照片拍攝的密度圖,大概需要這樣一些步驟:
- 抽取照片的EXIF信息(經度,緯度,創建時間等)
- 編寫腳本將抽取出來的信息轉換成通用格式(GeoJSON)
- 使用QGIS將這些點的集合導入為圖層
- 插入一些由六邊形組成的圖層(設置合適的大小)
- 計算落在各個多邊形中的點的個數,并生成新的圖層heatmap
- 使用MapServer來渲染基本地圖
數據抽取
Mac上的Photos會將照片的元數據存儲在一個SQLite3格式的數據庫中,文件名為Library.apdb
,通常位于這個位置
~/Pictures/Photos\ Library.photoslibrary/Database/apdb/Library.apdb
。這個文件可以通過SQLite3
的客戶端直接打開,不過由于可能有其他進程(Mac自己的)打開了該文件,所以會有鎖文件,你可能需要先將這個文件拷貝到另外一個位置。
然后將表RKVersion
中的部分信息導出即可,SQLite內置了很方便的導出功能,通過它提供的shell客戶端sqlite3
,將信息導出到csv文件中:
sqlite> .mode csv
sqlite> .headers on
sqlite> .output places-ive-been.csv
sqlite> select datetime(imageDate+978307200, 'unixepoch', 'localtime') as imageDate, exifLatitude, exifLongitude from RKVersion where exifLatitude and exiflongitude;
sqlite> .output stdout
注意這里的日期,蘋果的日期偏移和其他公司不同,始于2001年1月1日,所以要在imageDate
之后加上這個base
值,然后將文件以.csv
的格式導出到places-ive-been.csv
中,該文件包含3列:時間,緯度,精度。
imageDate,exifLatitude,exifLongitude
"2012-10-25 16:34:01",34.19216667,108.87316667
"2012-10-28 14:45:53",35.1795,109.9275
"2012-10-28 14:45:45",35.1795,109.9275
"2012-10-25 16:34:04",34.19216667,108.87316667
"2012-10-19 23:01:05",34.19833333,108.86733333
...
轉換為GeoJSON
方便以后的轉換起見,我們將這個文件轉換成GeoJSON
(其實很多客戶端工具可以支持CSV的導入,不過GeoJSON
更為標準一些)。
require 'csv'
require 'json'
lines = CSV.open("places-ive-been.csv").readlines
keys = lines.delete lines.first
File.open("places-ive-been.json", 'w') do |f|
data = lines.map do |row|
{
:type => "Feature",
:geometry => {
:type => "Point",
:coordinates => [row[2].to_f, row[1].to_f]
},
:properties => {
:created_at => row[0]
}
}
end
f.puts JSON.pretty_generate({
:type => "FeatureCollection",
:crs => {
:type => "name",
:properties => {
:name => "EPSG:4326"
}
},
:features => data
})
end
這段腳本可以將我們的.csv
轉換成標準的geojson
格式,注意此處的空間投影使用的是EPSG:4326
。
導入為QGIS圖層
QGIS是一個開源的GIS套件,包括桌面端的編輯器和服務器端,這里我們只是用器桌面端來進行圖層的編輯。
將我們的GeoJSON
導入之后,會看到這樣的一個可視化的效果!
我們還可以導入其他的地圖圖層,這樣可以清楚的看到點所在的區域(國家地圖圖層可以在此處下載):
好了,有了基礎數據之后,我們來作進一步的數據分析 – 即生成密度圖。首先使用QGIS的插件MMQGIS
的生成多邊形圖層功能(Create -> Create Grid Layer),為了處理速度,我們可以將地圖放大到一定范圍(我選擇西安市,我在這里活動比較密集)。
選擇六邊形hexagon
,并設置合適的大小(如果是3857
參考系,即按照公里數來設置,會比較容易一些,如果是4326,則需要自己計算)。簡而言之,需要保證每個格子都包含一些點,不至于太密,也不至于太稀疏。
計算密度
QGIS提供了很多的數據分析功能,我們在這個例子中使用(Vector -> Analysis Tools -> Points in Polygon)工具,這個工具需要兩個圖層,一個是點集圖層,一個是多邊形圖層。然后會將結果生成到一個新的圖層中,我們可以將其命名為places-ive-been-density.shp
,同時需要指定一個字段來存儲統計出來的值(density)。
這個過程可能會花費一點時間,根據需要計算的點集合多邊形的格式(也就是地圖上的區域)。
完成之后會得到一個Shapefile
(其實是一組,具體可以參看這里)。其實在這個過程中,絕大多數多邊形是不包含任何數據的,我們需要過濾掉這些多余的多邊形,這樣可以縮減繪制地圖的時間。
我們可以將這個文件導入到PostGIS中進行簡化:
shp2pgsql -I -s 4326 data/places-ive-been/places-ive-been-3857-density.shp places_density |\ PGUSER=gis PGPASSWORD=gis psql -h localhost -d playground
這里的shp2pgsql
命令是GDAL工具包提供的命令,用以將Shapefile
導入到PostGIS
中,你可以通過
$ brew install gdal --with-postgresql
來安裝。
GDAL會提供很多的工具,比如用來轉換各種數據格式,投影,查看信息等等。
導入之后,我們可以在PostGIS的客戶端查看,編輯這些數據等。比如在過濾之前,
select count(*) from places_density;
我們導入的數據中有103166
條記錄:
select count(*) from places_density where density IS NOT NULL;
而過濾之后,我們僅剩下749
條數據。
通過GDAL提供的另一個工具ogr2ogr
可以方便的執行過濾,并生成新的Shapefile
:
$ ogr2ogr -f "ESRI Shapefile" data/places-ive-been/places_heatmap.shp \ PG:"host=localhost user=gis dbname=playground pass word=gis" \ -sql "SELECT density, geom FROM places_density WHERE density IS NOT NULL;"
這條命令可以得到一個新的文件,這個就是最終的用來繪制地圖的文件了。
繪制地圖
開源世界中有很多的工具可以完成地圖的繪制,比如MapServer,GeoServer,Mapnik等等。我們在這篇文章中使用MapServer來完成地圖的繪制,MapServer的安裝和配置雖然比較容易,但是也需要花費一些時間,所以我將其放到了這個repo中,你可以直接clone下來使用。(需要你在虛擬機中安裝ansible來完成provision)。
MapServer的配置很簡單,類似于一個XML,不過是自定義的格式:
MAP
IMAGETYPE PNG
EXTENT 11859978.732852 3994742.227345 12753503.595559 4580388.268737
SIZE 8000 6000
SHAPEPATH "/data/heatmap"
IMAGECOLOR 255 255 255
PROJECTION
"init=epsg:3857" ##required
END
LAYER # States polygon layer begins here
NAME heatmap
DATA heatmap_3857
STATUS default
TYPE POLYGON
CLASS
NAME "basic"
STYLE
COLOR 255 255 178
OUTLINECOLOR 255 255 178
END
END
END
END
這些配置基本上都比較自解釋,比如設置圖片格式,圖片大小,Shapefile的路徑,圖層的名稱等,MapServer的文檔在開源軟件中來說,都算比較爛的,但是對于這些基本概念的解釋還比較詳盡,大家可以去這里參考。
這里我們定義了一個圖層,每個Map中可以定義多個圖層(我們完成的最終效果圖就是西安市的道路圖和照片拍攝密度圖兩個圖層的疊加)。
這個配置繪制出來的地圖是沒有顏色差異的,全部都是255 255 178
。不過MapServer的配置提供了很好的樣式定義,比如我們可以定義這樣的一些規則:
- 如果密度為1,則設置顏色為淡黃
- 如果密度在1-2,則設置為比淡黃紅一點的顏色
- 以此類推
LAYER
NAME heatmap
DATA heatmap_3857
STATUS default
TYPE POLYGON
#CLASSITEM density
CLASS
EXPRESSION ([density] = 1)
STYLE
COLOR 255 255 178
OUTLINECOLOR 255 255 178
END
END
CLASS
EXPRESSION ([density] > 1 AND [density] <= 2)
STYLE
COLOR 254 204 92
OUTLINECOLOR 254 204 92
END
END
CLASS
EXPRESSION ([density] > 2 AND [density] <= 3)
STYLE
COLOR 253 141 60
OUTLINECOLOR 253 141 60
END
END
CLASS
EXPRESSION ([density] > 3 AND [density] <= 10)
STYLE
COLOR 240 59 32
OUTLINECOLOR 240 59 32
END
END
CLASS
EXPRESSION ([density] > 10 AND [density] < 3438)
STYLE
COLOR 189 0 38
OUTLINECOLOR 189 0 38
END
END
END
這樣我們的地圖展現出來就會比較有層次感,而且通過顏色的加深,也能體現熱圖
本身的含義。
同樣的原理,如果將那些自己創建的多邊形替換為行政區域劃分的多邊形,則可以得到另外一種形式的熱圖
:
總結
我們通過使用一些開源工具(MapServer,QGis,PostGIS,GDAL等),構建出一個基于GIS的數據可視化框架。在這個stack上,我們可以很容易的將一些其他數據也通過可視化的方式展現出來(公用自行車站點分布,出租車分布等等)。MapServer可以發布標準的WMS服務,因此可以很好的和客戶端框架集成,從而帶來更加友好的用戶體驗。
Posted by Qiu Juntao Sep 18th, 2015
來源:原始鏈接