QGIS的能力

QGIS是一套开源的跨平台地理信息系统,支持的操作系统包括Windows、Mac、Linux和BSD,也即将支持Android。当前软件的最新版本为2.18,文档版本为2.14(2.16版正在更新,2.18遥遥无期)。

基础功能

地图展示

QGIS支持栅格(raster)和矢量(vector)两种图像,前者主要包括GeoTIFF、JPEG、PNG等文件格式,后者以点、折线和多边形三种要素(feature)的形式进行存储,既可以是文件,也可以是数据库。

栅格图即是点阵图,用于展示卫星遥感、土地利用、温度分布等直观影像。矢量图通常用于展示人为划分的功能性逻辑区域,比如用点表示城市、机场、加油站等不同级别的地点信息,用折线表示道路、河流等线型信息,用多边形表示湖泊、各类用地等区域信息。

在QGIS中导入矢量图后,可以通过鼠标操作对其直接进行修改(比如增加点或线段、拖动多边形顶点等),也可以用相同的方式根据项目需要创建自己的矢量图。

矢量图的展示效果有很大的调整空间,可以设置颜色、透明度、纹理、宽度、大小等属性,可以显示说明性文字或标签(如路名),更强大的是,矢量图中的每一个要素除了坐标信息以外,还可以具有任意多的属性值,用于记录如道路类别、建筑用途、区域面积等诸多额外信息,通过编写条件脚本,可以利用这些属性值实现如不同缩放程度下过滤显示不同级别的要素、以不同的颜色显示不同功能的区域等灵活的展示形式。

栅格与矢量两种图像在QGIS中的堆叠展示(这是南非的一个叫Swellendam的小镇)。我色调搭配得不好(比如用很丑的深红色标出路名),因为背景是花乱的栅格图,试想我们平时用百度地图导航的时候其实只显示了简约的矢量要素。

QGIS支持将编辑好的地图输出为各种格式的图片、PDF或SVG文件,可以添加网格、指北针、图例、比例尺等多种装饰信息,以制作有模有样的实用地图。

这是美国阿拉斯加州的西北一角(图片来自《QGIS User guide》)

空间分析

QGIS提供了强大的空间分析能力,除了自带的工具以外,还可以通过插件的形式进行扩展,同时集成了GRASS(Geographic Resources Analysis Support System)的400多项功能模块。

空间分析旨在挖掘地图中的有用信息,以得到特定问题的求解。比如针对房地产开发,需要计算及分析区域面积、与各大交通要道的距离、地势、居民密度等诸多影响因素,借助现成功能或自己编写脚本可以筛选出所需的最佳区域。

一个比较直观的例子是最短路径的计算,利用QGIS自带的插件「Road graph plugin」可以实现该功能。将代表道路的图层设置为计算依据,在地图上选择任意两点作为起点和终点,所得的最短路径将以红线标出,同时显示公里数和耗时。

通过条件设置,还可以根据道路类别(高速、小路等)、限速等信息进行不同使用场景下的道路规划——这让我想起了百度地图导航时「用时较少」、「距离较短」、「高速优先」之类的选项。

三维渲染

栅格图一个重要的作用是表示包含高程信息的地形图——数字高程模型DEM(Digital Elevation Model)。

QGIS加载DEM示例默认的是灰度图

QGIS有多种途径可以优化DEM的显示效果。

■ 使用自带功能

首先可以上点伪彩色(直接在图层属性里设置即可),已经比灰度图直观得多,但似乎还欠了点立体感。
使用DEM分析工具生成山体阴影(hillshade),立体感是有了,可又变回了黑白。
将伪彩色(透明度调成50%)和山体阴影堆叠,即可汲两者之长,形成漂亮的地形图。

■ 使用插件

使用QGIS自带的插件「Raster Terrain Analysis plugin」可以更便捷地实现DEM的三维渲染。

用插件生成不但操作简便,而且色彩和山体阴影集成于一个图层内。

■ 使用GRASS

GRASS作为一套始于1984年的GIS,是妥妥的前辈,为QGIS所集成,其诸多工具中也有一件可以用于DEM渲染。

使用GRASS的「r.colors.table」模块上色(图片来自《QGIS Training Manual》)
使用GRASS的「nviz」模块进行三维渲染(图片来自《QGIS Training Manual》),与山体阴影不同,这一三维模型可以任意转换查看的视角。

当然DEM除了提供直观的三维展示,同样可以用于空间分析,比如通过条件脚本突出显示一定坡度范围的区域,为房地产开发(要求地势平坦)提供参考。

数据库

