可视化你的足迹

可视化你的足迹 上一篇文章讲述了如何在服务器端通过MapServer来生成地图。虽然MapServer发布出来的地图是标准的WMS服务,但是我们还需要一个客户端程序来展现。我们在上一篇中,通过一些小脚本将照片中的地理信息抽取到了一个GeoJSON文件中。GeoJSON是一种向量图层格式,向量数据可以在服务器端绘制成栅格图,也可以直接在客户端canvas上直接绘制出来。当数据量比较大的时候,我们更倾向于在服务器端绘制,这样只需要在网络上传输一张图片(而且可以做缓存)。大数据量的客户端绘制在性能上会比较差(当然现在已经有了一些新的解决方案,我们后续再细谈),特别是有用户交互时,会出现明显的卡顿。 在本文中,我将分别使用客户端和服务端绘制的两种方式来展现两种不同的地图:使用OpenLayers直接在客户端绘制矢量图,以及使用Leaflet来展示在服务器端绘制好的栅格图层。 使用OpenLayers3展示GeoJSON 展示GeoJSON非常容易,也是一种比较直接的方式,只需要将GeoJSON文件发送到前端,然后直接通过客户端渲染即可。使用OpenLayers3的API,代码会是这样: $.getJSON('data/places-ive-been-3857.json').done(function(geojson) { var vectorSource = new ol.source.Vector({ features: (new ol.format.GeoJSON()).readFeatures(geojson) }); }); 客户端发送一个ajax请求,得到GeoJSON数据之后,将其转换成一个向量类型。OpenLayers定义了很多中格式读取器,比如KML的,GML的,GeoJSON的等等。然后我们可以定义一个样式函数: var image = new ol.style.Circle({ radius: 5, fill: null, stroke: new ol.style.Stroke({color: '#f04e98', width: 1}) }); var styles = { 'Point': [new ol.style.Style({ image: image })] }; var styleFunction = function(feature, resolution) { return styles[feature.getGeometry().getType()]; }; 这个函数会应用到向量集的Point类型,将其绘制为一个红色,半径为5像素的圆圈。有了数据和样式,我们再来创建一个新的向量,然后生成一个新的图层: var vectorLayer = new ol.layer.Vector({ source: vectorSource, style: styleFunction }); 创建地图,为了方便对照,我们加入了另外一个ol.source.Stamen图层作为参照。这样当缩放到较小的区域时,我们可以清楚的知道当前的点和地物的对照,比如道路名称,建筑名称等,从而确定目前的位置。这是一种非常常见的GIS应用的场景,但是需要注意的是,不同的图层需要有相同的空间映射方式,OpenLayers默认才用EPSG:3857,所以需要两者都采用该投影: var map = new ol....

September 20, 2015 1 min

可视化你的足迹

可视化你的足迹 数据可视化可以让读者以一种轻松的方式来消费数据,人类大脑在处理图形的速度是处理文本的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....

September 18, 2015 2 min

使用Openlayers可视化GeoJSON数据

OpenLayers 使用OpenLayers可以很容易的搭建基于Web的GIS系统,OpenLayers支持不同的数据源(符合WMS协议的服务器,Google Maps API, Bing Maps,KML以及GeoJSON等等)。通过将不同的数据源的数据整合,我们可以开发出丰富而用户友好的GIS系统。 OpenLayers可以轻松的处理GeoJSON数据,并将其生成矢量层,我们可以将这个层叠加在其他数据源(比如OSM)提供的地图上,以得到一个完整的小应用。 最后的运行结果是这样的: GeoJSON 美国地理信息调查局是一个科学组织,他公开了很多地球上的灾难信息,比如对地震的统计,并提供编程接口。它公开的地震统计信息,包含全世界各地报告过的地震,以及全美所有检测到的地震,并以多种周期(小时,天,周,月等),多种格式(GeoJSON,KML,Atom等),以便应用程序的开发者只用这些数据。 实现 设置基本环境 我们将借助bower来安装所有的代码依赖。首先,我们需要bower将所有的包都安装在components目录下,这个可以通过在当前目录的.bowerrc文件中制定directory: { "directory": "components" } 然后运行bower安装jquery以及openlayers: $ bower install jquery $ bower install openlayers 通过bower安装OpenLayers之后,可以通过OpenLayers自带的build工具将所有的源码合并压缩为一个文件: $ cd components/openlayers/build $ ./build.py #将会在当前目录下生成一个OpenLayers.js的文件 $ mv OpenLayers.js ../ 然后,创建一个简单的HTML文件,引用jquery.js和OpenLayers.js,以及我们的入口脚本app.js,本文所有的代码都只是修改这个文件。 <!DOCTYPE HTML> <html> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <title>Earthquake distribution</title> <link rel="stylesheet" href="style.css" /> </head> <body> <div id="container"> <div id="map"> </div> </div> <script src="components/jquery/jquery.js" type="text/javascript"></script> <script src="components/openlayers/OpenLayers.js" type="text/javascript"></script> <script src="app.js" type="text/javascript"></script> </body> </html> 还可以运行bower init来将生成bower....

