PostGIS-秘籍-全-
PostGIS 秘籍(全)
原文:
zh.annas-archive.org/md5/ccf8953196952c610033daa65e325f22译者:飞龙
前言
我孩子的学校附近最近的医院有多远?过去三个月里我城市里的财产犯罪发生在哪里?从我家到办公室的最短路线是什么?我应该为公司的送货卡车指定什么路线以最大化设备利用率并最小化燃料消耗?应该在何处建造下一个消防站以最小化响应时间?
人们每天都在这个星球上提出这些问题,以及其他类似的问题。回答这些问题需要一个能够在两个或更多维度中思考的机制。从历史上看,桌面 GIS 应用是唯一能够回答这些问题的应用。这种方法——尽管完全功能——但对于普通人来说并不可行;大多数人不需要这些应用所能提供的一切功能,或者他们不知道如何使用它们。此外,越来越多的基于位置的服务提供了人们使用的特定功能,并且即使是从他们的智能手机上也可以访问。显然,这些服务的普及化需要强大的后端平台来处理大量的地理操作。
由于需要或希望具有可扩展性、对大数据集的支持以及直接输入机制,大多数开发人员已经选择采用空间数据库作为他们的支持平台。目前有几种空间数据库软件可供选择,有些是专有的,有些是开源的。PostGIS 是一种开源空间数据库软件,可能是所有空间数据库软件中最易于访问的。
PostGIS 作为扩展运行,为 PostgreSQL 数据库提供空间功能。在这个能力下,PostGIS 允许将空间数据与数据库中通常发现的数据一起包含。通过将所有数据放在一起,可以提出像“在考虑每个响应时间的距离后,所有警察局的排名是什么?”这样的问题。通过构建在 PostGIS 提供的核心功能和 PostgreSQL 固有的可扩展性之上,可以实现新的或增强的功能。此外,这本书还邀请在新的 GIS 应用和基于位置的服务中包含位置隐私保护机制,以便用户感到受到尊重,而不是在分享他们的信息时必然处于风险之中,尤其是像他们的行踪这样敏感的信息。
《PostGIS 食谱,第二版》采用问题解决方法,帮助你获得对 PostGIS 的扎实理解。希望这本书能回答一些常见空间问题,并给你提供灵感和信心,在寻找解决具有挑战性的空间问题的解决方案时使用和增强 PostGIS。
本书面向对象
这本书是为那些寻找使用 PostGIS 解决空间问题最佳方法的人编写的。这些问题可能像找到特定位置最近的餐厅那样简单,也可能像找到从点 A 到点 B 的最短和/或最有效路线那样复杂。
对于刚开始使用 PostGIS 或甚至空间数据集的读者,本书的结构旨在帮助他们熟悉并精通在数据库中运行空间操作。对于经验丰富的用户,本书提供了深入了解高级主题的机会,如点云、栅格地图代数和 PostGIS 编程。
本书涵盖的内容
第一章,在 PostGIS 中移动数据进和出,涵盖了可用于将空间和非空间数据导入和导出到 PostGIS 的过程。这些过程包括使用 PostGIS 和第三方提供的实用程序,如 GDAL/OGR。
第二章,有效结构,讨论了如何使用通过 PostgreSQL 提供的机制来组织 PostGIS 数据。这些机制用于规范化可能不干净和无结构的导入数据。
第三章,处理矢量数据的基础,介绍了在 PostGIS 中对矢量数据(在 PostGIS 中称为几何和地理)通常执行的操作。包括处理无效几何、确定几何之间的关系以及简化复杂几何的操作。
第四章,处理矢量数据的高级食谱,深入分析几何的高级主题。您将学习如何利用 KNN 过滤器提高邻近查询的性能、从 LiDAR 数据创建多边形以及计算可用于邻域分析的多边形。
第五章,处理栅格数据,展示了在 PostGIS 中操作栅格的实用工作流程。您将学习如何导入栅格、修改栅格、对栅格进行分析以及以标准栅格格式导出栅格。
第六章,使用 pgRouting,介绍了 pgRouting 扩展,它将图遍历和分析功能引入 PostGIS。本章中的食谱回答了从点 A 条件导航到点 B 以及准确模拟复杂路线(如水道)等现实世界问题。
第七章,进入第 N 维,专注于在 PostGIS 中处理和分析多维空间数据(包括来自 LiDAR 的点云)所使用的工具和技术。包括将点云加载到 PostGIS 中、从点云创建 2.5D 和 3D 几何以及应用几个摄影测量原理。
第八章, 《PostGIS 编程》展示了如何使用 Python 语言编写操作和与 PostGIS 交互的应用程序。这些应用程序包括将外部数据集读写到 PostGIS 的方法,以及使用 OpenStreetMap 数据集的基本地理编码引擎。
第九章,《PostGIS 与 Web》介绍了使用 OGC 和 REST 网络服务将 PostGIS 数据和功能提供给 Web 的方法。本章讨论了使用 MapServer 和 GeoServer 提供 OGC、WFS 和 WMS 服务,以及从 OpenLayers 和 Leaflet 等客户端消费这些服务。然后展示了如何使用 GeoDjango 构建 Web 应用程序,以及如何将你的 PostGIS 数据包含在 Mapbox 应用程序中。
第十章, 《维护、优化和性能调整》从 PostGIS 退后一步,专注于 PostgreSQL 数据库服务器的功能。通过利用 PostgreSQL 提供的工具,你可以确保你的空间和非空间数据长期有效,并最大化各种 PostGIS 操作的性能。此外,它还探讨了新的特性,如 PostgreSQL 中的地理空间分片和并行性。
第十一章, 《使用桌面客户端》介绍了如何使用各种开源桌面 GIS 应用程序来消费和操作 PostGIS 中的空间数据。讨论了几个应用程序,以突出不同的空间数据交互方法,并帮助你找到适合任务的正确工具。
第十二章, 《位置隐私保护机制简介》对位置隐私的概念进行了初步介绍,并展示了两种不同的位置隐私保护机制的实现,这些机制可以包含在商业应用程序中,为用户的位置数据提供基本保护。
为了充分利用本书
在进一步学习本书之前,你需要安装最新版本的 PostgreSQL 和 PostGIS(分别为 9.6 或 10.3 和 2.3 或 2.41)。如果你更喜欢图形 SQL 工具,也可以安装 pgAdmin(1.18)。对于大多数计算环境(Windows、Linux、macOS X),安装程序和软件包包括 PostGIS 的所有必需依赖项。PostGIS 的最小必需依赖项包括 PROJ.4、GEOS、libjson 和 GDAL。
为了理解并适应本书中的代码,需要具备基本的 SQL 语言知识。
下载示例代码文件
你可以从www.packtpub.com的账户下载本书的示例代码文件。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
您可以通过以下步骤下载代码文件:
-
在 www.packtpub.com 登录或注册。
-
选择“支持”标签页。
-
点击“代码下载与勘误”。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
文件下载后,请确保使用最新版本的以下软件解压缩或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/PostGIS-Cookbook-Second-Edition。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包可供选择,这些书籍和视频可在 github.com/PacktPublishing/ 上找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/PostGISCookbookSecondEdition_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“我们将导入存储从各种 RSS 源收集的一系列网络新闻的 firenews.csv 文件。”
代码块如下设置:
SELECT ROUND(SUM(chp02.proportional_sum(ST_Transform(a.geom,3734), b.geom, b.pop))) AS population
FROM nc_walkzone AS a, census_viewpolygon as b
WHERE ST_Intersects(ST_Transform(a.geom, 3734), b.geom)
GROUP BY a.id;
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
SELECT ROUND(SUM(chp02.proportional_sum(ST_Transform(a.geom,3734), b.geom, b.pop))) AS population
FROM nc_walkzone AS a, census_viewpolygon as b
WHERE ST_Intersects(ST_Transform(a.geom, 3734), b.geom)
GROUP BY a.id;
任何命令行输入或输出都按以下方式编写:
> raster2pgsql -s 4322 -t 100x100 -F -I -C -Y C:\postgis_cookbook\data\chap5\PRISM\us_tmin_2012.*.asc chap5.prism | psql -d postgis_cookbook
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“点击“下一步”按钮将您带到下一个屏幕。”
警告或重要注意事项如下所示。
小贴士和技巧如下所示。
部分
在本书中,您将找到一些频繁出现的标题(准备工作、如何操作…、如何工作…、更多内容… 和 相关内容)。
为了清楚地说明如何完成食谱,请按照以下方式使用这些部分:
准备工作
本节告诉您在食谱中可以期待什么,并描述了为食谱设置任何软件或任何必需的初步设置的方法。
如何操作…
本节包含遵循食谱所需的步骤。
如何工作…
本节通常包含对上一节发生情况的详细解释。
更多内容…
本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。
相关内容
本节提供了对食谱其他有用信息的链接。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 请通过feedback@packtpub.com发送电子邮件,并在邮件主题中提及书籍标题。如果你对这本书的任何方面有疑问,请通过questions@packtpub.com给我们发送电子邮件。
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们将不胜感激,如果你能向我们报告这一点。请访问www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并附上材料的链接。
如果你有兴趣成为作者: 如果你有一个你擅长的主题,并且你对撰写或为书籍做出贡献感兴趣,请访问authors.packtpub.com。
评论
请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?潜在的读者可以看到并使用你的无偏见意见来做出购买决定,我们 Packt 可以了解你对我们的产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问packtpub.com。
第一章:在 PostGIS 中移动数据
在本章中,我们将涵盖:
-
使用 PostGIS 函数导入非空间表格数据(CSV)
-
使用 GDAL 导入非空间表格数据(CSV)
-
使用 shp2pgsql 导入 shapefile
-
使用 ogr2ogr GDAL 命令导入和导出数据
-
处理数据集的批量导入和导出
-
使用 pgsql2shp PostGIS 命令将数据导出为 shapefile
-
使用 osm2pgsql 命令导入 OpenStreetMap 数据
-
使用 raster2pgsql PostGIS 命令导入栅格数据
-
一次性导入多个栅格
-
使用 gdal_translate 和 gdalwarp GDAL 命令导出栅格
简介
PostGIS 是 PostgreSQL 数据库的一个开源扩展,它允许支持地理对象;在这本书中,您将找到一系列配方,它们将逐步引导您探索它提供的不同功能。
本书的目的在于成为理解 PostGIS 功能及其应用的一个有用工具,让您能够迅速掌握其应用。每个配方都包含一个准备阶段,以便组织好您的工作空间,包括您可能需要的所有东西,然后是一系列步骤,您需要执行这些步骤以达到任务的主要目标,这包括所有必要的外部命令和 SQL 语句(这些语句已在 Linux、Mac 和 Windows 环境中测试过),最后是对配方的简要总结。本书将涵盖地理信息系统和基于位置服务中的大量常见任务,这使得它成为您技术图书馆中必备的书籍。
在第一章中,我们将向您展示一系列配方,涵盖不同的工具和方法,用于从 PostGIS 空间数据库导入和导出地理数据,因为几乎在 GIS 中执行的所有常见操作都是从插入或导出地理数据开始的。
使用 PostGIS 函数导入非空间表格数据(CSV)
导入存储属性和几何形状的 逗号分隔值(CSV)文件有几种替代方法。在本配方中,我们将使用通过 PostgreSQL COPY 命令和几个 PostGIS 函数导入此类文件的方法。
准备工作
在欧洲森林火灾信息系统(EFFIS)的背景下,我们将导入存储一系列从各种与欧洲森林火灾相关的 RSS 源收集的网页新闻的 firenews.csv 文件,EFFIS 可在 effis.jrc.ec.europa.eu/ 找到。
对于每个新闻源,都有一些属性,例如 地名、火灾的 面积(以公顷为单位)、URL 等。最重要的是,有 x 和 y 字段,它们给出了地理定位新闻的位置,以十进制度数表示(在 WGS 84 空间参考系统,SRID = 4326)。
对于 Windows 机器,安装 OSGeo4W 是必要的,这是一个开源地理库集合,它将允许操作数据集。链接是:trac.osgeo.org/osgeo4w/
此外,将 OSGeo4W 和 Postgres 二进制文件夹包含在 Path 环境变量中,以便能够从 PC 的任何位置执行命令。
如何操作...
完成这个菜谱所需的步骤如下所示:
- 检查 CSV 文件
firenews.csv的结构,你可以在书籍数据集中找到它(如果你在 Windows 上,可以使用记事本等编辑器打开 CSV 文件)。
$ cd ~/postgis_cookbook/data/chp01/ $ head -n 5 firenews.csv
前一个命令的输出如下所示:

- 连接到 PostgreSQL,创建
chp01 SCHEMA,并创建以下表:
$ psql -U me -d postgis_cookbook
postgis_cookbook=> CREATE EXTENSION postgis;
postgis_cookbook=> CREATE SCHEMA chp01;
postgis_cookbook=> CREATE TABLE chp01.firenews
(
x float8,
y float8,
place varchar(100),
size float8,
update date,
startdate date,
enddate date,
title varchar(255),
url varchar(255),
the_geom geometry(POINT, 4326)
);
我们使用psql客户端连接到 PostgreSQL,但你也可以使用你喜欢的任何客户端,例如pgAdmin.
使用psql客户端,我们不会显示主机和端口选项,因为我们假设你正在使用标准端口上的本地 PostgreSQL 安装。
如果不是这种情况,请提供那些选项!
- 使用
COPY命令将 CSV 文件中的记录复制到 PostgreSQL 表(如果你在 Windows 上,使用输入目录如c:\temp而不是/tmp)如下所示:
postgis_cookbook=> COPY chp01.firenews (
x, y, place, size, update, startdate,
enddate, title, url
) FROM '/tmp/firenews.csv' WITH CSV HEADER;
确保文件firenews.csv位于 PostgreSQL 进程用户可访问的位置。例如,在 Linux 中,将文件复制到/tmp目录。
如果你在 Windows 系统上,在复制之前你很可能需要将编码设置为 UTF-8:postgis_cookbook=# set client_encoding to 'UTF-8'; 并且记得设置完整路径,'c:\\tmp\firenews.csv'.
- 检查所有记录是否已从 CSV 文件导入到 PostgreSQL 表中:
postgis_cookbook=> SELECT COUNT(*) FROM chp01.firenews;
前一个命令的输出如下:

- 检查与这个新表相关的记录是否在 PostGIS 的
geometry_columns元数据视图中:
postgis_cookbook=# SELECT f_table_name,
f_geometry_column, coord_dimension, srid, type
FROM geometry_columns where f_table_name = 'firenews';
前一个命令的输出如下:

在 PostGIS 2.0 之前,你必须分两步创建包含空间数据的表;实际上,geometry_columns视图是一个需要手动更新的表。为此,你必须使用AddGeometryColumn函数来创建列。例如,这是针对这个菜谱的:
postgis_cookbook=> CREATE TABLE chp01.firenews(
x float8,
y float8,
place varchar(100),
size float8,
update date,
startdate date,
enddate date,
title varchar(255),
url varchar(255))
WITHOUT OIDS;postgis_cookbook=> SELECT AddGeometryColumn('chp01', 'firenews', 'the_geom', 4326, 'POINT', 2);
chp01.firenews.the_geom SRID:4326 TYPE:POINT DIMS:2
在 PostGIS 2.0 中,如果你愿意,仍然可以使用AddGeometryColumn函数;然而,你需要将其use_typmod参数设置为false.
- 现在,使用
ST_MakePoint或ST_PointFromText函数将点导入几何列(使用以下两个更新命令之一):
postgis_cookbook=> UPDATE chp01.firenews
SET the_geom = ST_SetSRID(ST_MakePoint(x,y), 4326);
postgis_cookbook=> UPDATE chp01.firenews
SET the_geom = ST_PointFromText('POINT(' || x || ' ' || y || ')',
4326);
- 检查表中某些记录的几何字段是如何更新的:
postgis_cookbook=# SELECT place, ST_AsText(the_geom) AS wkt_geom
FROM chp01.firenews ORDER BY place LIMIT 5;
上述注释的输出如下:

- 最后,为表的几何列创建空间索引:
postgis_cookbook=> CREATE INDEX idx_firenews_geom
ON chp01.firenews USING GIST (the_geom);
它是如何工作的...
这个食谱向你展示了如何使用COPY PostgreSQL 命令在 PostGIS 中加载非空间表格数据(CSV 格式)。
在创建表并将 CSV 文件行复制到 PostgreSQL 表之后,你使用 PostGIS 提供的几何构造函数之一(ST_MakePoint和ST_PointFromText用于二维点)更新了几何列。
这些几何构造函数(在这种情况下,ST_MakePoint和ST_PointFromText)必须始终提供空间参考系标识符(SRID)以及点坐标来定义点几何形状。
数据库中任何表中添加的几何字段都会在geometry_columns PostGIS 元数据视图中跟踪一个记录。在先前的 PostGIS 版本(< 2.0)中,geometry_fields视图是一个表,需要手动更新,可能使用方便的AddGeometryColumn函数。
由于同样的原因,在先前版本的 PostGIS 中删除空间列或移除空间表时,为了保持更新的geometry_columns视图,存在DropGeometryColumn和DropGeometryTable函数。从 PostGIS 2.0 及更高版本开始,你不再需要使用这些函数,但你可以安全地使用标准的ALTER TABLE、DROP COLUMN和DROP TABLE SQL 命令来删除列或表。
在食谱的最后一步,你在表上创建了一个空间索引以改善性能。请注意,与字母数字数据库字段的情况一样,索引只有在使用SELECT命令读取数据时才会提高性能。在这种情况下,你正在对表进行多次更新(INSERT、UPDATE和DELETE);根据场景,在更新后删除并重新创建索引可能更节省时间。
使用 GDAL 导入非空间表格数据(CSV)
作为先前食谱的替代方法,你将使用ogr2ogr GDAL 命令和GDAL OGR 虚拟格式将 CSV 文件导入 PostGIS。地理空间数据抽象库(GDAL)是一个用于栅格地理空间数据格式的翻译库。OGR 是与 GDAL 相关的库,它为矢量数据格式提供类似的功能。
这次,作为额外步骤,你将只导入文件中的一部分特征,并将它们重新投影到不同的空间参考系中。
准备工作
你将从 NASA 的地球观测系统数据和信息系统(EOSDIS)导入Global_24h.csv文件到 PostGIS 数据库。
您可以复制本书此章节的数据集目录中的文件。
此文件表示在过去 24 小时内由中分辨率成像光谱仪(MODIS)卫星检测到的全球活跃热点。对于每一行,都有热点的坐标(纬度,经度)以十进制度数表示(在 WGS 84 空间参考系统中,SRID = 4326),以及一系列有用的字段,例如acquisition date(获取日期)、acquisition time(获取时间)和satellite type(卫星类型),仅举几例。
你将只导入由标记为T(Terra MODIS)类型的卫星扫描的活跃火灾数据,并将其使用球面墨卡托投影坐标系(EPSG:3857;有时标记为EPSG:900913,其中数字 900913 代表 1337 语言中的 Google,因为它最初被 Google Maps 广泛使用)进行投影。
如何做到这一点...
完成此食谱需要遵循的步骤如下:
- 分析
Global_24h.csv文件的结构(在 Windows 中,使用记事本等编辑器打开 CSV 文件):
$ cd ~/postgis_cookbook/data/chp01/ $ head -n 5 Global_24h.csv
前一个命令的输出如下:

- 创建一个由
Global_24h.csv文件派生的一个层组成的 GDAL 虚拟数据源。为此,在 CSV 文件所在的目录中创建一个名为global_24h.vrt的文本文件,并按以下方式编辑它:
<OGRVRTDataSource>
<OGRVRTLayer name="Global_24h">
<SrcDataSource>Global_24h.csv</SrcDataSource>
<GeometryType>wkbPoint</GeometryType>
<LayerSRS>EPSG:4326</LayerSRS>
<GeometryField encoding="PointFromColumns"
x="longitude" y="latitude"/>
</OGRVRTLayer>
</OGRVRTDataSource>
- 使用
ogrinfo命令检查虚拟层是否被 GDAL 正确识别。例如,分析层的模式及其第一个特征(fid=1):
$ ogrinfo global_24h.vrt Global_24h -fid 1
前一个命令的输出如下:

你也可以尝试使用支持 GDAL/OGR 虚拟驱动程序的桌面 GIS 软件打开虚拟层,例如Quantum GIS(QGIS)。在下面的屏幕截图中,Global_24h层与书中数据集目录中可以找到的国家 shapefile 一起显示:

全球 24 小时数据集覆盖国家层和所选特征的信息
- 现在,使用
ogr2ogrGDAL/OGR 命令将虚拟层作为新的表导出到 PostGIS(为了使此命令可用,您需要将 GDAL 安装文件夹添加到您的操作系统的PATH变量中)。您需要使用-f选项指定输出格式,使用-t_srs选项将点投影到EPSG:3857空间参考,使用-where选项仅加载 MODIS Terra 卫星类型的记录,并使用-lco层创建选项提供您想要存储表的模式:
$ ogr2ogr -f PostgreSQL -t_srs EPSG:3857
PG:"dbname='postgis_cookbook' user='me' password='mypassword'"
-lco SCHEMA=chp01 global_24h.vrt -where "satellite='T'"
-lco GEOMETRY_NAME=the_geom
- 检查
ogr2ogr命令如何创建表,如下面的命令所示:
$ pg_dump -t chp01.global_24h --schema-only -U me postgis_cookbook
CREATE TABLE global_24h (
ogc_fid integer NOT NULL,
latitude character varying,
longitude character varying,
brightness character varying,
scan character varying,
track character varying,
acq_date character varying,
acq_time character varying,
satellite character varying,
confidence character varying,
version character varying,
bright_t31 character varying,
frp character varying,
the_geom public.geometry(Point,3857)
);
- 现在,检查
geometry_columns元数据视图中应该出现的记录:
postgis_cookbook=# SELECT f_geometry_column, coord_dimension,
srid, type FROM geometry_columns
WHERE f_table_name = 'global_24h';
前一个命令的输出如下:

- 检查表中已导入的记录数量:
postgis_cookbook=# SELECT count(*) FROM chp01.global_24h;
前述命令的输出如下:

- 注意坐标是如何从
EPSG:4326投影到EPSG:3857的:
postgis_cookbook=# SELECT ST_AsEWKT(the_geom)
FROM chp01.global_24h LIMIT 1;
前述命令的输出如下:

它是如何工作的...
如 GDAL 文档中所述:
"OGR 虚拟格式是一个驱动程序,它根据 XML 控制文件中指定的标准将其他驱动程序读取的特征进行转换。"
GDAL 支持以 CSV 文件形式存储的非空间表格数据的读取和写入,但我们需要使用虚拟格式从 CSV 文件中的属性列(每个点的经纬度坐标)推导出层的几何形状。为此,您至少需要在驱动程序中指定 CSV 文件的路径(SrcDataSource 元素)、几何类型(GeometryType 元素)、层的空间参考定义(LayerSRS 元素)以及驱动程序推导几何信息的方式(GeometryField 元素)。
使用 OGR 虚拟格式有许多其他选项和原因;如果您想更好地理解,请参阅位于 www.gdal.org/drv_vrt.html 的 GDAL 文档。
在虚拟格式正确创建之后,原始的平面非空间数据集由 GDAL 和基于 GDAL 的软件在空间上支持。这就是为什么我们可以使用 GDAL 命令(如 ogrinfo 和 ogr2ogr)以及桌面 GIS 软件(如 QGIS)来操作这些文件的原因。
一旦我们验证了 GDAL 可以正确地从虚拟驱动程序读取要素,我们就可以使用流行的 ogr2ogr 命令行工具轻松地将它们导入 PostGIS。ogr2ogr 命令有众多选项,因此请参阅其位于 www.gdal.org/ogr2ogr.html 的文档以获取更深入的讨论。
在本食谱中,您已经看到了一些这些选项,例如:
-
-where:它用于仅导出原始要素类的一部分 -
-t_srs:它用于将数据重新投影到不同的空间参考系统 -
-lco layer creation:它用于提供我们想要存储表的架构(如果没有它,新的空间表将在public架构中创建)以及输出层中几何字段的名称
使用 shp2pgsql 导入 shapefile
如果您需要将 shapefile 导入 PostGIS,您至少有两个选项,例如之前看到的 ogr2ogr GDAL 命令,或者 shp2pgsql PostGIS 命令。
在本食谱中,您将使用 shp2pgsql 命令在数据库中加载 shapefile,使用 ogrinfo 命令分析它,并在 QGIS 桌面软件中显示它。
如何操作...
完成此食谱所需遵循的步骤如下:
- 使用
ogr2ogr命令(注意,在这种情况下,您不需要指定-f选项,因为 shapefile 是ogr2ogr命令的默认输出格式)从之前菜谱中创建的虚拟驱动程序创建 shapefile:
$ ogr2ogr global_24h.shp global_24h.vrt
- 使用
shp2pgsql命令生成 shapefile 的 SQL 导出文件。您将使用-G选项使用 geography 类型生成 PostGIS 空间表,并使用-I选项在几何列上生成空间索引:
$ shp2pgsql -G -I global_24h.shp
chp01.global_24h_geographic > global_24h.sql
- 分析
global_24h.sql文件(在 Windows 中,使用记事本等文本编辑器):
$ head -n 20 global_24h.sql
上述命令的输出如下:

- 在 PostgreSQL 中运行
global_24h.sql文件:
$ psql -U me -d postgis_cookbook -f global_24h.sql
如果您使用 Linux,可以将最后两个步骤中的命令合并为单行,如下所示:
$ shp2pgsql -G -I global_24h.shp chp01.global_24h_geographic | psql -U me -d postgis_cookbook
- 检查元数据记录是否在
geography_columns视图中可见(而不是在geometry_columns视图中,因为使用shp2pgsql命令的-G选项时,我们已选择geography类型):
postgis_cookbook=# SELECT f_geography_column, coord_dimension,
srid, type FROM geography_columns
WHERE f_table_name = 'global_24h_geographic';
上述命令的输出如下:

- 使用
ogrinfo分析新的 PostGIS 表(使用-fid选项仅显示表中的一条记录):
$ ogrinfo PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" chp01.global_24h_geographic -fid 1
上述命令的输出如下:

现在,打开 QGIS 并尝试将新图层添加到地图中。导航到图层 | 添加图层 | 添加 PostGIS 图层,并输入连接信息,然后将图层添加到地图中,如下面的截图所示:

工作原理...
PostGIS 命令 shp2pgsql 允许用户将 shapefile 导入到 PostGIS 数据库中。基本上,它生成一个 PostgreSQL 导出文件,可以通过在 PostgreSQL 中运行它来加载数据。
SQL 文件通常由以下部分组成:
-
CREATE TABLE部分(如果未选择-a选项,在这种情况下,表应已存在于数据库中) -
INSERT INTO部分(每个要从中导入的要素一个INSERT语句) -
CREATE INDEX部分(如果选择-I选项)
与 ogr2ogr 不同,没有方法可以对要导入的 shapefile 中的要素进行空间或属性选择(-spat,-where ogr2ogr 选项)。
另一方面,使用 shp2pgsql 命令,还可以导入要素的 m 坐标(截至编写时,ogr2ogr 只支持 x,y 和 z)。
要获取 shp2pgsql 命令的所有选项及其含义的完整列表,只需在 shell(或在 Windows 的命令提示符中)中输入命令名称并检查输出。
更多内容...
在 PostGIS 中管理数据进出的 GUI 工具通常集成到 GIS 桌面软件中,例如 QGIS。本书的最后一章,我们将探讨其中最受欢迎的一个。
使用 ogr2ogr GDAL 命令导入和导出数据
在这个菜谱中,您将使用流行的ogr2ogr GDAL 命令从 PostGIS 导入和导出矢量数据。
首先,您将使用ogr2ogr命令的最显著选项将 shapefile 导入到 PostGIS 中。然后,仍然使用ogr2ogr,您将把在 PostGIS 中执行的空间查询的结果导出到几个 GDAL 支持的矢量格式中。
如何操作...
完成此菜谱所需的步骤如下:
-
将
wborders.zip存档解压到您的工作目录。您可以在本书的数据集中找到此存档。 -
使用
ogr2ogr命令将世界国家 shapefile(wborders.shp)导入到 PostGIS 中。使用ogr2ogr的一些选项,您将只导入SUBREGION=2(非洲)的特征,以及ISO2和NAME属性,并将特征类重命名为africa_countries:
$ ogr2ogr -f PostgreSQL -sql "SELECT ISO2,
NAME AS country_name FROM wborders WHERE REGION=2" -nlt
MULTIPOLYGON PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" -nln africa_countries
-lco SCHEMA=chp01 -lco GEOMETRY_NAME=the_geom wborders.shp
-
检查 shapefile 是否已正确导入到 PostGIS 中,可以通过查询数据库中的空间表或在桌面 GIS 中显示它来验证。
-
使用上一道菜谱中创建的
global_24h表,查询 PostGIS 以获取亮度温度(bright_t31字段)最高的 100 个活跃热点列表:
postgis_cookbook=# SELECTST_AsText(the_geom) AS the_geom, bright_t31 FROM chp01.global_24h ORDER BY bright_t31 DESC LIMIT 100;
上述命令的输出如下:

- 您想找出这些热点位于哪些非洲国家。为此,您可以对上一步骤中产生的
africa_countries表进行空间连接:
postgis_cookbook=# SELECT ST_AsText(f.the_geom)
AS the_geom, f.bright_t31, ac.iso2, ac.country_name
FROM chp01.global_24h as f JOIN chp01.africa_countries as ac ON ST_Contains(ac.the_geom, ST_Transform(f.the_geom, 4326)) ORDER BY f.bright_t31 DESCLIMIT 100;
上述命令的输出如下:

现在,您将使用ogr2ogr将此查询的结果导出为 GDAL 支持的矢量格式,如 GeoJSON,在 WGS 84 空间参考下:
$ ogr2ogr -f GeoJSON -t_srs EPSG:4326 warmest_hs.geojson
PG:"dbname='postgis_cookbook' user='me' password='mypassword'" -sql "
SELECT f.the_geom as the_geom, f.bright_t31,
ac.iso2, ac.country_name
FROM chp01.global_24h as f JOIN chp01.africa_countries as ac
ON ST_Contains(ac.the_geom, ST_Transform(f.the_geom, 4326))
ORDER BY f.bright_t31 DESC LIMIT 100"
- 使用您最喜欢的桌面 GIS 打开 GeoJSON 文件并检查它。以下截图显示了它在 QGIS 中的样子:

- 将之前的查询导出到 CSV 文件。在这种情况下,您必须指明几何信息必须在文件中如何存储;这是通过使用
-lco GEOMETRY选项来完成的:
$ ogr2ogr -t_srs EPSG:4326 -f CSV -lco GEOMETRY=AS_XY
-lco SEPARATOR=TAB warmest_hs.csv PG:"dbname='postgis_cookbook'
user='me' password='mypassword'" -sql "
SELECT f.the_geom, f.bright_t31,
ac.iso2, ac.country_name
FROM chp01.global_24h as f JOIN chp01.africa_countries as ac
ON ST_Contains(ac.the_geom, ST_Transform(f.the_geom, 4326))
ORDER BY f.bright_t31 DESC LIMIT 100"
它是如何工作的...
GDAL 是一个开源库,它包含几个命令行实用程序,允许用户将栅格和矢量地理数据集转换为多种格式。在矢量数据集的情况下,有一个名为 OGR 的 GDAL 子库用于管理矢量数据集(因此,在 GDAL 的上下文中讨论矢量数据集时,我们也可以使用表达式OGR 数据集)。
当您使用 OGR 数据集工作时,最流行的两个 OGR 命令是ogrinfo,它可以列出来自 OGR 数据集的许多信息,以及ogr2ogr,它可以将 OGR 数据集从一种格式转换为另一种格式。
使用任何 OGR 命令的 -formats 选项可以检索支持的 OGR 向量格式列表,例如使用 ogr2ogr:
$ ogr2ogr --formats
前一个命令的输出如下:

注意,某些格式是只读的,而其他格式是读写。
PostGIS 是支持的读写 OGR 格式之一,因此可以使用 OGR API 或任何 OGR 命令(如 ogrinfo 和 ogr2ogr)来操作其数据集。
ogr2ogr 命令有许多选项和参数;在这个菜谱中,你已经看到了一些最显著的选项,例如 -f 用于定义输出格式,-t_srs 用于重新投影/转换数据集,以及 -sql 用于在输入 OGR 数据集中定义一个(最终是空间)查询。
当使用 ogrinfo 和 ogr2ogr 以及所需的选项和参数时,你必须定义数据集。当指定 PostGIS 数据集时,你需要一个如下定义的连接字符串:
PG:"dbname='postgis_cookbook' user='me' password='mypassword'"
参见
你可以在 GDAL 网站上找到有关 ogrinfo 和 ogr2ogr 命令的更多信息,网站地址为 www.gdal.org。
如果你需要更多关于 PostGIS 驱动的信息,你应该查看其相关文档页面,网站地址为 www.gdal.org/drv_pg.html。
处理数据集的批量导入和导出
在许多 GIS 工作流程中,有一个典型的场景,即必须将 PostGIS 表的子集部署到外部用户在文件系统格式中(最常见的是形状文件或空间数据库)。通常,也存在反向过程,即必须将来自不同用户的接收到的数据集上传到 PostGIS 数据库。
在这个菜谱中,我们将模拟这两种数据流。你将首先创建处理 PostGIS 中形状文件的数据流,然后是上传形状文件的反向数据流。
你将使用 bash 脚本和 ogr2ogr 命令的强大功能来完成这项操作。
准备工作
如果你没有遵循所有其他菜谱,请确保使用 ogr2ogr 导入热点 (Global_24h.csv) 和国家数据集 (countries.shp) 到 PostGIS。以下是如何操作的示例(你应该导入这两个数据集在其原始 SRID,4326,以使空间操作更快):
- 使用你之前菜谱中创建的
global_24.vrt虚拟驱动程序将Global_24h.csv文件导入 PostGIS:
$ ogr2ogr -f PostgreSQL PG:"dbname='postgis_cookbook'
user='me' password='mypassword'" -lco SCHEMA=chp01 global_24h.vrt
-lco OVERWRITE=YES -lco GEOMETRY_NAME=the_geom -nln hotspots
- 使用
ogr2ogr导入国家形状文件:
$ ogr2ogr -f PostgreSQL -sql "SELECT ISO2, NAME AS country_name
FROM wborders" -nlt MULTIPOLYGON PG:"dbname='postgis_cookbook'
user='me' password='mypassword'" -nln countries
-lco SCHEMA=chp01 -lco OVERWRITE=YES
-lco GEOMETRY_NAME=the_geom wborders.shp
如果你已经使用 3857 SRID 导入了热点数据集,你可以使用 PostGIS 2.0 方法,该方法允许用户修改现有空间表的几何类型列。你可以通过在几何对象上支持 typmod 的方式更新热点表的 SRID 定义:
postgis_cookbook=# ALTER TABLE chp01.hotspots
ALTER COLUMN the_geom
SET DATA TYPE geometry(Point, 4326)
USING ST_Transform(the_geom, 4326);
如何操作...
完成此菜谱所需的步骤如下:
- 使用以下查询检查每个不同国家有多少热点:
postgis_cookbook=> SELECT c.country_name, MIN(c.iso2)
as iso2, count(*) as hs_count FROM chp01.hotspots as hs
JOIN chp01.countries as c ON ST_Contains(c.the_geom, hs.the_geom)
GROUP BY c.country_name ORDER BY c.country_name;
前一个命令的输出如下:

- 使用相同的查询,使用 PostgreSQL 的
COPY命令或ogr2ogr命令(在第一种情况下,请确保 Postgre 服务用户对输出目录有完全写入权限)。如果您遵循COPY方法并且使用 Windows,请务必将/tmp/hs_countries.csv替换为不同的路径:
$ ogr2ogr -f CSV hs_countries.csv
PG:"dbname='postgis_cookbook' user='me' password='mypassword'"
-lco SCHEMA=chp01 -sql "SELECT c.country_name, MIN(c.iso2) as iso2,
count(*) as hs_count FROM chp01.hotspots as hs
JOIN chp01.countries as c ON ST_Contains(c.the_geom, hs.the_geom)
GROUP BY c.country_name ORDER BY c.country_name" postgis_cookbook=> COPY (SELECT c.country_name, MIN(c.iso2) as iso2,
count(*) as hs_count FROM chp01.hotspots as hs JOIN chp01.countries as c ON ST_Contains(c.the_geom, hs.the_geom) GROUP BY c.country_name ORDER BY c.country_name)
TO '/tmp/hs_countries.csv' WITH CSV HEADER;
- 如果您使用的是 Windows,请转到步骤 5。如果您使用 Linux,请创建一个名为
export_shapefiles.sh的 bash 脚本,该脚本遍历hs_countries.csv文件中的每个记录(国家),并为该国从 PostGIS 导出的热点生成相应的 shapefile:
#!/bin/bash
while IFS="," read country iso2 hs_count
do
echo "Generating shapefile $iso2.shp for country
$country ($iso2) containing $hs_count features."
ogr2ogr out_shapefiles/$iso2.shp
PG:"dbname='postgis_cookbook' user='me' password='mypassword'"
-lco SCHEMA=chp01 -sql "SELECT ST_Transform(hs.the_geom, 4326),
hs.acq_date, hs.acq_time, hs.bright_t31
FROM chp01.hotspots as hs JOIN chp01.countries as c
ON ST_Contains(c.the_geom, ST_Transform(hs.the_geom, 4326))
WHERE c.iso2 = '$iso2'" done < hs_countries.csv
- 给
bash文件执行权限,然后在创建输出目录(out_shapefiles)后运行它,该目录将用于存储脚本生成的 shapefile。然后,转到步骤 7:
chmod 775 export_shapefiles.sh mkdir out_shapefiles $ ./export_shapefiles.sh Generating shapefile AL.shp for country
Albania (AL) containing 66 features. Generating shapefile DZ.shp for country
Algeria (DZ) containing 361 features. ...
Generating shapefile ZM.shp for country
Zambia (ZM) containing 1575 features. Generating shapefile ZW.shp for country
Zimbabwe (ZW) containing 179 features.
如果您得到输出ERROR: function getsrid(geometry) does not exist LINE 1: SELECT getsrid("the_geom") FROM (SELECT,...,您需要在 PostGIS 中加载旧版支持,例如在 Debian Linux 盒子上:
psql -d postgis_cookbook -f /usr/share/postgresql/9.1/contrib/postgis-2.1/legacy.sql
- 如果您使用的是 Windows,请创建一个名为
export_shapefiles.bat的批处理文件,该文件遍历hs_countries.csv文件中的每个记录(国家),并为该国从 PostGIS 导出的热点生成相应的 shapefile:
@echo off
for /f "tokens=1-3 delims=, skip=1" %%a in (hs_countries.csv) do (
echo "Generating shapefile %%b.shp for country %%a
(%%b) containing %%c features"
ogr2ogr .\out_shapefiles\%%b.shp
PG:"dbname='postgis_cookbook' user='me' password='mypassword'"
-lco SCHEMA=chp01 -sql "SELECT ST_Transform(hs.the_geom, 4326),
hs.acq_date, hs.acq_time, hs.bright_t31
FROM chp01.hotspots as hs JOIN chp01.countries as c
ON ST_Contains(c.the_geom, ST_Transform(hs.the_geom, 4326))
WHERE c.iso2 = '%%b'"
)
- 在为脚本生成的 shapefile 创建输出目录(
out_shapefiles)后运行批处理文件:
>mkdir out_shapefiles >export_shapefiles.bat "Generating shapefile AL.shp for country
Albania (AL) containing 66 features" "Generating shapefile DZ.shp for country
Algeria (DZ) containing 361 features" ... "Generating shapefile ZW.shp for country
Zimbabwe (ZW) containing 179 features"
- 尝试在您最喜欢的桌面 GIS 中打开这些输出 shapefile 中的一两个。以下截图显示了它们在 QGIS 中的样子:

- 现在,您将进行返回之旅,将所有生成的 shapefile 上传到 PostGIS。您将上传每个 shapefile 的所有要素,包括上传日期时间和原始 shapefile 名称。首先,创建以下 PostgreSQL 表,您将上传 shapefile:
postgis_cookbook=# CREATE TABLE chp01.hs_uploaded ( ogc_fid serial NOT NULL, acq_date character varying(80), acq_time character varying(80), bright_t31 character varying(80), iso2 character varying, upload_datetime character varying, shapefile character varying, the_geom geometry(POINT, 4326), CONSTRAINT hs_uploaded_pk PRIMARY KEY (ogc_fid) );
- 如果您使用的是 Windows,请转到步骤 12。如果您使用 OS X,您需要使用
homebrew安装findutils并运行 Linux 脚本:
$ brew install findutils
- 使用 Linux 创建另一个名为
import_shapefiles.sh的 bash 脚本:
#!/bin/bash
for f in `find out_shapefiles -name \*.shp -printf "%f\n"`
do
echo "Importing shapefile $f to chp01.hs_uploaded PostGIS
table..." #, ${f%.*}"
ogr2ogr -append -update -f PostgreSQL
PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" out_shapefiles/$f
-nln chp01.hs_uploaded -sql "SELECT acq_date, acq_time,
bright_t31, '${f%.*}' AS iso2, '`date`' AS upload_datetime,
'out_shapefiles/$f' as shapefile FROM ${f%.*}"
done
- 为 bash 脚本分配执行权限并执行它:
$ chmod 775 import_shapefiles.sh $ ./import_shapefiles.sh Importing shapefile DO.shp to chp01.hs_uploaded PostGIS table
... Importing shapefile ID.shp to chp01.hs_uploaded PostGIS table
... Importing shapefile AR.shp to chp01.hs_uploaded PostGIS table
......
现在,转到步骤 14。
- 如果您使用的是 Windows,请创建一个名为
import_shapefiles.bat的批处理脚本:
@echo off
for %%I in (out_shapefiles\*.shp*) do (
echo Importing shapefile %%~nxI to chp01.hs_uploaded
PostGIS table...
ogr2ogr -append -update -f PostgreSQL
PG:"dbname='postgis_cookbook' user='me'
password='password'" out_shapefiles/%%~nxI
-nln chp01.hs_uploaded -sql "SELECT acq_date, acq_time,
bright_t31, '%%~nI' AS iso2, '%date%' AS upload_datetime,
'out_shapefiles/%%~nxI' as shapefile FROM %%~nI"
)
- 运行批处理脚本:
>import_shapefiles.bat Importing shapefile AL.shp to chp01.hs_uploaded PostGIS table... Importing shapefile AO.shp to chp01.hs_uploaded PostGIS table... Importing shapefile AR.shp to chp01.hs_uploaded PostGIS table......
- 使用 SQL 检查已上传到 PostGIS 表的一些记录:
postgis_cookbook=# SELECT upload_datetime,
shapefile, ST_AsText(wkb_geometry)
FROM chp01.hs_uploaded WHERE ISO2='AT';
前一个命令的输出如下:

- 使用
ogrinfo检查相同的查询:
$ ogrinfo PG:"dbname='postgis_cookbook' user='me'
password='mypassword'"
chp01.hs_uploaded -where "iso2='AT'"
前一个命令的输出如下:

它是如何工作的...
您可以利用ogr2ogr GDAL 命令的强大功能实现数据流(从 PostGIS 处理 shapefile,然后再次进入它)。
您已经在其他菜谱中以不同形式和最重要的输入参数使用过此命令,因此您现在应该很好地理解它。
在这里,值得提及的是 OGR 允许您将有关当前日期时间和原始 shapefile 名称的信息导出到 PostGIS 表中的方式。在import_shapefiles.sh(Linux,OS X)或import_shapefiles.bat(Windows)脚本中,核心是带有ogr2ogr命令的行(以下是 Linux 版本):
ogr2ogr -append -update -f PostgreSQL PG:"dbname='postgis_cookbook' user='me' password='mypassword'" out_shapefiles/$f -nln chp01.hs_uploaded -sql "SELECT acq_date, acq_time, bright_t31, '${f%.*}' AS iso2, '`date`' AS upload_datetime, 'out_shapefiles/$f' as shapefile FROM ${f%.*}"
由于-sql选项,您可以从系统日期命令和脚本中迭代的文件名中获取两个附加字段的值。
使用 PostGIS 的 pgsql2shp 命令将数据导出为 shapefile
在这个菜谱中,您将使用随任何 PostGIS 发行版一起提供的pgsql2shp命令将 PostGIS 表导出为 shapefile。
如何操作...
您需要遵循以下步骤来完成此菜谱:
- 如果您还没有这样做,请使用
ogr2ogr或shp2pgsql命令将国家 shapefile 导出到 PostGIS。shp2pgsql的方法如下:
$ shp2pgsql -I -d -s 4326 -W LATIN1 -g the_geom countries.shp
chp01.countries > countries.sql $ psql -U me -d postgis_cookbook -f countries.sql
ogr2ogr的方法如下:
$ ogr2ogr -f PostgreSQL PG:"dbname='postgis_cookbook' user='me'
password='mypassword'"
-lco SCHEMA=chp01 countries.shp -nlt MULTIPOLYGON -lco OVERWRITE=YES
-lco GEOMETRY_NAME=the_geom
- 现在,查询 PostGIS 以获取按
subregion字段分组的国家列表。为此,您将使用ST_UnionPostGIS 几何处理函数合并具有相同subregion代码的要素的几何形状:
postgis_cookbook=> SELECT subregion,
ST_Union(the_geom) AS the_geom, SUM(pop2005) AS pop2005
FROM chp01.countries GROUP BY subregion;
- 执行
pgsql2shpPostGIS 命令以将给定查询的结果导出到 shapefile。
$ pgsql2shp -f subregions.shp -h localhost -u me -P mypassword
postgis_cookbook "SELECT MIN(subregion) AS subregion,
ST_Union(the_geom) AS the_geom, SUM(pop2005) AS pop2005
FROM chp01.countries GROUP BY subregion;"
Initializing...
Done (postgis major version: 2).
Output shape: Polygon
Dumping: X [23 rows].
- 打开 shapefile,使用您最喜欢的桌面 GIS 进行检验。这是在 QGIS 中应用基于每个子区域聚合人口的分级分类符号样式后的样子:

在 QGIS 中根据人口和所选特征的信息对子区域进行分类的可视化
它是如何工作的...
您已使用pgsql2shp PostGIS 命令将空间查询的结果导出为 shapefile。您所使用的空间查询使用SUM PostgreSQL 函数对同一子区域的国家人口进行聚合,并使用ST_Union PostGIS 函数将相应的几何形状聚合为几何并集。
pgsql2shp命令允许您将 PostGIS 表和查询导出为 shapefile。您需要指定的选项与您使用psql连接到 PostgreSQL 时使用的选项相当。要获取这些选项的完整列表,只需在命令提示符中键入pgsql2shp并阅读输出。
使用 osm2pgsql 命令导入 OpenStreetMap 数据
在这个菜谱中,您将使用osm2pgsql命令将OpenStreetMap(OSM)数据导入到 PostGIS。
您首先需要从 OSM 网站下载一个样本数据集,然后使用osm2pgsql命令将其导入。
您将在 GIS 桌面软件中添加导入的层,并生成一个视图以获取子数据集,使用hstore PostgreSQL 附加模块根据它们的标签提取特征。
准备工作
在我们进行下一步骤之前,我们需要以下内容:
- 安装
osm2pgsql。如果您使用 Windows,请遵循wiki.openstreetmap.org/wiki/Osm2pgsql中提供的说明。如果您在 Linux 上,可以从先前的网站或从软件包中安装它。例如,对于 Debian 发行版,使用以下命令:
$ sudo apt-get install osm2pgsql
-
关于在其他 Linux 发行版、macOS X 和 MS Windows 上安装
osm2pgsql命令的更多信息,请参阅wiki.openstreetmap.org/wiki/Osm2pgsql上的osm2pgsql网页。 -
很可能您需要自己编译
osm2pgsql,因为与您的包管理器一起安装的版本可能已经过时。在我的 Linux Mint 12 系统中,情况就是这样(它是osm2pgsqlv0.75),所以我按照osm2pgsql网页上的说明安装了版本 0.80。您可以通过输入以下命令来检查已安装的版本:
$ osm2pgsqlosm2pgsql SVN version 0.80.0 (32bit id space)
- 我们将只为这个食谱创建一个不同的数据库,因为我们将在其他章节中使用这个 OSM 数据库。为此,创建一个名为
rome的新数据库并分配给您的用户权限:
postgres=# CREATE DATABASE rome OWNER me; postgres=# \connect rome; rome=# create extension postgis;
-
尽管如此,您不会在这个新数据库中创建不同的模式,因为截至写作时,
osm2pgsql命令只能导入公共模式中的 OSM 数据。 -
确保您的 PostgreSQL 安装支持
hstore(除了PostGIS)。如果不支持,请下载并安装它;例如,在基于 Debian 的 Linux 发行版中,您需要安装postgresql-contrib-9.6包。然后,使用CREATE EXTENSION语法将hstore支持添加到rome数据库中:
$ sudo apt-get update $ sudo apt-get install postgresql-contrib-9.6 $ psql -U me -d romerome=# CREATE EXTENSION hstore;
如何操作...
完成此食谱所需的步骤如下:
-
从 OpenStreetMap 网站下载一个
.osm文件(www.openstreetmap.org/#map=5/21.843/82.795)。 -
-
前往 OpenStreetMap 网站。
-
选择您想要导出数据的感兴趣区域。您不应该选择一个很大的区域,因为网站上的实时导出限制为 50,000 个节点。
-
如果您想要导出更大的区域,您应该考虑下载整个数据库,该数据库每天在planet.osm上构建(未压缩 250 GB,压缩 16 GB)。在planet.osm上,您还可以下载包含 OpenstreetMap 数据单个大陆、国家和大都市区的提取文件。
-
-
如果你想获取用于此菜谱的相同数据集,只需将以下 URL 复制并粘贴到你的浏览器中:
www.openstreetmap.org/export?lat=41.88745&lon=12.4899&zoom=15&layers=M;或者,从书籍数据集中获取它(chp01/map.osm文件)。 -
点击导出链接。
-
选择 OpenStreetMap XML 数据作为输出格式。
-
将
map.osm文件下载到你的工作目录。
-
-
运行
osm2pgsql将 OSM 数据导入到 PostGIS 数据库。使用-hstore选项,因为你希望在 PostgreSQL 表中添加带有额外hstore(键/值)列的标签:
$ osm2pgsql -d rome -U me --hstore map.osm osm2pgsql SVN version 0.80.0 (32bit id space)Using projection
SRS 900913 (Spherical Mercator)Setting up table:
planet_osm_point...All indexes on planet_osm_polygon created
in 1sCompleted planet_osm_polygonOsm2pgsql took 3s overall
- 到目前为止,你应该在你的数据库中有以下几何表:
rome=# SELECT f_table_name, f_geometry_column,
coord_dimension, srid, type FROM geometry_columns;
上述命令的输出如下所示:

-
注意,
osm2pgsql命令导入公共模式下的所有内容。如果你没有对命令的输入参数进行不同的处理,你的数据将导入到墨卡托投影(3857)。 -
使用你喜欢的桌面 GIS 打开 PostGIS 表并检查它们。以下截图显示了在 QGIS 中的样子。此时,所有不同的主题特征都混合在一起,所以看起来有点混乱:

- 生成一个 PostGIS 视图,提取所有标记为
trees作为land cover的多边形。为此,创建以下视图:
rome=# CREATE VIEW rome_trees AS SELECT way, tags
FROM planet_osm_polygon WHERE (tags -> 'landcover') = 'trees';
- 使用支持 PostGIS 视图的桌面 GIS(如 QGIS),打开视图并添加你的
rome_trees视图。上一张截图显示了它的样子。
它是如何工作的...
OpenStreetMap 是一个流行的协作项目,用于创建世界免费地图。每个参与项目的用户都可以编辑数据;同时,在撰写本文时,每个人都可以根据 Open Data Commons Open Database License(ODbL)条款下载那些数据集(.osm 数据文件,XML 格式)。
osm2pgsql 命令是一个命令行工具,可以将 .osm 数据文件(最终可能已压缩)导入到 PostGIS 数据库。要使用此命令,只需提供 PostgreSQL 连接参数和要导入的 .osm 文件即可。
可以只导入空间数据库中具有特定标签的特征,如 default.style 配置文件中定义的。你可以决定是否从该文件中导入或注释掉你想要导入的 OSM 标签特征。默认情况下,命令导出所有节点和路径到线字符串、点和几何 PostGIS 几何体。
强烈建议在 PostgreSQL 数据库中启用 hstore 支持,并在导入数据时使用 osm2pgsql 的 -hstore 选项。启用此支持后,每个特征的 OSM 标签将存储在 hstore PostgreSQL 数据类型中,该类型优化了单个字段中存储(和检索)键/值对集合。这样,就可以按照以下方式查询数据库:
SELECT way, tags FROM planet_osm_polygon WHERE (tags -> 'landcover') = 'trees';
使用 raster2pgsql PostGIS 命令导入栅格数据
PostGIS 2.0 现在完全支持栅格数据集,可以使用 raster2pgsql 命令导入栅格数据集。
在本菜谱中,您将使用 raster2pgsql 命令将栅格文件导入到 PostGIS。从 2.0 版本开始,该命令包含在任何 PostGIS 发行版中,能够生成 SQL 转储文件,以便在 PostGIS 中加载任何 GDAL 支持的栅格格式(与 shp2pgsql 命令为 shapefile 执行的操作类似)。
将栅格加载到 PostGIS 后,您将使用 SQL 命令(分析数据库中包含的栅格元数据信息)和 gdalinfo 命令行工具(了解输入 raster2pgsql 参数如何在 PostGIS 导入过程中反映)来检查它。
最后,您将在桌面 GIS 中打开栅格,并尝试进行基本的空间查询,混合矢量表和栅格表。
准备工作
在我们继续进行菜谱所需的步骤之前,我们需要以下内容:
-
从 worldclim 网站下载当前的温度栅格数据(
www.worldclim.org/current),包括最小和最大温度(本菜谱中仅使用最大温度的栅格)。或者,使用书中提供的数据集(data/chp01)。每个两个存档(data/tmax_10m_bil.zip和data/tmin_10m_bil.zip)包含 12 个栅格,每个栅格对应一个月。更多详细信息请参阅www.worldclim.org/formats。 -
将两个存档解压到您工作目录中名为
worldclim的目录中。 -
将每个栅格数据集重命名为带有两位数字的月份格式,例如,
tmax1.bil和tmax1.hdr将变为tmax01.bil和tmax01.hdr。 -
如果您还没有从之前的菜谱中加载国家形状文件到 PostGIS,请使用
ogr2ogr或shp2pgsql命令进行加载。以下为shp2pgsql的语法:
$ shp2pgsql -I -d -s 4326 -W LATIN1 -g the_geom countries.shp
chp01.countries > countries.sql $ psql -U me -d postgis_cookbook -f countries.sql
如何操作...
完成本菜谱所需的步骤如下:
- 使用以下命令使用
gdalinfo命令行工具获取一个栅格的信息:
$ gdalinfo worldclim/tmax09.bil Driver: EHdr/ESRI .hdr Labelled Files: worldclim/tmax9.bil worldclim/tmax9.hdr
Size is 2160, 900 Coordinate System is: GEOGCS[""WGS 84"",
DATUM[""WGS_1984"",
SPHEROID[""WGS 84"",6378137,298.257223563,
AUTHORITY[""EPSG"",""7030""]],
TOWGS84[0,0,0,0,0,0,0],
AUTHORITY[""EPSG"",""6326""]],
PRIMEM[""Greenwich"",0,
AUTHORITY[""EPSG"",""8901""]],
UNIT[""degree"",0.0174532925199433,
AUTHORITY[""EPSG"",""9108""]],
AUTHORITY[""EPSG"",""4326""]]
Origin = (-180.000000000000057,90.000000000000000) Pixel Size = (0.166666666666667,-0.166666666666667) Corner Coordinates: Upper Left (-180.0000000, 90.0000000) (180d 0'' 0.00""W, 90d
0'' 0.00""N)
Lower Left (-180.0000000, -60.0000000) (180d 0'' 0.00""W, 60d
0'' 0.00""S)
Upper Right ( 180.0000000, 90.0000000) (180d 0'' 0.00""E, 90d
0'' 0.00""N)
Lower Right ( 180.0000000, -60.0000000) (180d 0'' 0.00""E, 60d
0'' 0.00""S)
Center ( 0.0000000, 15.0000000) ( 0d 0'' 0.00""E, 15d
0'' 0.00""N)
Band 1 Block=2160x1 Type=Int16, ColorInterp=Undefined Min=-153.000 Max=441.000 NoData Value=-9999
-
gdalinfo命令提供了关于栅格的大量有用信息,例如,用于读取它的 GDAL 驱动程序,组成它的文件(在这种情况下,两个具有.bil和.hdr扩展名的文件),像素大小(2160 x 900),空间参考(WGS 84),地理范围,起点,像素大小(用于正确地理参照栅格),以及对于每个栅格波段(在本文件中只有一个),一些统计信息,如最小值和最大值(-153.000 和 441.000,对应于 -15.3 °C 和 44.1 °C 的温度。值以温度 * 10 在 °C 表示,根据worldclim.org/上的文档)。 -
使用
raster2pgsql文件生成.sql导出文件,然后导入 PostGIS 中的栅格数据:
$ raster2pgsql -I -C -F -t 100x100 -s 4326
worldclim/tmax01.bil chp01.tmax01 > tmax01.sql $ psql -d postgis_cookbook -U me -f tmax01.sql
如果你使用的是 Linux 系统,你可以将这两个命令放在一行中执行:
$ raster2pgsql -I -C -M -F -t 100x100 worldclim/tmax01.bil
chp01.tmax01 | psql -d postgis_cookbook -U me -f tmax01.sql
- 检查新表在 PostGIS 中的创建情况:
$ pg_dump -t chp01.tmax01 --schema-only -U me postgis_cookbook
...
CREATE TABLE tmax01 (
rid integer NOT NULL,
rast public.raster,
filename text,
CONSTRAINT enforce_height_rast CHECK (
(public.st_height(rast) = 100)
),
CONSTRAINT enforce_max_extent_rast CHECK (public.st_coveredby
(public.st_convexhull(rast), ''0103...''::public.geometry)
),
CONSTRAINT enforce_nodata_values_rast CHECK (
((public._raster_constraint_nodata_values(rast)
)::numeric(16,10)[] = ''{0}''::numeric(16,10)[])
),
CONSTRAINT enforce_num_bands_rast CHECK (
(public.st_numbands(rast) = 1)
),
CONSTRAINT enforce_out_db_rast CHECK (
(public._raster_constraint_out_db(rast) = ''{f}''::boolean[])
),
CONSTRAINT enforce_pixel_types_rast CHECK (
(public._raster_constraint_pixel_types(rast) =
''{16BUI}''::text[])
),
CONSTRAINT enforce_same_alignment_rast CHECK (
(public.st_samealignment(rast, ''01000...''::public.raster)
),
CONSTRAINT enforce_scalex_rast CHECK (
((public.st_scalex(rast))::numeric(16,10) =
0.166666666666667::numeric(16,10))
),
CONSTRAINT enforce_scaley_rast CHECK (
((public.st_scaley(rast))::numeric(16,10) =
(-0.166666666666667)::numeric(16,10))
),
CONSTRAINT enforce_srid_rast CHECK ((public.st_srid(rast) = 0)),
CONSTRAINT enforce_width_rast CHECK ((public.st_width(rast) = 100))
);
- 检查是否在
raster_columns元数据视图中出现此 PostGIS 栅格的记录,并注意存储在此处的主要元数据信息,例如模式、名称、栅格列名称(默认为 raster)、SRID、比例(对于 x 和 y)、块大小(对于 x 和 y)、波段数(1)、波段类型(16BUI)、零数据值(0)和db存储类型(out_db为false,因为我们已将栅格字节存储在数据库中;你可以使用-R选项将栅格注册为数据库外部的文件系统):
postgis_cookbook=# SELECT * FROM raster_columns;
- 如果你从开始就遵循了这个菜谱,你现在应该在栅格表中看到 198 行,每行代表一个栅格块大小(100 x 100 像素块,如
-traster2pgsql选项所示):
postgis_cookbook=# SELECT count(*) FROM chp01.tmax01;
前一个命令的输出如下:
count ------- 198 (1 row)
- 尝试使用
gdalinfo打开栅格表。你应该看到与之前分析原始 BIL 文件时相同的gdalinfo信息。唯一的区别是块大小,因为你从原始的(2160 x 900)移动到了更小的(100 x 100)。这就是为什么原始文件被分割成多个数据集(198 个)的原因:
gdalinfo PG":host=localhost port=5432 dbname=postgis_cookbook
user=me password=mypassword schema='chp01' table='tmax01'"
gdalinfo命令读取 PostGIS 栅格数据,显示由多个栅格子数据集组成(198 个,每个对应表中的一行)。你仍然可以使用mode=2选项在 PostGIS 栅格连接字符串中读取整个表作为一个单独的栅格(默认的mode=1)。检查差异:
gdalinfo PG":host=localhost port=5432 dbname=postgis_cookbook
user=me password=mypassword schema='chp01' table='tmax01' mode=2"
- 你可以通过将
tmax01表中所有 198 行的范围(每行代表一个栅格块)转换为形状文件来轻松地获得这些块的视觉表示,使用ogr2ogr:
$ ogr2ogr temp_grid.shp PG:"host=localhost port=5432
dbname='postgis_cookbook' user='me' password='mypassword'"
-sql "SELECT rid, filename, ST_Envelope(rast) as the_geom
FROM chp01.tmax01"
- 现在,尝试使用 QGIS(在撰写本文时,是少数几个支持此功能的桌面 GIS 工具之一)打开栅格表,同时使用之前步骤中生成的块形状文件(
temp_grid.shp)。你应该会看到以下截图类似的内容:

如果你使用的是 QGIS 2.6 或更高版本,你可以在数据库菜单下的 DB 管理器中看到图层,并将其拖到图层面板中。
- 作为最后一个奖励步骤,你将选择 1 月平均最高气温最低的 10 个国家(使用代表国家的多边形质心):
SELECT * FROM (
SELECT c.name, ST_Value(t.rast,
ST_Centroid(c.the_geom))/10 as tmax_jan FROM chp01.tmax01 AS t
JOIN chp01.countries AS c
ON ST_Intersects(t.rast, ST_Centroid(c.the_geom))
) AS foo
ORDER BY tmax_jan LIMIT 10;
输出如下:

它是如何工作的...
raster2pgsql 命令能够加载 GDAL 在 PostGIS 中支持的任何栅格格式。你可以通过输入以下命令来获取你 GDAL 安装支持的格式列表:
$ gdalinfo --formats
在这个菜谱中,你已经使用了一些最常用的 raster2pgsql 选项导入了一个栅格文件:
$ raster2pgsql -I -C -F -t 100x100 -s 4326 worldclim/tmax01.bil chp01.tmax01 > tmax01.sql
-I 选项为栅格列创建一个 GIST 空间索引。-C 选项将在栅格加载后创建标准约束集。-F 选项将为已加载的栅格添加一个包含文件名的列。当您将许多栅格文件附加到同一个 PostGIS 栅格表时,这很有用。-s 选项设置栅格的 SRID。
如果您决定包含 -t 选项,那么您将把原始栅格切割成瓦片,每个瓦片作为一个单独的行插入到栅格表中。在这种情况下,您决定将栅格切割成 100 x 100 的瓦片,结果在栅格表中产生 198 行。
另一个重要的选项是 -R,它将栅格注册为 out-of-db;在这种情况下,只有元数据将被插入到数据库中,而栅格将不在数据库中。
栅格表包含每行的标识符,栅格本身(如果使用了 -t 选项,则可能是其瓦片之一),以及如果使用了 -F 选项(如本例所示),则最终包含原始文件名。
您可以使用 SQL 命令或 gdalinfo 命令分析 PostGIS 栅格。使用 SQL,您可以通过查询 raster_columns 视图来获取最重要的栅格元数据(空间参考、波段号、比例、块大小等)。
使用 gdalinfo,您可以使用以下语法格式的连接字符串访问相同的信息:
gdalinfo PG":host=localhost port=5432 dbname=postgis_cookbook user=me password=mypassword schema='chp01' table='tmax01' mode=2"
如果您将整个栅格作为一个单独的块加载,则 mode 参数不会产生影响(例如,如果您没有指定 -t 选项)。但是,正如本菜谱的使用案例,如果您将其分割成瓦片,gdalinfo 将每个瓦片视为一个单独的子数据集,具有默认行为(mode=1)。如果您希望 GDAL 将栅格表视为一个唯一的栅格数据集,您必须指定模式选项,并明确将其设置为 2。
一次性导入多个栅格
此菜谱将指导您如何一次性导入多个栅格。
您将首先使用 raster2pgsql 命令将一些不同的单波段栅格导入一个独特的单波段栅格表。
然后,您将尝试一种替代方法,将原始单波段栅格合并到一个虚拟栅格中,每个原始栅格一个波段,然后将多波段栅格加载到栅格表中。为此,您将使用 GDAL 的 gdalbuildvrt 命令,然后使用 raster2pgsql 将数据加载到 PostGIS 中。
准备工作
请确保您已经拥有了之前菜谱中使用的所有原始栅格数据集。
如何操作...
完成此菜谱所需的步骤如下:
- 使用
raster2pgsql和然后psql(如果是在 Linux 系统中,最终可以将这两个命令通过管道连接)将单个 PostGIS 栅格表中的所有最大平均温度栅格导入:
$ raster2pgsql -d -I -C -M -F -t 100x100 -s 4326
worldclim/tmax*.bil chp01.tmax_2012 > tmax_2012.sql $ psql -d postgis_cookbook -U me -f tmax_2012.sql
- 检查在 PostGIS 中表是如何创建的,查询
raster_columns表。在这里,我们只查询一些重要的字段:
postgis_cookbook=# SELECT r_raster_column, srid,
ROUND(scale_x::numeric, 2) AS scale_x,
ROUND(scale_y::numeric, 2) AS scale_y, blocksize_x,
blocksize_y, num_bands, pixel_types, nodata_values, out_db
FROM raster_columns where r_table_schema='chp01'
AND r_table_name ='tmax_2012';

- 使用
ST_MetaData函数检查一些栅格统计信息:
SELECT rid, (foo.md).*
FROM (SELECT rid, ST_MetaData(rast) As md
FROM chp01.tmax_2012) As foo;
注意,表中每个加载的栅格记录都有不同的元数据。
前一个命令的输出如下所示:

- 如果你现在查询表,你将能够仅从
original_file列中推导出每个栅格行的月份。在表中,你为 12 个原始文件(如果你记得的话,我们将其分成了 100 x 100 个块)导入了 198 个不同的记录(栅格)。使用以下查询进行测试:
postgis_cookbook=# SELECT COUNT(*) AS num_raster,
MIN(filename) as original_file FROM chp01.tmax_2012 GROUP BY filename ORDER BY filename;

- 使用这种方法,通过
filename字段,你可以使用ST_ValuePostGIS 栅格函数来获取整个一年中某个地理区域的平均月最高温度:
SELECT REPLACE(REPLACE(filename, 'tmax', ''), '.bil', '') AS month,
(ST_VALUE(rast, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10) AS tmax
FROM chp01.tmax_2012
WHERE rid IN (
SELECT rid FROM chp01.tmax_2012
WHERE ST_Intersects(ST_Envelope(rast),
ST_SetSRID(ST_Point(12.49, 41.88), 4326))
)
ORDER BY month;
前一个命令的输出如下所示:

- 另一种方法是,将每个月的值存储在不同的栅格波段中。
raster2pgsql命令不允许你在现有的表中加载到不同的波段。但是,你可以通过结合使用gdalbuildvrt和gdal_translate命令来使用 GDAL。首先,使用gdalbuildvrt创建一个新的由 12 个波段组成的虚拟栅格,每个波段对应于一个月:
$ gdalbuildvrt -separate tmax_2012.vrt worldclim/tmax*.bil
- 使用文本编辑器分析
tmax_2012.vrtXML 文件。它应该为指向它的每个物理栅格有一个虚拟波段(VRTRasterBand):
<VRTDataset rasterXSize="2160" rasterYSize="900">
<SRS>GEOGCS...</SRS>
<GeoTransform>
-1.8000000000000006e+02, 1.6666666666666699e-01, ...
</GeoTransform>
<VRTRasterBand dataType="Int16" band="1">
<NoDataValue>-9.99900000000000E+03</NoDataValue>
<ComplexSource>
<SourceFilename relativeToVRT="1">
worldclim/tmax01.bil
</SourceFilename>
<SourceBand>1</SourceBand>
<SourceProperties RasterXSize="2160" RasterYSize="900"
DataType="Int16" BlockXSize="2160" BlockYSize="1" />
<SrcRect xOff="0" yOff="0" xSize="2160" ySize="900" />
<DstRect xOff="0" yOff="0" xSize="2160" ySize="900" />
<NODATA>-9999</NODATA>
</ComplexSource>
</VRTRasterBand>
<VRTRasterBand dataType="Int16" band="2">
...
- 现在,使用
gdalinfo分析这个输出虚拟栅格,以检查它是否确实由 12 个波段组成:
$ gdalinfo tmax_2012.vrt
前一个命令的输出如下:
...
- 导入由 12 个波段组成的虚拟栅格,每个波段对应于 12 个原始栅格中的一个,到一个由 12 个波段组成的 PostGIS 栅格表中。为此,你可以使用
raster2pgsql命令:
$ raster2pgsql -d -I -C -M -F -t 100x100 -s 4326 tmax_2012.vrt
chp01.tmax_2012_multi > tmax_2012_multi.sql $ psql -d postgis_cookbook -U me -f tmax_2012_multi.sql
- 查询
raster_columns视图以获取导入栅格的一些指标。注意,num_bands现在为 12:
postgis_cookbook=# SELECT r_raster_column, srid, blocksize_x,
blocksize_y, num_bands, pixel_types
from raster_columns where r_table_schema='chp01'
AND r_table_name ='tmax_2012_multi';

- 现在,让我们尝试使用之前的方法产生相同的输出。这次,考虑到表结构,我们将结果保存在一行中:
postgis_cookbook=# SELECT
(ST_VALUE(rast, 1, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS jan,
(ST_VALUE(rast, 2, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS feb,
(ST_VALUE(rast, 3, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS mar,
(ST_VALUE(rast, 4, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS apr,
(ST_VALUE(rast, 5, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS may,
(ST_VALUE(rast, 6, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS jun,
(ST_VALUE(rast, 7, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS jul,
(ST_VALUE(rast, 8, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS aug,
(ST_VALUE(rast, 9, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS sep,
(ST_VALUE(rast, 10, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS oct,
(ST_VALUE(rast, 11, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS nov,
(ST_VALUE(rast, 12, ST_SetSRID(ST_Point(12.49, 41.88), 4326))/10)
AS dec
FROM chp01.tmax_2012_multi WHERE rid IN (
SELECT rid FROM chp01.tmax_2012_multi
WHERE ST_Intersects(rast, ST_SetSRID(ST_Point(12.49, 41.88), 4326))
);
前一个命令的输出如下:

它是如何工作的...
你可以使用raster2pgsql命令将栅格数据集导入 PostGIS。
到目前为止,GDAL PostGIS 栅格不支持写入操作;因此,目前你不能使用gdal_translate和gdalwarp等 GDAL 命令。
在不久的将来,这将会发生变化,所以当你阅读这一章时,你可能会有这样一个额外的选项。
在您有多个代表同一变量在不同时间点的栅格的场景中,如本食谱所示,将所有原始栅格存储在单个 PostGIS 表中是有意义的。在这个食谱中,我们有一个相同的变量(平均最高温度),由每个月的单个栅格表示。您已经看到您可以通过两种不同的方式进行处理:
-
将每个单独的栅格(代表不同的月份)附加到相同的 PostGIS 单波段栅格表,并从文件名列中的值(使用
-F raster2pgsql选项添加到表中)中提取与月份相关的信息。 -
使用
gdalbuildvrt生成多波段栅格(一个包含 12 个波段的单个栅格,每个波段代表一个月),然后使用raster2pgsql命令将其导入单个多波段 PostGIS 表中。
使用 gdal_translate 和 gdalwarp GDAL 命令导出栅格
在这个食谱中,您将看到将 PostGIS 栅格导出到不同栅格格式的几个主要选项。它们都由 GDAL 提供作为命令行工具,分别是 gdal_translate 和 gdalwarp。
准备工作
在您开始执行食谱所需的步骤之前,您需要以下内容:
-
您需要完成前面的食谱并导入
tmax2012 数据集(12 个.bil文件)作为单个多波段(12 个波段)的栅格到 PostGIS 中。 -
您必须在 GDAL 中启用 PostGIS 矢量格式。为此,检查以下命令的输出:
$ gdalinfo --formats | grep -i postgis
前一个命令的输出如下:
PostGISRaster (rw): PostGIS Raster driver
- 您应该已经在前两个食谱中学习了如何使用 GDAL PostGIS 矢量驱动程序。您需要使用由以下参数组成的连接字符串:
$ gdalinfo PG:"host=localhost port=5432
dbname='postgis_cookbook' user='me' password='mypassword'
schema='chp01' table='tmax_2012_multi' mode='2'"
- 请参考前面的两个食谱,获取有关前面参数的更多信息。
如何操作...
完成此食谱所需的步骤如下:
- 作为初始测试,您将使用
gdal_translate命令导出 2012 年的前六个月tmax(tmax_2012_multiPostGIS 矢量栅格表中的前六个波段):
$ gdal_translate -b 1 -b 2 -b 3 -b 4 -b 5 -b 6
PG:"host=localhost port=5432 dbname='postgis_cookbook'
user='me' password='mypassword' schema='chp01'
table='tmax_2012_multi' mode='2'" tmax_2012_multi_123456.tif
- 作为第二次测试,您将导出所有波段,但仅限于包含意大利的地理区域。使用
ST_Extent命令获取该区域的地理范围:
postgis_cookbook=# SELECT ST_Extent(the_geom)
FROM chp01.countries WHERE name = 'Italy';
前一个命令的输出如下:

- 现在请使用带有
-projwin选项的gdal_translate命令来获得所需的目的:
$ gdal_translate -projwin 6.619 47.095 18.515 36.649
PG:"host=localhost port=5432 dbname='postgis_cookbook'
user='me' password='mypassword' schema='chp01'
table='tmax_2012_multi' mode='2'" tmax_2012_multi.tif
- 另一个 GDAL 命令
gdalwarp仍然是一个具有重投影和高级扭曲功能的转换工具。例如,您可以使用它导出 PostGIS 矢量表,将其重投影到不同的空间参考系统。这将把 PostGIS 矢量表转换为 GeoTiff,并将其从EPSG:4326重投影到EPSG:3857:
gdalwarp -t_srs EPSG:3857 PG:"host=localhost port=5432
dbname='postgis_cookbook' user='me' password='mypassword'
schema='chp01' table='tmax_2012_multi' mode='2'"
tmax_2012_multi_3857.tif
它是如何工作的...
gdal_translate和gdalwarp都可以将 PostGIS 栅格转换为所有 GDAL 支持的格式。要获取支持格式的完整列表,您可以使用 GDAL 命令行的--formats选项,如下所示:
$ gdalinfo --formats
对于这两个 GDAL 命令,默认输出格式是 GeoTiff;如果您需要不同的格式,您必须使用-of选项并将其分配给之前命令行产生的输出之一。
在这个菜谱中,您已经尝试了这两个命令的一些最常见选项。由于它们是复杂的工具,您可能还可以尝试一些额外的命令选项作为额外步骤。
参见
为了更好地理解,您应该查看 GDAL 网站上的优秀文档:
-
关于
gdal_translate命令的信息可在www.gdal.org/gdal_translate.html找到 -
关于
gdalwarp命令的信息可在www.gdal.org/gdalwarp.html找到
第二章:有效的结构
在本章中,我们将涵盖:
-
使用地理空间视图
-
使用触发器填充几何列
-
使用表继承结构化空间数据
-
扩展继承 – 表分区
-
标准化导入
-
标准化内部叠加
-
使用多边形叠加进行比例人口估计
简介
本章重点介绍使用 PostgreSQL 和 PostGIS 功能组合提供的数据结构化方法。这些方法对于结构化和清理导入数据、在输入时将表格数据“即时”转换为空间数据以及使用 PostgreSQL 和 PostGIS 强大组合的功能维护表和数据集之间的关系非常有用。我们将利用这些功能的三个技术类别:使用视图和触发器自动填充和修改数据、使用 PostgreSQL 表继承进行面向对象,以及使用 PostGIS 函数(存储过程)重建和标准化有问题的数据。
自动填充数据是本章的开始。通过利用 PostgreSQL 视图和触发器,我们可以创建灵活的解决方案,在表之间和表中创建连接。通过扩展,对于更正式或结构化的案例,PostgreSQL 提供了表继承和表分区,这允许在表之间显式地建立层次关系。这在对象继承模型强制数据关系以更好地表示数据、从而提高效率或减少随着时间的推移维护和访问数据集的行政负担的情况下非常有用。通过 PostGIS 扩展该功能,继承不仅适用于常用的表属性,还适用于利用表之间的空间关系,从而在非常大的数据集上实现更高的查询效率。最后,我们将探讨 PostGIS SQL 模式,它提供了数据输入的表标准化,因此来自平面文件系统或未标准化的数据集可以转换为我们在数据库中期望的形式。
使用地理空间视图
PostgreSQL 中的视图允许以不同的形式表示数据和数据关系。在本例中,我们将使用视图来允许基于表格输入自动创建点数据。我们可以想象一个案例,其中数据输入流是非空间数据,但包括经纬度或其他坐标。我们希望自动将此数据作为空间中的点显示。
准备工作
我们可以非常容易地创建一个视图来表示空间数据。创建视图的语法与创建表类似,例如:
CREATE VIEW viewname AS
SELECT...
在前面的命令行中,我们的 SELECT 查询为我们操作数据。让我们从一个小的数据集开始。在这种情况下,我们将从一些随机点开始,这些点可能是真实数据。
首先,我们创建一个表,视图将从该表构建,如下所示:
-- Drop the table in case it exists
DROP TABLE IF EXISTS chp02.xwhyzed CASCADE;
CREATE TABLE chp02.xwhyzed
-- This table will contain numeric x, y, and z values
(
x numeric,
y numeric,
z numeric
)
WITH (OIDS=FALSE);
ALTER TABLE chp02.xwhyzed OWNER TO me;
-- We will be disciplined and ensure we have a primary key
ALTER TABLE chp02.xwhyzed ADD COLUMN gid serial;
ALTER TABLE chp02.xwhyzed ADD PRIMARY KEY (gid);
现在,让我们使用以下查询用测试数据填充它:
INSERT INTO chp02.xwhyzed (x, y, z)
VALUES (random()*5, random()*7, random()*106);
INSERT INTO chp02.xwhyzed (x, y, z)
VALUES (random()*5, random()*7, random()*106);
INSERT INTO chp02.xwhyzed (x, y, z)
VALUES (random()*5, random()*7, random()*106);
INSERT INTO chp02.xwhyzed (x, y, z)
VALUES (random()*5, random()*7, random()*106);
如何做...
现在,要创建视图,我们将使用以下查询:
-- Ensure we don't try to duplicate the view
DROP VIEW IF EXISTS chp02.xbecausezed;
-- Retain original attributes, but also create a point attribute from x and y
CREATE VIEW chp02.xbecausezed AS
SELECT x, y, z, ST_MakePoint(x,y)
FROM chp02.xwhyzed;
它是如何工作的...
我们的视图实际上是对现有数据进行简单转换的结果,使用 PostGIS 的ST_MakePoint函数。ST_MakePoint函数接受两个数字作为输入以创建一个 PostGIS 点,在这种情况下,我们的视图简单地使用我们的x和y值来填充数据。每当表中更新以添加包含x和y值的新记录时,视图将填充一个点,这对于不断更新的数据非常有用。
这种方法有两个缺点。第一个缺点是我们没有在视图中声明我们的空间参考系统,因此任何消费这些点的软件将不知道我们使用的坐标系,即它是地理坐标系(纬度/经度)还是平面坐标系。我们将很快解决这个问题。第二个问题是许多访问这些点的软件系统可能不会自动检测并使用表中的空间信息。这个问题在使用触发器填充几何列食谱中得到了解决。
空间参考系统标识符(SRID)允许我们指定给定数据集的坐标系。编号系统是一个简单的整数值,用于指定一个特定的坐标系。SRIDs 最初来源于欧洲石油调查组(EPSG),现在由油气生产者国际协会(OGP)的测绘与定位委员会维护。用于 SRID 的有用工具是空间参考(spatialreference.org)和 Prj2EPSG(prj2epsg.org/search)。
还有更多...
要解决如何工作...部分中提到的第一个问题,我们可以简单地用另一个指定 SRID 为ST_SetSRID的函数包装现有的ST_MakePoint函数,如下面的查询所示:
-- Ensure we don't try to duplicate the view
DROP VIEW IF EXISTS chp02.xbecausezed;
-- Retain original attributes, but also create a point attribute from x and y
CREATE VIEW chp02.xbecausezed AS
SELECT x, y, z, ST_SetSRID(ST_MakePoint(x,y), 3734) -- Add ST_SetSRID
FROM chp02.xwhyzed;
参见
- 使用触发器填充几何列的食谱
使用触发器填充几何列
在这个食谱中,我们假设我们的数据库中数据不断增加,需要空间表示;然而,在这种情况下,我们希望硬编码的几何列在数据库上每次插入时更新,将x和y值转换为几何形状,就像它们被插入到数据库中一样。
这种方法的优势是,几何形状随后被注册在geometry_columns视图中,因此这种方法比创建新的地理空间视图与更多的 PostGIS 客户端类型更可靠。这也提供了允许进行空间索引的优势,这可以显著加快各种查询的速度。
准备工作
我们将首先创建另一个包含 x、y 和 z 值的随机点表格,如下查询所示:
DROP TABLE IF EXISTS chp02.xwhyzed1 CASCADE;
CREATE TABLE chp02.xwhyzed1
(
x numeric,
y numeric,
z numeric
)
WITH (OIDS=FALSE);
ALTER TABLE chp02.xwhyzed1 OWNER TO me;
ALTER TABLE chp02.xwhyzed1 ADD COLUMN gid serial;
ALTER TABLE chp02.xwhyzed1 ADD PRIMARY KEY (gid);
INSERT INTO chp02.xwhyzed1 (x, y, z)
VALUES (random()*5, random()*7, random()*106);
INSERT INTO chp02.xwhyzed1 (x, y, z)
VALUES (random()*5, random()*7, random()*106);
INSERT INTO chp02.xwhyzed1 (x, y, z)
VALUES (random()*5, random()*7, random()*106);
INSERT INTO chp02.xwhyzed1 (x, y, z)
VALUES (random()*5, random()*7, random()*106);
如何做到这一点...
现在我们需要一个几何列来填充。默认情况下,几何列将填充为空值。我们使用以下查询来填充几何列:
SELECT AddGeometryColumn ('chp02','xwhyzed1','geom',3734,'POINT',2);
现在我们有一个名为 geom 的列,其 SRID 为 3734;即二维的点几何类型。由于我们有 x、y 和 z 数据,原则上我们可以使用类似的方法填充一个 3D 点表。
由于所有几何值目前都是空值,我们将使用以下 UPDATE 语句来填充它们:
UPDATE chp02.xwhyzed1
SET the_geom = ST_SetSRID(ST_MakePoint(x,y), 3734);
当分解查询时,这里很简单。我们更新 xwhyzed1 表并使用 ST_MakePoint 设置 the_geom 列,使用 x 和 y 列构建我们的点,并将其包裹在 ST_SetSRID 函数中,以便应用适当的空间参考信息。到目前为止,我们只是设置了表格。现在,我们需要创建一个触发器,以便在表格使用时继续填充这些信息。触发器的第一部分是使用以下查询的新填充几何函数:
CREATE OR REPLACE FUNCTION chp02.before_insertXYZ()
RETURNS trigger AS
$$
BEGIN
if NEW.geom is null then
NEW.geom = ST_SetSRID(ST_MakePoint(NEW.x,NEW.y), 3734);
end if;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
实质上,我们创建了一个函数,它正好做了我们手动做的事情:使用 ST_SetSRID 和 ST_MakePoint 的组合更新表的几何列,但只更新新插入的注册,而不是整个表。
更多...
虽然我们已经创建了一个函数,但我们还没有将其作为触发器应用到表格上。让我们在这里按照以下步骤进行:
CREATE TRIGGER popgeom_insert
BEFORE INSERT ON chp02.xwhyzed1
FOR EACH ROW EXECUTE PROCEDURE chp02.before_insertXYZ();
假设一般的几何列更新尚未发生,那么原始的五个注册仍然在其几何列中为 null。现在,一旦触发器被激活,任何插入到我们的表中的数据都应该包含新的几何记录。让我们使用以下查询进行测试插入:
INSERT INTO chp02.xwhyzed1 (x, y, z)
VALUES (random()*5, random()*7, 106),
(random()*5, random()*7, 107),
(random()*5, random()*7, 108),
(random()*5, random()*7, 109),
(random()*5, random()*7, 110);
使用以下命令检查行以验证 geom 列是否已更新:
SELECT * FROM chp02.xwhyzed1;
或者使用 pgAdmin:

在应用一般更新后,所有注册的 geom 列都将有一个值:

进一步扩展...
到目前为止,我们已经实现了一个 insert 触发器。如果特定行的值发生变化怎么办?在这种情况下,我们将需要一个单独的更新触发器。我们将更改我们的原始函数以测试 UPDATE 的情况,并在我们的触发器中使用 WHEN 来约束对更改的列的更新。
此外,请注意,以下函数是基于用户希望始终根据变化值更新几何形状的假设编写的:
CREATE OR REPLACE FUNCTION chp02.before_insertXYZ()
RETURNS trigger AS
$$
BEGIN
if (TG_OP='INSERT') then
if (NEW.geom is null) then
NEW.geom = ST_SetSRID(ST_MakePoint(NEW.x,NEW.y), 3734);
end if;
ELSEIF (TG_OP='UPDATE') then
NEW.geom = ST_SetSRID(ST_MakePoint(NEW.x,NEW.y), 3734);
end if;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
CREATE TRIGGER popgeom_insert
BEFORE INSERT ON chp02.xwhyzed1
FOR EACH ROW EXECUTE PROCEDURE chp02.before_insertXYZ();
CREATE trigger popgeom_update
BEFORE UPDATE ON chp02.xwhyzed1
FOR EACH ROW
WHEN (OLD.X IS DISTINCT FROM NEW.X OR OLD.Y IS DISTINCT FROM
NEW.Y)
EXECUTE PROCEDURE chp02.before_insertXYZ();
参见
- 使用地理空间视图 的配方
使用表继承结构化空间数据
PostgreSQL 数据库的一个不寻常且有用的特性是它允许对象继承模型,这些模型适用于表。这意味着我们可以在表之间建立父子关系,并利用这一点以有意义的方式组织数据。在我们的例子中,我们将此应用于水文数据。这些数据可以是点、线、多边形或更复杂的结构,但它们有一个共同点:它们在物理上是明确链接的,并且本质上相关;它们都与水有关。水/水文是一个非常好的自然系统,可以以这种方式进行建模,因为我们对它的空间建模方式可以根据比例、细节、数据收集过程以及许多其他因素而大相径庭。
准备中
我们将要使用的数据是经过修改的工程蓝线(见以下截图)的水文数据,即非常详细且旨在用于接近 1:600 比例尺的数据。在原始应用中,这些数据作为断线,有助于详细的数字地形建模。

虽然本身很有用,但这些数据还进一步被操作,将线性特征与区域特征分开,并对区域特征进行了额外的多边形化,如下面的截图所示:

最后,数据被分类到基本水道类别,如下所示:

此外,还进行了一个过程来生成如河流等多边形特征的中心线,这些特征实际上是线性特征,如下所示:

因此,我们有三个相互独立但又相关的数据集:
-
cuyahoga_hydro_polygon -
cuyahoga_hydro_polyline -
cuyahoga_river_centerlines
现在,让我们看看表格数据的结构。从书籍存储库中解压水文文件并进入该目录。ogrinfo实用程序可以帮助我们,如下面的命令所示:
> ogrinfo cuyahoga_hydro_polygon.shp -al -so
输出如下:

在每个形状文件上执行此查询,我们看到以下所有形状文件都通用的字段:
-
name -
hyd_type -
geom_type
通过理解我们的通用字段,我们可以应用继承来完全组织我们的数据。
如何操作...
现在我们知道了我们的通用字段,创建一个继承模型就很容易了。首先,我们将创建一个父表,其中包含所有表共有的字段,使用以下查询:
CREATE TABLE chp02.hydrology (
gid SERIAL PRIMARY KEY,
"name" text,
hyd_type text,
geom_type text,
the_geom geometry
);
如果你注意到了,我们还在其中添加了一个geometry字段,因为所有我们的形状文件都隐含了这种共性。使用继承,任何插入到子表中的记录也将保存在我们的父表中,但这些记录将存储没有为子表指定的额外字段。
为了为给定的表建立继承关系,我们只需要使用以下查询声明子表包含的额外字段:
CREATE TABLE chp02.hydrology_centerlines (
"length" numeric
) INHERITS (chp02.hydrology);
CREATE TABLE chp02.hydrology_polygon (
area numeric,
perimeter numeric
) INHERITS (chp02.hydrology);
CREATE TABLE chp02.hydrology_linestring (
sinuosity numeric
) INHERITS (chp02.hydrology_centerlines);
现在,我们已经准备好使用以下命令加载数据:
-
shp2pgsql -s 3734 -a -i -I -W LATIN1 -g the_geom cuyahoga_hydro_polygon chp02.hydrology_polygon | psql -U me -d postgis_cookbook -
shp2pgsql -s 3734 -a -i -I -W LATIN1 -g the_geom cuyahoga_hydro_polyline chp02.hydrology_linestring | psql -U me -d postgis_cookbook -
shp2pgsql -s 3734 -a -i -I -W LATIN1 -g the_geom cuyahoga_river_centerlines chp02.hydrology_centerlines | psql -U me -d postgis_cookbook
如果我们查看父表,我们将看到所有子表中的所有记录。以下是对hydrology字段的一个截图:

将它与hydrology_linestring中可用的字段进行比较,将揭示感兴趣的特定字段:

它是如何工作的...
PostgreSQL 表继承允许我们在表之间强制执行基本层次关系。在这种情况下,我们利用继承来允许相关数据集之间的共性。现在,如果我们想查询这些表中的数据,我们可以直接从父表查询,如下所示,具体取决于我们是否想要混合几何形状或只是针对特定数据集:
SELECT * FROM chp02.hydrology
从任何子表,我们可以使用以下查询:
SELECT * FROM chp02.hydrology_polygon
参见
可以通过结合使用CHECK约束和继承来扩展这个概念,以利用和优化存储和查询。有关更多信息,请参阅扩展继承 – 表分区配方。
扩展继承 – 表分区
表分区是特定于 PostgreSQL 的一种方法,它扩展了继承,用于模型化通常在可用字段上没有差异的表。但是,子表代表基于各种因素的数据的逻辑分区,无论是时间、值范围、分类,还是在我们的案例中,空间关系。分区的好处包括由于索引较小和针对数据的扫描而提高的查询性能,以及绕过真空操作成本的批量加载和删除。因此,可以将常用数据放在更快、更昂贵的存储上,而将剩余数据放在较慢、更便宜的存储上。与 PostGIS 结合使用,我们获得了空间分区的新颖功能,这对于大型数据集来说是一个非常强大的特性。
准备工作
我们可以举出许多大型数据集的例子,这些数据集可以从分区中受益。在我们的案例中,我们将使用等高线数据集。等高线是表示地形数据的有用方式,因为它们已经建立并且因此通常被解释。等高线还可以将地形数据压缩成线性表示,从而使其能够与其他数据一起轻松显示。
问题在于,轮廓数据的存储可能相当昂贵。一个美国县区的两英尺轮廓可能需要 20 到 40 GB,而对于更大区域,如地区或国家,存储此类数据可能从性能的角度来看变得相当有约束力。
如何操作...
在这个案例中,第一步可能是准备数据。如果我们有一个名为 cuy_contours_2 的单一轮廓表,我们可以选择将数据裁剪成一系列矩形,这些矩形将作为我们的表分区;在这种情况下,使用以下查询的 chp02.contour_clip:
CREATE TABLE chp02.contour_2_cm_only AS
SELECT contour.elevation, contour.gid, contour.div_10, contour.div_20, contour.div_50,
contour.div_100, cc.id, ST_Intersection(contour.the_geom, cc.the_geom) AS the_geom FROM
chp02.cuy_contours_2 AS contour, chp02.contour_clip as cc
WHERE ST_Within(contour.the_geom,cc.the_geom
OR
ST_Crosses(contour.the_geom,cc.the_geom);
在这里,我们在查询中执行了两个测试。我们使用了 ST_Within,它测试一个给定的轮廓是否完全位于我们的兴趣区域内。如果是这样,我们执行交集;结果几何形状应该是轮廓的几何形状。
ST_Crosses 函数检查轮廓是否穿过我们正在测试的几何形状的边界。这应该会捕获所有部分位于我们区域内部和外部几何形状。这些是我们将真正相交以获得结果形状的几何形状。
在我们的案例中,这样做更容易,我们不需要这一步。我们的轮廓形状已经是裁剪到矩形边界的独立形状文件,如下面的截图所示:

由于数据已经被裁剪成我们分区所需的块,我们只需继续创建适当的分区。
与继承类似,我们首先使用以下查询创建父表:
CREATE TABLE chp02.contours
(
gid serial NOT NULL,
elevation integer,
__gid double precision,
the_geom geometry(MultiLineStringZM,3734),
CONSTRAINT contours_pkey PRIMARY KEY (gid)
)
WITH (
OIDS=FALSE
);
在这里,我们再次保持约束,如 PRIMARY KEY,并指定几何类型(MultiLineStringZM),并不是因为这些会传播到子表中,而是为了让任何访问父表的客户端软件能够预测这些约束。
现在,我们可以开始创建继承自父表的新表。在这个过程中,我们将创建一个 CHECK 约束,指定我们相关几何形状的限制,如下所示:
CREATE TABLE chp02.contour_N2260630
(CHECK
(ST_CoveredBy(the_geom,ST_GeomFromText
('POLYGON((2260000, 630000, 2260000 635000, 2265000 635000,
2265000 630000, 2260000 630000))',3734)
)
)) INHERITS (chp02.contours);
我们可以使用与剩余表类似的 CREATE TABLE 查询来完成表结构的分区,如下所示:
CREATE TABLE chp02.contour_N2260635
(CHECK
(ST_CoveredBy(the_geom,ST_GeomFromText
('POLYGON((2260000 635000, 2260000 640000,
2265000 640000, 2265000 635000, 2260000 635000))', 3734)
)
)) INHERITS (chp02.contours);
CREATE TABLE chp02.contour_N2260640
(CHECK
(ST_CoveredBy(the_geom,ST_GeomFromText
('POLYGON((2260000 640000, 2260000 645000, 2265000 645000,
2265000 640000, 2260000 640000))', 3734)
)
)) INHERITS (chp02.contours);
CREATE TABLE chp02.contour_N2265630
(CHECK
(ST_CoveredBy(the_geom,ST_GeomFromText
('POLYGON((2265000 630000, 2265000 635000, 2270000 635000,
2270000 630000, 2265000 630000))', 3734)
)
)) INHERITS (chp02.contours);
CREATE TABLE chp02.contour_N2265635
(CHECK
(ST_CoveredBy(the_geom,ST_GeomFromText
('POLYGON((2265000 635000, 2265000 640000, 2270000 640000,
2270000 635000, 2265000 635000))', 3734)
)
)) INHERITS (chp02.contours);
CREATE TABLE chp02.contour_N2265640
(CHECK
(ST_CoveredBy(the_geom,ST_GeomFromText
('POLYGON((2265000 640000, 2265000 645000, 2270000 645000,
2270000 640000, 2265000 640000))', 3734)
)
)) INHERITS (chp02.contours);
CREATE TABLE chp02.contour_N2270630
(CHECK
(ST_CoveredBy(the_geom,ST_GeomFromText
('POLYGON((2270000 630000, 2270000 635000, 2275000 635000,
2275000 630000, 2270000 630000))', 3734)
)
)) INHERITS (chp02.contours);
CREATE TABLE chp02.contour_N2270635
(CHECK
(ST_CoveredBy(the_geom,ST_GeomFromText
('POLYGON((2270000 635000, 2270000 640000, 2275000 640000,
2275000 635000, 2270000 635000))', 3734)
)
)) INHERITS (chp02.contours);
CREATE TABLE chp02.contour_N2270640
(CHECK
(ST_CoveredBy(the_geom,ST_GeomFromText
('POLYGON((2270000 640000, 2270000 645000, 2275000 645000,
2275000 640000, 2270000 640000))', 3734)
)
)) INHERITS (chp02.contours);
现在,我们可以使用以下命令将 contours1 ZIP 文件中找到的轮廓形状文件加载到每个子表中,通过替换文件名。如果我们想的话,我们甚至可以在父表上实现一个触发器,将每个插入操作放入正确的子表中,尽管这可能会影响性能:
shp2pgsql -s 3734 -a -i -I -W LATIN1 -g the_geom N2265630 chp02.contour_N2265630 | psql -U me -d postgis_cookbook
它是如何工作的...
在这种情况下,结合继承的 CHECK 约束是构建表分区所需的所有内容。我们正在使用边界框作为 CHECK 约束,并简单地从父表继承列。现在我们已经设置了这些,对父表的查询将首先检查我们的 CHECK 约束,然后再执行查询。
这也允许我们将任何我们较少使用的等高线表放置在更便宜、更慢的存储上,从而允许对大型数据集进行成本效益的优化。这种结构对于快速变化的数据也有益,因为可以对整个区域应用更新;该区域的整个表可以有效地删除并重新填充,而无需遍历整个数据集。
参见
更多关于表继承的信息,特别是与子表中使用备用列相关的灵活性,请参阅之前的配方,使用表继承结构化空间数据。
规范化导入
通常,空间数据库中使用的数据是从其他来源导入的。因此,它可能不是我们当前应用有用的形式。在这种情况下,编写辅助函数将数据转换成对我们应用更有用的形式可能是有用的。这尤其适用于从平面文件格式,如 Shapefile,到关系数据库,如 PostgreSQL 的转换。
Shapefile 是一种事实上的以及格式规范,用于存储空间数据,可能是空间数据最常用的交付格式。尽管名为 Shapefile,但它绝不是单个文件,而是一组文件的集合。它至少包含*.shp(包含几何信息)、*.shx(索引文件)和*.dbf(包含 Shapefile 的表格信息)。这是一个强大且有用的格式,但作为一个平面文件,它本质上是非关系的。每个几何体与表格中的每一行都有一个一对一的关系。
有许多结构可能作为 Shapefile 中关系存储的代理。在这里我们将探讨其中一个:一个字段,用于分隔多个关系。这是将多个关系编码到平面文件中的一种不太常见的技巧。另一种常见的方法是创建多个字段来存储在关系安排中本应是一个单一字段的内容。
准备工作
我们将要处理的数据集是一个公园系统中一系列小径的线性范围数据集。这些数据是典型的 GIS 世界中的数据;作为一个平面 Shapefile,数据中没有显式的关系结构。
首先,解压trails.zip文件,然后使用命令行进入它,然后使用以下命令加载数据:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom trails chp02.trails | psql -U me -d postgis_cookbook
观察线性数据,我们有关于用途类型的几个类别:

我们希望保留这些信息以及名称。不幸的是,label_name字段是一个混乱的字段,其中包含各种相关名称,通过和符号(&)连接,如下面的查询所示:
SELECT DISTINCT label_name FROM chp02.trails
WHERE label_name LIKE '%&%' LIMIT 10;
它将返回以下输出:

这是我们开始表规范化的地方。
如何做到...
我们需要做的第一件事是找到所有没有&的字段,并使用这些作为我们可用的独特路径列表。在我们的案例中,我们可以这样做,因为每个路径至少有一个唯一命名的段,并且不与另一个路径名称相关联。这种方法并不适用于所有数据集,所以在应用此方法之前要仔细理解你的数据。
要选择没有&的排序字段,我们使用以下查询:
SELECT DISTINCT label_name, res
FROM chp02.trails
WHERE label_name NOT LIKE '%&%'
ORDER BY label_name, res;
它将返回以下输出:

接下来,我们想要搜索所有匹配这些独特路径名称的记录。这将给我们一个记录列表,这些记录将作为关系。进行此搜索的第一步是在我们的独特列表中添加百分号(%)以构建一个可以使用LIKE查询进行搜索的字符串:
SELECT '%' || label_name || '%' AS label_name, label_name as label, res FROM
(SELECT DISTINCT label_name, res
FROM chp02.trails
WHERE label_name NOT LIKE '%&%'
ORDER BY label_name, res
) AS label;
最后,我们将在WITH块中使用这个来执行规范化本身。这将为我们提供第一列中每个段唯一的 ID 表,以及相关的label列。为了保险起见,我们将以CREATE TABLE过程的形式执行此操作,如下查询所示:
CREATE TABLE chp02.trails_names AS WITH labellike AS
(
SELECT '%' || label_name || '%' AS label_name, label_name as label, res FROM
(SELECT DISTINCT label_name, res
FROM chp02.trails
WHERE label_name NOT LIKE '%&%'
ORDER BY label_name, res
) AS label
)
SELECT t.gid, ll.label, ll.res
FROM chp02.trails AS t, labellike AS ll
WHERE t.label_name LIKE ll.label_name
AND
t.res = ll.res
ORDER BY gid;
如果我们查看创建的表的第一个行,trails_names,我们使用pgAdmin得到以下输出:

现在我们有了关系表,我们需要一个与gid关联的几何表。相比之下,这相当简单,如下查询所示:
CREATE TABLE chp02.trails_geom AS
SELECT gid, the_geom
FROM chp02.trails;
它是如何工作的...
在这个例子中,我们生成了一组可能的唯一记录列表,并与关联记录的搜索一起构建表关系。在一个表中,我们有每个空间记录的几何和唯一 ID;在另一个表中,我们有与每个唯一 ID 关联的名称。现在我们可以明确地利用这些关系。
首先,我们需要建立我们独特的 ID 作为主键,如下所示:
ALTER TABLE chp02.trails_geom ADD PRIMARY KEY (gid);
现在,我们可以使用以下查询在trails_names表中将其作为FOREIGN KEY的PRIMARY KEY:
ALTER TABLE chp02.trails_names ADD FOREIGN KEY (gid) REFERENCES chp02.trails_geom(gid);
这一步并非绝对必要,但它确实强制执行了以下查询等查询的引用完整性:
SELECT geo.gid, geo.the_geom, names.label FROM
chp02.trails_geom AS geo, chp02.trails_names AS names
WHERE geo.gid = names.gid;
输出如下:

还有更多...
如果我们想要规范化多个字段,我们可以为每个字段编写CREATE TABLE查询。
值得注意的是,本食谱中提出的方法不仅限于我们有分隔字段的情况。这种方法可以提供一个相对通用的解决方案来规范化平面文件的问题。例如,如果我们有一个多个字段来表示关系信息的案例,例如label1、label2、label3或类似的多属性名称,我们可以编写一个简单的查询将它们连接起来,然后再将信息输入到我们的查询中。
规范化内部叠加
来自外部源的数据可能在表结构以及拓扑结构上存在问题,这是地理空间数据本身的固有属性。以具有重叠多边形的数据问题为例。如果我们的数据集中有多边形与内部重叠,那么对面积、周长和其他指标的查询可能不会产生可预测或一致的结果。
有几种方法可以解决具有内部重叠的多边形数据集的问题。这里提出的一般方法最初是由Refractions Research的凯文·纽菲尔德提出的。
在编写查询的过程中,我们还将提供一个将多边形转换为线字符串的解决方案。
准备工作
首先,解压use_area.zip文件,然后使用命令行进入它;接着,使用以下命令加载数据集:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom cm_usearea_polygon chp02.use_area | psql -U me -d postgis_cookbook
如何做到这一点...
现在数据已加载到数据库中的表中,我们可以利用 PostGIS 来展平和获取多边形的并集,以便我们有一个规范化的数据集。使用这种方法的第一步将是将多边形转换为线字符串。然后我们可以连接这些线字符串并将它们转换回多边形,表示所有多边形输入的并集。我们将执行以下任务:
-
将多边形转换为线字符串
-
将线字符串转换回多边形
-
找到结果多边形的中心点
-
使用结果点来查询表格关系
要将多边形转换为线字符串,我们需要使用ST_ExteriorRing提取我们想要的多边形部分,使用ST_DumpPoints将这些部分转换为点,然后使用ST_MakeLine将这些点连接成线,就像连接点彩图一样。
进一步分解,ST_ExteriorRing (the_geom)将仅获取我们多边形的边界。但是ST_ExteriorRing返回多边形,因此我们需要将输出转换为线。最简单的方法是使用ST_DumpPoints将其转换为点,然后连接这些点。默认情况下,Dump函数返回一个名为geometry_dump的对象,它不仅仅是简单的几何形状,而是几何形状与整数数组的组合。最简单的方法是利用对象符号来提取geometry_dump的几何部分,如下所示:
(ST_DumpPoints(geom)).geom
使用ST_ExteriorRing将几何形状重新拼接起来,可以使用以下查询完成:
SELECT (ST_DumpPoints(ST_ExteriorRing(geom))).geom
这应该会给出一个点列表,按照从我们想要使用ST_MakeLine构建线的所有点的外部环的顺序排列,如下查询所示:
SELECT ST_MakeLine(geom) FROM (
SELECT (ST_DumpPoints(ST_ExteriorRing(geom))).geom) AS linpoints
由于前面的方法是我们可能在许多其他地方想要使用的过程,因此可能明智地使用以下查询创建一个函数:
CREATE OR REPLACE FUNCTION chp02.polygon_to_line(geometry)
RETURNS geometry AS
$BODY$
SELECT ST_MakeLine(geom) FROM (
SELECT (ST_DumpPoints(ST_ExteriorRing((ST_Dump($1)).geom))).geom
) AS linpoints
$BODY$
LANGUAGE sql VOLATILE;
ALTER FUNCTION chp02.polygon_to_line(geometry)
OWNER TO me;
现在我们有了polygon_to_line函数,我们仍然需要强制连接我们特定情况下的重叠线。ST_Union函数将有助于此,如下查询所示:
SELECT ST_Union(the_geom) AS geom FROM (
SELECT chp02.polygon_to_line(geom) AS geom FROM
chp02.use_area
) AS unioned;
现在让我们将线字符串转换回多边形,为此我们可以使用ST_Polygonize函数来多边形化结果,如下查询所示:
SELECT ST_Polygonize(geom) AS geom FROM (
SELECT ST_Union(the_geom) AS geom FROM (
SELECT chp02.polygon_to_line(geom) AS geom FROM
chp02.use_area
) AS unioned
) as polygonized;
ST_Polygonize函数将创建一个单一的多边形,所以如果我们想对它做任何有用的操作,我们需要将其分解成多个单一的多边形几何体。同时,我们还可以在CREATE TABLE语句中做以下操作:
CREATE TABLE chp02.use_area_alt AS (
SELECT (ST_Dump(the_geom)).geom AS the_geom FROM (
SELECT ST_Polygonize(the_geom) AS the_geom FROM (
SELECT ST_Union(the_geom) AS the_geom FROM (
SELECT chp02.polygon_to_line(the_geom) AS the_geom
FROM chp02.use_area
) AS unioned
) as polygonized
) AS exploded
);
我们将对这个几何体执行空间查询,因此我们应该创建一个索引以确保我们的查询性能良好,如下查询所示:
CREATE INDEX chp02_use_area_alt_the_geom_gist
ON chp02.use_area_alt
USING gist(the_geom);
为了从原始几何体中找到适当的表信息并将其应用到我们的结果几何体上,我们将执行点在多边形内查询。为此,我们首先需要在结果几何体上计算质心:
CREATE TABLE chp02.use_area_alt_p AS
SELECT ST_SetSRID(ST_PointOnSurface(the_geom), 3734) AS
the_geom FROM
chp02.use_area_alt;
ALTER TABLE chp02.use_area_alt_p ADD COLUMN gid serial;
ALTER TABLE chp02.use_area_alt_p ADD PRIMARY KEY (gid);
并且,一如既往地,使用以下查询创建空间索引:
CREATE INDEX chp02_use_area_alt_p_the_geom_gist
ON chp02.use_area_alt_p
USING gist(the_geom);
然后,质心将根据以下查询在原始表格信息和结果多边形之间建立点在多边形内(ST_Intersects)的关系:
CREATE TABLE chp02.use_area_alt_relation AS
SELECT points.gid, cu.location FROM
chp02.use_area_alt_p AS points,
chp02.use_area AS cu
WHERE ST_Intersects(points.the_geom, cu.the_geom);
如果我们查看表的前几行,我们可以看到它将点的标识符与其相应的位置联系起来:

它是如何工作的...
我们的基本方法是查看几何体的底层拓扑,并重建一个非重叠的拓扑,然后使用这个新几何体的质心来构建一个查询,以建立与原始数据的关系。
还有更多...
在这个阶段,我们可以选择使用外键建立引用完整性框架,如下所示:
ALTER TABLE chp02.use_area_alt_relation ADD FOREIGN KEY (gid) REFERENCES chp02.use_area_alt_p (gid);
使用多边形叠加进行比例人口普查估计
PostgreSQL 提供了许多用于表格数据聚合的函数,包括sum、count、min、max等。作为框架的 PostGIS 并没有这些函数的空间等价物,但这并不妨碍我们使用 PostgreSQL 的聚合函数与 PostGIS 的空间功能一起构建函数。
在这个菜谱中,我们将探索使用美国人口普查数据的空间汇总。美国人口普查数据本质上就是汇总数据。这是有意为之,以保护公民的隐私。但当涉及到使用这些数据进行分析时,数据的汇总性质可能会成为问题。有一些技巧可以分解数据。其中最简单的一种是使用比例和,我们将在这次练习中这样做。
准备工作
当前的问题是,为了向公众提供服务,已经绘制了一条提议的路径。这个例子可以适用于道路建设,甚至可以用于寻找商业地产的地点,以提供服务。
首先,解压trail_census.zip文件,然后使用以下命令从解压的文件夹中快速加载数据:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom census chp02.trail_census | psql -U me -d postgis_cookbook
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom trail_alignment_proposed_buffer chp02.trail_buffer | psql -U me -d postgis_cookbook
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom trail_alignment_proposed chp02.trail_alignment_prop | psql -U me -d postgis_cookbook
上述命令将产生以下输出:

如果我们在我们最喜欢的桌面 GIS 中查看拟议的小径,我们会看到以下内容:

在我们的案例中,我们想要了解距离小径 1 英里范围内的居民数量,假设居住在距离小径 1 英里范围内的居民最有可能使用它,因此最有可能被它服务。
为了找出这个拟议小径附近的人口,我们在人口普查街区组人口密度信息上叠加。下一个截图展示了拟议小径周围的 1 英里缓冲区:

关于这些人口普查数据,我们可能会注意到人口密度和人口普查街区组大小的广泛范围。计算人口的一种方法就是简单地选择所有与我们的区域相交的街区,如下面的截图所示:

这是一个简单的程序,它给我们提供了一个估计值,即距离小径 1 英里范围内居住的 130 到 288 人,但通过观察选择的形状,我们可以看到,我们在估计中采用了完整的街区,因此高估了人口数量。
同样,如果我们只使用其质心位于我们拟议的小径 1 英里范围内的街区组,我们就会低估人口数量。
相反,我们将做出一些有用的假设。街区组被设计为在街区组人口分布内适度同质。假设这一点适用于我们的数据,我们可以假设对于给定的街区组,如果街区组的 50%位于我们的目标区域内,我们可以将那个街区组的一半人口归入我们的估计。将这一点应用于所有我们的街区组,将它们相加,我们就得到了一个更精确的估计,这比纯交集或质心查询更有可能准确。因此,我们采用比例求和。
如何操作...
由于比例求和问题是一个通用问题,它可能适用于许多问题。我们将把底层比例作为函数来编写。函数接受输入并返回一个值。在我们的案例中,我们希望我们的比例函数接受两个几何形状作为输入,即我们缓冲小径的几何形状以及街区组以及我们想要比例的值,我们希望它返回比例后的值:
CREATE OR REPLACE FUNCTION chp02.proportional_sum(geometry, geometry, numeric)
RETURNS numeric AS
$BODY$
-- SQL here
$BODY$
LANGUAGE sql VOLATILE;
现在,为了我们的计算目的,对于缓冲区域和街区组的任何给定交集,我们想要找到交集占整个街区组的比例。然后这个值应该乘以我们想要缩放的值。
在 SQL 中,函数看起来像以下查询:
SELECT $3 * areacalc FROM
(SELECT (ST_Area(ST_Intersection($1, $2)) / ST_Area($2)):: numeric AS areacalc
) AS areac;
上述查询的完整形式如下:
CREATE OR REPLACE FUNCTION chp02.proportional_sum(geometry, geometry, numeric)
RETURNS numeric AS
$BODY$
SELECT $3 * areacalc FROM
(SELECT (ST_Area(ST_Intersection($1, $2))/ST_Area($2))::numeric AS areacalc
) AS areac
;
$BODY$
LANGUAGE sql VOLATILE;
它是如何工作的...
由于我们将查询编写为一个函数,查询使用SELECT语句遍历所有可用记录,并给出一个按比例的人口。敏锐的读者会注意到,我们还没有进行任何汇总工作;我们只处理了问题的比例部分。我们可以在调用函数时使用 PostgreSQL 的内置聚合函数来进行汇总。这种方法的优点在于,我们不仅需要应用求和,还可以计算其他聚合,如最小值或最大值。在下面的示例中,我们将只应用求和:
SELECT ROUND(SUM(chp02.proportional_sum(a.the_geom, b.the_geom, b.pop))) FROM
chp02.trail_buffer AS a, chp02.trail_census as b
WHERE ST_Intersects(a.the_geom, b.the_geom)
GROUP BY a.gid;
返回的值相当不同(人口为 96,081),这更有可能是准确的。
第三章:处理矢量数据 - 基础
在本章中,我们将介绍以下菜谱:
-
使用 GPS 数据
-
修复无效几何形状
-
使用空间连接进行 GIS 分析
-
简化几何形状
-
测量距离
-
使用公共属性合并多边形
-
计算交集
-
将几何形状裁剪以部署数据
-
使用 PostGIS 拓扑简化几何形状
简介
在本章中,你将使用一组 PostGIS 函数和矢量数据集。你首先将了解如何使用 PostGIS 与 GPS 数据交互——你将使用 ogr2ogr 导入此类数据集,然后使用 ST_MakeLine 函数从点几何形状中组合折线。
然后,你将看到 PostGIS 如何帮助你使用诸如 ST_MakeValid、ST_IsValid、ST_IsValidReason 和 ST_IsValidDetails 等函数查找和修复无效的几何形状。
然后,你将了解空间数据库中最强大的元素之一,空间连接。PostGIS 为你提供了一组丰富的运算符,例如 ST_Intersects、ST_Contains、ST_Covers、ST_Crosses 和 ST_DWithin,用于此目的。
之后,你将使用 ST_Simplify 和 ST_SimplifyPrevergeTopology 函数简化(泛化)几何形状,当你不需要太多细节时。虽然此函数在线性几何形状上表现良好,但对于多边形可能会引入拓扑异常。在这种情况下,你应该考虑使用外部 GIS 工具,如 GRASS。
然后,你将游览 PostGIS 函数以进行距离测量——ST_Distance、ST_DistanceSphere 和 ST_DistanceSpheroid 都在途中。
本章中解释的一个菜谱将指导你通过典型的 GIS 工作流程根据公共属性合并多边形;你将使用 ST_Union 函数来完成此目的。
然后,你将学习如何使用 ST_Intersection 函数裁剪几何形状,在最后介绍的第 2.0 版本中深入探讨 PostGIS 拓扑。
使用 GPS 数据
在这个菜谱中,你将使用 GPS 数据。这类数据通常保存在 .gpx 文件中。你将从流行的跑步者社交网络 RunKeeper 导入一系列 .gpx 文件到 PostGIS。
如果你有一个 RunKeeper 账户,你可以导出你的 .gpx 文件,并按照本菜谱中的说明进行处理。否则,你可以使用位于代码包中 chp03 目录下的 runkeeper-gpx.zip 文件中包含的 RunKeeper .gpx 文件。
你首先将创建一个 bash 脚本,使用 ogr2ogr 将 .gpx 文件导入到 PostGIS 表中。导入完成后,你将尝试编写一些 SQL 查询,并测试一些非常有用的函数,例如 ST_MakeLine 从点几何形状生成折线,ST_Length 计算距离,以及 ST_Intersects 执行空间连接操作。
准备工作
将 data/chp03/runkeeper-gpx.zip 文件解压到 working/chp03/runkeeper_gpx。如果你还没有通过 第一章,在 PostGIS 中移动数据进和出,确保 countries 数据集在 PostGIS 数据库中。
如何操作...
首先,确保你需要的 .gpx 文件格式正确。打开其中一个并检查文件结构——每个文件必须由一个 <trk> 元素组成,该元素包含一个 <trkseg> 元素,该元素包含多个 <trkpt> 元素(从跑步者的 GPS 设备存储的点)。将这些点导入到 PostGIS 的 Point 表中:
- 使用以下命令创建一个名为
chp03的新模式,用于存储本章中所有食谱的数据:
postgis_cookbook=# create schema chp03;
- 在 PostgreSQL 中通过执行以下命令行创建
chp03.rk_track_points表:
postgis_cookbook=# CREATE TABLE chp03.rk_track_points
(
fid serial NOT NULL,
the_geom geometry(Point,4326),
ele double precision,
"time" timestamp with time zone,
CONSTRAINT activities_pk PRIMARY KEY (fid)
);
- 创建以下脚本,使用 GDAL 的
ogr2ogr命令将chp03.rk_track_points表中的所有.gpx文件导入:
以下是在 Linux 上的版本(命名为 working/chp03/import_gpx.sh):
#!/bin/bash
for f in `find runkeeper_gpx -name \*.gpx -printf "%f\n"`
do
echo "Importing gpx file $f to chp03.rk_track_points
PostGIS table..." #, ${f%.*}"
ogr2ogr -append -update -f PostgreSQL
PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" runkeeper_gpx/$f
-nln chp03.rk_track_points
-sql "SELECT ele, time FROM track_points"
done
以下是在 macOS 上的命令(命名为 working/chp03/import_gpx.sh):
#!/bin/bash
for f in `find runkeeper_gpx -name \*.gpx `
do
echo "Importing gpx file $f to chp03.rk_track_points
PostGIS table..." #, ${f%.*}"
ogr2ogr -append -update -f PostgreSQL
PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" $f
-nln chp03.rk_track_points
-sql "SELECT ele, time FROM track_points"
done
以下是在 Windows 上的版本(命名为 working/chp03/import_gpx.bat):
@echo off
for %%I in (runkeeper_gpx\*.gpx*) do (
echo Importing gpx file %%~nxI to chp03.rk_track_points
PostGIS table...
ogr2ogr -append -update -f PostgreSQL
PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" runkeeper_gpx/%%~nxI
-nln chp03.rk_track_points
-sql "SELECT ele, time FROM track_points"
)
- 在 Linux 和 macOS 中,在运行之前别忘了为脚本分配执行权限。然后,运行以下脚本:
$ chmod 775 import_gpx.sh
$ ./import_gpx.sh
Importing gpx file 2012-02-26-0930.gpx to chp03.rk_track_points
PostGIS table...
Importing gpx file 2012-02-29-1235.gpx to chp03.rk_track_points
PostGIS table...
...
Importing gpx file 2011-04-15-1906.gpx to chp03.rk_track_points
PostGIS table...
在 Windows 中,只需双击 .bat 文件或从命令提示符使用以下命令运行它:
> import_gpx.bat
- 现在,使用
ST_MakeLine函数创建一个包含单个跑步者轨迹详情的多线表。假设跑步者在每个不同的日子只进行了一次训练。在这个表中,你应该包括轨迹详情的开始和结束时间,如下所示:
postgis_cookbook=# SELECT
ST_MakeLine(the_geom) AS the_geom,
run_date::date,
MIN(run_time) as start_time,
MAX(run_time) as end_time
INTO chp03.tracks
FROM (
SELECT the_geom,
"time"::date as run_date,
"time" as run_time
FROM chp03.rk_track_points
ORDER BY run_time
) AS foo GROUP BY run_date;
- 在查询创建的表之前,别忘了为两个表都添加空间索引以提高其性能,如下所示:
postgis_cookbook=# CREATE INDEX rk_track_points_geom_idx
ON chp03.rk_track_points USING gist(the_geom);
postgis_cookbook=# CREATE INDEX tracks_geom_idx
ON chp03.tracks USING gist(the_geom);
-
如果你在任何给定的一天尝试在一个桌面 GIS 上打开这两个空间表,你应该会看到
rk_track_points表中的点在tracks表中组成一个单独的多线几何记录,如下面的截图所示:![图片]()
-
如果你在一个桌面 GIS(如 QGIS)中打开所有轨迹,你会看到以下内容:

- 现在,查询
tracks表以获取跑步者每月跑步的总距离报告(单位:公里)。为此,使用ST_Length函数,如下所示:
postgis_cookbook=# SELECT
EXTRACT(year FROM run_date) AS run_year,
EXTRACT(MONTH FROM run_date) as run_month,
SUM(ST_Length(geography(the_geom)))/1000 AS distance
FROM chp03.tracks
GROUP BY run_year, run_month ORDER BY run_year, run_month;

(28 rows)
- 通过在
tracks和countries表之间进行空间连接,并再次使用ST_Length函数,如下所示,你可以得到跑步者按国家计算的跑步距离报告(单位:公里):
postgis_cookbook=# SELECT
c.country_name,
SUM(ST_Length(geography(t.the_geom)))/1000 AS run_distance
FROM chp03.tracks AS t
JOIN chp01.countries AS c
ON ST_Intersects(t.the_geom, c.the_geom)
GROUP BY c.country_name
ORDER BY run_distance DESC;

(4 rows)
它是如何工作的...
.gpx 文件存储所有点的详情在 WGS 84 空间参考系中;因此,我们创建了具有 SRID(4326)的 rk_track_points 表。
在创建 rk_track_points 表之后,我们使用 bash 脚本导入了 runkeeper_gpx 目录中的所有 .gpx 文件。该 bash 脚本迭代 runkeeper_gpx 目录中所有扩展名为 *.gpx 的文件。对于这些文件中的每一个,脚本运行 ogr2ogr 命令,使用 GPX GDAL 驱动程序将 .gpx 文件导入到 PostGIS 中(更多详情,请参阅 www.gdal.org/drv_gpx.html)。
在 GDAL 的抽象中,一个 .gpx 文件是一个由几个层组成的 OGR 数据源,如下所示:

在 .gpx 文件(OGR 数据源)中,你只有 tracks 和 track_points 层。作为一个快捷方式,你可以使用 ogr2ogr 只导入 tracks 层,但你需要从 track_points 层开始,以便使用一些 PostGIS 函数生成 tracks 层本身。这就是为什么在 bash 脚本中的 ogr2ogr 部分中,我们将来自 track_points 层的点几何形状以及一些有用的属性(如 elevation 和 timestamp)导入到 rk_track_points PostGIS 表中。
一旦导入记录,我们使用子查询向名为 tracks 的新多段线表中输入数据,并从 rk_track_points 表中选择了所有点几何形状及其日期和时间,按日期分组,并使用 ST_MakeLine 函数对几何形状进行聚合。此函数能够从点几何形状创建线字符串(更多详情,请参阅 www.postgis.org/docs/ST_MakeLine.html)。
你不应该忘记在子查询中按 datetime 对点进行排序;否则,你将获得一个不规则的线字符串,从一个点到另一个点跳跃,并且不遵循正确的顺序。
在加载 tracks 表之后,我们测试了两个空间查询。
首先,你得到了跑步者每月跑步总距离的报告。为此目的,你选择了按日期(年和月)分组的所有轨迹记录,通过将单条轨迹的长度(使用 ST_Length 函数获得)相加来获得总距离。要从 run_date 函数中获取年和月,你使用了 PostgreSQL 的 EXTRACT 函数。请注意,如果你使用 WGS 84 系统中的几何形状来测量距离,你将获得以度为单位的结果。因此,你必须将几何形状投影到一个为特定区域设计的平面度量系统中,该区域的数据将被投影。
对于大规模区域,例如在我们的案例中,我们有一些跨越整个欧洲的点,如最后查询结果所示,一个好的选择是使用 PostGIS 1.5 中引入的 geography 数据类型。计算可能会慢一些,但比其他系统更准确。这就是为什么在测量之前将几何形状转换为 geography 数据类型的原因。
最后一个空间查询使用了 ST_Intersects 函数进行空间连接,以获取跑步者每条跑步轨迹所在国家的名称(假设跑步者没有跑过跨国轨迹)。获取每个国家跑步的总距离只需对 country_name 字段的选择进行聚合,并使用 PostgreSQL 的 SUM 操作符对轨迹距离进行聚合。
修复无效几何形状
您通常会在您的 PostGIS 数据库中找到无效的几何形状。这些无效的几何形状可能会损害 PostGIS 本身以及任何使用它的外部工具(如 QGIS 和 MapServer)的功能。作为符合 OGC 简单特征规范的系统,PostGIS 必须管理和处理有效的几何形状。
幸运的是,PostGIS 2.0 为您提供了 ST_MakeValid 函数,该函数与 ST_IsValid、ST_IsValidReason 和 ST_IsValidDetails 函数一起,是检查和修复数据库中几何形状的理想工具包。在本教程中,您将学习如何修复无效几何形状的常见情况。
准备工作
将 data/TM_WORLD_BORDERS-0.3.zip 文件解压到您的当前工作目录 working/chp3 中,然后使用 shp2pgsql 命令将 shapefile 导入到 PostGIS 中,如下所示:
$ shp2pgsql -s 4326 -g the_geom -W LATIN1 -I TM_WORLD_BORDERS-0.3.shp chp03.countries > countries.sql
$ psql -U me -d postgis_cookbook -f countries.sql
该文件还包含 wborders 名称,因为对于某些操作系统,它不能与 TM_WORLD_BORDERS-0.3.shp 中的字符一起使用。
如何操作...
完成此教程所需执行的步骤如下:
- 首先,调查导入的表中是否有任何几何形状无效。正如您在以下查询中可以看到的,使用
ST_IsValid和ST_IsValidReason函数,我们找到了四个无效的几何形状,它们都是由于相同的理由——环自相交:
postgis_cookbook=# SELECT gid, name, ST_IsValidReason(the_geom)
FROM chp03.countries
WHERE ST_IsValid(the_geom)=false;

(4 rows)
- 现在,只关注一个无效的几何形状,例如,在表示俄罗斯的 multipolygon 几何形状中。创建一个只包含产生无效性的环的表,使用上一步
ST_IsValidReason响应中给出的点坐标选择该表:
postgis_cookbook=# SELECT * INTO chp03.invalid_geometries FROM (
SELECT 'broken'::varchar(10) as status,
ST_GeometryN(the_geom, generate_series(
1, ST_NRings(the_geom)))::geometry(Polygon,4326)
as the_geom FROM chp03.countries
WHERE name = 'Russia') AS foo
WHERE ST_Intersects(the_geom,
ST_SetSRID(ST_Point(143.661926,49.31221), 4326));
ST_MakeValid 函数需要 GEOS 3.3.0 或更高版本;您可以使用以下 PostGIS_full_version 函数来检查您的系统是否支持它:

- 现在,使用
ST_MakeValid函数,在之前创建的表中添加一个包含相同几何形状有效版本的记录:
postgis_cookbook=# INSERT INTO chp03.invalid_geometries
VALUES ('repaired', (SELECT ST_MakeValid(the_geom)
FROM chp03.invalid_geometries));
- 在您的桌面 GIS 中打开此几何形状;无效的几何形状只有一个自相交的环,在内部产生一个洞。虽然这在 ESRI shapefile 格式规范中是可接受的(这是您最初导入的数据集),但 OGC 标准不允许自相交的环,因此 PostGIS 也不允许:

- 现在,在
invalid_geometries表中,你有多边形的无效和有效版本。很容易看出,自相交的环是通过ST_MakeValid移除的,它向原始多边形添加了一个补充环,从而根据 OGC 标准生成了一个有效的几何形状:
postgis_cookbook=# SELECT status, ST_NRings(the_geom)
FROM chp03.invalid_geometries;

(2 rows)
- 现在你已经确定了问题和解决方案,不要忘记通过执行以下代码修复
countries表中的所有其他无效几何形状:
postgis_cookbook=# UPDATE chp03.countries
SET the_geom = ST_MakeValid(the_geom)
WHERE ST_IsValid(the_geom) = false;
一种聪明的做法是在数据库中完全不保留无效几何形状,即在表上添加一个CHECK约束来检查有效性。这将增加更新或插入新几何形状时的计算时间,但会保持你的数据集有效。例如,在countries表中,可以按以下方式实现:
ALTER TABLE chp03.countries
ADD CONSTRAINT geometry_valid_check
CHECK (ST_IsValid(the_geom));
在实际应用中,很多时候你需要移除这样的约束,以便能够从不同的来源导入记录。在用ST_MakeValid函数进行验证后,你可以安全地再次添加该约束。
它是如何工作的...
有许多原因可能导致数据库中出现无效几何形状;例如,由多边形组成的环必须是闭合的,不能自相交或与另一个环共享超过一个点。
在使用ST_IsValid和ST_IsValidReason函数导入country形状文件后,你会发现有四个导入的几何形状是无效的,所有这些都是因为它们的多边形有自相交的环。
在这一点上,一个很好的方法是通过对多边形进行分解,检查其组成部分的环,来调查无效的多边形几何形状。为此,我们使用ST_GeometryN函数导出了导致无效性的环的几何形状,该函数能够从多边形中提取第n个环。我们将此函数与有用的 PostgreSQL generate_series函数相结合,迭代组成几何形状的所有环,并使用ST_Intersects函数选择所需的环。
如预期的那样,这个环产生无效性的原因是它是自相交的,并在多边形中产生了一个洞。虽然这符合形状文件规范,但它不符合 OGC 规范。
通过运行 ST_MakeValid 函数,PostGIS 已经能够使几何有效,生成第二个环。请注意,ST_MakeValid 函数仅在最新版本的 PostGIS(编译了最新 GEOS(3.3.0+))中可用。如果这不是你的工作环境设置,并且你无法升级(升级始终是推荐的!),你可以遵循过去使用的技术,并在保罗·拉姆齐(Paul Ramsey)在 blog.opengeo.org/2010/09/08/tips-for-the-postgis-power-user/ 上讨论的非常受欢迎、优秀的演示中找到这些技术。
使用空间连接进行 GIS 分析
对于常规 SQL 表的连接具有关系数据库的实际功能,而空间连接是像 PostGIS 这样的空间数据库引擎中最令人印象深刻的特性之一。
基本上,可以根据输入图层中每个特征的几何关系来关联不同图层的信息。在本教程中,我们将探讨一些空间连接的常见用例。
准备工作
-
首先,将一些数据导入到 PostGIS 中作为测试平台。从美国地质调查局网站
earthquake.usgs.gov/earthquakes/eqarchives/epic/kml/2012_Earthquakes_ALL.kmz下载包含 2012 年全球地震信息的.kmz文件。将其保存在working/chp03目录中(或者,你也可以使用本书附带代码包中包含的该文件副本)。 -
.kmz文件是由 ZIP 压缩器打包的.kml文件集合。因此,在解压文件后(你可能需要将.kmz文件扩展名更改为.zip),你可能会注意到它仅由一个.kml文件组成。这个文件在 GDAL 抽象中,构成了一个由九个不同图层组成的 OGR KML 数据源,包含 3D 点几何形状。每个图层包含不同地震震级的地震数据:
$ ogrinfo 2012_Earthquakes_ALL.kml
此操作的输出如下:

- 通过使用
ogr2ogr命令执行以下脚本之一,同时将所有这些图层导入名为chp03.earthquakes的 PostGIS 表中。
以下为 Linux 版本(命名为 import_eq.sh):
#!/bin/bash
for ((i = 1; i < 9 ; i++)) ; do
echo "Importing earthquakes with magnitude $i
to chp03.earthquakes PostGIS table..."
ogr2ogr -append -f PostgreSQL -nln chp03.earthquakes
PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" 2012_Earthquakes_ALL.kml
-sql "SELECT name, description, CAST($i AS integer)
AS magnitude FROM \"Magnitude $i\""
done
以下为 Windows 版本(命名为 import_eq.bat):
@echo off
for /l %%i in (1, 1, 9) do (
echo "Importing earthquakes with magnitude %%i
to chp03.earthquakes PostGIS table..."
ogr2ogr -append -f PostgreSQL -nln chp03.earthquakes
PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" 2012_Earthquakes_ALL.kml
-sql "SELECT name, description, CAST(%%i AS integer)
AS magnitude FROM \"Magnitude %%i\""
)
- 执行以下脚本(对于 Linux,你需要给它添加
execute权限):
$ chmod 775 import_eq.sh
$ ./import_eq.sh
Importing earthquakes with magnitude 1 to chp03.earthquakes
PostGIS table...
Importing earthquakes with magnitude 2 to chp03.earthquakes
PostGIS table...
...
- 为了与本书的约定保持一致,将几何列
wkb_geometry(ogr2ogr中的默认几何输出名称)重命名为the_geom,如下命令所示:
postgis_cookbook=# ALTER TABLE chp03.earthquakes
RENAME wkb_geometry TO the_geom;
- 从
nationalmap.gov/网站下载美国的cities形状文件,地址为dds.cr.usgs.gov/pub/data/nationalatlas/citiesx020_nt00007.tar.gz(此存档也包含在此书提供的代码包中),并在 PostGIS 中通过执行以下代码导入:
$ ogr2ogr -f PostgreSQL -s_srs EPSG:4269 -t_srs EPSG:4326
-lco GEOMETRY_NAME=the_geom -nln chp03.cities
PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" citiesx020.shp
- 从
nationalmap.gov/网站下载美国的states形状文件,地址为dds.cr.usgs.gov/pub/data/nationalatlas/statesp020_nt00032.tar.gz(此存档也包含在此书提供的代码包中),并在 PostGIS 中通过执行以下代码导入:
$ ogr2ogr -f PostgreSQL -s_srs EPSG:4269 -t_srs EPSG:4326
-lco GEOMETRY_NAME=the_geom -nln chp03.states -nlt MULTIPOLYGON
PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" statesp020.shp
如何做到这一点...
在这个菜谱中,您将亲自看到通过使用空间连接解决一系列典型问题来发挥空间 SQL 的力量:
- 首先,查询 PostGIS 以获取 2012 年按州划分的注册地震数量:
postgis_cookbook=# SELECT s.state, COUNT(*) AS hq_count
FROM chp03.states AS s
JOIN chp03.earthquakes AS e
ON ST_Intersects(s.the_geom, e.the_geom)
GROUP BY s.state
ORDER BY hq_count DESC;

(33 rows)
- 现在,为了使问题稍微复杂一些,查询 PostGIS 以获取距离美国拥有超过 100 万居民的城镇不超过 200 公里的地震数量,按震级分组;执行以下代码:
postgis_cookbook=# SELECT c.name, e.magnitude, count(*) as hq_count
FROM chp03.cities AS c
JOIN chp03.earthquakes AS e
ON ST_DWithin(geography(c.the_geom), geography(e.the_geom), 200000)
WHERE c.pop_2000 > 1000000
GROUP BY c.name, e.magnitude
ORDER BY c.name, e.magnitude, hq_count;

(18 rows)
- 作为之前查询的一个变体,执行以下代码将给出地震的完整列表以及它们与城市的距离(以米为单位):
postgis_cookbook=# SELECT c.name, e.magnitude,
ST_Distance(geography(c.the_geom), geography(e.the_geom))
AS distance FROM chp03.cities AS c
JOIN chp03.earthquakes AS e
ON ST_DWithin(geography(c.the_geom), geography(e.the_geom), 200000)
WHERE c.pop_2000 > 1000000
ORDER BY distance;

(488 rows)
- 现在,通过执行以下代码请求 PostGIS 获取每个州的城镇数量和总人口:
postgis_cookbook-# SELECT s.state, COUNT(*)
AS city_count, SUM(pop_2000) AS pop_2000
FROM chp03.states AS s
JOIN chp03.cities AS c
ON ST_Intersects(s.the_geom, c.the_geom)
WHERE c.pop_2000 > 0 -- NULL values is -9999 on this field!
GROUP BY s.state
ORDER BY pop_2000 DESC;

(51 rows)
- 作为最后的测试,使用空间连接更新现有表。您需要将
state_fips字段中的信息添加到earthquake表中的states表。首先,为了存储此类信息,您需要创建一个列,如下命令所示:
postgis_cookbook-# ALTER TABLE chp03.earthquakes
ADD COLUMN state_fips character varying(2);
- 然后,您可以使用空间连接更新新列,如下所示:
postgis_cookbook-# UPDATE chp03.earthquakes AS e
SET state_fips = s.state_fips
FROM chp03.states AS s
WHERE ST_Intersects(s.the_geom, e.the_geom);
它是如何工作的...
空间连接是释放 PostGIS 空间功能的关键特性之一。对于常规连接,可以通过公共字段将两个不同表中的实体相关联。对于空间连接,可以使用任何空间关系函数(如ST_Contains、ST_Covers、ST_Crosses和ST_DWithin)将两个不同的空间表中的要素相关联。
在第一个查询中,我们使用了ST_Intersects函数将地震点与其相应的州连接起来。我们通过state列对查询进行分组,以获得州内的地震数量。
在第二个查询中,我们使用了ST_DWithin函数将每个城市与其 200 公里范围内的地震点相关联。我们过滤掉了人口少于 100 万的城市,并按城市名称和地震震级分组,以获取每个城市和震级的地震数量报告。
第三个查询与第二个查询类似,但不是按城市和震级分组。距离是通过ST_Distance函数计算的。请注意,由于特征坐标存储在 WGS 84 中,您需要将几何列转换为椭球体,并使用椭球体来获取以米为单位的距离。或者,您可以使用ST_Transform函数将几何形状投影到一个平面坐标系中,该坐标系适用于我们在此菜谱中研究的区域(在这种情况下,ESPG:2163,US National Atlas Equal Area将是一个不错的选择)。然而,在处理像我们在此菜谱中处理的大面积区域时,将几何体转换为地理类型通常是最佳选择,因为它提供了更准确的结果。
第四个查询使用了ST_Intersects函数。在这种情况下,我们按state列分组,并使用两个聚合 SQL 函数(SUM和COUNT)来获取所需的结果。
最后,在最后一个查询中,您使用空间连接的结果更新一个空间表。这个概念与上一个查询类似,但它是UPDATE SQL 命令的上下文。
简化几何形状
在许多情况下,您将需要生成一个更详细和更轻量级的矢量数据集版本,因为您可能不需要非常详细的特征,可能有几个原因。考虑一下您将要发布数据集到网站,并且性能是一个关注点的情况,或者也许您需要将数据集部署给一个不需要太多细节的同事,因为他们正在使用它来创建大面积地图。在这些所有情况下,GIS 工具都包括从给定数据集中减少不必要细节的实现简化算法。基本上,这些算法通过一定容差减少包含在其中的顶点数,该容差以距离单位表示。
为了这个目的,PostGIS 为您提供了ST_Simplify和ST_SimplifyPreserveTopology函数。在许多情况下,它们是简化任务的正确解决方案,但在某些情况下,特别是对于多边形特征,它们可能不是最佳选择,您可能需要使用不同的 GIS 工具,如GRASS或新的 PostGIS 拓扑支持。
如何操作...
完成此菜谱所需采取的步骤如下:
- 使用以下代码设置 PostgreSQL
search_path变量,以便所有您新创建的数据库对象都将存储在chp03模式中:
postgis_cookbook=# SET search_path TO chp03,public;
- 假设您需要一个更详细版本的
states层用于您的映射网站或部署给客户端;您可以考虑使用以下ST_SimplifyPreserveTopology函数:
postgis_cookbook=# CREATE TABLE states_simplify_topology AS
SELECT ST_SimplifyPreserveTopology(ST_Transform(
the_geom, 2163), 500) FROM states;
- 之前的命令运行迅速,使用的是 Douglas-Peucker 算法的变体,并有效地减少了顶点数量。但有时,生成的多边形不再相邻。如果你放大查看任何多边形边界,你应该会注意到以下截图所示的内容:两个多边形共享边界的共有部分存在孔洞和重叠。这是因为 PostGIS 使用的是 OGC 简单特征模型,该模型不实现拓扑,所以该函数只是移除了冗余顶点,而没有考虑相邻的多边形:

- 看起来
ST_SimplifyPreserveTopology函数在处理线性特征时运行良好,但在处理多边形时会产生拓扑异常。如果你想要进行拓扑简化,另一种方法是以下由 Paul Ramsey 提出的代码(gis.stackexchange.com/questions/178/simplifying-adjacent-polygons)以及在 Webspaces 博客文章中改进的代码(webspaces.net.nz/page.php?view=polygon-dissolve-and-generalise):
SET search_path TO chp03, public;
-- first project the spatial table to a planar system
(recommended for simplification operations)
CREATE TABLE states_2163 AS SELECT ST_Transform
(the_geom, 2163)::geometry(MultiPolygon, 2163)
AS the_geom, state FROM states;
-- now decompose the geometries from multipolygons to polygons (2895)
using the ST_Dump function
CREATE TABLE polygons AS SELECT (ST_Dump(the_geom)).geom AS the_geom
FROM states_2163;
-- now decompose from polygons (2895) to rings (3150)
using the ST_DumpRings function
CREATE TABLE rings AS SELECT (ST_DumpRings(the_geom)).geom
AS the_geom FROM polygons;
-- now decompose from rings (3150) to linestrings (3150)
using the ST_Boundary function
CREATE TABLE ringlines AS SELECT(ST_boundary(the_geom))
AS the_geom FROM rings;
-- now merge all linestrings (3150) in a single merged linestring
(this way duplicate linestrings at polygon borders disappear)
CREATE TABLE mergedringlines AS SELECT ST_Union(the_geom)
AS the_geom FROM ringlines;
-- finally simplify the linestring with a tolerance of 150 meters
CREATE TABLE simplified_ringlines AS SELECT
ST_SimplifyPreserveTopology(the_geom, 150)
AS the_geom FROM mergedringlines;
-- now compose a polygons collection from the linestring
using the ST_Polygonize function
CREATE TABLE simplified_polycollection AS SELECT
ST_Polygonize(the_geom) AS the_geom FROM simplified_ringlines;
-- here you generate polygons (2895) from the polygons collection
using ST_Dumps
CREATE TABLE simplified_polygons AS SELECT
ST_Transform((ST_Dump(the_geom)).geom,
4326)::geometry(Polygon,4326)
AS the_geom FROM simplified_polycollection;
-- time to create an index, to make next operations faster
CREATE INDEX simplified_polygons_gist ON simplified_polygons
USING GIST (the_geom);
-- now copy the state name attribute from old layer with a spatial
join using the ST_Intersects and ST_PointOnSurface function
CREATE TABLE simplified_polygonsattr AS SELECT new.the_geom,
old.state FROM simplified_polygons new, states old
WHERE ST_Intersects(new.the_geom, old.the_geom)
AND ST_Intersects(ST_PointOnSurface(new.the_geom), old.the_geom);
-- now make the union of all polygons with a common name
CREATE TABLE states_simplified AS SELECT ST_Union(the_geom)
AS the_geom, state FROM simplified_polygonsattr GROUP BY state;
-
这种方法看起来运行顺畅,但如果尝试将简化容差从 150 米增加到,比如说,500 米,你将再次遇到拓扑异常(你可以亲自测试一下)。更好的方法是使用 PostGIS 拓扑(你将在 使用 PostGIS 拓扑简化几何形状 章节中这样做)或能够像
GRASS一样管理拓扑操作的第三方 GIS 工具。对于这个配方,你将使用GRASS方法。- 如果你还没有安装
GRASS,请在你的系统上安装它。然后,创建一个目录来包含GRASS数据库(在GRASS术语中,称为GISDBASE),如下所示:
$ mkdir grass_db-
现在,通过在 Linux 命令提示符中输入
grass或者在 Windows 中双击 GRASS GUI 图标(开始 | 所有程序 | OSGeo4W | GRASS GIS 6.4.3 | GRASS 6.4.3 GUI)或在 macOS 中的应用程序中,来启动GRASS。你将被提示选择grass_db数据库作为 GIS 数据目录,但应该选择你在上一步中创建的目录。-
使用“位置向导”按钮,创建一个名为
postgis_cookbook的位置,标题为“PostGIS 烹饪书”(GRASS使用名为位置的子目录,其中所有数据都保存在相同的坐标系、地图投影和地理边界中)。 -
在创建新位置时,选择 EPSG 2163 作为空间参考系统(你需要在“选择创建新位置的方法”下选择“选择空间参考系统的 EPSG 代码”选项)。
-
现在通过点击“启动 GRASS”按钮来启动
GRASS。程序的控制台将如以下截图所示启动:![图片]()
-
将
statesPostGIS 空间表导入GRASS位置。为此,使用v.in.ogr GRASS命令,它将使用 OGR PostgreSQL 驱动程序(实际上,PostGIS 连接字符串语法是相同的):
GRASS 6.4.1 (postgis_cookbook):~ > v.in.ogr input=PG:"dbname='postgis_cookbook' user='me' password='mypassword'" layer=chp03.states_2163 out=statesGRASS将导入 OGR PostGIS 表,并同时为该层构建拓扑,该层由点、线、区域等组成。可以使用v.info命令结合-c选项来检查属性表并获取有关导入层的更多信息,如下所示:
GRASS 6.4.1 (postgis_cookbook):~ > v.info states![]()
- 现在,你可以使用具有 500 米容差(阈值)的
v.generalizeGRASS命令简化多边形几何形状。如果你使用与本配方相同的数据集,你将最终从原始的 346,914 个顶点中获得 47,191 个顶点,组成 1,919 个多边形(区域),这些多边形来自原始的 2,895 个多边形:
GRASS 6.4.1 (postgis_cookbook):~ > v.generalize input=states output=states_generalized_from_grass method=douglas threshold=500- 使用
v.out.ogr命令(v.in.ogr的对应命令)将结果导回 PostGIS,如下所示:
GRASS 6.4.1 (postgis_cookbook):~ > v.out.ogr input=states_generalized_from_grass type=area dsn=PG:"dbname='postgis_cookbook' user='me' password='mypassword'" olayer=chp03.states_simplified_from_grass format=PostgreSQL- 现在,打开桌面 GIS,检查
ST_SimplifyPreserveTopologyPostGIS 函数和GRASS执行的几何形状简化之间的差异。在共享多边形边界处不应有孔洞或重叠。在下面的屏幕截图中,原始层边界用红色表示,由ST_SimplifyPreserveTopology构建的边界用蓝色表示,由GRASS构建的边界用绿色表示:![]()
它是如何工作的...
ST_SimplifyPostGIS 函数能够使用 Douglas-Peucker 算法(更多详情请参阅en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm)简化并泛化(简单或多)线性或多边形几何形状。由于在某些情况下它可能创建无效的几何形状,因此建议使用其进化版本——ST_SimplifyPreserveTopology函数,这将仅产生有效的几何形状。虽然这些函数与(多)线性几何形状配合得很好,但在(多)多边形的情况下,它们很可能会在共享多边形边界处创建拓扑异常,如重叠和孔洞。
要在编写时获得有效的、拓扑简化的数据集,有以下两种选择:
-
在外部 GIS 工具(如
GRASS)上执行简化过程 -
使用新的 PostGIS 拓扑支持
虽然你将在“使用 PostGIS 拓扑简化几何形状”的配方中看到新的 PostGIS 拓扑特征,但在本配方中,你已经使用
GRASS来执行简化过程。我们打开了
GRASS,创建了一个 GIS 数据目录和一个项目位置,然后使用v.ogr.in命令将多边形 PostGIS 表导入GRASS位置,正如其名称所暗示的那样,基于 GDAL/OGR。到目前为止,你一直在使用
GRASS v.generalize命令,通过以米为单位的容差(阈值)来执行数据集的简化。在简化数据集后,你使用
v.ogr.out GRASS命令将其导入 PostGIS,然后在桌面 GIS 中打开派生的空间表,以查看该过程是否以拓扑正确的方式进行。测量距离
在这个菜谱中,我们将检查用于距离测量的 PostGIS 函数(
ST_Distance及其变体),并找出考虑地球曲率在测量远距离点之间距离时会产生重大差异。准备工作
你应该导入表示我们之前在菜谱中生成的美国城市的 shapefile(名为
chp03.cities的 PostGIS 表)。如果你还没有这样做,请从nationalmap.gov/网站下载该 shapefile(dds.cr.usgs.gov/pub/data/nationalatlas/citiesx020_nt00007.tar.gz),并将其导入 PostGIS:$ ogr2ogr -f PostgreSQL -s_srs EPSG:4269 -t_srs EPSG:4326 -lco GEOMETRY_NAME=the_geom -nln chp03.cities PG:"dbname='postgis_cookbook' user='me' password='mypassword'" citiesx020.shp如何操作...
完成此菜谱所需执行的步骤如下:
- 首先,使用
ST_Distance函数,通过球面墨卡托平面投影坐标系(EPSG:900913, EPSG:3857, 或 EPSG:3785;所有这些 SRID 表示都是等效的)来计算美国拥有超过 100 万人口的城市的距离。如果你想得到以米为单位的距离结果,可以使用以下ST_Transform函数将点坐标从经纬度度数(因为坐标最初在 EPSG:4326 中)转换为平面度量系统:
postgis_cookbook=# SELECT c1.name, c2.name, ST_Distance(ST_Transform(c1.the_geom, 900913), ST_Transform(c2.the_geom, 900913))/1000 AS distance_900913 FROM chp03.cities AS c1 CROSS JOIN chp03.cities AS c2 WHERE c1.pop_2000 > 1000000 AND c2.pop_2000 > 1000000 AND c1.name < c2.name ORDER BY distance_900913 DESC;![图片]()
(36 rows)- 现在请像我们在上一个菜谱中做的那样,但使用一个更紧凑的表达式,通过 PostgreSQL 公共表表达式(CTE)来编写相同的查询:
WITH cities AS ( SELECT name, the_geom FROM chp03.cities WHERE pop_2000 > 1000000 ) SELECT c1.name, c2.name, ST_Distance(ST_Transform(c1.the_geom, 900913), ST_Transform(c2.the_geom, 900913))/1000 AS distance_900913 FROM cities c1 CROSS JOIN cities c2 where c1.name < c2.name ORDER BY distance_900913 DESC;- 对于像这种情况的大距离,使用平面空间参考系是不正确的,但你应该考虑地球的曲率来进行计算。例如,之前使用的墨卡托平面系统,虽然非常适合用于地图输出,但在测量距离和面积时非常糟糕,因为它评估方向。为此,最好使用能够测量距离的空间参考系。你也可以使用
ST_Distance_Sphere或ST_Distance_Spheroid函数(第一个更快,但精度较低,因为它在球体而不是椭球体上执行计算)。更好的选择是将几何形状转换为地理数据类型,这样你就可以直接使用ST_Distance,因为它将自动使用椭球体进行计算。请注意,这与使用ST_DistanceSpheroid完全等效。尝试检查使用相同查询的各种方法之间的差异:
WITH cities AS ( SELECT name, the_geom FROM chp03.cities WHERE pop_2000 > 1000000 ) SELECT c1.name, c2.name, ST_Distance(ST_Transform(c1.the_geom, 900913), ST_Transform(c2.the_geom, 900913))/1000 AS d_900913, ST_Distance_Sphere(c1.the_geom, c2.the_geom)/1000 AS d_4326_sphere, ST_Distance_Spheroid(c1.the_geom, c2.the_geom, 'SPHEROID["GRS_1980",6378137,298.257222101]')/1000 AS d_4326_spheroid, ST_Distance(geography(c1.the_geom), geography(c2.the_geom))/1000 AS d_4326_geography FROM cities c1 CROSS JOIN cities c2 where c1.name < c2.name ORDER BY d_900913 DESC;![]()
(36 rows)- 您可以从输出结果中轻松验证,使用平面系统(EPSG:900913,如在
d_900913列中)而不是考虑地球曲率的系统有很大的不同。
它是如何工作的...
如果您需要计算两点之间的最小笛卡尔距离,可以使用 PostGIS 的
ST_Distance函数。此函数接受两个几何形状作为输入参数,并且这些几何形状必须在相同的空间参考系统中指定。如果两个输入几何形状使用不同的空间参考,您可以使用
ST_Transform函数对其中一个或两个进行操作,使它们与单个空间参考系统保持一致。为了获得更好的结果,您应该考虑地球的曲率,这在测量大距离时是强制性的,并使用
ST_Distance_Sphere或ST_Distance_Spheroid函数。或者,使用ST_Distance,但将输入几何形状转换为地理空间数据类型,该类型针对此类操作进行了优化。地理类型将几何形状存储在 WGS 84 经纬度度数中,但它始终以米为单位返回测量值。在此食谱中,您使用了 PostgreSQL CTE,这是一种在主查询上下文中提供子查询的便捷方式。您可以将 CTE 视为仅在主查询范围内使用的临时表。
使用公共属性合并多边形
在 GIS 工作流程中,有许多情况需要根据公共属性合并多边形数据集。一个典型的例子是从Nomenclature des Units Territoriales Statistiques(NUTS)第 4 级开始合并欧洲行政区域(您可以在
en.wikipedia.org/wiki/Nomenclature_of_Territorial_Units_for_Statistics中看到),直到 NUTS 第 1 级,使用 NUTS 代码合并,或者使用州代码合并美国县图层以获得州图层。PostGIS 允许您使用
ST_Union函数执行此类处理操作。准备工作
从
nationalmap.gov/网站下载美国国家形状文件,地址为dds.cr.usgs.gov/pub/data/nationalatlas/co2000p020_nt00157.tar.gz(此存档也包含在此书提供的代码包中),并在 PostGIS 中按照以下步骤导入:$ ogr2ogr -f PostgreSQL -s_srs EPSG:4269 -t_srs EPSG:4326 -lco GEOMETRY_NAME=the_geom -nln chp03.counties -nlt MULTIPOLYGON PG:"dbname='postgis_cookbook' user='me' password='mypassword'" co2000p020.shp如何操作...
完成此食谱所需的步骤如下:
- 首先,通过运行以下命令检查导入的表:
postgis_cookbook=# SELECT county, fips, state_fips FROM chp03.counties ORDER BY county;![]()
(6138 rows)- 现在根据
state_fips字段执行合并操作,使用 PostGIS 的ST_Union函数:
postgis_cookbook=# CREATE TABLE chp03.states_from_counties AS SELECT ST_Multi(ST_Union(the_geom)) as the_geom, state_fips FROM chp03.counties GROUP BY state_fips;- 以下截图显示了桌面 GIS 中输出 PostGIS 层的外观;聚合县已成功由各自的州组成(由粗蓝色边框表示):
![]()
它是如何工作的...
你一直在使用
ST_UnionPostGIS 函数在公共属性上执行多边形合并。此函数可以用作聚合 PostgreSQL 函数(如SUM、COUNT、MIN和MAX)在层的几何字段上,使用GROUP BY子句中的公共属性。注意,
ST_Union也可以用作非聚合函数,以执行两个几何体(即两个输入参数)的并集。计算交集
一个典型的 GIS 地理处理工作流程是计算由相交线性几何体产生的交集。
PostGIS 提供了一套丰富的函数来解决这类特定问题,你将在本配方中查看它们。
准备工作
对于这个配方,我们将使用北美和欧洲的河流 + 湖泊中心线数据集,比例尺为 1:10m。从以下
naturalearthdata.com网站下载rivers数据集(或使用本书提供的代码包中包含的 ZIP 文件):或者在其网站上找到它:
www.naturalearthdata.com/downloads/10m-physical-vectors/将 shapefile 解压到工作目录
chp03/working中。使用以下shp2pgsql命令将 shapefile 导入 PostGIS:$ shp2pgsql -I -W LATIN1 -s 4326 -g the_geom ne_10m_rivers_lake_centerlines.shp chp03.rivers > rivers.sql $ psql -U me -d postgis_cookbook -f rivers.sql如何做到这一点...
你需要执行以下步骤来完成这个配方:
- 首先,在
MultiLineString数据集和 PostGIS 的ST_Intersects函数之间执行自空间连接,并使用ST_IntersectionPostGIS 函数在连接上下文中找到交集。以下是一个基本查询,结果选择了 1,448 条记录:
postgis_cookbook=# SELECT r1.gid AS gid1, r2.gid AS gid2, ST_AsText(ST_Intersection(r1.the_geom, r2.the_geom)) AS the_geom FROM chp03.rivers r1 JOIN chp03.rivers r2 ON ST_Intersects(r1.the_geom, r2.the_geom) WHERE r1.gid != r2.gid;-
你可能会草率地假设所有交集都是单点,但这并非事实;如果你使用
ST_GeometryType函数检查几何交集的几何类型,你将遇到三种不同的交集情况,导致以下几何体:- 对于两个线性几何体之间的简单交集,使用
ST_POINT几何体。
- 对于两个线性几何体之间的简单交集,使用
-
-
如果两个线性几何体在多个点上相交,则使用
ST_MultiPoint几何体。 -
在两个
MultiLineString对象相交并共享部分线的情况下,使用ST_GeometryCollection几何体。在这种情况下,几何体集合由ST_Point和/或ST_Line几何体组成。
-
- 你可以通过以下查询检查不同的案例:
postgis_cookbook=# SELECT COUNT(*), ST_GeometryType(ST_Intersection(r1.the_geom, r2.the_geom)) AS geometry_type FROM chp03.rivers r1 JOIN chp03.rivers r2 ON ST_Intersects(r1.the_geom, r2.the_geom) WHERE r1.gid != r2.gid GROUP BY geometry_type;![]()
(3 rows)- 首先,尝试仅计算前两种情况(由
ST_Point和ST_MultiPoint几何形状组成的交点)的交点。只需生成一个包含Point和MultiPoint几何形状的表,排除具有由几何集合组成的交点的记录。通过执行以下命令,1,444 条记录中的 1,448 条被导入(使用ST_GeometryType函数忽略了具有几何集合的四个记录):
postgis_cookbook=# CREATE TABLE chp03.intersections_simple AS SELECT r1.gid AS gid1, r2.gid AS gid2, ST_Multi(ST_Intersection(r1.the_geom, r2.the_geom))::geometry(MultiPoint, 4326) AS the_geom FROM chp03.rivers r1 JOIN chp03.rivers r2 ON ST_Intersects(r1.the_geom, r2.the_geom) WHERE r1.gid != r2.gid AND ST_GeometryType(ST_Intersection(r1.the_geom, r2.the_geom)) != 'ST_GeometryCollection';- 如果您想从几何集合中导入点(但仅限于点,忽略可能的线字符串),一种方法是在
SELECT``CASEPostgreSQL 条件语句的上下文中使用ST_CollectionExtract函数;这样,您可以按照以下方式导入所有 1,448 个交点:
postgis_cookbook=# CREATE TABLE chp03.intersections_all AS SELECT gid1, gid2, the_geom::geometry(MultiPoint, 4326) FROM ( SELECT r1.gid AS gid1, r2.gid AS gid2, CASE WHEN ST_GeometryType(ST_Intersection(r1.the_geom, r2.the_geom)) != 'ST_GeometryCollection' THEN ST_Multi(ST_Intersection(r1.the_geom, r2.the_geom)) ELSE ST_CollectionExtract(ST_Intersection(r1.the_geom, r2.the_geom), 1) END AS the_geom FROM chp03.rivers r1 JOIN chp03.rivers r2 ON ST_Intersects(r1.the_geom, r2.the_geom) WHERE r1.gid != r2.gid ) AS only_multipoints_geometries;- 您可以通过以下方式查看两种过程之间的差异,计算生成的每个表中点的总数,如下所示:
postgis_cookbook=# SELECT SUM(ST_NPoints(the_geom)) FROM chp03.intersections_simple; --2268 points per 1444 records postgis_cookbook=# SELECT SUM(ST_NPoints(the_geom)) FROM chp03.intersections_all; --2282 points per 1448 records- 在以下屏幕截图(来自 QGIS)中,您可能会注意到两种方法生成的交点。在
intersection_all层的情况下,您会注意到计算出了更多的交点(以红色显示):![]()
在 QGIS 中可视化的河流交点层
它是如何工作的...
我们一直在使用线性 PostGIS 空间层的自空间连接来查找由该层特征生成的交点。
为了生成自空间连接,我们使用了
ST_Intersects函数。这样,我们发现所有特征在其各自的几何形状中至少有一个交点。在相同的自空间连接上下文中,我们使用
ST_Intersection函数找到了交点。问题在于计算出的交点并不总是单个点。实际上,两条相交的线可以产生一个单点几何形状(
ST_Point)的起点,如果两条线仅相交一次。但是,两条相交的线可以产生一个点集合(ST_MultiPoint)或甚至几何集合的起点,如果两条线在更多点上相交/共享公共部分。由于我们的目标是使用
ST_GeometryType函数计算所有点交点(ST_Point和ST_MultiPoint),我们使用 SQLSELECT CASE构造过滤了值,其中特征具有GeometryCollection几何形状,我们使用ST_CollectionExtract函数(参数类型=1)从组成集合中提取了仅有的点(而不是可能的线字符串)。最后,我们比较了两个结果集,既使用纯 SQL 又使用桌面 GIS。计算出的交点从输出几何形状中过滤掉了几何集合,并从所有由交点生成的几何形状中计算出的交点,包括
GeometryCollection特征。剪切几何形状以部署数据
一个常见的 GIS 应用场景是将大型数据集裁剪成小部分(子集),每个子集可能代表一个感兴趣的区域。在这个菜谱中,您将从表示世界河流的 PostGIS 层导出,每个国家都有一个独特的 shapefile 组成河流。为此,您将使用
ST_Intersection函数。准备工作
确保您已将用于上一个菜谱的相同河流数据集(一个 shapefile)导入到 PostGIS 中。
如何操作...
完成此菜谱所需的步骤如下:
- 首先,您将创建一个视图,使用
ST_Intersection和ST_Intersects函数裁剪每个国家的河流几何形状。将视图命名为rivers_clipped_by_country:
postgis_cookbook=> CREATE VIEW chp03.rivers_clipped_by_country AS SELECT r.name, c.iso2, ST_Intersection(r.the_geom, c.the_geom)::geometry(Geometry,4326) AS the_geom FROM chp03.countries AS c JOIN chp03.rivers AS r ON ST_Intersects(r.the_geom, c.the_geom);- 按照以下步骤创建名为
rivers的目录:
mkdir working/chp03/rivers- 创建以下脚本以为每个国家导出
riversshapefile。
以下为 Linux 版本(命名为
export_rivers.sh):#!/bin/bash for f in `ogrinfo PG:"dbname='postgis_cookbook' user='me' password='mypassword'" -sql "SELECT DISTINCT(iso2) FROM chp03.countries ORDER BY iso2" | grep iso2 | awk '{print $4}'` do echo "Exporting river shapefile for $f country..." ogr2ogr rivers/rivers_$f.shp PG:"dbname='postgis_cookbook' user='me' password='mypassword'" -sql "SELECT * FROM chp03.rivers_clipped_by_country WHERE iso2 = '$f'" done以下为 Windows 版本(命名为
export_rivers.bat):FOR /F "tokens=*" %%f IN ('ogrinfo PG:"dbname=postgis_cookbook user=me password=password" -sql "SELECT DISTINCT(iso2) FROM chp03.countries ORDER BY iso2" ^| grep iso2 ^| gawk "{print $4}"') DO ( echo "Exporting river shapefile for %%f country..." ogr2ogr rivers/rivers_%%f.shp PG:"dbname='postgis_cookbook' user='me' password='password'" -sql "SELECT * FROM chp03.rivers_clipped_by_country WHERE iso2 = '%%f'" )对于 Windows 用户
该脚本使用
grep和awkLinux 命令,因此您需要从unxutils.sourceforge.net/下载它们的 Windows 版本。脚本已使用UnxUpdates.zip文件(包含gawk但不包含awk)进行测试,但您也可以下载sourceforge.net/projects/unxutils/上可用的完整版本。同时,请记住将包含可执行文件的文件夹添加到 Windows 路径中。如果您已安装 OSGeo4W,一个为 win32 环境提供大量开源地理空间软件的二进制发行版,那么您可能已经安装了它们。您可以在trac.osgeo.org/osgeo4w/找到它。- 在 Windows 中运行批处理文件:
C:\export_rivers.bat- 现在,运行以下脚本(在 Linux 或 macOS 中,您需要在运行 shell 文件之前将脚本的
execute权限分配给脚本):
$ chmod 775 export_rivers.sh $ ./export_rivers.sh Exporting river shapefile for AD country... Exporting river shapefile for AE country... ... Exporting river shapefile for ZM country... Exporting river shapefile for ZW country...您最终可以跳过创建
rivers_clipped_by_country视图,只需将上一个脚本中的ogr2ogr语句替换为以下命令(ogr2ogr将-sql选项的内容直接传递给 PostGIS);对于 Windows,请使用%%f:ogr2ogr rivers/rivers_$f.shp PG:"dbname='postgis_cookbook' user='me' password='mypassword'" -sql "SELECT r.name, c.iso2, ST_Intersection(r.the_geom, c.the_geom) AS the_geom FROM chp03.countries AS c JOIN chp03.rivers AS r ON ST_Intersects(r.the_geom, c.the_geom) WHERE c.iso2 = '$f'"- 使用
ogrinfo或桌面 GIS 检查输出。以下截图显示了在 QGIS 中的输出效果;我们添加了原始的 PostGISchp03.rivers层和几个生成的 shapefile:![]()
它是如何工作的...
您可以使用
ST_Intersection函数从另一个数据集中剪切一个数据集。在这个菜谱中,您首先创建了一个视图,在该视图中,您使用ST_Intersects函数在多边形层(国家)和线性层(河流)之间执行空间连接。在空间连接的上下文中,您使用了ST_Intersection函数来生成每个国家的河流快照。然后,您创建了一个 bash 脚本,遍历每个国家,并使用
ogr2ogr和之前创建的视图作为输入层,将该国剪切的河流导出为 shapefile。在脚本中遍历国家时,您使用了带有
-sql选项的ogrinfo,使用一个 SQLSELECT DISTINCT语句。您使用了grep和awkLinux 命令的组合,通过管道连接以获取每个国家的代码。grep命令是一个用于在纯文本数据集中搜索匹配正则表达式的行的实用程序,而awk是一种用于文本处理的解释性编程语言,通常用作数据提取和报告工具。使用 PostGIS 拓扑简化几何形状
在之前的菜谱中,我们使用了
ST_SimplifyPreserveTopology函数来尝试生成一个多边形 PostGIS 层的简化。不幸的是,虽然该函数对线性层工作良好,但它会在共享多边形边界处产生拓扑异常(重叠和空洞)。您使用外部工具集(
GRASS)生成了有效的拓扑简化。在这个菜谱中,您将使用 PostGIS 拓扑支持在空间数据库中执行相同的任务,而无需将数据集导出到不同的工具集。
准备工作
要开始,请执行以下步骤:
- 确保您已在数据库实例中启用了 PostGIS 拓扑支持。此支持作为单独的扩展程序打包,如果您使用的是 PostgreSQL 9.1 或更高版本,您可以使用以下 SQL
CREATE EXTENSION命令进行安装:
postgis_cookbook=# CREATE EXTENSION postgis_topology;-
从
gadm.org网站下载匈牙利的行政区域存档gadm.org/country(或使用本书提供的代码包中的副本)。 -
将
HUN_adm1.shpshapefile 从存档中提取到您的当前工作目录,working/chp03。 -
使用
ogr2ogr或shp2pgsql等工具将 shapefile 导入 PostGIS,如下所示:
ogr2ogr -f PostgreSQL -t_srs EPSG:3857 -nlt MULTIPOLYGON -lco GEOMETRY_NAME=the_geom -nln chp03.hungary PG:"dbname='postgis_cookbook' user='me' password='mypassword'" HUN_adm1.shp- 导入过程完成后,您可以使用以下命令来检查计数;请注意,这个空间表由 20 个多边形组成,每个多边形代表匈牙利的一个行政区域:
postgis_cookbook=# SELECT COUNT(*) FROM chp03.hungary;![图片]()
(1 row)如何操作...
您需要采取以下步骤来完成此菜谱:
- 与拓扑模块相关的所有函数和表都安装在一个名为
topology的模式中,所以让我们将其添加到搜索路径中,以避免在每次使用topology函数或对象之前都加上前缀:
postgis_cookbook=# SET search_path TO chp03, topology, public;- 现在,您将使用
CreateTopology函数创建一个新的名为hu_topo的topology模式,您将从中导入hungary表中的 20 个行政区域。在 PostGIS 拓扑中,一个拓扑模式所需的所有拓扑实体和关系都存储在单个 PostgreSQL 模式中,使用相同的空间参考系统。您将命名此模式为hu_topo并使用 EPSG:3857 空间参考(原始 shapefile 中使用的参考):
postgis_cookbook=# SELECT CreateTopology('hu_topo', 3857);- 注意以下代码中记录是如何被添加到
topology.topology表的:
postgis_cookbook=# SELECT * FROM topology.topology;![图片]()
(1 rows)- 还要注意,为了存储和管理拓扑,已在该名为
hu_topo的模式中生成四个表和一个视图,该模式由CreateTopology函数创建:
postgis_cookbook=# \dtv hu_topo.*![图片]()
(5 rows)- 使用
topologysummary函数检查创建的拓扑的初始信息,如下所示;然而,仍然没有任何拓扑实体(节点、边、面等)被初始化:
postgis_cookbook=# SELECT topologysummary('hu_topo');![图片]()
(1 row)- 按照以下步骤创建一个新的 PostGIS 表以存储拓扑行政边界:
postgis_cookbook=# CREATE TABLE chp03.hu_topo_polygons(gid serial primary key, name_1 varchar(75));- 使用
AddTopoGeometryColumn函数按以下方式向此表添加拓扑几何列:
postgis_cookbook=# SELECT AddTopoGeometryColumn('hu_topo', 'chp03', 'hu_topo_polygons', 'the_geom_topo', 'MULTIPOLYGON') As layer_id;- 使用
toTopoGeom函数将非拓扑的hungary空间表中的多边形插入到拓扑表中:
postgis_cookbook=> INSERT INTO chp03.hu_topo_polygons(name_1, the_geom_topo) SELECT name_1, toTopoGeom(the_geom, 'hu_topo', 1) FROM chp03.hungary; Query returned successfully: 20 rows affected, 10598 ms execution time.- 现在运行以下代码以检查
toTopoGeom函数如何修改拓扑模式的内容;你预计会有 20 个面,每个代表一个匈牙利行政区域,但结果却有 92 个:
postgis_cookbook=# SELECT topologysummary('hu_topo');![图片]()
- 通过分析
hu_topo.face表或使用桌面 GIS,可以轻松地识别问题。如果您使用ST_Area函数按面积对表中的多边形进行排序,您将在第一个多边形的详细信息之后注意到,该多边形有 1 个空面积(用于下一步中的拓扑截图)和 20 个大面积(每个代表一个行政区域),还有 77 个由拓扑异常(多边形重叠和空洞)生成的小多边形:
postgis_cookbook=# SELECT row_number() OVER (ORDER BY ST_Area(mbr) DESC) as rownum, ST_Area(mbr)/100000 AS area FROM hu_topo.face ORDER BY area DESC;![图片]()
(93 rows)-
您最终可以使用桌面 GIS 查看构建的拓扑元素(节点、边、面、拓扑几何体或 topogeoms)。以下截图显示了它们在 QGIS 中的外观:
![图片]()
-
现在,您将使用
CreateTopology函数重建拓扑,使用一个小容差值——1 米——作为CreateTopology函数的附加参数,以消除不必要的面(容差将合并顶点,消除小多边形)。首先,使用DropTopology函数和DROP TABLE命令删除您的拓扑模式,并使用 1 米的拓扑容差重建它们,如下所示:
postgis_cookbook=# SELECT DropTopology('hu_topo'); postgis_cookbook=# DROP TABLE chp03.hu_topo_polygons; postgis_cookbook=# SELECT CreateTopology('hu_topo', 3857, 1); postgis_cookbook=# CREATE TABLE chp03.hu_topo_polygons( gid serial primary key, name_1 varchar(75)); postgis_cookbook=# SELECT AddTopoGeometryColumn('hu_topo', 'chp03', 'hu_topo_polygons', 'the_geom_topo', 'MULTIPOLYGON') As layer_id; postgis_cookbook=# INSERT INTO chp03.hu_topo_polygons(name_1, the_geom_topo) SELECT name_1, toTopoGeom(the_geom, 'hu_topo', 1) FROM chp03.hungary;- 现在,如果你使用
topologysummary函数检查与拓扑相关的信息,如下所示,你可以看到每个行政边界都有一个面,并且之前由拓扑异常生成的 72 个面已经被消除,只剩下 20 个:
postgis_cookbook=# SELECT topologysummary('hu_topo');![]()
(1 row)- 最后,使用 500 米的容差简化
topo_polygons表中的多边形,如下所示:
postgis_cookbook=# SELECT ST_ChangeEdgeGeom('hu_topo', edge_id, ST_SimplifyPreserveTopology(geom, 500)) FROM hu_topo.edge;- 现在是时候使用以下命令通过连接
hu_topo_polygons表来更新原始的hungary表了:
postgis_cookbook=# UPDATE chp03.hungary hu SET the_geom = hut.the_geom_topo FROM chp03.hu_topo_polygons hut WHERE hu.name_1 = hut.name_1;- 简化过程应该顺利运行并生成一个有效的拓扑数据集。以下截图显示了简化后的拓扑(红色)与原始拓扑(黑色)的对比:
![]()
它是如何工作的...
我们使用
CreateTopology函数创建了一个新的 PostGIS 拓扑模式。此函数创建了一个新的 PostgreSQL 模式,其中存储了所有拓扑实体。在同一个空间数据库中,我们可以有更多的拓扑模式,每个模式包含在不同的 PostgreSQL 模式中。PostGIS 的
topology.topology表管理所有拓扑模式的元数据。每个拓扑模式由一系列表和视图组成,用于管理拓扑实体(如边、边数据、面、节点和 topogeoms)及其关系。
我们可以使用
topologysummary函数快速查看单个拓扑模式的描述,该函数总结了主要元数据信息——名称、SRID 和精度;节点、边、面、topogeoms 和拓扑层的数量;以及对于每个拓扑层,几何类型和 topogeoms 的数量。在创建拓扑模式后,我们使用
AddTopoGeometryColumn函数创建了一个新的 PostGIS 表,并向其中添加了一个拓扑几何列(在 PostGIS 拓扑术语中称为topogeom)。然后,我们使用
ST_ChangeEdgeGeom函数通过ST_SimplifyPreserveTopology函数更改拓扑边的几何形状,容差为 500 米,并检查了在拓扑模式上下文中使用此函数是否为多边形生成拓扑正确的结果。 -
- 如果你还没有安装
第四章:与矢量数据一起工作 - 高级技巧
在本章中,我们将涵盖:
-
使用 KNN 改进邻近过滤
-
使用 KNN 改进邻近过滤 - 高级
-
旋转几何形状
-
改进 ST_Polygonize
-
平移、缩放和旋转几何形状 - 高级
-
从激光雷达获取详细的建筑足迹
-
从一组点创建固定数量的聚类
-
计算 Voronoi 图
简介
除了作为一个能够存储和查询空间数据的空间数据库之外,PostGIS 还是一个非常强大的分析工具。这意味着对于用户来说,在 PostgreSQL 数据库中暴露和封装深层空间分析的能力是巨大的。
本章中的食谱大致可以分为四个主要部分:
-
高度优化的查询:
-
使用 KNN 改进邻近过滤
-
使用 KNN 改进邻近过滤 - 高级
-
-
使用数据库创建和修改几何形状:
-
旋转几何形状
-
改进 ST_Polygonize
-
平移、缩放和旋转几何形状 - 高级
-
从激光雷达获取详细的建筑足迹
-
-
从一组点创建固定数量的聚类:
-
使用 PostGIS 函数
ST_ClusterKMeans从一组点创建 K 个聚类 -
使用最小边界圆通过
ST_MinimumBoundingCircle函数可视地表示聚类
-
-
计算 Voronoi 图:
- 使用
ST_VoronoiPolygon函数来计算 Voronoi 图
- 使用
使用 KNN 改进邻近过滤
我们在这个食谱中试图回答的基本问题是基本距离问题,离我最近的五家咖啡店是哪些? 虽然这是一个基本问题,但并不总是容易回答,尽管我们将在本食谱中使其成为可能。我们将分两步进行。第一步我们将以简单启发式的方式接近这个问题,这将使我们能够快速找到解决方案。然后,我们将利用更深入的 PostGIS 功能,通过 k-Nearest Neighbor (KNN)方法使解决方案更快、更通用。
我们一开始就需要理解的一个概念是空间索引。空间索引,就像其他数据库索引一样,就像一本书的索引一样工作。它是一个特殊的结构,使得在表中查找内容变得更加容易,就像书索引帮助我们更快地找到书中的内容一样。在空间索引的情况下,它帮助我们找到在空间中查找事物更快的方法。因此,通过在我们的地理搜索中使用空间索引,我们可以以数量级的方式加快搜索速度。
要了解更多关于空间索引的信息,请参阅 en.wikipedia.org/wiki/Spatial_index#Spatial_index。
准备工作
我们将首先加载数据。我们的数据是美国俄亥俄州库耶霍加县的地址记录:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom CUY_ADDRESS_POINTS chp04.knn_addresses | psql -U me -d postgis_cookbook
由于此数据集可能需要一段时间才能加载,您可以另外加载一个子集:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom CUY_ADDRESS_POINTS_subset chp04.knn_addresses | psql -U me -d postgis_cookbook
我们指定了-I标志,以便在导入这些数据时创建空间索引。
让我们先看看我们正在处理多少条记录:
SELECT COUNT(*) FROM chp04.knn_addresses;
--484958
在这个地址表中,我们几乎有五十万个地址记录,这对于执行查询来说不是一个小数目。
如何做到这一点...
KNN 是一种寻找与给定点最近的任意数量点的搜索方法。如果没有合适的工具,这可能会是一个非常缓慢的过程,需要测试感兴趣点与所有可能的邻居之间的距离。这种方法的缺点是,随着点的数量增加,搜索速度会呈指数级下降。让我们从这种朴素的方法开始,然后对其进行改进。
假设我们感兴趣的是找到距离地理坐标-81.738624, 41.396679 最近的 10 条记录。朴素的方法是将这个值转换成我们的本地坐标系,并从搜索点比较数据库中每个点到点的距离,按距离排序,并将搜索限制在最近的 10 条记录(不建议运行以下查询,因为它可能会无限期地运行):
SELECT ST_Distance(searchpoint.the_geom, addr.the_geom) AS dist, * FROM
chp04.knn_addresses addr,
(SELECT ST_Transform(ST_SetSRID(ST_MakePoint(-81.738624, 41.396679),
4326), 3734) AS the_geom) searchpoint
ORDER BY ST_Distance(searchpoint.the_geom, addr.the_geom)
LIMIT 10;
对于较小的数据集,这是一个很好的方法。对于相对较小的记录数量,这是一个逻辑简单、快速的方法;然而,这种方法的可扩展性非常差,随着记录的增加,速度会呈指数级下降(有 50 万个点时,这将花费很长时间)。
另一种方法是仅将感兴趣点与已知靠近的点进行比较,通过设置搜索距离。例如,在下面的图中,我们有一个代表当前位置的星号,我们想知道最近的 10 个地址。图中的网格长度为 100 英尺,因此我们可以在 200 英尺范围内搜索点,然后测量这些点到每个点的距离,并返回最近的 10 个点:

因此,我们回答这个问题的方法是通过使用ST_DWithin运算符限制搜索,只搜索一定距离内的记录。ST_DWithin使用我们的空间索引,因此初始距离搜索很快,返回的记录列表应该足够短,以便进行与之前在本节中相同的成对距离比较。在我们的案例中,我们可以将搜索限制在 200 英尺以内:
SELECT ST_Distance(searchpoint.the_geom, addr.the_geom) AS dist, * FROM
chp04.knn_addresses addr,
(SELECT ST_Transform(ST_SetSRID(ST_MakePoint(-81.738624, 41.396679),
4326), 3734) AS the_geom) searchpoint
WHERE ST_DWithin(searchpoint.the_geom, addr.the_geom, 200)
ORDER BY ST_Distance(searchpoint.the_geom, addr.the_geom)
LIMIT 10;
之前查询的输出如下:

只要我们的搜索窗口ST_DWithin适合数据,这种方法就会表现良好。这种方法的缺点是,为了优化它,我们需要知道如何设置一个大致合适的搜索窗口。如果比合适的尺寸大,查询将比我们希望的运行得更慢。如果比合适的尺寸小,我们可能不会得到我们需要的所有点。本质上,我们事先不知道这一点,所以我们只能寄希望于最好的猜测。
在这个相同的数据集中,如果我们将在另一个位置应用相同的查询,输出将返回没有点,因为最近的 10 个点距离超过 200 英尺。我们可以在下面的图中看到这一点:

幸运的是,对于 PostGIS 2.0+,我们可以利用距离运算符(<->和<#>)来进行索引最近邻搜索。这使得 KNN 搜索非常快,而且不需要我们提前猜测需要搜索多远。为什么搜索这么快?当然,空间索引有所帮助,但在距离运算符的情况下,我们正在使用索引本身的分层结构,以非常快速地排序我们的邻居。
当在ORDER BY子句中使用时,距离运算符使用索引:
SELECT ST_Distance(searchpoint.the_geom, addr.the_geom) AS dist, * FROM
chp04.knn_addresses addr,
(SELECT ST_Transform(ST_SetSRID(ST_MakePoint(-81.738624, 41.396679),
4326), 3734) AS the_geom) searchpoint
ORDER BY addr.the_geom <-> searchpoint.the_geom
LIMIT 10;
这种方法不需要事先知道最近邻可能有多远。它也具有良好的可扩展性,在返回少量记录所需的时间内可以返回数千条记录。它有时比使用ST_DWithin慢,这取决于我们的搜索距离有多小以及我们处理的数据集有多大。但权衡是,我们不需要猜测搜索距离,对于大型查询,它可能比原始方法快得多。
它是如何工作的...
使这种魔法成为可能的是,PostGIS 使用 R 树索引。这意味着索引本身是根据空间信息进行分层排序的。正如演示的那样,我们可以利用索引的结构来对从给定任意位置的距离进行排序,从而直接使用索引返回排序后的记录。这意味着空间索引的结构本身帮助我们快速且经济地回答这样的基本问题。
关于 KNN 和 R 树的更多信息可以在workshops.boundlessgeo.com/postgis-intro/knn.html和en.wikipedia.org/wiki/R-tree找到。
参见
- 使用 KNN 改进邻近度过滤 – 高级配方
使用 KNN 改进邻近度过滤 – 高级
在前面的配方中,我们想要回答一个简单的问题:给定点的最近 10 个位置是哪些。还有一个简单的问题,其答案却出人意料地复杂。问题是,当我们想要遍历整个数据集并测试每个记录的最近邻时,我们如何解决这个问题?
我们的问题如下:对于表格中的每个点,我们感兴趣的是到另一个表格中最近物体的角度。一个演示这种场景的例子是,如果我们想将地址点表示为与相邻道路对齐的类似建筑的方块,类似于历史上的美国地质调查局(USGS)四边形地图,如下面的截图所示:

对于更大的建筑,USGS 四分图显示了建筑的足迹,但对于低于其最小阈值的住宅建筑,点只是旋转的正方形——这是一种很好的制图效果,可以用地址点轻松复制。
准备工作
与之前的配方一样,我们将从加载数据开始。我们的数据是美国俄亥俄州库亚霍加县的地址记录。如果您在之前的配方中已加载此数据,则无需重新加载数据。如果您尚未加载数据,请运行以下命令:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom CUY_ADDRESS_POINTS chp04.knn_addresses | psql -U me -d postgis_cookbook
由于这个数据集可能需要一段时间才能加载,您可以使用以下命令加载一个子集:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom CUY_ADDRESS_POINTS_subset chp04.knn_addresses | psql -U me -d postgis_cookbook
地址点将作为我们建筑结构的代理。然而,为了将我们的结构对齐到附近的街道,我们需要一个streets层。我们将使用库亚霍加县的街道中心线数据:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom CUY_STREETS chp04.knn_streets | psql -U me -d postgis_cookbook
在我们开始之前,我们必须考虑使用索引的另一个方面,这是我们之前 KNN 配方中不需要考虑的。当我们的 KNN 方法只使用点时,我们的索引是精确的——点的边界框实际上就是一个点。由于边界框是索引建立的基础,我们的索引距离估计完美地反映了我们点之间的实际距离。在我们的例子中,即非点几何的情况下,边界框是我们将要比较的点所在线的近似。换句话说,这意味着我们的最近邻可能不是我们非常近的邻居,但很可能是我们的近似最近邻,或者是我们最近邻之一。
在实践中,我们采用一种启发式方法:我们简单地收集比我们感兴趣的最近邻数量稍多的记录,然后根据实际距离对它们进行排序,以便只收集我们感兴趣的记录。这样,我们只需要对少量记录进行排序。
如何做到这一点...
由于 KNN 是解决这些问题的细微方法,强迫 KNN 在数据集的所有记录上运行,我称之为一种古老而值得尊敬的方法。换句话说,这需要一点点的“黑客”技巧。
更多关于在函数中使用 KNN 的一般解决方案,可以在 Alexandre Neto 在 PostGIS 用户列表上的帖子中找到,链接如下:
lists.osgeo.org/pipermail/postgis-users/2012-May/034017.html
在 SQL 中,典型的循环方式是使用SELECT语句。在我们的情况下,我们没有函数可以通过在表中的记录上执行 KNN 循环来使用;我们只有一个允许我们按给定记录的距离有效地排序返回记录的操作符。解决方案是编写一个临时函数,从而能够使用SELECT为我们循环记录。代价是函数的创建和删除,以及查询的工作,这两种代价的组合是这种方法的“黑客式”的代价。
首先,考虑以下函数:
CREATE OR REPLACE FUNCTION chp04.angle_to_street (geometry) RETURNS double precision AS $$
WITH index_query as (SELECT ST_Distance($1,road.the_geom) as dist, degrees(ST_Azimuth($1, ST_ClosestPoint(road.the_geom, $1))) as azimuth FROM chp04.knn_streets As road ORDER BY $1 <#> road.the_geom limit 5)
SELECT azimuth FROM index_query ORDER BY dist
LIMIT 1;
$$ LANGUAGE SQL;
现在,我们可以非常容易地使用这个函数:
CREATE TABLE chp04.knn_address_points_rot AS SELECT addr.*, chp04.angle_to_street(addr.the_geom) FROM chp04.knn_addresses addr;
如果你已经加载了整个地址数据集,这将需要一段时间。
如果我们选择,我们可以选择性地删除函数,这样就不会在我们的数据库中留下额外的函数:
DROP FUNCTION chp04.angle_to_street (geometry);
在下一个菜谱中,旋转几何形状,将使用计算出的角度来构建新的几何形状。
它是如何工作的...
我们的功能很简单,除了 KNN 魔法之外。作为函数的输入,我们允许几何形状,如下面的查询所示:
CREATE OR REPLACE FUNCTION chp04.angle_to_street (geometry) RETURNS double precision AS $$
前面的函数返回一个浮点值。
然后,我们使用WITH语句创建一个临时表,该表返回到我们感兴趣点的五个最近线。记住,由于索引使用边界框,我们实际上不知道哪条线是最接近的,所以我们收集一些额外的点,然后根据距离过滤它们。这个想法在以下查询中实现:
WITH index_query as (SELECT ST_Distance($1,road.geom) as dist, degrees(ST_Azimuth($1, ST_ClosestPoint(road.geom, $1))) as azimuth
FROM street_centerlines As road
ORDER BY $1 <#> road.geom LIMIT 5)
注意,我们实际上是在返回到列。第一列是dist,在其中我们计算到最近五条道路线的距离。注意,这个操作是在使用ORDER BY和LIMIT函数作为过滤器之后执行的,所以这并不需要太多的计算。然后,我们使用ST_Azimuth来计算从我们的点到最近五条线上的最近点的角度。总的来说,我们的临时index_query表返回的是到最近五条线的距离以及到最近五条线的相应旋转角度。
然而,如果我们回想起来,我们寻找的不是到最近的五个角度,而是到真正的最近道路线的角度。为此,我们按距离排序结果,并进一步使用LIMIT 1:
SELECT azimuth FROM index_query ORDER BY dist
LIMIT 1;
相关内容
- 使用 KNN 改进邻近过滤菜谱
旋转几何形状
在 PostGIS 提供的许多函数中,几何形状操作是一个非常强大的补充。在这个菜谱中,我们将探索使用ST_Rotate函数旋转几何形状的简单示例。我们将使用使用 KNN 改进邻近过滤 - 高级菜谱中的函数来计算我们的旋转值。
准备工作
ST_Rotate有几个变体:ST_RotateX、ST_RotateY和ST_RotateZ,其中ST_Rotate函数是ST_RotateZ的别名。因此,对于二维情况,ST_Rotate是一个典型用例。
在使用 KNN 改进邻近过滤 - 高级菜谱中,我们的函数计算了从建筑中心点或地址点到最近道路的角度。我们可以根据那个旋转因子将那个建筑点的位置表示为一个正方形符号,但更有趣的是,我们可以明确地在实际空间中构建该足迹区域,并将其旋转以匹配我们计算出的旋转角度。
如何做...
回想一下使用 KNN 改进邻近过滤 - 高级菜谱中的函数:
CREATE OR REPLACE FUNCTION chp04.angle_to_street (geometry) RETURNS double precision AS $$
WITH index_query as (SELECT ST_Distance($1,road.the_geom) as dist, degrees(ST_Azimuth($1, ST_ClosestPoint(road.the_geom, $1))) as azimuth
FROM chp04.knn_streets As road
ORDER BY $1 <#> road.the_geom limit 5)
SELECT azimuth FROM index_query ORDER BY dist
LIMIT 1;
$$ LANGUAGE SQL;
这个函数将计算几何形状到最近道路线的角度。现在,为了使用这个计算构建几何形状,运行以下函数:
CREATE TABLE chp04.tsr_building AS
SELECT ST_Rotate(ST_Envelope(ST_Buffer(the_geom, 20)), radians(90 - chp04.angle_to_street(addr.the_geom)), addr.the_geom)
AS the_geom FROM chp04.knn_addresses addr
LIMIT 500;
它是如何工作的...
在第一步中,我们对每个点首先应用 20 英尺的缓冲区:
ST_Buffer(the_geom, 20)
然后,我们计算缓冲区的边界,为我们提供了一个围绕该缓冲区的正方形。这是从点创建指定大小正方形几何体的快速简单方法:
ST_Envelope(ST_Buffer(the_geom, 20))
最后,我们使用 ST_Rotate 将几何体旋转到适当的角。在这里,查询变得难以阅读。ST_Rotate 函数接受两个参数:
ST_Rotate(geometry to rotate, angle, origin around which to rotate)
我们正在使用的是从缓冲和边界创建中计算出的新几何体。角度是我们使用 chp04.angle_to_street 函数计算出的角度。最后,我们旋转的起点是输入点本身,导致以下查询部分:
ST_Rotate(ST_Envelope(ST_Buffer(the_geom, 20)), radians(90 -chp04.angle_to_street(addr.the_geom)), addr.the_geom);
这为我们提供了一些非常漂亮的制图,如下面的图所示:

参见
-
使用 KNN 改进邻近度过滤 - 高级 方法
-
转换、缩放和旋转几何体 - 高级 方法
改进 ST_Polygonize
在这个简短的食谱中,我们将使用在构建几何体时使用 ST_Polygonize 的常见编码模式,并将其形式化为可重用的函数。
ST_Polygonize 是一个非常有用的函数。你可以向 ST_Polygonize 传递一组 联合 的线或线数组,该函数将从输入中构建多边形。ST_Polygonize 会非常积极地构建所有可能的多边形。该函数的一个令人沮丧的方面是它不会返回多边形,而是返回一个几何集合。几何集合在第三方工具中与 PostGIS 交互时可能会出现问题,因为许多第三方工具没有识别和显示几何集合的机制。
我们在这里将形式化的模式是当需要将几何集合转换为多边形时通常推荐的方法。这种方法不仅对 ST_Polygonize 有用,我们将在后续的食谱中使用它,还可以适应其他情况,其中函数返回几何集合,在所有实际应用中,这些几何集合都是多边形。因此,它值得有一个专门的食谱。
准备工作
处理几何集合的基本模式是使用 ST_Dump 将其转换为转储类型,提取转储的几何部分,收集几何体,然后将此集合转换为多边形。转储类型是一种特殊的 PostGIS 类型,它是几何体和几何体索引号的组合。通常使用 ST_Dump 将几何集合转换为转储类型,然后从那里进一步处理数据。很少直接使用转储对象,但它通常是数据的中介类型。
如何做到...
我们期望这个函数接收一个几何体并返回一个多边形几何体:
CREATE OR REPLACE FUNCTION chp04.polygonize_to_multi (geometry) RETURNS geometry AS $$
为了提高可读性,我们将使用WITH语句构建几何形状的一系列变换。首先,我们将多边形化:
WITH polygonized AS (
SELECT ST_Polygonize($1) AS the_geom
),
然后,我们将输出:
dumped AS (
SELECT (ST_Dump(the_geom)).geom AS the_geom FROM polygonized
)
现在,我们可以从我们的结果中收集和构建一个多边形:
SELECT ST_Multi(ST_Collect(the_geom)) FROM dumped;
将这些合并成一个单独的函数:
CREATE OR REPLACE FUNCTION chp04.polygonize_to_multi (geometry) RETURNS geometry AS $$
WITH polygonized AS (
SELECT ST_Polygonize($1) AS the_geom
),
dumped AS (
SELECT (ST_Dump(the_geom)).geom AS the_geom FROM polygonized
)
SELECT ST_Multi(ST_Collect(the_geom)) FROM dumped;
$$ LANGUAGE SQL;
现在,我们可以直接从一组闭合线条中多边形化,并跳过使用ST_Polygonize函数时的典型中间步骤,即处理几何集合。
参见
- 转换、缩放和旋转几何形状 – 高级菜谱
转换、缩放和旋转几何形状 – 高级
通常,在空间数据库中,我们感兴趣的是明确表示数据中隐含的几何形状。在我们将要使用的示例中,几何形状的显式部分是一个单点坐标,其中进行了现场调查绘图。在下面的屏幕截图中,这个显式位置是点。隐含的几何形状是实际现场调查的范围,包括 10 个子图,排列成 5 x 2 的阵列,并按照方位角旋转。
这些子图是以下图中紫色方块:

准备工作
有许多方法可以解决这个问题。为了简化,我们首先构建我们的网格,然后就地旋转它。此外,原则上我们可以使用ST_Buffer函数与ST_Extent结合来构建结果几何中的正方形,但是,由于ST_Extent为了效率而使用几何形状的浮点近似,这可能导致子图边缘出现一些不匹配。
我们构建子图的途径是使用一系列ST_MakeLine构建网格,并使用ST_Node来展平或节点化结果。这确保了我们的所有线条都正确地相交。然后ST_Polygonize将为我们构建多边形几何形状。我们将通过从改进 ST_Polygonize菜谱中的包装函数利用这个功能。
我们的图表边长为 10 个单位,排列成 5 x 2 的阵列。因此,我们可以想象一个函数,我们向其中传递我们的图表原点,该函数返回所有子图几何形状的多边形。需要考虑的一个额外元素是,我们图表布局的朝向已经旋转到方位角。我们期望该函数实际上使用两个输入,因此原点和旋转将是我们将传递给函数的变量。
如何做到这一点...
我们可以将几何形状和浮点值作为输入,并希望函数返回几何形状:
CREATE OR REPLACE FUNCTION chp04.create_grid (geometry, float) RETURNS geometry AS $$
为了构建子图,我们需要三条与X轴平行的线:
WITH middleline AS (
SELECT ST_MakeLine(ST_Translate($1, -10, 0),
ST_Translate($1, 40.0, 0)) AS the_geom
),
topline AS (
SELECT ST_MakeLine(ST_Translate($1, -10, 10.0),
ST_Translate($1, 40.0, 10)) AS the_geom
),
bottomline AS (
SELECT ST_MakeLine(ST_Translate($1, -10, -10.0),
ST_Translate($1, 40.0, -10)) AS the_geom
),
我们需要六条与Y轴平行的线:
oneline AS (
SELECT ST_MakeLine(ST_Translate($1, -10, 10.0),
ST_Translate($1, -10, -10)) AS the_geom
),
twoline AS (
SELECT ST_MakeLine(ST_Translate($1, 0, 10.0),
ST_Translate($1, 0, -10)) AS the_geom
),
threeline AS (
SELECT ST_MakeLine(ST_Translate($1, 10, 10.0),
ST_Translate($1, 10, -10)) AS the_geom
),
fourline AS (
SELECT ST_MakeLine(ST_Translate($1, 20, 10.0),
ST_Translate($1, 20, -10)) AS the_geom
),
fiveline AS (
SELECT ST_MakeLine(ST_Translate($1, 30, 10.0),
ST_Translate($1, 30, -10)) AS the_geom
),
sixline AS (
SELECT ST_MakeLine(ST_Translate($1, 40, 10.0),
ST_Translate($1, 40, -10)) AS the_geom
),
要将这些用于多边形构造,我们需要它们在交叉和接触的地方有节点。一个 UNION ALL 函数将把这些线合并到单个记录中;ST_Union 将提供必要的几何处理来构建感兴趣的节点,并将我们的线合并成一个准备用于 chp04.polygonize_to_multi 的单一实体:
combined AS (
SELECT ST_Union(the_geom) AS the_geom FROM
(
SELECT the_geom FROM middleline
UNION ALL
SELECT the_geom FROM topline
UNION ALL
SELECT the_geom FROM bottomline
UNION ALL
SELECT the_geom FROM oneline
UNION ALL
SELECT the_geom FROM twoline
UNION ALL
SELECT the_geom FROM threeline
UNION ALL
SELECT the_geom FROM fourline
UNION ALL
SELECT the_geom FROM fiveline
UNION ALL
SELECT the_geom FROM sixline
) AS alllines
)
但我们还没有创建多边形,只是线。最后一步,使用我们的 polygonize_to_multi 函数,为我们完成了工作:
SELECT chp04.polygonize_to_multi(ST_Rotate(the_geom, $2, $1)) AS the_geom FROM combined;
合并查询如下:
CREATE OR REPLACE FUNCTION chp04.create_grid (geometry, float) RETURNS geometry AS $$
WITH middleline AS (
SELECT ST_MakeLine(ST_Translate($1, -10, 0),
ST_Translate($1, 40.0, 0)) AS the_geom
),
topline AS (
SELECT ST_MakeLine(ST_Translate($1, -10, 10.0),
ST_Translate($1, 40.0, 10)) AS the_geom
),
bottomline AS (
SELECT ST_MakeLine(ST_Translate($1, -10, -10.0),
ST_Translate($1, 40.0, -10)) AS the_geom
),
oneline AS (
SELECT ST_MakeLine(ST_Translate($1, -10, 10.0),
ST_Translate($1, -10, -10)) AS the_geom
),
twoline AS (
SELECT ST_MakeLine(ST_Translate($1, 0, 10.0),
ST_Translate($1, 0, -10)) AS the_geom
),
threeline AS (
SELECT ST_MakeLine(ST_Translate($1, 10, 10.0),
ST_Translate($1, 10, -10)) AS the_geom
),
fourline AS (
SELECT ST_MakeLine(ST_Translate($1, 20, 10.0),
ST_Translate($1, 20, -10)) AS the_geom
),
fiveline AS (
SELECT ST_MakeLine(ST_Translate($1, 30, 10.0),
ST_Translate($1, 30, -10)) AS the_geom
),
sixline AS (
SELECT ST_MakeLine(ST_Translate($1, 40, 10.0),
ST_Translate($1, 40, -10)) AS the_geom
),
combined AS (
SELECT ST_Union(the_geom) AS the_geom FROM
(
SELECT the_geom FROM middleline
UNION ALL
SELECT the_geom FROM topline
UNION ALL
SELECT the_geom FROM bottomline
UNION ALL
SELECT the_geom FROM oneline
UNION ALL
SELECT the_geom FROM twoline
UNION ALL
SELECT the_geom FROM threeline
UNION ALL
SELECT the_geom FROM fourline
UNION ALL
SELECT the_geom FROM fiveline
UNION ALL
SELECT the_geom FROM sixline
) AS alllines
)
SELECT chp04.polygonize_to_multi(ST_Rotate(the_geom, $2, $1)) AS the_geom FROM combined;
$$ LANGUAGE SQL;
它是如何工作的...
如前所述的该函数,本质上是从单个输入点和旋转值中绘制几何形状。它是通过使用九个 ST_MakeLine 实例来做到这一点的。通常,人们可能会将 ST_MakeLine 与 ST_MakePoint 结合使用来完成这项任务。我们通过使函数消耗一个点几何作为输入来绕过这种需求。因此,我们可以使用 ST_Translate 将此点几何移动到感兴趣线的端点,以便使用 ST_MakeLine 构建我们的线。
当然,最后一步是测试我们新的几何构造函数的使用:
CREATE TABLE chp04.tsr_grid AS
-- embed inside the function
SELECT chp04.create_grid(ST_SetSRID(ST_MakePoint(0,0),
3734), 0) AS the_geom
UNION ALL
SELECT chp04.create_grid(ST_SetSRID(ST_MakePoint(0,100),
3734), 0.274352 * pi()) AS the_geom
UNION ALL
SELECT chp04.create_grid(ST_SetSRID(ST_MakePoint(100,0),
3734), 0.824378 * pi()) AS the_geom
UNION ALL
SELECT chp04.create_grid(ST_SetSRID(ST_MakePoint(0,-100), 3734),
0.43587 * pi()) AS the_geom
UNION ALL
SELECT chp04.create_grid(ST_SetSRID(ST_MakePoint(-100,0), 3734),
1 * pi()) AS the_geom;
之前函数生成的不同网格如下:

参见
-
改进 ST_Polygonize 配方
-
使用 KNN 提高邻近性过滤 - 高级 配方
LiDAR 的详细建筑足迹
在空间分析中,我们经常收到一种形式的数据,看起来很有前景,但我们需要的是另一种更广泛的形式。激光雷达是解决这类问题的绝佳方案;激光雷达数据是通过空中平台,如固定翼飞机或直升机,或地面单元进行激光扫描的。激光雷达设备通常返回一个点云,这些点参考空间中的绝对或相对位置。作为一个原始数据集,它们在未经处理的情况下通常不如处理后的有用。许多激光雷达数据集被分类为土地覆盖类型,因此,除了包含空间中所有采样点的 x、y 和 z 值的数据外,激光雷达数据集通常还包含被分类为地面、植被、高大植被、建筑物等的数据。
尽管这很有用,但数据密集,也就是说,它有离散的点,而不是广泛的,因为多边形表示这样的数据将会是广泛的。这个配方被开发为一个简单的方法,使用 PostGIS 将密集的激光雷达建筑样本转换为广泛的建筑足迹:

准备工作
我们将使用的激光雷达数据集是 2006 年的收集,它被分类为地面、高大植被(> 20 英尺)、建筑物等。接下来分析的一个特点是,我们假设分类是正确的,因此我们不会重新检查分类的质量或尝试在 PostGIS 中改进它。
LiDAR 数据集的一个特点是,对于相对平坦的表面,每 5 英尺至少存在一个样本点。这将告诉你我们如何处理数据。
首先,让我们使用以下命令加载数据集:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom lidar_buildings chp04.lidar_buildings | psql -U me -d postgis_cookbook
如何做...
将点数据转换为多边形数据的最简单方法是通过它们的已知分离来缓冲点:
ST_Buffer(the_geom, 5)
然而,我们可以想象,这种简单的方法可能看起来很奇怪:

因此,最好将这些几何形状进行合并,以溶解内部边界:
ST_Union(ST_Buffer(the_geom, 5))
现在,我们可以看到一些简单建筑轮廓的起点:

虽然这略有改善,但结果相当粗糙。我们将使用ST_Simplify_PreserveTopology函数简化多边形,然后仅获取外部环以移除内部空洞:
CREATE TABLE chp04.lidar_buildings_buffer AS
WITH lidar_query AS
(SELECT ST_ExteriorRing(ST_SimplifyPreserveTopology(
(ST_Dump(ST_Union(ST_Buffer(the_geom, 5)))).geom, 10
)) AS the_geom FROM chp04.lidar_buildings)
SELECT chp04.polygonize_to_multi(the_geom) AS the_geom from lidar_query;
现在,我们有了我们缓冲几何形状的简化版本:

这里有两点需要注意。与采样密度相比,建筑越大,看起来就越好。我们可能会查询以消除较小的建筑,这些建筑在使用这种方法时可能会退化,具体取决于我们 LiDAR 数据的密度。
它是如何工作的...
不正式地说,我们的缓冲技术有效地将相邻样本聚在一起或聚类。这仅因为我们有规则采样数据,但这没关系。LiDAR 数据的密度和扫描模式是此类数据集的典型特征,因此我们可以预期这种方法适用于其他数据集。
ST_Union函数将这些离散缓冲点转换为一个具有溶解内部边界的单一记录。为了完成聚类,我们只需使用ST_Dump将这些边界转换回离散多边形,这样我们就可以利用单个建筑轮廓。最后,我们使用ST_SimplifyPreserveTopology简化模式并提取外部环,或者在这些多边形外部使用ST_ExteriorRing,以移除建筑轮廓内部的空洞。由于ST_ExteriorRing返回一条线,我们必须重建我们的多边形。我们使用chp04.polygonize_to_multi函数,这是我们写在改进 ST_Polygonize菜谱中的一个函数,来处理这样的情况。此外,您还可以查看第二章中的正常化内部叠加菜谱,以了解如何纠正可能存在地理错误的 polygons。
从一组点创建固定数量的聚类
在 PostGIS 2.3 版本中,引入了一些聚类功能。在这个菜谱中,我们将探索ST_ClusterKMeans函数,该函数将几何形状聚合到k个聚类中,并为输入中的每个几何形状检索分配的聚类id。该函数的一般语法如下:
ST_ClusterKMeans(geometry winset geom, integer number_of_clusters);
准备工作
在这个食谱中,我们将使用第三章,“使用矢量数据 - 基础”中包含的地震数据集作为函数的输入几何形状。我们还需要定义函数将输出的聚类数量;本例中k的值将是 10。您可以尝试调整这个值,看看函数输出的不同聚类排列;k的值越大,每个聚类包含的几何形状数量就越少。
如果您之前没有将地震数据导入到第三章,“使用矢量数据 - 基础”模式,请参阅“准备就绪”部分的“使用空间连接进行 GIS 分析”食谱。
一旦我们创建了chp03.earthquake表,我们还需要两个表。第一个表将包含聚类的质心几何形状及其相应的 ID,这是ST_ClusterKMeans函数检索到的。第二个表将包含每个聚类的最小边界圆的几何形状。为此,运行以下 SQL 命令:
CREATE TABLE chp04.earthq_cent (
cid integer PRIMARY KEY, the_geom geometry('POINT',4326)
);
CREATE TABLE chp04.earthq_circ (
cid integer PRIMARY KEY, the_geom geometry('POLYGON',4326)
);
如何操作...
然后,我们将通过使用ST_ClusterKMeans函数为chp03.earthquakes中的每个几何形状生成聚类 ID 来填充质心表,然后我们将使用ST_Centroid函数计算每个聚类的 10 个质心:
INSERT INTO chp04.earthq_cent (the_geom, cid) (
SELECT DISTINCT ST_SetSRID(ST_Centroid(tab2.ge2), 4326) as centroid,
tab2.cid FROM(
SELECT ST_UNION(tab.ge) OVER (partition by tab.cid ORDER BY tab.cid)
as ge2, tab.cid as cid FROM(
SELECT ST_ClusterKMeans(e.the_geom, 10) OVER() AS cid, e.the_geom
as ge FROM chp03.earthquakes as e) as tab
)as tab2
);
如果我们使用以下命令检查插入的行:
SELECT * FROM chp04.earthq_cent;
输出结果如下:

然后,在chp04.earthq_circ表中插入对应聚类的最小边界圆。执行以下 SQL 命令:
# INSERT INTO chp04.earthq_circ (the_geom, cid) (
SELECT DISTINCT ST_SetSRID(
ST_MinimumBoundingCircle(tab2.ge2), 4326) as circle, tab2.cid
FROM(
SELECT ST_UNION(tab.ge) OVER (partition by tab.cid ORDER BY tab.cid)
as ge2, tab.cid as cid
FROM(
SELECT ST_ClusterKMeans(e.the_geom, 10) OVER() as cid, e.the_geom
as ge FROM chp03.earthquakes AS e
) as tab
)as tab2
);
在桌面 GIS 中,将三个表(chp03.earthquakes、chp04.earthq_cent和chp04.earthq_circ)作为图层导入,以便可视化它们并理解聚类。请注意,圆圈可能会重叠;然而,这并不意味着聚类也如此,因为每个点只属于一个聚类,但一个聚类的最小边界圆可能与另一个聚类的最小边界圆重叠:

计算 Voronoi 图
在 2.3 版本中,PostGIS 提供了一种从几何形状的顶点创建 Voronoi 图的方法;这仅适用于 GEOS 版本大于或等于 3.5.0 的情况。
以下是从一组地址点生成的 Voronoi 图。注意,生成图所用的点与分割它们的线等距。从上方观察到的肥皂泡堆积形成类似形状的网络:

Voronoi 图是一种空间填充方法,对于各种空间分析问题非常有用。我们可以使用这些方法在点周围创建空间填充多边形,其边缘与所有周围点等距。
关于 Voronoi 图更详细的信息可以在以下链接中找到:
PostGIS 函数ST_VoronoiPolygons()接收以下参数:用于构建 Voronoi 图的几何形状,一个容差值,它是一个浮点数,将告诉函数在哪个距离内顶点将被视为等效输出,以及一个extent_to几何形状,它将告诉如果这个几何形状大于从输入顶点计算出的输出范围,则图的范围。对于这个菜谱,我们将不使用容差,它默认为 0.0 单位,也不使用extend_to,它默认设置为NULL。
准备工作
我们将创建一个小的任意点数据集,将其输入到我们的函数中,我们将围绕这个函数计算 Voronoi 图:
DROP TABLE IF EXISTS chp04.voronoi_test_points;
CREATE TABLE chp04.voronoi_test_points
(
x numeric,
y numeric
)
WITH (OIDS=FALSE);
ALTER TABLE chp04.voronoi_test_points ADD COLUMN gid serial;
ALTER TABLE chp04.voronoi_test_points ADD PRIMARY KEY (gid);
INSERT INTO chp04.voronoi_test_points (x, y)
VALUES (random() * 5, random() * 7);
INSERT INTO chp04.voronoi_test_points (x, y)
VALUES (random() * 2, random() * 8);
INSERT INTO chp04.voronoi_test_points (x, y)
VALUES (random() * 10, random() * 4);
INSERT INTO chp04.voronoi_test_points (x, y)
VALUES (random() * 1, random() * 15);
INSERT INTO chp04.voronoi_test_points (x, y)
VALUES (random() * 4, random() * 9);
INSERT INTO chp04.voronoi_test_points (x, y)
VALUES (random() * 8, random() * 3);
INSERT INTO chp04.voronoi_test_points (x, y)
VALUES (random() * 5, random() * 3);
INSERT INTO chp04.voronoi_test_points (x, y)
VALUES (random() * 20, random() * 0.1);
INSERT INTO chp04.voronoi_test_points (x, y)
VALUES (random() * 5, random() * 7);
SELECT AddGeometryColumn ('chp04','voronoi_test_points','the_geom',3734,'POINT',2);
UPDATE chp04.voronoi_test_points
SET the_geom = ST_SetSRID(ST_MakePoint(x,y), 3734)
WHERE the_geom IS NULL
;
如何做到这一点...
准备就绪后,我们现在可以创建 Voronoi 图。首先,我们将创建一个包含MultiPolygon的表:
DROP TABLE IF EXISTS chp04.voronoi_diagram;
CREATE TABLE chp04.voronoi_diagram(
gid serial PRIMARY KEY,
the_geom geometry(MultiPolygon, 3734)
);
现在,为了计算 Voronoi 图,我们使用ST_Collect来为ST_VoronoiPolygons函数提供一个MultiPoint对象。仅此输出的结果将是GeometryCollection;然而,我们感兴趣的是得到一个MultiPolygon,因此我们需要使用ST_CollectionExtract函数,当第二个参数为3时,它会从一个GeometryCollection中提取所有多边形:
INSERT INTO chp04.voronoi_diagram(the_geom)(
SELECT ST_CollectionExtract(
ST_SetSRID(
ST_VoronoiPolygons(points.the_geom),
3734),
3)
FROM (
SELECT
ST_Collect(the_geom) as the_geom
FROM chp04.voronoi_test_points
)
as points);
如果我们将voronoi_test_points和voronoi_diagram层导入桌面 GIS 中,我们得到以下随机生成点的 Voronoi 图:

现在我们可以处理更大的数据集了。以下是从 改进 KNN 的邻近性过滤 - 高级 菜谱中提取的地址点生成的 Voronoi 图,颜色基于到最近街道的方位角,该方位角也在该菜谱中计算:

第五章:处理和加载栅格数据
在本章中,我们将涵盖以下内容:
-
获取和加载栅格数据
-
处理和基本分析栅格信息
-
执行简单的地图代数运算
-
将几何形状与栅格数据结合进行分析
-
在栅格和几何形状之间进行转换
-
使用 GDAL VRT 处理和加载栅格
-
变换和重采样栅格
-
执行高级地图代数运算
-
执行 DEM 操作
-
通过 SQL 共享和可视化栅格
简介
在本章中,我们将以逐步工作流程的形式介绍配方,您可以在处理栅格时应用这些配方。这包括加载栅格,获取栅格的基本理解,处理和分析它,并将其交付给消费者。我们故意在工作流程中添加一些绕道,以反映现实情况,即原始的栅格可能令人困惑且不适合分析。在本章结束时,您应该能够将所学到的知识应用到解决您的栅格问题中。
在继续之前,我们应该描述什么是栅格,以及栅格的用途。在最简单的层面上,栅格是带有描述如何在地球表面上放置栅格的信息的照片或图像。照片通常有三组值:每组对应于一种主要颜色(红色、绿色和蓝色)。栅格也有值集,通常比照片中的值集更多。每组值被称为波段。因此,照片通常有三个波段,而栅格至少有一个波段。像数字照片一样,栅格有多种文件格式。您可能会遇到的常见栅格格式包括 PNG、JPEG、GeoTIFF、HDF5 和 NetCDF。由于栅格可以有许多波段和更多的值,它们可以以高效的方式存储大量数据。由于它们的效率,栅格被用于卫星和航空传感器以及建模表面,如天气预报。
本章和 PostGIS 生态系统中使用的一些关键字需要定义:
-
栅格:这是 PostGIS 用于在 PostgreSQL 中存储栅格文件的数据类型。
-
瓦片:这是原始栅格文件中的一小块,将被存储在表格行的某一列中。每个瓦片都有自己的空间信息集,因此它独立于同一表格同一列中的所有其他瓦片,即使其他瓦片来自相同的原始栅格文件。
-
覆盖:这由单个栅格列中的一个表格的所有瓦片组成。
在本章中,我们大量使用 GDAL。GDAL 通常被认为是处理栅格的瑞士军刀。GDAL 不是一个单一的应用程序,而是一个具有许多有用工具的栅格抽象库。通过 GDAL,您可以获取栅格的元数据,将该栅格转换为不同的格式,并在许多其他功能中扭曲该栅格。对于本章的需求,我们将使用三个 GDAL 工具:gdalinfo、gdalbuildvrt 和 gdal_translate。
获取和加载栅格
在这个配方中,我们将加载本章中使用的绝大多数栅格。这些栅格是卫星图像和模型生成的表面的示例,这两种是最常见的栅格来源。
准备工作
如果您还没有这样做,请创建一个目录并复制章节的数据集;对于 Windows,请使用以下命令:
> mkdir C:\postgis_cookbook\data\chap05
> cp -r /path/to/book_dataset/chap05 C:\postgis_cookbook\data\chap05
对于 Linux 或 macOS,进入您希望使用的文件夹,并运行以下命令,其中 /path/to/book_dataset/chap05 是您最初存储书籍源代码的路径:
> mkdir -p data/chap05
> cd data/chap05
> cp -r /path/to/book_dataset/chap05
您还应该在数据库中为这一章创建一个新的模式:
> psql -d postgis_cookbook -c "CREATE SCHEMA chp05"
如何做到这一点...
我们将使用 2016 年美国大陆的 PRISM 平均月最低温度栅格数据集开始。该栅格由俄勒冈州立大学的 PRISM 气候组提供,更多栅格可在 www.prism.oregonstate.edu/mtd/ 获取。
在命令行中,按照以下方式导航到 PRISM 目录:
> cd C:\postgis_cookbook\data\chap05\PRISM
让我们使用 GDAL 工具 gdalinfo 检查其中一个 PRISM 栅格。检查至少一个栅格以了解元数据并确保栅格没有问题总是一个好习惯。可以使用以下命令完成:
> gdalinfo PRISM_tmin_provisional_4kmM2_201703_asc.asc
gdalinfo 的输出如下:

gdalinfo 的输出表明栅格没有问题,正如 Corner Coordinates、Pixel Size、Band 和 Coordinate System 不为空所证明的。
通过查看元数据,我们发现关于空间参考系统的元数据表明栅格使用 NAD83 坐标系统。我们可以通过在 spatial_ref_sys 表中搜索 NAD83 的详细信息来双重检查这一点:
SELECT srid, auth_name, auth_srid, srtext, proj4text
FROM spatial_ref_sys WHERE proj4text LIKE '%NAD83%'
将 srtext 的文本与 PRISM 栅格的元数据空间属性进行比较,我们发现栅格位于 EPSG (SRID 4269)。
您可以使用 raster2pgsql 将 PRISM 栅格数据加载到 chp05.prism 表中,该命令将以与 shp2pgsql 命令类似的方式将栅格文件导入数据库:
> raster2pgsql -s 4269 -t 100x100 -F -I -C -Y .\PRISM_tmin_provisional_4kmM2_*_asc.asc
chp05.prism | psql -d postgis_cookbook -U me
raster2pgsql 命令使用以下标志:
-
-s:此标志将 SRID4269分配给导入的栅格。 -
-t:此标志表示瓦片大小。它将导入的栅格分成更小、更易于管理的块;添加到表中的每个记录最多为 100 x 100 像素。 -
-F:此标志向表中添加一个列,并用栅格的文件名填充它。 -
-I:此标志在表的栅格列上创建一个 GIST 空间索引。 -
-C:此标志在表上应用标准约束集。标准约束集包括对维度、比例、倾斜、左上角坐标和 SRID 的检查。 -
-Y:此标志指示raster2pgsql使用COPY语句而不是INSERT语句。COPY通常比INSERT快。
我们之所以传递-F给raster2pgsql,是有原因的。如果您查看 PRISM 栅格的文件名,您会注意到年份和月份。因此,让我们将filename列中的值转换为表中的日期:
ALTER TABLE chp05.prism ADD COLUMN month_year DATE;
UPDATE chp05.prism SET month_year = ( SUBSTRING(split_part(filename, '_', 5), 0, 5) || '-' || SUBSTRING(split_part(filename, '_', 5), 5, 4) || '-01' ) :: DATE;
目前对于PRISM栅格需要做的就这些。
现在,让我们导入一个航天飞机雷达地形测绘任务(SRTM)栅格。这个 SRTM 栅格是由美国国家航空航天局喷气推进实验室在 2000 年 2 月进行的。这个栅格和其他类似的栅格可以在以下网址找到:dds.cr.usgs.gov/srtm/version2_1/SRTM1/.
将当前目录更改为SRTM目录:
> cd C:\postgis_cookbook\data\chap05\SRTM
确保您使用gdalinfo检查 SRTM 栅格,以确保它是有效的并且有坐标系统的值。一旦检查完毕,将 SRTM 栅格导入到chp05.srtm表中:
> raster2pgsql -s 4326 -t 100x100 -F -I -C -Y N37W123.hgt chp05.srtm | psql -d postgis_cookbook
我们为 SRTM 栅格和 PRISM 栅格使用相同的raster2pgsql标志。
我们还需要导入由旧金山和旧金山市提供的shapefile,这些文件与书中的数据集文件一起提供,或者可以从以下链接找到,在将数据导出为 shapefile 之后:
data.sfgov.org/Geographic-Locations-and-Boundaries/SF-Shoreline-and-Islands/rgcx-5tix

书中的文件将用于后续的许多食谱,并且必须按照以下方式加载到数据库中:
> cd C:\postgis_cookbook\data\chap05\SFPoly
> shp2pgsql -s 4326 -I sfpoly.shp chp05.sfpoly | psql -d postgis_cookbook -U me
它是如何工作的...
在这个食谱中,我们导入了后续食谱所需的 PRISM 和 SRTM 栅格,还导入了包含旧金山边界的shapefile,用于各种栅格分析。现在,让我们进入有趣的部分!
处理基本栅格信息和分析
到目前为止,我们已经检查并导入了 PRISM 和 SRTM 栅格到postgis_cookbook数据库的chp05模式中。现在,我们将继续在数据库中处理这些栅格。
准备工作
在这个食谱中,我们探索了提供对postgis_cookbook数据库中栅格属性和特征洞察的函数。通过这样做,我们可以查看数据库中找到的内容是否与通过访问gdalinfo提供的信息相匹配。
如何操作...
PostGIS 包括raster_columns视图,以提供数据库中所有栅格列的高级总结。这个视图在功能和形式上与geometry_columns和geography_columns视图类似。
让我们在raster_columns视图中运行以下 SQL 查询,以查看prism表中可用的信息:
SELECT
r_table_name,
r_raster_column,
srid,
scale_x,
scale_y,
blocksize_x,
blocksize_y,
same_alignment,
regular_blocking,
num_bands,
pixel_types,
nodata_values,
out_db,
ST_AsText(extent) AS extent FROM raster_columns WHERE r_table_name = 'prism';
SQL 查询返回的记录类似于以下内容:


(1 row)
如果你回顾一下 PRISM 栅格之一的gdalinfo输出,你会看到尺度(像素大小)的值是一致的。传递给raster2pgsql的标志,指定瓦片大小和SRID,是有效的。
让我们看看单个栅格瓦片的元数据是什么样的。我们将使用ST_Metadata()函数:
SELECT rid, (ST_Metadata(rast)).*
FROM chp05.prism
WHERE month_year = '2017-03-01'::date
LIMIT 1;
输出将类似于以下内容:

使用ST_BandMetadata()来检查记录 ID 54的栅格瓦片的第一和唯一波段:
SELECT rid, (ST_BandMetadata(rast, 1)).*
FROM chp05.prism
WHERE rid = 54;
结果表明,该波段是像素类型32BF,并且具有NODATA值为-9999。NODATA值是分配给空像素的值:

现在,为了做一些更有用的操作,对这个栅格瓦片运行一些基本的统计函数。
首先,让我们使用ST_SummaryStats()计算特定栅格(在这种情况下,编号为 54)的汇总统计(计数、平均值、标准差、最小值和最大值):
WITH stats AS (SELECT (ST_SummaryStats(rast, 1)).* FROM prism WHERE rid = 54)
SELECT count, sum, round(mean::numeric, 2) AS mean, round(stddev::numeric, 2) AS stddev, min, max
FROM stats;
上述代码的输出如下:

在汇总统计中,如果count指示小于10,000 (100²),则表示该栅格是 10,000-count/100。在这种情况下,栅格瓦片大约是 0% NODATA。
让我们看看如何使用ST_Histogram()来分布栅格瓦片的值:
WITH hist AS (
SELECT (ST_Histogram(rast, 1)).* FROM chp05.prism WHERE rid = 54
)
SELECT round(min::numeric, 2) AS min, round(max::numeric, 2) AS max, count, round(percent::numeric, 2) AS percent FROM hist
ORDER BY min;
输出将如下所示:

看起来大约 78%的所有值都在1370.50或以下。另一种查看像素值分布的方法是使用ST_Quantile():
SELECT (ST_Quantile(rast, 1)).*
FROM chp05.prism
WHERE rid = 54;
上述代码的输出如下:

让我们看看使用ST_ValueCount()在栅格瓦片中出现的最频繁的 10 个值:
SELECT (ST_ValueCount(rast, 1)).*
FROM chp05.prism WHERE rid = 54
ORDER BY count DESC, value
LIMIT 10;
代码的输出如下:

ST_ValueCount允许其他参数组合,这将允许将值四舍五入以聚合一些结果,但必须定义要查找的前一个值子集;例如,以下代码将计算值2、3、2.5、5.612999和4.176四舍五入到第五位小数0.00001的出现次数:
SELECT (ST_ValueCount(rast, 1, true, ARRAY[2,3,2.5,5.612999,4.176]::double precision[] ,0.0001)).*
FROM chp05.prism
WHERE rid = 54
ORDER BY count DESC, value
LIMIT 10;
结果显示了与数组中四舍五入值相似出现的元素数量。从上一个图例中借用的两个值,证实了计数:

它是如何工作的...
在本食谱的第一部分,我们查看了prism栅格表和单个栅格瓦片的元数据。我们专注于单个栅格瓦片来运行各种统计。这些统计提供了一些关于数据外观的线索。
我们提到,当我们查看ST_SummaryStats()的输出时,像素值看起来是错误的。这个问题在后续的统计函数输出中也持续存在。我们还发现这些值是以摄氏度表示的。在下一个菜谱中,我们将使用地图代数操作重新计算所有像素值,使其成为它们的真实值。
执行简单的地图代数操作
在上一个菜谱中,我们看到了 PRISM 栅格中的值对于温度值看起来并不正确。在查看 PRISM 元数据后,我们了解到这些值被100倍缩放了。
在这个菜谱中,我们将处理缩放值以获取真实值。这样做将防止未来的最终用户混淆,这始终是一件好事。
准备工作
PostGIS 提供了两种类型的地图代数函数,这两种函数都返回一个单波段的新栅格。你使用的类型取决于要解决的问题和涉及的栅格带数量。
第一个地图代数函数(ST_MapAlgebra()或ST_MapAlgebraExpr())依赖于一个有效的、用户提供的 PostgreSQL 代数表达式,该表达式为每个像素调用。表达式可以简单到只是一个方程,也可以复杂到是一个逻辑密集型的 SQL 表达式。如果地图代数操作只需要最多两个栅格带,并且表达式不复杂,你应该没有问题使用基于表达式的地图代数函数。
第二个地图代数函数(ST_MapAlgebra()、ST_MapAlgebraFct()或ST_MapAlgebraFctNgb())要求用户为每个像素提供一个适当的 PostgreSQL 函数来调用。被调用的函数可以用 PostgreSQL PL 语言中的任何一种编写(例如,PL/pgSQL、PL/R、PL/Perl),并且可以按照需要复杂。这种类型比表达式地图代数函数类型更具有挑战性,但它具有处理任意数量栅格带的灵活性。
对于这个菜谱,我们只使用基于表达式的地图代数函数ST_MapAlgebra()来创建一个带有华氏温度值的新的波段,然后将这个波段附加到处理过的栅格上。如果你不是使用 PostGIS 2.1 或更高版本,请使用等效的ST_MapAlgebraExpr()函数。
如何操作...
对于任何可能需要花费较长时间或修改存储栅格的操作,最好测试该操作以确保没有错误,并且输出看起来是正确的。
让我们在一个栅格瓦片上运行ST_MapAlgebra(),并比较地图代数操作前后的摘要统计信息:
WITH stats AS (
SELECT
'before' AS state,
(ST_SummaryStats(rast, 1)).*
FROM chp05.prism
WHERE rid = 54
UNION ALL
SELECT
'after' AS state, (ST_SummaryStats(ST_MapAlgebra(rast, 1, '32BF', '([rast]*9/5)+32', -9999), 1 )).*
FROM chp05.prism
WHERE rid = 54
)
SELECT
state,
count,
round(sum::numeric, 2) AS sum,
round(mean::numeric, 2) AS mean,
round(stddev::numeric, 2) AS stddev,
round(min::numeric, 2) AS min,
round(max::numeric, 2) AS max
FROM stats ORDER BY state DESC;
输出看起来如下:

在ST_MapAlgebra()函数中,我们指定输出栅格的波段将具有32BF像素类型和NODATA值-9999。我们使用表达式'([rast]*9/5)+32'将每个像素值转换为华氏度的新值。在ST_MapAlgebra()评估表达式之前,像素值替换占位符'[rast]'。还有几个其他占位符可用,它们可以在ST_MapAlgebra()文档中找到。
通过查看摘要统计信息并比较处理前后的情况,我们发现地图代数操作是正确的。因此,让我们纠正整个表格。我们将把由ST_MapAlgebra()创建的波段附加到现有栅格上:
UPDATE chp05.prism SET rast = ST_AddBand(rast, ST_MapAlgebra(rast, 1, '32BF', '([rast]*9/5)+32', -999), 1 );
ERROR: new row for relation "prism" violates check constraint " enforce_nodata_values_rast"
SQL 查询将不会工作。为什么?如果你记得,当我们加载 PRISM 栅格时,我们指示raster2pgsql使用-C标志添加标准约束。看起来我们违反了至少其中一条约束。
当安装时,标准约束对表中每个栅格列的每个值强制执行一组规则。这些规则保证每个栅格列值具有相同的(或适当的)属性。标准约束包括以下规则:
-
宽度和高度:此规则说明所有栅格必须具有相同的宽度和高度
-
X 和 Y 比例:此规则说明所有栅格必须具有相同的 X 和 Y 比例
-
SRID:此规则说明所有栅格必须具有相同的 SRID
-
相同对齐:此规则说明所有栅格必须相互对齐
-
最大范围:此规则说明所有栅格必须在表的最大范围内
-
波段数量:此规则说明所有栅格必须具有相同的波段数量
-
NODATA 值:此规则说明特定索引处的所有栅格波段必须具有相同的 NODATA 值
-
Out-db:此规则说明特定索引处的所有栅格波段必须是
in-db或out-db,不能同时是两者 -
像素类型:此规则说明特定索引处的所有栅格波段必须是相同的像素类型
错误信息表明我们违反了out-db约束。但我们不能接受这个错误信息,因为我们没有做任何与out-db相关的事情。我们只是向栅格添加了第二个波段。添加第二个波段违反了out-db约束,因为该约束是为栅格中的一个波段准备的,而不是具有两个波段的栅格。
我们必须删除约束,进行更改,并重新应用约束:
SELECT DropRasterConstraints('chp05', 'prism', 'rast'::name);
执行此命令后,我们将得到以下输出,显示约束已被删除:

UPDATE chp05.prism SET rast = ST_AddBand(rast, ST_MapAlgebra(rast, 1, '32BF', ' ([rast]*9/5)+32', -9999), 1);
SELECT AddRasterConstraints('chp05', 'prism', 'rast'::name);
UPDATE操作将花费一些时间,输出将如下所示,显示约束已再次添加:

输出中提供的信息不多,因此我们将检查栅格。我们将查看一个栅格瓦片:
SELECT (ST_Metadata(rast)).numbands
FROM chp05.prism
WHERE rid = 54;
输出如下:

栅格有两个波段。以下为这两个波段的详细信息:
SELECT 1 AS bandnum, (ST_BandMetadata(rast, 1)).*
FROM chp05.prism
WHERE rid = 54
UNION ALL
SELECT 2 AS bandnum, (ST_BandMetadata(rast, 2)).*
FROM chp05.prism
WHERE rid = 54
ORDER BY bandnum;
输出看起来如下:

第一个波段与具有正确属性(32BF 像素类型和 -9999 的 NODATA value)的新第二个波段相同,这是我们调用 ST_MapAlgebra() 时指定的。然而,真正的测试是查看汇总统计:
WITH stats AS (
SELECT
1 AS bandnum,
(ST_SummaryStats(rast, 1)).*
FROM chp05.prism
WHERE rid = 54
UNION ALL
SELECT
2 AS bandnum,
(ST_SummaryStats(rast, 2)).*
FROM chp05.prism
WHERE rid = 54
)
SELECT
bandnum,
count,
round(sum::numeric, 2) AS sum,
round(mean::numeric, 2) AS mean,
round(stddev::numeric, 2) AS stddev,
round(min::numeric, 2) AS min,
round(max::numeric, 2) AS max
FROM stats ORDER BY bandnum;
输出如下:

汇总统计显示,在将波段 1 的值转换为华氏度后,波段 2 是正确的;也就是说,波段 1 的平均温度为 6.05 摄氏度,波段 2 中的华氏度为 42.90)。
它是如何工作的...
在这个食谱中,我们使用 ST_MapAlgebra() 应用了一个简单的地图代数操作来纠正像素值。在后面的食谱中,我们将展示一个高级地图代数操作,以展示 ST_MapAlgebra() 的强大功能。
将几何形状与栅格结合进行分析
在前两个食谱中,我们只对一个栅格瓦片运行了基本统计。虽然对特定栅格运行操作很好,但对于回答实际问题并不很有帮助。在这个食谱中,我们将使用几何形状来过滤、裁剪和合并栅格瓦片,以便我们可以回答特定区域的问题。
准备中
我们将使用之前导入到 sfpoly 表中的旧金山边界几何形状。如果您尚未导入边界,请参阅本章的第一个食谱以获取说明。
如何操作...
由于我们将要查看旧金山的栅格,一个简单的问题就是:2017 年 3 月旧金山的平均温度是多少?看看以下代码:
SELECT (ST_SummaryStats(ST_Union(ST_Clip(prism.rast, 1, ST_Transform(sf.geom, 4269), TRUE)), 1)).mean
FROM chp05.prism
JOIN chp05.sfpoly sf ON ST_Intersects(prism.rast, ST_Transform(sf.geom, 4269))
WHERE prism.month_year = '2017-03-01'::date;
在前面的 SQL 查询中,有四个需要注意的项目,如下所述:
-
ST_Transform():此方法将几何形状的坐标从一个空间参考系统转换为另一个。转换几何形状通常比转换栅格更快。转换栅格需要重采样像素值,这是一个计算密集型过程,可能会引入不希望的结果。如果可能,始终在转换栅格之前转换几何形状,因为空间连接需要使用相同的 SRID。 -
ST_Intersects():在JOIN ON子句中找到的ST_Intersects()方法测试栅格瓦片和几何形状是否在空间上相交。它将使用任何可用的空间索引。根据安装的 PostGIS 版本,ST_Intersects()将在比较两个输入之前隐式地将输入几何形状转换为栅格(PostGIS 2.0),或将输入栅格转换为几何形状(PostGIS 2.1)。 -
ST_Clip(): 此方法仅将每个相交的栅格瓦片裁剪到与几何形状相交的区域。它消除了不属于几何形状空间部分的像素。与ST_Intersects()类似,在裁剪之前,几何形状隐式转换为栅格。 -
ST_Union(): 此方法聚合并合并裁剪的栅格瓦片,以便进行进一步处理。
下面的输出显示了旧金山的平均最低温度:

2017 年 3 月的旧金山真的很冷。那么,2017 年的其余时间又是怎样的呢?旧金山是不是一直都很冷?
SELECT prism.month_year, (ST_SummaryStats(ST_Union(ST_Clip(prism.rast, 1, ST_Transform(sf.geom, 4269), TRUE)), 1)).mean
FROM chp05.prism
JOIN chp05.sfpoly sf ON ST_Intersects(prism.rast, ST_Transform(sf.geom, 4269))
GROUP BY prism.month_year
ORDER BY prism.month_year;
与之前的 SQL 查询相比,唯一的改变是删除了WHERE子句并添加了GROUP BY子句。由于ST_Union()是一个聚合函数,我们需要按month_year对裁剪的栅格进行分组。
输出如下:

根据结果,2017 年晚夏月份是最热的,尽管温差并不大。
它是如何工作的...
通过使用几何形状来过滤棱柱表中的栅格,只需对与几何形状相交并联合计算平均值的少量栅格进行裁剪。这最大化了查询性能,更重要的是,提供了我们问题的答案。
栅格和几何形状之间的转换
在上一个菜谱中,我们使用几何形状来过滤和裁剪仅到感兴趣区域的栅格。ST_Clip()和ST_Intersects()函数在将其与栅格关联之前隐式转换了几何形状。
PostGIS 提供了几个将栅格转换为几何形状的函数。根据函数的不同,像素可以返回为一个区域或一个点。
PostGIS 提供了一个将几何形状转换为栅格的函数。
准备中
在这个菜谱中,我们将转换栅格到几何形状,并将几何形状转换为栅格。我们将使用ST_DumpAsPolygons()和ST_PixelsAsPolygons()函数将栅格转换为几何形状。然后,我们将使用ST_AsRaster()将几何形状转换为栅格。
如何做到这一点...
让我们调整上一个菜谱中使用的查询的一部分,以找出旧金山的平均最低温度。我们将ST_SummaryStats()替换为ST_DumpAsPolygons(),然后以WKT返回几何形状:
WITH geoms AS (SELECT ST_DumpAsPolygons(ST_Union(ST_Clip(prism.rast, 1, ST_Transform(sf.geom, 4269), TRUE)), 1 ) AS gv
FROM chp05.prism
JOIN chp05.sfpoly sf ON ST_Intersects(prism.rast, ST_Transform(sf.geom, 4269))
WHERE prism.month_year = '2017-03-01'::date )
SELECT (gv).val, ST_AsText((gv).geom) AS geom
FROM geoms;
输出如下:

现在,将ST_DumpAsPolygons()函数替换为ST_PixelsAsPolyons():
WITH geoms AS (SELECT (ST_PixelAsPolygons(ST_Union(ST_Clip(prism.rast, 1, ST_Transform(sf.geom, 4269), TRUE)), 1 )) AS gv
FROM chp05.prism
JOIN chp05.sfpoly sf ON ST_Intersects(prism.rast, ST_Transform(sf.geom, 4269))
WHERE prism.month_year = '2017-03-01'::date)
SELECT (gv).val, ST_AsText((gv).geom) AS geom
FROM geoms;
输出如下:

再次,查询结果已被裁剪。重要的是返回的行数。ST_PixelsAsPolygons()返回的几何形状比ST_DumpAsPolygons()多得多。这是由于每个函数中使用的不同机制造成的。
以下图像显示了 ST_DumpAsPolygons() 和 ST_PixelsAsPolygons() 之间的区别。ST_DumpAsPolygons() 函数仅输出具有值的像素,并将这些像素合并在一起。ST_PixelsAsPolygons() 函数不合并像素,并输出所有像素,如下面的图示所示:

ST_PixelsAsPolygons() 函数为每个像素返回一个几何图形。如果有 100 个像素,将有 100 个几何图形。ST_DumpAsPolygons() 的每个几何图形是具有相同值的区域中所有像素的并集。如果有 100 个像素,可能有最多 100 个几何图形。
ST_PixelAsPolygons() 和 ST_DumpAsPolygons() 之间有一个显著的区别。与 ST_DumpAsPolygons() 不同,ST_PixelAsPolygons() 对于具有 NODATA 值的像素返回一个几何图形,并且 val 列具有空值。
让我们使用 ST_AsRaster() 将一个几何图形转换为栅格。我们插入 ST_AsRaster() 以返回一个像素大小为 100 米乘以-100 米的栅格,包含像素类型 8BUI 的四个波段。这些波段中的每一个都将具有像素 NODATA 值 0,以及特定的像素值(每个波段分别为 29、194、178 和 255)。像素大小的单位由几何图形的投影确定,这也是创建的栅格的投影:
SELECT ST_AsRaster(
sf.geom,
100., -100.,
ARRAY['8BUI', '8BUI', '8BUI', '8BUI']::text[],
ARRAY[29, 194, 178, 255]::double precision[],
ARRAY[0, 0, 0, 0]::double precision[]
)
FROM sfpoly sf;
如果我们将生成的旧金山边界栅格和源几何图形可视化,并将它们叠加,我们得到以下结果,这是使用 ST_AsRaster() 将旧金山边界几何图形转换为栅格的放大视图:

虽然几何图形现在是栅格很棒,但将生成的栅格与其他栅格相关联需要额外的处理。这是因为生成的栅格和其他栅格很可能不会对齐。如果两个栅格没有对齐,大多数 PostGIS 栅格函数将无法工作。以下图示显示了两个未对齐的栅格(简化为像素网格):

栅格 1 和栅格 2 的像素网格未对齐。如果栅格对齐,一个网格的单元格边缘将位于另一个网格单元格边缘的上方。
当需要将几何图形转换为栅格以与现有栅格相关联时,在调用 ST_AsRaster() 时使用该现有栅格作为参考:
SELECT ST_AsRaster(
sf.geom, prism.rast,
ARRAY['8BUI', '8BUI', '8BUI', '8BUI']::text[],
ARRAY[29, 194, 178, 255]::double precision[],
ARRAY[0, 0, 0, 0]::double precision[]
)
FROM chp05.sfpoly sf
CROSS JOIN chp05.prism
WHERE prism.rid = 1;
在前面的查询中,我们使用 rid = 1 的栅格瓦片作为我们的参考栅格。ST_AsRaster() 函数使用参考栅格的元数据来创建几何图形的栅格。如果几何图形和参考栅格具有不同的 SRID,几何图形在创建栅格之前将转换为相同的 SRID。
它是如何工作的...
在这个菜谱中,我们将栅格转换为几何图形。我们还从几何图形创建了新的栅格。在栅格和几何图形之间进行转换的能力允许使用其他情况下不可能使用的函数。
使用 GDAL VRT 处理和加载栅格
虽然 PostGIS 有许多用于处理栅格数据的函数,但在某些情况下,在将它们导入数据库之前在源栅格上工作可能更方便、更高效。在数据库外处理栅格数据更有效的情况之一是栅格包含子数据集,通常在 HDF4、HDF5 和 NetCDF 文件中找到。
准备工作
在此配方中,我们将使用 GDAL VRT 格式预处理 MODIS 栅格数据,以过滤和重新排列子数据集。内部,VRT 文件由 XML 标签组成。这意味着我们可以使用任何文本编辑器创建 VRT 文件。但由于手动创建 VRT 文件可能很繁琐,我们将使用 gdalbuildvrt 工具。
我们使用的 MODIS 栅格数据由 NASA 提供,可在源代码包中找到。
您需要使用具有 HDF4 支持构建的 GDAL 继续此配方,因为 MODIS 栅格数据通常以 HDF4-EOS 格式存储。
以下截图显示了本配方和下一个两个配方中使用的 MODIS 栅格。在以下图像中,我们可以看到加利福尼亚州、内华达州、亚利桑那州和下加利福尼亚州的部分地区:

为了让 PostGIS 正确支持 MODIS 栅格数据,我们还需要将 MODIS Sinusoidal 投影添加到 spatial_ref_sys 表中。
如何操作...
在命令行中,导航到 MODIS 目录:
> cd C:\postgis_cookbook\data\chap05\MODIS
在 MODIS 目录中,应该有几个文件。其中之一文件名为 srs.sql,包含用于 MODIS Sinusoidal 投影所需的 INSERT 语句。运行 INSERT 语句:
> psql -d postgis_cookbook -f srs.sql
主文件扩展名为 HDF。让我们检查该 HDF 文件的元数据:
> gdalinfo MYD09A1.A2012161.h08v05.005.2012170065756.hdf
当运行时,gdalinfo 会输出大量信息。我们在 Subdatasets 部分寻找找到的子数据集列表:
Subdatasets:

每个子数据集是 MODIS 栅格在本书源代码中的一个变量。就我们的目的而言,我们只需要前四个子数据集,如下所示:
-
子数据集 1:620 - 670 nm(红色)
-
子数据集 2:841 - 876 nm(近红外或 NIR)
-
子数据集 3:459 - 479 nm(蓝色)
-
子数据集 4:545 - 565 nm(绿色)
VRT 格式允许我们选择要包含在 VRT 栅格中的子数据集以及更改子数据集的顺序。我们希望重新排列子数据集,使它们按照 RGB 顺序排列。
让我们调用 gdalbuildvrt 来为我们的 MODIS 栅格创建一个 VRT 文件。不要运行以下命令!
> gdalbuildvrt -separate modis.vrt
HDF4_EOS:EOS_GRID:"MYD09A1.A2012161.h08v05.005.2012170065756.hdf":MOD_Grid_500m_Surface_Reflectance:sur_refl_b01
HDF4_EOS:EOS_GRID:"MYD09A1.A2012161.h08v05.005.2012170065756.hdf":MOD_Grid_500m_Surface_Reflectance:sur_refl_b04
HDF4_EOS:EOS_GRID:"MYD09A1.A2012161.h08v05.005.2012170065756.hdf":MOD_Grid_500m_Surface_Reflectance:sur_refl_b03
HDF4_EOS:EOS_GRID:"MYD09A1.A2012161.h08v05.005.2012170065756.hdf":MOD_Grid_500m_Surface_Reflectance:sur_refl_b02
我们真心希望您没有运行前面的代码。该命令确实有效,但太长且繁琐。如果能通过一个文件来指定要包含的子数据集及其在 VRT 中的顺序会更好。幸运的是,gdalbuildvrt 提供了这样的选项,即 -input_file_list 标志。
在 MODIS 目录中,可以使用 -input_file_list 标志将 modis.txt 文件传递给 gdalbuildvrt。modis.txt 文件的每一行都是一个子数据集的名称。文本文件中子数据集的顺序决定了每个子数据集在 VRT 中的位置:
HDF4_EOS:EOS_GRID:"MYD09A1.A2012161.h08v05.005.2012170065756.hdf":MOD_Grid_500m_Surface_Reflectance:sur_refl_b01
HDF4_EOS:EOS_GRID:"MYD09A1.A2012161.h08v05.005.2012170065756.hdf":MOD_Grid_500m_Surface_Reflectance:sur_refl_b04
HDF4_EOS:EOS_GRID:"MYD09A1.A2012161.h08v05.005.2012170065756.hdf":MOD_Grid_500m_Surface_Reflectance:sur_refl_b03
HDF4_EOS:EOS_GRID:"MYD09A1.A2012161.h08v05.005.2012170065756.hdf":MOD_Grid_500m_Surface_Reflectance:sur_refl_b02
现在,按照以下方式调用 gdalbuildvrt 命令,使用 modis.txt:
> gdalbuildvrt -separate -input_file_list modis.txt modis.vrt
随意使用您喜欢的文本编辑器检查生成的 modis.vrt VRT 文件。由于 VRT 文件的内容仅仅是 XML 标签,因此添加、更改和删除内容都很简单。
在将处理过的 MODIS 栅格导入 PostGIS 之前,我们最后要做的最后一件事是将 VRT 文件转换为 GeoTIFF 文件,使用 gdal_translate 工具,因为并非所有应用程序都内置了对 HDF4、HDF5、NetCDF 或 VRT 的支持,而 GeoTIFF 具有更好的可移植性:
> gdal_translate -of GTiff modis.vrt modis.tif
最后,使用 raster2pgsql 导入 modis.tif:
> raster2pgsql -s 96974 -F -I -C -Y modis.tif chp05.modis | psql -d postgis_cookbook
raster2pgsql 支持一系列输入格式。您可以使用带有 -G 选项的命令查看完整的列表。
工作原理...
这个配方完全是关于将 MODIS 栅格处理成适合在 PostGIS 中使用的格式。我们使用了 gdalbuildvrt 工具来创建我们的 VRT。作为额外的好处,我们使用了 gdal_translate 在栅格格式之间进行转换;在这种情况下,从 VRT 转换到 GeoTIFF。
如果您特别有冒险精神,尝试使用 gdalbuildvrt 创建一个包含 12 个 PRISM 栅格的 VRT 文件,每个栅格作为一个单独的波段。
变形和重采样栅格
在之前的配方中,我们处理了一个 MODIS 栅格,仅提取那些有意义的子数据集,并按更合适的顺序排列。一旦提取完成,我们就将 MODIS 栅格导入到它自己的表中。
在这里,我们利用 PostGIS 提供的变形能力。这包括从简单地将 MODIS 栅格转换到更合适的投影,到通过重采样像素大小创建概述。
准备工作
我们将使用几个 PostGIS 变形函数,特别是 ST_Transform() 和 ST_Rescale()。ST_Transform() 函数将栅格重新投影到新的空间参考系统(例如,从 WGS84 到 NAD83)。ST_Rescale() 函数缩小或放大栅格的像素大小。
如何操作...
我们首先要做的是转换我们的栅格,因为 MODIS 栅格有自己的独特空间参考系统。我们将栅格从 MODIS Sinusoidal 投影转换为美国国家地图等面积投影(SRID 2163)。
在转换栅格之前,我们将使用旧金山的边界几何形状裁剪 MODIS 栅格。通过在转换之前裁剪栅格,操作所需的时间比先转换然后裁剪栅格所需的时间要少:
SELECT ST_Transform(ST_Clip(m.rast, ST_Transform(sf.geom, 96974)), 2163)
FROM chp05.modis m
CROSS JOIN chp05.sfpoly sf;
以下图像显示了裁剪后的 MODIS 栅格,上面有旧金山的边界以供比较:

当我们在 MODIS 栅格上调用ST_Transform()时,我们只传递了目标SRID 2163。我们可以指定其他参数,例如重采样算法和误差容限。默认的重采样算法和误差容限设置为NearestNeighbor和0.125。使用不同的算法和/或降低误差容限可能会提高重采样栅格的质量,但会增加处理时间。
让我们再次转换 MODIS 栅格,这次指定重采样算法和误差容限分别为Cubic和0.05。我们还指出,转换后的栅格必须与参考栅格对齐:
SELECT ST_Transform(ST_Clip(m.rast, ST_Transform(sf.geom, 96974)),
prism.rast, 'cubic', 0.05)
FROM chp05.modis m
CROSS JOIN chp05.prism
CROSS JOIN chp05.sfpoly sf
WHERE prism.rid = 1;
与之前转换 MODIS 栅格的查询不同,让我们创建一个概览。概览是源栅格的低分辨率版本。如果你熟悉金字塔,概览是金字塔的第一层,而源栅格是基础层:
WITH meta AS (SELECT (ST_Metadata(rast)).* FROM chp05.modis)
SELECT ST_Rescale(modis.rast, meta.scalex * 4., meta.scaley * 4., 'cubic') AS rast
FROM chp05.modis
CROSS JOIN meta;
概览是原始 MODIS 栅格分辨率的 25%。这意味着放大了四倍,宽度和高度缩小了四分之一。为了避免硬编码所需的 X 轴和 Y 轴比例,我们使用ST_Metadata()返回的 MODIS 栅格的 X 轴和 Y 轴比例。如图所示,概览的分辨率更粗糙:

由于分辨率降低,重采样后的栅格像素化更明显。
它是如何工作的...
使用 PostGIS 的一些重采样功能,我们使用ST_Transform()将 MODIS 栅格投影到不同的空间参考系,并控制了投影栅格的质量。我们还使用ST_Rescale()创建了一个概览。
使用这些函数和其他 PostGIS 重采样函数,你应该能够操作所有栅格。
执行高级地图代数操作
在之前的配方中,我们使用了基于表达式的地图代数函数ST_MapAlgebra()将 PRISM 像素值转换为它们的真实值。基于表达式的ST_MapAlgebra()方法易于使用,但仅限于最多操作两个栅格波段。这限制了ST_MapAlgebra()函数在需要超过两个输入栅格波段的过程(如归一化植被指数(NDVI)和增强植被指数(EVI))中的有用性。
有一种ST_MapAlgebra()的变体旨在支持无限数量的输入栅格波段。这种ST_MapAlgebra()变体不采用表达式,而是需要一个回调函数。这个回调函数为每一组输入像素值运行,并返回新的像素值,或者对于输出像素返回NULL。此外,这种ST_MapAlgebra()变体还允许对邻域(围绕中心像素的像素集)进行操作。
PostGIS 附带了一组现成的ST_MapAlgebra()回调函数。所有这些函数都是用于邻域计算,例如计算邻域的平均值或插值空像素值。
准备工作
我们将使用 MODIS 栅格数据计算 EVI。EVI 是一个由红、蓝和近红外波段组成的三波段操作。要对三个波段执行ST_MapAlgebra()操作,需要 PostGIS 2.1 或更高版本。
如何操作...
要在超过两个波段上使用ST_MapAlgebra(),我们必须使用回调函数变体。这意味着我们需要创建一个回调函数。回调函数可以用任何 PostgreSQL PL 语言编写,例如 PL/pgSQL 或 PL/R。我们的回调函数都是用 PL/pgSQL 编写的,因为这种语言总是包含在基本的 PostgreSQL 安装中。
我们的回调函数使用以下方程来计算三波段 EVI:

以下代码实现了 SQL 中的 MODIS EVI 函数:
CREATE OR REPLACE FUNCTION chp05.modis_evi(value double precision[][][], "position" int[][], VARIADIC userargs text[])
RETURNS double precision
AS $$
DECLARE
L double precision;
C1 double precision;
C2 double precision;
G double precision;
_value double precision[3];
_n double precision;
_d double precision;
BEGIN
-- userargs provides coefficients
L := userargs[1]::double precision;
C1 := userargs[2]::double precision;
C2 := userargs[3]::double precision;
G := userargs[4]::double precision;
-- rescale values, optional
_value[1] := value[1][1][1] * 0.0001;
_value[2] := value[2][1][1] * 0.0001;
_value[3] := value[3][1][1] * 0.0001;
-- value can't be NULL
IF
_value[1] IS NULL OR
_value[2] IS NULL OR
_value[3] IS NULL
THEN
RETURN NULL;
END IF;
-- compute numerator and denominator
_n := (_value[3] - _value[1]);
_d := (_value[3] + (C1 * _value[1]) - (C2 * _value[2]) + L);
-- prevent division by zero
IF _d::numeric(16, 10) = 0.::numeric(16, 10) THEN
RETURN NULL;
END IF;
RETURN G * (_n / _d);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
如果您无法创建函数,那么您可能没有数据库中必要的权限。
所有回调函数都需要满足一些特性。具体如下:
- 所有
ST_MapAlgebra()回调函数都必须有三个输入参数,即double precision[]、integer[]和variadic text[]。value参数是一个三维数组,其中第一个维度表示栅格索引,第二个维度表示 Y 轴,第三个维度表示 X 轴。位置参数是一个二维数组,第一个维度表示栅格索引,第二个维度包含中心像素的 X、Y 坐标。最后一个参数userargs是一个包含用户想要传递给callback函数的值的 0 个或多个元素的 1D 数组。如果可视化,参数看起来如下:
value = ARRAY[ 1 =>
[ -- raster 1
[pixval, pixval, pixval], -- row of raster 1
[pixval, pixval, pixval],
[pixval, pixval, pixval]
],
2 => [ -- raster 2
[pixval, pixval, pixval], -- row of raster 2
[pixval, pixval, pixval],
[pixval, pixval, pixval]
],
...
N => [ -- raster N
[pixval, pixval, pixval], -- row of raster
[pixval, pixval, pixval],
[pixval, pixval, pixval]
]
];
pos := ARRAY[
0 => [x-coordinate, y-coordinate], -- center pixel o f output raster
1 => [x-coordinate, y-coordinate], -- center pixel o f raster 1
2 => [x-coordinate, y-coordinate], -- center pixel o f raster 2
...
N => [x-coordinate, y-coordinate], -- center pixel o f raster N
];
userargs := ARRAY[
'arg1',
'arg2',
...
'argN'
];
- 所有
ST_MapAlgebra()回调函数都必须返回一个双精度值。
如果回调函数的结构不正确,ST_MapAlgebra()函数将失败或表现不正确。
在函数体中,我们将用户参数转换为正确的数据类型,调整像素值,检查没有像素值是NULL(与NULL值的算术运算总是导致NULL),计算 EVI 的分子和分母组件,检查分母不是零(防止除以零),然后完成 EVI 的计算。
现在我们使用ST_MapAlgebra()调用我们的回调函数modis_evi():
SELECT ST_MapAlgebra(rast, ARRAY[1, 3, 4]::int[], -- only use the red, blue a
nd near infrared bands 'chp05.modis_evi(
double precision[], int[], text[])'::regprocedure,
-- signature for callback function '32BF',
-- output pixel type 'FIRST',
NULL, 0, 0, '1.', -- L '6.', -- C1 '7.5', -- C2 '2.5' -- G
) AS rast
FROM modis m;
在我们对ST_MapAlgebra()的调用中,有三个需要注意的标准,具体如下:
-
modis_evi()回调函数的签名。当将回调函数传递给ST_MapAlgebra()时,它必须写成包含函数名和输入参数类型的字符串。 -
最后四个函数参数(
'1.','6.','7.5','2.5')是用户定义的参数,传递给回调函数进行处理。 -
波段编号的顺序会影响传递给回调函数的像素值顺序。
以下图像显示了运行 EVI 操作前后的 MODIS 栅格图像。EVI 栅格应用了从浅白色到深绿色的颜色映射,以突出植被茂盛的区域:

如果您无法运行标准的 EVI 操作,或者想要更多的练习,我们现在将计算一个两波段 EVI。我们将使用 ST_MapAlgebraFct() 函数。请注意,ST_MapAlgebraFct() 在 PostGIS 2.1 中已被弃用,并且可能在未来的版本中删除。
对于两波段 EVI,我们将使用以下 callback 函数。两波段 EVI 方程式通过以下代码计算:
CREATE OR REPLACE FUNCTION chp05.modis_evi2(value1 double precision, value2 double precision, pos int[], VARIADIC userargs text[])
RETURNS double precision
AS $$
DECLARE
L double precision;
C double precision;
G double precision;
_value1 double precision;
_value2 double precision;
_n double precision;
_d double precision;
BEGIN
-- userargs provides coefficients
L := userargs[1]::double precision;
C := userargs[2]::double precision;
G := userargs[3]::double precision;
-- value can't be NULL
IF
value1 IS NULL OR
value2 IS NULL
THEN
RETURN NULL;
END IF;
_value1 := value1 * 0.0001;
_value2 := value2 * 0.0001;
-- compute numerator and denominator
_n := (_value2 - _value1);
_d := (L + _value2 + (C * _value1));
-- prevent division by zero
IF _d::numeric(16, 10) = 0.::numeric(16, 10) THEN
RETURN NULL;
END IF;
RETURN G * (_n / _d);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
与 ST_MapAlgebra() 回调函数类似,ST_MapAlgebraFct() 要求回调函数以特定的方式组织。ST_MapAlgebraFct() 的回调函数与 ST_MapAlgebra() 的先前回调函数之间有一个区别。此函数有两个简单的像素值参数,而不是所有像素值的数组:
SELECT ST_MapAlgebraFct(
rast, 1, -- red band
rast, 4, -- NIR band
'modis_evi2(double precision, double precision, int[], text[])'::regprocedure,
-- signature for callback function '32BF', -- output pixel type 'FIRST',
'1.', -- L '2.4', -- C '2.5' -- G) AS rast
FROM chp05.modis m;
除了函数名不同之外,ST_MapAlgebraFct() 的调用方式与 ST_MapAlgebra() 不同。相同的栅格被传递给 ST_MapAlgebraFct() 两次。另一个区别是传递给回调函数的用户定义参数少一个,因为两波段 EVI 的系数少一个。
它是如何工作的...
我们通过计算 MODIS 栅格的三波段和两波段 EVI 来展示了 PostGIS 地图代数函数的一些高级用法。这是通过分别使用 ST_MapAlgebra() 和 ST_MapAlgebraFct() 实现的。通过一些规划,PostGIS 的地图代数函数可以应用于其他用途,例如边缘检测和对比度拉伸。
为了进行额外的练习,编写自己的回调函数,从 MODIS 栅格生成 NDVI 栅格。NDVI 的方程式为:NDVI = ((IR - R)/(IR + R)),其中 IR 是红外波段的像素值,R* 是红波段的像素值。此指数生成介于 -1.0 和 1.0 之间的值,其中负值通常表示非绿色元素(水、雪、云),而接近零的值表示岩石和荒地。
执行 DEM 操作
PostGIS 包含几个用于数字高程模型(DEM)栅格以解决地形相关问题的函数。虽然这些问题在历史上一直属于水文学领域,但现在它们可以在其他地方找到;例如,从点 A 到点 B 寻找最省油的路线或确定屋顶上太阳能板的最佳位置。PostGIS 2.0 引入了 ST_Slope()、ST_Aspect() 和 ST_HillShade(),而 PostGIS 2.1 添加了新的函数 ST_TRI()、ST_TPI() 和 ST_Roughness(),以及现有高程函数的新变体。
准备工作
在本章的第一个菜谱中,我们将使用加载为 100 x 100 瓦片的 SRTM 栅格,利用它生成坡度和阴影栅格,以旧金山作为我们的兴趣区域。
在“如何做”部分接下来的两个查询中,使用的是仅在 PostGIS 2.1 或更高版本中可用的 ST_Slope() 和 ST_HillShade() 的变体。新变体允许指定自定义范围以限制输入栅格的处理区域。
如何做...
让我们使用 ST_Slope() 从我们 SRTM 栅格瓦片的一个子集中生成一个坡度栅格。坡度栅格计算从一个像素到相邻像素的地面高度变化率:
WITH r AS ( -- union of filtered tiles
SELECT ST_Transform(ST_Union(srtm.rast), 3310) AS rast
FROM chp05.srtm
JOIN chp05.sfpoly sf ON ST_DWithin(ST_Transform(srtm.rast::geometry,
3310), ST_Transform(sf.geom, 3310), 1000)),
cx AS ( -- custom extent
SELECT ST_AsRaster(ST_Transform(sf.geom, 3310), r.rast) AS rast
FROM chp05.sfpoly sf CROSS JOIN r
)
SELECT ST_Clip(ST_Slope(r.rast, 1, cx.rast), ST_Transform(sf.geom, 3310)) AS rast FROM r
CROSS JOIN cx
CROSS JOIN chp05.sfpoly sf;
本查询中的所有空间对象都投影到 加利福尼亚阿尔伯斯投影(SRID 3310),该投影的单位是米。这种投影简化了 ST_DWithin() 的使用,可以将我们的兴趣区域扩展到旧金山边界内 1,000 米的瓦片,从而提高了旧金山边界边缘像素的计算坡度值。我们还使用旧金山的栅格化边界作为自定义范围,以限制计算区域。运行 ST_Slope() 后,我们仅裁剪坡度栅格到旧金山。
我们可以重用 ST_Slope() 查询,并用 ST_HillShade() 替换 ST_Slope() 来创建阴影栅格,显示太阳如何照亮 SRTM 栅格的地形:
WITH r AS ( -- union of filtered tiles
SELECT ST_Transform(ST_Union(srtm.rast), 3310) AS rast
FROM chp05.srtm
JOIN chp05.sfpoly sf ON ST_DWithin(ST_Transform(srtm.rast::geometry,
3310), ST_Transform(sf.geom, 3310), 1000)),
cx AS ( -- custom extent
SELECT ST_AsRaster(ST_Transform(sf.geom, 3310), r.rast) AS rast FROM chp05.sfpoly sf CROSS JOIN r)
SELECT ST_Clip(ST_HillShade(r.rast, 1, cx.rast),ST_Transform(sf.geom, 3310)) AS rast FROM r
CROSS JOIN cx
CROSS JOIN chp05.sfpoly sf;
在这种情况下,ST_HillShade() 可以作为 ST_Slope() 的直接替代,因为我们没有为这两个函数指定任何特殊的输入参数。如果我们需要为 ST_Slope() 或 ST_HillShade() 指定额外的参数,所有更改都仅限于一行。
以下图像显示了处理 ST_Slope() 和 ST_HillShade() 之前和之后的 SRTM 栅格:

如截图所示,坡度和阴影栅格帮助我们更好地理解旧金山的地形。
如果有 PostGIS 2.0,我们仍然可以使用 2.0 的 ST_Slope() 和 ST_HillShade() 来创建坡度和阴影栅格。但你需要注意以下几个差异:
-
ST_Slope()和ST_Aspect()返回的栅格值以弧度为单位,而不是度 -
ST_HillShade()的某些输入参数以弧度表示,而不是度 -
由
ST_Slope()、ST_Aspect()或ST_HillShade()计算出的栅格在所有四边都有一个空的 1 像素边界
我们可以通过删除自定义范围的创建和应用来调整本菜谱开头的 ST_Slope() 查询。由于自定义范围将计算限制在特定区域,无法指定此类约束意味着 PostGIS 2.0 的 ST_Slope() 将执行得更慢:
WITH r AS ( -- union of filtered tiles
SELECT ST_Transform(ST_Union(srtm.rast), 3310) AS rast FROM srtm
JOIN sfpoly sf ON ST_DWithin(ST_Transform(srtm.rast::geometry, 3310),
ST_Transform(sf.geom, 3310), 1000)
)
SELECT ST_Clip(ST_Slope(r.rast, 1), ST_Transform(sf.geom, 3310)) AS rast
FROM r CROSS JOIN sfpoly sf;
工作原理...
PostGIS 中的 DEM 函数使我们能够快速分析我们的 SRTM 栅格。在基本用例中,我们能够交换一个函数用于另一个函数而不会出现任何问题。
这些 DEM 函数令人印象深刻之处在于,它们都是围绕ST_MapAlgebra()的包装器。ST_MapAlgebra()的强大之处在于其适应不同问题的能力。
通过 SQL 共享和可视化栅格
在第四章中,处理矢量数据 - 高级配方,我们使用了gdal_translate将 PostGIS 栅格导出到文件。这提供了一种将文件从一个用户传输到另一个用户或从一个位置传输到另一个位置的方法。这种方法的问题在于,您可能无法访问gdal_translate实用程序。
另一种但同样功能的方法是使用 PostGIS 中可用的ST_AsGDALRaster()函数族。除了ST_AsGDALRaster()之外,PostGIS 还提供了ST_AsTIFF()、ST_AsPNG()和ST_AsJPEG()以支持最常见的栅格文件格式。
为了轻松可视化栅格文件而无需 GIS 应用程序,PostGIS 2.1 及更高版本提供了ST_ColorMap()函数。此函数将内置或用户指定的调色板应用于栅格,当使用ST_AsGDALRaster()导出时,可以使用任何图像查看器查看,例如网页浏览器。
准备工作
在本配方中,我们将使用ST_AsTIFF()和ST_AsPNG()将栅格导出为 GeoTIFF 和 PNG 文件格式,分别。我们还将应用ST_ColorMap(),以便我们可以在任何图像查看器中查看它们。
要在 PostGIS 中启用 GDAL 驱动程序,您应该在pgAdmin中运行以下命令:
SET postgis.gdal_enabled_drivers = 'ENABLE_ALL';
SELECT short_name
FROM ST_GDALDrivers();
以下查询可以在标准 SQL 客户端中运行,例如psql或pgAdminIII;然而,我们无法使用返回的输出,因为输出已转义,而这些客户端无法撤销转义。具有较低级别 API 函数的应用程序可以取消转义查询输出。此类示例包括 PHP 脚本、将记录传递给pg_unescape_bytea()的 pass-a-record 元素,或使用 Psycopg2 的隐式解码在获取记录时使用的 Python 脚本。本章的data目录中可以找到一个示例 PHP 脚本(save_raster_to_file.php)。
如何操作...
假设一位同事要求在夏季月份获取旧金山的月度最低温度数据作为一个单独的栅格文件。这涉及到将我们的 PRISM 栅格限制在六月、七月和八月,将每个月度的栅格裁剪到旧金山的边界内,创建一个包含每个月度栅格作为通道的栅格,然后将组合栅格输出到可移植栅格格式。我们将组合栅格转换为 GeoTIFF 格式:
WITH months AS ( -- extract monthly rasters clipped to San Francisco
SELECT prism.month_year, ST_Union(ST_Clip(prism.rast, 2, ST_Transform(sf.geom, 4269), TRUE)) AS rast
FROM chp05.prism
JOIN chp05.sfpoly sf ON ST_Intersects(prism.rast, ST_Transform(sf.geom, 4269))
WHERE prism.month_year BETWEEN '2017-06-01'::date AND '2017-08-01'::date
GROUP BY prism.month_year
ORDER BY prism.month_year
), summer AS ( -- new raster with each monthly raster as a band
SELECT ST_AddBand(NULL::raster, array_agg(rast)) AS rast FROM months)
SELECT -- export as GeoTIFF ST_AsTIFF(rast) AS content FROM summer;
要过滤我们的 PRISM 栅格,我们使用ST_Intersects()仅保留与旧金山东边界空间相交的栅格瓦片。我们还移除了所有相关月份不是六月、七月或八月的栅格。然后,我们使用ST_AddBand()创建一个新的栅格,其中包含每个夏季月份的新栅格通道。最后,我们将组合栅格传递给ST_AsTIFF()以生成 GeoTIFF。
如果您将 ST_AsTIFF() 返回的值输出到文件,在该文件上运行 gdalinfo。gdalinfo 输出显示 GeoTIFF 文件有三个波段,SRID 4322 的坐标系:
Driver: GTiff/GeoTIFF
Files: surface.tif
Size is 20, 7
Coordinate System is:
GEOGCS["WGS 72",
DATUM["WGS_1972",
SPHEROID["WGS 72",6378135,298.2600000000045, AUTHORITY["EPSG","7043"]],
TOWGS84[0,0,4.5,0,0,0.554,0.2263], AUTHORITY["EPSG","6322"]],
PRIMEM["Greenwich",0], UNIT["degree",0.0174532925199433],
AUTHORITY["EPSG","4322"]]
Origin = (-123.145833333333314,37.937500000000114)
Pixel Size = (0.041666666666667,-0.041666666666667)
Metadata:
AREA_OR_POINT=Area
Image Structure Metadata:
INTERLEAVE=PIXEL
Corner Coordinates:
Upper Left (-123.1458333, 37.9375000) (123d 8'45.00"W, 37d56'15.00"N)
Lower Left (-123.1458333, 37.6458333) (123d 8'45.00"W, 37d38'45.00"N)
Upper Right (-122.3125000, 37.9375000) (122d18'45.00"W, 37d56'15.00"N)
Lower Right (-122.3125000, 37.6458333) (122d18'45.00"W, 37d38'45.00"N)
Center (-122.7291667, 37.7916667) (122d43'45.00"W, 37d47'30.00"N)
Band 1 Block=20x7 Type=Float32, ColorInterp=Gray
NoData Value=-9999
Band 2 Block=20x7 Type=Float32, ColorInterp=Undefined
NoData Value=-9999
Band 3 Block=20x7 Type=Float32, ColorInterp=Undefined
NoData Value=-9999
GeoTIFF 栅格的问题是我们通常无法在标准图像查看器中查看它。如果我们使用 ST_AsPNG() 或 ST_AsJPEG(),生成的图像更容易查看。但 PNG 和 JPEG 图像受支持的像素类型 8BUI 和 16BUI(仅限 PNG)的限制。这两种格式也最多限于三个波段(如果有 alpha 波段,则为四个波段)。
为了帮助克服各种文件格式限制,我们可以使用 ST_MapAlgebra()、ST_Reclass() 或 ST_ColorMap() 来完成这个配方。ST.ColorMap() 函数将任何像素类型的栅格波段转换为最多四个 8BUI 波段。这有助于创建灰度、RGB 或 RGBA 图像,然后传递给 ST_AsPNG() 或 ST_AsJPEG()。
从先前的配方中,我们使用 SRTM 栅格计算旧金山的坡度栅格查询,我们可以应用 ST_ColorMap() 函数的内置颜色映射之一,然后将生成的栅格传递给 ST_AsPNG() 创建 PNG 图像:
WITH r AS (SELECT ST_Transform(ST_Union(srtm.rast), 3310) AS rast
FROM chp05.srtm
JOIN chp05.sfpoly sf ON ST_DWithin(ST_Transform(srtm.rast::geometry, 3310),
ST_Transform(sf.geom, 3310), 1000)
), cx AS (
SELECT ST_AsRaster(ST_Transform(sf.geom, 3310), r.rast) AS rast
FROM sfpoly sf CROSS JOIN r
)
SELECT ST_AsPNG(ST_ColorMap(ST_Clip(ST_Slope(r.rast, 1, cx.rast), ST_Transform(sf.geom, 3310) ), 'bluered')) AS rast
FROM r
CROSS JOIN cx
CROSS JOIN chp05.sfpoly sf;
蓝红颜色映射表将最小、中值和最大像素值分别设置为深蓝色、浅白色和亮红色。介于最小、中值和最大值之间的像素值被分配为从最小到中值或从中值到最大值范围进行线性插值的颜色。生成的图像可以清楚地显示旧金山的最大坡度。
以下是由 ST_ColorMap() 和 ST_AsPNG() 应用蓝红颜色映射表生成的 PNG 图像。红色像素代表最陡峭的坡度:

在我们使用 ST_AsTIFF() 和 ST_AsPNG() 时,我们将要转换的栅格作为唯一参数传递。这两个函数都有额外的参数来定制输出的 TIFF 或 PNG 文件。这些额外的参数包括各种压缩和数据组织设置。
它是如何工作的...
使用 ST_AsTIFF() 和 ST_AsPNG(),我们将从 PostGIS 导出的栅格导出为 GeoTIFF 和 PNG。ST_ColorMap() 函数帮助我们生成可以在任何图像查看器中打开的图像。如果我们需要将这些图像导出为 GDAL 支持的任何其他格式,我们将使用 ST_AsGDALRaster()。
第六章:使用 pgRouting
在本章中,我们将涵盖以下主题:
-
启动 – Dijkstra 路由
-
从 OpenStreetMap 加载数据并使用 A* 寻找最短路径
-
计算驾驶距离/服务区域
-
使用人口统计数据计算驾驶距离
-
提取多边形的中心线
简介
到目前为止,我们已将 PostGIS 作为矢量工具和栅格工具使用,使用相对简单的对象关系和简单结构。在本章中,我们将回顾一个额外的与 PostGIS 相关的扩展:pgRouting。pgRouting 允许我们查询图结构,以回答诸如“我从哪里到我要去的最短路线是什么?”等问题。这是一个现有网络 API(如 Google、Bing、MapQuest 等)和服务高度占据的领域,但我们可以通过自己构建服务来更好地服务于许多用例。哪些用例?在尝试回答现有服务未解决的问题、我们拥有的数据更好或更适用,或者我们需要或希望避免这些 API 的服务条款的情况下,创建自己的服务可能是个好主意。
启动 – Dijkstra 路由
pgRouting 是一个独立的扩展,除了 PostGIS 之外还可以使用,现在它已经包含在 Application Stack Builder 上的 PostGIS 套件中(推荐用于 Windows)。它也可以通过 DEB、RPM、macOS X 软件包和 Windows 可执行文件进行下载和安装,这些文件可在 pgrouting.org/download.html 找到。
对于 macOS 用户,建议您使用 Git 上的源代码包(github.com/pgRouting/pgrouting/releases),并使用 CMake 进行构建安装,CMake 可在 cmake.org/download/ 找到。
Linux Ubuntu 用户的软件包可以在 trac.osgeo.org/postgis/wiki/UsersWikiPostGIS22UbuntuPGSQL95Apt 找到。
准备工作
pgRouting 不太擅长处理非默认模式,因此在我们开始之前,我们将使用以下命令设置我们的用户首选项中的模式:
ALTER ROLE me SET search_path TO chp06,public;
接下来,我们需要将 pgrouting 扩展添加到我们的数据库中。如果 PostGIS 还未安装到数据库中,我们需要将其作为扩展添加:
CREATE EXTENSION postgis;
CREATE EXTENSION pgrouting;
我们将首先加载一个测试数据集。您可以从 docs.pgrouting.org/latest/en/sampledata.html 获取一些非常基本的样本数据。
这个样本数据由一个小街道网格组成,其中可以运行任何功能。
然后,运行数据集网站上的创建表和数据插入脚本。您应该调整以保留 chp06 的模式结构——例如:
CREATE TABLE chp06.edge_table (
id BIGSERIAL,
dir character varying,
source BIGINT,
target BIGINT,
cost FLOAT,
reverse_cost FLOAT,
capacity BIGINT,
reverse_capacity BIGINT,
category_id INTEGER,
reverse_category_id INTEGER,
x1 FLOAT,
y1 FLOAT,
x2 FLOAT,
y2 FLOAT,
the_geom geometry
);
现在数据已加载,让我们在表上构建拓扑(如果您在数据加载过程中还没有这样做):
SELECT pgr_createTopology('chp06.edge_table',0.001);
构建拓扑为我们创建了一个新的节点表—chp06.edge_table_vertices_pgr—以便我们查看。这个表将帮助我们开发查询。
如何做到这一点...
现在数据已加载,我们可以进行快速测试。我们将使用一个简单的算法 Dijkstra 来计算从节点 5 到节点 12 的最短路径。
Dijkstra 算法是一种有效且简单的路由算法,它在网络中从点 A 到点 B 的所有可用路径上运行搜索,也称为图结构。它不是最有效的路由算法,但总是会找到最佳路线。有关 Dijkstra 算法的更多信息,请参考维基百科,它有很好的解释,并配有插图,请参阅en.wikipedia.org/wiki/Dijkstra%27s_algorithm。en.wikipedia.org/wiki/File:Dijkstras_progress_animation.gif中的动画特别有帮助。
需要注意的一个重要点是,在 pgRouting 拓扑创建过程中创建的节点在某些版本中是不经意间创建的。这在未来的版本中已经得到了修复,但对于某些版本的 pgRouting 来说,这意味着您的节点编号将不会与本书中使用的节点编号相同。在应用程序中查看您的数据以确定要使用哪些节点,或者您是否应该使用 k-最近邻搜索来找到最近的静态地理点。有关查看 PostGIS 数据的更多信息,请参阅第十一章,使用桌面客户端,以及有关自动查找最近节点的方法的第四章,处理矢量数据 - 高级食谱:
SELECT * FROM pgr_dijkstra(
'SELECT id, source, target, cost
FROM chp06.edge_table_vertices_pgr', 2, 9,
);
上述查询将产生以下结果:

当我们使用 Dijkstra 和其他路由算法请求路线时,结果通常以下列形式出现:
-
seq:这返回序列号,以便我们可以保持输出的顺序 -
node:这是节点 ID -
edge:这是边 ID -
cost:这是路线遍历的成本(通常是距离) -
agg_cost:这是从起始节点到路线的聚合成本
例如,要获取几何形状,我们需要将边 ID 与原始表重新连接。为了使这种方法透明地工作,我们将使用WITH公共表表达式创建一个临时表,然后我们将将其与我们的几何形状连接:
WITH dijkstra AS (
SELECT pgr_dijkstra(
'SELECT id, source, target, cost, x1, x2, y1, y2
FROM chp06.edge_table', 2, 9
)
)
SELECT id, ST_AsText(the_geom)
FROM chp06.edge_table et, dijkstra d
WHERE et.id = (d.pgr_dijkstra).edge;
上述代码将给出以下输出:

恭喜!您刚刚在 pgRouting 中完成了一条路线。以下图表说明了这一场景:

从 OpenStreetMap 加载数据并使用 A*算法找到最短路径
测试数据对于理解算法的工作原理非常有用,但真实数据通常更有趣。全球真实数据的良好来源是OpenStreetMap(OSM),这是一个全球性的、可访问的、维基风格的地理空间数据集。使用 OSM 与 pgRouting 结合的奇妙之处在于,它本质上是一个拓扑模型,这意味着它在构建时遵循与我们在 pgRouting 中的图遍历相同的规则。由于 OSM 中的编辑和社区参与方式,它通常与商业数据源一样好,甚至更好,并且当然与我们的开源模式非常兼容。
另一个很棒的功能是,有免费的开源软件可以导入 OSM 数据并将其导入到路由数据库中——osm2pgrouting。
准备工作
建议您从我们提供的示例数据集中下载可下载的文件,该数据集可在www.packtpub.com/support找到。您将使用 XML OSM 数据。您也可以直接从网页界面www.openstreetmap.org/或通过使用 overpass turbo 界面访问 OSM 数据(overpass-turbo.eu/)来获取定制提取,但这可能会限制我们能够提取的区域。
一旦我们有了数据,我们需要使用我们喜欢的压缩工具解压它。在 Windows 和 macOS 机器上,双击文件通常可以解压。Linux 上解压的两个好工具是bunzip2和zip。剩下的就是我们想要的用于路由的 XML 数据提取。在我们的用例中,我们正在下载克利夫兰地区的相关数据。
现在我们需要一个工具将数据放入可路由的数据库中。一个这样的工具示例是osm2pgrouting,可以通过github.com/pgRouting/osm2pgrouting上的说明进行下载和编译。使用cmake.org/download/中的 CMake 在 macOS 上构建安装。对于 Linux Ubuntu 用户,有一个可用的包packages.ubuntu.com/artful/osm2pgrouting。
如何操作...
当osm2pgrouting在没有设置任何内容的情况下运行时,输出会显示osm2pgrouting所需的和可用的选项:

要运行osm2pgrouting命令,我们需要一些必要的参数。在运行以下命令之前,请仔细检查指向mapconfig.xml和cleveland.osm的路径:
osm2pgrouting --file cleveland.osm --conf /usr/share/osm2pgrouting/mapconfig.xml --dbname postgis_cookbook --user me --schema chp06 --host localhost --prefix cleveland_ --clean
我们的数据集可能相当大,处理和导入可能需要一些时间——请耐心等待。输出结果的结尾应该类似于以下内容:

我们的新向量表默认命名为 cleveland_ways。如果没有使用 -prefix 标志,表名就只是 ways。
你应该创建了以下表:

它是如何工作的...
osm2pgrouting 是一个强大的工具,它处理将 OSM 数据转换为可用于 pgRouting 的格式的许多翻译工作。在这种情况下,它从我们的输入文件中创建八个表。在这八个表中,我们将关注两个主要表:ways 表和 nodes 表。
我们的 ways 表代表所有在 OSM 中的街道、道路和小径的线条表。节点表包含所有交叉口。这有助于我们识别路由的起点和终点。
让我们应用 A(“A 星”)路由方法来解决这个问题。
A 是迪杰斯特拉算法的一个扩展,它使用启发式方法来加速最短路径的搜索,但偶尔可能无法找到最优路径。有关更多信息,请参阅 en.wikipedia.org/wiki/A* 和 en.wikipedia.org/wiki/File:Astar_progress_animation.gif。
你可以从迪杰斯特拉算法中识别出以下语法:
WITH astar AS (
SELECT * FROM pgr_astar(
'SELECT gid AS id, source, target,
length AS cost, x1, y1, x2, y2
FROM chp06.cleveland_ways', 89475, 14584, false
)
)
SELECT gid, the_geom
FROM chp06.cleveland_ways w, astar a
WHERE w.gid = a.edge;
以下截图显示了在地图上显示的结果(地图瓦片由 *Stamen Design 提供,根据 CC BY 3.0 许可;数据由 *OpenStreetMap 提供,根据 CC BY SA 许可)):

使用 pgRouting 在 QGIS 中可视化的示例计算路线
计算驾驶距离/服务区域
驾驶距离(pgr_drivingDistance)是一个查询,它计算从起始节点到指定驾驶距离内的所有节点。这是一个可选的 pgRouting 函数;因此,如果你自己编译 pgRouting,请确保启用它并包含 CGAL 库,这是 pgr_drivingDistance 的一个可选依赖项。
驾驶距离在需要提供真实驾驶距离估计时很有用,例如,对于所有距离五英里以驾驶、骑行或步行方式到达的客户。这些估计可以与缓冲技术进行对比,缓冲技术假设没有旅行障碍,并且对于揭示与个人位置相关的交通网络的基本结构很有用。
准备工作
我们将加载与 Startup – Dijkstra 路由 菜单中使用的相同数据集。请参考此菜谱以导入数据。
如何操作...
在以下示例中,我们将查看距离起点三单位距离内的所有用户——即节点 2 处的一个提议的自行车店:
SELECT * FROM pgr_drivingDistance(
'SELECT id, source, target, cost FROM chp06.edge_table',
2, 3
);
前面的命令给出了以下输出:

如同往常,我们只是从 pgr_drivingDistance 表中获取一个列表,在这个例子中,它包括序列、节点、边成本和总成本。PgRouting,就像 PostGIS 一样,给我们提供了低级功能;我们需要从这些低级功能中重建我们需要的几何形状。我们可以使用那个节点 ID 来执行以下脚本,从而提取所有节点的几何形状:
WITH DD AS (
SELECT * FROM pgr_drivingDistance(
'SELECT id, source, target, cost
FROM chp06.edge_table', 2, 3
)
)
SELECT ST_AsText(the_geom)
FROM chp06.edge_table_vertices_pgr w, DD d
WHERE w.id = d.node;
前面的命令给出了以下输出:

但我们看到的是只是一个点簇。通常,当我们想到驾驶距离时,我们会将其可视化为一个多边形。幸运的是,我们有 pgr_alphaShape 函数提供了这个功能。这个函数期望输入 id、x 和 y 值,因此我们首先将之前的查询更改为我们之前在 edge_table_vertices_pgr 中的几何形状转换为 x 和 y:
WITH DD AS (
SELECT * FROM pgr_drivingDistance(
'SELECT id, source, target, cost FROM chp06.edge_table',
2, 3
)
)
SELECT id::integer, ST_X(the_geom)::float AS x, ST_Y(the_geom)::float AS y
FROM chp06.edge_table_vertices_pgr w, DD d
WHERE w.id = d.node;
输出如下:

现在我们可以将前面的脚本包裹在 alphashape 函数中:
WITH alphashape AS (
SELECT pgr_alphaShape('
WITH DD AS (
SELECT * FROM pgr_drivingDistance(
''SELECT id, source, target, cost
FROM chp06.edge_table'', 2, 3
)
),
dd_points AS(
SELECT id::integer, ST_X(the_geom)::float AS x,
ST_Y(the_geom)::float AS y
FROM chp06.edge_table_vertices_pgr w, DD d
WHERE w.id = d.node
)
SELECT * FROM dd_points
')
),
因此,首先,我们将获取我们的点簇。正如我们之前所做的那样,我们将明确地将文本转换为几何点:
alphapoints AS (
SELECT ST_MakePoint((pgr_alphashape).x, (pgr_alphashape).y) FROM alphashape
),
现在我们有了点,我们可以通过连接它们来创建一条线:
alphaline AS (
SELECT ST_Makeline(ST_MakePoint) FROM alphapoints
)
SELECT ST_MakePolygon(ST_AddPoint(ST_Makeline, ST_StartPoint(ST_Makeline))) FROM alphaline;
最后,我们使用 ST_MakePolygon 将线构造为多边形。这需要通过执行 ST_StartPoint 来添加起点,以便正确地闭合多边形。完整的代码如下:
WITH alphashape AS (
SELECT pgr_alphaShape('
WITH DD AS (
SELECT * FROM pgr_drivingDistance(
''SELECT id, source, target, cost
FROM chp06.edge_table'', 2, 3
)
),
dd_points AS(
SELECT id::integer, ST_X(the_geom)::float AS x,
ST_Y(the_geom)::float AS y
FROM chp06.edge_table_vertices_pgr w, DD d
WHERE w.id = d.node
)
SELECT * FROM dd_points
')
),
alphapoints AS (
SELECT ST_MakePoint((pgr_alphashape).x,
(pgr_alphashape).y)
FROM alphashape
),
alphaline AS (
SELECT ST_Makeline(ST_MakePoint) FROM alphapoints
)
SELECT ST_MakePolygon(
ST_AddPoint(ST_Makeline, ST_StartPoint(ST_Makeline))
)
FROM alphaline;
我们第一次的驾驶距离计算可以通过以下图表更好地理解,其中我们可以从节点 2 以 3 英里的驾驶距离到达节点 9、11、13:

参见
- 使用人口统计数据计算驾驶距离 菜单
使用人口统计数据计算驾驶距离
在 第二章 的 Using polygon overlays for proportional census estimates 菜单中,我们在 Structures That Work 中使用了一个围绕小径对齐的简单缓冲区,结合人口统计数据来估计小径周围步行距离内的人们的统计数据,估计为 1 英里长。当然,这种方法的问题在于它假设这是一个“直线距离”的估计。实际上,河流、大型道路和没有道路的路段是人们穿越空间的真正障碍。使用 pgRouting 的 pgr_drivingDistance 函数,我们可以在可路由网络上现实地模拟人们的移动,并获得更好的估计。对于我们的用例,我们将保持模拟比小径对齐简单一些——我们将考虑一个公园设施的人口统计数据,比如克利夫兰都会动物园,以及在其 4 英里范围内的潜在自行车使用者,这大约相当于 15 分钟的自行车骑行时间。
准备工作
对于我们的分析,我们将使用第二章,有效结构中的proportional_sum函数,所以如果你还没有将其添加到你的 PostGIS 工具包中,请运行以下命令:
CREATE OR REPLACE FUNCTION chp02.proportional_sum(geometry, geometry, numeric)
RETURNS numeric AS
$BODY$
SELECT $3 * areacalc FROM
(
SELECT (ST_Area(ST_Intersection($1, $2))/ST_Area($2))::numeric AS areacalc
) AS areac
;
$BODY$
LANGUAGE sql VOLATILE;
proportional_sum函数将考虑我们的输入几何形状和人口计数值,并返回比例人口估计。
现在我们需要加载我们的普查数据。使用以下命令:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom census chp06.census | psql -U me -d postgis_cookbook -h localhost
此外,如果你还没有加载从 OpenStreetMap 加载数据并找到最短路径 A配方中提到的数据,请现在花时间加载。
一旦所有数据都输入完毕,我们就可以进行分析了。
如何做到这一点...
我们创建的pgr_drivingdistance多边形是人口分析的第一步。如果你需要熟悉其使用,请参考行驶距离/服务区域计算配方。在这种情况下,我们将考虑骑行距离。根据我们加载的数据集,克利夫兰市立动物园最近的节点是 24746;因此,我们将使用该点作为pgr_drivingdistance计算的中心点,并使用大约 6 公里作为我们的距离,因为我们想知道在克利夫兰市立动物园这个距离内的动物园游客数量。然而,由于我们的数据使用的是 4326 EPSG,我们将给函数提供的距离将以度为单位,因此 0.05 将给我们大约 6 公里的距离,这将与pgr_drivingDistance函数兼容:
CREATE TABLE chp06.zoo_bikezone AS (
WITH alphashape AS (
SELECT pgr_alphaShape('
WITH DD AS (
SELECT * FROM pgr_drivingDistance(
''SELECT gid AS id, source, target, reverse_cost
AS cost FROM chp06.cleveland_ways'',
24746, 0.05, false
)
),
dd_points AS(
SELECT id::int4, ST_X(the_geom)::float8 as x,
ST_Y(the_geom)::float8 AS y
FROM chp06.cleveland_ways_vertices_pgr w, DD d
WHERE w.id = d.node
)
SELECT * FROM dd_points
')
),
alphapoints AS (
SELECT ST_MakePoint((pgr_alphashape).x, (pgr_alphashape).y)
FROM alphashape
),
alphaline AS (
SELECT ST_Makeline(ST_MakePoint) FROM alphapoints
)
SELECT 1 as id, ST_SetSRID(ST_MakePolygon(ST_AddPoint(ST_Makeline, ST_StartPoint(ST_Makeline))), 4326) AS the_geom FROM alphaline
);
前面的脚本给我们一个非常有趣的形状(由Stamen Design设计,根据 CC BY 3.0 许可;数据由OpenStreetMap提供,根据 CC BY SA 许可)。请看以下截图:


在前面的截图中,我们可以看到实际道路网络中骑行的距离(用蓝色阴影表示)与等效的 4 英里缓冲区或直线距离之间的差异。让我们使用以下脚本将此应用于我们的人口分析:
SELECT ROUND(SUM(chp02.proportional_sum(
ST_Transform(a.the_geom,3734), b.the_geom, b.pop))) AS population
FROM Chp06.zoo_bikezone AS a, chp06.census as b
WHERE ST_Intersects(ST_Transform(a.the_geom, 3734), b.the_geom)
GROUP BY a.id;
输出如下:

(1 row)
那么,前面的输出与如果我们查看缓冲距离我们会得到什么相比如何?
SELECT ROUND(SUM(chp02.proportional_sum(
ST_Transform(a.the_geom,3734), b.the_geom, b.pop))) AS population
FROM (SELECT 1 AS id, ST_Buffer(ST_Transform(the_geom, 3734), 17000)
AS the_geom
FROM chp06.cleveland_ways_vertices_pgr WHERE id = 24746
) AS a, chp06.census as b
WHERE ST_Intersects(ST_Transform(a.the_geom, 3734), b.the_geom)
GROUP BY a.id;

(1 row)
前面的输出显示了超过 60,000 人的差异。换句话说,使用缓冲区比使用pgr_drivingdistance高估了人口。
提取多边形的中心线
在第四章,“处理矢量数据 - 高级食谱”中的几个食谱中,我们探讨了从点集提取 Voronoi 多边形。在这个食谱中,我们将使用在“使用外部脚本来嵌入新功能以计算 Voronoi 多边形”部分中使用的 Voronoi 函数,作为提取多边形中心线的第一步。也可以使用“使用外部脚本来嵌入新功能以计算 Voronoi 多边形 - 高级”食谱,它在大数据集上运行得更快。对于这个食谱,我们将使用更简单但更慢的方法。
另一个附加的依赖项是我们将使用来自第二章,“正常化内部叠加”食谱中的chp02.polygon_to_line(geometry)函数。
我们所说的多边形中心线是什么意思?想象一下一条数字化的河流在其两侧之间流动,如下面的截图所示:

如果我们想要找到这个区域的中心以便模拟水流,我们可以使用骨架化方法来提取它,如下面的截图所示:

正如我们很快就会看到的,骨架化方法的困难在于它们通常容易受到噪声的影响,而自然特征,如我们的河流,会产生大量的噪声。这意味着典型的骨架化,如果简单地使用 Voronoi 方法就可以完成,因此对于我们的目的来说本质上是不够的。
这就带我们来到了为什么骨架化方法被包含在本章中的原因。路由是我们简化由 Voronoi 方法得到的骨架的一种方式。它允许我们从主要特征的一端追踪到另一端,并跳过中间的所有噪声。
准备工作
由于我们将使用第四章,“处理矢量数据 - 高级食谱”中“使用外部脚本来嵌入新功能以计算 Voronoi 多边形”食谱中的 Voronoi 计算,你应该参考那个食谱来准备使用本食谱中使用的函数。
我们将使用本书源包中水文文件夹下找到的流数据集。要加载它,请使用以下命令:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom ebrr_polygon chp06.voronoi_hydro | psql -U me -d postgis_cookbook
我们创建的河流将如下截图所示:

如何做到这一点...
为了执行基本的骨架化,我们将计算组成原始河流多边形的节点上的 Voronoi 多边形。默认情况下,Voronoi 多边形的边缘找到点之间的中点所界定的线。我们将利用这种趋势,将我们的线条视为点——向线条添加额外的点,然后将线条转换为点集。这种方法与 Voronoi 方法相结合,将提供多边形中心线的初始估计。
我们将使用ST_Segmentize函数向我们的输入几何体添加额外的点,然后使用ST_DumpPoints将几何体转换为点:
CREATE TABLE chp06.voronoi_points AS(
SELECT (ST_DumpPoints(ST_Segmentize(the_geom, 5))).geom AS the_geom
FROM chp06.voronoi_hydro
UNION ALL
SELECT (ST_DumpPoints(ST_Extent(the_geom))).geom AS the_geom
FROM chp06.voronoi_hydro
)
如果我们在桌面 GIS 上查看,以下截图显示了我们的多边形作为一组点:

前面的截图中的点集是我们输入到 Voronoi 计算中的:
CREATE TABLE chp06.voronoi AS(
SELECT (ST_Dump(
ST_SetSRID(
ST_VoronoiPolygons(points.the_geom),
3734))).geom as the_geom
FROM (SELECT ST_Collect(ST_SetSRID(the_geom, 3734)) as the_geom FROM chp06.voronoi_points) as points);
以下截图显示了从我们的点派生出的 Voronoi 图:

如果您仔细观察前面的截图,您将看到我们新数据中显示的基本中心线。现在我们将迈出第一步来提取它。我们应该对我们的输入进行索引,然后将 Voronoi 输出与原始流多边形相交,以便将数据清理回合理的状态。在提取过程中,我们还将从多边形中提取边缘,并移除原始多边形上的边缘,以便在我们的路由步骤之前移除任何多余的线条。这已在以下脚本中实现:
CREATE INDEX chp06_voronoi_geom_gist
ON chp06.voronoi
USING gist(the_geom);
DROP TABLE IF EXISTS voronoi_intersect;
CREATE TABLE chp06.voronoi_intersect AS WITH vintersect AS (
SELECT ST_Intersection(ST_SetSRID(ST_MakeValid(a.the_geom), 3734),
ST_MakeValid(b.the_geom)) AS the_geom
FROM Chp06.voronoi a, chp06.voronoi_hydro b
WHERE ST_Intersects(ST_SetSRID(a.the_geom, 3734), b.the_geom)
),
linework AS (
SELECT chp02.polygon_to_line(the_geom) AS the_geom
FROM vintersect
),
polylines AS (
SELECT ((ST_Dump(ST_Union(lw.the_geom))).geom)
::geometry(linestring, 3734) AS the_geom
FROM linework AS lw
),
externalbounds AS (
SELECT chp02.polygon_to_line(the_geom) AS the_geom
FROM voronoi_hydro
)
SELECT (ST_Dump(ST_Union(p.the_geom))).geom
FROM polylines p, externalbounds b
WHERE NOT ST_DWithin(p.the_geom, b.the_geom, 5);
现在,我们有了骨骼的第二级近似(如下面的截图所示)。它很杂乱,但它开始突出我们寻求的中心线:

还有更多...
现在,我们几乎准备好进行路由了。我们拥有的中心线计算是对直线骨骼的良好近似,但仍然受到自然世界噪声的影响。我们希望通过选择我们的特征并通过路由强调它们来消除这种噪声。首先,我们需要准备表格以允许进行路由计算,如下面的命令所示:
ALTER TABLE chp06.voronoi_intersect ADD COLUMN gid serial;
ALTER TABLE chp06.voronoi_intersect ADD PRIMARY KEY (gid);
ALTER TABLE chp06.voronoi_intersect ADD COLUMN source integer;
ALTER TABLE chp06.voronoi_intersect ADD COLUMN target integer;
然后,要从我们的骨骼创建可路由的网络,输入以下命令:
SELECT pgr_createTopology('voronoi_intersect', 0.001, 'the_geom', 'gid', 'source', 'target', 'true');
CREATE INDEX source_idx ON chp06.voronoi_intersect("source");
CREATE INDEX target_idx ON chp06.voronoi_intersect("target");
ALTER TABLE chp06.voronoi_intersect ADD COLUMN length double precision;
UPDATE chp06.voronoi_intersect SET length = ST_Length(the_geom);
ALTER TABLE chp06.voronoi_intersect ADD COLUMN reverse_cost double precision;
UPDATE chp06.voronoi_intersect SET reverse_cost = length;
现在,我们可以使用以下命令沿着我们多边形的主要中心线进行路由:
CREATE TABLE chp06.voronoi_route AS
WITH dijkstra AS (
SELECT * FROM pgr_dijkstra('SELECT gid AS id, source, target, length
AS cost FROM chp06.voronoi_intersect', 10851, 3, false)
)
SELECT gid, geom
FROM voronoi_intersect et, dijkstra d
WHERE et.gid = d.edge;
如果我们查看这个路由的细节,我们会看到以下内容:

现在,我们可以将原始多边形与其中心线的轨迹进行比较:

前面的截图显示了流的原几何形状与我们的中心线或骨骼的对比。这是一个非常优秀的输出,它极大地简化了我们的输入几何形状,同时保留了其相关特征。
第七章:进入第 N 维
在本章中,我们将涵盖:
-
导入激光雷达数据
-
在激光雷达点云上执行 3D 查询
-
构建和提供 2.5D 建筑
-
使用 ST_Extrude 扩展建筑底图
-
为 PostGIS 创建任意 3D 对象
-
将模型导出为 X3D 格式以供网页使用
-
使用 PostGIS 3D 重建无人机(UAV)图像底图
-
PostGIS 中的无人机摄影测量 – 点云
-
PostGIS 中的无人机摄影测量 – 创建数字表面模型(DSM)
简介
在本章中,我们将探讨 PostGIS 的 3D 功能。我们将重点关注三个主要类别:如何将 3D 数据插入到 PostGIS 中,如何使用 3D 数据进行分析和执行查询,以及如何将 3D 数据从 PostGIS 中导出。本章将使用 3D 点云作为 3D 数据,包括激光雷达数据和由运动结构(SfM)技术派生出的数据。此外,我们将构建一个将建筑底图扩展到 3D 的功能。
需要注意的是,在本章中,我们将讨论 postgreSQL-pointcloud 扩展;点云通常是指在一个坐标系中三维点坐标的三维表示的大型数据集。点云用于以高精度表示感知对象的表面,例如使用地理激光雷达数据。pointcloud 扩展将帮助我们将激光雷达数据存储到数据库中的点云对象中。此外,此扩展还添加了允许您将点云对象转换为几何形状并使用点云数据进行空间过滤的功能。有关此扩展的更多信息,您可以访问官方 GitHub 仓库github.com/pgpointcloud/pointcloud。此外,您还可以查看 Paul Ramsey 的教程workshops.boundlessgeo.com/tutorial-lidar/。
下载我们提供的示例数据集以供您使用,可在www.packtpub.com/support找到。
导入激光雷达数据
光探测与测距(LiDAR)是生成点云数据最常用的设备之一。该系统捕捉给定空间中对象或表面的 3D 位置和其他属性。这种方法与雷达非常相似,因为它使用电磁波来测量距离和亮度等。然而,LiDAR 与雷达的主要区别在于,前者使用激光束技术,而不是微波或无线电波。另一个区别是,LiDAR 通常发送一个单一的聚焦脉冲并测量返回脉冲的时间,计算距离和深度。相比之下,雷达在接收到返回脉冲之前会发送多个脉冲,因此需要额外的处理来确定每个脉冲的来源。
LiDAR 数据与地面和空中应用相结合已成为相当常见,有助于地面测量,增强并极大地自动化了摄影测量工程的一些方面。有许多数据源拥有大量的 LiDAR 数据。
LiDAR 数据通常以 LAS 或 LASer 格式分发。美国摄影测量与遥感学会(ASPRS)建立了 LAS 标准。LAS 是一种二进制格式,因此将其读入 PostGIS 数据库是非平凡的。幸运的是,我们可以利用开源工具 PDAL。
准备工作
我们的数据源将以 LAS 格式存在,我们将使用可从 www.pdal.io/ 获取的 PDAL 库将其插入到我们的数据库中。此工具适用于 Linux/UNIX 和 Mac 用户;对于 Windows,它可通过 OSGeo4W 软件包获得(www.pdal.io/workshop/osgeo4w.html)。
LAS 数据可以包含很多有趣的数据,不仅仅是 X、Y 和 Z 值。它还可以包括从被感知对象返回的强度以及对象的分类(地面、植被、建筑物)。当我们将 LAS 文件放入我们的 PostGIS 数据库中时,我们可以选择收集这些信息中的任何一项。此外,PDAL 内部构建一个管道来转换用于读取、处理和写入的数据。
在为此做准备之前,我们需要创建一个表示 PDAL 处理管道的 JSON 文件。对于每个 LAS 文件,我们创建一个 JSON 文件来配置读取器和写入器使用 postgres-pointcloud 选项。我们还需要编写数据库连接参数。对于测试文件 test_1.las,代码如下:

现在,我们可以下载我们的数据。建议您从 gis5.oit.ohio.gov/geodatadownload/ 下载,或者下载我们为您提供的样本数据集,可在 www.packtpub.com/support 获取。
如何做到这一点...
首先,我们需要将我们的 LAS 文件转换为 PDAL 可以使用的格式。我们创建了一个 Python 脚本,该脚本从 LAS 文件目录中读取并生成相应的 JSON。使用此脚本,如果我们有一个大文件目录,我们可以自动化生成。此外,我们选择 Python 是因为它简单,并且无论使用什么操作系统都可以执行脚本。要执行脚本,请在控制台中运行以下命令(对于 Windows 用户,请确保 Python 解释器已包含在 PATH 变量中):
$ python insert_files.py -f <lasfiles_path>
此脚本将读取每个 LAS 文件,并将与要插入数据库的 LAS 文件相关的所有元数据存储在名为 pipelines 的文件夹中。
现在,使用 PDAL,我们执行一个 for 循环将 LAS 文件插入到 Postgres:
$ for file in `ls pipelines/*.json`;
do
pdal pipeline $file;
done
这点云数据被分成三个不同的表。如果我们想合并它们,我们需要执行以下 SQL 命令:
DROP TABLE IF EXISTS chp07.lidar;
CREATE TABLE chp07.lidar AS WITH patches AS
(
SELECT
pa
FROM "chp07"."N2210595"
UNION ALL
SELECT
pa
FROM "chp07"."N2215595"
UNION ALL
SELECT
pa
FROM "chp07"."N2220595"
)
SELECT
2 AS id,
PC_Union(pa) AS pa
FROM patches;
postgres-pointcloud 扩展使用两个主要的点云对象作为变量:PcPoint 对象,它是一个可以有多个维度但至少包含 X 和 Y 值的点,这些值放置在空间中;以及 PcPatch 对象,它是一组紧密相邻的多个 PcPoint。根据插件的文档,将大量点作为单个记录存储在表中会变得效率低下。
现在我们已经将所有数据放入单个表中,如果我们想可视化点云数据,我们需要创建一个空间表,以便我们的图层查看器可以理解;例如,QGIS。Postgres 的点云插件具有 PostGIS 集成,因此我们可以将 PcPatch 和 PcPoint 对象转换为几何对象,并使用 PostGIS 函数分析数据:
CREATE TABLE chp07.lidar_patches AS WITH pts AS
(
SELECT
PC_Explode(pa) AS pt
FROM chp07.lidar
)
SELECT
pt::geometry AS the_geom
FROM pts;
ALTER TABLE chp07.lidar_patches ADD COLUMN gid serial;
ALTER TABLE chp07.lidar_patches ADD PRIMARY KEY (gid);
这个 SQL 脚本执行一个内部查询,最初使用 PC_Explode 函数从 PcPatch 返回一组 PcPoints。然后,对于返回的每个点,我们将从 PcPoint 对象转换为 PostGIS 几何对象。最后,我们创建 gid 列并将其添加到表中作为主键。
现在,我们可以使用我们最喜欢的桌面 GIS 查看我们的数据,如下面的图像所示:

参见
- 在 LiDAR 点云上执行 3D 查询 配方
在 LiDAR 点云上执行 3D 查询
在之前的配方中,导入 LiDAR 数据,我们将一个 LiDAR 3D 点云导入到 PostGIS 中,从输入中创建了一个显式的 3D 数据集。有了 3D 形式的数据,我们就有能力对其执行空间查询。在这个配方中,我们将利用 3D 索引,以便我们的最近邻搜索可以在数据的所有维度上工作。
如何做到这一点...
我们将使用之前配方中导入的 LiDAR 数据作为我们的数据集。我们给那个表命名为 chp07.lidar。为了执行最近邻搜索,我们需要在数据集上创建一个索引。空间索引与普通数据库表索引类似,就像书籍索引一样,它们帮助我们更快地找到我们想要的东西。通常,这样一个索引创建步骤看起来如下(这次我们不会运行它):
CREATE INDEX chp07_lidar_the_geom_idx
ON chp07.lidar USING gist(the_geom);
3D 索引在 2D 查询中不如 2D 索引执行得快,因此 CREATE INDEX 查询默认创建一个 2D 索引。在我们的情况下,我们希望强制 gist 应用到所有三个维度,因此我们将明确告诉 PostgreSQL 使用索引的多维版本:
CREATE INDEX chp07_lidar_the_geom_3dx
ON chp07.lidar USING gist(the_geom gist_geometry_ops_nd);
注意,之前代码中描述的方法如果存在时间维度或 3D 加时间维度也会有效。让我们加载第二个 3D 数据集以及我们将用于查询的流中心线:
$ shp2pgsql -s 3734 -d -i -I -W LATIN1 -t 3DZ -g the_geom hydro_line chp07.hydro | PGPASSWORD=me psql -U me -d "postgis-cookbook" -h localhost
如下所示,这些数据与我们的 LiDAR 点云叠加得很好:

现在,我们可以构建一个简单的查询来检索所有位于我们溪流中心线一英尺范围内的激光雷达点:
DROP TABLE IF EXISTS chp07.lidar_patches_within;
CREATE TABLE chp07.lidar_patches_within AS
SELECT chp07.lidar_patches.gid, chp07.lidar_patches.the_geom
FROM chp07.lidar_patches, chp07.hydro
WHERE ST_3DDWithin(chp07.hydro.the_geom, chp07.lidar_patches.the_geom, 5);
但是,这有点草率;我们可能会得到重复的激光雷达点,因此我们将使用LEFT JOIN和SELECT DISTINCT来细化我们的查询,但继续使用ST_DWithin作为限制条件:
DROP TABLE IF EXISTS chp07.lidar_patches_within_distinct;
CREATE TABLE chp07.lidar_patches_within_distinct AS
SELECT DISTINCT (chp07.lidar_patches.the_geom), chp07.lidar_patches.gid
FROM chp07.lidar_patches, chp07.hydro
WHERE ST_3DDWithin(chp07.hydro.the_geom, chp07.lidar_patches.the_geom, 5);
现在,我们可以可视化我们返回的点,如下面的图像所示:

尝试使用ST_DWithin而不是ST_3DDWithin进行此查询。你会发现返回点的数量有有趣的不同,因为ST_DWithin将收集 XY 平面上可能接近我们的流线的激光雷达点,但在 3D 距离上并不那么接近。
你可以想象ST_3DWithin在围绕我们的线进行查询。相比之下,ST_DWithin将查询一个垂直的激光雷达点墙,因为它只基于 XY 距离搜索相邻点,完全忽略高度,因此收集所有在点上方和下方的狭窄墙体内的点。
构建 2.5D 建筑和提供服务
在第四章“详细建筑足迹从激光雷达数据中生成”的配方中,我们在“处理矢量数据 - 高级配方”部分探讨了使用激光雷达数据自动生成建筑足迹。我们试图从 3D 数据中创建 2D 数据。在这个配方中,我们尝试做相反的事情。我们以 2D 建筑足迹多边形开始,并将它们输入到一个函数中,使其扩展为 3D 多边形。
准备工作
对于这个配方,我们将挤压我们自行制作的建筑足迹。让我们快速创建一个包含单个建筑足迹的表,用于测试,如下所示:
DROP TABLE IF EXISTS chp07.simple_building;
CREATE TABLE chp07.simple_building AS
SELECT 1 AS gid, ST_MakePolygon(
ST_GeomFromText(
'LINESTRING(0 0,2 0, 2 1, 1 1, 1 2, 0 2, 0 0)'
)
) AS the_geom;
将 3D 建筑的创建尽可能简单地封装在一个函数中是有益的:
CREATE OR REPLACE FUNCTION chp07.threedbuilding(footprint geometry, height numeric)
RETURNS geometry AS
$BODY$
我们的功能接受两个输入:建筑足迹和要挤压的高度。我们还可以想象一个接受第三个参数的功能:建筑底座的高度。
要构建建筑墙体,我们首先需要将我们的多边形转换为线字符串,然后进一步将线字符串分离成它们各自的两点段:
WITH simple_lines AS
(
SELECT
1 AS gid,
ST_MakeLine(ST_PointN(the_geom,pointn),
ST_PointN(the_geom,pointn+1)) AS the_geom
FROM (
SELECT 1 AS gid,
polygon_to_line($1) AS the_geom
) AS a
LEFT JOIN(
SELECT
1 AS gid,
generate_series(1,
ST_NumPoints(polygon_to_line($1))-1
) AS pointn
) AS b
ON a.gid = b.gid
),
上述代码返回我们原始形状的两个点段。例如,对于simple_building,输出如下:

现在我们有一系列单独的线,我们可以使用这些线来构建建筑的墙体。首先,我们需要使用ST_Force3DZ将我们的 2D 线重新表示为 3D:
threeDlines AS
(
SELECT ST_Force3DZ(the_geom) AS the_geom FROM simple_lines
),
输出如下:

下一步是将MULTILINESTRING中的每条线分解成许多LINESTRINGS:
explodedLine AS
(
SELECT (ST_Dump(the_geom)).geom AS the_geom FROM threeDLines
),
这个输出的结果如下:

下一步是构建一个代表挤压墙体边界的线:
threeDline AS
(
SELECT ST_MakeLine(
ARRAY[
ST_StartPoint(the_geom),
ST_EndPoint(the_geom),
ST_Translate(ST_EndPoint(the_geom), 0, 0, $2),
ST_Translate(ST_StartPoint(the_geom), 0, 0, $2),
ST_StartPoint(the_geom)
]
)
AS the_geom FROM explodedLine
),
现在,我们需要将每条线字符串转换为polygon.threeDwall:
threeDwall AS
(
SELECT ST_MakePolygon(the_geom) as the_geom FROM threeDline
),
最后,在我们的建筑上加上屋顶和地板,使用原始几何形状作为地板(强制转换为 3D)和原始几何形状的副本,该副本已平移到我们的输入高度:
buildingTop AS
(
SELECT ST_Translate(ST_Force3DZ($1), 0, 0, $2) AS the_geom
),
-- and a floor
buildingBottom AS
(
SELECT ST_Translate(ST_Force3DZ($1), 0, 0, 0) AS the_geom
),
我们将墙壁、屋顶和地板组合在一起,在过程中将其转换为 3D MULTIPOLYGON:
wholeBuilding AS
(
SELECT the_geom FROM buildingBottom
UNION ALL
SELECT the_geom FROM threeDwall
UNION ALL
SELECT the_geom FROM buildingTop
),
-- then convert this collecion to a multipolygon
multiBuilding AS
(
SELECT ST_Multi(ST_Collect(the_geom)) AS the_geom FROM
wholeBuilding
),
虽然我们可以将我们的几何形状保留为MULTIPOLYGON,但我们会做得更规范,并使用非正式的铸造到POLYHEDRALSURFACE。在我们的情况下,我们已经是POLYHEDRALSURFACE的有效格式,所以我们将使用ST_AsText将我们的几何形状转换为文本,用POLYHEDRALSURFACE替换单词,然后使用ST_GeomFromText将我们的文本转换回几何形状:
textBuilding AS
(
SELECT ST_AsText(the_geom) textbuilding FROM multiBuilding
),
textBuildSurface AS
(
SELECT ST_GeomFromText(replace(textbuilding, 'MULTIPOLYGON',
'POLYHEDRALSURFACE')) AS the_geom FROM textBuilding
)
SELECT the_geom FROM textBuildSurface
最后,整个函数是:
CREATE OR REPLACE FUNCTION chp07.threedbuilding(footprint geometry,
height numeric)
RETURNS geometry AS
$BODY$
-- make our polygons into lines, and then chop up into individual line segments
WITH simple_lines AS
(
SELECT 1 AS gid, ST_MakeLine(ST_PointN(the_geom,pointn),
ST_PointN(the_geom,pointn+1)) AS the_geom
FROM (SELECT 1 AS gid, polygon_to_line($1) AS the_geom ) AS a
LEFT JOIN
(SELECT 1 AS gid, generate_series(1,
ST_NumPoints(polygon_to_line($1))-1) AS pointn
) AS b
ON a.gid = b.gid
),
-- convert our lines into 3D lines, which will set our third coordinate to 0 by default
threeDlines AS
(
SELECT ST_Force3DZ(the_geom) AS the_geom FROM simple_lines
),
-- now we need our lines as individual records, so we dump them out using ST_Dump, and then just grab the geometry portion of the dump
explodedLine AS
(
SELECT (ST_Dump(the_geom)).geom AS the_geom FROM threeDLines
),
-- Next step is to construct a line representing the boundary of the extruded "wall"
threeDline AS
(
SELECT ST_MakeLine(
ARRAY[
ST_StartPoint(the_geom),
ST_EndPoint(the_geom),
ST_Translate(ST_EndPoint(the_geom), 0, 0, $2),
ST_Translate(ST_StartPoint(the_geom), 0, 0, $2),
ST_StartPoint(the_geom)
]
)
AS the_geom FROM explodedLine
),
-- we convert this line into a polygon
threeDwall AS
(
SELECT ST_MakePolygon(the_geom) as the_geom FROM threeDline
),
-- add a top to the building
buildingTop AS
(
SELECT ST_Translate(ST_Force3DZ($1), 0, 0, $2) AS the_geom
),
-- and a floor
buildingBottom AS
(
SELECT ST_Translate(ST_Force3DZ($1), 0, 0, 0) AS the_geom
),
-- now we put the walls, roof, and floor together
wholeBuilding AS
(
SELECT the_geom FROM buildingBottom
UNION ALL
SELECT the_geom FROM threeDwall
UNION ALL
SELECT the_geom FROM buildingTop
),
-- then convert this collecion to a multipolygon
multiBuilding AS
(
SELECT ST_Multi(ST_Collect(the_geom)) AS the_geom FROM wholeBuilding
),
-- While we could leave this as a multipolygon, we'll do things properly and munge an informal cast
-- to polyhedralsurfacem which is more widely recognized as the appropriate format for a geometry like
-- this. In our case, we are already formatted as a polyhedralsurface, minus the official designation,
-- so we'll just convert to text, replace the word MULTIPOLYGON with POLYHEDRALSURFACE and then convert
-- back to geometry with ST_GeomFromText
textBuilding AS
(
SELECT ST_AsText(the_geom) textbuilding FROM multiBuilding
),
textBuildSurface AS
(
SELECT ST_GeomFromText(replace(textbuilding, 'MULTIPOLYGON',
'POLYHEDRALSURFACE')) AS the_geom FROM textBuilding
)
SELECT the_geom FROM textBuildSurface
;
$BODY$
LANGUAGE sql VOLATILE
COST 100;
ALTER FUNCTION chp07.threedbuilding(geometry, numeric)
OWNER TO me;
如何做到这一点...
现在我们有了 3D 建筑拉伸函数,我们可以轻松地使用我们封装良好的函数拉伸我们的建筑底图:
DROP TABLE IF EXISTS chp07.threed_building;
CREATE TABLE chp07.threed_building AS
SELECT chp07.threeDbuilding(the_geom, 10) AS the_geom
FROM chp07.simple_building;
我们可以将此函数应用于实际的建筑底图数据集(位于我们的数据目录中),在这种情况下,如果我们有一个高度场,我们可以根据它进行拉伸:
shp2pgsql -s 3734 -d -i -I -W LATIN1 -g the_geom building_footprints\chp07.building_footprints | psql -U me -d postgis-cookbook \ -h <HOST> -p <PORT>
DROP TABLE IF EXISTS chp07.build_footprints_threed;
CREATE TABLE chp07.build_footprints_threed AS
SELECT gid, height, chp07.threeDbuilding(the_geom, height) AS the_geom
FROM chp07.building_footprints;
结果输出给我们一组漂亮的拉伸建筑底图,如下面的图像所示:

第四章中的“从 LiDAR 获取详细建筑底图”配方,处理矢量数据 - 高级配方,探讨了从 LiDAR 中提取建筑底图。可以设想一个完整的工作流程,该流程从 LiDAR 中提取建筑底图,然后使用当前配方重建多边形几何形状,从而将点云转换为表面,将当前配方与之前引用的配方相结合。
使用ST_Extrude来拉伸建筑底图
PostGIS 2.1 为 PostGIS 带来了许多真正酷的附加功能。PostGIS 2.1 带来的更重要改进之一是对 PostGIS 栅格类型的操作。SFCGAL 库作为 PostGIS 的可选扩展被添加,是一个更安静但同样强大的变革者。根据网站sfcgal.org/,SFCGAL 是一个围绕 CGAL 的 C++包装库,旨在支持 ISO 19107:2013 和 OGC 简单特征访问 1.2 的 3D 操作。
从实际的角度来看,这意味着什么?这意味着 PostGIS 正朝着完全功能的 3D 环境发展,从几何形状本身及其 3D 几何形状的操作。更多信息请参阅postgis.net/docs/reference.html#reference_sfcgal。
本文档和几个其他配方将假设您已安装带有 SFCGAL 编译和启用的 PostGIS 版本。这样做将启用以下功能:
-
ST_Extrude:将表面拉伸到相关体积 -
ST_StraightSkeleton:从这个几何形状计算直骨骼 -
ST_IsPlanar:检查表面是否为平面 -
ST_Orientation:这决定了表面方向 -
ST_ForceLHR:这强制 LHR 方向 -
ST_MinkowskiSum:这计算 Minkowski 和 -
ST_Tesselate:这执行表面细分
如何操作...
对于这个配方,我们将像在之前的配方构建和提供 2.5D 建筑中使用我们自己的自定义函数一样使用ST_Extrude。与之前的配方相比的优势是,我们不需要在 PostGIS 中编译 SFCGAL 库。这个配方的优势在于我们能够更多地控制挤压过程;也就是说,我们可以在三个维度上进行挤压。
ST_Extrude返回一个几何体,具体是一个多面体表面。它需要四个参数:输入几何体以及沿X、Y和Z轴的挤压量:
DROP TABLE IF EXISTS chp07.buildings_extruded;
CREATE TABLE chp07.buildings_extruded AS
SELECT gid, ST_CollectionExtract(ST_Extrude(the_geom, 20, 20, 40), 3) as the_geom
FROM chp07.building_footprints

因此,借助构建和提供 2.5D 建筑配方,我们得到了挤压的建筑,但有一些额外的灵活性。
为 PostGIS 创建任意 3D 对象
3D 信息来源不仅来自 LiDAR,也不是纯粹从 2D 几何形状及其相关属性中合成,如构建和提供 2.5D 建筑和使用 ST_Extrude 挤压建筑足迹配方中所述,它们还可以根据计算机视觉原理创建。从图像之间相关关键点的关联计算 3D 信息的过程称为 SfM。
作为计算机视觉的概念,我们可以利用 SfM 以类似于人类大脑在 3D 中感知世界的方式生成 3D 信息,并将其进一步存储和处理在 PostGIS 数据库中。
计算机视觉是计算机科学中的一个学科,专注于从图像和视频中自动分析和推理。它被认为是一个开发算法的研究领域,这些算法以类似于人类视觉的方式解释世界。有关该领域的优秀总结可以在en.wikipedia.org/wiki/Computer_vision找到。
许多开源项目已经成熟,用于解决 SfM 问题。其中最受欢迎的是 Bundler,可以在phototour.cs.washington.edu/bundler/找到,还有ccwu.me/vsfm/的VisualSFM。这些工具的多个平台都有二进制文件,包括版本。这样的项目的优点是,可以使用一组简单的照片来重建 3D 场景。
对于我们的目的,我们将使用 VisualSFM 并跳过该软件的安装和配置。原因是 SfM 超出了 PostGIS 书籍详细介绍的范畴,我们将专注于如何在 PostGIS 中使用这些数据。
准备中
重要的是要理解,虽然 SfM 技术非常有效,但在将图像有效处理成点云的图像类型方面存在某些局限性。这些技术依赖于在后续图像之间找到匹配,因此可能会在处理平滑、缺少相机嵌入的 Exchangeable Image File Format (EXIF) 信息或来自手机相机的图像时遇到困难。
EXIF 标签是图像的元数据格式。这些标签中存储的通常是相机设置、相机类型、镜头类型以及其他与 SfM 提取相关的信息。
我们将从一个已知大部分可以工作的照片系列开始处理图像系列到点云,但随着你对 SfM 进行实验,你也可以输入你自己的照片系列。有关如何创建将产生 3D 模型的照片系列的优秀提示,可以在 www.youtube.com/watch?v=IStU-WP2XKs&t=348s 和 www.cubify.com/products/capture/photography_tips.aspx 找到。
如何操作...
从 ccwu.me/vsfm/ 下载 VisualSFM。在控制台终端中,执行以下命令:
Visualsfm <IMAGES_FOLDER>
VisualSFM 将开始使用图像文件夹作为输入渲染 3D 模型。处理可能需要几个小时。完成后,它将返回一个点云文件。
我们可以在 MeshLab meshlab.sourceforge.net/ 这样的程序中查看这些数据。有关如何使用 MeshLab 查看点云的优秀教程可以在 www.cse.iitd.ac.in/~mcs112609/Meshlab%20Tutorial.pdf 找到。
以下图像显示了在 MeshLab 中查看点云时的样子:

在 VisualSFM 的输出中,有一个扩展名为 .ply 的文件,例如,giraffe.ply(包含在本章源代码中)。如果你在文本编辑器中打开此文件,它看起来可能如下所示:

这是文件的头部部分。它指定了 .ply 格式、编码 format ascii 1.0、顶点数,以及所有返回数据的列名:x、y、z、nx、ny、nz、red、green 和 blue。
对于导入到 PostGIS,我们将导入所有字段,但将重点放在我们的点云的 x、y 和 z 上,以及查看颜色。对于我们的目的,此文件指定了相对的 x、y 和 z 坐标,以及每个点的颜色在通道 red、green 和 blue 中。这些颜色是 24 位颜色,因此它们可以具有介于 0 和 255 之间的整数值。
在接下来的食谱中,我们将创建一个 PDAL 管道,修改 JSON 结构读取器以读取.ply文件。查看本章中关于导入 LiDAR 数据的食谱,了解如何创建 PDAL 管道:
{
"pipeline": [{
"type": "readers.ply",
"filename": "/data/giraffe/giraffe.ply"
}, {
"type": "writers.pgpointcloud",
"connection": "host='localhost' dbname='postgis-cookbook' user='me'
password='me' port='5432'",
"table": "giraffe",
"srid": "3734",
"schema": "chp07"
}]
}
然后,我们在终端中执行以下操作:
$ pdal pipeline giraffe.json"
这个输出将作为下一个食谱的输入。
将模型导出为 X3D 以供网络使用
在 PostGIS 数据库中输入 3D 数据,如果没有能力以某种可用的形式提取数据,那么这几乎就没有什么趣味。解决这个问题的方法之一是利用 PostGIS 将 3D 表格写入 X3D 格式的能力。
X3D 是用于显示 3D 数据的 XML 标准,通过网络工作得很好。对于那些熟悉虚拟现实建模语言(VRML)的人来说,X3D 是那个语言的下一代。
要在浏览器中查看 X3D,用户可以选择各种插件,或者他们可以利用 JavaScript API 来实现查看功能。我们将执行后者,因为它不需要用户配置即可工作。我们将使用 X3DOM 的 JavaScript 框架来完成这项工作。X3DOM 是 HTML5 和 3D 集成的演示,它使用Web 图形库(WebGL);(en.wikipedia.org/wiki/WebGL)来允许在浏览器中渲染和交互 3D 内容。这意味着我们的数据将不会在不支持 WebGL 的浏览器中显示。
准备工作
我们将使用前一个示例中的点云数据,以 X3D 格式提供服务。PostGIS 关于 X3D 的文档中包含了一个使用ST_AsX3D函数输出格式化 X3D 代码的示例:
COPY(WITH pts AS (SELECT PC_Explode(pa) AS pt FROM chp07.giraffe) SELECT '
<X3D
showStat="false" showLog="false" x="0px" y="0px" width="800px"
height="600px">
<Scene>
<Transform>
<Shape>' || ST_AsX3D(ST_Union(pt::geometry)) ||'</Shape>
</Transform>
</Scene>
</X3D>' FROM pts)
TO STDOUT WITH CSV;
我们包括了复制到STDOUT WITH CSV以进行原始代码的转储。用户可以将此查询保存为 SQL 脚本文件,并在控制台中执行它,以便将结果转储到文件中。例如:
$ psql -U me -d postgis-cookbook -h localhost -f "x3d_query.sql" > result.html
如何操作...
这个示例虽然完整地提供了纯 X3D,但需要额外的代码来允许在浏览器中查看。我们通过包含样式表和适当的 X3DOM 包含 XHTML 文档的标题来实现这一点:
<link rel="stylesheet" type="text/css" href="http://x3dom.org/x3dom/example/x3dom.css" />
<script type="text/javascript" src="img/x3dom.js"></script>
生成 X3D 数据的 XHTML 的完整查询如下所示:
COPY(WITH pts AS (
SELECT PC_Explode(pa) AS pt FROM chp07.giraffe
)
SELECT regexp_replace('
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html >
<head>
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Point Cloud in a Browser</title>
<link rel="stylesheet" type="text/css"
href="http://x3dom.org/x3dom/example/x3dom.css" />
<script type="text/javascript"
src="img/x3dom.js">
</script>
</head>
<body>
<h1>Point Cloud in the Browser</h1>
<p>
Use mouse to rotate, scroll wheel to zoom, and control
(or command) click to pan.
</p>
<X3D pl-k"> showStat="false" showLog="false" x="0px" y="0px" width="800px"
height="600px">
<Scene>
<Transform>
<Shape>' || ST_AsX3D(ST_Union(pt::geometry)) || '</Shape>
</Transform>
</Scene>
</X3D>
</body>
</html>', E'[\\n\\r]+','', 'g')
FROM pts)TO STDOUT;
如果我们在最喜欢的浏览器中打开.html文件,我们将得到以下内容:

还有更多...
有些人可能希望将这个 X3D 转换作为一个函数使用,将几何形状输入到函数中,然后返回一个页面。这样,我们可以轻松地重用代码来处理其他表格。将 X3D 转换封装在函数中如下所示:
CREATE OR REPLACE FUNCTION AsX3D_XHTML(geometry)
RETURNS character varying AS
$BODY$
SELECT regexp_replace(
'
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns= "http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Point Cloud in a Browser</title>
<link rel="stylesheet" type="text/css"
href="http://x3dom.org/x3dom/example/x3dom.css"/>
<script type="text/javascript"
src="img/x3dom.js">
</script>
</head>
<body>
<h1>Point Cloud in the Browser</h1>
<p>
Use mouse to rotate, scroll wheel to zoom, and control
(or command) click to pan.
</p>
<X3D xmlns="http://www.web3d.org/specifications/x3d-namespace"
showStat="false" showLog="false" x="0px" y="0px" width="800px"
height="600px">
<Scene>
<Transform>
<Shape>'|| ST_AsX3D($1) || '</Shape>
</Transform>
</Scene>
</X3D>
</body>
</html>
', E'[\\n\\r]+' , '' , 'g' ) As x3dXHTML;
$BODY$
LANGUAGE sql VOLATILE
COST 100;
为了使函数能够工作,我们需要首先在几何参数上使用ST_UNION,然后将结果传递给AsX3D_XHTML函数:
copy(
WITH pts AS (
SELECT
PC_Explode(pa) AS pt
FROM giraffe
)
SELECT AsX3D_XHTML(ST_UNION(pt::geometry)) FROM pts) to stdout;
我们现在可以非常简单地直接从命令行或 Web 框架中生成适当的 XHTML。
使用 PostGIS 3D 重建无人机(UAV)图像足迹
作为数据收集器的无人机系统(UAS),也称为无人机(UAVs),其快速发展正在彻底改变各个领域的远程数据收集。在军事领域之外更广泛的应用障碍包括防止某些国家(如美国)飞行的监管框架,以及缺乏开源的后处理软件实现。在接下来的四个菜谱中,我们将尝试解决这两个障碍中的后者。
对于这个菜谱,我们将使用俄亥俄州交通运输部在辛尼卡县进行的无人机飞行元数据来绘制飞行覆盖范围。这些数据包含在本章的代码文件夹中。
这个菜谱的基本思路是估计无人机摄像头的视场,生成一个代表该视场的 3D 金字塔,并使用飞行星历(航向、俯仰角和滚转角)来估计地面覆盖。
开始
我们为飞行提供的元数据或星历包括无人机的航向、俯仰角和滚转角,以及其高度和位置:

为了将这些星历转换为 PostGIS 术语,我们假设以下内容:
-
90 度减去俯仰角等于
ST_RotateX -
负滚转等于
ST_RotateY -
90 度减去航向等于
ST_RotateZ
为了执行我们的分析,我们需要外部函数。这些函数可以从github.com/smathermather/postgis-etc/tree/master/3D下载。
我们将使用 ST_RotateX、ST_RotateY(ST_RotateX.sql 和 ST_RotateY.sql)的修补版本,这些版本允许我们在输入点周围旋转几何形状,以及一个用于计算我们视场的函数 pyramidMaker.sql。PostGIS 的未来版本将内置这些版本的 ST_RotateX 和 ST_RotateY。我们还有一个名为 ST_RotateXYZ 的函数,它基于这些函数,并将通过允许我们同时指定三个旋转轴来简化我们的代码。
对于最后一步,我们需要执行体积交集(3D 交集的等效)的能力。为此,我们将使用 volumetricIntersection.sql,它允许我们仅返回交集的体积部分作为一个三角不规则网络(TIN)。
TIN 是表示表面和体积的 3D 表面模型,它是一个三角形的网格。
我们将按照以下方式安装功能:
psql -U me -d postgis_cookbook -f ST_RotateX.sql
psql -U me -d postgis_cookbook -f ST_RotateY.sql
psql -U me -d postgis_cookbook -f ST_RotateXYZ.sql
psql -U me -d postgis_cookbook -f pyramidMaker.sql
psql -U me -d postgis_cookbook -f volumetricIntersection.sql
如何做到这一点...
为了计算视场足迹,我们将计算从视点到地面的矩形金字塔。这个金字塔需要根据无人机的滚转指向天顶的左右两侧,根据俯仰角从飞行器的前后移动,并根据航向相对于飞行器的移动方向进行定位。
pyramidMaker函数将为我们构建金字塔,而ST_RotateXYZ将旋转金字塔,以补偿翻滚、俯仰和方位。
以下图像是单个图像计算出的足迹示例地图。注意这个示例中轻微的向左倾斜,从上方看时导致金字塔看起来不对称:

以下图像显示了无人机飞行轨迹叠加在等高线图上的总轨迹:

使用 QGIS 查看的飞行总轨迹
我们将编写一个函数来计算我们的足迹金字塔。为了输入函数,我们需要无人机位置作为几何形状(原点),俯仰、方位和翻滚,以及相机在x和y方向上的视场角。最后,我们需要无人机相对于地面的相对高度:
CREATE OR REPLACE FUNCTION chp07.pbr(origin geometry, pitch numeric,
bearing numeric, roll numeric, anglex numeric, angley numeric,
height numeric)
RETURNS geometry AS
$BODY$
我们的金字塔函数假设我们知道金字塔的底面积。我们最初不知道这个,所以我们将根据相机的视场角和无人机的高度来计算其大小:
WITH widthx AS
(
SELECT height / tan(anglex) AS basex
),
widthy AS
(
SELECT height / tan(angley) AS basey
),
现在,我们有了足够的信息来构建我们的金字塔:
iViewCone AS (
SELECT pyramidMaker(origin, basex::numeric, basey::numeric, height)
AS the_geom
FROM widthx, widthy
),
我们需要以下代码来相对于俯仰、翻滚和方位旋转我们的视图:
iViewRotated AS (
SELECT ST_RotateXYZ(the_geom, pi() - pitch, 0 - roll, pi() -
bearing, origin) AS the_geom FROM iViewCone
)
SELECT the_geom FROM iViewRotated
整个函数如下所示:
CREATE OR REPLACE FUNCTION chp07.pbr(origin geometry, pitch numeric,
bearing numeric, roll numeric, anglex numeric, angley numeric,
height numeric)
RETURNS geometry AS
$BODY$
WITH widthx AS
(
SELECT height / tan(anglex) AS basex
),
widthy AS
(
SELECT height / tan(angley) AS basey
),
iViewCone AS (
SELECT pyramidMaker(origin, basex::numeric, basey::numeric, height)
AS the_geom
FROM widthx, widthy
),
iViewRotated AS (
SELECT ST_RotateXYZ(the_geom, pi() - pitch, 0 - roll, pi() -
bearing, origin) AS the_geom FROM iViewCone
)
SELECT the_geom FROM iViewRotated
;
$BODY$
LANGUAGE sql VOLATILE
COST 100;
现在,为了使用我们的函数,让我们从本章源代码中包含的uas_locations形状文件中导入无人机位置:
shp2pgsql -s 3734 -W LATIN1 uas_locations_altitude_hpr_3734 uas_locations | \PGPASSWORD=me psql -U me -d postgis-cookbook -h localhost
现在,我们可以为每个无人机位置计算一个估计的足迹:
DROP TABLE IF EXISTS chp07.viewshed;
CREATE TABLE chp07.viewshed AS
SELECT 1 AS gid, roll, pitch, heading, fileName, chp07.pbr(ST_Force3D(geom),
radians(0)::numeric, radians(heading)::numeric, radians(roll)::numeric,
radians(40)::numeric, radians(50)::numeric,
( (3.2808399 * altitude_a) - 838)::numeric)
AS the_geom FROM uas_locations;
如果你用你喜欢的桌面 GIS 导入此代码,例如 QGIS,你将能够看到以下内容:

使用地形模型,我们可以在这个分析中更进一步。由于我们的无人机足迹是体积的,我们首先加载地形模型。我们将从这个章节源代码中包含的.backup文件中加载它:
pg_restore -h localhost -p 8000 -U me -d "postgis-cookbook" \ --schema chp07 --verbose "lidar_tin.backup"
接下来,我们将创建一个较小的viewshed表版本:
DROP TABLE IF EXISTS chp07.viewshed;
CREATE TABLE chp07.viewshed AS
SELECT 1 AS gid, roll, pitch, heading, fileName, chp07.pbr(ST_Force3D(geom), radians(0)::numeric, radians(heading)::numeric, radians(roll) ::numeric, radians(40)::numeric, radians(50)::numeric, 1000::numeric) AS the_geom
FROM uas_locations
WHERE fileName = 'IMG_0512.JPG';
如果你用你喜欢的桌面 GIS 导入此代码,例如 QGIS,你将能够看到以下内容:

PostGIS 中的无人机航摄影像测量 – 点云
我们将使用我们在之前名为“为 PostGIS 创建任意 3D 对象”的配方中使用的技巧,学习如何在 PostGIS 中创建和导入无人机衍生的点云。
在我们开始之前有一个注意事项是,虽然我们将处理地理空间数据,但我们将在相对空间中这样做,而不是在已知坐标系中。换句话说,这种方法将在任意坐标系中计算我们的数据集。ST_Affine可以与位置的字段测量相结合,将我们的数据转换到已知坐标系中,但这超出了本书的范围。
准备工作
与为 PostGIS 创建任意 3D 对象配方类似,我们将从一个图像系列转换成点云。然而,在本例中,我们的图像系列将来自无人机影像。下载本章代码文件夹中包含的图像系列uas_flight,并将其输入到 VisualSFM 中(有关如何使用此工具的更多信息,请参阅ccwu.me/vsfm/);为了检索点云,将其命名为uas_points.ply(此文件也包含在此文件夹中,以防您想使用它)。
PostGIS 的输入与之前相同。创建一个 JSON 文件,并使用 PDAL 将其存储到数据库中:
{
"pipeline": [{
"type": "readers.ply",
"filename": "/data/uas_flight/uas_points.ply"
}, {
"type": "writers.pgpointcloud",
"connection": "host='localhost' dbname='postgis-cookbook' user='me'
password='me' port='5432'",
"table": "uas",
"schema": "chp07"
}]
}
如何操作...
现在,我们将点云数据复制到我们的表格中。请参考本章中的导入 LiDAR 数据配方以验证点云扩展对象表示:
$ pdal pipeline uas_points.json
从 MeshLab (www.meshlab.net/)中查看的.ply文件,这些数据非常有趣:

原始数据是彩色红外影像,因此植被显示为红色,农田和道路为灰色。注意天空中的亮色;这些是我们需要过滤掉的相机位置点。
下一步是从这些数据生成正射影像。
PostGIS 中的无人机摄影测量 – 创建 DSM
如果我们不从输入中生成数字地形模型,则摄影测量示例将是不完整的。在这里,将输入点云分类为地面点、建筑点和植被点的完全严格解决方案是不可行的,但此配方将提供实现此类解决方案的基本框架。
在此配方中,我们将创建一个 3D TIN,它将代表点云的表面。
准备工作
在开始之前,ST_DelaunayTriangles仅在 PostGIS 2.1 使用 GEOS 3.4 时可用。这是本书中要求此类高级版本 PostGIS 和 GEOS 的少数配方之一。
如何操作...
ST_DelaunayTriangles将计算带有正确标志的 3D TIN:几何 ST_DelaunayTriangles(几何 g1,浮点 tolerance,整数 4 flags):
DROP TABLE IF EXISTS chp07.uas_tin;
CREATE TABLE chp07.uas_tin AS WITH pts AS
(
SELECT PC_Explode(pa) AS pt
FROM chp07.uas_flights
)
SELECT ST_DelaunayTriangles(ST_Union(pt::geometry), 0.0, 2) AS the_geom
FROM pts;
现在,我们有了数字表面模型的完整 TIN:

第八章:PostGIS 编程
在本章中,我们将涵盖以下主题:
-
使用 Psycopg 编写 PostGIS 向量数据
-
使用 OGR Python 绑定编写 PostGIS 向量数据
-
使用 PL/Python 编写 PostGIS 函数
-
使用 GeoNames 数据集进行地理编码和反向地理编码
-
使用 OSM 数据集和 trigrams 进行地理编码
-
使用 geopy 和 PL/Python 进行地理编码
-
使用 Python 和 GDAL 导入 NetCDF 数据集
简介
有几种方法可以编写 PostGIS 程序,在本章中我们将看到其中的一些。你将主要在本章中使用 Python 语言。Python 是一种出色的语言,拥有大量的 GIS 和科学库,可以与 PostGIS 结合编写出色的地理空间应用程序。
如果你刚开始接触 Python,你可以通过这些优秀的网络资源快速变得高效:
-
官方的 Python 教程在
docs.python.org/2/tutorial/ -
流行的 深入 Python 书籍在
www.diveintopython.net/
你可以将 Python 与一些优秀且流行的库结合起来,例如:
-
Psycopg:这是最完整且最受欢迎的 PostgreSQL Python DB API 实现;请参阅
initd.org/psycopg/ -
GDAL:用于在 Python 脚本中解锁强大的 GDAL 库;请参阅
www.gdal.org/gdal_tutorial.html -
requests:这是一个方便的 Python 标准库,用于管理 HTTP 事务,例如打开 URL
-
simplejson:这是一个简单且快速的 JSON 编码器/解码器
本章中的食谱将涵盖一些其他有用的地理空间 Python 库,如果你正在开发地理空间应用程序,这些库值得一看。在这些 Python 库中,包括以下库:
-
Shapely:这是用于操作和分析平面几何对象的 GEOS 库的 Python 接口:
toblerity.github.io/shapely/ -
Fiona:这是一个非常轻量级的 OGR Python API,可以用作本章中使用的 OGR 绑定的替代品,以管理向量数据集:
github.com/Toblerity/Fiona -
Rasterio:这是一个 Pythonic 的 GDAL Python API,可以用作本章中使用的 GDAL 绑定的替代品,以管理栅格数据集:
github.com/mapbox/rasterio -
pyproj:这是到 PROJ.4 库的 Python 接口:
pypi.python.org/pypi/pyproj -
Rtree:这是
ctypePython 包装器到libspatialindex库,提供了一些对某些类型的地理空间开发非常有用的空间索引功能:toblerity.github.io/rtree/
在第一个小节中,你将编写一个程序,使用 Python 及其实用工具(如psycopg、requests和simplejson)从网络获取天气数据并将其导入 PostGIS。
在第二个小节中,我们将引导你使用 Python 和 GDAL OGR Python 绑定库创建一个脚本,用于使用 GeoNames 网络服务之一对地名列表进行地理编码。
然后,你将使用 PL/Python 语言为 PostGIS 编写一个 Python 函数,查询openweathermap.org/ 网络服务,该服务已在第一个小节中使用,以计算 PostGIS 几何体的天气情况,并在 PostgreSQL 函数内部进行。
在第四个小节中,你将创建两个 PL/pgSQL PostGIS 函数,这些函数将允许你使用 GeoNames 数据集执行地理编码和反向地理编码。
在此之后,有一个小节,你将使用导入到 PostGIS 中的OpenStreetMap街道数据集,使用 Python 实现一个非常基本的 Python 类,以便为类的消费者提供使用 PostGIS 三重支持的地标实现。
第六个小节将展示如何使用 geopy 库创建一个 PL/Python 函数,使用网络地理编码 API(如 Google Maps、Yahoo! Maps、Geocoder、GeoNames 等)进行地址地理编码。
在本章的最后一个小节中,你将创建一个 Python 脚本,使用 GDAL Python 绑定将netCDF格式的数据导入 PostGIS。
在开始本章的小节之前,让我们先看看一些注意事项。
如果你使用 Linux 或 macOS,请按照以下步骤操作:
- 创建一个 Python
virtualenv(www.virtualenv.org/en/latest/),以保持一个 Python 隔离环境,用于本书中所有 Python 小节的 Python 脚本,并激活它。请在中央目录中创建它,因为你将需要使用它来编写本书中大多数 Python 小节的 Python 脚本:
$ cd ~/virtualenvs
$ virtualenv --no-site-packages postgis-cb-env
$ source postgis-cb-env/bin/activate
- 一旦激活,你就可以安装本章小节所需的 Python 库:
$ pip install simplejson
$ pip install psycopg2
$ pip install numpy
$ pip install requests
$ pip install gdal
$ pip install geopy
- 如果你刚开始使用虚拟环境,并且想知道库安装在哪里,你应该在我们的开发箱中的
virtualenv目录中找到所有内容。你可以使用以下命令查找库:
$ ls /home/capooti/virtualenv/postgis-cb-env/lib/
python2.7/site-packages
如果你想知道前面的命令行发生了什么,那么virtualenv是一个用于创建隔离 Python 环境的工具,你可以在www.virtualenv.org上找到更多关于这个工具的信息,而pip (www.pip-installer.org)是一个用于安装和管理用 Python 编写的软件包的包管理系统。
如果你使用 Windows,请按照以下步骤操作:
-
获取 Python 以及本章小节所需的所有库的最简单方法是使用OSGeo4W,它是 Windows 上流行的开源地理空间软件的二进制发行版。你可以从
trac.osgeo.org/osgeo4w/下载它。 -
在我们编写这本书的时候,Windows 盒子中的 OSGeo4W shell 附带 Python 2.7,GDAL 2.2 Python 绑定,simplejson,psycopg2 和 numpy。你只需要安装 geopy。
-
安装 geopy 以及最终向 OSGeo4W shell 添加更多 Python 库的最简单方法是按照
www.pip-installer.org/en/latest/installing.html中找到的说明安装setuptools和pip。打开 OSGeo4W shell,只需输入以下命令:
> python ez_setup.py
> python get-pip.py
> pip install requests
> pip install geopy
使用 Psycopg 编写 PostGIS 矢量数据
在这个菜谱中,你将使用 Python 结合 Psycopg,这是 Python 中最流行的 PostgreSQL 数据库库,以便使用 SQL 语言将一些数据写入 PostGIS。
你将编写一个过程来导入美国人口最多的城市的气象数据。你将从www.openweatherdata.org/导入此类气象数据,这是一个提供免费气象数据和预报 API 的 Web 服务。你将要编写的过程将迭代每个主要美国城市,并使用www.openweatherdata.org/ Web 服务 API 从最近的气象站获取其实际温度,以 JSON 格式获取输出。(如果你对 JSON 格式不熟悉,可以在www.json.org/找到有关它的详细信息。)
你还将生成一个新的 PostGIS 图层,包含每个城市最近的 10 个气象站。
准备工作
- 使用以下命令为本章的菜谱创建数据库模式:
postgis_cookbook=# CREATE SCHEMA chp08;
- 从
nationalmap.gov/网站下载 USA 城市的 shapefile,地址为dds.cr.usgs.gov/pub/data/nationalatlas/citiesx020_nt00007.tar.gz(此存档也包含在本书的数据集中,与代码包一起提供),将其提取到working/chp08,并在 PostGIS 中导入它,过滤掉人口少于 100,000 的城市:
$ ogr2ogr -f PostgreSQL -s_srs EPSG:4269 -t_srs EPSG:4326
-lco GEOMETRY_NAME=the_geom -nln chp08.cities
PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" -where "POP_2000 $ 100000" citiesx020.shp
- 使用以下命令为每个城市添加一个存储温度的
real字段:
postgis_cookbook=# ALTER TABLE chp08.cities
ADD COLUMN temperature real;
- 如果你使用 Linux,请确保遵循本章的初始说明并创建一个 Python 虚拟环境,以便为本书中所有 Python 菜谱保持一个 Python 隔离的环境。然后,激活它:
$ source postgis-cb-env/bin/activate
如何做到这一点...
执行以下步骤:
- 创建以下表格以存储气象站的数据:
CREATE TABLE chp08.wstations
(
id bigint NOT NULL,
the_geom geometry(Point,4326),
name character varying(48),
temperature real,
CONSTRAINT wstations_pk PRIMARY KEY (id )
);
-
在
openweathermap.org创建一个账户以获取 API 密钥。然后,检查你将要使用的 Web 服务的 JSON 响应。如果你想从一个点(城市质心)获取最近的 10 个气象站,你需要运行的请求如下(在浏览器中测试它):api.openweathermap.org/data/2.5/find?lat=55&lon=37&cnt=10&appid=YOURKEY -
你应该得到以下 JSON 响应(最近的 10 个站点及其相对数据按距离点坐标的顺序排列,在这种情况下,坐标为
lon=37和lat=55):
{
"message": "accurate",
"cod": "200",
"count": 10,
"list": [
{
"id": 529315,
"name": "Marinki",
"coord": {
"lat": 55.0944,
"lon": 37.03
},
"main": {
"temp": 272.15,
"pressure": 1011,
"humidity": 80,
"temp_min": 272.15,
"temp_max": 272.15
}, "dt": 1515114000,
"wind": {
"speed": 3,
"deg": 140
},
"sys": {
"country": ""
},
"rain": null,
"snow": null,
"clouds": {
"all": 90
},
"weather": [
{
"id": 804,
"main": "Clouds",
"description": "overcast clouds",
"icon": "04n"
}
]
},
- 现在,创建一个将提供所需输出的 Python 程序,并将其命名为
get_weather_data.py:
import sys
import requests
import simplejson as json
import psycopg2
def GetWeatherData(lon, lat, key):
"""
Get the 10 closest weather stations data for a given point.
"""
# uri to access the JSON openweathermap web service
uri = (
'https://api.openweathermap.org/data/2.5/find?
lat=%s&lon=%s&cnt=10&appid=%s'
% (lat, lon, key))
print 'Fetching weather data: %s' % uri
try:
data = requests.get(uri)
print 'request status: %s' % data.status_code
js_data = json.loads(data.text)
return js_data['list']
except:
print 'There was an error getting the weather data.'
print sys.exc_info()[0]
return []
def AddWeatherStation(station_id, lon, lat, name, temperature):
"""
Add a weather station to the database, but only if it does
not already exists.
"""
curws = conn.cursor()
curws.execute('SELECT * FROM chp08.wstations WHERE id=%s',
(station_id,))
count = curws.rowcount
if count==0: # we need to add the weather station
curws.execute(
"""INSERT INTO chp08.wstations (id, the_geom, name,
temperature) VALUES (%s, ST_GeomFromText('POINT(%s %s)',
4326), %s, %s)""",
(station_id, lon, lat, name, temperature)
)
curws.close()
print 'Added the %s weather station to the database.' % name
return True
else: # weather station already in database
print 'The %s weather station is already in the database.' % name
return False
# program starts here
# get a connection to the database
conn = psycopg2.connect('dbname=postgis_cookbook user=me
password=password')
# we do not need transaction here, so set the connection
# to autocommit mode
conn.set_isolation_level(0)
# open a cursor to update the table with weather data
cur = conn.cursor()
# iterate all of the cities in the cities PostGIS layer,
# and for each of them grap the actual temperature from the
# closest weather station, and add the 10
# closest stations to the city to the wstation PostGIS layer
cur.execute("""SELECT ogc_fid, name,
ST_X(the_geom) AS long, ST_Y(the_geom) AS lat
FROM chp08.cities;""")
for record in cur:
ogc_fid = record[0]
city_name = record[1]
lon = record[2]
lat = record[3]
stations = GetWeatherData(lon, lat, 'YOURKEY')
print stations
for station in stations:
print station
station_id = station['id']
name = station['name']
# for weather data we need to access the 'main' section in the
# json 'main': {'pressure': 990, 'temp': 272.15, 'humidity': 54}
if 'main' in station:
if 'temp' in station['main']:
temperature = station['main']['temp']
else:
temperature = -9999
# in some case the temperature is not available
# "coord":{"lat":55.8622,"lon":37.395}
station_lat = station['coord']['lat']
station_lon = station['coord']['lon']
# add the weather station to the database
AddWeatherStation(station_id, station_lon, station_lat,
name, temperature)
# first weather station from the json API response is always
# the closest to the city, so we are grabbing this temperature
# and store in the temperature field in cities PostGIS layer
if station_id == stations[0]['id']:
print 'Setting temperature to %s for city %s'
% (temperature, city_name)
cur2 = conn.cursor()
cur2.execute(
'UPDATE chp08.cities SET temperature=%s WHERE ogc_fid=%s',
(temperature, ogc_fid))
cur2.close()
# close cursor, commit and close connection to database
cur.close()
conn.close()
- 运行 Python 程序:
(postgis-cb-env)$ python get_weather_data.py
Added the PAMR weather station to the database.
Setting temperature to 268.15 for city Anchorage
Added the PAED weather station to the database.
Added the PANC weather station to the database.
...
The KMFE weather station is already in the database.
Added the KOPM weather station to the database.
The KBKS weather station is already in the database.
- 检查你刚刚编写的 Python 程序的输出。使用你最喜欢的 GIS 桌面工具打开两个 PostGIS 图层
cities和wstations,并调查结果。以下截图显示了在 QGIS 中的样子:

它是如何工作的...
Psycopg 是 Python 中最流行的 PostgreSQL 适配器,它可以用来创建发送 SQL 命令到 PostGIS 的 Python 脚本。在这个菜谱中,你创建了一个 Python 脚本,使用流行的 JSON 格式从 openweathermap.org/ 网络服务器查询天气数据,然后使用这些数据更新两个 PostGIS 图层。
对于其中一个图层 cities,使用离城市最近的气象站的温度数据来更新 temperature 字段。为此,你使用了 UPDATE SQL 命令。另一个图层 wstations 在从气象数据中识别到新的气象站并将其插入图层时更新。在这种情况下,你使用了 INSERT SQL 语句。
这是对脚本行为的快速概述(更多详细信息可以在 Python 代码中的注释中找到)。一开始,使用 Psycopg 的 connection 对象创建了一个 PostgreSQL 连接。connection 对象是通过主要连接参数(dbname、user 和 password)创建的,而 server name 和 port 的默认值未指定;相反,使用 localhost 和 5432。连接行为设置为 auto commit,这样 Psycopg 执行的任何 SQL 都将立即运行,而不会嵌入事务中。
使用游标,你首先迭代 cities PostGIS 图层中的所有记录;对于每个城市,你需要从 openweathermap.org/ 网络服务器获取温度。为此,对于每个城市,你调用 GetWeatherData 方法,并将城市的坐标传递给它。该方法使用 requests 库查询服务器,并使用 simplejson Python 库解析 JSON 响应。
你应该将 URL 请求发送到 try...catch 块。这样,如果网络服务(互联网连接不可用、HTTP 状态码不同于 200 或其他任何问题)出现任何问题,进程可以安全地继续使用下一个城市的(迭代)数据。
JSON 响应包含根据请求的信息,关于城市最近的 10 个气象站。你将使用离城市最近的第一个气象站的信息来设置城市的 temperature 字段。
然后,你将迭代所有的 station JSON 对象,并使用 AddWeatherStation 方法在 wstation PostGIS 图层中创建一个气象站,但前提是不存在具有相同 id 的气象站。
使用 OGR Python 绑定写入 PostGIS 向量数据
在这个菜谱中,你将使用 Python 和 GDAL/OGR 库的 Python 绑定来创建一个脚本来使用 GeoNames 网络服务之一对地点名称列表进行地理编码(www.geonames.org/export/ws-overview.html)。你将使用 Wikipedia Fulltext Search 网络服务(www.geonames.org/export/wikipedia-webservice.html#wikipediaSearch),该服务对于给定的搜索字符串返回匹配该搜索字符串的地点坐标作为输出,以及来自维基百科的一些其他有用属性,包括维基百科的 页面标题 和 url。
脚本首先应该创建一个名为 wikiplaces 的 PostGIS 点图层,其中将存储由网络服务返回的所有位置及其属性。
这个菜谱应该为你提供使用其他类似网络服务的基础,例如 Google Maps、Yahoo! BOSS Geo Services 等,以类似的方式获取结果。
在开始之前,请注意 GeoNames 的使用条款:www.geonames.org/export/。简而言之,在撰写本文时,每个应用程序(通过 username 参数识别)每天有 30,000 个信用额的限制;每小时限制为 2,000 个信用额。信用额是大多数服务的网络服务请求命中。
你将使用 GDAL/OGR Python 绑定(trac.osgeo.org/gdal/wiki/GdalOgrInPython)生成包含地理编码地点名称的 PostGIS 表。
准备工作
-
要访问 GeoNames 网络服务,你需要在
www.geonames.org/login创建一个用户。我们为这个菜谱创建的用户是postgis;每次查询 GeoNames 网络服务 URL 时,你需要将其更改为你的用户名。 -
如果你使用的是 Windows,请确保已按照本章初始说明中建议安装 OSGeo4W。
-
如果你使用的是 Linux,请遵循本章的初始说明,创建一个 Python
virtualenv,以便为本书中所有 Python 菜谱保持一个独立的 Python 环境,并激活它:
$ source postgis-cb-env/bin/activate
- 一旦激活,如果你还没有这样做,你必须安装此菜谱所需的
gdal和simplejsonPython 包:
(postgis-cb-env)$ pip install gdal
(postgis-cb-env)$ pip install simplejson
如何做到这一点...
执行以下步骤:
- 首先,使用以下请求自行测试网络服务和它们的 JSON 输出(根据需要更改
q和username参数):api.geonames.org/wikipediaSearchJSON?formatted=true&q=london&maxRows=10&username=postgis&style=full.
你应该得到以下 JSON 输出:
{
"geonames": [
{
"summary": "London is the capital and most populous city of
England and United Kingdom. Standing on the River Thames,
London has been a major settlement for two millennia,
its history going back to its founding by the Romans,
who named it Londinium (...)",
"elevation": 8,
"geoNameId": 2643743,
"feature": "city",
"lng": -0.11832,
"countryCode": "GB",
"rank": 100,
"thumbnailImg": "http://www.geonames.org/img/wikipedia/
43000/thumb-42715-100.jpg",
"lang": "en",
"title": "London",
"lat": 51.50939,
"wikipediaUrl": "en.wikipedia.org/wiki/London"
},
{
"summary": "New London is a city and a port of entry on the
northeast coast of the United States. It is located at
the mouth of the Thames River in New London County,
southeastern Connecticut. New London is located about from
the state capital of Hartford,
from Boston, Massachusetts, from Providence, Rhode (...)",
"elevation": 27,
"feature": "landmark",
"lng": -72.10083333333333,
"countryCode": "US",
"rank": 100,
"thumbnailImg": "http://www.geonames.org/img/wikipedia/
160000/thumb-159123-100.jpg",
"lang": "en",
"title": "New London, Connecticut",
"lat": 41.355555555555554,
"wikipediaUrl": "en.wikipedia.org/wiki/
New_London%2C_Connecticut"
},...
]
}
-
如您从 GeoNames 网络服务的 JSON 输出中看到的,对于给定的查询字符串(一个位置名称),您会得到一个与该位置相关的 Wikipedia 页面的列表,格式为 JSON。对于代表 Wikipedia 页面的每个 JSON 对象,您可以访问属性,例如
page title(页面标题)、summary(摘要)、url和位置的coordinates(坐标)。 -
现在,创建一个名为
working/chp08/names.txt的文本文件,其中包含您希望从 Wikipedia 全文搜索网络服务中地理编码的位置名称。添加一些地点名称,例如(在 Windows 中,使用记事本等文本编辑器):
$ vi names.txt
London
Rome
Boston
Chicago
Madrid
Paris
...
- 现在,在
working/chp08/下创建一个名为import_places.py的文件,并将此菜谱的 Python 脚本添加到其中。以下是如何编写脚本(您应该可以通过阅读内联注释和 How it works... 部分来理解它):
import sys
import requests
import simplejson as json
from osgeo import ogr, osr
MAXROWS = 10
USERNAME = 'postgis' #enter your username here
def CreatePGLayer():
"""
Create the PostGIS table.
"""
driver = ogr.GetDriverByName('PostgreSQL')
srs = osr.SpatialReference()
srs.ImportFromEPSG(4326)
ogr.UseExceptions()
pg_ds = ogr.Open("PG:dbname='postgis_cookbook' host='localhost'
port='5432' user='me' password='password'", update = 1)
pg_layer = pg_ds.CreateLayer('wikiplaces', srs = srs,
geom_type=ogr.wkbPoint, options = [
'DIM=3',
# we want to store the elevation value in point z coordinate
'GEOMETRY_NAME=the_geom',
'OVERWRITE=YES',
# this will drop and recreate the table every time
'SCHEMA=chp08',
])
# add the fields
fd_title = ogr.FieldDefn('title', ogr.OFTString)
pg_layer.CreateField(fd_title)
fd_countrycode = ogr.FieldDefn('countrycode', ogr.OFTString)
pg_layer.CreateField(fd_countrycode)
fd_feature = ogr.FieldDefn('feature', ogr.OFTString)
pg_layer.CreateField(fd_feature)
fd_thumbnail = ogr.FieldDefn('thumbnail', ogr.OFTString)
pg_layer.CreateField(fd_thumbnail)
fd_wikipediaurl = ogr.FieldDefn('wikipediaurl', ogr.OFTString)
pg_layer.CreateField(fd_wikipediaurl)
return pg_ds, pg_layer
def AddPlacesToLayer(places):
"""
Read the places dictionary list and add features in the
PostGIS table for each place.
"""
# iterate every place dictionary in the list
print "places: ", places
for place in places:
lng = place['lng']
lat = place['lat']
z = place.get('elevation') if 'elevation' in place else 0
# we generate a point representation in wkt,
# and create an ogr geometry
point_wkt = 'POINT(%s %s %s)' % (lng, lat, z)
point = ogr.CreateGeometryFromWkt(point_wkt)
# we create a LayerDefn for the feature using the one
# from the layer
featureDefn = pg_layer.GetLayerDefn()
feature = ogr.Feature(featureDefn)
# now time to assign the geometry and all the
# other feature's fields, if the keys are contained
# in the dictionary (not always the GeoNames
# Wikipedia Fulltext Search contains all of the information)
feature.SetGeometry(point)
feature.SetField('title',
place['title'].encode("utf-8") if 'title' in place else '')
feature.SetField('countrycode',
place['countryCode'] if 'countryCode' in place else '')
feature.SetField('feature',
place['feature'] if 'feature' in place else '')
feature.SetField('thumbnail',
place['thumbnailImg'] if 'thumbnailImg' in place else '')
feature.SetField('wikipediaurl',
place['wikipediaUrl'] if 'wikipediaUrl' in place else '')
# here we create the feature (the INSERT SQL is issued here)
pg_layer.CreateFeature(feature)
print 'Created a places titled %s.' % place['title']
def GetPlaces(placename):
"""
Get the places list for a given placename.
"""
# uri to access the JSON GeoNames Wikipedia Fulltext Search
# web service
uri = ('http://api.geonames.org/wikipediaSearchJSON?
formatted=true&q=%s&maxRows=%s&username=%s&style=full'
% (placename, MAXROWS, USERNAME))
data = requests.get(uri)
js_data = json.loads(data.text)
return js_data['geonames']
def GetNamesList(filepath):
"""
Open a file with a given filepath containing place names
and return a list.
"""
f = open(filepath, 'r')
return f.read().splitlines()
# first we need to create a PostGIS table to contains the places
# we must keep the PostGIS OGR dataset and layer global,
# for the reasons
# described here: http://trac.osgeo.org/gdal/wiki/PythonGotchas
from osgeo import gdal
gdal.UseExceptions()
pg_ds, pg_layer = CreatePGLayer()
try:
# query geonames for each name and store found
# places in the table
names = GetNamesList('names.txt')
print names
for name in names:
AddPlacesToLayer(GetPlaces(name))
except Exception as e:
print(e)
print sys.exc_info()[0]
- 现在,执行 Python 脚本:
(postgis-cb-env)$ python import_places.py

- 使用 SQL 测试表是否正确创建并填充,并使用您喜欢的 GIS 桌面工具显示该层:
postgis_cookbook=# select ST_AsText(the_geom), title,
countrycode, feature from chp08.wikiplaces;

(60 rows)
How it works...
此 Python 脚本使用 requests 和 simplejson 库从 GeoNames 的 wikipediaSearchJSON 网络服务获取数据,并使用 GDAL/OGR 库在 PostGIS 数据库中存储地理信息。
首先,您创建一个 PostGIS 点表来存储地理数据。这是使用 GDAL/OGR 绑定完成的。您需要实例化一个 OGR PostGIS 驱动程序(www.gdal.org/drv_pg.html),从该驱动程序可以实例化一个数据集以使用指定的连接字符串连接到您的 postgis_cookbook 数据库。
连接字符串中的 update 参数指定 GDAL 驱动程序您将打开数据集以进行更新。
从 PostGIS 数据集中,我们创建了一个名为 wikiplaces 的 PostGIS 层,该层将使用 WGS 84 空间参考系统(srs.ImportFromEPSG(4326))存储点(geom_type=ogr.wkbPoint)。在创建层时,我们还指定了其他参数,例如 dimension(3,因为你想存储 z 值),GEOMETRY_NAME(几何字段的名称)和 schema。创建层后,你可以使用 CreateField 层方法创建所有存储信息所需的字段。每个字段都将有一个特定的 name 和 datatype(在这种情况下,所有这些都是 ogr.OFTString)。
层创建完成后(请注意,我们需要在整个脚本中始终保持pg_ds和pg_layer对象处于上下文中,正如在trac.osgeo.org/gdal/wiki/PythonGotchas中所述),您可以使用urllib2库查询names.txt文件中每个地点名称的 GeoNames 网络服务。
我们使用simplejson库解析了 JSON 响应,然后迭代了 JSON 对象列表,并为 JSON 输出中的每个对象在 PostGIS 层中添加了一个特征。对于每个元素,我们使用ogr.CreateGeometryFromWkt方法创建了一个具有点wkt几何形状的特征(使用lng、lat和elevation对象属性),并使用特征setField方法(title、countryCode等)更新了其他字段,使用 GeoNames 返回的其他对象属性。
您可以通过以下由Chris Garrard提供的优秀资源获取更多关于使用 GDAL Python 绑定的编程信息:
www.manning.com/books/geoprocessing-with-python
使用 PL/Python 编写 PostGIS 函数
在这个菜谱中,您将使用 PL/Python 语言为 PostGIS 编写一个 Python 函数。PL/Python 过程语言允许您使用 Python 语言编写 PostgreSQL 函数。
您将使用 Python 查询openweathermap.org/网络服务,这些服务已在之前的菜谱中使用,以从 PostgreSQL 函数内部获取 PostGIS 几何形状的天气。
准备工作
- 验证您的 PostgreSQL 服务器安装是否具有 PL/Python 支持。在 Windows 上,这应该已经包含在内,但如果您使用的是例如 Ubuntu 16.04 LTS,则这不是默认设置,因此您可能需要安装它:
$ sudo apt-get install postgresql-plpython-9.1
- 在数据库上安装 PL/Python(您可以考虑在
template1数据库中安装它;这样,每个新创建的数据库都将默认具有 PL/Python 支持):
您可以选择使用createlang shell 命令将 PL/Python 支持添加到您的数据库中(如果您使用的是 PostgreSQL 版本 9.1 或更低版本,这是唯一的方法):
$ createlang plpythonu postgis_cookbook
$ psql -U me postgis_cookbook
postgis_cookbook=# CREATE EXTENSION plpythonu;
如何做到这一点...
执行以下步骤:
-
在这个菜谱中,就像上一个菜谱一样,您将使用一个
openweathermap.org/网络服务来获取从最近的气象站获取的温度。您需要运行的请求(在浏览器中测试它)是api.openweathermap.org/data/2.5/find?lat=55&lon=37&cnt=10&appid=YOURKEY。 -
您应该得到以下 JSON 输出(从最近的气象站读取的温度数据,该数据与您提供的经纬度点最接近):
{
message: "",
cod: "200",
calctime: "",
cnt: 1,
list: [
{
id: 9191,
dt: 1369343192,
name: "100704-1",
type: 2,
coord: {
lat: 13.7408,
lon: 100.5478
},
distance: 6.244,
main: {
temp: 300.37
},
wind: {
speed: 0,
deg: 141
},
rang: 30,
rain: {
1h: 0,
24h: 3.302,
today: 0
}
}
]
}
- 在 Python 中使用 PL/Python 语言创建以下 PostgreSQL 函数:
CREATE OR REPLACE FUNCTION chp08.GetWeather(lon float, lat float)
RETURNS float AS $$
import urllib2
import simplejson as json
data = urllib2.urlopen(
'http://api.openweathermap.org/data/
2.1/find/station?lat=%s&lon=%s&cnt=1'
% (lat, lon))
js_data = json.load(data)
if js_data['cod'] == '200':
# only if cod is 200 we got some effective results
if int(js_data['cnt'])>0:
# check if we have at least a weather station
station = js_data['list'][0]
print 'Data from weather station %s' % station['name']
if 'main' in station:
if 'temp' in station['main']:
temperature = station['main']['temp'] - 273.15
# we want the temperature in Celsius
else:
temperature = None
else:
temperature = None
return temperature
$$ LANGUAGE plpythonu;
- 现在,测试你的函数;例如,获取曼谷 Wat Pho Templum 附近气象站的温度:
postgis_cookbook=# SELECT chp08.GetWeather(100.49, 13.74);
getweather
------------
27.22
(1 row)
- 如果你想要获取 PostGIS 表中点特征的温度,你可以使用每个特征的几何体的坐标:
postgis_cookbook=# SELECT name, temperature,
chp08.GetWeather(ST_X(the_geom), ST_Y(the_geom))
AS temperature2 FROM chp08.cities LIMIT 5;
name | temperature | temperature2
-------------+-------------+--------------
Minneapolis | 275.15 | 15
Saint Paul | 274.15 | 16
Buffalo | 274.15 | 19.44
New York | 280.93 | 19.44
Jersey City | 282.15 | 21.67
(5 rows)
- 现在如果我们的函数能够接受一个点的坐标,同时也能接受一个真正的 PostGIS 几何体以及一个输入参数那就太好了。对于某个特征的温度,你可以返回离该特征几何体质心最近的气象站的温度。你可以通过函数重载轻松实现这种行为。添加一个新的函数,具有相同的名称,直接支持 PostGIS 几何体作为输入参数。在函数体中,调用之前的函数,传递几何体质心的坐标。请注意,在这种情况下,你可以使用 PL/PostgreSQL 语言而不使用 Python 编写函数:
CREATE OR REPLACE FUNCTION chp08.GetWeather(geom geometry)
RETURNS float AS $$
BEGIN
RETURN chp08.GetWeather(ST_X(ST_Centroid(geom)),
ST_Y(ST_Centroid(geom)));
END;
$$ LANGUAGE plpgsql;
- 现在,测试这个函数,传递一个 PostGIS 几何体给函数:
postgis_cookbook=# SELECT chp08.GetWeather(
ST_GeomFromText('POINT(-71.064544 42.28787)'));
getweather
------------
23.89
(1 row)
- 如果你在一个 PostGIS 图层上使用这个函数,你可以直接传递特征的几何体给函数,使用用 PL/PostgreSQL 语言编写的重载函数:
postgis_cookbook=# SELECT name, temperature,
chp08.GetWeather(the_geom) AS temperature2
FROM chp08.cities LIMIT 5;
name | temperature | temperature2
-------------+-------------+--------------
Minneapolis | 275.15 | 17.22
Saint Paul | 274.15 | 16
Buffalo | 274.15 | 18.89
New York | 280.93 | 19.44
Jersey City | 282.15 | 21.67
(5 rows)
它是如何工作的...
在这个菜谱中,你使用 PL/Python 语言在 PostGIS 中编写了一个 Python 函数。在 PostgreSQL 和 PostGIS 函数中使用 Python 给你带来了巨大的优势,即能够使用你想要的任何 Python 库。因此,你将能够编写比使用标准 PL/PostgreSQL 语言编写的函数更强大的函数。
实际上,在这种情况下,你使用了urllib2和simplejsonPython 库在 PostgreSQL 函数中查询一个网络服务——这将是使用纯 PL/PostgreSQL 无法完成的操作。你也已经看到了如何通过使用不同的输入参数方式来重载函数,以便为函数的用户提供不同的访问函数的方式。
使用 GeoNames 数据集进行地理编码和反向地理编码
在这个菜谱中,你将编写两个 PL/PostgreSQL PostGIS 函数,这将允许你使用 GeoNames 数据集执行地理编码和反向地理编码。
GeoNames 是一个包含世界上地名数据库,包含超过 800 万条记录,这些记录可以免费下载。为了这个菜谱的目的,你将下载数据库的一部分,将其加载到 PostGIS 中,然后在使用两个函数进行地理编码和反向地理编码。地理编码是从地理数据(如地址或地名)中查找坐标的过程,而反向地理编码是从坐标中查找地理数据(如地址或地名)的过程。
你将使用 PL/pgSQL 编写这两个函数,它是在 PostgreSQL SQL 命令的基础上添加了更多的命令和查询组合的能力、一系列控制结构、游标、错误管理和其他优点。
准备工作
下载 GeoNames 数据集。在撰写本文时,你可以从download.geonames.org/export/dump/下载一些准备好的数据集。你可以决定使用哪个数据集;如果你想遵循这个食谱,下载意大利数据集IT.zip(包含在本书的数据集中,位于chp08目录中)就足够了。
如果你想要下载完整的 GeoNames 数据集,你需要下载allCountries.zip文件;由于它大约有 250MB,所以下载时间会更长。
如何做...
执行以下步骤:
- 将
IT.zip文件解压到working/chp08目录。将提取两个文件:包含 GeoNames 数据库结构信息的readme.txt文件——你可以阅读它以获取更多信息——以及包含意大利所有 GeoNames 实体的.csv文件IT.txt。如readme.txt文件中建议的,CSV 文件的内容由以下属性组成的记录组成:
geonameid : integer id of record in geonames database
name : name of geographical point (utf8) varchar(200)
asciiname : name of geographical point in plain
ascii characters, varchar(200)
alternatenames : alternatenames, comma separated varchar(5000)
latitude : latitude in decimal degrees (wgs84)
longitude : longitude in decimal degrees (wgs84)
...
- 使用
ogrinfo获取此 CSV 数据集的概览:
$ ogrinfo CSV:IT.txt IT -al -so

- 你可以将
IT.txt文件作为 OGR 实体查询。例如,分析数据集的一个特征,如下面的代码所示:
$ ogrinfo CSV:IT.txt IT -where "NAME = 'San Gimignano'"

- 对于你的目的,你只需要
name、asciiname、latitude和longitude属性。你将使用 CSV OGR 驱动程序将文件导入 PostGIS(www.gdal.org/drv_csv.html)。使用ogr2ogr命令将此 GeoNames 数据集导入 PostGIS:
$ ogr2ogr -f PostgreSQL -s_srs EPSG:4326 -t_srs EPSG:4326
-lco GEOMETRY_NAME=the_geom -nln chp08.geonames
PG:"dbname='postgis_cookbook' user='me' password='mypassword'"
CSV:IT.txt -sql "SELECT NAME, ASCIINAME FROM IT"
- 尝试查询 PostGIS 中的新
geonames表,以查看过程是否正确工作:
postgis_cookbook=# SELECT ST_AsText(the_geom), name
FROM chp08.geonames LIMIT 10;
- 现在,创建一个 PL/PostgreSQL 函数,该函数将返回给定点的五个最近地点及其坐标(反向地理编码):
CREATE OR REPLACE FUNCTION chp08.Get_Closest_PlaceNames(
in_geom geometry, num_results int DEFAULT 5,
OUT geom geometry, OUT place_name character varying)
RETURNS SETOF RECORD AS $$
BEGIN
RETURN QUERY
SELECT the_geom as geom, name as place_name
FROM chp08.geonames
ORDER BY the_geom <-> ST_Centroid(in_geom) LIMIT num_results;
END;
$$ LANGUAGE plpgsql;
- 查询新函数。你可以通过传递可选的
num_results输入参数来指定你想要的结果数量:
postgis_cookbook=# SELECT * FROM chp08.Get_Closest_PlaceNames(
ST_PointFromText('POINT(13.5 42.19)', 4326), 10);
以下是该查询的输出:

- 如果你没有指定
num_results可选参数,它将默认为五个结果:
postgis_cookbook=# SELECT * FROM chp08.Get_Closest_PlaceNames(
ST_PointFromText('POINT(13.5 42.19)', 4326));
你将得到以下行:

- 现在,创建一个 PL/pgSQL 函数,该函数将返回包含其名称字段中文本搜索的地点名称和几何形状的列表(地理编码):
CREATE OR REPLACE FUNCTION chp08.Find_PlaceNames(search_string text,
num_results int DEFAULT 5,
OUT geom geometry,
OUT place_name character varying)
RETURNS SETOF RECORD AS $$
BEGIN
RETURN QUERY
SELECT the_geom as geom, name as place_name
FROM chp08.geonames
WHERE name @@ to_tsquery(search_string)
LIMIT num_results;
END;
$$ LANGUAGE plpgsql;
- 查询第二个函数以检查它是否正常工作:
postgis_cookbook=# SELECT * FROM chp08.Find_PlaceNames('Rocca', 10);

它是如何工作的...
在这个菜谱中,你编写了两个 PostgreSQL 函数来执行地理编码和反向地理编码。对于这两个函数,你定义了一组输入和输出参数,并在一些 PL/PostgreSQL 处理之后,通过执行查询将一组记录返回给函数客户端。
作为输入参数,Get_Closest_PlaceNames函数接受一个 PostGIS 几何形状和一个可选的num_results参数,如果没有由函数调用者提供,则默认设置为 5。这个函数的输出是SETOF RECORD,在函数体(由$$符号定义)中运行查询后返回。在这里,查询找到输入几何形状的重心最近的地点。这是通过索引最近邻搜索(KNN 索引)完成的,这是 PostGIS 2 中的一项新功能。
Find_PlaceNames函数接受作为输入参数的搜索字符串以及一个可选的num_results参数,在此情况下,如果没有由函数调用者提供,则默认设置为5。输出是一个SETOF RECORD,在运行使用to_tsquery PostgreSQL 文本搜索函数的查询后返回。查询的结果是从数据库中包含在名称字段中的search_string值的地点。
使用 OSM 数据集和三元组进行地理编码
在这个菜谱中,你将使用导入到 PostGIS 中的OpenStreetMap街道数据集来实现一个基本的 Python 类,以便为类的消费者提供地理编码功能。地理编码引擎将基于 PostgreSQL 的contrib模块提供的 PostgreSQL 三元组的实现:pg_trgm。
三元组是字符串中包含的三个连续字符的组合,通过计算两个字符串共有的三元组的数量,这是一种非常有效的方式来衡量两个字符串的相似度。
这个菜谱旨在提供一个非常基础的示例来实现一些地理编码功能(它将仅从街道名称返回一个或多个点),但它可以被扩展以支持更高级的功能。
准备工作
- 对于这个菜谱,请确保你有最新的 GDAL,至少版本 1.10,因为你将使用
ogr2ogrOGR OSM 驱动程序(www.gdal.org/drv_osm.html):
$ ogrinfo --version GDAL 2.1.2, released 2016/10/24
$ ogrinfo --formats | grep -i osm
-> "OSM -vector- (rov): OpenStreetMap XML and PBF"
- 由于你将使用 PostgreSQL 三元组,你需要安装 PostgreSQL 的
contrib包(它包括pg_trgm)。Windows EDB 安装程序应该已经包括这个包。在 Ubuntu 12.4 盒子上,以下命令将帮助你完成它:
$ sudo apt-get install postgresql-contrib-9.1
- 确保将
pg_trgm扩展添加到数据库中:
postgis_cookbook=# CREATE EXTENSION pg_trgm;
CREATE EXTENSION
你将需要使用包含在本章源中的某些 OSM 数据集。(在data/chp08书籍数据集目录中)。如果你使用 Windows,请确保已经安装了如本章初始说明中所建议的 OSGeo4W 套件。
- 如果你使用的是 Linux,请遵循本章的初始说明,创建一个 Python 虚拟环境,以保持一个用于本书所有 Python 脚本的 Python 隔离环境。然后,按照以下方式激活它:
$ source postgis-cb-env/bin/activate
- 一旦环境被激活,如果你还没有这样做,你可以安装这个菜谱所需的 Python 包:
(postgis-cb-env)$ pip install pygdal
(postgis-cb-env)$ pip install psycopg2
如何操作...
执行以下步骤:
- 首先,使用
ogrinfo检查 OSM.pbf文件是如何构建的。PBF 是一种二进制格式,旨在作为 OSM XML 格式的替代品,主要是因为它要小得多。正如你可能已经注意到的,它由几个层组成——你将导出lines层到 PostGIS,因为这个层包含你将用于整体地理编码过程的街道名称:
$ ogrinfo lazio.pbf
Had to open data source read-only.
INFO: Open of `lazio.pbf'
using driver `OSM' successful.
1: points (Point)
2: lines (Line String)
3: multilinestrings (Multi Line String)
4: multipolygons (Multi Polygon)
5: other_relations (Geometry Collection)
- 使用
ogr2ogr将行 OSM 特征导出到 PostGIS 表中(ogr2ogr,像往常一样,将隐式创建pg_trgm模块运行所需的 GiST 索引):
$ ogr2ogr -f PostgreSQL -lco GEOMETRY_NAME=the_geom
-nln chp08.osm_roads
PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" lazio.pbf lines
- 现在尝试使用以下查询进行三元组匹配以识别与给定搜索文本相似的街道名称。注意,
similarity函数返回一个值,该值随着单词相似性的降低而从1减少到0(1时字符串相同;0时字符串完全不同):
postgis_cookbook=# SELECT name,
similarity(name, 'via benedetto croce') AS sml,
ST_AsText(ST_Centroid(the_geom)) AS the_geom
FROM chp08.osm_roads
WHERE name % 'via benedetto croce'
ORDER BY sml DESC, name;

- 作为变体,你将使用以下查询来完成菜谱(在这种情况下,当权重为 0 时,字符串相同):
postgis_cookbook=# SELECT name,
name <-> 'via benedetto croce' AS weight
FROM chp08.osm_roads
ORDER BY weight LIMIT 10;

- 我们将使用最后一个查询作为 Python 类的 SQL 核心部分,该类将为消费者提供地理编码功能,使用我们刚刚导入到 PostGIS 中的层(
chp08.osm_roads)。首先,创建一个名为osmgeocoder.py的文件,并将以下类添加到其中:
import sys
import psycopg2
class OSMGeocoder(object):
"""
A class to provide geocoding features using an OSM
dataset in PostGIS.
"""
def __init__(self, db_connectionstring):
# initialize db connection parameters
self.db_connectionstring = db_connectionstring
def geocode(self, placename):
"""
Geocode a given place name.
"""
# here we create the connection object
conn = psycopg2.connect(self.db_connectionstring)
cur = conn.cursor()
# this is the core sql query, using trigrams to detect
# streets similar to a given placename
sql = """
SELECT name, name <-> '%s' AS weight,
ST_AsText(ST_Centroid(the_geom)) as point
FROM chp08.osm_roads
ORDER BY weight LIMIT 10;
""" % placename
# here we execute the sql and return all of the results
cur.execute(sql)
rows = cur.fetchall()
cur.close()
conn.close()
return rows
- 现在,添加
__main__检查,为类用户提供一个直接从命令行使用地理编码器的方法:
if __name__ == '__main__':
# the user must provide at least two parameters, the place name
# and the connection string to PostGIS
if len(sys.argv) < 3 or len(sys.argv) > 3:
print "usage: <placename> <connection string>"
raise SystemExit
placename = sys.argv[1]
db_connectionstring = sys.argv[2]
# here we instantiate the geocoder, providing the needed
# PostGIS connection parameters
geocoder = OSMGeocoder(db_connectionstring)
# here we query the geocode method, for getting the
# geocoded points for the given placename
results = geocoder.geocode(placename)
print results
- 现在你可以通过调用脚本测试这个类,如下所示:
(postgis-cb-env)$ python osmgeocoder.py "Via Benedetto Croce"
"dbname=postgis_cookbook user=me password=mypassword"
[('Via Benedetto Croce', 0.0, 'POINT(12.6999095325807
42.058016054317)'),...
- 因此,现在你已经编写了一个可以用于地理编码街道名称的类,假设另一个用户想要使用它来地理编码一个包含街道名称列表的文件,以便将其导入新的 PostGIS 层。以下是用户可以这样做的方法(也尝试一下)。首先,创建一个名为
streets.txt的文件,其中包含街道名称列表;例如:
Via Delle Sette Chiese
Via Benedetto Croce
Lungotevere Degli Inventori
Viale Marco Polo
Via Cavour
- 现在,创建一个名为
geocode_streets.py的文件,并将以下 Python 代码添加到其中(你将使用OSMGeocoder类来地理编码街道名称列表,并使用 GDAL/OGR 创建一个新的 PostGIS 层来存储街道名称的地理编码点):
from osmgeocoder import OSMGeocoder
from osgeo import ogr, osr
# here we read the file
f = open('streets.txt')
streets = f.read().splitlines()
f.close()
# here we create the PostGIS layer using gdal/ogr
driver = ogr.GetDriverByName('PostgreSQL')
srs = osr.SpatialReference()
srs.ImportFromEPSG(4326)
pg_ds = ogr.Open(
"PG:dbname='postgis_cookbook' host='localhost' port='5432'
user='me' password='mypassword'", update = 1 )
pg_layer = pg_ds.CreateLayer('geocoded_points', srs = srs,
geom_type=ogr.wkbPoint, options = [
'GEOMETRY_NAME=the_geom',
'OVERWRITE=YES',
# this will drop and recreate the table every time
'SCHEMA=chp08',
])
# here we add the field to the PostGIS layer
fd_name = ogr.FieldDefn('name', ogr.OFTString)
pg_layer.CreateField(fd_name)
print 'Table created.'
# now we geocode all of the streets in the file
# using the osmgeocoder class
geocoder = OSMGeocoder('dbname=postgis_cookbook user=me
password=mypassword')
for street in streets:
print street
geocoded_street = geocoder.geocode(street)[0]
print geocoded_street
# format is
# ('Via delle Sette Chiese', 0.0,
# 'POINT(12.5002166330412 41.859774874774)')
point_wkt = geocoded_street[2]
point = ogr.CreateGeometryFromWkt(point_wkt)
# we create a LayerDefn for the feature using the
# one from the layer
featureDefn = pg_layer.GetLayerDefn()
feature = ogr.Feature(featureDefn)
# now we store the feature geometry and
# the value for the name field
feature.SetGeometry(point)
feature.SetField('name', geocoded_street[0])
# finally we create the feature
# (an INSERT command is issued only here)
pg_layer.CreateFeature(feature)
- 运行前面的脚本,然后使用你喜欢的 PostgreSQL 客户端或 GIS 桌面工具检查街道名称的点是否正确进行了地理编码:
(postgis-cb-env)capooti@ubuntu:~/postgis_cookbook/working/chp08$
python geocode_streets.py
Table created.
Via Delle Sette Chiese
('Via delle Sette Chiese', 0.0,
'POINT(12.5002166330412 41.859774874774)')
...
Via Cavour
('Via Cavour', 0.0, 'POINT(12.7519263341222 41.9631244835521)')
它是如何工作的...
对于这个菜谱,你首先使用 GDAL OSM 驱动程序,通过 ogr2ogr 将 OSM 数据集导入到 PostGIS 中。
然后,你创建了一个 Python 类,OSMGeocoder,为类消费者提供对地理编码街道名称的基本支持,使用的是导入到 PostGIS 中的 OSM 数据。为此,你使用了 PostgreSQL 中的 trigram 支持以及 pg_trgm contrib 模块。
你编写的类主要由两个方法组成:__init__ 方法,其中必须传入连接参数以实例化一个 OSMGeocoder 对象,以及 geocode 方法。geocode 方法接受一个输入参数 placename,并使用 Psycopg2 库连接到 PostGIS 数据库,以执行查询以找到与 placename 参数名称相似的街道。
这个类可以从命令行使用 __name__ == '__main__' 代码块或从外部 Python 代码中消费。你尝试了这两种方法。在后一种方法中,你创建了一个新的 Python 脚本,在其中导入了 OSMGeocoder 类,并结合 GDAL/OGR Python 绑定来生成一个新的 PostGIS 点层,该层包含来自地理编码街道名称列表的特征。
使用 geopy 和 PL/Python 进行地理编码
在这个菜谱中,你将使用网络地理编码 API(如 Google Maps、Yahoo! Maps、Geocoder、GeoNames 等)来地理编码地址。在使用这些 API 进行生产之前,请务必仔细阅读其服务条款。
geopy Python 库(github.com/geopy/geopy)提供了对所有这些网络服务的方便统一访问。因此,你将使用它来创建一个 PL/Python PostgreSQL 函数,该函数可以在你的 SQL 命令中使用,以查询所有这些引擎。
准备工作
- 全局安装
geopy。在这种情况下,你不能使用虚拟环境,因为运行 PostgreSQL 服务的用户需要通过其 Python 路径访问它。
在 Debian/Ubuntu 系统中,操作就像输入以下命令一样简单:
$ sudo pip install geopy
在 Windows 中,你可以使用以下命令:
> pip install geopy
- 如果你还没有使用 PL/Python,请验证你的 PostgreSQL 服务器安装是否支持它。Windows EDB 安装程序应该已经包含了支持,但如果你使用的是例如 Ubuntu 16.04 LTS,那么这并不是默认设置,所以你很可能需要安装它:
$ sudo apt-get install postgresql-plpython-9.1
- 在数据库中安装 PL/Python(你可以考虑在
template1数据库中安装它;这样,每个新创建的数据库都将默认支持 PL/Python):
$ psql -U me postgis_cookbook
psql (9.1.6, server 9.1.8)
Type "help" for help.
postgis_cookbook=# CREATE EXTENSION plpythonu;
或者,你也可以使用 createlang 命令行来为你的数据库添加 PL/Python 支持(如果你使用的是 PostgreSQL 版本 9.1 及以下,这是唯一的方法):
$ createlang plpythonu postgis_cookbook
如何操作...
执行以下步骤:
- 作为第一个测试,打开你喜欢的 SQL 客户端(
psql或pgAdmin),编写一个非常基本的 PL/Python 函数,仅使用geopy的 GoogleV3 地理编码 API。该函数将接受地址字符串作为输入参数,并在导入geopy后,实例化一个geopyGoogle 地理编码器,运行地理编码过程,然后使用ST_GeomFromText函数和geopy输出返回点几何:
CREATE OR REPLACE FUNCTION chp08.Geocode(address text)
RETURNS geometry(Point,4326) AS $$
from geopy import geocoders
g = geocoders.GoogleV3()
place, (lat, lng) = g.geocode(address)
plpy.info('Geocoded %s for the address: %s' % (place, address))
plpy.info('Longitude is %s, Latitude is %s.' % (lng, lat))
plpy.info("SELECT ST_GeomFromText('POINT(%s %s)', 4326)"
% (lng, lat))
result = plpy.execute("SELECT ST_GeomFromText('POINT(%s %s)',
4326) AS point_geocoded" % (lng, lat))
geometry = result[0]["point_geocoded"]
return geometry
$$ LANGUAGE plpythonu;
- 创建函数后,尝试测试它:
postgis_cookbook=# SELECT chp08.Geocode('Viale Ostiense 36, Rome');
INFO: Geocoded Via Ostiense, 36, 00154 Rome,
Italy for the address: Viale Ostiense 36, Rome
CONTEXT: PL/Python function "geocode"
INFO: Longitude is 12.480457, Latitude is 41.874345.
CONTEXT: PL/Python function "geocode"
INFO: SELECT ST_GeomFromText('POINT(12.480457 41.874345)', 4326)
CONTEXT: PL/Python function "geocode"
geocode
----------------------------------------------------
0101000020E6100000BF44BC75FEF52840E7357689EAEF4440
(1 row)
- 现在,你将使函数变得更加复杂。首先,你将添加另一个输入参数,让用户指定地理编码 API 引擎(默认为 GoogleV3)。然后,使用 Python 的
try...except块,你将尝试添加某种错误管理,以防 geopy 地理编码器无法返回有效结果:
CREATE OR REPLACE FUNCTION chp08.Geocode(address text,
api text DEFAULT 'google')
RETURNS geometry(Point,4326) AS $$
from geopy import geocoders
plpy.info('Geocoing the given address using the %s api' % (api))
if api.lower() == 'geonames':
g = geocoders.GeoNames()
elif api.lower() == 'geocoderdotus':
g = geocoders.GeocoderDotUS()
else: # in all other cases, we use google
g = geocoders.GoogleV3()
try:
place, (lat, lng) = g.geocode(address)
plpy.info('Geocoded %s for the address: %s' % (place, address))
plpy.info('Longitude is %s, Latitude is %s.' % (lng, lat))
result = plpy.execute("SELECT ST_GeomFromText('POINT(%s %s)',
4326) AS point_geocoded" % (lng, lat))
geometry = result[0]["point_geocoded"]
return geometry
except:
plpy.warning('There was an error in the geocoding process,
setting geometry to Null.')
return None
$$ LANGUAGE plpythonu;
- 在不指定 API 参数的情况下测试你函数的新版本。在这种情况下,它应该默认为 Google API:
postgis_cookbook=# SELECT chp08.Geocode('161 Court Street,
Brooklyn, NY');
INFO: Geocoing the given address using the google api
CONTEXT: PL/Python function "geocode2"
INFO: Geocoded 161 Court Street, Brooklyn, NY 11201,
USA for the address: 161 Court Street, Brooklyn, NY
CONTEXT: PL/Python function "geocode2"
INFO: Longitude is -73.9924659, Latitude is 40.688665.
CONTEXT: PL/Python function "geocode2"
INFO: SELECT ST_GeomFromText('POINT(-73.9924659 40.688665)', 4326)
CONTEXT: PL/Python function "geocode2"
geocode2
----------------------------------------------------
0101000020E61000004BB9B18F847F52C02E73BA2C26584440
(1 row)
- 如果你通过指定不同的 API 进行测试,它应该返回针对给定 API 处理的结果。例如:
postgis_cookbook=# SELECT chp08.Geocode('161 Court Street,
Brooklyn, NY', 'GeocoderDotUS');
INFO: Geocoing the given address using the GeocoderDotUS api
CONTEXT: PL/Python function "geocode2"
INFO: Geocoded 161 Court St, New York, NY 11201 for the address: 161
Court Street, Brooklyn, NY
CONTEXT: PL/Python function "geocode2"
INFO: Longitude is -73.992809, Latitude is 40.688774.
CONTEXT: PL/Python function "geocode2"
INFO: SELECT ST_GeomFromText('POINT(-73.992809 40.688774)', 4326)
CONTEXT: PL/Python function "geocode2"
geocode2
----------------------------------------------------
0101000020E61000002A8BC22E8A7F52C0E52A16BF29584440
(1 row)
- 作为额外步骤,在 PostgreSQL 中创建一个包含街道地址的表,并生成一个新的点 PostGIS 层,存储 Geocode 函数返回的地理编码点。
它是如何工作的...
你编写了一个 PL/Python 函数来对地址进行地理编码。为此,你使用了 geopy Python 库,它允许你以相同的方式查询多个地理编码 API。
使用 geopy,你需要使用给定的 API 实例化一个 geocoder 对象,并查询它以获取结果,例如地点名称和一对坐标。你可以使用 plpy 模块工具在数据库上运行查询,使用 PostGIS 的 ST_GeomFromText 函数,并为用户记录信息性消息和警告。
如果地理编码过程失败,你将使用 try..except Python 块向用户返回一个带有警告信息的 NULL 几何形状。
使用 Python 和 GDAL 导入 NetCDF 数据集
在这个菜谱中,你将编写一个 Python 脚本来从 NetCDF 格式导入数据到 PostGIS。
NetCDF 是一个开放标准格式,广泛用于科学应用,可以包含多个栅格数据集,每个数据集由一系列波段组成。为此,你将使用 GDAL Python 绑定和流行的 NumPy (www.numpy.org/) 科学库。
准备工作
- 如果你正在使用 Windows,请确保安装 OSGeo4W,正如本章初始说明中所建议的。这将包括 Python 和 GDAL Python 绑定,以及支持 NumPy。
对于 Linux 用户,如果你还没有这样做,请遵循本章初始说明,创建一个 Python 虚拟环境,以保持一个用于本书中所有 Python 脚本的 Python 隔离环境。然后,激活它:
$ source postgis-cb-env/bin/activate
- 对于这个菜谱,您需要 GDAL Python 绑定和 NumPy,后者是 GDAL 方法(
ReadAsArray)所需的数组。在最可能的情况下,您已经将 GDAL 安装在了您的虚拟环境中,因为您一直在使用它来运行其他菜谱,所以请确保在安装 NumPy 后将其移除并重新安装。实际上,如果您想使用其数组功能,GDAL 需要与 NumPy 支持一起编译:
(postgis-cb-env)$ pip uninstall gdal
(postgis-cb-env)$ pip install numpy
(postgis-cb-env)$ pip install gdal
- 为了这个菜谱的目的,您将使用 NOAA 地球系统研究实验室(ESRL)的样本数据集。ESRL 的优秀网络门户提供了大量 NetCDF 格式的数据,可以免费下载。例如,从 ESRL CPC 土壤湿度数据存储库下载以下数据集(您可以在本书的数据集目录中找到这个数据集的副本):
www.esrl.noaa.gov/psd/data/gridded/data.cpcsoil.html。
如何操作...
执行以下步骤:
- 作为第一步,使用
gdalinfo调查您下载的数据集的 NetCDF 格式。这类数据集由几个子数据集组成,您可能已经通过查看gdalinfo输出意识到了这一点:
$ gdalinfo NETCDF:"soilw.mon.ltm.v2.nc"

- 使用
gdalinfo调查文件的一个子数据集。NetCDF GDAL 驱动程序(www.gdal.org/frmt_netcdf.html)使用的语法是在文件名末尾附加一个冒号,后跟变量名。例如,尝试找出soilw子数据集由多少个波段组成。这个子数据集代表lwe_thickness_of_soil_moisture_content,由 12 个波段组成。根据其元数据获得的信息,每个波段代表给定月份的 CPC 月度土壤湿度。月份由NETCDF_DIM_time元数据值标识,这是从年初开始的天数(1 月为 0,2 月为 31,3 月为 59,等等):
$ gdalinfo NETCDF:"soilw.mon.ltm.v2.nc":soilw

...(12 bands)...
- 您将要执行的操作是创建一个使用 GDAL 和 NumPy 的 Python 脚本。您将读取一个给定的 NetCDF 数据集,迭代其子数据集,然后迭代每个子数据集的波段。对于每个子数据集,您将创建一个点 PostGIS 层,并为每个波段添加一个字段以在层表中存储波段值。然后,您将迭代波段的单元格,并为每个单元格在层中添加一个带有相应波段值的点。因此,创建一个
netcdf2postgis.py文件,并将以下 Python 代码添加到其中:
import sys
from osgeo import gdal, ogr, osr
from osgeo.gdalconst import GA_ReadOnly, GA_Update
def netcdf2postgis(file_nc, pg_connection_string,
postgis_table_prefix):
# register gdal drivers
gdal.AllRegister()
# postgis driver, needed to create the tables
driver = ogr.GetDriverByName('PostgreSQL')
srs = osr.SpatialReference()
# for simplicity we will assume all of the bands in the datasets
# are in the same spatial reference, wgs 84
srs.ImportFromEPSG(4326)
# first, check if dataset exists
ds = gdal.Open(file_nc, GA_ReadOnly)
if ds is None:
print 'Cannot open ' + file_nc
sys.exit(1)
# 1\. iterate subdatasets
for sds in ds.GetSubDatasets():
dataset_name = sds[0]
variable = sds[0].split(':')[-1]
print 'Importing from %s the variable %s...' %
(dataset_name, variable)
# open subdataset and read its properties
sds = gdal.Open(dataset_name, GA_ReadOnly)
cols = sds.RasterXSize
rows = sds.RasterYSize
bands = sds.RasterCount
# create a PostGIS table for the subdataset variable
table_name = '%s_%s' % (postgis_table_prefix, variable)
pg_ds = ogr.Open(pg_connection_string, GA_Update )
pg_layer = pg_ds.CreateLayer(table_name, srs = srs,
geom_type=ogr.wkbPoint, options = [
'GEOMETRY_NAME=the_geom',
'OVERWRITE=YES',
# this will drop and recreate the table every time
'SCHEMA=chp08',
])
print 'Table %s created.' % table_name
# get georeference transformation information
transform = sds.GetGeoTransform()
pixelWidth = transform[1]
pixelHeight = transform[5]
xOrigin = transform[0] + (pixelWidth/2)
yOrigin = transform[3] - (pixelWidth/2)
# 2\. iterate subdataset bands and append them to data
data = []
for b in range(1, bands+1):
band = sds.GetRasterBand(b)
band_data = band.ReadAsArray(0, 0, cols, rows)
data.append(band_data)
# here we add the fields to the table, a field for each band
# check datatype (Float32, 'Float64', ...)
datatype = gdal.GetDataTypeName(band.DataType)
ogr_ft = ogr.OFTString # default for a field is string
if datatype in ('Float32', 'Float64'):
ogr_ft = ogr.OFTReal
elif datatype in ('Int16', 'Int32'):
ogr_ft = ogr.OFTInteger
# here we add the field to the PostGIS layer
fd_band = ogr.FieldDefn('band_%s' % b, ogr_ft)
pg_layer.CreateField(fd_band)
print 'Field band_%s created.' % b
# 3\. iterate rows and cols
for r in range(0, rows):
y = yOrigin + (r * pixelHeight)
for c in range(0, cols):
x = xOrigin + (c * pixelWidth)
# for each cell, let's add a point feature
# in the PostGIS table
point_wkt = 'POINT(%s %s)' % (x, y)
point = ogr.CreateGeometryFromWkt(point_wkt)
featureDefn = pg_layer.GetLayerDefn()
feature = ogr.Feature(featureDefn)
# now iterate bands, and add a value for each table's field
for b in range(1, bands+1):
band = sds.GetRasterBand(1)
datatype = gdal.GetDataTypeName(band.DataType)
value = data[b-1][r,c]
print 'Storing a value for variable %s in point x: %s,
y: %s, band: %s, value: %s' % (variable, x, y, b, value)
if datatype in ('Float32', 'Float64'):
value = float(data[b-1][r,c])
elif datatype in ('Int16', 'Int32'):
value = int(data[b-1][r,c])
else:
value = data[r,c]
feature.SetField('band_%s' % b, value)
# set the feature's geometry and finalize its creation
feature.SetGeometry(point)
pg_layer.CreateFeature(feature)
- 要从命令行运行
netcdf2postgis方法,请添加脚本的入口点。代码将检查脚本用户是否正确使用了三个必需的参数,这些参数是 NetCDF 文件路径、GDAL PostGIS 连接字符串以及用于 PostGIS 中表名的前缀/后缀:
if __name__ == '__main__':
# the user must provide at least three parameters,
# the netCDF file path, the PostGIS GDAL connection string
# and the prefix suffix to use for PostGIS table names
if len(sys.argv) < 4 or len(sys.argv) > 4:
print "usage: <netCDF file path> <GDAL PostGIS connection
string><PostGIS table prefix>"
raise SystemExit
file_nc = sys.argv[1]
pg_connection_string = sys.argv[2]
postgis_table_prefix = sys.argv[3]
netcdf2postgis(file_nc, pg_connection_string,
postgis_table_prefix)
- 运行脚本。请确保使用正确的 NetCDF 文件路径、GDAL PostGIS 连接字符串(请检查格式在
www.gdal.org/drv_pg.html),以及必须附加到将在 PostGIS 中创建的表名的表前缀:
(postgis-cb-env)$ python netcdf2postgis.py
NETCDF:"soilw.mon.ltm.v2.nc"
"PG:dbname='postgis_cookbook' host='localhost'
port='5432' user='me' password='mypassword'" netcdf
Importing from NETCDF:"soilw.mon.ltm.v2.nc":
climatology_bounds the variable climatology_bounds...
...
Importing from NETCDF:"soilw.mon.ltm.v2.nc":soilw the
variable soilw...
Table netcdf_soilw created.
Field band_1 created.
Field band_2 created.
...
Field band_11 created.
Field band_12 created.
Storing a value for variable soilw in point x: 0.25,
y: 89.75, band: 2, value: -9.96921e+36
Storing a value for variable soilw in point x: 0.25,
y: 89.75, band: 3, value: -9.96921e+36
...
- 在过程结束时,通过使用您最喜欢的 GIS 桌面工具打开其中一个输出 PostGIS 表来检查结果。以下截图显示了它在 QGIS
soilw图层中的样子,其背后是原始的 NetCDF 数据集:

它是如何工作的...
您已经使用 Python 和 GDAL 以及 NumPy 创建了一个命令行实用程序,用于将 NetCDF 数据集导入到 PostGIS。
NetCDF 数据集由多个子数据集组成,每个子数据集由多个栅格波段组成。每个波段由单元格组成。在调查一个示例 NetCDF 数据集后,使用 gdalinfo GDAL 命令工具,这种结构应该对您来说很清晰。
有几种方法可以将单元格值导出到 PostGIS。您在这里采用的方法是为每个子数据集生成一个 PostGIS 点图层,该图层由每个子数据集波段的一个字段组成。然后,您迭代栅格单元格,并将从每个单元格波段读取的值附加到 PostGIS 图层中。
您使用 Python 的方式是通过使用 GDAL Python 绑定。对于读取,您打开 NetCDF 数据集,对于更新,您打开 PostGIS 数据库,使用正确的 GDAL 和 OGR 驱动程序。然后,您使用 GetSubDatasets 方法迭代 NetCDF 子数据集,并使用 CreateLayer 方法为每个子数据集创建一个名为 NetCDF 子数据集变量(带有前缀)的 PostGIS 表。
对于每个子数据集,您使用 GetRasterBand 方法迭代其波段。为了读取每个波段,您运行 ReadAsArray 方法,该方法使用 NumPy 将波段作为数组获取。
对于每个波段,您在 PostGIS 图层中创建一个字段,该字段具有正确的字段数据类型,可以存储波段的值。为了选择正确的数据类型,您调查波段的数据类型,使用 DataType 属性。
最后,您通过使用子数据集变换参数(通过 GetGeoTransform 方法获取)读取正确的 x 和 y 坐标来迭代栅格单元格。对于每个单元格,您使用 CreateGeometryFromWkt 方法创建一个点,然后设置字段值,并使用 SetField 功能方法从波段数组中读取。
最后,您使用 CreateFeature 方法将新点附加到 PostGIS 图层。
第九章:PostGIS 和网络
在本章中,我们将涵盖以下主题:
-
使用 MapServer 创建 WMS 和 WFS 服务
-
使用 GeoServer 创建 WMS 和 WFS 服务
-
使用 MapServer 创建 WMS 时间服务
-
使用 OpenLayers 消费 WMS 服务
-
使用 Leaflet 消费 WMS 服务
-
使用 OpenLayers 消费 WFS-T 服务
-
使用 GeoDjango 开发网络应用程序 – 第一部分
-
使用 GeoDjango 开发网络应用程序 – 第二部分
-
使用 Mapbox 开发网络 GPX 查看器
简介
在本章中,我们将尝试为您概述如何使用 PostGIS 开发强大的 GIS 网络应用程序,利用 开放地理空间联盟 (OGC) 网络标准,如 网络地图服务 (WMS) 和 网络要素服务 (WFS)。
在前两个食谱中,您将概述两个非常流行的开源网络地图引擎,MapServer 和 GeoServer。在这两个食谱中,您将了解如何使用 PostGIS 层实现 WMS 和 WFS 服务。
在第三个食谱中,您将使用 MapServer 实现 WMS 时间服务,以公开时间序列数据。
在接下来的两个食谱中,您将学习如何使用两个非常流行的 JavaScript 客户端来创建网络地图查看器。在第四个食谱中,您将使用 OpenLayers 来使用 WMS 服务,而在第五个食谱中,您将使用 Leaflet 来做同样的事情。
在第六个食谱中,您将探索事务性 WFS 的力量,以创建允许编辑数据的网络地图应用程序。
在接下来的两个食谱中,您将释放流行的基于 Python 的 Django 网络框架及其优秀的 GeoDjango 库的力量,并了解如何实现一个强大的 CRUD GIS 网络应用程序。在第七个食谱中,您将使用 Django Admin 站点为该应用程序创建后台,而在本章的最后一个食谱中,您将为用户提供一个前端,以便在基于 Leaflet 的网络地图中显示应用程序中的数据。
最后,在本章的最后一个食谱中,您将学习如何使用 OGR 将您的 PostGIS 数据导入 Mapbox,以创建一个定制的网络 GPX 查看器。
使用 MapServer 创建 WMS 和 WFS 服务
在本食谱中,您将了解如何使用流行的开源网络地图引擎 MapServer,从 PostGIS 层创建 WMS 和 WFS。
然后,您将使用这些服务,通过首先使用浏览器然后使用桌面工具(如 QGIS)来测试它们公开的请求(您可以使用其他软件,如 uDig、gvSIG 和 OpenJUMP GIS 来完成此操作)。
准备工作
在准备工作之前,请遵循以下步骤:
- 使用以下命令在
postgis_cookbook数据库中为本章创建一个模式:
postgis_cookbook=# create schema chp09;
-
确保已安装 Apache HTTP(MapServer 将作为 CGI 在其上运行)并检查其是否正常工作,通过访问其主页
http://localhost(通常,如果您尚未自定义任何功能,将显示It works!消息)。 -
按照其安装指南 (
mapserver.org/installation/index.html) 安装 MapServer。
在 Windows 上为 Apache 安装 MapServer 并使其运行的一个便捷方法是安装 OSGeo4W (trac.osgeo.org/osgeo4w/) 或 MS4W (www.maptools.org/ms4w/) 软件包。
对于 Linux,几乎任何类型的发行版都有相应的软件包。
对于 macOS,您可以使用 CMake 应用程序来构建安装,或者使用 Homebrew 并使用以下命令(注意编译时需要使用的标志以支持 Postgres):
brew install mapserver --with-postgresql --with-geos
- 通过以命令行工具运行并使用
-v选项来检查 MapServer 是否已正确安装,并且已启用POSTGIS、WMS_SERVER和WFS_SERVER支持。
在 Linux 上,运行 $ /usr/lib/cgi-bin/mapserv -v 命令并检查以下输出:
MapServer version 7.0.7 OUTPUT=GIF OUTPUT=PNG OUTPUT=JPEG SUPPORTS=PROJ
SUPPORTS=GD SUPPORTS=AGG SUPPORTS=FREETYPE SUPPORTS=CAIRO
SUPPORTS=SVG_SYMBOLS
SUPPORTS=ICONV SUPPORTS=FRIBIDI SUPPORTS=WMS_SERVER SUPPORTS=WMS_CLIENT
SUPPORTS=WFS_SERVER SUPPORTS=WFS_CLIENT SUPPORTS=WCS_SERVER
SUPPORTS=SOS_SERVER SUPPORTS=FASTCGI SUPPORTS=THREADS SUPPORTS=GEOS
INPUT=JPEG INPUT=POSTGIS INPUT=OGR INPUT=GDAL INPUT=SHAPEFILE
在 Windows 上,运行以下命令:
c:\ms4w\Apache\cgi-bin\mapserv.exe -v
在 macOS 上,使用 $ mapserv -v 命令:

-
现在,通过使用
http://localhost/cgi-bin/mapserv(http://localhost/cgi-bin/mapserv.exe对于 Windows)检查 MapServer 是否在 HTTPD 中运行。如果您收到No query information to decode. QUERY_STRING is set, but empty的响应消息,则 MapServer 正确作为 Apache 中的 CGI 脚本运行,并准备好接受 HTTP 请求。 -
从
thematicmapping.org/downloads/TM_WORLD_BORDERS-0.3.zip下载世界国家形状文件。本书数据集中包含此形状文件的副本,用于 第一章,将数据导入和导出 PostGIS。将形状文件提取到working/chp09目录,并使用 shp2pgsql 工具将其导入 PostGIS(确保使用-s选项指定空间参考系统,EPSG:4326),如下所示:
$ shp2pgsql -s 4326 -W LATIN1 -g the_geom -I TM_WORLD_BORDERS-0.3.shp
chp09.countries > countries.sql
Shapefile type: Polygon
Postgis type: MULTIPOLYGON[2]
$ psql -U me -d postgis_cookbook -f countries.sql
如何做到这一点……
执行以下步骤:
- MapServer 通过
mapfile文本文件格式公开其地图服务,该格式可以用来在网络上定义 PostGIS 层,启用 GDAL 支持的任何矢量或栅格格式,并指定每个图层要公开的服务(WMS/WFS/WCS)。创建一个名为countries.map的新文本文件,并添加以下代码:
MAP # Start of mapfile
NAME 'population_per_country_map'
IMAGETYPE PNG
EXTENT -180 -90 180 90
SIZE 800 400
IMAGECOLOR 255 255 255
# map projection definition
PROJECTION
'init=epsg:4326'
END
# web section: here we define the ows services
WEB
# WMS and WFS server settings
METADATA
'ows_enable_request' '*'
'ows_title' 'Mapserver sample map'
'ows_abstract' 'OWS services about
population per
country map'
'wms_onlineresource' 'http://localhost/cgi-
bin/mapserv?map=/var
/www/data/
countries.map&'
'ows_srs' 'EPSG:4326 EPSG:900913
EPSG:3857'
'wms_enable_request' 'GetCapabilities,
GetMap,
GetFeatureInfo'
'wms_feature_info_mime_type' 'text/html'
END
END
# Start of layers definition
LAYER # Countries polygon layer begins here
NAME countries
CONNECTIONTYPE POSTGIS
CONNECTION 'host=localhost dbname=postgis_cookbook
user=me password=mypassword port=5432'
DATA 'the_geom from chp09.countries'
TEMPLATE 'template.html'
METADATA
'ows_title' 'countries'
'ows_abstract' 'OWS service about population per
country map in 2005'
'gml_include_items' 'all'
END
STATUS ON
TYPE POLYGON
# layer projection definition
PROJECTION
'init=epsg:4326'
END
# we define 3 population classes based on the pop2005
attribute
CLASSITEM 'pop2005'
CLASS # first class
NAME '0 - 50M inhabitants'
EXPRESSION ( ([pop2005] >= 0) AND ([pop2005] <=
50000000) )
STYLE
WIDTH 1
OUTLINECOLOR 0 0 0
COLOR 254 240 217
END # end of style
END # end of first class
CLASS # second class
NAME '50M - 200M inhabitants'
EXPRESSION ( ([pop2005] > 50000000) AND
([pop2005] <= 200000000) )
STYLE
WIDTH 1
OUTLINECOLOR 0 0 0
COLOR 252 141 89
END # end of style
END # end of second class
CLASS # third class
NAME '> 200M inhabitants'
EXPRESSION ( ([pop2005] > 200000000) )
STYLE
WIDTH 1
OUTLINECOLOR 0 0 0
COLOR 179 0 0
END # end of style
END # end of third class
END # Countries polygon layer ends here
END # End of mapfile
- 将我们刚刚创建的文件保存在 Apache 用户可访问的位置。例如,在 Debian 上是
/var/www/data,在 Windows 上可以是C:\ms4w\Apache\htdocs;对于 macOS,您应该使用/Library/WebServer/Documents。
确保文件及其所在的目录对 Apache 用户可访问。
- 在与
mapfile相同的位置创建一个名为template.html的文件,并在其中输入以下代码(此文件由GetFeatureInfoWMS 请求用于向客户端输出 HTML 响应):
<!-- MapServer Template -->
<ul>
<li><strong>Name: </strong>[item name=name]</li>
<li><strong>ISO2: </strong>[item name=iso2]</li>
<li><strong>ISO3: </strong>[item name=iso3]</li>
<li>
<strong>Population 2005:</strong> [item name=pop2005]
</li>
</ul>
-
使用你刚刚创建的
mapfile,你已将countriesPostGIS 图层暴露为 WMS 和 WFS 服务。这两个服务都向用户暴露了一系列请求,你现在将使用浏览器来测试它们。首先,不调用任何服务,通过在浏览器中输入以下 URL 来测试mapfile是否正确工作:-
http://localhost/cgi-bin/mapserv?map=/var/www/data/countries.map&layer=countries&mode=map(适用于 Linux) -
http://localhost/cgi-bin/mapserv.exe?map=C:\ms4w\Apache\htdocs\countries.map&layer=countries&mode=map(适用于 Windows) -
http://localhost/cgi-bin/mapserv?map=/Library/WebServer/Documents/countries.map&layer=countries&mode=map(适用于 macOS)
-
你应该看到countries图层以mapfile中定义的三个符号类渲染,如下面的截图所示:

如你所见,Windows、Linux 和 macOS 中使用的 URL 之间有一个小的差异。我们现在将参考 Linux,但你很容易将这些 URL 适配到 Windows 或 macOS。
- 现在你将开始测试 WMS 服务;你将尝试运行
GetCapabilities、GetMap和GetFeatureInfo请求。要测试GetCapabilities请求,在浏览器中输入 URL:http://localhost/cgi-bin/mapserv?map=/var/www/data/countries.map&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetCapabilities。你应该从服务器收到一个长的 XML 响应(如下所示),其中更重要的片段是<Service>部分中的 WMS 服务定义,请求在<Capability>部分中启用,暴露的图层及其主要细节(例如,名称、摘要、投影和范围)在每个图层的<Layer>部分中:
<WMT_MS_Capabilities version="1.1.1">
...
<Service>
<Name>OGC:WMS</Name>
<Title>Population per country map</Title>
<Abstract>Map server sample map</Abstract>
<OnlineResource
xlink:href="http://localhost/cgi-
bin/mapserv?map=/var/www/data/countries.map&"/>
<ContactInformation> </ContactInformation>
</Service>
<Capability>
<Request>
<GetCapabilities>
...
</GetCapabilities>
<GetMap>
<Format>image/png</Format>
...
<Format>image/tiff</Format>
...
</GetMap>
<GetFeatureInfo>
<Format>text/plain</Format>
...
</GetFeatureInfo>
...
</Request>
...
<Layer>
<Name>population_per_country_map</Name>
<Title>Population per country map</Title>
<Abstract>OWS service about population per country map
in 2005</Abstract>
<SRS>EPSG:4326</SRS>
<SRS>EPSG:3857</SRS>
<LatLonBoundingBox minx="-180" miny="-90" maxx="180"
maxy="90" />
...
</Layer>
</Layer>
</Capability>
</WMT_MS_Capabilities>
-
现在测试 WMS 服务,使用其典型的
GetMapWMS 请求,许多客户端使用它来向用户显示地图。输入 URLhttp://localhost//cgi-bin/mapserv?map=/var/www/data/countries.map&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-26,-111,36,-38&CRS=EPSG:4326&WIDTH=1000&HEIGHT=800&LAYERS=countries&STYLES=&FORMAT=image/png -
在浏览器中检查 MapServer
GetMap请求返回的图像,如下面的截图所示:

- 另一个典型的 WMS 请求是
GetFeatureInfo,客户端使用它来查询给定坐标(点)的地图图层。输入以下 URL,你应该看到给定特征的字段值作为输出(输出是使用template.html文件构建的):
http://localhost/cgi-bin/mapserv?map=/var/www/data/
countries.map&layer=countries&REQUEST=GetFeatureInfo&
SERVICE=WMS&VERSION=1.1.1&LAYERS=countries&
QUERY_LAYERS=countries&SRS=EPSG:4326&BBOX=-122.545074509804,
37.6736653056517,-122.35457254902,37.8428758708189&
X=652&Y=368&WIDTH=1020&HEIGHT=906&INFO_FORMAT=text/html
输出应该如下所示:

- 现在,您将使用 QGIS 来使用 WMS 服务。启动 QGIS,点击添加 WMS 图层按钮(或者,导航到图层 | 添加 WMS 图层或使用 QGIS 浏览器),并创建一个新的 WMS 连接,如下面的截图所示。在名称字段中输入类似
MapServer on localhost的内容,并在 URL 字段中输入http://localhost/cgi-bin/mapserv?map=/var/www/data/countries.map&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetCapabilities,然后点击确定按钮(请记住根据您操作系统的配置调整 Apache URL;检查第 4 步):

- 现在,点击以下截图所示的连接按钮。然后,选择国家图层,并使用添加按钮将其添加到 QGIS 地图窗口中,确保选择坐标系统 EPSG:4326:

- 现在,浏览到您的 WMS 国家图层,并尝试执行一些识别操作。QGIS 将在幕后为您生成所需的
GetMap和GetFeatureInfoWMS 请求,以产生以下输出:

-
现在您已经了解了 WMS 服务的工作原理,您现在将开始使用 WFS。与 WMS 类似,WFS 也向用户提供了一个
GetCapabilities请求,从而产生了与 WMS 的GetCapabilities请求类似的输出。将 URLhttp://localhost/cgi-bin/mapserv?map=/var/www/data/countries.map&SERVICE=WFS&VERSION=1.0.0&REQUEST=GetCapabilities输入到浏览器窗口中,以检查 XML 响应。 -
主要的 WFS 请求是
GetFeature。它允许您使用多个标准查询地图图层,并以地理标记语言(GML)输出返回一个特征集合。通过在浏览器中输入以下 URL 来测试请求:http://localhost/cgi-bin/mapserv?map=/var/www/data/countries.map&SERVICE=WFS&VERSION=1.0.0&REQUEST=getfeature&TYPENAME=countries&MAXFEATURES=5。 -
您应该从浏览器中获得一个 XML(GML)响应,如下面的代码所示,其中包含一个由五个
<gml:featureMember>元素组成的<wfs:FeatureCollection>元素(如请求中的MAXFEATURES参数所示),每个元素代表一个国家。对于每个特征,WFS 返回几何形状和所有字段值(这种行为是通过在mapfile中的METADATA图层指令中设置gml_include_items变量来指定的)。您将看到以下几何形状:
<gml:featureMember>
<ms:countries>
<gml:boundedBy>
<gml:Box srsName="EPSG:4326">
<gml:coordinates>-61.891113,16.989719 -
61.666389,17.724998</gml:coordinates>
</gml:Box>
</gml:boundedBy>
<ms:msGeometry>
<gml:MultiPolygon srsName="EPSG:4326">
<gml:polygonMember>
<gml:Polygon>
<gml:outerBoundaryIs>
<gml:LinearRing>
<gml:coordinates>
-61.686668,17.024441 ...
</gml:coordinates>
</gml:LinearRing>
</gml:outerBoundaryIs>
</gml:Polygon>
</gml:polygonMember>
...
</gml:MultiPolygon>
</ms:msGeometry>
<ms:gid>1</ms:gid>
<ms:fips>AC</ms:fips>
<ms:iso2>AG</ms:iso2>
<ms:iso3>ATG</ms:iso3>
<ms:un>28</ms:un>
<ms:name>Antigua and Barbuda</ms:name>
<ms:area>44</ms:area>
<ms:pop2005>83039</ms:pop2005>
<ms:region>19</ms:region>
<ms:subregion>29</ms:subregion>
<ms:lon>-61.783</ms:lon>
<ms:lat>17.078</ms:lat>
</ms:countries>
</gml:featureMember>
-
由于在上一步骤中执行了 WFS
GetFeature请求,MapServer 只返回了countries图层的前五个特征。现在,使用GetFeature请求通过过滤器对图层进行查询,并获取相应的特征。通过输入 URLhttp://localhost/cgi-bin/mapserv?map=/var/www/data/countries.map&SERVICE=WFS&VERSION=1.0.0&REQUEST=getfeature&TYPENAME=countries&MAXFEATURES=5&Filter=<Filter> <PropertyIsEqualTo><PropertyName>name</PropertyName> <Literal>Italy</Literal></PropertyIsEqualTo></Filter>,你可以获取数据库中name字段设置为Italy的特征。 -
在浏览器中测试 WFS 请求后,尝试使用 QGIS 中的“添加 WFS 图层”按钮打开 WFS 服务(或者导航到“图层”|“添加 WFS 图层”或使用 QGIS 浏览器)。你应该看到你之前创建的相同的 MapServer on Localhost 连接。点击“连接”按钮,选择 countries 图层,将其添加到 QGIS 项目中,并通过缩放、平移和识别一些特征来浏览它。与 WMS 相比,最大的不同是,使用 WFS,你从服务器接收的是特征几何形状,而不仅仅是图像,因此你甚至可以将图层导出为不同的格式,如 shapefile 或 spatialite!从服务器添加 WFS 图层的窗口截图如下:

现在,你应该能够在 QGIS 中看到矢量地图并检查其特征:

它是如何工作的...
在这个示例中,你使用 MapServer 开源网络地图引擎为 PostGIS 图层实现了 WMS 和 WFS 服务。当你想要开发一个跨多个组织互操作的网络 GIS 时,WMS 和 WFS 是两个需要考虑的核心概念。开放地理空间联盟(OGC)定义了这两个标准(以及许多其他标准),以便以开放和标准的方式公开网络地图服务。这样,这些服务就可以被不同的应用程序使用;例如,你在这个示例中看到,一个 GIS 桌面工具,如 QGIS,可以浏览和查询这些服务,因为它理解这些 OGC 标准(你可以用其他工具,如 gvSIG、uDig、OpenJUMP 和 ArcGIS Desktop 等,得到相同的结果)。同样,JavaScript API 库,尤其是 OpenLayers 和 Leaflet(你将在本章的其他示例中使用这些库),可以以标准方式使用这些服务,为网络应用程序提供网络地图功能。
WMS 是一种服务,用于生成客户端显示的地图。这些地图使用图像格式生成,例如 PNG、JPEG 以及许多其他格式。以下是一些最典型的 WMS 请求:
-
GetCapabilities: 这提供了 WMS 提供的服务概述,特别是可用图层列表以及每个图层的某些详细信息(图层范围、坐标参考系统、数据 URI 等)。 -
GetMap: 这返回一个地图图像,表示一个或多个图层,对于指定的范围和空间参考,以指定的图像文件格式和大小。 -
GetFeatureInfo: 这是 WMS 的一个可选请求,它以不同的格式返回给定地图点上的功能属性值。您已经看到了如何通过引入一个必须设置在mapfile中的模板文件来自定义响应。
WFS 提供了一种方便、标准的方式来通过 Web 请求访问矢量图层的功能。服务将请求的功能以 GML(由 OGC 定义的 XML 标记)的形式流式传输到客户端,GML 用于定义地理特征。
一些 WFS 请求如下:
-
GetCapabilities: 这提供了 WFS 服务提供的服务和图层的描述。 -
GetFeature: 这允许客户端获取给定图层的一组功能,对应于给定的标准。
这些 WMS 和 WFS 请求可以通过 HTTP 协议由客户端消费。您已经看到了如何通过在浏览器中输入带有多个附加参数的 URL 来查询并从客户端获取响应。例如,以下 WMS GetMap请求将返回一个地图图像,该图像包含使用LAYERS参数指定的图层,以使用FORMAT参数指定的格式,使用WIDTH和HEIGHT参数指定的大小,使用BBOX参数指定的范围,以及使用CRS参数指定的空间参考系统。
http://localhost/cgi-bin/mapserv?map=/var/www/data/countries.map&&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-26,-111,36,-38&CRS=EPSG:4326&WIDTH=806&HEIGHT=688&LAYERS=countries&STYLES=&FORMAT=image/png
在 MapServer 中,您可以使用其指令在mapfile中创建 WMS 和 WFS 服务。mapfile是一个由几个部分组成的文本文件,是 MapServer 的核心。在mapfile的开始部分,需要定义地图的一般属性,如标题、范围、空间参考、输出图像格式以及要返回给用户的维度。
然后,可以定义要公开哪些 OWS(如 WMS、WFS 和 WCS 等 OGC 网络服务)请求。
然后是mapfile的主要部分,其中定义了图层(每个图层都在LAYER指令中定义)。您已经看到了如何定义一个 PostGIS 图层。需要定义其连接信息(数据库、用户、密码等),数据库中的 SQL 定义(可以使用仅 PostGIS 表名,但最终可以使用查询来定义定义图层的功能集和属性),几何类型和投影。
使用整个指令(CLASS)来定义图层功能如何渲染。您可以使用不同的类,就像在这个菜谱中做的那样,根据使用CLASSITEM设置定义的属性来以不同的方式渲染功能。在这个菜谱中,您定义了三个不同的类,每个类代表一个人口类别,使用不同的颜色。
参考信息
-
您可以通过访问其项目主页上的详细文档(
mapserver.org/it/index.html)来获取更多关于使用 MapServer 的信息。您会发现www.mapserver.org/mapfile/上的 mapfile 文档非常有助于阅读。 -
您可以在
mapserver.org/tutorial/example1-1.html找到一篇很好的教程,了解如何生成 mapfiles。 -
如果您想更好地理解 WMS 和 WFS 标准,请查看 OGC 网站上的规范。对于 WMS 服务,请访问
www.opengeospatial.org/standards/wms,而对于 WFS,请访问www.opengeospatial.org/standards/wfs。
使用 GeoServer 创建 WMS 和 WFS 服务
在上一个食谱中,您使用 MapServer 从 PostGIS 层创建了 WMS 和 WFS。在这个食谱中,您将使用另一个流行的开源网络地图引擎-GeoServer 来完成这项工作。然后,您将像使用 MapServer 一样使用创建的服务,测试其暴露的请求,首先使用浏览器,然后使用 QGIS 桌面工具(您可以使用其他软件,如 uDig、gvSIG、OpenJUMP GIS 和 ArcGIS Desktop)。
准备工作
虽然 MapServer 是用 C 语言编写的,并使用 Apache 作为其 web 服务器,但 GeoServer 是用 Java 编写的,因此您需要在系统中安装Java 虚拟机(JVM);它必须从一个 servlet 容器中使用,例如Jetty和Tomcat。在安装 servlet 容器后,您将能够将 GeoServer 应用程序部署到其中。例如,在 Tomcat 中,您可以通过将 GeoServer 的WAR(web archive)文件复制到 Tomcat 的webapps目录中来部署 GeoServer。对于这个食谱,我们假设您系统中已经有一个正在运行的 GeoServer;如果不是这种情况,请按照 GeoServer 网站上的详细安装步骤([docs.geoserver.org/stable/en/user/installation/](http://docs.geoserver.org/stable/en/user/installation/))进行安装,然后返回到这个食谱。按照以下步骤操作:
- 从https://nationalmap.gov/网站下载美国县级行政区划的 shapefile(
dds.cr.usgs.gov/pub/data/nationalatlas/countyp020_nt00009.tar.gz),(这个存档包含在本书的代码包中)。从working/chp09中提取存档,并使用ogr2ogr命令将其导入 PostGIS,如下所示:
$ ogr2ogr -f PostgreSQL -a_srs EPSG:4326 -lco GEOMETRY_NAME=the_geom
-nln chp09.counties PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" countyp020.shp
如何操作...
执行以下步骤:
- 在您的浏览器中打开 GeoServer 管理界面,通常位于
http://localhost:8080/geoserver,并使用您的凭据(用户名为admin,密码为geoserver)登录,如果您只是使用 GeoServer 默认安装且未进行任何自定义。启动 GeoServer 后,您应该看到以下内容:

在浏览器中查看的 GeoServer 欢迎屏幕
-
成功登录后,通过点击 GeoServer 应用程序主菜单左侧面板中的“工作”下的“工作空间”链接,然后点击“添加新工作空间”链接来创建一个工作空间。在出现的表单文本框中指定以下值,然后点击“提交”按钮:
-
在名称字段中输入
postgis_cookbook -
在命名空间 URI 字段中输入 URL
www.packtpub.com/big-data-and-business-intelligence/postgis-cookbook
-
-
现在,要创建 PostGIS 存储,点击“数据”下左侧面板中的“存储”链接。现在,点击“添加新存储”链接,然后在矢量数据源下的“PostGIS”链接,如图所示:

GeoServer 屏幕配置新数据源
-
在“新矢量数据源”页面,按照以下方式填写表单的字段:
-
从工作空间下拉列表中选择 postgis_cookbook。
-
在数据源名称字段中输入
postgis_cookbook -
在主机字段中输入
localhost -
在端口字段中输入
5432 -
在数据库字段中输入
postgis_cookbook -
在模式字段中输入
chp09 -
在用户字段中输入
me -
在 passwd 字段中输入
mypassword
-
新矢量数据源页面如图所示:

-
现在,点击“保存”按钮以成功创建您的 PostGIS 存储。
-
现在,您已准备好将 PostGIS 的
counties图层作为 WMS 和 WFS 发布。在“图层”页面,点击“添加新资源”链接。现在,从“从下拉列表添加图层”中选择 postgis_cookbook。点击counties图层右侧的“发布”链接。 -
在以下截图所示的“编辑图层”页面,点击“从数据计算”和“从原生边界计算”链接,然后点击“保存”按钮:

GeoServer 屏幕编辑用于发布的国家图层
-
现在,您需要定义用于向用户显示图层的样式。与 MapServer 不同,GeoServer 使用 OGC 标准的样式图层描述符(SLD)符号。在“数据”下点击“样式”链接,然后点击“添加新样式”链接。按照以下方式填写表单中的文本字段:
-
在名称字段中输入
Counties classified per size -
在工作空间字段中输入
postgis_cookbook
-
-
在 SLD 的文本区域中,添加以下定义
counties层样式的 XML 代码。然后,点击验证按钮检查您的 SLD 定义是否正确,然后点击提交按钮保存新的样式:
<?xml version="1.0" encoding="UTF-8"?>
<sld:StyledLayerDescriptor
version="1.0.0">
<sld:NamedLayer>
<sld:Name>county_classification</sld:Name>
<sld:UserStyle>
<sld:Name>county_classification</sld:Name>
<sld:Title>County area classification</sld:Title>
<sld:FeatureTypeStyle>
<sld:Name>name</sld:Name>
<sld:Rule>
<sld:Title>Large counties</sld:Title>
<ogc:Filter>
<ogc:PropertyIsGreaterThanOrEqualTo>
<ogc:PropertyName>square_mil</ogc:PropertyName>
<ogc:Literal>5000</ogc:Literal>
</ogc:PropertyIsGreaterThanOrEqualTo>
</ogc:Filter>
<sld:PolygonSymbolizer>
<sld:Fill>
<sld:CssParameter
name="fill">#FF0000</sld:CssParameter>
</sld:Fill>
<sld:Stroke/>
</sld:PolygonSymbolizer>
</sld:Rule>
<sld:Rule>
<sld:Title>Small counties</sld:Title>
<ogc:Filter>
<ogc:PropertyIsLessThan>
<ogc:PropertyName>square_mil</ogc:PropertyName>
<ogc:Literal>5000</ogc:Literal>
</ogc:PropertyIsLessThan>
</ogc:Filter>
<sld:PolygonSymbolizer>
<sld:Fill>
<sld:CssParameter
name="fill">#0000FF</sld:CssParameter>
</sld:Fill>
<sld:Stroke/>
</sld:PolygonSymbolizer>
</sld:Rule>
</sld:FeatureTypeStyle>
</sld:UserStyle>
</sld:NamedLayer>
</sld:StyledLayerDescriptor>
以下截图显示了新样式在“新样式 GeoServer”页面上的外观:

GeoServer 创建新样式作为 SLD 文档的屏幕截图
-
现在,您需要将创建的样式与
counties层关联起来。返回到层页面(数据 | 层),点击counties层链接,然后在编辑层页面,点击发布部分。在默认样式下拉列表中选择按大小分类的县,然后点击保存按钮。 -
现在您的 PostGIS
counties层的 WMS 和 WFS 服务已经准备好了,是时候开始使用它们了!首先,测试GetCapabilitiesWMS 请求。为此,您可以在 GeoServer 网页应用程序主页的右侧面板上点击其中一个链接。您可以点击 WMS 版本 1.1.1 或 WMS 版本 1.3.0 的链接。点击其中一个链接或直接在浏览器中输入GetCapabilities请求,格式为http://localhost:8080/geoserver/ows?service=wms&version=1.3.0&request=GetCapabilities。 -
现在,我们将调查以下所示的
GetCapabilities响应。您将发现有关 WMS 的许多信息都可在您的 GeoServer 实例上找到,例如 WMS 支持的请求、投影以及每个发布的层的大量其他信息。在counties层的情况下,以下代码是从GetCapabilities文档中提取的。注意主要层信息,如名称、标题、摘要(您可以使用 GeoServer 网页应用程序重新定义所有这些),支持的坐标参考系统(CRS)、地理范围以及关联的样式:
<Layer queryable="1">
<Name>postgis_cookbook:counties</Name>
<Title>counties</Title>
<Abstract/>
<KeywordList>
<Keyword>counties</Keyword>
<Keyword>features</Keyword>
</KeywordList>
<CRS>EPSG:4326</CRS>
<CRS>CRS:84</CRS>
<EX_GeographicBoundingBox>
<westBoundLongitude>-179.133392333984
</westBoundLongitude>
<eastBoundLongitude>-64.566162109375
</eastBoundLongitude>
<southBoundLatitude>17.6746921539307
</southBoundLatitude>
<northBoundLatitude>71.3980484008789
</northBoundLatitude>
</EX_GeographicBoundingBox>
<BoundingBox CRS="CRS:84" minx="-179.133392333984"
miny="17.6746921539307" maxx="-64.566162109375"
maxy="71.3980484008789"/>
<BoundingBox CRS="EPSG:4326" minx="17.6746921539307"
miny="-179.133392333984" maxx="71.3980484008789" maxy="-
64.566162109375"/>
<Style>
<Name>Counties classified per size</Name>
<Title>County area classification</Title>
<Abstract/>
<LegendURL width="20" height="20">
<Format>image/png</Format>
<OnlineResource
xlink:type="simple" xlink:href=
"http://localhost:8080/geoserver/
ows?service=WMS&request=GetLegendGraphic&
format=image%2Fpng&width=20&height=20&
layer=counties"/>
</LegendURL>
</Style>
</Layer>
-
要测试
GetMap和GetFeatureInfoWMS 请求,GeoServer 网页应用程序提供了一个非常方便的方法,即层预览页面。导航到数据 | 层预览,然后点击counties层旁边的 OpenLayers 链接。层预览页面基于 OpenLayers JavaScript 库,并允许您对GetMap和GetFeatureInfo请求进行实验。 -
尝试在地图中导航;在每次缩放和平移操作时,GeoServer 都会根据响应输出向
GetMap请求提供新的图像。通过点击地图,您可以执行GetFeatureInfo请求,用户界面将显示您点击的地图上的点的特征属性。在导航地图时检查请求发送到 GeoServer 的方式,使用 Firefox Firebug 插件或 Chrome(或如果您使用 Linux,则使用 Chromium)开发者工具是非常有效的方法。使用这些工具,您将能够识别从 OpenLayers 观察器到 GeoServer 后台发送的GetMap和GetFeatureInfo请求。以下是一个这样的地图的截图:

当您使用任何浏览器开发者工具检查请求时,检查请求 URL 并验证发送到 GeoServer 的参数;以下是使用 Firefox 的样子:

-
现在,尝试通过在浏览器中输入 URL
http://localhost:8080/geoserver/postgis_cookbook/wms?LAYERS=postgis_cookbook%3Acounties&STYLES=&FORMAT=image%2Fpng&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG%3A4326&BBOX=-200.50286594033,7.6152902245522,-43.196688503029,81.457450330258&WIDTH=703&HEIGHT=330来执行 WMSGetMap请求。 -
也尝试使用 WMS
GetFeatureInfo请求,通过输入 URLhttp://localhost:8080/geoserver/postgis_cookbook/wms?REQUEST=GetFeatureInfo&EXCEPTIONS=application%2Fvnd.ogc.se_xml&BBOX=-126.094303%2C37.16812%2C-116.262667%2C41.783255&SERVICE=WMS&INFO_FORMAT=text%2Fhtml&QUERY_LAYERS=postgis_cookbook%3Acounties&FEATURE_COUNT=50&Layers=postgis_cookbook%3Acounties&WIDTH=703&HEIGHT=330&format=image%2Fpng&styles=&srs=EPSG%3A4326&version=1.1.1&x=330&y=158.
这将通过提示前面的 URL 来显示:

- 现在,就像您为 MapService WMS 所做的那样,在 QGIS 中测试 GeoServer WMS。创建一个名为
GeoServer on localhost的 WMS 连接,指向 GeoServer 的GetCapabilities文档(http://localhost:8080/geoserver/ows?service=wms&version=1.3.0&request=GetCapabilities)。然后,连接到 WMS 服务器(例如,从 QGIS 浏览器),从图层列表中选择counties,并将其添加到地图中,如图所示;然后导航图层并尝试识别一些特征:

-
在使用 WMS 之后,尝试测试几个 WFS 请求。一个典型的 WFS
GetCapability请求可以通过输入 URLhttp://localhost:8080/geoserver/wfs?service=wfs&version=1.1.0&request=GetCapabilities来执行。您也可以点击 GeoServer 网页界面的首页上的 WFS 链接之一。 -
调查 XML 的
GetCapabilities响应,并尝试识别有关您图层的信息。您应该有一个<FeatureType>元素,如下所示,对应于counties图层:
<FeatureType>
<Name>postgis_cookbook:counties</Name>
<Title>counties</Title>
<Abstract/>
<Keywords>counties, features</Keywords>
<SRS>EPSG:4326</SRS>
<LatLongBoundingBox minx="-179.133392333984"
miny="17.6746921539307" maxx="-64.566162109375"
maxy="71.3980484008789"/>
</FeatureType>
- 如前一个菜谱所示,典型的 WFS 请求是
GetFeature,这将导致 GML 响应。在您的浏览器中输入 URLhttp://localhost:8080/geoserver/wfs?service=wfs&version=1.0.0&request=GetFeature&typeName=postgis_cookbook:counties&maxFeatures=5尝试。您将收到一个由<wfs:FeatureCollection>元素和一系列<gml:featureMember>元素(可能五个元素,如maxFeatures请求参数中指定的)组成的 GML 输出。您将得到一个类似于以下代码的输出:
<gml:featureMember>
<postgis_cookbook:counties fid="counties.3962">
<postgis_cookbook:the_geom>
<gml:Polygon srsName="http://www.opengis.net/
gml/srs/epsg.xml#4326">
<gml:outerBoundaryIs>
<gml:LinearRing>
<gml:coordinates xmlns:gml=
"http://www.opengis.net/gml"
decimal="." cs="," ts="">
-101.62554932,36.50246048 -
101.0908432,36.50032043 ...
...
...
</gml:coordinates>
</gml:LinearRing>
</gml:outerBoundaryIs>
</gml:Polygon>
</postgis_cookbook:the_geom>
<postgis_cookbook:area>0.240</postgis_cookbook:area>
<postgis_cookbook:perimeter>1.967
</postgis_cookbook:perimeter>
<postgis_cookbook:co2000p020>3963.0
</postgis_cookbook:co2000p020>
<postgis_cookbook:state>TX</postgis_cookbook:state>
<postgis_cookbook:county>Hansford
County</postgis_cookbook:county>
<postgis_cookbook:fips>48195</postgis_cookbook:fips>
<postgis_cookbook:state_fips>48
</postgis_cookbook:state_fips>
<postgis_cookbook:square_mil>919.801
</postgis_cookbook:square_mil>
</postgis_cookbook:counties>
</gml:featureMember>
-
现在,就像您使用 WMS 一样,尝试在 QGIS(或您喜欢的桌面 GIS 客户端)中使用 counties WFS。通过使用 QGIS 浏览器或添加 WFS 图层按钮,然后点击“新建连接”按钮来创建一个新的 WFS 连接。在创建新的 WFS 连接对话框中,在“名称”字段中输入
GeoServer on localhost,并在“URL”字段中添加 WFSGetCapabilitiesURL (http://localhost:8080/geoserver/wfs?service=wfs&version=1.1.0&request=GetCapabilities)。 -
从上一个对话框中添加 WFS
counties图层,并作为一个测试,选择一些县并使用图层上下文菜单中的“另存为”命令将它们导出到一个新的 shapefile,如图下所示:

它是如何工作的...
在上一个菜谱中,您通过 MapServer 介绍了 OGC WMS 和 WFS 标准的基本概念。在本菜谱中,您使用了另一个流行的开源网络地图引擎 GeoServer 来完成同样的任务。
与用 C 语言编写的 MapServer 不同,MapServer 可以作为 CGI 程序在 Apache HTTP(HTTPD)或 Microsoft Internet Information Server(IIS)等 Web 服务器上使用,GeoServer 是用 Java 编写的,并且需要一个如 Apache Tomcat 或 Eclipse Jetty 之类的 Servlet 容器才能运行。
GeoServer 不仅为用户提供了一个高度可扩展和标准的网络地图引擎实现,而且还提供了一个良好的用户界面,即 Web 管理界面。因此,与需要掌握 mapfile 语法才能使用 MapServer 相比,初学者通常更容易创建 WMS 和 WFS 服务。
GeoServer 为 PostGIS 图层创建 WMS 和 WFS 服务的流程是首先创建一个 PostGIS 存储库,在那里您需要关联主要的 PostGIS 连接参数(服务器名称、模式、用户等)。存储库创建正确后,您可以发布该 PostGIS 存储库可用的图层。您在本菜谱中已经看到,使用 GeoServer 网络管理界面整个过程是多么简单。
为了定义渲染特性的图层样式,GeoServer 使用基于 XML 的 SLD 架构,这是一个 OGC 标准。在本食谱中,我们编写了两个不同的规则来渲染面积大于 5,000 平方英里的县,以与其他县不同的方式渲染。为了以不同的方式渲染县,我们使用了两个 <ogc:Rule> SLD 元素,在其中您定义了一个 <ogc:Filter> 元素。对于这些元素中的每一个,您都定义了过滤图层特性的标准,使用了 <ogc:PropertyIsGreaterThanOrEqualTo> 和 <ogc:PropertyIsLessThan> 元素。生成图层 SLD 的一个非常方便的方法是使用能够导出图层 SLD 文件的桌面 GIS 工具(QGIS 可以做到这一点)。导出文件后,您可以通过将 SLD 文件内容复制到“添加新样式”页面来上传它到 GeoServer。
在为县图层创建了 WMS 和 WFS 服务后,您通过使用便捷的图层预览 GeoServer 界面(基于 OpenLayers)生成请求,然后在浏览器中直接输入请求来测试它们。您可以从图层预览界面或直接在 URL 查询字符串中更改每个服务请求的参数。
最后,您使用 QGIS 测试了服务,并看到了如何使用 WFS 服务导出图层的一些特性。
参见
如果您想了解更多关于 GeoServer 的信息,您可以查看其优秀的文档docs.geoserver.org/,或者阅读 Packt Publishing 出版的精彩的《GeoServer 初学者指南》一书(www.packtpub.com/geoserver-share-edit-geospatial-data-beginners-guide/book)。
使用 MapServer 创建 WMS 时间服务
在本食谱中,您将实现一个带有 MapServer 的 WMS 时间服务。对于时间序列数据,以及当您有持续更新的地理数据并且需要将其作为 Web GIS 中的 WMS 公开时,WMS 时间是最佳选择。这是通过在 WMS 请求中提供 TIME 参数一个时间值来实现的,通常在 GetMap 请求中。
在这里,您将为热点实现一个 WMS 时间服务,代表由 NASA 的地球观测系统数据和信息系统(EOSDIS)获取的可能火灾数据。这个优秀的系统提供了来自 MODIS 图像的过去 24 小时、48 小时和 7 天的数据,这些数据可以下载为 shapefile、KML、WMS 或文本文件格式。您将加载大量此类数据到 PostGIS,使用 MapServer 创建一个 WMS 时间服务,并使用通用浏览器测试 WMS 的 GetCapabilities 和 GetMap 请求。
如果您对 WMS 标准不熟悉,请查看前面的两个食谱以获取更多信息。
准备工作
-
首先,从 EOSDIS 网站下载一周的活跃火灾数据(热点)。例如,EOSDIS 的 Firedata 可以在这个链接中找到:
earthdata.nasa.gov/earth-observation-data/near-real-time/firms/active-fire-data。本书代码包中包含此 shapefile 的副本。如果您想使用以下步骤中使用的 SQL 和 WMS 参数,请使用它。 -
将
Global_7d.zip归档中的 shapefile 提取到working/chp09目录,并使用shp2pgsql命令将此 shapefile 导入 PostGIS,如下所示:
$ shp2pgsql -s 4326 -g the_geom -I
MODIS_C6_Global_7d.shp chp09.hotspots > hotspots.sql
$ psql -U me -d postgis_cookbook -f hotspots.sql
- 导入完成后,检查您刚刚导入到 PostGIS 中的点火灾数据(热点)。每个热点都包含大量有用的信息,最值得注意的是存储在
acq_date和acq_time字段中的几何形状和采集日期和时间。您可以使用以下命令轻松地看到从 shapefile 加载的特征跨越了连续的八天:
postgis_cookbook=# SELECT acq_date, count(*) AS hotspots_count
FROM chp09.hotspots GROUP BY acq_date ORDER BY acq_date;
之前的命令将产生以下输出:

如何做到这一点...
执行以下步骤:
- 我们首先将为 PostGIS 热点层创建一个 WMS。在 HTTPD(或 IIS)用户可访问的目录中创建一个名为
hotspots.map的mapfile(例如,在 Linux 中为/var/www/data,在 macOS 中为/Library/WebServer/Documents/,在 Windows 中为C:\ms4w\Apache\htdocs),在调整数据库连接设置后执行以下代码:
MAP # Start of mapfile
NAME 'hotspots_time_series'
IMAGETYPE PNG
EXTENT -180 -90 180 90
SIZE 800 400
IMAGECOLOR 255 255 255
# map projection definition
PROJECTION
'init=epsg:4326'
END
# a symbol for hotspots
SYMBOL
NAME "circle"
TYPE ellipse
FILLED true
POINTS
1 1
END
END
# web section: here we define the ows services
WEB
# WMS and WFS server settings
METADATA
'wms_name' 'Hotspots'
'wms_title' 'World hotspots time
series'
'wms_abstract' 'Active fire data detected
by NASA Earth Observing
System Data and Information
System (EOSDIS)'
'wms_onlineresource' 'http://localhost/cgi-bin/
mapserv?map=/var/www/data/
hotspots.map&'
'wms_srs' 'EPSG:4326 EPSG:3857'
'wms_enable_request' '*'
'wms_feature_info_mime_type' 'text/html'
END
END
# Start of layers definition
LAYER # Hotspots point layer begins here
NAME hotspots
CONNECTIONTYPE POSTGIS
CONNECTION 'host=localhost dbname=postgis_cookbook
user=me
password=mypassword port=5432'
DATA 'the_geom from chp09.hotspots'
TEMPLATE 'template.html'
METADATA
'wms_title' 'World hotspots time
series'
'gml_include_items' 'all'
END
STATUS ON
TYPE POINT
CLASS
SYMBOL 'circle'
SIZE 4
COLOR 255 0 0
END # end of class
END # hotspots layer ends here
END # End of mapfile
-
通过在浏览器中输入以下 URL 检查此 mapfile 的 WMS GetCapabilities 请求是否正常工作:
-
http://localhost/cgi-bin/mapserv?map=/var/www/data/hotspots.map&SERVICE=WMS&VERSION=1.0.0&REQUEST=GetCapabilities(在 Linux 系统中) -
http://localhost/cgi-bin/mapserv.exe?map=C:\ms4w\Apache\htdoc\shotspots.map&SERVICE=WMS&VERSION=1.0.0&REQUEST=GetCapabilities(在 Windows 系统中) -
http://localhost/cgi-bin/mapserv?map=/Library/WebServer/Documents/hotspots.map& SERVICE=WMS&VERSION=1.0.0&REQUEST=GetCapabilities(在 macOS 系统中)
-
在以下步骤中,我们将参考 Linux。如果您使用的是 Windows,只需将 http://localhost/cgi-bin/mapserv?map=/var/www/data/hotspots.map 替换为 http://localhost/cgi-bin/mapserv.exe?map=C:\ms4w\Apache\htdoc\shotspots.map;或者如果您使用的是 macOS,则在每个请求中将它替换为 http://localhost/cgi-bin/mapserv?map=/Library/WebServer/Documents/hotsposts.map:
- 现在,使用
GetMap请求查询 WMS 服务。在浏览器中输入以下 URL。如果一切正常,MapServer 应该返回一个包含一些热点的图像作为响应。URL 是http://localhost/cgi-bin/mapserv?map=/var/www/data/hotspots.map&&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-25,-100,35,-35&CRS=EPSG:4326&WIDTH=1000&HEIGHT=800&LAYERS=hotspots&STYLES=&FORMAT=image/png。
浏览器上显示的地图将如下所示:

- 到目前为止,你已经实现了一个简单的 WMS 服务。现在,为了使
TIME参数可用于 WMS 时间请求,在LAYER METADATA部分添加wms_timeextent、wms_timeitem和wms_timedefault变量,如下所示:
METADATA
'wms_title' 'World hotspots time
series'
'gml_include_items' 'all'
'wms_timeextent' '2000-01-01/2020-12-31' # time extent
for which the service will give a response
'wms_timeitem' 'acq_date' # layer field to use to filter
on the TIME parameter
'wms_timedefault' '2013-05-30' # default parameter if not
added to the request
END
- 在
LAYER METADATA地图文件部分添加了这些参数后,WMSGetCapabilities响应应该会改变。现在,热点图层定义包括由<Dimension>和<Extent>元素定义的时间维度。你将得到如下响应:

-
你最终可以测试具有时间支持的 WMS 服务。你只需要记住在
GetMap请求中添加TIME参数(否则,GetMap将使用默认日期过滤数据,在这个例子中是2017-12-12)使用 URLhttp://localhost/cgi-bin/mapserv?map=/var/www/data/hotspots.map&&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-25,-100,35,-35&CRS=EPSG:4326&WIDTH=1000&HEIGHT=800&LAYERS=hotspots&STYLES=&FORMAT=image/png&TIME=2017-12-10。 -
在前面的 URL 中玩一下
TIME参数,看看 GetMap 图像响应是如何一天天变化的。记住,对于我们导入的数据集,acq_date的范围是从2017-12-07到2017-12-14;但如果你没有使用书中数据集包含的 hostpots shapefile,时间范围将不同!以下是在提到的日期和用于查询服务的完整 URL 的不同输出:
http://localhost/cgi-bin/mapserv?map=/var/www/data/hotspots.map&&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-25,-100,35,-35&CRS=EPSG:4326&WIDTH=1000&HEIGHT=800&LAYERS=hotspots&STYLES=&FORMAT=image/png&TIME=2017-12-14. 输出如下(2017-12-14):

-
http://localhost/cgi-bin/mapserv?map=/var/www/data/hotspots.map&&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-25,-100,35,-35&CRS=EPSG:4326&WIDTH=1000&HEIGHT=800&LAYERS=hotspots&STYLES=&FORMAT=image/png&TIME=2017-12-07. 输出如下(2017-12-07):

它是如何工作的...
在这个食谱中,你看到了如何使用 MapServer 开源网络地图引擎创建 WMS 时间服务。WMS 时间服务在您有随时间变化的时空序列和地理数据时非常有用。WMS 时间服务允许用户通过在 WMS 请求中提供包含时间值的TIME参数来过滤请求的数据。
为了这个目的,你首先创建了一个简单的 WMS;如果你对 WMS 标准、mapfile 和 MapServer 不熟悉,可以查看本章的第一个食谱。你已经在 PostGIS 中导入了一个包含一周热点数据的点 shapefile,并为此图层创建了一个简单的 WMS。
在通过测试 WMS GetCapabilities 和 GetMap 请求验证此 WMS 工作良好后,您可以通过在 LAYER METADATA 地图文件部分添加三个参数来启用 WMS 的时间功能:wms_timeextent、wms_timeitem 和 wms_timedefault。
wms_timeextent 参数表示服务将给出响应的时间段。它定义了用于过滤 TIME 参数(在本例中为 acq_date 字段)的 PostGIS table 字段。wms_timedefault 参数指定了当请求 WMS 服务未提供 TIME 参数时使用的默认时间值。
到目前为止,WMS 已启用时间;这意味着 WMS GetCapabilities 请求现在包括 PostGIS 热点层的时间维度定义,更重要的是,GetMap WMS 请求允许用户添加 TIME 参数以查询特定日期的图层。
使用 OpenLayers 消费 WMS 服务
在本菜谱中,你将使用 MapServer 和 Geoserver WMS,这些是在本章前两个菜谱中创建的,并使用 OpenLayers 开源 JavaScript API。
这个优秀的库帮助开发者快速使用地图查看器和功能构建网页。在本菜谱中,你将创建一个 HTML 页面,在其中添加一个 OpenLayers 地图以及该地图的一组控件用于导航、切换图层和识别图层特征。我们还将查看两个指向 PostGIS 表的 WMS 图层,这些图层由 MapServer 和 GeoServer 实现。
准备工作
MapServer 使用 PROJ.4 (trac.osgeo.org/proj/) 进行投影管理。这个库默认不包含定义了 Spherical Mercator 投影 (EPSG:900913)。这种投影通常由商业地图 API 提供商使用,如 GoogleMaps、Yahoo! Maps 和 Microsoft Bing,并且可以为您的地图提供优秀的基础图层。
对于这个菜谱,我们需要考虑以下内容:
-
由于 JavaScript 的安全限制,无法使用
XMLHttpRequest从远程域检索信息。在菜谱中,当你向通常在端口 8080 上运行的 Tomcat 上的 GeoServer 发送 WMSGetFeatureInfo请求时,以及从运行在 Apache 或 ISS 端口 80 上的 HTML 页面发送请求时,你将遇到这个问题。因此,除非你使用 HTTPD URL 转写运行你的 GeoServer 实例,否则解决方案是创建一个代理脚本。 -
将书中数据集包含的代理脚本复制到您计算机的 Web
cgi目录中(在 Linux 中为/usr/lib/cgi-bin/,在 macOS 中为/Library/WebServer/CGI-Executables,在 Windows 中为C:\ms4w\Apache\cgi-bin),打开代理.cgi文件,并将localhost:8080添加到allowedHosts列表中。
如何操作...
执行以下步骤:
- 创建
openlayers.html文件并添加<head>和<body>标签。在<head>标签中,通过执行以下代码导入 OpenLayers JavaScript 库:
<!doctype html>
<html>
<head>
<title>OpenLayers Example</title>
<script src="img/OpenLayers.js">
</script>
</head>
<body>
</body>
</html>
- 首先,在
<body>标签中添加一个<div>元素,该元素将包含 OpenLayers 地图。地图应设置为 900 像素宽和 500 像素高,使用以下代码:
<div style="width:900px; height:500px" id="map"></div>
- 在地图放置在
<div>之后,添加一个 JavaScript 脚本并创建一个 OpenLayersmap对象。在地图构造函数参数中,您将添加一个空的controls数组并声明地图具有球面墨卡托投影,如下所示:
<script defer="defer" type="text/javascript">
// instantiate the map object
var map = new OpenLayers.Map("map", {
controls: [],
projection: new OpenLayers.Projection("EPSG:3857")
});
</script>
- 在
map变量声明后立即,向地图添加一些 OpenLayers 控件。对于您正在创建的 Web GIS 查看器,您将添加Navigation控件(它通过鼠标事件处理地图浏览,例如拖动、双击和滚动鼠标滚轮)、PanZoomBar控件(使用位于缩放垂直滑块上方的箭头进行四个方向的导航)、LayerSwitcher控件(它处理添加到地图的图层的开关)和MousePosition控件(它显示地图坐标,当用户移动鼠标时坐标会变化),使用以下代码:
// add some controls on the map
map.addControl(new OpenLayers.Control.Navigation());
map.addControl(new OpenLayers.Control.PanZoomBar()),
map.addControl(new OpenLayers.Control.LayerSwitcher(
{"div":OpenLayers.Util.getElement("layerswitcher")}));
map.addControl(new OpenLayers.Control.MousePosition());
- 现在创建一个 OSM 基础图层,使用以下代码:
// set the OSM layer
var osm_layer = new OpenLayers.Layer.OSM();
- 为 WMS GeoServer 和 MapServer URL 设置两个变量,您将使用这些 URL(它们是您在本章前两个菜谱中创建的服务 URL):
-
- 对于 Linux,添加以下代码:
// set the WMS
var geoserver_url = "http://localhost:8080/geoserver/wms";
var mapserver_url = http://localhost/cgi-
bin/mapserv?map=/var/www/data/countries.map&
-
- 对于 Windows,添加以下代码:
// set the WMS
var geoserver_url = "http://localhost:8080/geoserver/wms";
var mapserver_url = http://localhost/cgi-
bin/mapserv.exe?map=C:\\ms4w\\Apache\\
htdocs\\countries.map&
-
- 对于 macOS,添加以下代码:
// set the WMS
var geoserver_url = "http://localhost:8080/geoserver/wms";
var mapserver_url = http://localhost/cgi-
bin/mapserv? map=/Library/WebServer/
Documents/countries.map&
- 现在,创建一个 WMS GeoServer 图层以显示 OpenLayers 地图中的 PostGIS 图层县。您将为该图层设置不透明度,以便可以看到其后面的其他图层(县)。
isBaseLayer属性设置为false,因为您希望这个图层位于 Google Maps 基础图层之上,而不是作为它们的替代品(默认情况下,OpenLayers 中的所有 WMS 图层都被视为基础图层)。使用以下代码创建 WMS GeoServer 图层:
// set the GeoServer WMS
var geoserver_wms = new OpenLayers.Layer.WMS( "GeoServer WMS",
geoserver_url,
{
layers: "postgis_cookbook:counties",
transparent: "true",
format: "image/png",
},
{
isBaseLayer: false,
opacity: 0.4
} );
- 现在,创建一个 WMS MapServer 图层以在 OpenLayers 地图中显示来自 PostGIS 图层的国家,使用以下代码:
// set the MapServer WMS
var mapserver_wms = new OpenLayers.Layer.WMS( "MapServer WMS",
mapserver_url,
{
layers: "countries",
transparent: "true",
format: "image/png",
},
{
isBaseLayer: false
} );
- 在创建 OSM 和 WMS 图层后,您需要使用以下代码将所有这些图层添加到地图中:
// add all of the layers to the map
map.addLayers([mapserver_wms, geoserver_wms, osm_layer]);
map.zoomToMaxExtent();
Proxy...
// add the WMSGetFeatureInfo control
OpenLayers.ProxyHost = "/cgi-bin/proxy.cgi?url=";
- 您希望为用户提供识别县 WMS 中特征的可能性。添加
WMSGetFeatureInfoOpenLayers 控件(它将在幕后发送GetFeatureInfo请求到 GeoServer WMS),指向由 GeoServer WMS 提供的县 PostGIS 图层,使用以下代码:
var info = new OpenLayers.Control.WMSGetFeatureInfo({
url: geoserver_url,
title: 'Identify',
queryVisible: true,
eventListeners: {
getfeatureinfo: function(event) {
map.addPopup(new OpenLayers.Popup.FramedCloud(
"WMSIdentify",
map.getLonLatFromPixel(event.xy),
null,
event.text,
null,
true
));
}
}
});
map.addControl(info);
info.activate();
- 最后,设置地图的中心和其初始缩放级别,使用以下代码:
// center map
var cpoint = new OpenLayers.LonLat(-11000000, 4800000);
map.setCenter(cpoint, 3);
您的 HTML 文件现在应该看起来像 data/chp09 中包含的 openlayers.html 文件。您现在可以将此文件部署到您的网络服务器(Apache HTTPD 或 IIS)。如果您在 Linux 上使用 Apache HTTPD,可以将文件复制到 /var/www 下的 data 目录,如果您使用 Windows,可以将它复制到 C:\ms4w\Apache\htdocs 下的数据目录(如果尚未存在,请创建 data 目录)。然后,使用 URL http://localhost/data/openlayers.html 访问它。
现在,使用您喜欢的浏览器访问 openlayers 网页。开始浏览地图:缩放、平移,尝试使用图层切换控件打开和关闭基础图层和叠加图层,并尝试点击一个点以从县 PostGIS 图层中识别一个要素。以下截图显示了地图:

它是如何工作的...
您已经看到了如何使用 OpenLayers JavaScript 库创建一个网络地图查看器。这个库允许开发者使用 JavaScript 在 HTML 页面中定义各种地图组件。核心对象是一个由 控件 和 图层 组成的地图。
OpenLayers 提供了大量的控件(dev.openlayers.org/docs/files/OpenLayers/Control-js.html),甚至可以创建自定义的控件。
OpenLayers 的另一个出色功能是能够在地图中添加大量地理数据源作为图层(您只添加了其中几种类型到地图中,例如 OpenStreetMap 和 WMS),并且您可以添加来源,如 WFS、GML、KML、GeoRSS、OSM 数据、ArcGIS Rest、TMS、WMTS 和 WorldWind,仅举几例。
使用 Leaflet 消费 WMS 服务
在前面的菜谱中,您已经看到了如何使用 OpenLayers JavaScript API 创建一个网络 GIS,然后添加了从 MapServer 和 GeoServer 服务的 WMS PostGIS 图层。
为了替代广泛使用的 OpenLayers JavaScript API,创建了一个更轻量级的替代品,名为 Leaflet。在本菜谱中,您将看到如何使用此 JavaScript API 创建一个网络 GIS,将来自 PostGIS 的 WMS 图层添加到该地图中,并实现一个 识别工具,向 MapServer WMS 发送 GetFeatureInfo 请求。然而,与 OpenLayers 不同,Leaflet 不自带 WMSGetFeatureInfo 控件,因此在本菜谱中我们将看到如何创建此功能。
如何做到这一点...
执行以下步骤:
- 创建一个新的 HTML 文件,并将其命名为
leaflet.html(可在本书源代码包中找到)。打开它并添加<head>和<body>标签。在<head>部分,导入 Leaflet CSS 和 JavaScript 库以及 jQuery JavaScript 库(您将使用 jQuery 向 MapServer WMS 发送 AJAX 请求到GetFeatureInfo):
<html>
<head>
<title>Leaflet Example</title>
<link rel="stylesheet"
href= "https://unpkg.com/leaflet@1.2.0/dist/leaflet.css" />
<script src= "https://unpkg.com/leaflet@1.2.0/dist/leaflet.js">
</script>
<script src="img/jquery.min.js">
</script>
</head>
<body>
</body>
</html>
- 在
<body>元素中开始添加<div>标签以将 Leaflet 地图包含到您的文件中,如下面的代码所示;地图的宽度为 800 像素,高度为 500 像素:
<div id="map" style="width:800px; height:500px"></div>
- 在包含地图的
<div>元素之后,添加以下 JavaScript 代码。使用基于OpenStreetMap数据的tile.osm.org服务创建一个 LeaflettileLayer对象:
<script defer="defer" type="text/javascript">
// osm layer
var osm = L.tileLayer('http://{s}.tile.osm.org
/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: "Data by OpenStreetMap"
});
</script>
- 创建第二个图层,该图层将使用你在这个章节的几个菜谱中创建的 MapServer WMS。如果你使用 Linux、Windows 或 macOS,需要设置不同的
ms_url变量:
-
- 对于 Linux,使用以下代码:
// mapserver layer
var ms_url = "http://localhost/cgi-bin/mapserv?
map=/var/www/data/countries.map&";
var countries = L.tileLayer.wms(ms_url, {
layers: 'countries',
format: 'image/png',
transparent: true,
opacity: 0.7
});
-
- 对于 Windows,使用以下代码:
// mapserver layer
var ms_url = "http://localhost
/cgi-bin/mapserv.exe?map=C:%5Cms4w%5CApache%5
Chtdocs%5Ccountries.map&";
var countries = L.tileLayer.wms(ms_url, {
layers: 'countries',
format: 'image/png',
transparent: true,
opacity: 0.7
});
-
- 对于 macOS,使用以下代码:
// mapserver layer
var ms_url = "http://localhost/cgi-bin/mapserv?
map=/Library/WebServer/Documents/countries.map&";
var countries = L.tileLayer.wms(ms_url, {
layers: 'countries',
format: 'image/png',
transparent: true,
opacity: 0.7
});
- 创建 Leaflet
map并向其中添加层,如下面的代码所示:
// map creation
var map = new L.Map('map', {
center: new L.LatLng(15, 0),
zoom: 2,
layers: [osm, countries],
zoomControl: true
});
- 现在,通过执行以下代码将鼠标点击事件与一个函数关联起来,该函数将在
countries层上执行GetFeatureInfoWMS 请求:
// getfeatureinfo event
map.addEventListener('click', Identify);
function Identify(e) {
// set parameters needed for GetFeatureInfo WMS request
var BBOX = map.getBounds().toBBoxString();
var WIDTH = map.getSize().x;
var HEIGHT = map.getSize().y;
var X = map.layerPointToContainerPoint(e.layerPoint).x;
var Y = map.layerPointToContainerPoint(e.layerPoint).y;
// compose the URL for the request
var URL = ms_url + 'SERVICE=WMS&VERSION=1.1.1&
REQUEST=GetFeatureInfo&LAYERS=countries&
QUERY_LAYERS=countries&BBOX='+BBOX+'&FEATURE_COUNT=1&
HEIGHT='+HEIGHT+'&WIDTH='+WIDTH+'&
INFO_FORMAT=text%2Fhtml&SRS=EPSG%3A4326&X='+X+'&Y='+Y;
//send the asynchronous HTTP request using
jQuery $.ajax
$.ajax({
url: URL,
dataType: "html",
type: "GET",
success: function(data) {
var popup = new L.Popup({
maxWidth: 300
});
popup.setContent(data);
popup.setLatLng(e.latlng);
map.openPopup(popup);
}
});
}
-
你的 HTML 文件现在应该看起来像
data/chp09中包含的leaflet.html文件。你现在可以将这个文件部署到你的 web 服务器上(即 Apache HTTPD 或 IIS)。如果你在 Linux 上使用 Apache HTTPD,可以将文件复制到/var/www/data目录;如果你运行 macOS,可以复制到/Library/WebServer/Documents/data;如果你使用 Windows,可以复制到C:\ms4w\Apache\htdocs\data(如果该目录不存在,则需要创建它)。然后,通过 URLhttp://localhost/data/leaflet.html访问它。 -
使用你喜欢的浏览器打开网页,并开始导航地图;缩放、平移,并尝试点击一个点以从
countriesPostGIS 层中识别一个要素,如下面的截图所示:

它是如何工作的...
在这个菜谱中,你看到了如何使用 Leaflet JavaScript API 库在 HTML 页面中添加地图。首先,你从一个外部服务器创建了一个图层作为基础地图。然后,你使用之前菜谱中实现的 MapServer WMS 创建了另一个图层,以将 PostGIS 层暴露给网络。然后,你创建了一个新的地图对象并将其添加到这两个图层中。最后,使用 jQuery,你实现了对 GetFeatureInfo WMS 请求的 AJAX 调用,并在 Leaflet Popup 对象中显示结果。
Leaflet 是 OpenLayers 库的一个非常不错且紧凑的替代品,当你的 webGIS 服务需要从移动设备(如平板电脑和智能手机)使用时,它能够给出非常好的结果。此外,它拥有大量的插件,并且可以轻松地与 JavaScript 库(如 Raphael 和 JS3D)集成。
使用 OpenLayers 消费 WFS-T 服务
在这个菜谱中,你将使用 GeoServer 开源网络地图引擎从 PostGIS 层创建 事务性 Web 要素服务(WFS-T),然后创建一个能够使用此服务的 OpenLayers 基本应用程序。
这样,应用程序的用户将能够管理远程 PostGIS 层上的事务。WFS-T 允许创建、删除和更新要素。在这个菜谱中,你将允许用户仅添加要素,但这个菜谱应该能让你开始创建更复杂的用例。
如果您是 GeoServer 和 OpenLayers 的新手,您应该首先阅读 使用 GeoServer 创建 WMS 和 WFS 服务 和 使用 OpenLayers 消费 WMS 服务 菜谱,然后返回此菜谱。
准备工作
-
创建代理脚本并将其部署到您的 Web 服务器(即 HTTPD 或 IIS),如 使用 OpenLayers 消费 WMS 服务 菜谱中的 准备工作 部分所示。
-
创建以下名为
sites的 PostGIS 点图层:
CREATE TABLE chp09.sites
(
gid serial NOT NULL,
the_geom geometry(Point,4326),
CONSTRAINT sites_pkey PRIMARY KEY (gid )
);
CREATE INDEX sites_the_geom_gist ON chp09.sites
USING gist (the_geom );
- 现在,在 GeoServer 中为
chp09.sites表创建一个 PostGIS 图层。有关更多信息,请参阅本章中的 使用 GeoServer 创建 WMS 和 WFS 服务 菜谱。
如何操作...
执行以下步骤:
- 创建一个名为
wfst.html的新文件。打开它并添加<head>和<body>标签。在<head>标签中,导入以下OpenLayers库:
<html>
<head>
<title>Consuming a WFS-T with OpenLayers</title>
<script
src="img/OpenLayers.js">
</script>
</head>
<body>
</body>
</html>
- 在
<body>标签中添加一个<div>标签以包含 OpenLayers 地图,如下面的代码所示;地图的宽度为 700 像素,高度为 400 像素:
<div style="width:700px; height:400px" id="map"></div>
- 在创建用于包含地图的
<div>标签之后,添加一个 JavaScript 脚本。在脚本内部,将ProxyHost设置为部署代理脚本的网络位置。然后创建一个新的 OpenLayers 地图,如下面的代码所示:
<script type="text/javascript">
// set the proxy
OpenLayers.ProxyHost = "/cgi-bin/proxy.cgi?url=";
// create the map
var map = new OpenLayers.Map('map');
</script>
- 现在,在脚本中,在创建地图之后,创建一个
OpenStreetMap图层,您将在地图中使用它作为基础图层,如下面的代码所示:
// create an OSM base layer
var osm = new OpenLayers.Layer.OSM();
- 现在,使用
StyleMap对象创建 WFS-T 图层的OpenLayers对象,以使用红色点渲染 PostGIS 图层功能,如下面的截图所示:
// create the wfs layer
var saveStrategy = new OpenLayers.Strategy.Save();
var wfs = new OpenLayers.Layer.Vector("Sites",
{
strategies: [new OpenLayers.Strategy.BBOX(), saveStrategy],
projection: new OpenLayers.Projection("EPSG:4326"),
styleMap: new OpenLayers.StyleMap({
pointRadius: 7,
fillColor: "#FF0000"
}),
protocol: new OpenLayers.Protocol.WFS({
version: "1.1.0",
srsName: "EPSG:4326",
url: "http://localhost:8080/geoserver/wfs",
featurePrefix: 'postgis_cookbook',
featureType: "sites",
featureNS: "https://www.packtpub.com/application-development/
postgis-cookbook-second-edition",
geometryName: "the_geom"
})
});
- 将 WFS 图层添加到地图中,使地图居中,并设置初始缩放。您可以使用
geometry转换方法将点从存储图层的EPSG:4326转换为ESPG:900913,这是查看器使用的坐标系统,如下面的代码所示:
// add layers to map and center it
map.addLayers([osm, wfs]);
var fromProjection = new OpenLayers.Projection("EPSG:4326");
var toProjection = new OpenLayers.Projection("EPSG:900913");
var cpoint = new OpenLayers.LonLat(12.5, 41.85).transform(
fromProjection, toProjection);
map.setCenter(cpoint, 10);
- 现在,您将创建一个包含 Draw Point 工具(用于添加新功能)和 Save Features 工具(用于将功能保存到底层的 WFS-T)的面板。我们首先创建面板,如下面的代码所示:
// create a panel for tools
var panel = new OpenLayers.Control.Panel({
displayClass: "olControlEditingToolbar"
});
- 现在,我们将创建名为 Draw Point 的工具,如下面的代码所示:
// create a draw point tool
var draw = new OpenLayers.Control.DrawFeature(
wfs, OpenLayers.Handler.Point,
{
handlerOptions: {freehand: false, multi: false},
displayClass: "olControlDrawFeaturePoint"
}
);
- 然后,我们将创建名为 Save Features 的工具,使用以下代码:
// create a save tool
var save = new OpenLayers.Control.Button({
title: "Save Features",
trigger: function() {
saveStrategy.save();
},
displayClass: "olControlSaveFeatures"
});
- 最后,将工具添加到面板中,包括导航控件,并将面板作为地图的控件,使用以下代码:
// add tools to panel and add it to map
panel.addControls([
new OpenLayers.Control.Navigation(),
save, draw
]);
map.addControl(panel);
-
您的 HTML 文件现在应该看起来像
chp09目录中包含的wfst.html文件。将其部署到您的 Web 服务器(即 Apache HTTPD 或 IIS)。如果您在 Linux 上使用 Apache HTTPD,可以将文件复制到/var/www下的data目录,而如果您使用 Windows,则可以将其复制到C:\ms4w\Apache\htdocs下的数据目录(如果尚不存在,则创建该目录)。然后,使用http://localhost/data/wfst.html访问它。 -
使用你喜欢的浏览器打开网页,并开始向地图添加一些点。现在,点击保存按钮并重新加载页面;之前添加的点应该仍然在那里,因为它们已经被 WFS-T 存储在底层的
PostGIS表中,如下面的截图所示:

使用浏览器上的 OpenLayers 控件添加的点
它是如何工作的...
在这个食谱中,你首先创建了一个点 PostGIS 表,然后使用 GeoServer 将其发布为 WFS-T。然后你创建了一个基本的 OpenLayers 应用程序,使用 WFS-T 图层,允许用户向底层的 PostGIS 图层添加要素。
在 OpenLayers 中,实现此类服务所需的核心对象是通过定义 WFS 协议的矢量图层。当定义 WFS 协议时,你必须提供使用数据集空间参考系统的 WFS 版本,服务的 URI,图层的名称(对于 GeoServer,名称是图层工作区、FeaturePrefix 和图层名称 FeatureType 的组合),以及将要修改的 geometry 字段的名称。你还可以向矢量图层构造函数传递一个 StyleMap 值来定义图层的渲染行为。
你然后通过向 OpenLayers 地图添加一些点来测试应用程序,并检查这些点是否确实存储在 PostGIS 中。当使用 WFS-T 图层添加点时,借助 Firefox Firebug 或 Chrome(Chromium)开发者工具,你可以详细调查你对 WFS-T 发出的请求及其响应。
例如,当添加一个点时,你会看到发送了一个 Insert 请求到 WFS-T。以下 XML 被发送到服务(注意点几何形状是如何插入到 <wfs:Insert> 元素的主体中的):
<wfs:Transaction
service="WFS" version="1.1.0"
xsi:schemaLocation="http://www.opengis.net/wfs
http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"
>
<wfs:Insert>
<feature:sites >
<feature:the_geom>
<gml:Point
srsName="EPSG:4326"> <gml:pos>12.450561523436999 41.94302128455888</gml:pos> </gml:Point>
</feature:the_geom>
</feature:sites>
</wfs:Insert>
</wfs:Transaction>
如以下代码所示,如果过程顺利进行并且要素已存储,WFS-T 将发送 <wfs:TransactionResponse> 响应(注意在这种情况下,<wfs:totalInserted> 元素的值设置为 1,因为只存储了一个要素):
<?xml version="1.0" encoding="UTF-8"?>
<wfs:TransactionResponse version="1.1.0" ...[CLIP]... >
<wfs:TransactionSummary> <wfs:totalInserted>1</wfs:totalInserted> <wfs:totalUpdated>0</wfs:totalUpdated> <wfs:totalDeleted>0</wfs:totalDeleted> </wfs:TransactionSummary>
<wfs:TransactionResults/>
<wfs:InsertResults>
<wfs:Feature>
<ogc:FeatureId fid="sites.17"/>
</wfs:Feature>
</wfs:InsertResults>
</wfs:TransactionResponse>
使用 GeoDjango 开发网络应用程序 - 第一部分
在这个食谱和下一个食谱中,你将使用 Django 网络框架创建一个使用 PostGIS 数据存储来管理野生动物目击事件的网络应用程序。在这个食谱中,你将构建网络应用程序的后端,基于 Django 管理站点。
在访问后台办公室后,经过身份验证,管理员用户将能够管理(插入、更新和删除)数据库的主要实体(动物和目击事件)。在食谱的下一部分,你将构建一个前端,它基于 Leaflet JavaScript 库在地图上显示目击事件。
您可以在chp09/wildlife目录下的代码包中找到一个您将要构建的整个项目的副本。如果某个概念不清楚或您在执行食谱步骤时想复制粘贴代码,而不是从头开始编写代码,请参考它。
准备工作
-
如果您是 Django 的新手,请查看官方 Django 教程
docs.djangoproject.com/en/dev/intro/tutorial01/,然后返回到本食谱。 -
创建一个 Python virtualenv (
www.virtualenv.org/en/latest/),以创建一个用于您将在本食谱和下一个食谱中构建的 Web 应用的隔离 Python 环境。然后,按照以下方式激活环境:
-
- 在 Linux 中使用以下命令:
$ cd ~/virtualenvs/
$ virtualenv --no-site-packages chp09-env
$ source chp09-env/bin/activate
-
- 在 Windows 中输入以下命令(有关在 Windows 上安装
virtualenv的步骤,请参阅zignar.net/2012/06/17/install-python-on-windows/):
- 在 Windows 中输入以下命令(有关在 Windows 上安装
cd c:\virtualenvs
C:\Python27\Scripts\virtualenv.exe
-no-site-packages chp09-env
chp09-env\Scripts\activate
- 一旦激活,您可以使用
pip工具(www.pip-installer.org/en/latest/)安装您将为这个食谱以及下一个食谱使用的 Python 包。
-
- 在 Linux 中,命令如下:
(chp09-env)$ pip install django==1.10
(chp09-env)$ pip install psycopg2==2.7
(chp09-env)$ pip install Pillow
-
- 在 Windows 中,命令如下:
(chp09-env) C:\virtualenvs> pip install django==1.10
(chp09-env) C:\virtualenvs> pip install psycopg2=2.7
(chp09-env) C:\virtualenvs> easy_install Pillow
- 如果您之前还没有这样做,请从
thematicmapping.org/downloads/TM_WORLD_BORDERS-0.3.zip下载世界国家形状文件。本书的代码包中也包含了这个形状文件的副本。将形状文件提取到working/chp09目录下。
如何操作...
执行以下步骤:
- 使用
django-admin命令的startproject选项创建一个 Django 项目。将项目命名为wildlife。创建项目的命令如下:
(chp09-env)$ cd ~/postgis_cookbook/working/chp09
(chp09-env)$ django-admin.py startproject wildlife
- 使用
django-admin命令的startapp选项创建一个 Django 应用。将应用命名为sightings。命令如下:
(chp09-env)$ cd wildlife/
(chp09-env)$ django-admin.py startapp sightings
现在您应该有以下目录结构:

- 您需要编辑一些文件。打开您喜欢的编辑器(Sublime Text可以完成这项工作)并转到代码包中
chp09/wildlife/wildlife目录下的settings.py文件中的设置。首先,DATABASES设置应如下所示,以便为您的应用程序数据使用postgis_cookbookPostGIS 数据库:
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'postgis_cookbook',
'USER': 'me',
'PASSWORD': 'mypassword',
'HOST': 'localhost',
'PORT': '',
}
}
- 在
wildlife/settings.py文件的顶部添加以下两行代码(PROJECT_PATH是您将在设置菜单中输入项目路径的变量):
import os
PROJECT_PATH = os.path.abspath(os.path.dirname(__file__))
- 确保在
chp09/wildlife/wildlife目录下的settings.py文件中,MEDIA_ROOT和MEDIA_URL设置正确,如下所示(这是为了设置媒体文件的路径和 URL,以便上传图像的管理员用户使用):
MEDIA_ROOT = os.path.join(PROJECT_PATH, "media")
MEDIA_URL = '/media/'
- 确保在
settings.py文件中的INSTALLED_APPS设置看起来如下所示。你将使用 Django 管理站点(django.contrib.admin)、GeoDjango 核心库(django.contrib.gis)以及你在此配方和下一个配方中创建的 sightings 应用程序。为此,添加最后三行:
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.gis',
'sightings',
)
- 现在,使用 Django 的
migrations管理命令同步数据库。当提示创建一个 超级用户 时,回答yes并选择一个首选的管理员用户名和密码:
(chp09-env)$ python manage.py makemigrations
(chp09-env)$ python manage.py migrate
- 现在,你将添加应用程序需要的模型。编辑位于
chp09/wildlife/sightings下的models.py文件,并添加以下代码:
from django.db import models
from django.contrib.gis.db import models as gismodels
class Country(gismodels.Model):
"""
Model to represent countries.
"""
isocode = gismodels.CharField(max_length=2)
name = gismodels.CharField(max_length=255)
geometry = gismodels.MultiPolygonField(srid=4326)
objects = gismodels.GeoManager()
def __unicode__(self):
return '%s' % (self.name)
class Animal(models.Model):
"""
Model to represent animals.
"""
name = models.CharField(max_length=255)
image = models.ImageField(upload_to='animals.images')
def __unicode__(self):
return '%s' % (self.name)
def image_url(self):
return u'<img src="img/%s" alt="%s" width="80"></img>' %
(self.image.url, self.name)
image_url.allow_tags = True
class Meta:
ordering = ['name']
class Sighting(gismodels.Model):
"""
Model to represent sightings.
"""
RATE_CHOICES = (
(1, '*'),
(2, '**'),
(3, '***'),
)
date = gismodels.DateTimeField()
description = gismodels.TextField()
rate = gismodels.IntegerField(choices=RATE_CHOICES)
animal = gismodels.ForeignKey(Animal)
geometry = gismodels.PointField(srid=4326)
objects = gismodels.GeoManager()
def __unicode__(self):
return '%s' % (self.date)
class Meta:
ordering = ['date']
-
每个模型都将成为数据库中的一个表,使用
models和gismodels类定义相应的字段。请注意,county和sighting层中的geometry变量将变成MultiPolygon和PointPostGIS 几何列,这要归功于 GeoDjango 库。 -
在
chp09/wildlife/sightings下创建一个admin.py文件,并将其中的以下代码添加到该文件中。该文件中的类将定义和自定义 Django 管理站点在浏览应用程序模型或表时的行为(要显示的字段、用于过滤记录的字段以及用于排序记录的字段)。通过执行以下代码创建该文件:
from django.contrib import admin
from django.contrib.gis.admin import GeoModelAdmin
from models import Country, Animal, Sighting
class SightingAdmin(GeoModelAdmin):
"""
Web admin behavior for the Sighting model.
"""
model = Sighting
list_display = ['date', 'animal', 'rate']
list_filter = ['date', 'animal', 'rate']
date_hierarchy = 'date'
class AnimalAdmin(admin.ModelAdmin):
"""
Web admin behavior for the Animal model.
"""
model = Animal
list_display = ['name', 'image_url',]
class CountryAdmin(GeoModelAdmin):
"""
Web admin behavior for the Country model.
"""
model = Country
list_display = ['isocode', 'name']
ordering = ('name',)
class Meta:
verbose_name_plural = 'countries'
admin.site.register(Animal, AnimalAdmin)
admin.site.register(Sighting, SightingAdmin)
admin.site.register(Country, CountryAdmin)
- 现在,为了同步数据库,请在 Django 项目文件夹中执行以下命令:
(chp09-env)$ python manage.py makemigrations
(chp09-env)$ python manage.py migrate
输出结果应如下所示:

-
现在,对于
models.py中的每个模型,应该已经创建了一个 PostgreSQL 表。请使用你喜欢的客户端(即psql或pgAdmin)检查你的 PostgreSQL 数据库是否确实包含了前面命令中创建的三个表,以及sightings_sighting和sightings_country表是否包含 PostGIS 几何字段。 -
任何网络应用都需要定义可以访问页面的 URL。因此,请编辑位于
chp09/wildlife/wildlife下的urls.py文件,并添加以下代码:
from django.conf.urls import url
from django.contrib import admin
import settings
from django.conf.urls.static import static
admin.autodiscover()
urlpatterns = [
url(r'^admin/', admin.site.urls),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
在urls.py文件中,你基本上定义了后台位置(使用 Django 管理应用程序构建)以及由 Django 管理员上传的媒体(图像)文件的位置,当在数据库中添加新的动物实体时。现在运行 Django 开发服务器,使用以下runserver管理命令:
(chp09-env)$ python manage.py runserver
-
通过
http://localhost:8000/admin/访问 Django 管理站点,并使用在之前步骤中提供的超级用户凭据登录(步骤 7)。 -
现在,导航到
http://localhost:8000/admin/sightings/animal/,并使用添加动物按钮添加一些动物。对于每个动物,定义一个名称和一个将被前端使用的图像,你将在下一个配方中构建该前端。多亏了 Django 管理,你几乎不需要写代码就创建了此页面。以下截图显示了添加了一些实体后动物列表页面将看起来是什么样子:

- 导航到
http://localhost:8000/admin/sightings/sighting/并使用“添加观测”按钮添加一些观测。对于每个观测,定义日期、时间、观测到的动物名称、评分和位置。GeoDjango 已为您在 Django 管理站点中添加了基于 OpenLayers JavaScript 库的地图小部件,以便添加或修改几何特征。以下截图显示了观测页面:

- 由于 Django 管理的高效性,观测列表页面将为管理用户提供有用的功能来排序、过滤和导航系统中所有观测的日期层次结构,如下面的截图所示:

- 现在,您将导入
countries形状文件到其模型中。在下一个食谱中,您将使用此模型来查找每个观测发生的国家。在继续本食谱之前,调查形状文件结构;您需要使用以下命令将NAME和ISO2属性导入模型作为name和isocode属性:
$ ogrinfo TM_WORLD_BORDERS-0.3.shp TM_WORLD_BORDERS-0.3 -al -so

- 在
chp09/wildlife/sightings目录下添加一个load_countries.py文件,并使用LayerMappingGeoDjango 实用工具将形状文件导入 PostGIS,使用以下代码:
"""
Script to load the data for the country model from a shapefile.
"""
from django.contrib.gis.utils import LayerMapping
from models import Country
country_mapping = {
'isocode' : 'ISO2',
'name' : 'NAME',
'geometry' : 'MULTIPOLYGON',
}
country_shp = 'TM_WORLD_BORDERS-0.3.shp'
country_lm = LayerMapping(Country, country_shp, country_mapping,
transform=False, encoding='iso-8859-1')
country_lm.save(verbose=True, progress=True)
- 为了使此代码正常工作,您应该在
chp09/wildlife目录下放置TM_WORLD_BORDERS-0.3.shp文件。进入 Python Django shell 并运行utils.py脚本。然后,使用以下命令检查国家是否已正确插入到您的 PostgreSQL 数据库中的sightings_country表中:
(chp09-env)$ python manage.py shell >>> from sightings import load_countries
Saved: Antigua and Barbuda
Saved: Algeria Saved: Azerbaijan
...
Saved: Taiwan
现在,当使用以下命令运行 Django 服务器时,您应该在http://localhost:8000/admin/sightings/country/的管理界面中看到国家:
(chp09-env)$ python manage.py runserver

它是如何工作的...
在本食谱中,您已经看到了如何快速高效地使用Django(最受欢迎的 Python 网络框架之一)组装后台应用程序;这要归功于其对象关系映射器,它可以自动创建应用程序所需的数据库表,并提供自动 API 来管理(插入、更新和删除)以及查询实体,而无需使用 SQL。
感谢GeoDjango库,两个应用程序模型(县和观测)在引入数据库表中的geometric PostGIS 字段时被地理启用。
您已自定义了强大的自动管理界面,可以快速组装应用程序的后台页面。使用Django URL 分发器,您以简洁的方式定义了应用程序的 URL 路由。
如你所注意到的,Django 抽象的一个极好的特点是自动实现数据访问层 API,使用模型。你现在可以使用 Python 代码添加、更新、删除和查询记录,而不需要任何 SQL 知识。尝试使用 Django Python shell 做这件事;你将从数据库中选择一个动物,为该动物添加一个新的观测,然后最终删除该观测。你可以使用以下命令,在任何时候调查 Django 后台生成的 SQL,使用 django.db.connection 类:
(chp09-env-bis)$ python manage.py shell
>>> from django.db import connection
>>> from datetime import datetime
>>> from sightings.models import Sighting, Animal
>>> an_animal = Animal.objects.all()[0]
>>> an_animal
<Animal: Lion>
>>> print connection.queries[-1]['sql']
SELECT "sightings_animal"."id", "sightings_animal"."name", "sightings_animal"."image" FROM "sightings_animal" ORDER BY "sightings_animal"."name" ASC LIMIT 1'
my_sight = Sighting(date=datetime.now(), description='What a lion I have seen!', rate=1, animal=an_animal, geometry='POINT(10 10)')
>>> my_sight.save()
print connection.queries[-1]['sql']
INSERT INTO "sightings_sighting" ("date", "description", "rate", "animal_id", "geometry") VALUES ('2013-06-12 14:37:36.544268-05:00', 'What a lion I have seen!', 1, 2, ST_GeomFromEWKB('\x0101000020e610000000000000000024400000000000002440'::bytea)) RETURNING "sightings_sighting"."id"
>>> my_sight.delete()
>>> print connection.queries[-1]['sql']
DELETE FROM "sightings_sighting" WHERE "id" IN (5)
你是否和我们一样喜欢 Django?在下一个菜谱中,你将创建应用程序的前端。用户将能够使用 Leaflet JavaScript 库实现的地图浏览观测。所以继续阅读!
Developing web applications with GeoDjango – part 2
在这个菜谱中,你将创建之前菜谱中使用的 Django 创建的 Web 应用程序的前端。
使用 HTML 和 Django 模板语言,你将创建一个显示地图的网页,该地图使用 Leaflet 实现,并为用户提供一个包含系统中所有可用观测的列表。用户将能够导航地图并识别观测以获取更多信息。
准备工作
-
确保你已经完成了上一个菜谱中的每一个步骤,并且 Web 应用程序的后端正在运行,其数据库已用一些实体填充。
-
激活你在 Developing web applications with GeoDjango –Part 1) 菜谱中创建的
virtualenv,如下所示:
-
- 在 Linux 上使用以下命令:
$ cd ~/virtualenvs/ $ source chp09-env/bin/activate
-
- 在 Windows 上使用以下命令:
cd c:\virtualenvs > chp09-env\Scripts\activate
- 安装你将在本菜谱中使用的库;你需要
simplejson和vectorformatsPython 库来生成 GeoJSON (www.geojson.org/) 响应,该响应将用于填充 Leaflet 中的观测层:
-
- 在 Linux 上使用以下命令:
(chp09-env)$ pip install simplejson
(chp09-env)$ pip install vectorformats
-
- 在 Windows 上使用以下命令:
(chp09-env) C:\virtualenvs> pip install simplejson
(chp09-env) C:\virtualenvs> pip install vectorformats
如何做...
你现在将创建你的 Web 应用程序的首页,如下所示:
- 前往包含 Django 野生动物 Web 应用程序的目录,并将以下行添加到
chp09/wildlife/wildlife文件夹下的urls.py文件中:
from django.conf.urls import patterns, include, url
from django.conf import settings
from sightings.views import get_geojson, home
from django.contrib import admin
admin.autodiscover()
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^geojson/', get_geojson),
url(r'^$', home),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# media files
- 打开
chp09/wildlife/sightings文件夹下的views.py文件,并添加以下代码。home视图将返回你的应用程序的首页,包含观测列表和 Leaflet 地图。地图中的sighting层将显示由get_geojson视图给出的 GeoJSON 响应:
from django.shortcuts import render
from django.http import HttpResponse
from vectorformats.Formats import Django, GeoJSON
from models import Sighting
def home(request):
"""
Display the home page with the list and a map of the sightings.
"""
sightings = Sighting.objects.all()
return render("sightings/home.html", {'sightings' : sightings})
def get_geojson(request):
"""
Get geojson (needed by the map) for all of the sightings.
"""
sightings = Sighting.objects.all()
djf = Django.Django(geodjango='geometry',
properties=['animal_name', 'animal_image_url', 'description',
'rate', 'date_formatted', 'country_name'])
geoj = GeoJSON.GeoJSON()
s = geoj.encode(djf.decode(sightings))
return HttpResponse(s)
- 将以下
@property定义添加到chp09/wildlife/sightings文件夹下的models.py文件中的Sighting类。get_geojson视图将需要使用这些属性来组合从 Leaflet 地图和信息弹出窗口中需要的 GeoJSON 视图。注意在country_name属性中,你使用了 GeoDjango,它包含一个空间查找QuerySet操作符来检测观测发生的国家:
@property
def date_formatted(self):
return self.date.strftime('%m/%d/%Y')
@property
def animal_name(self):
return self.animal.name
@property
def animal_image_url(self):
return self.animal.image_url()
@property
def country_name(self):
country = Country.objects.filter
(geometry__contains=self.geometry)[0]
return country.name
- 在
sightings/templates/sightings下添加一个home.html文件,包含以下代码。使用 Django 模板语言,您将显示系统中的目击事件数量,以及这些目击事件的列表,其中包含每个事件的主要信息,以及 Leaflet 地图。使用 Leaflet JavaScript API,您将基础 OpenStreetMap 层添加到地图中。然后,您使用 jQuery 进行异步调用,调用get_geojson视图(通过在请求 URL 中添加/geojson来访问)。如果查询成功,它将使用来自目击 PostGIS 层的功能填充 Leaflet GeoJSON 层,并将每个功能与一个信息弹出窗口关联。此弹出窗口将在用户点击代表目击事件的地图上的点时打开,显示该实体的主要信息:
<!DOCTYPE html>
<html>
<head>
<title>Wildlife's Sightings</title>
<link rel="stylesheet"
href="https://unpkg.com/leaflet@1.2.0/dist/leaflet.css"
integrity="sha512-M2wvCLH6DSRazYeZRIm1JnYyh
22purTM+FDB5CsyxtQJYeKq83arPe5wgbNmcFXGqiSH2XR8dT
/fJISVA1r/zQ==" crossorigin=""/>
<script src="img/leaflet.js"
integrity="sha512-lInM/apFSqyy1o6s89K4iQUKg6ppXEgsVxT35HbzUup
EVRh2Eu9Wdl4tHj7dZO0s1uvplcYGmt3498TtHq+log==" crossorigin="">
</script>
<script src="img/jquery.min.js">
</script>
</head>
<body>
<h1>Wildlife's Sightings</h1>
<p>There are {{ sightings.count }} sightings
in the database.</p>
<div id="map" style="width:800px; height:500px"></div>
<ul>
{% for s in sightings %}
<li><strong>{{ s.animal }}</strong>,
seen in {{ s.country_name }} on {{ s.date }}
and rated {{ s.rate }}
</li> {% endfor %}
</ul>
<script type="text/javascript">
// OSM layer
var osm = L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}
.png', {
maxZoom: 18,
attribution: "Data by OpenStreetMap"
});
// map creation
var map = new L.Map('map', {
center: new L.LatLng(15, 0),
zoom: 2,
layers: [osm],
zoomControl: true
});
// add GeoJSON layer
$.ajax({
type: "GET",
url: "geojson",
dataType: 'json',
success: function (response) {
geojsonLayer = L.geoJson(response, {
style: function (feature) {
return {color: feature.properties.color};
},
onEachFeature: function (feature, layer) {
var html = "<strong>" +
feature.properties.animal_name +
"</strong><br />" +
feature.properties.animal_image_url +
"<br /><strong>Description:</strong> " +
feature.properties.description +
"<br /><strong>Rate:</strong> " +
feature.properties.rate +
"<br /><strong>Date:</strong> " +
feature.properties.date_formatted +
"<br /><strong>Country:</strong> " +
feature.properties.country_name
layer.bindPopup(html);
}
}).addTo(map);
}
});
</script>
</body>
</html>
- 现在您的前端页面已完成,您最终可以在
http://localhost:8000/访问它。导航地图并尝试识别一些显示的目击事件,以检查弹出窗口是否打开,如下面的截图所示:

它是如何工作的...
你为之前菜谱中开发的 Web 应用程序创建了一个 HTML 前端页面。该 HTML 是使用 Django 模板语言(docs.djangoproject.com/en/dev/topics/templates/)动态创建的,并且地图是通过 Leaflet JavaScript 库实现的。
Django 模板语言使用主页视图的响应来生成系统中所有目击事件的列表。
该地图使用 Leaflet 创建。首先,使用 OpenStreetMap 层作为基础地图。然后,使用 jQuery,你提供了一个 GeoJSON 层,该层显示由 get_geojson 视图生成的所有功能。你将一个弹出窗口与该层关联,每次用户点击一个目击实体时都会打开。该弹出窗口显示该目击事件的主要信息,包括被目击动物的图片。
使用 Mapbox 开发 Web GPX 查看器
对于这个菜谱,我们将使用来自第三章,处理矢量数据 - 基础的航点数据集。参考菜谱中名为处理 GPS 数据的脚本,了解如何将 .gpx 文件轨迹导入到 PostGIS 中。你还需要一个 Mapbox 令牌;为此,请访问他们的网站(www.mapbox.com)并注册一个。
如何做到...
- 为了准备 Mapbox 的 GeoJSON 格式的数据,使用
ogr2ogr从第三章,处理矢量数据 - 基础导出 tracks 表格,以下代码:
ogr2ogr -f GeoJSON tracks.json \
"PG:host=localhost dbname=postgis_cookbook user=me" \
-sql "select * from chp03.tracks
- 使用你喜欢的编辑器在新
.json文件上删除crs定义行:

- 前往你的 Mapbox 账户,在数据集菜单中上传
tracks.json文件。上传成功后,你将看到以下消息:

- 创建数据集并将其导出为瓦片集:

- 现在,使用户外模板创建一个新的样式:

-
添加轨道层并发布它。注意你可以使用的样式 URL,你可以用它来分享或进一步开发你的地图;复制它以便在代码中使用。
-
现在我们准备创建一个 mapbox.html 文件;在 head 部分添加以下内容以使用 Mapbox JS 和 CSS 库:
<script src='https://api.mapbox.com/mapbox-gl-js
/v0.42.0/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js
/v0.42.0/mapbox-gl.css' rel='stylesheet' />
- 在正文插入一个
map,使用你的令牌和我们刚刚创建的样式:
<div id='map' style='width: 800px; height: 600px;'></div>
<script>
mapboxgl.accessToken = YOUR_TOKEN';
var map = new mapboxgl.Map({
container: 'map',
style: 'YOUR_STYLE_URL'
});
// Add zoom and rotation controls to the map.
map.addControl(new mapboxgl.NavigationControl());
</script>
- 就这样,你可以双击并使用你喜欢的浏览器打开 HTML 文件,Mapbox API 将为你提供地图:

它是如何工作的...
要快速发布和可视化 webGIS 中的数据,你可以使用 Mapbox API 使用你自己的数据创建美丽的地图;你将需要保持 GeoJSON 格式,并且不要超过提供的带宽容量。在这个菜谱中,你学习了如何将你的 PostGIS 数据导出,以便在 Mapbox 中以 JS 的形式发布。
第十章:维护、优化和性能调整
本章将涵盖以下菜谱:
-
组织数据库
-
设置正确的数据权限机制
-
备份数据库
-
使用索引
-
为了效率进行聚类
-
优化 SQL 查询
-
将 PostGIS 数据库迁移到不同的服务器
-
使用流复制复制 PostGIS 数据库
-
地理空间分片
-
在 PostgreSQL 中并行处理
简介
与前面的章节不同,本章不讨论 PostGIS 的功能或应用。相反,它侧重于组织数据库的技术、提高查询性能以及确保空间数据的长期可行性。
这些技术通常被大多数 PostGIS 用户忽视,直到为时已晚——例如,当数据已经因为用户的行为而丢失,或者当数据量或用户数量增加时,性能已经下降。
这种忽视通常是由于学习每种技术所需的时间以及实施它们所需的时间量。本章试图以提炼的方式展示每种技术,以最大限度地减少学习曲线并最大限度地提高效益。
组织数据库
在创建和使用数据库时,需要考虑的最重要的事情之一是如何组织数据。布局应在首次建立数据库时确定。布局可以在稍后决定或更改,但这几乎可以保证是一个繁琐的,如果不是困难的任务。如果从未决定,数据库将随着时间的推移而变得杂乱无章,在升级组件或运行备份时引入重大障碍。
默认情况下,新的 PostgreSQL 数据库只有一个模式——即public。大多数用户将所有数据(他们自己的和第三方模块,如 PostGIS)放在public模式中。这样做会混合来自不同来源的不同信息。一种简单的方法来分离信息是使用模式。这使得我们可以为我们的数据使用一个模式,为其他所有事物使用一个单独的模式。
准备工作
在这个菜谱中,我们将创建一个数据库并在其自己的模式中安装 PostGIS。我们还将加载一些几何形状和栅格数据,以便在本书的其他菜谱中使用。
以下是在创建 PostGIS 启用数据库的两种方法:
-
使用
CREATE EXTENSION语句 -
使用 PostgreSQL 客户端运行安装 SQL 脚本
如果您正在运行 PostgreSQL 9.1 或更高版本,则可以使用CREATE EXTENSION方法,这是安装 PostGIS 的推荐方法:
- 访问以下链接下载加利福尼亚学校和警察局的 shapefiles:
scec.usc.edu/internships/useit/content/california-emergency-facilities-shapefile。
如何操作...
执行以下步骤以创建和组织数据库:
- 通过执行以下命令创建一个名为
chapter10的数据库:
CREATE DATABASE chapter10;
- 在
chapter10数据库中创建一个名为postgis的模式,我们将在这里安装 PostGIS。执行以下命令:
CREATE SCHEMA postgis;
-
在
chapter10数据库的postgis模式中安装 PostGIS:- 如果你运行的是 PostgreSQL 9.1 或更高版本,使用
CREATE EXTENSION语句:
- 如果你运行的是 PostgreSQL 9.1 或更高版本,使用
CREATE EXTENSION postgis WITH SCHEMA postgis;
CREATE EXTENSION语句的WITH SCHEMA子句指示 PostgreSQL 在postgis模式中安装 PostGIS 及其对象。
- 通过运行以下命令检查 PostGIS 安装是否成功:
> psql -U me -d chapter10
> chapter10=# SET search_path = public, postgis;

验证模式中关系列表,它应该包括由扩展创建的所有关系:

如果你使用pgAdmin或类似的数据库系统,你还可以在图形界面中检查模式、视图和表是否已成功创建。
SET语句指示 PostgreSQL 在处理来自我们客户端连接的任何 SQL 语句时考虑public和postgis模式。没有SET语句,\d命令将不会从postgis模式返回任何关系。
- 为了防止每次客户端连接到
chapter10数据库时都需要手动使用SET语句,通过执行以下命令修改数据库:
ALTER DATABASE chapter10 SET search_path = public, postgis;
所有未来的连接和查询到chapter10都将导致 PostgreSQL 自动使用public和postgis模式。
注意:对于 Windows 用户,此选项可能不起作用;在版本 9.6.7 中它起作用,但在版本 9.6.3 中不起作用。如果不起作用,你可能需要在每个命令中明确定义search_path。两种版本都提供。
- 通过执行以下命令加载 PRISM 栅格和旧金山边界几何形状,这些我们在第五章,“处理栅格数据”中使用过:
> raster2pgsql -s 4322 -t 100x100 -I -F -C -Y
C:\postgis_cookbook\data\chap5
\PRISM\ PRISM_tmin_provisional_4kmM2_201703_asc.asc
prism | psql -d chapter10 -U me
然后,定义搜索路径:
> raster2pgsql -s 4322 -t 100x100 -I -F -C -Y
C\:postgis_cookbook\data\chap5
\PRISM\PRISM_tmin_provisional_4kmM2_201703_asc.asc
prism | psql "dbname=chapter10 options=--search_path=postgis" me
- 正如我们在第五章,“处理栅格数据”中所做的那样,我们将通过执行以下命令对栅格文件名进行后处理,将其添加到
date列:
ALTER TABLE postgis.prism ADD COLUMN month_year DATE;
UPDATE postgis.prism SET month_year = (
SUBSTRING(split_part(filename, '_', 5), 0, 5) || '-' ||
SUBSTRING(split_part(filename, '_', 5), 5, 4) || '-01'
) :: DATE;
- 然后,我们通过执行以下命令加载旧金山边界:
> shp2pgsql -s 3310 -I
C\:postgis_cookbook\data\chap5\SFPoly\sfpoly.shp sfpoly |
psql -d chapter10 -U me
然后,定义搜索路径:
> shp2pgsql -s 3310 -I
C\:postgis_cookbook\data\chap5\SFPoly\sfpoly.shp
sfpoly | psql "dbname=chapter10 options=--search_path=postgis" me
- 将本章的数据集复制到其自己的目录中,通过执行以下命令:
> mkdir C:\postgis_cookbook\data\chap10
> cp -r /path/to/book_dataset/chap10
C\:postgis_cookbook\data\chap10
我们将使用南加州大学 USEIT 项目提供的加利福尼亚州学校和警察局的 shapefiles。通过执行以下命令导入 shapefiles;仅对警察局的 shapefile 使用空间索引标志-I:
> shp2pgsql -s 4269 -I
C\:postgis_cookbook\data\chap10\CAEmergencyFacilities\CA_police.shp
capolice | psql -d chapter10 -U me
然后,定义搜索路径:
> shp2pgsql -s 4269 C\:postgis_cookbook\data\chap10
\CAEmergencyFacilities\CA_schools.shp
caschools | psql -d chapter10 -U me
然后,定义搜索路径:
shp2pgsql -s 4269 -I C\:postgis_cookbook\data\chap10
\CAEmergencyFacilities\CA_schools.shp
caschools | psql "dbname=chapter10 options=--search_path=postgis"
me shp2pgsql -s 4269 -I
C\:postgis_cookbook\data\chap10\CAEmergencyFacilities\CA_police.shp
capolice | psql "dbname=chapter10 options=--search_path=postgis" me
它是如何工作的...
在这个菜谱中,我们创建了一个新的数据库并在其自己的模式中安装了 PostGIS。我们将 PostGIS 对象与我们的几何和栅格数据保持分离,而没有在 public 模式中安装 PostGIS。这种分离使 public 模式保持整洁,并减少了意外修改或删除 PostGIS 对象的风险。如果搜索路径的定义不起作用,那么在所有命令中使用显式的模式定义,如下所示。
在接下来的菜谱中,我们将看到我们决定在单独的模式中安装 PostGIS 的决定在维护数据库时导致的问题更少。
设置正确的数据权限机制
PostgreSQL 提供了一个细粒度的权限系统,它规定了谁可以使用特定的一组数据以及一组数据如何被授权用户访问。由于其细粒度特性,创建一个有效的权限集可能会令人困惑,并可能导致不希望的行为。可以提供不同级别的访问权限,从控制谁可以连接到数据库服务器本身,到谁可以查询视图,到谁可以执行 PostGIS 函数。
通过将数据库视为洋葱,可以最小化建立良好权限集的挑战。最外层有通用规则,每一层向内应用的规则比上一层更具体。一个例子是只有公司网络可以访问的公司数据库服务器。
公司的某个部门只能访问数据库 A,该数据库包含每个部门的模式。在一个模式内,所有用户都可以对视图执行 SELECT 查询,但只有特定的用户可以添加、更新或从表中删除记录。
在 PostgreSQL 中,用户和组被称为角色。一个角色可以是其他角色的父角色,而这些角色本身还可以是更多角色的父角色。
准备工作
在这个菜谱中,我们专注于为之前菜谱中创建的 postgis 模式建立最佳的权限集。通过正确的权限选择,我们可以控制谁可以使用几何、地理或栅格列的内容并对其执行操作。
值得一提的一个方面是,数据库对象(如数据库本身、模式或表)的所有者始终对该对象拥有完全控制权。除非有人更改所有者,否则通常创建数据库对象的用户是该对象的所有者。
再次强调,当在 Windows 上测试时,关于权限授予的功能在版本 9.6.7 上工作,但在版本 9.6.3 上则不工作。
如何操作...
在前面的菜谱中,我们将几个栅格和形状文件导入到相应的表中。默认情况下,对这些表的访问权限仅限于执行导入操作的用户,也称为所有者。以下步骤允许其他用户访问这些表:
- 为了演示和测试通过以下命令在
chapter10数据库中设置的权限,我们需要创建几个组和用户:
CREATE ROLE group1 NOLOGIN;
CREATE ROLE group2 NOLOGIN;
CREATE ROLE user1 LOGIN PASSWORD 'pass1' IN ROLE group1;
CREATE ROLE user2 LOGIN PASSWORD 'pass2' IN ROLE group1;
CREATE ROLE user3 LOGIN PASSWORD 'pass3' IN ROLE group2;
前两个CREATE ROLE语句创建了group1和group2组。最后三个CREATE ROLE语句创建了三个用户,其中user1和user2用户分配给group1,而user3用户分配给group2。
- 我们希望
group1和group2能够访问chapter10数据库。我们希望group1被允许连接到数据库并创建临时表,而group2应被授予所有数据库级别的权限,因此我们使用以下GRANT语句:
GRANT CONNECT, TEMP ON DATABASE chapter10 TO GROUP group1;
GRANT ALL ON DATABASE chapter10 TO GROUP group2;
- 让我们通过执行以下命令来检查
GRANT语句是否生效:
> psql -U me -d chapter10

如您所见,group1和group2出现在chapter10数据库的访问权限列中:
group1=Tc/postgres
group2=CTc/postgres
- 在
chapter10的权限中有一件事可能让我们感到担忧:
=Tc/postgres
与group1和group2的权限列表不同,这个列表在等号之前没有值(*=*)。这个列表是为特殊元组public准备的,它是 PostgreSQL 内置的,并且所有用户和组都自动属于这个元组。
- 我们不希望每个人都能够访问
chapter10数据库,因此我们需要使用REVOKE语句通过以下命令从public元组中移除权限:
REVOKE ALL ON DATABASE chapter10 FROM public;
- 让我们通过执行以下命令来查看
chapter10数据库模式的初始权限:

postgis模式没有列出任何权限。然而,这并不意味着没有人可以访问postgis模式。只有模式的所有者(在这个例子中是postgres)可以访问它。我们将通过执行以下命令将postgis模式的访问权限授予group1和group2:
GRANT USAGE ON SCHEMA postgis TO group1, group2;
我们通常不希望将CREATE权限授予postgis模式中的任何用户或组。不应该向postgis模式添加新的对象(如函数、视图和表)。
- 如果我们想让所有用户和组都能访问
postgis模式,我们可以通过执行以下命令将USAGE权限授予元组public:
GRANT USAGE ON SCHEMA postgis TO public;
如果你想撤销这个权限,请使用以下命令:
REVOKE USAGE ON SCHEMA postgis FROM public;
- 在继续之前,我们应该检查我们的权限是否已经在数据库中反映出来:

将USAGE权限授予一个模式并不允许被授予的用户和组在该模式中使用任何对象。USAGE权限仅允许用户和组查看模式的孩子对象。每个孩子对象都有自己的权限集,我们将在接下来的步骤中设置这些权限。
PostGIS 附带超过 1,000 个函数。为每个函数单独设置权限是不合理的。相反,我们授予元组public的EXECUTE权限,然后为特定的函数(如管理函数)授予和/或撤销权限。
- 首先,通过执行以下命令将
EXECUTE权限授予元组public:
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA postgis TO public;
- 现在,通过执行以下命令来撤销
public元组对某些函数(如postgis_full_version())的EXECUTE权限:
REVOKE ALL ON FUNCTION postgis_full_version() FROM public;
如果在postgis模式上访问函数有问题,请使用以下命令:
REVOKE ALL ON FUNCTION postgis.postgis_full_version() FROM public;
GRANT和REVOKE语句不会区分表和视图,因此必须小心确保所应用的权限适用于对象。
- 我们将通过执行以下命令将
SELECT、REFERENCES和TRIGGER权限授予public元组在所有postgis表和视图中;这些权限中的任何一个都不会赋予public元组更改表或视图内容的能力:
GRANT SELECT, REFERENCES, TRIGGER
ON ALL TABLES IN SCHEMA postgis TO public;
- 我们希望允许
group1能够向spatial_ref_sys表插入新记录,因此我们必须执行以下命令:
GRANT INSERT ON spatial_ref_sys TO group1;
不属于group1(如group2)的组和用户只能使用spatial_ref_sys上的SELECT语句。现在是group1的组和用户可以使用INSERT语句添加新的空间参考系统。
- 让我们通过执行以下命令,给
user2(group1的成员)赋予在spatial_ref_sys上使用UPDATE和DELETE语句的能力;我们不会给任何人赋予在spatial_ref_sys上使用TRUNCATE语句的权限:
GRANT UPDATE, DELETE ON spatial_ref_sys TO user2;
- 在建立权限后,始终检查它们是否实际起作用是个好习惯。最好的做法是以其中一个用户登录到数据库。我们将通过执行以下命令使用
user3用户来做这件事:
> psql -d chapter10 -u user3
- 现在,通过执行以下命令检查我们是否可以在
spatial_ref_sys表上运行SELECT语句:
chapter10=# SELECT count(*) FROM spatial_ref_sys;
如果需要定义模式,请使用以下语句:

- 让我们通过执行以下命令尝试在
spatial_ref_sys中插入一条新记录:
chapter10=# INSERT INTO spatial_ref_sys
VALUES (99999, 'test', 99999, '', '');
ERROR: permission denied for relation spatial_ref_sys
- 太好了!现在通过执行以下命令更新
spatial_ref_sys中的记录:
chapter10=# UPDATE spatial_ref_sys SET srtext = 'Lorum ipsum';
ERROR: permission denied for relation spatial_ref_sys
- 通过执行以下命令对
postgis_full_version()函数进行最后的检查:
chapter10=# SELECT postgis_full_version();
ERROR: permission denied for function postgis_full_version
它是如何工作的...
在这个菜谱中,我们根据组或用户授予和撤销权限,随着组或用户向下进入数据库,安全性逐渐提高。这导致group1和group2能够连接到chapter10数据库并使用postgis模式中的对象。group1还可以向spatial_ref_sys表插入新记录。只有user2被允许更新或删除spatial_ref_sys的记录。
在这个菜谱中使用的 GRANT 和 REVOKE 语句是有效的,但使用命令行实用程序(如 psql)使用它们可能会很繁琐。相反,使用提供授权向导的图形工具,如 pgAdmin。此类工具还使检查授权和撤销权限后数据库的行为变得更加容易。
为了进行额外的练习,设置 public 模式及其子对象的权限,以便尽管 group1 和 group2 将能够在表上运行 SELECT 查询,但只有 group2 能够在 caschools 表上使用 INSERT 语句。你还需要确保 group2 的用户执行的 INSERT 语句实际上可以工作。
备份数据库
维护数据和工作功能备份可能是最不受重视的,但却是提高你生产力和压力水平最重要的事情。你可能认为你不需要备份你的 PostGIS 数据库,因为你已经将原始数据导入到数据库中,但你记得你为开发最终产品所做的工作吗?中间产品呢?即使你记得过程中的每一步,创建中间和最终产品需要多少时间?
如果这些任何问题让你犹豫,你需要为你的数据创建一个备份。幸运的是,PostgreSQL 使备份过程变得简单,或者至少比其他方法痛苦少一些。
准备工作
在这个菜谱中,我们使用 PostgreSQL 的 pg_dump 工具。pg_dump 工具确保正在备份的数据是一致的,即使它目前正在使用中。
如何操作...
使用以下步骤来备份数据库:
- 通过执行以下命令开始备份
chapter10数据库:
> pg_dump -f chapter10.backup -F custom chapter10
我们使用 -f 标志指定备份应放置在 chapter10.backup 文件中。我们还使用 -F 标志将备份输出格式设置为自定义 - 默认情况下,这是 pg_dump 输出格式中最灵活和压缩的。
- 通过执行以下命令将内容输出到 SQL 文件来检查备份文件:
> pg_restore -f chapter10.sql chapter10.backup
在创建备份后,确保备份有效是一个好习惯。我们使用 PostgreSQL 的 pg_restore 工具这样做。-f 标志指示 pg_restore 将恢复输出到文件而不是数据库。发出的输出包括标准 SQL 语句。
- 使用文本编辑器查看
chapter10.sql。你应该会看到创建表、填充已创建的表和设置权限的 SQL 语句块,如下所示:

并且文件继续显示有关表、序列等信息:

- 由于我们使用自定义格式备份了
chapter10数据库,因此我们对pg_restore的行为以及它恢复的内容有细粒度的控制。让我们使用-n标志仅提取public模式,如下所示:
> pg_restore -f chapter10_public.sql -n public chapter10.backup
如果你将 chapter10_public.sql 与前一步导出的 chapter10.sql 文件进行比较,你会看到 postgis 模式没有被恢复。
它是如何工作的...
如你所见,在 PostgreSQL 中备份数据库非常简单。不幸的是,如果备份不是定期进行的,那么备份就没有意义。如果数据库丢失或损坏,自上次备份以来所做的任何工作也将丢失。建议你按照最小化丢失工作量的时间间隔进行备份。理想的时间间隔将取决于对数据库进行的更改频率。
可以通过向操作系统的任务调度器添加一个作业来安排 pg_dump 工具定期运行;有关如何操作的说明可以在 PostgreSQL 维基百科的 wiki.postgresql.org/wiki/Automated_Backup_on_Windows 和 wiki.postgresql.org/wiki/Automated_Backup_on_Linux 中找到。
pg_dump 工具并不适用于所有情况。如果你有一个不断变化的数据库或者大于几十个吉字节,你需要一个比本食谱中讨论的更强大的备份机制。有关这些强大机制的信息可以在 PostgreSQL 文档的 www.postgresql.org/docs/current/static/backup.html 中找到。
以下是一些可用于建立强大和高级备份方案的第三方备份工具:
-
Barman 可在
www.pgbarman.org获取 -
pg-rman 可在
code.google.com/p/pg-rman获取
使用索引
数据库索引非常类似于书籍的索引(如这本书的索引)。虽然一本书的索引指示了包含某个单词的页面,但数据库列索引指示了包含搜索值的表中的行。就像一本书的索引不会指示单词在页面上的确切位置一样,数据库索引可能无法表示搜索值在行列中的确切位置。
PostgreSQL 有几种索引类型,如 B-Tree、Hash、GIST、SP-GIST 和 GIN。所有这些索引类型都是为了帮助查询更快地找到匹配的行而设计的。使索引不同的地方在于其底层算法。通常,为了保持简单,几乎所有 PostgreSQL 索引都是 B-Tree 类型。PostGIS(空间)索引是 GIST 类型。
几何形状、地理空间和栅格都是大型的复杂对象,与这些对象相关联或在这些对象之间进行关联需要时间。空间索引被添加到 PostGIS 数据类型中以提高搜索性能。性能提升不是来自比较实际的、可能复杂的空间对象,而是这些对象的简单边界框。
准备工作
对于这个菜谱,psql将如下使用来计时查询:
> psql -U me -d chapter10
chapter10=# \timing on
我们将使用本章第一个菜谱中加载的caschools和sfpoly表。
如何做到这一点...
要了解查询如何受到索引的影响,最好的方法是运行添加索引前后的查询。在这个菜谱中,为了避免需要定义模式,所有表都被假定为位于 public 模式中。以下步骤将指导您通过使用索引优化查询的过程:
- 运行以下查询,该查询返回在旧金山找到的所有学校的名称:
SELECT schoolid FROM caschools sc JOIN sfpoly sf
ON ST_Intersects(sf.geom, ST_Transform(sc.geom, 3310));
- 查询的结果并不重要。我们更感兴趣的是查询的运行时间。当我们运行查询三次时,它的运行时间如下;您的数字可能与这些数字不同:
Time: 136.643 ms
Time: 140.863 ms
Time: 135.859 ms
- 查询运行得很快。但是,如果需要多次运行查询(比如说 1,000 次),那么运行这么多次将需要超过 500 秒。查询能否运行得更快?使用
EXPLAIN ANALYZE来查看 PostgreSQL 如何运行查询,如下所示:
EXPLAIN ANALYZE
SELECT schoolid FROM caschools sc JOIN sfpoly sf
ON ST_Intersects(sf.geom, ST_Transform(sc.geom, 3310));
在查询之前添加EXPLAIN ANALYZE指示 PostgreSQL 返回用于执行查询的实际计划,如下所示:

在前面的查询计划中,重要的是连接过滤器,它消耗了大部分执行时间。这可能是因为caschools表在geom列上没有空间索引。
- 按如下方式在
geom列上添加空间索引:
CREATE INDEX caschools_geom_idx ON caschools
USING gist (geom);
- 重复执行步骤 1 三次以最小化运行时间的差异。使用空间索引,查询的运行时间如下:
Time: 95.807 ms
Time: 101.626 ms
Time: 103.748 ms
查询并没有因为空间索引而运行得更快。发生了什么?我们需要检查查询计划。
- 您可以使用
EXPLAIN ANALYZE查看 PostgreSQL 中查询计划是否以及如何改变,如下所示:

查询计划表与步骤 4中找到的相同。查询没有使用空间索引。为什么?
如果查看查询,我们使用了ST_Transform()将caschools.geom重新投影到sfpoly.geom的空间参考系上。在ST_Intersects()空间测试中使用的ST_Transform()几何形状位于 SRID 3310,但用于caschools_geom_idx索引的几何形状位于 SRID 4269。这种空间参考系的不同阻止了在查询中使用索引。
- 我们可以创建一个使用在所需空间参考系中投影的几何形状的空间索引。使用函数的索引被称为函数索引。它可以按照以下方式创建:
CREATE INDEX caschools_geom_3310_idx ON caschools
USING gist (ST_Transform(geom, 3310));
- 重复执行步骤 1 三次以获得以下输出:
Time: 63.359 ms
Time: 64.611 ms
Time: 56.485 ms
这更好!过程的持续时间从大约 135 毫秒减少到 60 毫秒。
- 按如下方式检查
查询计划表:

计划显示查询使用了caschools_geom_3310_idx索引。Index Scan命令比之前使用的Join Filter命令快得多。
它是如何工作的...
数据库索引帮助我们快速有效地找到我们感兴趣的值。通常,使用索引的查询比不使用索引的查询要快,但性能提升可能不会像本食谱中找到的那样显著。
关于 PostgreSQL 和 PostGIS 索引的更多信息可以在以下链接中找到:
我们将在本章后面的食谱中更详细地讨论查询计划。通过理解查询计划,可以优化性能不佳的查询。
效率聚类
大多数用户在添加适当的索引后就会停止优化表性能。这通常是因为性能已经足够好。但如果表有数百万或数十亿条记录呢?这么多的信息可能无法适应数据库服务器的 RAM,从而迫使访问硬盘。通常,表记录在硬盘中按顺序存储。但查询从硬盘中检索的数据可能需要访问硬盘的许多不同部分。需要访问硬盘的不同部分是一个已知的性能限制。
为了减轻硬盘性能问题,数据库表可以在硬盘中重新排序其记录,以便相似的记录数据存储在彼此附近。数据库表的重新排序称为聚类,并用于 PostgreSQL 中的CLUSTER语句。
准备工作
我们将使用加利福尼亚学校(caschools)和旧金山边界(sfpoly)表来完成本食谱。如果这两个表都不可用,请参考本章的第一个食谱。
本食谱的查询将使用psql实用工具,如下所示:
> psql -U me -d chapter10
chapter10=# \timing on
如何操作...
使用以下步骤对表进行聚类:
- 在使用
CLUSTER语句之前,通过执行以下命令检查之前食谱中使用的查询的时间:
SELECT schoolid FROM caschools sc JOIN sfpoly sf
ON ST_Intersects(sf.geom, ST_Transform(sc.geom, 3310));
- 我们得到了以下三次查询运行的性能数字:
Time: 80.746 ms
Time: 80.172 ms
Time: 80.004 ms
- 使用
caschools_geom_3310_idx索引对caschools表进行聚类,如下所示:
CLUSTER caschools USING caschools_geom_3310_idx;
- 重新运行第一步中的查询三次,以获取以下性能时间:
Time: 57.880 ms
Time: 55.939 ms
Time: 53.107 ms
性能提升并不显著。
它是如何工作的...
在caschools表上使用CLUSTER语句并没有带来显著的性能提升。这里的教训是,尽管数据是根据索引信息在物理上进行重新排序以优化搜索,但并不能保证查询性能在分簇表中会得到提升。分簇应仅用于已添加适当索引并优化了查询的表,并且只有当记录很大时才使用。
优化 SQL 查询
当接收到一个 SQL 查询时,PostgreSQL 会通过其规划器运行查询以决定最佳执行计划。最佳执行计划通常会导致最快的查询性能。尽管规划器通常做出正确的选择,但有时特定的查询可能会有次优的执行计划。
对于这些情况,以下是一些可以采取的措施来改变 PostgreSQL 规划器的行为:
-
向相关表添加适当的列索引
-
更新数据库表的统计信息
-
通过评估查询的执行计划并使用 PostgreSQL 安装中可用的功能来重写 SQL 查询
-
考虑更改或添加数据库表的布局
-
更改查询规划器的配置
添加索引(第一个要点)在本章的另一个单独的食谱中有讨论。更新统计信息(第二个要点)通常在表活动达到一定量后由 PostgreSQL 自动完成,但可以使用ANALYZE语句手动更新统计信息。更改数据库布局和查询规划器的配置(分别对应第四和第五个要点)是仅在尝试了前三个要点之后才使用的先进操作,因此将不再进一步讨论。
本食谱仅讨论第三种选项——即通过重写 SQL 查询来优化性能。
准备工作
对于这个食谱,我们将找到每个学校的最近警察局以及旧金山每个学校与其最近警察局之间的米距离;我们将尝试尽可能快地完成这项任务。这需要我们多次重写查询以使其更高效并利用新的 PostgreSQL 功能。为此食谱,请确保还包括capolice表。
如何做到这一点...
以下步骤将指导您通过迭代过程来提高查询性能:
- 要找到旧金山的每个学校的最近警察局以及每个学校与其最近警察局之间的距离,我们首先将执行以下查询:
SELECT
di.school,
police_address,
distance
FROM ( -- for each school, get the minimum distance to a
-- police station
SELECT
gid,
school,
min(distance) AS distance
FROM ( -- get distance between every school and every police
-- station in San Francisco
SELECT
sc.gid,
sc.name AS school,
po.address AS police_address,
ST_Distance(po.geom_3310, sc.geom_3310) AS distance
FROM ( -- get schools in San Francisco
SELECT
ca.gid,
ca.name,
ST_Transform(ca.geom, 3310) AS geom_3310
FROM sfpoly sf
JOIN caschools ca
ON ST_Intersects(sf.geom, ST_Transform(ca.geom, 3310))
) sc
CROSS JOIN ( -- get police stations in San Francisco
SELECT
ca.address,
ST_Transform(ca.geom, 3310) AS geom_3310
FROM sfpoly sf
JOIN capolice ca
ON ST_Intersects(sf.geom, ST_Transform(ca.geom, 3310))
) po ORDER BY 1, 2, 4
) scpo
GROUP BY 1, 2
ORDER BY 2
) di JOIN ( -- for each school, collect the police station
-- addresses ordered by distance
SELECT
gid,
school,
(array_agg(police_address))[1] AS police_address
FROM (-- get distance between every school and
every police station in San Francisco
SELECT
sc.gid,
sc.name AS school,
po.address AS police_address,
ST_Distance(po.geom_3310, sc.geom_3310) AS distance
FROM ( -- get schools in San Francisco
SELECT
ca.gid,
ca.name,
ST_Transform(ca.geom, 3310) AS geom_3310
FROM sfpoly sf
JOIN caschools ca
ON ST_Intersects(sf.geom, ST_Transform(ca.geom, 3310))
) sc
CROSS JOIN ( -- get police stations in San Francisco
SELECT
ca.address,
ST_Transform(ca.geom, 3310) AS geom_3310
FROM sfpoly sf JOIN capolice ca
ON ST_Intersects(sf.geom, ST_Transform(ca.geom, 3310))
) po
ORDER BY 1, 2, 4
) scpo
GROUP BY 1, 2
ORDER BY 2
) po
ON di.gid = po.gid
ORDER BY di.school;
- 一般而言,这是一个粗略且简单的查询。子查询
scpo在查询中出现了两次,因为它需要计算学校到最近警察局的最近距离以及每个学校最近的警察局名称。如果每个scpo实例需要 10 秒来计算,那么两个实例就需要 20 秒。这对性能非常有害。
注意:实验的时间可能会因机器配置、数据库使用等因素而有很大差异。然而,实验持续时间的改变将是明显的,并且应该遵循本节中展示的相同改进比率。
查询输出如下:

...

- 查询结果提供了旧金山的学校地址,以及每所学校最近的警察局的地址和距离。然而,我们也很感兴趣尽快得到答案。在
psql中开启计时,我们得到以下三次查询的性能数据:
Time: 5076.363 ms
Time: 4974.282 ms
Time: 5027.721 ms
- 只需查看第一步中的查询,我们就可以看到存在冗余的子查询。让我们使用在 PostgreSQL 8.4 版本中引入的公共表表达式(CTEs)来消除这些重复。CTEs 用于逻辑上和语法上从查询的后续部分中分离出一段 SQL。由于 CTEs 是逻辑上分离的,它们在查询执行开始时运行,并且其结果被缓存以供后续使用:
WITH scpo AS ( -- get distance between every school and every
-- police station in San Francisco
SELECT
sc.gid,
sc.name AS school,
po.address AS police_address,
ST_Distance(po.geom_3310, sc.geom_3310) AS distance
FROM ( -- get schools in San Francisco
SELECT
ca.*,
ST_Transform(ca.geom, 3310) AS geom_3310
FROM sfpoly sf
JOIN caschools ca
ON ST_Intersects(sf.geom, ST_Transform(ca.geom, 3310))
) sc
CROSS JOIN ( -- get police stations in San Francisco
SELECT
ca.*,
ST_Transform(ca.geom, 3310) AS geom_3310
FROM sfpoly sf
JOIN capolice ca
ON ST_Intersects(sf.geom, ST_Transform(ca.geom, 3310))
) po
ORDER BY 1, 2, 4
)
SELECT
di.school,
police_address,
distance
FROM ( -- for each school, get the minimum distance to a
-- police station
SELECT
gid,
school,
min(distance) AS distance
FROM scpo
GROUP BY 1, 2
ORDER BY 2
) di
JOIN ( -- for each school, collect the police station
-- addresses ordered by distance
SELECT
gid,
school,
(array_agg(police_address))[1] AS police_address
FROM scpo
GROUP BY 1, 2
ORDER BY 2
) po
ON di.gid = po.gid
ORDER BY 1;
- 不仅查询在语法上更简洁,性能也有所提高,如下所示:
Time: 2803.923 ms
Time: 2798.105 ms
Time: 2796.481 ms
执行时间从超过 5 秒减少到不到 3 秒。
- 尽管有些人可能会在这个时候停止优化这个查询,但我们将继续提高查询性能。我们可以使用窗口函数,这是在 v8.4 版本中引入的另一个 PostgreSQL 功能。如下使用窗口函数,我们可以消除
JOIN表达式:
WITH scpo AS ( -- get distance between every school and every
-- police station in San Francisco
SELECT
sc.name AS school,
po.address AS police_address,
ST_Distance(po.geom_3310, sc.geom_3310) AS distance
FROM ( -- get schools in San Francisco
SELECT
ca.name,
ST_Transform(ca.geom, 3310) AS geom_3310
FROM sfpoly sf
JOIN caschools ca
ON ST_Intersects(sf.geom, ST_Transform(ca.geom, 3310))
) sc
CROSS JOIN ( -- get police stations in San Francisco
SELECT
ca.address,
ST_Transform(ca.geom, 3310) AS geom_3310
FROM sfpoly sf
JOIN capolice ca
ON ST_Intersects(sf.geom, ST_Transform(ca.geom, 3310))
) po
ORDER BY 1, 3, 2
)
SELECT
DISTINCT school,
first_value(police_address)
OVER (PARTITION BY school ORDER BY distance),
first_value(distance)
OVER (PARTITION BY school ORDER BY distance)
FROM scpo
ORDER BY 1;
- 我们使用
first_value()窗口函数提取每个学校按学校与警察局之间的距离排序的第一个police_address和distance值。改进相当显著,从几乎 3 秒减少到大约 1.2 秒:
Time: 1261.473 ms
Time: 1217.843 ms
Time: 1215.086 ms
- 然而,使用
EXPLAIN ANALYZE VERBOSE检查执行计划是值得的,以查看是什么降低了查询性能。由于输出非常冗长,我们只剪裁了以下几行感兴趣的内容:

...
-> Nested Loop (cost=0.15..311.48 rows=1 width=48)
(actual time=15.047..1186.907 rows=7956 loops=1)
Output: ca.name, ca_1.address,
st_distance(st_transform(ca_1.geom, 3310),
st_transform(ca.geom, 3310))
-
在
EXPLAIN ANALYZE VERBOSE的输出中,我们想要检查实际时间的值,这些值提供了查询该部分的实际开始和结束时间。在所有实际时间范围内,Nested Loop(在先前的输出中突出显示)的实际时间值 15.047..1186.907 是最差的。这个查询步骤至少消耗了总执行时间的 80%,因此任何旨在提高性能的工作都必须在这个步骤中完成。 -
从慢速
Nested Loop实用程序返回的列可以在输出值中找到。在这些列中,st_distance()只存在于这一步,而不在任何内部步骤中。这意味着我们需要减轻对ST_Distance()的调用次数。 -
在这一步,没有运行 PostgreSQL 9.1 或更高版本,进一步的查询改进是不可能的。PostgreSQL 9.1 引入了使用
<->和<#>运算符的索引最近邻搜索,分别用于比较几何图形的凸包和边界框。对于点几何图形,这两个运算符得出相同的答案。 -
让我们重写查询以利用
<->运算符。以下查询仍然使用了 CTEs 和窗口函数:
WITH sc AS ( -- get schools in San Francisco
SELECT
ca.gid,
ca.name,
ca.geom
FROM sfpoly sf
JOIN caschools ca
ON ST_Intersects(sf.geom, ST_Transform(ca.geom, 3310))
), po AS ( -- get police stations in San Francisco
SELECT
ca.gid,
ca.address,
ca.geom
FROM sfpoly sf
JOIN capolice ca
ON ST_Intersects(sf.geom, ST_Transform(ca.geom, 3310))
)
SELECT
school,
police_address,
ST_Distance(ST_Transform(school_geom, 3310),
ST_Transform(police_geom, 3310)) AS distance
FROM ( -- for each school, number and order the police
-- stations by how close each station is to the school
SELECT
ROW_NUMBER() OVER (
PARTITION BY sc.gid ORDER BY sc.geom <-> po.geom
) AS r,
sc.name AS school,
sc.geom AS school_geom,
po.address AS police_address,
po.geom AS police_geom
FROM sc
CROSS JOIN po
) scpo
WHERE r < 2
ORDER BY 1;
- 该查询有以下性能指标:
Time: 83.002 ms
Time: 82.586 ms
Time: 83.327 ms
哇!使用带索引的最近邻搜索和<->运算符,我们将初始查询从一秒减少到不到十分之一秒。
它是如何工作的...
在这个菜谱中,我们优化了一个用户在使用 PostGIS 时可能会遇到的查询。我们首先利用 PostgreSQL 的功能来提高查询的性能和语法。当性能不能再提高时,我们运行EXPLAIN ANALYZE VERBOSE以找出消耗查询执行时间最多的是什么。我们了解到ST_Distance()函数消耗了执行计划中最多的时间。我们最终使用了 PostgreSQL 9.1 的<->运算符,显著提高了查询执行时间,使其低于一秒。
本菜谱中使用的EXPLAIN ANALYZE VERBOSE输出不易理解。对于复杂的查询,建议您使用 pgAdmin 中的可视化输出(在单独章节的菜谱中讨论)或explain.depesz.com/网络服务提供的颜色编码,如下面的截图所示:

将 PostGIS 数据库迁移到不同的服务器
在某个时候,用户数据库需要迁移到不同的服务器。这种服务器迁移的需求可能是由于新的硬件或数据库服务器软件升级。
以下是将数据库迁移的三个可用方法:
-
使用
pg_dump和pg_restore导出和恢复数据库 -
使用
pg_upgrade对数据库进行就地升级 -
从一个服务器到另一个服务器的流式复制
准备工作
在本菜谱中,我们将使用dump和restore方法将用户数据移动到具有新 PostGIS 安装的新数据库。与其他方法不同,这种方法是最保险的,适用于所有情况,并在预期不按预期工作时存储备份。
如前所述,为与 PostGIS 一起使用而创建的特定模式可能对 Windows 用户不起作用。在public模式上工作是一个测试结果的选择。
如何操作...
在命令行中,执行以下步骤:
- 尽管在本章的第三个菜谱中已创建了一个备份文件,但通过执行以下命令创建一个新的备份文件:
> pg_dump -U me -f chapter10.backup -F custom chapter10
- 通过执行以下命令创建一个新的数据库,将备份文件恢复到该数据库:
> psql -d postgres -U me
postgres=# CREATE DATABASE new10;
- 连接到
new10数据库并创建一个postgis模式,如下所示:
postgres=# \c new10
new10=# CREATE SCHEMA postgis;
- 执行
CREATE EXTENSION命令在postgis模式中安装 PostGIS 扩展:
new10=# CREATE EXTENSION postgis WITH SCHEMA postgis;
- 确保您已将
search_path参数设置为包含postgis模式,如下所示:
new10=# ALTER DATABASE new10 SET search_path = public, postgis;
- 通过执行以下命令仅从备份文件中恢复
public模式到new10数据库:
> pg_restore -U me -d new10 --schema=public chapter10.backup
restore方法运行时不应生成错误。如果生成了错误,将出现如下错误消息:
pg_restore: [archiver (db)] Error while PROCESSING TOC:
pg_restore: [archiver (db)] Error from TOC entry 3781; 03496229
TABLE DATA prism postgres
pg_restore: [archiver (db)] COPY failed for table "prism":
ERROR: function st_bandmetadata(postgis.raster, integer[])
does not exist
LINE 1: SELECT array_agg(pixeltype)::text[]
FROM st_bandmetadata($1...
我们现在已在postgis模式中安装了 PostGIS,但数据库服务器找不到ST_BandMetadata()函数。如果找不到函数,通常与search_path有关。我们将在下一步修复此问题。
- 通过执行以下命令检查
pg_restore实际上执行了什么:
pg_restore -f chapter10.sql --schema=public chapter10.backup
- 查看 prism 表的
COPY语句,一切看起来都很正常。但在此表之前显示的search_path方法没有包含postgis模式,如下所示:
SET search_path = public, pg_catalog;
- 通过执行以下命令将
chapter10.sql中的search_path值更改为包含postgis模式:
SET search_path = public, postgis, pg_catalog;
- 使用
psql运行chapter10.sql,如下所示;原始chapter10.backup文件不能使用,因为无法将必要更改应用于pg_restore:
> psql -U me -d new10 -f chapter10.sql
它是如何工作的...
此过程本质上与标准的 PostgreSQL 备份和恢复周期相同。它可能不是简单的,但具有使用工具和过程每个步骤中可用的控制的优点。尽管其他迁移方法可能方便,但它们通常需要相信一个不透明的过程或安装额外的软件。
使用流复制复制 PostGIS 数据库
世界的现实是,给定足够的时间,一切都会出问题。这包括运行 PostgreSQL 的计算机的硬件和软件。为了保护 PostgreSQL 中的数据免受损坏或丢失,使用pg_dump等工具进行备份。然而,恢复数据库备份可能需要非常长的时间,在此期间用户无法使用数据库。
当必须将停机时间保持在最低限度或不可接受时,可以使用一个或多个备用服务器来补偿失败的 PostgreSQL 主服务器。备用服务器上的数据将通过尽可能频繁地流式传输数据与主 PostgreSQL 服务器保持同步。
此外,强烈建议你不要尝试混合不同的 PostgreSQL 版本。主服务器和备用服务器必须运行相同的 PostgreSQL 版本。
准备工作
在这个菜谱中,我们将使用 PostgreSQL 9.X 中引入的流复制功能。这个菜谱将使用一个服务器上的两个并行 PostgreSQL 安装,而不是典型的两个或更多服务器,每个服务器上有一个 PostgreSQL 安装。我们将使用两个新的数据库集群以保持事情简单。
如何操作...
使用以下步骤来复制一个 PostGIS 数据库:
- 通过执行以下命令创建主数据库集群和备用数据库集群的目录:
> mkdir postgis_cookbook/db
> mkdir postgis_cookbook/db/primary
> mkdir postgis_cookbook/db/standby
- 使用
initdb初始化数据库集群,如下所示,将用户me定义为数据库的所有者:
> cd postgis_cookbook/db
> initdb --encoding=utf8 --locale=en_US.utf-8 -U me -D primary
> initdb --encoding=utf8 --locale=en_US.utf-8 -U me -D standby
-
如果出现错误,你可以选择避免使用
--locale=en_US.utf-8选项;在这种情况下,系统将采用计算机上的默认区域设置。 -
通过执行以下命令创建主数据库集群和备用数据库集群的归档目录:
> mkdir postgis_cookbook/db/primary/archive
> mkdir postgis_cookbook/db/standby/archive
-
使用你喜欢的编辑应用程序打开主集群的
pg_hba.conf认证文件。 -
如果你正在运行 PostgreSQL 9.0,请将以下文本添加到
pg_hba.conf文件的末尾:

对于 PostgreSQL 9.1 或更高版本,配置行已经包含在 pg_hba.conf 文件中。你只需从每个匹配行的开头删除注释字符(#)。
- 编辑主集群的
postgresql.conf配置文件,设置流复制参数。搜索每个参数,取消注释并替换分配的值如下:
port = 5433
wal_level = hot_standby
max_wal_senders = 5
wal_keep_segments = 32
archive_mode = on
archive_command = 'copy "%p"
"C:\\postgis_cookbook\\db\\primary\\archive\\%f"' # for Windows
也可以使用相对位置:
archive_command = 'copy "%p" "archive\\%f" "%p"'
当使用 Linux 或 macOS 时,请使用以下类型:
archive_command = 'cp %p archive\/%f'
- 通过执行以下命令在主数据库集群上启动 PostgreSQL:
> pg_ctl start -D primary -l primary\postgres.log
- 创建主数据库集群的基础备份并将其复制到备用数据库集群。在执行备份之前,通过执行以下命令为
xcopy创建一个排除列表文件(仅限 Windows):
> notepad exclude.txt
- 将以下内容添加到
exclude.txt:
postmaster.pid
pg_xlog
- 执行以下操作以运行基础备份并将目录内容从主数据库集群复制到备用数据库集群:
> psql -p 5433 -U me -c "SELECT pg_start_backup('base_backup', true)"
> xcopy primary\* standby\ /e /exclude:primary\exclude.txt
> psql -p 5433 -U me -c "SELECT pg_stop_backup()"
- 对备用集群的
postgresql.conf配置文件进行以下更改,取消注释这些参数并调整值:
port = 5434
hot_standby = on
archive_command = 'copy "%p"
"C:\\postgis_cookbook\\db\\standby\\archive\\%f"' # for Windows
也可以使用相对位置:
archive_command = 'copy ".\\archive\\%f" "%p"'
当使用 Linux 或 macOS 时,请使用以下类型:
archive_command = 'cp %p archive\/%f'
- 通过执行以下命令在备用集群目录中创建
recovery.conf配置文件(针对 Windows):
> notepad standby\recovery.conf
对于 Linux 或 macOS:
> nano standby\recovery.conf
- 在
recovery.conf配置文件中输入以下内容并保存更改:
standby_mode = 'on'
primary_conninfo = 'port=5433 user=me'
restore_command = 'copy
"C:\\postgis_cookbook\\db\\standby\\archive\\%f" "%p"'
或者也可以使用相对位置:
restore_command = 'copy ".\\archive\\%f" "%p"'
对于 Linux 或 macOS 使用:
restore_command = 'cp %p \archive\/%f"'
- 通过执行以下命令在备用数据库集群上启动 PostgreSQL:
> pg_ctl start -U me -D standby -l standby\postgres.log
-
运行一些简单的测试以确保复制工作正常。
-
通过执行以下命令在主数据库服务器上创建
test数据库和test表:
> psql -p 5433 -U me
postgres=# CREATE DATABASE test;
postgres=# \c test
test=# CREATE TABLE test AS SELECT 1 AS id, 'one'::text AS value;
- 通过执行以下命令连接到备用数据库服务器:
> psql -p 5434 -U me
- 通过执行以下命令检查
test数据库是否存在:
postgres=# \l

- 通过执行以下命令连接到
test数据库并获取表列表:
postgres=# \c test

- 通过执行以下命令获取
test表中的记录(如果有):

恭喜!流式复制工作正常。
它是如何工作的...
如本配方所示,流式复制的基本设置非常简单。对主数据库服务器所做的更改会迅速推送到备用数据库服务器。
有第三方应用程序可以帮助在生产服务器上建立、管理和维护流式复制。这些应用程序允许复杂的复制策略,包括多主、多备用和适当的故障转移。以下是一些这些应用程序的例子:
-
Pgpool-II,可在
www.pgpool.net找到 -
Bucardo,可在
bucardo.org/wiki/Bucardo找到 -
Postgres-XC,可在
postgresxc.wikia.com/wiki/Postgres-XC_Wiki找到 -
Slony-I,可在
slony.info找到
地理空间分片
对于数据库引擎来说,处理大型数据集可能具有挑战性,尤其是当它们存储在单个表或单个数据库中时。PostgreSQL 提供了一个选项,可以将数据分割成几个外部数据库,这些数据库具有较小的表,逻辑上作为一个整体工作。分片允许将大型数据集的存储和处理负载进行分布,从而减少大型本地表的影响。
使其工作最重要的一个问题是定义一个函数来分类和均匀分配数据。鉴于这个函数可以是地理属性,因此可以将分片应用于地理空间数据。
准备工作
在本配方中,我们将使用postgres_fdw扩展,它允许创建外数据包装器,这是访问存储在外部 PostgreSQL 数据库中的数据所必需的。为了使用此扩展,我们需要几个概念的组合:服务器、外数据包装器、用户映射、外表和表继承。我们将在本配方中展示它们的作用,并欢迎您在 PostgreSQL 文档中详细探索它们。
我们将使用第一章,在 PostGIS 中移动数据进和出中使用的火灾热点数据集和世界国家边界 shapefile,根据地理标准分配热点数据的记录,我们将创建热点数据集的新分布式版本。
我们将使用postgis_cookbook数据库来完成这个菜谱。
如何做到这一点...
如果您没有遵循第一章,在 PostGIS 中移动数据进和出的菜谱,请确保将热点(Global_24h.csv)导入 PostGIS。以下步骤解释了如何使用ogr2ogr(您应该以原始 SRID,4326,导入数据集,以使空间操作更快):
- 在
postgis_cookbook数据库中启动一个会话:
> psql -d postgis_cookbook -U me
- 在
postgis_cookbook数据库中创建一个新的模式chp10:
postgis_cookbook=# CREATE SCHEMA chp10;
- 我们需要创建
hotspots_dist表,它将作为外键表的父亲:
postgis_cookbook =# CREATE TABLE chp10.hotspots_dist (id serial
PRIMARY KEY, the_geom public.geometry(Point,4326));
- 退出
psql环境:
postgis_cookbook=# \q
- 以 postgres 用户连接到 psql 环境:
> psql -U me
- 创建远程数据库,连接它们,创建
postgis扩展,并创建将接收分片数据的外键表。然后,退出psql环境。为此,执行以下 SQL 命令:
postgres=# CREATE DATABASE quad_NW;
CREATE DATABASE quad_NE;
CREATE DATABASE quad_SW;
CREATE DATABASE quad_SE;
postgres=# \c quad_NW;
quad_NW =# CREAT EXTENSION postgis;
quad_NW =# CREATE TABLE hotspots_quad_NW (
id serial PRIMARY KEY,
the_geom public.geometry(Point,4326)
);
quad_NW =# \c quad_NE;
quad_NE =# CREAT EXTENSION postgis;
quad_NE =# CREATE TABLE hotspots_quad_NE (
id serial PRIMARY KEY,
the_geom public.geometry(Point,4326)
);
quad_NW =# \c quad_SW;
quad_SW =# CREAT EXTENSION postgis;
quad_SW =# CREATE TABLE hotspots_quad_SW (
id serial PRIMARY KEY,
the_geom public.geometry(Point,4326)
);
quad_SW =# \c quad_SE;
quad_SE =# CREAT EXTENSION postgis;
quad_SE =# CREATE TABLE hotspots_quad_SE (
id serial PRIMARY KEY,
the_geom public.geometry(Point,4326)
);
quad_SE =# \q
- 为了导入火灾数据集,创建一个由
Global_24h.csv文件的一个层派生的 GDAL 虚拟数据源。为此,在 CSV 文件所在的同一目录中创建一个名为global_24h.vrt的文本文件,并按如下方式编辑它:
<OGRVRTDataSource>
<OGRVRTLayer name="Global_24h">
<SrcDataSource>Global_24h.csv</SrcDataSource>
<GeometryType>wkbPoint</GeometryType>
<LayerSRS>EPSG:4326</LayerSRS>
<GeometryField encoding="PointFromColumns"
x="longitude" y="latitude"/>
</OGRVRTLayer>
</OGRVRTDataSource>
- 使用您在先前的菜谱中创建的
global_24.vrt虚拟驱动程序将Global_24h.csv文件导入 PostGIS:
$ ogr2ogr -f PostgreSQL PG:"dbname='postgis_cookbook' user='me'
password='mypassword'" -lco SCHEMA=chp10 global_24h.vrt
-lco OVERWRITE=YES -lco GEOMETRY_NAME=the_geom -nln hotspots
- 在数据库中创建
postgres_fdw扩展:
postgis_cookbook =# CREATE EXTENSION postgres_fdw;
- 定义将托管外部数据库的服务器。您需要定义数据库名称、主机地址以及数据库将接收连接的端口号。在这种情况下,我们将创建 4 个数据库,每个全球象限一个,根据墨卡托 SRID 的纬度和经度。执行以下命令以创建四个服务器:
postgis_cookbook =# CREATE SERVER quad_NW
FOREIGN DATA WRAPPER postgres_fdw OPTIONS
(dbname 'quad_NW', host 'localhost', port '5432');
CREATE SERVER quad_SW FOREIGN DATA WRAPPER postgres_fdw OPTIONS
(dbname 'quad_SW', host 'localhost', port '5432');
CREATE SERVER quad_NE FOREIGN DATA WRAPPER postgres_fdw OPTIONS
(dbname 'quad_NE', host 'localhost', port '5432');
CREATE SERVER quad_SE FOREIGN DATA WRAPPER postgres_fdw OPTIONS
(dbname 'quad_SE', host 'localhost', port '5432');
-
对于这个例子,我们将使用本地数据库,但主机参数可以是 IP 地址或数据库文件。创建这些命令的用户将被定义为服务器的本地所有者。
-
创建用户映射,以便能够连接到外部数据库。为此,您需要在本地服务器上写入外部数据库所有者的登录信息:
postgis_cookbook =# CREATE USER MAPPING FOR POSTGRES SERVER quad_NW
OPTIONS (user 'remoteme1', password 'myPassremote1');
CREATE USER MAPPING FOR POSTGRES SERVER quad_SW
OPTIONS (user 'remoteme2', password 'myPassremote2');
CREATE USER MAPPING FOR POSTGRES SERVER quad_NE
OPTIONS (user 'remoteme3', password 'myPassremote3');
CREATE USER MAPPING FOR POSTGRES SERVER quad_SE
OPTIONS (user 'remoteme4', password 'myPassremote4');
- 在外部数据库中创建表,基于本地表
chp10.hotspots_dist:
postgis_cookbook =# CREATE FOREIGN TABLE hotspots_quad_NW ()
INHERITS (chp10.hotspots_dist) SERVER quad_NW
OPTIONS (table_name 'hotspots_quad_sw');
CREATE FOREIGN TABLE hotspots_quad_SW () INHERITS (chp10.hotspots_dist)
SERVER quad_SW OPTIONS (table_name 'hotspots_quad_sw');
CREATE FOREIGN TABLE hotspots_quad_NE () INHERITS (chp10.hotspots_dist)
SERVER quad_NE OPTIONS (table_name 'hotspots_quad_ne');
CREATE FOREIGN TABLE hotspots_quad_SE () INHERITS (chp10.hotspots_dist)
SERVER quad_SE OPTIONS (table_name 'hotspots_quad_se');
-
表名最好写成小写。
-
创建一个函数,用于计算要插入数据库的点所在的象限:
postgis_cookbook=# CREATE OR REPLACE
FUNCTION __trigger_users_before_insert() RETURNS trigger AS $__$
DECLARE
angle integer;
BEGIN
EXECUTE $$ select (st_azimuth(ST_geomfromtext('Point(0 0)',4326),
$1)
/(2*PI()))*360 $$ INTO angle
USING NEW.the_geom;
IF (angle >= 0 AND angle<90) THEN
EXECUTE $$
INSERT INTO hotspots_quad_ne (the_geom) VALUES ($1)
$$ USING
NEW.the_geom;
END IF;
IF (angle >= 90 AND angle <180) THEN
EXECUTE $$ INSERT INTO hotspots_quad_NW (the_geom) VALUES ($1)
$$ USING NEW.the_geom;
END IF;
IF (angle >= 180 AND angle <270) THEN
EXECUTE $$ INSERT INTO hotspots_quad_SW (the_geom) VALUES ($1)
$$ USING NEW.the_geom;
END IF;
IF (angle >= 270 AND angle <360) THEN
EXECUTE $$ INSERT INTO hotspots_quad_SE (the_geom) VALUES ($1)
$$ USING NEW.the_geom;
END IF;
RETURN null;
END;
$__$ LANGUAGE plpgsql;
CREATE TRIGGER users_before_insert
BEFORE INSERT ON chp10.hotspots_dist
FOR EACH ROW EXECUTE PROCEDURE __trigger_users_before_insert();
- 插入测试坐标(10, 10),(-10, 10)和(-10 -10)。第一个应该存储在东北象限,第二个在东南象限,第三个在西南象限。
postgis_cookbook=# INSERT INTO CHP10.hotspots_dist (the_geom)
VALUES (0, st_geomfromtext('POINT (10 10)',4326));
INSERT INTO CHP10.hotspots_dist (the_geom)
VALUES ( st_geomfromtext('POINT (-10 10)',4326));
INSERT INTO CHP10.hotspots_dist (the_geom)
VALUES ( st_geomfromtext('POINT (-10 -10)',4326));
- 检查表中的数据插入,包括本地视图和外部数据库
hotspots_quad_NE:
postgis_cookbook=# SELECT ST_ASTEXT(the_geom)
FROM CHP10.hotspots_dist;

- 如所示,本地版本显示了所有插入的点。现在,在远程数据库上执行查询:
postgis_cookbook=# SELECT ST_ASTEXT(the_geom) FROM hotspots_quad_ne;

远程数据库只存储根据之前定义的触发函数应该存储的点。
- 现在,从原始热点表插入所有点,这些点在步骤 8 中已导入。对于这次测试,我们只需插入几何信息。执行以下 SQL 语句:
postgis_cookbook=# insert into CHP10.hotspots_dist
(the_geom, quadrant)
select the_geom, 0 as geom from chp10.hotspots;
- 如同步骤 15,为了检查结果是否被正确分类和存储,对本地表
hotspots_dist和远程表hotsports_quad_ne执行以下查询:
postgis_cookbook=# SELECT ST_ASTEXT(the_geom)
FROM CHP10.hotspots_dist;

- 结果显示了本地数据库逻辑版本中存储的前 10 个点。
postgis_cookbook=# SELECT ST_ASTEXT(the_geom) FROM hotspots_quad_ne;

- 结果显示了远程数据库中存储的前 10 个点,包括 NE 象限中的所有点。这些点确实显示它们都具有正的纬度和经度值。当在 GIS 应用程序中展示时,结果是以下内容:

它是如何工作的...
在这个配方中,展示了地理分片的基本设置。可以在相同提出的结构上轻松实现更复杂的功能。此外,对于重负载应用程序,市场上有一些产品可以考虑。
所示的示例部分基于以下链接中找到的 GitHub 实现:gist.github.com/sylr/623bab09edd04d53ee4e。
在 PostgreSQL 中并行化
与分片类似,在 PostgreSQL 的地理空间表中处理大量行,将导致单个工作者的处理时间很长。随着 PostgreSQL 9.6 的发布,服务器能够执行可以由多个 CPU 处理的查询,以获得更快的答案。根据 PostgreSQL 文档,根据表的大小和查询计划,在实现并行查询而不是串行查询时,可能不会带来明显的优势。
准备工作
对于这个配方,我们需要 PostgreSQL 的一个特定版本。您不必下载和安装将要使用的 PostgreSQL 版本。原因是,一些开发者可能已经配置了一个带有数据的 PostgreSQL 数据库版本,而在计算机内运行多个服务器可能会在以后引起问题。
为了克服这个问题,我们将使用一个docker 容器。容器可以被定义为一个轻量级的软件应用实例,它与其他容器以及你的计算机主机隔离。类似于虚拟机,你可以在主机中存储多个软件版本,并在需要时启动多个容器。
首先,我们将从docs.docker.com/install/下载 docker,并安装社区版(CE)。然后,我们将拉取一个已经预编译的 docker 镜像。打开一个终端并运行以下命令:
$ docker pull shongololo/postgis
这个 docker 镜像包含 PostgreSQL 10、Postgis 2.4 和 SFCGAL 插件。现在我们需要根据镜像启动一个实例。一个重要的部分是-p 5433:5432。这些参数将你的主机(本地)计算机上接收到的每个连接和请求映射到容器的5432端口:
$ docker run --name parallel -p 5433:5432 -v <SHP_PATH>:/data shongololo/postgis
现在,你可以连接到你的 PostgreSQL 容器:
$ docker exec -it parallel /bin/bash
root@d842288536c9:/# psql -U postgres
psql (10.1)
Type "help" for help.
postgres=#
其中 root 和d842288536c9分别对应你的容器用户名和组。
如何操作...
因为我们已经创建了一个隔离的 PostgreSQL 数据库实例,我们必须重新创建数据库名称和模式才能使用。这些操作是可选的。然而,我们鼓励你遵循以下步骤,以确保这个食谱与本书的其他部分保持一致:
- 在你的容器中创建用户
me:
root@d842288536c9:/# psql -U postgres
psql (10.1)
Type "help" for help.
postgres=# CREATE USER me WITH PASSWORD 'me';
CREATE ROLE
postgres=# ALTER USER me WITH SUPERUSER;
ALTER ROLE
- 重新连接到数据库,但现在作为用户
me来创建数据库和模式:
root@d842288536c9:/# PGPASSWORD=me psql -U me -d postgres
postgres=# CREATE DATABASE "postgis-cookbook";
CREATE DATABASE
postgres=# \c postgis-cookbook
你现在已连接到数据库postgis-cookbook,用户为me:
postgis-cookbook=# CREATE SCHEMA chp10;
CREATE SCHEMA
postgis-cookbook=# CREATE EXTENSION postgis;
CREATE EXTENSION
- 在数据库中插入一个层。在这种情况下,我们将使用来自哥伦比亚的
gis.osm_buildings_a_free_1 shapefile。在启动容器之前,请确保这些文件位于SHP_PATH中。这种数据库插入可以以两种形式运行:第一种是在你的 docker 容器内部:
root@d842288536c9:/# /usr/lib/postgresql/10/bin/shp2pgsql -s 3734
-W latin1 /data/gis.osm_buildings_a_free_1.shp chp10.buildings |
PGPASSWORD=me psql -U me -h localhost -p 5432 -d postgis-cookbook
第二种选项是在你的主机计算机上。确保正确设置你的 shapefiles 路径和映射到容器端口5432的主机端口。此外,你的主机必须已安装postgresql-client:
$ shp2pgsql -s 3734 -W latin1 <SHP_PATH>
/gis.osm_buildings_a_free_1.shp chp10.buildings | PGPASSWORD=me
psql -U me -h localhost -p 5433 -d postgis-cookbook
- 执行并行查询。使用建筑表,我们可以并行执行一个
postgis命令。为了检查创建了多少个工作线程,我们使用EXPLAIN ANALYZE命令。所以,例如,如果我们想在一个串行查询中计算表中所有几何形状的总和:
postgis-cookbook=# EXPLAIN ANALYZE SELECT Sum(ST_Area(geom))
FROM chp10.buildings;
我们得到了以下结果:
Aggregate (cost=35490.10..35490.11 rows=1 width=8)
(actual time=319.299..319.2 99 rows=1 loops=1)
-> Seq Scan on buildings (cost=0.00..19776.16 rows=571416 width=142)
(actual time=0.017..68.961 rows=571416 loops=1)
Planning time: 0.088 ms
Execution time: 319.358 ms
(4 rows)
现在,如果我们修改max_parallel_workers和max_parallel_workers_per_gather参数,我们就可以激活 PostgreSQL 的并行查询功能:
Aggregate (cost=35490.10..35490.11 rows=1 width=8)
(actual time=319.299..319.299 rows=1 loops=1)
-> Seq Scan on buildings (cost=0.00..19776.16 rows=571416 width=142)
(actual time=0.017..68.961 rows=571416 loops=1)
Planning time: 0.088 ms
Execution time: 319.358 ms
(4 rows)
这个命令会在终端打印:
Finalize Aggregate (cost=21974.61..21974.62 rows=1 width=8)
(actual time=232.081..232.081 rows=1 loops=1)
-> Gather (cost=21974.30..21974.61 rows=3 width=8)
(actual time=232.074..232.078 rows=4 loops=1)
Workers Planned: 3
Workers Launched: 3
-> Partial Aggregate (cost=20974.30..20974.31 rows=1 width=8)
(actual time=151.785..151.785 rows=1 loops=4)
-> Parallel Seq Scan on buildings
(cost=0.00..15905.28 rows=184328 width=142)
(actual time=0.017..58.480 rows=142854 loops=4)
Planning time: 0.086 ms
Execution time: 239.393 ms
(8 rows)
- 执行并行扫描。例如,如果我们想选择面积高于某个值的多边形:
postgis-cookbook=# EXPLAIN ANALYZE SELECT * FROM chp10.buildings
WHERE ST_Area(geom) > 10000;
我们得到了以下结果:
Seq Scan on buildings (cost=0.00..35490.10 rows=190472 width=190)
(actual time=270.904..270.904 rows=0 loops=1)
Filter: (st_area(geom) > '10000'::double precision)
Rows Removed by Filter: 571416
Planning time: 0.279 ms
Execution time: 270.937 ms
(5 rows)
这个查询不会并行执行。这是因为ST_Area函数被定义为具有COST值为10。对于 PostgreSQL 来说,COST是一个正数,表示函数的估计执行成本。如果我们把这个值增加到100,我们可以得到一个并行计划:
postgis-cookbook=# ALTER FUNCTION ST_Area(geometry) COST 100;
postgis-cookbook=# EXPLAIN ANALYZE SELECT * FROM chp10.buildings
WHERE ST_Area(geom) > 10000;
现在我们有一个并行计划,并且有 3 个工作者正在执行查询:
Gather (cost=1000.00..82495.23 rows=190472 width=190)
(actual time=189.748..189.748 rows=0 loops=1)
Workers Planned: 3
Workers Launched: 3
-> Parallel Seq Scan on buildings
(cost=0.00..62448.03 rows=61443 width=190)
(actual time=130.117..130.117 rows=0 loops=4)
Filter: (st_area(geom) > '10000'::double precision)
Rows Removed by Filter: 142854
Planning time: 0.165 ms
Execution time: 190.300 ms
(8 rows)
- 执行并行连接。首先,我们创建一个点表,其中每个多边形随机创建
10个点:
postgis-cookbook=# DROP TABLE IF EXISTS chp10.pts_10;
postgis-cookbook=# CREATE TABLE chp10.pts_10 AS
SELECT (ST_Dump(ST_GeneratePoints(geom, 10))).geom
::Geometry(point, 3734) AS geom,
gid, osm_id, code, fclass, name, type FROM chp10.buildings;
postgis-cookbook=# CREATE INDEX pts_10_gix
ON chp10.pts_10 USING GIST (geom);
现在,我们可以在两个表之间运行表连接,但这不会给我们一个并行计划:
Nested Loop (cost=0.41..89034428.58 rows=15293156466 width=269)
-> Seq Scan on buildings (cost=0.00..19776.16 rows=571416 width=190)
-> Index Scan using pts_10_gix on pts_10
(cost=0.41..153.88 rows=190 width=79)
Index Cond: (buildings.geom && geom)
Filter: _st_intersects(buildings.geom, geom)
对于这种情况,我们需要修改参数parallel_tuple_cost,该参数设置规划器对从一个并行工作者进程传输一个元组到另一个进程的成本的估计。将值设置为0.001会给我们一个并行计划:
Nested Loop (cost=0.41..89034428.58 rows=15293156466 width=269)
-> Seq Scan on buildings (cost=0.00..19776.16 rows=571416 width=190)
-> Index Scan using pts_10_gix on pts_10
(cost=0.41..153.88 rows=190 width=79)
Index Cond: (buildings.geom && geom)
Filter: _st_intersects(buildings.geom, geom)
它是如何工作的...
如本菜谱所示,在 PostgreSQL 中并行化查询允许优化涉及大量数据集的操作。数据库引擎已经能够实现并行性,但定义适当的配置对于利用该功能至关重要。
在这个菜谱中,我们使用了max_parallel_workers和parallel_tuple_cost来配置所需的并行度。我们可以使用ANALYZE函数来评估性能。
第十一章:使用桌面客户端
在本章中,我们将涵盖以下主题:
-
添加 PostGIS 图层 – QGIS
-
使用数据库管理器插件 – QGIS
-
添加 PostGIS 图层 – OpenJUMP GIS
-
运行数据库查询 – OpenJUMP GIS
-
添加 PostGIS 图层 – gvSIG
-
添加 PostGIS 图层 – uDig
简介
至少,桌面 GIS 程序允许你从 PostGIS 数据库中可视化数据。这种关系随着在数据库外和在动态播放环境中编辑和操作数据的能力而变得更加有趣。
做一个改变,看到改变!因此,可视化存储在 PostGIS 中的数据对于有效的空间数据库管理通常是至关重要的——至少作为一个不时进行的理智检查。本章将演示数据库和桌面客户端之间的动态和静态关系。
无论你在地理空间社区中的经验水平或角色如何,你应该至少找到一个四个 GIS 程序作为你 PostGIS 数据库和最终产品之间潜在的中间阶段环境。
在本章中,我们将使用以下桌面 GIS 程序连接到 PostGIS:QGIS、OpenJUMP GIS、gvSIG和uDig。
一旦连接到 PostGIS,将特别强调使用数据库管理器(DB Manager)插件和数据存储查询分别提供的QGIS和OpenJUMP GIS的一些更高级的功能。
添加 PostGIS 图层 – QGIS
在这个菜谱中,我们将建立与我们的 PostGIS 数据库的连接,以便将一个表作为QGIS(以前称为Quantum GIS)中的图层添加。将表格作为图层查看对于创建地图或简单地在外部数据库中工作副本非常有用。
请导航到以下网站以安装 QGIS(2.18 – Las Palmas,撰写本文时)的最新版本 LTR:
在此页面上,点击下载现在,你将能够选择合适的操作系统和相关设置。QGIS 适用于 Android、Linux、macOS X 和 Windows。你也可能倾向于点击探索 QGIS,以获取有关程序的基本信息,包括功能、截图和案例研究。
准备工作
首先,为这一章创建名为chp11的模式;然后,从美国人口普查局的 FTP 网站下载数据:
ftp2.census.gov/geo/tiger/TIGER2012/EDGES/tl_2012_39035_edges.zip
该 shapefile 是俄亥俄州库亚霍加县的All Lines,其中包括道路和其他线要素。
将 ZIP 文件解压到你的工作目录,然后使用shp2pgsql将其加载到你的数据库中。务必指定空间参考系统,EPSG/SRID:4269。如果你对使用投影有疑问,请使用以下网站提供的 OpenGeo 服务:
使用以下命令生成加载chp11模式中表的 shapefile 的 SQL:
shp2pgsql -s 4269 -W LATIN1 -g the_geom -I tl_2012_39035_edges.shp chp11.tl_2012_39035_edges > tl_2012_39035_edges.sql
如何做到这一点...
现在是时候使用 QGIS 查看我们下载的数据了。我们必须首先创建到数据库的连接,以便访问表。连接并按照以下步骤将表添加为图层:
- 点击添加 PostGIS 图层图标:

-
点击下拉菜单下方的新建按钮。
-
创建一个新的 PostGIS 连接。在添加 PostGIS 表(s)窗口打开后,为连接创建一个名称,并填写数据库的一些参数,包括主机、端口、数据库名称、用户名和密码:

-
一旦您已输入数据库的所有相关信息,请点击测试连接按钮以验证连接是否成功。如果连接不成功,请仔细检查是否有拼写错误和错误。此外,请确保您正在尝试连接到启用了 PostGIS 的数据库。
-
如果连接成功,请继续勾选保存用户名和保存密码复选框。这将防止您在整个练习过程中多次输入登录信息。
-
点击菜单底部的 OK 按钮以应用连接设置。现在您可以连接了!
确保您的 PostGIS 连接名称出现在下拉菜单中,然后点击连接按钮。如果您选择不存储用户名和密码,每次尝试访问数据库时都会要求您提交此信息。
连接后,数据库中的所有模式都将显示,通过展开目标模式,表将变得可见。
- 通过单击表名或其行上的任何位置来选择要添加为图层的表(s)。选定的内容将以蓝色突出显示。要取消选择一个表,请再次单击它,它将不再突出显示。选择在章节开头下载的
tl_2012_39035_edges表,然后点击添加按钮,如图所示:

-
表的一个子集也可以作为一个图层添加。这通过双击所需的表名来完成。
-
查询构建器窗口将打开,它有助于创建简单的 SQL
WHERE子句语句。通过选择roadflg = Y的记录来添加道路。这可以通过输入查询或使用查询构建器中的按钮来完成:

- 点击 OK 按钮,然后点击添加按钮。现在,表的子集已作为图层加载到 QGIS 中。该图层严格是数据库的静态、临时副本。您可以对该图层进行任何更改,而不会影响数据库表。
同样,反过来也是如此。对数据库中表的更改将对 QGIS 中的图层没有影响。
如果需要,你可以将临时图层保存为多种格式,如 DXF、GeoJSON、KML 或 SHP。只需在图层面板中右键单击图层名称,然后单击“另存为”。这将创建一个文件,你可以在以后回忆或与他人分享。
以下截图显示了 Cuyahoga 县的道路网络:

你也可以使用 QGIS 浏览器面板在现在已连接的 PostGIS 数据库中导航,并列出模式和表。此面板允许你双击以将空间图层添加到当前项目,不仅为连接的数据库,而且为你的机器上的任何目录提供更好的用户体验:

它是如何工作的...
你已经使用内置的添加 PostGIS 表 GUI 将 PostGIS 图层添加到 QGIS 中。这是通过创建一个新的连接并输入你的数据库参数来实现的。
可以同时设置任意数量的数据库连接。如果你的工作流程中更常见地使用多个数据库,将所有连接保存到一个 XML 文件中(参见上一节中的提示)会在返回 QGIS 中的这些项目时节省时间和精力。
使用数据库管理器插件 – QGIS
数据库管理器(DB Manager)通过允许用户以多种方式与数据库交互,允许用户与 PostGIS 建立更复杂的关系。该插件模仿了 pgAdmin 的一些核心功能,并增加了数据可视化的好处。
在本配方中,我们将使用 DB Manager 在数据库中创建、修改和删除项目,然后对 SQL 窗口进行操作。在本节结束时,你将能够做到以下几件事情:
-
导航到 DB Manager 菜单
-
创建、修改和删除数据库模式和表
-
运行 SQL 查询以添加新的 QGIS 图层或在数据库中创建新表
需要安装 QGIS 才能使用此配方。请参考本章的第一个配方以获取有关下载安装程序的信息。
准备工作
让我们确保插件已启用并连接到数据库。
- 点击 QGIS 菜单栏上的插件菜单,然后从下拉菜单中选择“管理插件”和“安装插件”,如图所示:

- QGIS 插件管理器窗口将打开。在插件列表中搜索 DB Manager,并确保它被勾选(已启用),如图所示:

- 现在 DB Manager 已启用,让我们打开插件并检查数据库连接的状态。通过导航到 QGIS 菜单栏上的数据库 | DB Manager 打开 DB Manager,如图所示:

-
在数据库管理器的树窗口中展开 PostGIS 目录。在上一个菜谱中创建的 PostGIS 连接将出现。展开连接以查看您的模式(s)和表(s)。如果您选择不保存凭据,您将需要输入用户名和密码。
在主窗口上方的“信息”标签页中,您可以查看有关数据的信息,例如空间参考、几何类型、字段名称、字段类型等等,如下面的截图所示:

如果您无法展开 PostGIS 菜单,则 PostGIS 连接尚未设置。如果您需要建立连接,请参阅添加PostGIS图层 – QGIS菜谱中的步骤 1到4。在使用数据库管理器之前必须建立连接。
如何操作...
导航到数据库管理器菜单并执行以下步骤:
-
在树窗口中选择
tl_2012_39035_edges表。 -
接下来,点击“表”标签以查看实际的数据表,如下面的截图所示:

- 最后一个标签页“预览”用于可视化数据,如下面的截图所示:

要创建、修改和删除数据库模式和表,请按照以下步骤操作:
- 首先,在数据库中创建一个新的模式以存储本章的数据。从菜单栏中选择“模式”|“创建模式”,如下面的截图所示:

- 将模式名称输入为
new_schema,然后点击确定。如果您在列表中看不到new_schema,您可能需要在树窗口中选择数据库连接并点击刷新按钮或按F5(见以下截图):

- 您的新空模式现在将在“树”窗口中可见:

现在让我们继续使用包含tl_2012_39035_edges表的 chp11 模式。让我们将表名修改为更通用的名称。如何?lines?您可以通过在树窗口中点击表来更改表名。一旦文本被高亮显示并且光标闪烁,您就可以删除现有的名称并输入新名称,lines。
目前,我们的lines表的数据正在使用度作为当前投影的测量单位(EPSG: 4269)。让我们添加一个新的几何列,使用 EPSG: 3734,这是一个以英尺为单位的州平面坐标系。要运行 SQL 查询,请按照以下步骤操作:
- 在数据库管理器中点击 SQL 窗口按钮或按F2,如下面的截图所示:

- 将以下查询复制到 SQL 窗口中,然后点击执行:
SELECT AddGeometryColumn('chp11', 'lines','geom_sp',3734,
'MULTILINESTRING', 2);
UPDATE "chp11".lines
SET geom_sp = ST_Transform(the_geom,3734);
查询创建了一个名为 geom_sp 的新几何列,然后通过将原始几何形状(geom)从 EPSG 4269 转换为 3734 来更新几何信息,如下截图所示:

- 刷新
chp11架构,你会注意到数据库中的表现在有两个几何列,在树窗口中独立处理:

上一张截图显示了原始几何形状。下一张截图显示了创建的几何形状:

- 对于我们的下一个查询,让我们从
lines表中取一个子集。类似于上一节中我们所做的,我们只查看关于道路的数据。然而,这次,我们可以限制我们想要加载到图层中的列,以及执行更复杂的空间查询。我们将通过执行以下命令使用新的几何列(geom_sp)应用 10 英尺的缓冲区:
SELECT gid, ST_Buffer(geom_sp, 10) AS geom, fullname, roadflg
FROM "chp11".lines WHERE roadflg = 'Y'
选择“加载为新图层”复选框,然后选择 gid 作为唯一 ID,geom 作为几何形状。为图层创建一个名称,然后点击“立即加载!”,你将看到以下截图所示的内容:

查询将结果作为临时图层添加到 QGIS 中。
- 现在我们将修改查询,通过执行以下命令在数据库中创建一个表,而不是将查询作为图层加载:
CREATE TABLE "chp11".roads_buffer_sp AS SELECT gid,
ST_Buffer(geom_sp, 10) AS geom, fullname, roadflg
FROM "chp11".lines WHERE roadflg = 'Y'
以下截图显示了 Cuyahoga 县的道路网络:

工作原理...
连接到 PostGIS 数据库(参见本章中的 添加 PostGIS 图层 – QGIS 菜谱)允许你使用 DB Manager 插件。一旦启用 DB Manager,我们就可以在信息、表和预览选项卡之间切换,以有效地查看元数据、表格数据和数据可视化。
接下来,我们通过查询窗口对数据库进行了更改,在架构 chp11 的表 lines 上运行,以转换投影。注意 SQL 窗口中的自动完成功能,这使得编写查询变得轻而易举。
通过刷新数据库连接,在 DB Manager 中可以看到对数据库的更改。
添加 PostGIS 图层 – OpenJUMP GIS
在本节中,我们将使用 OpenJUMP GIS(OpenJUMP)连接到 PostGIS,以便将空间表作为图层添加。接下来,我们将编辑临时图层并在数据库的新表中更新它。
OpenJUMP 中的 JUMP 代表 Java Unified Mapping Platform。要了解更多关于该程序的信息,或需要安装最新版本,请访问:
点击页面上的“下载最新版本”链接 (sourceforge.net/projects/jump-pilot/files/OpenJUMP/1.12/) 以查看安装程序列表。选择适合您操作系统的版本(Windows 的.exe和 Linux 及 mac OS 的.jar)。有关安装 OpenJUMP 的详细说明以及其他文档和信息,可以在以下链接的 OpenJUMP Wiki 页面上找到:
ojwiki.soldin.de/index.php?title=Main_Page
准备工作
我们将重用和构建在添加 PostGIS 图层 – QGIS菜谱中使用的数据。如果您跳过了这个菜谱,您需要执行以下操作:
- 从美国人口普查局的 FTP 站点下载以下 ZIP 文件:
ftp://ftp2.census.gov/geo/tiger/TIGER2012/EDGES/tl_2012_39035_edges.zip
形状文件是俄亥俄州库耶霍加县的“所有线路”,包括道路和其他线状特征。
- 将 ZIP 文件解压到您的工作目录,然后使用
shp2pgsql将其加载到数据库中。请确保指定空间参考系统,即EPSG: 4269,并将表命名为lines。
如何操作...
可以通过以下步骤添加数据源图层:
-
点击打开(文件夹)图标或转到文件 | 打开。
-
从左侧菜单选择数据存储图层,如下截图所示:

-
点击连接下拉列表右侧的图标以打开连接管理器。
-
点击添加按钮。这将弹出一个新窗口,您必须在其中输入数据库参数:

- 为连接输入一个名称,然后输入以下数据库参数的值:
-
-
服务器/主机
-
端口
-
数据库
-
用户名
-
密码
-
-
然后点击“确定”。现在您已连接到数据库。
-
如果出现连接错误消息,请检查拼写错误和错误。如果连接成功,选择连接并点击“确定”。
-
现在您可以选择要添加的线条表,如下截图所示:

如果存在多个几何列,您可以选择您想要使用的列。添加数据的状态平面协调几何(geom_sp),如使用数据库管理器插件 – QGIS菜谱中所示。
如果只需要表的一个子集,可以使用简单的 SQL WHERE子句。
- 点击“完成”以将数据集加载到主窗口中,如下截图所示:

- 现在快速编辑临时图层并将其保存回数据库。选择编辑工具箱按钮。当工具箱加载后,点击屏幕左上角的“选择特征工具”按钮,如下截图所示:

- 选择一些看起来在地图上不合适的线条,如图下所示。特别是县北部的线条;这些线条实际上延伸到了伊利湖。您可以通过点击并拖动光标矩形或按住Shift键同时点击线段来选择多个线条。选择一些线条后,按Delete键(或在 macOS 上按Fn + Delete)将它们从数据中删除:

-
保存对图层所做的更改,并将数据库中现有的表替换为编辑后的副本。在左侧面板上右键单击图层名称,然后选择“另存为数据集”。
-
使用连接下拉菜单选择您的 PostGIS 连接,选择适当的表名,然后确保选中了“创建新表或替换现有表”选项,如图下所示:

- 点击“确定”;你现在已成功在 OpenJUMP 中编辑了 PostGIS 表。
它是如何工作的...
我们在 OpenJUMP 中使用“开放数据存储图层”菜单添加了 PostGIS 图层。这是在创建新连接并输入我们的数据库参数后实现的。
在示例中,添加了人口普查数据,其中包括库耶霍加县的边界。边界的一部分延伸到伊利湖,与加拿大的国际边界相交。虽然技术上正确,但水边界通常不用于实际制图目的。在这种情况下,很容易可视化需要删除哪些数据。
OpenJUMP 使我们能够轻松地查看和删除应从表中删除的记录。选定的线条已被删除,并将表保存到数据库中。
运行数据库查询 – OpenJUMP GIS
在 OpenJUMP 中执行即席查询简单且提供了一些独特功能。可以在特定的数据选择上运行查询,允许手动控制查询区域而不考虑属性。同样,可以即时绘制临时围栏(区域),并在查询中使用表面的几何形状。在本食谱中,我们将探索这些情况中的每一个。
准备工作
如果您需要安装 OpenJUMP 或需要连接到数据库的帮助,请参考前面的食谱。
如何操作...
执行以下步骤以运行数据存储查询:
-
导航到文件 | 运行数据存储查询。从连接下拉菜单中选择 PostGIS 连接,或者如果您未连接到数据库,请使用连接管理器实用程序。
-
我们将通过执行以下查询创建并命名该地区主要河流的多边形图层:
SELECT gid, ST_BUFFER("chp11".lines.geom_sp, 75)
AS the_geom, fullname
FROM "chp11".lines WHERE fullname <> '' AND hydroflg = 'Y'
前面的查询如下截图所示:

上述查询选择了地图上代表水文单元,如"hydroflg" = 'Y'和河流的线条。选定的河流线条(使用州平面几何)被缓冲了 75 英尺,应该会产生如下截图所示的结果:

ST_Buffer函数使用数据投影的单位进行缓冲。因此,如果你的数据仍然具有原始的空间参考EPSG: 4269,你将通过 75 度来缓冲线条,这确实会导致非常奇怪的结果!修改以下 SQL 查询以转换你的几何形状:
SELECT AddGeometryColumn('chp11', 'lines','geom_sp',3734,
'MULTILINESTRING', 2);
UPDATE "chp11".lines SET geom_sp = ST_Transform(geom,3734);
-
然后,你需要回到步骤 2以创建以英尺为单位的缓冲区。
-
接下来,在地图上平移和缩放,找到两个彼此靠近的独立多边形。
-
在主菜单上选择围栏图标,如下截图所示:

-
使用围栏工具在两个未连接的多边形之间绘制一个连接(重叠连接是可以的)。点击一次围栏按钮来创建一个顶点,并在你的多边形完成时双击它。
-
切换到选择特征工具,如下截图所示,并选择你在围栏桥两侧绘制的多边形;通过按住Shift键选择多个特征:

现在你应该在所选多边形之间有一个围栏接点。你应该看到如下截图所示的内容:

-
导航到文件 | 再次运行数据存储查询。
-
这次,我们将使用窗口右侧的按钮来连接。
在选择和围栏上一起运行ST_UNION以填充间隙。我们使用以下查询来完成此操作:
SELECT ST_UNION(geom1, geom2) AS geom
使用选择和围栏按钮代替geom1和geom2,以便你的查询看起来如下截图所示:

- 点击确定,通过关闭主河流和围栏图层来查看查询结果,如下截图所示:

它是如何工作的...
我们使用运行数据存储查询菜单在 OpenJUMP 中添加了 PostGIS 图层的缓冲子集。我们从数据库表中提取了线条并将它们转换为多边形,以便在 OpenJUMP 中查看。
然后,我们手动选择了一个感兴趣的区域,该区域有两个相互分离的代表河流多边形,其想法是河流在自然状态下是相连的,或者现在是相连的。
使用围栏工具在河流之间绘制了一个自由多边形。然后执行了一个union查询,将两个河流多边形和围栏结合起来。围栏允许我们在对数据库表执行空间查询时创建临时表。
添加 PostGIS 图层 – gvSIG
gvSIG 是为西班牙的瓦伦西亚自治区(gv)开发的 GIS 软件包。SIG 是 GIS 在西班牙的对应词。适用于全球使用,gvSIG 提供十多种语言的版本。
准备工作
在以下网站上可以找到 gvSIG 的安装程序、文档以及更多详细信息:
要下载 gvSIG,点击最新版本(撰写本文时为 gvSIG 2.0)。gvSIG 网站上的全包含版本推荐使用。在选择.exe或.bin版本时要小心;否则,你可能会下载到你不懂语言的程序。
如何操作...
可以按照以下步骤添加 GeoDB 图层:
-
在项目管理器部分选择文档类型为“视图”,然后点击“新建”按钮。将打开一个空白视图(画布)。
-
点击菜单栏上的“添加图层”按钮,如图所示:

-
接下来,选择 GeoDB 选项卡,并点击“选择连接”下拉菜单右侧的按钮。
-
在连接参数中输入值,并确保选择“驱动程序”为 PostgreSQLExplorer,如图所示:

-
点击“确定”。所有表格都应出现在“选择表”中。一次可以添加一个或多个表格。你还可以执行以下操作:
-
选择你想要在每个图层中添加的列
-
选择在存在多个几何图形时使用的几何列
-
给每个图层一个独特的名称
-
执行 SQL
WHERE子句查询以加载数据集的子集
-
你可以在以下屏幕截图中看到这些步骤的执行:

- 当你准备好时,点击“确定”。数据将加载到新创建的视图中,如图所示:

工作原理...
使用添加图层菜单将 PostGIS 图层添加到 gvSIG。GeoDB 选项卡允许我们设置 PostGIS 连接。选择一个表后,gvSIG 提供了许多选项。图层名称可以被重命名为更有意义的内容,并且可以从表中省略不必要的列。
添加 PostGIS 图层 – uDig
使用Eclipse构建的用户友好的桌面互联网 GIS(uDig)程序的一个特点是它可以作为一个独立的应用程序或现有应用程序的插件使用。有关 uDig 项目的详细信息以及安装程序,可以在以下网站上找到:
点击前述网站上的“下载”以查看版本和安装程序的列表。撰写本文时,2.0.0.RC1 是最新稳定版本。uDig 支持 Windows、macOS X 和 Linux。
在本菜谱中,我们将快速连接到 PostGIS 数据库,然后向 uDig 添加一个图层。
如何操作...
执行以下步骤:
- 从主菜单导航到文件 | 新图层,如下截图所示:

- 选择 PostGIS 作为数据源并点击下一步按钮继续,如下截图所示:

- 填写您的 PostGIS 连接参数,然后点击下一步按钮,如下截图所示:

- 在数据库下拉菜单中选择目标数据库。点击列表按钮查看数据库中所有具有有效几何形状的表。然后,勾选一个或多个表的复选框以将它们添加为图层,如下截图所示:

- 点击完成,您的数据将被加载到地图窗口中。

它是如何工作的...
uDig 中的新图层菜单生成了一长串可能的源列表,可以添加。PostGIS 被设置为数据库,并输入了您的数据库参数。随后,uDig 连接到了数据库。点击列表会显示连接数据库中可用的总表数。可以一次性添加任意数量的表。
第十二章:位置隐私保护机制简介
在本章中,我们将涵盖以下内容:
-
向位置数据添加噪声以保护
-
在地理查询结果中创建冗余
本章包含了一系列对文档(新闻、法律、学术作品等)的引用,这些引用将在文本中使用[#]格式进行引用。
引言
本章致力于一个新兴的设计和实现基于位置信息系统的议题:LBISs。智能手机在各种应用中的日益普及,以及它们获取和报告用户位置的能力,已被许多服务提供商采纳为核心功能。全天候允许访问用户的准确位置,这为他们的请求提供了上下文,并使公司能够更好地了解他们的客户并提供任何相关的个性化服务;然而,这些信息可能包含关于用户的信息远不止他们想要访问的服务上下文,例如他们的日常作息、常去的地方、聚集的人群等。这些模式可以从手机中获取,然后进行分析并用于对客户进行分类或建立档案;然而,这些信息如果落入错误之手,可能会被用来对付个人。
尽管对如何处理位置信息以保证用户隐私的规定很少[1]甚至没有,但在设计阶段包含适当的政策和实施措施是非常重要的。
幸运的是,在地理隐私研究人员中,存在各种各样的机制,可以用来帮助减轻 LBISs 中的隐私问题。
本章与其他章节有所不同,因为为了理解每种位置隐私技术的背景,我们认为包括支持这些食谱的理论基础是重要的,这些食谱据我们所知只能通过学术出版物获得,尚未作为实际经验呈现。
位置隐私保护机制的定义 – LPPMs
位置隐私可以由 Duckham 和 Kulik 在[2]中定义为:一种特殊的信息隐私,涉及个人决定何时、如何以及到什么程度将有关他们的位置信息传达给他人。根据这个定义,用户应该有权控制他们的位置信息;然而,众所周知,在许多情况下这并不是现实。通常,服务提供商需要完全访问用户的位置信息,以便服务可用。
此外,由于服务提供商可以记录的位置信息质量没有限制,因此获取精确的 GPS 坐标是常见的,即使这与服务本身无关。
LPPMs 的主要目标应该是允许用户隐藏或降低这种位置信息的质量,这样用户仍然可以拥有足够的服务功能,并且服务提供商仍然可以从空间分析产生的洞察中受益。
为了提供地理隐私,理解位置信息组件非常重要,这些组件包括:身份、位置和时间。如果对手能够将这三个方面联系起来,位置隐私就会受到损害。这些组件形成一个位置信息实例;一系列这样的实例提供了历史位置信息,使其他人能够建立行为模式,然后使他们能够识别用户的家庭、工作和日常活动。大多数 LPPMs 至少攻击这些组件中的一个以保护隐私。
假设攻击者获得了用户的身份和时间,但不知道用户访问过哪些地方。由于位置组件已被模糊化,攻击者能够推断的信息非常有限,因为上下文发生了高度改变,数据失去了其潜在的可用性。(这个特定场景对应于位置隐私。)
另一种流行的解决方案是实施身份隐私或匿名,用户旅行路径可以访问,但它们不提供关于受试者身份的信息,甚至无法确定它们是否是不同的用户;然而,仅此信息就足以通过电话簿上的记录匹配来推断一个人的身份,正如[3]实验中所进行的那样。
最后,当指定了用户的位置和身份,但缺少时间组件时,结果信息缺乏上下文,因此路径可能无法准确重建;然而,实现这种发生的模型不太可能,因为请求和 LBS 响应发生在特定时间,延迟查询可能导致它们失去相关性。
LPPMs 的分类
基于位置的服务中的隐私通常被视为在性能和用户隐私之间达到一个理想的权衡;提供的隐私越多,服务在无隐私方案下正常工作的可能性就越小,或者在没有遭受其架构或应用层改变的情况下。由于 LBS 提供了一系列不断变化的功能,这些功能能够跟上用户的需求,同时利用最新的可用技术并适应社会行为,它们为 LPPMs 提供了一个类似的场景,旨在覆盖这些服务。
在主动式位置服务(PLBS)的情况下,用户不断报告他们的位置[4],LPPMs 的目的是尽可能改变路线,同时仍然提供最低程度的准确性,以便 LBS 提供相关信息。这可能具有挑战性,因为许多 PLBS,如交通导航应用程序,需要用户的精确位置。因此,除非原始数据可以恢复或以更改的格式使用,否则这些应用程序实现 LPPM 将非常复杂。其他服务,如地理营销或 FriendFinder,可能可以容忍更大的数据更改,即使更改无法撤销。
另一方面,旨在为反应式位置服务(RLBS)设计的机制通常不需要高精度,因此可以容忍改变主体的位置以提供位置隐私。
一些 LPPMs 需要与常规客户端-服务器架构一起使用特殊功能,例如特殊的数据库结构、额外的数据处理层、第三方服务、代理、特殊电子设备、LBS 用户社区之间的对等连接方法等。
基于此,提出了一种基于 PLBS 和 RLBS 应用对 LPPMs 进行分类的方法。其中一些技术足够通用,可以在两个领域都使用,但每个领域都有不同的影响:

图 1. LPPMs 的分类
在本章中,将展示两个 LPPM 实现的示例:基于噪声的位置模糊化和私有信息检索。这些都会对 LBIS 和地理数据库的设计产生影响。
向位置数据添加噪声以保护隐私
一些为位置隐私保护设计的机制基于位置模糊化,如[5]中所述,是故意降低个人位置信息质量的手段,以保护该个人的位置隐私。
这可能是实现 LBISs 中位置隐私保护的最简单方法,因为它几乎不影响应用程序的服务器端,并且通常容易在客户端实现。另一种实现方式是在服务器端运行,定期处理新数据,或作为应用于每个新条目的函数。
这些技术的主要目标是向手机或其他位置感知设备获取的原始位置添加随机噪声,以降低数据的准确性。在这种情况下,用户通常可以定义他们想要添加的最大和/或最小噪声量。添加的噪声越高,服务质量越低;因此,合理设置此参数非常重要。例如,如果实时跟踪应用程序接收到的数据被 1 公里改变,提供给用户的信息可能不会与实际位置相关。
每种基于噪声的位置模糊技术都提供了一种生成噪声的不同方法:

当噪声以极坐标生成时,它会在圆形区域的投影上更加均匀地分布,因为角度和距离都遵循该分布。在基于笛卡尔坐标的噪声情况下,点似乎在整个区域内均匀生成,导致中心附近的点密度较低。以下图显示了 500 个随机点在圆形和矩形投影中的差异。在这本书中,我们将使用基于极坐标的随机生成:

以下图说明了N-RAND[6]、θ-RAND[7]和Pinwheel[8]技术的工作方式:

N-RAND在给定区域内生成 N 个点,并选择离中心最远的点。Θ-RAND做同样的事情,但在圆形区域的特定扇区。可以选择的领域可能不止一个。最后,Pinwheel机制与N-RAND和θ-RAND不同,因为它不为点生成随机距离,而是为圆周上的每个角度定义一个特定的距离,这使得在生成随机点时选择半径的过程更加确定。在这种情况下,生成过程中的唯一随机变量是角度 α。计算给定角度 α 的半径的公式在(1)中给出,如下所示:

其中 φ 是用户定义的预设参数,它决定了几何形状的翼幅,类似于螺旋桨。
φ 的值越低,螺旋桨的叶片就越多,但那些叶片也会越薄;另一方面,值越高,更宽的叶片数量就越少:

一旦位置被更改,你几乎不可能恢复原始信息;然而,文献中提供了过滤噪声的技术,这些技术可以减少更改的影响,并允许更好地估计位置数据。其中一种基于噪声过滤的机制是一个称为 Tis-Bad [9] 的指数移动平均(EMA)。
关于多少位置信息的退化足以提供给用户位置隐私,以及当访问 LBS 时,由此产生的模糊信息是否仍然有用,仍然存在开放讨论。毕竟,在进行地理空间分析时获得相关响应是 LBS 和地理参考数据研究的主要问题之一。
准备工作
在这个菜谱中,我们将创建实现三种基于噪声的混淆机制的 PLPGSQL 函数:Rand、N-Rand 和 Pinwheel。然后我们将为表创建一个触发函数以更改所有新插入的点。对于本章,我们将重用第三章,处理矢量数据——基础知识中使用的rk_track_points数据集。
在这个菜谱中,我们将使用ST_Project函数向单个点添加噪声。然后,我们将比较原始数据与 QGIS 中的混淆数据。最后,我们将展示噪声过滤对混淆数据的影响。
您还需要将data/chp03/runkeeper-gpx.zip文件提取到working/chp12/runkeeper_gpx。
在菜谱中,我们将使用与第三章,处理矢量数据——基础知识中相同的步骤,但针对一个新的模式。
首先,请确保您需要导入到 PostGIS 中的.gpx文件的格式。打开其中一个文件并检查文件结构——每个文件都必须是 XML 格式,由一个<trk>元素组成,该元素包含一个<trkseg>元素,该元素包含多个<trkpt>元素(从跑步者的 GPS 设备存储的点)。
如何做到这一点...
执行以下步骤以创建函数:
- 使用以下命令创建一个名为
chp12的新模式以存储本章中所有菜谱的数据:
postgis_cookbook=# create schema chp12;
Rand的实现需要创建一个 PLPGSQL 函数,该函数接收radius参数,它定义了最大距离,以及要更改的几何the_geom。
ST_Project函数将点移动到其原始位置给定距离和角度的位置。为了简化表达式,我们将使用极坐标噪声生成。执行以下 SQL 命令:
postgis_cookbook=# CREATE OR REPLACE
FUNCTION chp12.rand(radius numeric, the_geom geometry)
returns geometry as $$
BEGIN
return st_Project(the_geom, random()*radius,
radians(random()*360));
END;
$$
LANGUAGE plpgsql;
N-Rand的实现需要n参数,即查找从原始点最长距离的尝试次数,以及radius参数,它定义了最大距离,以及要更改的几何the_geom。执行以下 SQL 命令:
postgis_cookbook=# CREATE OR REPLACE FUNCTION chp12.nrand(n integer,
radius numeric, the_geom geometry)
returns geometry as $$
DECLARE
tempdist numeric;
maxdist numeric;
BEGIN
tempdist := 0;
maxdist := 0;
FOR i IN 1..n
LOOP
tempdist := random()*radius;
IF maxdist < tempdist THEN
maxdist := tempdist;
END IF;
END LOOP;
return st_Project(the_geom,maxdist, radians(random()*360));
END;
$$
LANGUAGE plpgsql;
Pinwheel的实现需要n参数,即查找从原始点最长距离的尝试次数,以及radius参数,它定义了最大距离,以及要更改的几何the_geom。执行以下 SQL 命令:
postgis_cookbook=# CREATE OR REPLACE FUNCTION chp12.pinwheel
(theta numeric, radius numeric, the_geom geometry)
returns geometry as $$
DECLARE
angle numeric;
BEGIN
angle = random()*360;
return st_Project(the_geom,mod(
CAST(angle as integer), theta)/theta*radius, radians(angle));
END;
$$
LANGUAGE plpgsql;
- 现在,我们将复制第三章中的一部分步骤,处理矢量数据——基础知识,但针对
chp12模式。通过执行以下命令行在 PostgreSQL 中创建chp12.rk_track_points表:
postgis_cookbook=# CREATE TABLE chp12.rk_track_points
(
fid serial NOT NULL,
the_geom geometry(Point,4326),
ele double precision,
"time" timestamp with time zone,
CONSTRAINT activities_pk PRIMARY KEY (fid)
);
- 例如,让我们使用
nrand函数为rk_track_points表中所有新插入的点创建触发器。为了模拟这种情况,我们将创建一个新表,我们将使用它。
此函数将返回一个新的几何体:
CREATE OR REPLACE FUNCTION __trigger_rk_track_points_before_insert(
) RETURNS trigger AS $__$
DECLARE
maxdist integer;
n integer;
BEGIN
maxdist = 500;
n = 4;
NEW.the_geom = chp12.nrand(n, maxdist, NEW.the_geom);
RETURN NEW;
END;
$__$ LANGUAGE plpgsql;
CREATE TRIGGER rk_track_points_before_insert
BEFORE INSERT ON chp12.rk_track_points FOR EACH ROW
EXECUTE PROCEDURE __trigger_rk_track_points_before_insert();
- 创建以下脚本,使用 GDAL 的
ogr2ogr命令导入chp12.rk_track_points表中的所有.gpx文件。
以下是在 Linux 版本(命名为 working/chp03/import_gpx.sh):
#!/bin/bash
for f in `find runkeeper_gpx -name \*.gpx -printf "%f\n"`
do
echo "Importing gpx file $f to chp12.rk_track_points
PostGIS table..." #, ${f%.*}"
ogr2ogr -append -update -f PostgreSQL
PG:"dbname='postgis_cookbook' user='me' password='mypassword'"
runkeeper_gpx/$f -nln chp12.rk_track_points
-sql "SELECT ele, time FROM track_points"
done
以下是在 Windows 版本(命名为 working/chp03/import_gpx.bat):
@echo off
for %%I in (runkeeper_gpx\*.gpx*) do (
echo Importing gpx file %%~nxI to chp12.rk_track_points
PostGIS table...
ogr2ogr -append -update -f PostgreSQL
PG:"dbname='postgis_cookbook' user='me' password='mypassword'"
runkeeper_gpx/%%~nxI -nln chp12.rk_track_points
-sql "SELECT ele, time FROM track_points"
)
- 在 Linux 中,在运行之前不要忘记为其分配执行权限。运行以下脚本:
$ chmod 775 import_gpx.sh
$ ./import_gpx.sh
Importing gpx file 2012-02-26-0930.gpx to chp12.rk_track_points
PostGIS table...
Importing gpx file 2012-02-29-1235.gpx to chp12.rk_track_points
PostGIS table...
...
Importing gpx file 2011-04-15-1906.gpx to chp12.rk_track_points
PostGIS table...
在 Windows 中,双击 .bat 文件,或使用以下命令从命令提示符运行它:
> import_gpx.bat
- 一旦调用插入命令,触发器将调用
nrand函数,修改行中的传入几何形状并存储数据的新的版本。如果我们比较原始表chp03.rk_track_points的前 10 个值与chp12.rk_track_points,可以看出它们略有不同,这是由于添加的噪声造成的。执行以下查询以查看结果:
select ST_ASTEXT(rk.the_geom), ST_ASTEXT(rk2.the_geom)
from chp03.rk_track_points as rk, chp12.rk_track_points as rk2
where rk.fid = rk2.fid
limit 10;
查询的结果如下:

- 为了评估数据中噪声的影响,我们将创建两个表来存储具有不同噪声级别的模糊化数据:500 米和 1 公里。我们将使用先前定义的函数
rand。执行以下 SQL 命令以创建表:
CREATE TABLE chp12.rk_points_rand_500 AS (
SELECT chp12.rand(500, the_geom)
FROM chp12.rk_track_points
);
CREATE TABLE chp12.rk_points_rand_1000 AS (
SELECT chp12.rand(1000, the_geom)
FROM chp12.rk_track_points
);
- 在 QGIS 或您喜欢的桌面 GIS 中加载表。以下图显示了原始数据和通过 500 米和 1 公里模糊化的点的比较:

它是如何工作的...
在这个菜谱中,我们应用了三种基于噪声的位置模糊化机制:Rand、N-Rand 和 Pinwheel,为每种方法在 PLPGSQL 中定义了 PostgreSQL 函数。我们使用其中一个函数在触发器中自动修改传入的数据,这样在用户端的应用程序上就不需要做出任何更改。此外,我们还展示了通过比较修改后的数据的两个版本来展示噪声的影响,这样我们可以更好地理解配置噪声设置的影响。
在以下菜谱中,我们将查看基于私有信息检索的 LPPM 的实现。
在地理查询结果中创建冗余
私有信息检索(PIR)LPPMs 通过将空间上下文映射以提供一种私密的方式来查询服务,而不会泄露任何第三方可能获得的位置信息。
根据第 [9] 节,基于 PIR 的方法可以分为基于密码学或基于硬件的方法。基于硬件的方法使用一种特殊的 安全协处理器(SC),它作为安全保护的空间,PIR 查询以不可解密的方式处理,如[10]所述。基于密码学的技术仅使用逻辑资源,并且不需要服务器或客户端的特殊物理配置。
在[10]中,作者提出了一种混合技术,通过使用不同大小的网格希尔伯特曲线来限制基于密码学的 PIR 算法的通用搜索域;然而,根据他们的实验,PIR 在数据库上的处理仍然昂贵,对于用户定义的隐私级别来说并不实用。这是因为该方法不允许用户指定伪装网格单元格的大小,一旦整个网格计算完成,也不能更改;换句话说,无法向系统中添加新的兴趣点。其他技术可以在[12]中找到。
PIR 也可以与其他技术结合使用,以增加隐私级别。一种兼容的 LPPM 是模拟查询技术,为更大搜索区域(例如城市、县、州)内的任意位置生成一组随机虚假或模拟查询[13],[14]。其目的是隐藏用户实际想要发送的位置。
模拟查询技术的缺点主要是用户和服务器端发送和处理大量请求的整体成本。此外,其中一个查询将包含用户原始的确切位置和兴趣点,因此可以根据用户的查询记录追踪原始轨迹——尤其是在生成模拟数据时没有应用智能的情况下。在[15]中讨论了对此方法的改进,其中不是为每个点发送单独的查询,而是将所有模拟和真实位置以及用户指定的位置兴趣点一起发送。在[16]中,作者提出了一种方法来避免为每个迭代随机生成点,这应该会减少检测真实点趋势的可能性;但此技术在为每个模拟路径生成轨迹时需要大量设备资源,为每条路径生成单独的查询,并且仍然揭示了用户的位置。
本书作为示例介绍的 LPPM 是 MaPIR——基于地图的 PIR[17]。这是一种将映射技术应用于提供用户和服务器之间的通用语言的方法,并且还能够为单个查询提供冗余答案,而不会在服务器端产生开销,这反过来可以减少对地理查询的使用,从而提高响应时间。
此技术创建了一个特定区域的冗余地理映射,使用实际坐标的 PoI 在不同搜索尺度上生成 ID。在 MaPIR 论文中,用于查询的坐标的十进制位数。接近赤道时,每个数字可以近似表示一定的距离,如下面的图所示:

这可以概括为说,在较大的比例尺下(接近位置的整数部分),附近的地点将看起来很近,但在较小的比例尺下不一定如此。它也可能将相对较远的点显示得似乎更近,如果它们共享相同的数字集(纬度和经度的第 n 位数字)。
一旦获得了数字,根据所选的比例尺,需要一个映射技术将数字减少到单个 ID。在纸上,应用了一个简单的伪随机函数将二维域减少到一维:
ID(Lat_Nth, Lon_Nth) = (((Lat_Nth + 1) * (Lon_Nth + 1)) mod p) - 1
在前面的方程中,我们可以看到p是比最大期望 ID 大的下一个素数。鉴于论文中的最大 ID 是9,p的值是11。应用此函数后,最终的地图看起来如下:

下图显示了一个示例PoI ID,它代表位于10.964824,-74.804778的餐厅。最终的映射网格单元将是2,6,和1,分别使用比例尺 k = 3,2,和 1。
这些信息可以存储在数据库中的特定表中,或者按照 DBA 为应用程序确定的最佳方式:

基于这个结构,用户生成的查询将需要定义搜索的比例尺(在 100 米内,1 公里等),他们要寻找的业务的类型,以及他们所在的网格单元。服务器将接收参数并查找与用户相同单元格 ID 的所有餐厅。结果将返回位于具有相同 ID 的单元格中的所有餐厅,即使它们离用户不远。鉴于单元格是不可区分的,如果攻击者获得了服务器的日志访问权限,他们将只会看到用户在 10 个单元格 ID 中的 1 个。当然,一些 ID 可能位于不可居住的地区(如森林或湖泊),但总会有一定程度的冗余。
准备工作
在这个菜谱中,我们将重点关注 MaPIR 技术的实现,作为一个基于 PIR 和虚拟查询的 LPPM 的例子。为此,我们将一个小型超市数据集加载到数据库中作为 PoIs。这些点将按照 MaPIR 中解释的方式进行处理和存储,然后由用户进行查询。
数据集是从哥伦比亚开放数据平台Datos Abiertos通过以下链接获得的:
www.datos.gov.co/Comercio-Industria-y-Turismo/Mapa-supermercados-Guadalajara-de-Buga/26ma-3v68
数据集中的点在以下图中展示:

如何做到这一点...
在前面的菜谱中,我们创建了临时表来存储原始数据,以及包含要稍后由用户查询的 MaPIR 信息的表。以下步骤允许其他用户访问这些表:
- 首先,创建名为
supermarkets的表来存储从数据集中提取的信息,以及名为supermarkets_mapir的表来存储每个超市登记处的 MaPIR 相关信息。执行以下命令:
CREATE TABLE chp12.supermarkets (
sup_id serial,
the_geom geometry(Point,4326),
latitude numeric,
longitude numeric,
PRIMARY KEY (sup_id)
);
CREATE TABLE chp12.supermarkets_mapir (
sup_id int REFERENCES chp12.supermarkets (sup_id),
cellid int,
levelid int
);
- 现在,创建一个触发函数,该函数将应用于
supermarkets表中插入的所有新登记项,以便新登记项将被插入到supermarkets_mapir表中,计算cellid和levelid值。以下代码将创建该函数:
CREATE OR REPLACE FUNCTION __trigger_supermarkets_after_insert(
) RETURNS trigger AS $__$
DECLARE
tempcelliD integer;
BEGIN
FOR i IN -2..6
LOOP
tempcellid = mod((mod(CAST(TRUNC(ABS(NEW.latitude)*POWER(10,i))
as int),10)+1) * (mod(CAST(TRUNC(ABS(NEW.longitude)*POWER(10,i))
as int),10)+1), 11)-1;
INSERT INTO chp12.supermarkets_mapir (sup_id, cellid, levelid)
VALUES (NEW.sup_id, tempcellid, i);
END LOOP;
Return NEW;
END;
$__$ LANGUAGE plpgsql;
CREATE TRIGGER supermarkets_after_insert
AFTER INSERT ON chp12.supermarkets FOR EACH ROW
EXECUTE PROCEDURE __trigger_supermarkets_after_insert ();
- 由于数据集没有正确组织,我们提取了超市的位置信息并构建了以下查询。执行后,
supermarkets和supermarkets_mapir两个表都应该被填充。执行以下命令:
INSERT INTO chp12.supermarkets (the_geom, longitude, latitude) VALUES
(ST_GEOMFROMTEXT('POINT(-76.304202 3.8992)',4326),
-76.304202, 3.8992),
(ST_GEOMFROMTEXT('POINT(-76.308476 3.894591)',4326),
-76.308476, 3.894591),
(ST_GEOMFROMTEXT('POINT(-76.297893 3.890615)',4326),
-76.297893, 3.890615),
(ST_GEOMFROMTEXT('POINT(-76.299017 3.901726)',4326),
-76.299017, 3.901726),
(ST_GEOMFROMTEXT('POINT(-76.292027 3.909094)',4326),
-76.292027, 3.909094),
(ST_GEOMFROMTEXT('POINT(-76.299687 3.888735)',4326),
-76.299687, 3.888735),
(ST_GEOMFROMTEXT('POINT(-76.307102 3.899181)',4326),
-76.307102, 3.899181),
(ST_GEOMFROMTEXT('POINT(-76.310342 3.90145)',4326),
-76.310342, 3.90145),
(ST_GEOMFROMTEXT('POINT(-76.297366 3.889721)',4326),
-76.297366, 3.889721),
(ST_GEOMFROMTEXT('POINT(-76.293296 3.906171)',4326),
-76.293296, 3.906171),
(ST_GEOMFROMTEXT('POINT(-76.300154 3.901235)',4326),
-76.300154, 3.901235),
(ST_GEOMFROMTEXT('POINT(-76.299755 3.899361)',4326),
-76.299755, 3.899361),
(ST_GEOMFROMTEXT('POINT(-76.303509 3.911253)',4326),
-76.303509, 3.911253),
(ST_GEOMFROMTEXT('POINT(-76.300152 3.901175)',4326),
-76.300152, 3.901175),
(ST_GEOMFROMTEXT('POINT(-76.299286 3.900895)',4326),
-76.299286, 3.900895),
(ST_GEOMFROMTEXT('POINT(-76.309937 3.912021)',4326),
-76.309937, 3.912021);
- 现在,所有插入到
supermarket表中的超市都将有它们在supermarkets_mapir表中的 MaPIR 相关信息。以下查询将说明为给定登记项存储在supermarkets_mapir表中的信息:
SELECT * FROM supermarkets_mapir WHERE sup_id = 8;
查询的结果如下表所示:

-
现在,超市数据已经准备好了,假设用户位于坐标
(-76.299017, 3.901726),这符合某个超市的位置,并且他们想使用比例尺 2(对应于赤道附近大约 1 km² 的网格单元大小)。 -
移动应用应生成一个查询,请求
levelid = 2和cellid = 9,这些值是从latitude = 0和longitude = 9的第二位十进制数字计算得出的。这个计算可以在之前显示的映射表中验证,其中Lat Nth +1 = 1和Long Nth + 1 = 10:
SELECT sm.the_geom AS the_geom
FROM chp12.supermarkets_mapir AS smm, chp12.supermarkets AS sm
WHERE smm.levelid = 2 AND smm.cellid = 9 AND smm.sup_id = sm.sup_id;
注意,查询中不再需要任何地理信息,因为映射是在预处理阶段完成的。这减少了查询时间,因为它不需要使用复杂的内部函数来确定距离;然而,映射不能保证返回所有附近的结果,因为相邻单元中具有不同 ID 的结果可能不会出现。在以下图中,你可以看到之前查询中的超市(黑色)没有包括一些靠近用户位置(白色,靠近箭头)的超市。可以应用一些可能的对策来解决这个问题,例如对网格单元边缘附近的一些元素进行双重映射:

它是如何工作的...
在这个菜谱中,我们实现了一个使用 PIR 和一个名为 MaPIR 的虚拟查询的 LPPM。它为兴趣点创建了一个映射函数,使我们能够使用不同的比例尺进行查询。它还包括了冗余答案,提供隐私保护,因为它没有透露用户的实际位置。
计算数据集映射的过程应存储在用于用户查询的表中。在 MaPIR 论文中,尽管有多个结果,但 MaPIR 查询的执行时间比基于距离的地理查询时间少了一半。
参考文献
-
欧洲联盟关于隐私和电子通信的指令,2002 年。
-
M. Duckham 和 L. Kulik,《位置隐私和位置感知计算》,动态移动 GIS 研究变化空间,第 3 卷,第 35-51 页,2006 年。
-
J. Krumm,《对位置轨迹的推理攻击》,收录于《普适计算》。Springer,2007 年,第 127-143 页。
-
M. A. Labrador,A. J. Perez,和 P. Wightman,《基于位置的信息系统:开发实时跟踪应用》。波卡罗顿:CRC 出版社,2011 年。
-
M. Duckham 和 L. Kulik,《位置隐私和位置感知计算的形式模型》,收录于《普适计算》。Springer,2005 年,第 152-170 页。
-
P. Wightman,W. Coronell,D. Jabba,M. Jimeno,和 M. Labrador,《基于位置信息系统的位置模糊化技术隐私评估》,收录于《通信(LATINCOM)第 2011 年 IEEE 拉丁美洲会议》,第 1-6 页。
-
P. Wightman,M. Zurbarán,E. Zurek,A. Salazar,D. Jabba,和 M. Jimeno,《基于圆扇区的随机噪声位置模糊化:θ-Rand》,收录于 IEEE 国际工业电子与应用研讨会(ISIEA),2013 年。
-
P. Wightman,M. Zurbarán,和 A. Santander,《高变异性地理模糊化以保护位置隐私》,2013 年第 47 届国际卡纳汉安全技术会议(ICCST),麦德林,2013 年,第 1-6 页。
-
A. Labrador,P. Wightman,A. Santander,D. Jabba,M. Jimeno,《基于时间序列的解模糊化算法:Tis-Bad》,收录于《工程研究与创新》。西蒙·玻利瓦尔大学。第 3 卷(1),第 1-8 页。2015 年。
-
A. Khoshgozaran,H. Shirani-Mehr,和 C. Shahabi,《SPIRAL:一种可扩展的私有信息检索方法以保护位置隐私》,收录于移动数据管理研讨会,2008 年。MDMW 2008。第九届国际会议,第 55-62 页。
-
F. Olumofin,P. K. Tysowski,I. Goldberg,和 U. Hengartner,《实现基于位置服务的有效查询隐私》,收录于《隐私增强技术》,2010 年,第 93-110 页。
-
G. Ghinita,P. Kalnis,A. Khoshgozaran,C. Shahabi,和 K. Tan,《基于位置服务的私有查询:匿名化器不是必要的》,收录于 2008 年 ACM SIGMOD 国际数据管理会议论文集,第 121-132 页。
-
D. Quercia,I. Leontiadis,L. McNamara,C. Mascolo,和 J. Crowcroft,《如果你能 SpotME:在手机上的位置模糊化随机响应》,收录于《分布式计算系统(ICDCS)第 31 届国际会议》,2011 年,第 363-372 页。
-
H. Kido,Y. Yanagisawa,和 T. Satoh,使用替身的位置服务匿名通信技术,在普适服务,2005. ICPS '05. 国际会议论文集,第 88-97 页。
-
H. Lu,C. S. Jensen,和 M. L. Yiu,Pad: 移动服务中的隐私区域感知、基于替身的位置隐私,在第七届 ACM 国际无线和移动访问数据工程研讨会论文集,第 16-23 页。
-
P. Shankar,V. Ganapathy,和 L. Iftode(2009 年 9 月),使用 sybilquery 对基于位置的服务进行隐私查询。在 2009 年第 11 届国际普适计算会议论文集中,第 31-40 页。
-
P. M. Wightman, M. Zurbarán, M. Rodríguez, 和 M. A. Labrador,MaPIR: 基于映射的隐私信息检索在 LBISs 中的位置隐私,38 届 IEEE 年度局部计算机网络会议 - 工作坊,悉尼,新南威尔士州,2013 年,第 964-971 页。






















浙公网安备 33010602011771号