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日 蘇州&無錫

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容