April 15, 2014 2 min

使用Mapnik搭建GIS服务器

渲染引擎Mapnik 上一篇文章中大概介绍了Mapnik,它是一个渲染引擎,一般开发中都会使用他的python的bind做开发。 Mapnik的文档写的比较详细,我们这里只是做一些必要的介绍,详细的细节可以参看Mapnik在Github上的文档。 在Mac下,安装Mapnik十分容易,使用brew即可,注意我们在此处带上--with-postgresql选项,使得Mapnik可以通过PostGIS来访问数据库: brew install mapnik --with-postgresql 安装完成之后,可以通过一个小的python脚本来测试: import mapnik map = mapnik.Map(256, 256) map.background = mapnik.Color('red') map.zoom_all() mapnik.render_to_file(map, 'red.png', 'png') 这段脚本可以在当前目录下生成一个红色的256x256的小图片。好了,有了渲染引擎,我们需要一些数据来进行渲染了。 数据源 最通用的数据格式为Shapefiles,目前有很多的免费地理信息供公共下载,我们可以从Metro的站点上下载一些小的数据文件。 $ wget http://osm-extracted-metros.s3.amazonaws.com/chengdu.osm2pgsql-shapefiles.zip $ mkdir chengdu $ cd chengdu $ unzip chengdu.osm2pgsql-shapefiles.zip 这样就得到了一组文件: $ find . -name "*.shp" ./chengdu.osm-line.shp ./chengdu.osm-point.shp ./chengdu.osm-polygon.shp 每一个shp文件都会对应几个其他类型的文件,比如投影信息,属性表等。仅仅查看shp的话,有表示所有点的文件chengdu.osm-line.shp,又表示所有线的chengdu.osm-line.shp,以及表示所有面(区域)的chengdu.osm-polygon.shp文件。 有了这些文件,我们就可以做一些测试了,比如我们首先加载所有的线条,并根据这些线条生成一个图层: import mapnik map = mapnik.Map(800, 800) map.background = mapnik.Color('#ffffff') style = mapnik.Style() rule = mapnik.Rule() point_symbolizer = mapnik.PointSymbolizer() rule.symbols.append(point_symbolizer) style.rules.append(rule) map.append_style('default', style) ds_point = mapnik....

April 12, 2014 2 min

地理信息系统GIS简介