与栅格图相比,矢量图在诸多方面都比较灵活,在逻辑上可以将其视作包含坐标信息和其他信息的表格。因此矢量图除了可以以文件形式存储,还可以存储在数据库中。存储地理空间数据的数据库,称为空间数据库(spatial database)。QGIS支持与Oracle Spatial、SQL Server、MySQL、PostgreSQL等多种主流数据库连接,其中PostgreSQL为QGIS的主打数据库。

PostgreSQL是一种对象-关系数据库,在传统的关系数据库中引入了面向对象技术,以存储非结构化的空间数据——引入一种叫geometry的键类型。空间数据的操作与普通数据有所不同,于是出现了PostGIS,作为PostgreSQL的一个扩展,它提供了许多便捷的空间数据处理函数。

概念与操作

空间数据类层次图(图片来自《OpenGIS® Implementation Specification for Geographic information - Simple feature access - Part 1: Common architecture》)

开放地理空间信息联盟OGC(Open Geospatial Consortium)制定了空间信息在数据库中的存储规格——SFS模型(Simple Feature for SQL Model),该模型中,三种空间要素的坐标信息有WKT(well-known text)和WKB(well-known binary)两种表示格式。其中WKT易于人类阅读,WKB是用于存储和传输的二进制。

比如在PostGIS环境下定义一个坐标为(1,1)的点:
select st_pointfromtext(’POINT(1 1)’);
这里st_pointfromtext就是PostGIS提供的函数,POINT(1 1)就是点的WKT表示。

所得结果(一长串十六进制码就是点的WKB表示):

st_pointfromtext
--------------------------------------------
0101000000000000000000F03F000000000000F03F
(1 row)

如果希望结果显示为WKT形式,则需要用到转换函数st_astext
select st_astext(st_pointfromtext(’POINT(1 1)’));

结果:

st_astext
------------
POINT(1 1)
(1 row)

在实际使用中,通常一个图层在数据库中用一张表表示,其中一个键设为geometry类型,且限定其只能赋值为「ST_Point」、「ST_LineString」或「ST_Polygon」,即一个图层中的要素要么都是点,要么都是折线,要么都是多边形。

一个插入点的示例(其中「4326」为坐标参考系的代号):

insert into people (name,house_no, street_id, phone_no, the_geom)
values (’Fault Towers’,34,3,’072 812 31 28’,’SRID=4326;POINT(33 -33)’);

一个更新折线的示例:

update streets set the_geom = ’SRID=4326;LINESTRING(20 -33, 21 -34, 24 -33)’
where streets.id=2;

一个插入多边形的示例:

insert into cities (name, the_geom)
values (’Tokyo Outer Wards’, ’SRID=4326;POLYGON((20 10, 20 20, 35 20, 20 10),(-10 -30, -5 0, -15 -15, -10 -30))’);

可见,无论何种类型的要素,都是以点为基本单位构建的,多边形要求首末两点重合。

导入与导出

PostGIS和QGIS提供了多种工具,以实现矢量图在文件与数据库之间的相互转换,使得处理GIS数据时,可以兼得QGIS图形界面之便捷与数据库ODBC之灵活。

  • shp2pgsql:将矢量图文件导入数据库的命令行工具。
  • pgsql2shp:从数据库中导出矢量图文件的命令行工具。
  • ogr2ogr:文件与数据库互转的命令行工具。
  • SPIT:将矢量图文件导入数据库的QGIS插件。
  • DB Manager:QGIS自带的数据库管理工具,支持文件与数据库的互转。

除了传统数据库的C/S模式,还有一种将整套数据库系统存储在一个独立文件中的做法——SQLite,号称世界上最小的数据库。对SQLite进行空间数据处理能力的扩展,得到SpatiaLite。QGIS支持SpatiaLite,兼得文件之便携与数据库之灵活。

GPS

GPS数据分为路点(waypoint)、路线(route)、轨迹(track)三种,路点表示离散的位置,路线表示计划要走的一系列位置,轨迹表示已走过的一系列位置。在GIS中,路点由点表示,路线和轨迹由折线表示。

使用插件「GPS Tools」可实现与GPS设备的数据交互,也可以从GPX(GPS Exchange Format)文件中导入GPS数据(GPX是一种用于记录GPS数据的轻量级XML格式)。QGIS本身也支持将点和折线的图层保存为GPX文件。

但上述操作的数据都是静态的,QGIS还提供了实时显示GPS信号的功能,在「GPS Information Panel」操作面板中,还可以看到经纬度值、GPS信号强度、附近卫星分布情况等诸多信息。

C/S模式

QGIS除了可以操作本地数据,还提供了C/S模式。QGIS客户端向公共的或自己搭建的服务器请求地图数据,QGIS服务器则将地图项目发布出去。