GIS系统如何工作 去年十二月中旬从RCA项目上下来之后,就一直在一个GIS项目上做咨询。在新的项目上,日常工作的重点主要是放在前端开发上(比如AngularJS,Grunt,Jamsine之类),对于业务(与GIS相关)方面,则完全没有涉及。 虽说之前也接触过一点GIS相关的开发,比如Google Maps API,OpenLayers之类,但是仅仅停留在使用别人的API搭建个小应用的层次。 直到最近,在GIS专家芦康平的指导下,才真正开始接触GIS,很快我就发现这是另一个十分好玩的新天地。简而言之,这个新的天地里,所有的东西都有一种似曾相识的感觉,但是又非常新鲜。比如地图服务器,渲染引擎,缓存,地理信息数据库等,都可以在其他的系统中找到对应。这种感觉好比收集硬币,或者收集邮票一样,当你看到新的有着不同花纹,大小,材质,年代的硬币时,那种既在意料之中又在意料之外的感觉简直太有意思了。 GIS系统,毋庸置疑可以帮助人们更加直观的分析数据,当数据与地理信息有所关联的时候,GIS系统会变得十分友好,也可以更充分的提供信息。 鉴于GIS对我来说是一个完全崭新的领域,那么学习之前,自然有很多的问题出现: 地图的信息(建筑物,河流,街道)从何而来? 数据在服务器端以何种方式存储? 地图数据到底如何被渲染出来? 一个GIS系统的部署结构是什么样的?需要哪些组件? 业界的标准是什么,有哪些开源的项目和工具可供参考? 等等。 地图是如何被渲染的? 通常来讲,我们看到的地图是由一个底图和若干个层的叠加来达到的最终结果。其中每个层次都会保存不同类型的地理信息,比如将所有的河流信息放在一个层,将建筑物放在另外一个层。 这些信息存储在数据文件中(shapefiles)或者数据库中,通过使用专门的工具来将这些地理信息转换成图片。由于每张图片都是透明的,这样叠加起来的最后效果就是如Google Maps之类应用的结果了。当然,叠加过程一般都发生在服务器端(有些简单应用则是在客户端完成某些层次的绘制,比如我之前发过的我去过的地方,这些热力图就是在客户端通过JavaScript加上去的。)。 地图在服务器端被渲染出来之后,尺寸一般会非常大。需要有工具将这些大图切分成很多组的小图,这些小图被称之为瓦片(tile)。为了给不同缩放级别的客户端提供不同的图片,这些瓦片被精心的分成了多个组,每个组都有编号。如果地图支持18级的缩放,就会现有18个分组。当然分组好越靠后,分组中的瓦片越多。 比如当客户端请求缩放级别为10的地图时,客户端(比如OpenLayers)会根据经纬度计算好图片的边界,然后请求第10级的一些瓦片,并将这些瓦片排列在画布上。一般而言,这些瓦片都是正方形(256x256或者512x512)。 WMS服务 WMS(Web Map Service)是一个基于HTTP的简单协议,客户端发送的请求中包含请求类型,地图的层次,边界等信息,服务器根据这个信息生成图片,并返回该图片: 当然,WMS本身支持多种请求,最常见的就是GetMap,细节可以参考OGC规范及具体服务器的实现。而对于后端的服务器来说,从请求中获取这些信息之后,会首先从数据库/数据文件中得到数据,并使用渲染引擎绘制图片,并最后将图片返回客户端。 图片类型 图片分为栅格类型和矢量类型两种。栅格图片一般的原始来源是航拍,遥感等,本质上来说是照片,照片必然会有大小,如果放大到某一个范围之外,就会模糊。而矢量图是数学上的抽象,比如在某个坐标系统中,在某处有一个点A,另一处有一个点B,两点之间有一条线连接。矢量图的特点是与缩放程度无关。 栅格图的特点是真实,矢量图的特点是抽象(存储方便,占用空间更少,也更容易修改)。但是为了绘制正确,完整的地图,两种类型的图片信息都是必要的: 常用文件格式 Shapefiles是Esri公司开发出来的用于存储地理信息的文件格式。说是文件,其实是一个文件族,Shapefile包含了数种文件,其中有三种必须的(.shp,.shx,.dbf)。其他有一些可选的(.prj,.sbn/.sbx等等)。 OSM格式是由OpenStreetMap采用的文件格式,其实是一个XML。 <osm version="0.6" generator="Osmosis 0.43.1"> <bounds minlon="144.26600" minlat="-38.55200" maxlon="145.81000" maxlat="-37.36500" origin="http://www.openstreetmap.org/api/0.6"/> <node id="579259" version="3" timestamp="2008-12-17T02:28:22Z" uid="57437" user="Canley" changeset="431325" lat="-37.9309048" lon="145.1282066"/> <node id="579260" version="5" timestamp="2009-12-03T21:42:45Z" uid="1679" user="andrewpmk" changeset="3284133" lat="-37.9388304" lon="145.1266866"/> <node id="579261" version="4" timestamp="2013-02-15T20:00:37Z" uid="79475" user="AlexOnTheBus" changeset="15043978" lat="-37.9404366" lon="145.1395848"/> <node id="579262" version="18" timestamp="2013-01-31T21:37:02Z" uid="79475" user="AlexOnTheBus" changeset="14864580" lat="-37....

April 10, 2014 1 min