常见的地图数据服务有:

  • WMS(Web Map Service):收到请求后,服务器将地图渲染成图片格式(如JPEG或PNG)后发送至客户端,客户端只能拖拽和缩放,无法进行修改。如果服务器上的数据有更新,客户端显示的画面也将相应刷新。
我从WMS「http://ows.terrestris.de/osm/service」上扒下来的世界地图,每一次拖拽或缩放都会重新请求图片。
也许因为服务器远在国外,每一次刷新都非常耗时,费了好大劲才看见太湖。
  • WMTS(Web Map Tile Service):使用瓦片技术的WMS,服务器预先将地图渲染成一格格的小图片,即瓦片,针对不同请求返回不同的瓦片组合,因此响应时不再需要即时渲染,速度比WMS快。
务器会准备好各个缩放比例下的瓦片(图片来自《QGIS User guide》)
这幅来自WMTS(http://maps.wien.gv.at/basemap/1.0.0/WMTSCapabilities.xml)的奥地利正摄影像带着浓浓的瓦片感……
  • WFS(Web Feature Service):服务器将矢量图原木原样地发送给客户端,客户端可以像操作本地数据一样修改地图,但传递的数据量远大于WMS/WMTS,对带宽要求之高,几乎找不到公共的WFS服务器。
在向WFS请求数据时,可以用SQL指定一些筛选条件,以省去多余的数据传递,在提高灵活性的同时,也减轻了WFS的带宽压力。
  • WFS-T(Web Feature Service - Transactional):WFS支持从服务器上获取矢量要素,而WFS-T,即带事务的WFS还支持客户端对服务器上的要素进行增删改。

  • WCS(Web Coverage Service):WCS提供涉及时空信息的多维数据,如卫星影像、土地覆盖数据、DEM等。

QGIS服务器是需要与web服务器配合使用的FastCGI/CGI程序,上述这些服务本质上响应的都是HTTP请求。对于WMS,将请求的URL放到web浏览器里同样可以刷出地图。

服务器通过接收HTTP请求参数的形式,支持GetCapabilities、GetMap(针对WMS)、GetFeature(针对WFS)、GetCoverage(针对WCS)等多种请求指令,还通过其他附带参数,实现灵活的请求操作,例如用参数「MAP」指定服务器上QGIS项目文件的路径,用参数「DPI」指定所得图像的分辨率:

http://server/path/qgis_mapserv.fcgi?REQUEST=GetMap&MAP=/home/qgis/mymap.qgs&DPI=300&...

二次开发

基于SIP和PyQT4,QGIS支持多种形式的Python二次开发——PyQGIS,对于QGIS桌面软件或QGIS客户端:

  • QGIS启动时自动运行的脚本:编辑一个指定的.py文件,用于在QGIS启动时执行如清理变量「sys.path」等准备工作。

  • 在QGIS内嵌的控制台中执行的语句:QGIS已经为用户做好了导入PyQGIS模块等初始化工作,我们可以在控制台里直接操作画布、菜单、工具栏等。

先查看一下QGIS所用的Python版本,是2.7。再通过变量「iface」获得当前激活的图层编号及其所含要素的数量。
  • 插件开发

  • 应用程序开发:通过导入PyQGIS的模块,如「qgis.core」和「qgis.gui」,就可以在我们自己的Python程序中进行GIS数据处理。我根据《PyQGIS Developer Cookbook》中的例子拼了一个简单的小程序——在窗口中显示栅格图,支持拖拽缩放和矩形涂鸦。

from qgis.gui import *
from PyQt4.QtGui import QAction, QMainWindow
from PyQt4.QtCore import SIGNAL, Qt, QFileInfo

# 自定义矩形绘制工具
class RectangleMapTool(QgsMapToolEmitPoint):
    def __init__(self, canvas):
        self.canvas = canvas
        QgsMapToolEmitPoint.__init__(self, self.canvas)
        self.rubberBand = QgsRubberBand(self.canvas, QGis.Polygon)
        self.rubberBand.setColor(Qt.red)
        self.rubberBand.setWidth(1)
        self.reset()
    def reset(self):
        self.startPoint = self.endPoint = None
        self.isEmittingPoint = False
        self.rubberBand.reset(QGis.Polygon)
    def canvasPressEvent(self, e):
        self.startPoint = self.toMapCoordinates(e.pos())
        self.endPoint = self.startPoint
        self.isEmittingPoint = True
        self.showRect(self.startPoint, self.endPoint)
    def canvasReleaseEvent(self, e):
        self.isEmittingPoint = False
        r = self.rectangle()
        if r is not None:
            print "Rectangle:", r.xMinimum(), r.yMinimum(), r.xMaximum(), r.yMaximum()
    def canvasMoveEvent(self, e):
        if not self.isEmittingPoint:
            return
        self.endPoint = self.toMapCoordinates(e.pos())
        self.showRect(self.startPoint, self.endPoint)
    def showRect(self, startPoint, endPoint):
        self.rubberBand.reset(QGis.Polygon)
        if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y():
            return
        point1 = QgsPoint(startPoint.x(), startPoint.y())
        point2 = QgsPoint(startPoint.x(), endPoint.y())
        point3 = QgsPoint(endPoint.x(), endPoint.y())
        point4 = QgsPoint(endPoint.x(), startPoint.y())
        self.rubberBand.addPoint(point1, False)
        self.rubberBand.addPoint(point2, False)
        self.rubberBand.addPoint(point3, False)
        self.rubberBand.addPoint(point4, True) # true to update canvas
        self.rubberBand.show()
    def rectangle(self):
        if self.startPoint is None or self.endPoint is None:
            return None
        elif self.startPoint.x() == self.endPoint.x() or self.startPoint.y() == self.endPoint.y():
            return None
        return QgsRectangle(self.startPoint, self.endPoint)
    def deactivate(self):
        super(RectangleMapTool, self).deactivate()
        self.emit(SIGNAL("deactivated()"))

# 定义主窗口
class MyWnd(QMainWindow):
    def __init__(self, layer):
        QMainWindow.__init__(self)
        self.canvas = QgsMapCanvas()
        self.canvas.setCanvasColor(Qt.white)
        self.canvas.setExtent(layer.extent())
        self.canvas.setLayerSet([QgsMapCanvasLayer(layer)])
        self.setCentralWidget(self.canvas)
        actionZoomIn = QAction("Zoom in", self)
        actionZoomOut = QAction("Zoom out", self)
        actionPan = QAction("Pan", self)
        actionRectangle = QAction("Rectangle", self)
        actionZoomIn.setCheckable(True)
        actionZoomOut.setCheckable(True)
        actionPan.setCheckable(True)
        actionRectangle.setCheckable(True)
        self.connect(actionZoomIn, SIGNAL("triggered()"), self.zoomIn)
        self.connect(actionZoomOut, SIGNAL("triggered()"), self.zoomOut)
        self.connect(actionPan, SIGNAL("triggered()"), self.pan)
        self.connect(actionRectangle, SIGNAL("triggered()"), self.rectangle)
        self.toolbar = self.addToolBar("Canvas actions")
        self.toolbar.addAction(actionZoomIn)
        self.toolbar.addAction(actionZoomOut)
        self.toolbar.addAction(actionPan)
        self.toolbar.addAction(actionRectangle)
        # create the map tools
        self.toolPan = QgsMapToolPan(self.canvas)
        self.toolPan.setAction(actionPan)
        self.toolZoomIn = QgsMapToolZoom(self.canvas, False) # false = in
        self.toolZoomIn.setAction(actionZoomIn)
        self.toolZoomOut = QgsMapToolZoom(self.canvas, True) # true = out
        self.toolZoomOut.setAction(actionZoomOut)
        self.toolRectangle = RectangleMapTool(self.canvas)
        self.toolRectangle.setAction(actionRectangle)
        self.pan()
    def zoomIn(self):
        self.canvas.setMapTool(self.toolZoomIn)
    def zoomOut(self):
        self.canvas.setMapTool(self.toolZoomOut)
    def pan(self):
        self.canvas.setMapTool(self.toolPan)
    def rectangle(self):
        self.canvas.setMapTool(self.toolRectangle)

# 将美国阿拉斯加州的地图加载为图层,并显示到窗口中
fileName = "D:/qgis_sample_data/raster/landcover.img"
fileInfo = QFileInfo(fileName)
baseName = fileInfo.baseName()
rlayer = QgsRasterLayer(fileName, baseName)
if not rlayer.isValid():
    print "Layer failed to load!"
QgsMapLayerRegistry.instance().addMapLayer(rlayer)
w = MyWnd(rlayer)
w.show()
工具栏有四项工具可选, 左上角的红色矩形是涂鸦。

对于QGIS服务器,也可以用PyQGIS进行插件和独立程序的开发。通过插件开发,可以实现修改现有服务(WMS、WFS等)、根据请求进行额外处理(如身份验证)甚至修改请求参数、修改响应结果(如给图片增加水印)、新建自定义服务、设置访问权限等灵活的功能。

一种应用架构

QGIS Client和QGIS Server都可以替换为自己开发的PyQGIS程序。或者出于瘦客户端的考虑,QGIS还支持配置出简洁的用户界面。

可以删掉一切多余的窗口部件,只给用户留下必要的功能。

学习资料


2017年2月1日~6日 苏州&无锡

推荐阅读更多精彩内容