Python-地理空间分析秘籍-全-
Python 地理空间分析秘籍(全)
原文:
zh.annas-archive.org/md5/e0f13854535dc71e09f81e2e942f7465译者:飞龙
前言
地理空间分析并不特殊;与其他类型的分析(如金融市场分析)相比,它只是有所不同。我们处理几何对象,如线、点和多边形,并将这些几何形状与属性(如业务数据)连接起来。我们提出“在哪里”的问题,例如“最近的酒吧在哪里?”“我的所有客户都位于哪里?”“我的竞争对手位于哪里?”其他位置问题包括,“这栋新建筑会投射到公园上吗?”“去学校的最短路线是什么?”“我孩子去学校最安全的路线是什么?”“这栋建筑会阻挡我观看山景的视线吗?”“在哪里建造我的下一家商店是最优的位置?”确定消防车从其站点出发 5 分钟、10 分钟或 20 分钟内可以到达的区域,等等。
所有这些问题共同的一点是,您需要知道某些对象的位置才能回答它们。没有空间组件,您无法回答这些问题,这正是地理空间分析的全部内容。
地理空间特征相互叠加,模式和趋势很容易识别。这种看到模式或趋势的能力是地理空间分析最简单的形式。
在本书中,提供了简单和复杂的代码配方作为小型工作模型,这些模型可以轻松集成或扩展到更大的项目或模型中。
分析是 GIS 中有趣的部分,涉及可视化关系、识别趋势,以及看到在电子表格中不可见的模式。
Python 编程语言简洁、清晰、简洁,非常适合初学者。它还拥有高级功能,帮助专业人士快速编码解决复杂问题的解决方案。Python 使专家或初学者处理地理空间数据时的可视化变得快速且简单。就这么简单。
本书涵盖的内容
第一章,设置您的地理空间 Python 环境,探讨了如何一次性设置您的计算机以处理所有软件需求,例如 pyproj、NumPy 和 Shapely。本章满足了您在 Windows 和 Linux 上启用空间分析或地理处理的全部开发软件需求。
第二章,处理投影,解释了如何处理投影或未投影的空间数据。您可以学习和发现如何将数据转换为正确的投影,以便为分析做准备。
第三章,将空间数据从一个格式转换为另一个格式,解释了地理空间数据以许多不同的格式出现,以及从一种格式到另一种格式的消息数据是日常工作的例行公事。在本章中,您将了解最常见的数据管理任务。
第四章, 使用 PostGIS,展示了我们的大多数地理空间数据是如何存储在空间数据库中的,以及如何使用 Python 访问、操作这些数据,这正是本章的主题。
第五章, 矢量分析,介绍了一种非常常见的地理空间数据格式,即矢量数据格式。为了在矢量数据上执行分析功能,我们将探索通过捕捉、裁剪、切割和叠加矢量数据集来创建新数据的模式,然后确定 3D 地面距离和总高程增益。
第六章, 叠加分析,解释了如何通过叠加两组数据来创建新数据,从而结合空间数据。
第七章, 栅格分析,展示了如何创建高程剖面和快速合并图像的方法,以在你的数据上执行栅格分析功能。
第八章, 网络路由分析,展示了寻找最近的事物是一个常见的地理空间分析功能。本章将揭示如何解决室内网络类型问题,并演示一些在建筑物内进行寻路的一些常见用例。
第九章, 拓扑检查和数据验证,涵盖了数据质量和连接。在本章中,你将学习如何使用自定义拓扑函数验证你的数据以查找错误。
第十章, 可视化你的分析,解释了地理空间数据天生具有可视化特性,你将了解如何在网络地图和 3D 网络上展示你的分析。
第十一章, 使用 GeoDjango 进行网络分析,基于第八章, 网络路由分析,其中你将创建一个室内路由网络应用程序。你将能够轻松地将一个人从 A 点路由到建筑内的 B 点,使用真实的 3D 网络数据。这些关键特性将通过汇集你迄今为止所学食谱的所有部分来展示。
附录 A, 其他地理空间 Python 库,解释了 Python 如何与地理空间库蓬勃发展,你还将找到许多用于数据分析的流行库列表,无论它们是否具有空间性。这可能会激发你的兴趣。
附录 B, 地图图标库,快速概述了在 Python 地理空间工作环境中扮演特殊角色的图标库。
你需要这本书的内容
要使用这本书,您应该熟悉编程语言 Python 以及编程中涉及的概念。这意味着如果您还没有安装,您应该能够在您的机器(Windows、Linux 或 OS X)上安装 Python 2.7.x。与 GIS(地理信息系统)相关的概念肯定有帮助,但不是必需的。这本书的入门书籍可以是使用 Python 进行地理空间分析入门,作者 Joel Lawhead或Python 地理空间开发,作者 Eric Westra,均由Packt Publishing出版。
适合这本书的读者
如果你是学生、教师、程序员、地理空间或 IT 管理员、GIS 分析师、研究人员或科学家,想要学习空间分析,那么这本书适合你。任何试图回答简单到复杂空间分析问题的人,都将通过实际数据帮助,了解 Python 的强大功能。你们中的一些人可能是初学者,但大多数人可能已经对地理空间分析和编程有了一定的了解。
部分
在这本书中,您会发现一些经常出现的标题(准备工作、如何操作...、工作原理...、更多内容...和相关信息)。
为了清楚地说明如何完成食谱,我们使用以下章节:
准备工作
本节将向您介绍在食谱中可以期待的内容,并描述如何设置任何软件或食谱所需的任何初步设置。
如何操作…
本节包含遵循食谱所需的步骤。
工作原理…
本节通常包含对前一个章节发生情况的详细解释。
更多内容…
本节包含有关食谱的附加信息,以便使读者对食谱有更深入的了解。
相关内容
本节提供了指向其他有用信息的链接,这些信息对食谱很有帮助。
惯例
在这本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词显示如下:“如果由于某种原因workon没有启动您的虚拟环境,您可以通过从命令行执行source /home/mdiener/.venvs/pygeoan_cb/bin/activate来简单地启动它。”
代码块按照以下方式设置:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from osgeo import ogr
shp_driver = ogr.GetDriverByName('ESRI Shapefile')
shp_dataset = shp_driver.Open(r'../geodata/schools.shp')
shp_layer = shp_dataset.GetLayer()
shp_srs = shp_layer.GetSpatialRef()
print shp_srs
任何命令行输入或输出都按照以下方式编写:
$ sudo apt-get install python-setuptools python-pip
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“选择路由到:并输入2以查看二楼选项。”
注意
警告或重要注意事项以如下框的形式出现。
小贴士
技巧和窍门如下所示。
读者反馈
我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中受益的标题非常重要。
要发送一般反馈,只需将电子邮件发送到<feedback@packtpub.com>,并在邮件主题中提及书籍标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的骄傲拥有者,我们有众多事项可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。这些彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/5079OS_ColorImage.pdf下载此文件。
勘误
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/support,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分中。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。
我们感谢您在保护我们的作者以及为我们提供有价值内容的能力方面的帮助。
问答
如果您在本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。
第一章. 设置你的地理空间 Python 环境
在本章中,我们将涵盖以下主题:
-
安装 virtualenv 和 virtualenvwrapper
-
安装 pyproj 和 NumPy
-
安装 shapely、matplotlib 和 descartes
-
安装 pyshp、geojson 和 pandas
-
安装 SciPy、PySal 和 IPython
-
安装 GDAL 和 OGR
-
使用 PostGIS 安装 GeoDjango 和 PostgreSQL
简介
本章将为你完成一些基础工作,以便你可以自由地、积极地完成本书中的所有菜谱。我们将从安装你将使用的每个库开始。一旦每个步骤完成,我们将测试每个库的安装以确保其工作。由于本书面向那些已经在处理空间数据的人,如果你已经安装了这些库,你可以跳过本章。如果没有,你将在这里找到有用的安装说明作为参考。
Python 库的选择基于行业验证的可靠性和功能性。Python 库中的众多功能导致了众多顶级桌面 GIS 系统上 GIS 支持的蓬勃发展,例如 QGIS 和 ESRI ArcGIS。
本书还包括一个 installer.sh bash 文件。installer.sh 文件可用于使用 pip 和其他依赖项通过 apt-get 命令安装你的虚拟环境中可用的 Python 库。installer.sh bash 文件从命令行执行,几乎一次性安装几乎所有内容,所以请查看它。对于那些第一次使用 Python 的你,请遵循本章中的说明,你的机器将被设置以完成不同的菜谱。
对于高级用户来说,安装有时可能很棘手,所以你会在本章中找到一些最常见的陷阱和连接方法。
这些菜谱的开发是在一个全新的 Linux/Ubuntu 14.04 机器上完成的。因此,除非另有说明,否则代码示例是针对 Linux/Ubuntu 的,必要时会有 Windows 的注意事项。
安装 virtualenv 和 virtualenvwrapper
这个菜谱将使你能够管理多个项目中不同库的不同版本。我们使用 virtualenv 来创建虚拟 Python 环境,在隔离的目录中托管项目特定的库集合。例如,你可能有一个使用 Django 1.4 的旧遗留项目,而一个新的项目则需要你使用 Django 版本 1.8。使用 virtualenv,你可以在同一台机器上安装这两个版本的 Django,并且每个项目都可以访问适当的 Django 版本,而不会出现任何冲突或问题。
没有使用 virtualenv,你被迫要么升级旧项目,要么找到一种方法来实现其他版本的新的功能,因此限制了或复杂化了新项目。
virtualenv 允许你轻松地在不同的 Python 虚拟环境中切换你的个人项目。这还有一个额外的好处,就是你可以轻松快速地设置一台新机器进行测试,或者帮助新开发者尽快将机器配置好。
准备工作
在做任何事情之前,我们假设你已经有一个运行 Linux/Ubuntu 的机器或一个 virtualbox 实例,这样你就可以遵循这些说明。
小贴士
我还建议尝试使用 Vagrant (www.vagrantup.com),它使用 virtualbox 来封装和标准化你的开发环境。
Ubuntu 14.04 预装了 Python 2.7.6 和 Python 3.4;其他库的安装责任由以下章节解释。
Windows 用户需要从 Python 主页 www.python.org/downloads/windows/ 下载并安装 Python 2.7.x;请下载 2.7.x 系列的最新版本,因为本书是以 2.7.X 为前提编写的。安装程序包含 pip 的捆绑版本,所以请确保安装它!
仔细查看正确的版本以下载,确保你获得的是 32 位 或 64 位 下载。你不能混合使用版本,所以请小心,并记住安装正确的版本。
可以在 www.lfd.uci.edu/~gohlke/pythonlibs/ 找到其他类型 Windows 二进制文件的优秀网站。Wheel 文件是安装的新规范,可以从命令行执行,如下所示:
python pip install libraryName.whl
注意
在 Windows 上,请确保你的 Python 解释器已设置在系统路径中。这样,你就可以通过命令提示符使用 C:\Users\Michael> python filename.py 命令直接调用 Python。如果你需要更多帮助,可以通过遵循在线说明之一来获取信息,信息可以在 pypi.python.org/pypi/pip 找到。
从 Python 2.7.9 及更高版本开始,pip 在安装时可用。
使用 Python 3 会很棒,对于许多 Python GIS 库来说,它已经准备好展示其功能。不幸的是,并非所有 GIS 库都与 Python 3 (pyproj) 兼容,正如在撰写本文时人们所期望的那样。如果你想尝试 Python 3.x,请随意尝试。一个检查库兼容性的优秀网页可以在 caniusepython3.com/ 找到。
要安装 virtualenv,您需要有一个运行的 Python 和 pip 安装。pip 软件包管理器管理和安装 Python 软件包,使我们的生活更轻松。在这本书的整个过程中,如果我们需要安装一个软件包,pip 将是我们完成这项工作的首选工具。pip 的官方安装说明可以在 pip.pypa.io/en/latest/installing.html 找到。要从命令行安装 pip,我们首先需要安装 easy_install。让我们在终端中试试:
$ sudo apt-get install python-setuptools python-pip
这一行代码就安装了 pip 和 easy_install。
注意
什么是 sudo?
sudo 是一个用于类 Unix 计算机操作系统的程序,允许用户以其他用户的权限(通常是超级用户或 root)运行程序。其名称是 su(substitute user)和 do(take action)的组合。查看 en.wikipedia.org/wiki/Sudo 了解有关 sudo 的更多信息。
命令 sudo 表示以超级用户权限运行执行。如果失败,您将需要获取 ez_setup.py 文件,该文件可在 bootstrap.pypa.io/ez_setup.py 找到。下载文件后,您可以从命令行运行它:
$ python ez_setup.py
现在 pip 应该已经启动并运行,您可以执行命令来完成 virtualenv 和 virtualenvwrapper 的安装。virtualenvwrapper 创建了快捷方式,这是创建或删除虚拟环境的更快方式。您可以按照以下方式测试它:
$ pip install virtualenv
如何做到这一点...
安装您的 Python virtualenv 和 virtualenvwrapper 软件包的步骤如下:
-
使用 pip 安装程序安装
virtualenv:$ sudo pip install virtualenv -
使用
easy_install安装virtualenvwrapper:$ sudo easy_install virtualenvwrapper注意
我们使用
easy_install而不是pip,因为在 Ubuntu 14.04 中,virtualenvwrapper.sh文件不幸地没有位于/usr/local/bin/virtualenvwrapper.sh,而根据在线文档,它应该在那里。 -
将
WORKON_HOME变量分配给您的家目录,文件夹名称为venvs。创建一个文件夹,用于存储您所有的不同 Python 虚拟环境;在我的情况下,该文件夹位于/home/mdiener/venvs:$ export WORKON_HOME=~/venvs $ mkdir $WORKON_HOME -
运行 source 命令以执行
virtualenvrapper.shbash 文件:$ source /usr/local/bin/virtualenvwrapper.sh -
接下来,我们创建一个新的虚拟环境,命名为
pygeoan_cb,这也是虚拟环境安装的新文件夹名称:$ mkvirtualenv pygeoan_cb为了在下次启动计算机时使用
virtualenvwrapper,我们需要设置它,以便您的 bash 终端在计算机启动时运行virtualenvwrapper.sh脚本。 -
首先,将其放入您的
~/.bashrc文件中:$ echo "export WORKON_HOME=$WORKON_HOME" >> ~/.bashrc -
接下来,我们将导入 bash 中的
virtualenvwrapper函数:$ echo "source /usr/local/bin/virtualenvwrapper.sh" >> ~/.bashrc -
现在,我们可以执行我们的 bash:
$ source ~/.bashrc
它是如何工作的...
第一步展示了 pip 如何将virtualenv包安装到您的系统级 Python 安装中。第二步展示了如何使用easy_install安装virtualenvwrapper辅助包,因为virtualenvwrapper.sh文件不是使用 pip 安装程序创建的。这将帮助我们轻松地创建、进入以及通常在 Python 虚拟环境之间工作或切换。第三步将WORKON_HOME变量分配给一个目录,我们希望在那里拥有所有虚拟环境。然后,我们将创建一个新的目录来存放所有虚拟环境。在第四步中,使用命令 source 执行 shell 脚本以设置virtualenvwrapper包。在第五步中,我们看到如何在/home/mdiener/venvs目录中创建一个新的名为pygeoan_cb的virtualenv。这一步将自动启动我们的virtualenv会话。
一旦virtualenv会话开始,我们现在可以看到括号中的virtualenv名称,如下所示:
(pygeoan_cb)mdiener@mdiener-VirtualBox:~$
要退出virtualenv,只需输入以下代码:
$ deactivate
现在,您的命令行应该恢复到正常状态,如下所示:
mdiener@mdiener-VirtualBox:~$
要重新激活virtualenv,只需输入以下命令:
$ workon pygeoan_cb
小贴士
workon命令有Tab自动完成功能。所以,只需输入workon,然后输入您想要进入的虚拟环境名称的第一个字母,例如py。按Tab键,它将自动完成名称。
在/venvs文件夹中,您将找到每个项目的特定虚拟环境,形式为一个子文件夹。virtualenvwrapper包将为每个新创建的项目创建一个新的文件夹。因此,您可以轻松地删除一个文件夹,这将删除您的虚拟环境。
要快速将所有已安装的库列表打印到文件中,我们将使用pip命令:
$ pip freeze > requirements.txt
这将在当前文件夹中创建一个名为requirements.txt的文本文件。该文本文件包含当前运行的 Python 虚拟环境中安装的所有 Python 包的列表。
要从需求文件创建新的virtualenv,请使用以下命令:
$ pip install -r /path/to/requirements.txt
还有更多...
对于那些刚开始进行地理空间 Python 开发的人来说,应该注意的是,你应该将项目特定的代码保存在 Python 虚拟环境文件夹外的另一个位置。例如,我总是将每个项目相关的代码包含在一个名为01_projects的单独文件夹中,这是我的主要文件夹。我的项目文件夹的路径是/home/mdiener/01_projects,我两个项目的结构如下:
-
01_projects/Name_project1 -
01_projects/Name_project2
所有虚拟环境都位于/home/mdiener/venvs/下。通常,我会将它们命名为与项目相同的名称,以保持整洁,如下所示:
-
/home/mdiener/venvs/Name_project1 -
/home/mdiener/venvs/Name_project2
安装 pyproj 和 NumPy
pyproj 是围绕 PROJ.4 库的一个包装器,它在 Python 中用于处理投影和执行变换 (pypi.python.org/pypi/pyproj/)。所有你的地理信息都应该投影到由 欧洲石油调查组 (EPSG) 支持的众多坐标系之一。这些信息对于系统正确地将数据放置在地球上的适当位置是必要的。然后,地理数据可以像层层叠加的数据一样放置在彼此之上,以创建地图或执行分析。数据必须正确定位,否则我们无法将其添加、组合或与其他数据源在空间上进行比较。
数据来自许多来源,并且,通常,投影并不等同于数据集。更糟糕的是,数据可能由数据提供者提供描述,声称它在投影 UTM31 中,而实际上数据在投影 UTM34 中!这可能导致在尝试使数据协同工作时出现大问题,程序可能会抛出一些难看的错误信息。
NumPy 是用于科学计算数组以及复数的科学基础,这些被用于驱动多个流行的地理空间库,包括 GDAL (地理空间抽象库)。NumPy 的强大之处在于其对大型矩阵、数组和数学函数的支持。因此,NumPy 的安装对于其他库能够顺利运行是必要的,但在我们进行空间分析的过程中,NumPy 很少被直接使用。
准备工作
启动你的虚拟环境,如果它还没有运行,请使用以下标准启动命令:
$ workon pygeoan_cb
你的提示符现在应该看起来像这样:
(pygeoan_cb)mdiener@mdiener-VirtualBox:~$
注意
如果由于某种原因 workon 没有启动你的虚拟环境,你可以简单地通过在命令行中执行 source /home/mdiener/venvs/pygeoan_cb/bin/activate 来启动它;再次尝试列出在 安装 virtualenv 和 virtualenvwrapper 菜单中列出的步骤以使其运行。
现在,我们需要安装一些 Python 开发工具,以便我们可以安装 NumPy,因此运行此命令:
$ sudo apt-get install -y python-dev
现在,你已经准备好继续安装 pyproj 和 NumPy 到你的运行虚拟环境中。
如何操作...
简单地启动 virtualenv,我们将使用 pip 安装程序来完成所有繁重的工作,如下所示:
-
使用 pip 继续安装 NumPy;这可能需要几分钟,因为屏幕上会写出许多安装详细输出:
$ pip install numpyWindows 用户可以获取 NumPy 的
.whl文件并使用以下命令执行它:pip install numpy -1.9.2+mkl-cp27-none-win32.whl -
再次使用
pip来安装 pyproj:$ pip install pyprojWindows 用户可以使用以下命令来安装 pyproj:
pip install pyproj-1.9.4-cp27-none-win_amd64.whl -
等待几分钟;NumPy 应该现在与 pyproj 一起运行。要测试它是否成功,请在 Python 控制台中输入以下命令。输出应该看起来像这样:
(pygeoan_cb)mdiener@mdiener-VirtualBox:~/venv$ python Python 2.7.3 (default, Feb 27 2014, 19:58:35) [GCC 4.6.3] on linux2 Type “help”, “copyright”, “credits”, or “license” for more information. >> import numpy >> import pyproj
希望没有错误。你现在已经成功安装了 NumPy 和 pyproj。
注意
可能会出现各种错误,所以请查看相应的安装链接以帮助您解决问题:
对于 pyproj:pypi.python.org/pypi/pyproj/
对于 NumPy:www.numpy.org
它是如何工作的...
这种简单的安装使用标准的 pip 安装方法。不需要任何技巧或特殊命令。您只需执行 pip install <library_name> 命令,然后就可以开始了。
小贴士
如果您不确定要安装的确切名称,可以通过访问 pypi.python.org/pypi 网页来查找库名称。
安装 shapely、matplotlib 和 descartes
地理空间分析和可视化的大部分工作都是通过使用 Shapely、matplotlib、GDAL、OGR 和 descartes 实现的,这些库将在之后安装。这里的大部分食谱将广泛使用这些库,因此设置它们是完成我们的练习所必需的。
Shapely (toblerity.org/shapely) 使用与 AutoCAD 相同的笛卡尔坐标系进行纯空间分析,对于那些熟悉类似 CAD 的程序的人来说。使用平面坐标系统的优点是,所有欧几里得几何和分析几何的规则都适用。为了快速回顾我们在学校学过的坐标系,这里有一个小图来快速唤醒您的记忆。

注意
描述:一个展示平面以绘制和测量几何形状的笛卡尔坐标系。
插图 1: 来源:en.wikipedia.org/wiki/Cartesian_coordinate_system.
Shapely 在使用 GEOS 库作为其后台工作马力的经典叠加分析和其他几何计算中表现出色。
对于 matplotlib (matplotlib.org/),它是将图形和数据渲染到屏幕上的图像或 可缩放矢量图形(svg)的绘图引擎。matplotlib 的用途仅限于您的想象力。因此,就像名字部分所暗示的那样,matplotlib 使您能够将数据绘制在图表上,甚至是在地图上。对于那些熟悉 MATLAB 的人来说,您会发现 matplotlib 在功能上非常相似。
descartes 库提供了 Shapely 几何对象与 Matplotlib 的更好集成。在这里,您将看到 descartes 打开了 matplotlib 绘图的 fill 和 patch,以便与 Shapely 的几何体一起工作,并为您节省了单独输入它们的麻烦。
准备工作
为了准备安装,有必要安装一些全局包,例如 libgeos_c,因为这些是 Shapely 所必需的。NumPy 也是我们已满足的要求,并且也被 Shapely 使用。
按照以下方式从命令行安装 matplotlib 的需求:
$ sudo apt-get install freetype* libpng-dev libjpeg8-dev
这些是 matplotlib 的依赖项,可以在 Ubuntu 14.04 机器上看到。
如何操作...
按照以下说明操作:
-
运行 pip 安装 shapely:
$ pip install shapely -
运行 pip 安装 matplotlib:
$ pip install matplotlib -
最后,运行 pip 安装 descartes:
$ pip install descartes
另一个测试是否一切顺利的方法是简单地进入 Python 控制台并尝试导入这些包,如果没有错误发生,你的控制台应该显示一个空的 Python 光标。输出应该看起来像以下代码所示:
(pygeoan_cb)mdiener@mdiener-VirtualBox:~/venv$ python
Python 2.7.3 (default, Feb 27 2014, 19:58:35)
[GCC 4.6.3] on linux2
Type “help”, “copyright”, “credits”, or “license” for more information.
>>> import shapely
>>> import matplotlib
>>> import descartes
>>>
# type exit() to return
>>> exit()
如果出现任何错误,Python 通常会提供一些很好的线索,告诉你问题所在,并且总有 Stack Overflow。例如,查看stackoverflow.com/questions/19742406/could-not-find-library-geos-c-or-load-any-of-its-variants/23057508#2305750823057508。
它是如何工作的...
在这里,安装包的顺序非常重要。descartes 包依赖于 matplotlib,而 matplotlib 依赖于 NumPy、freetype 和 libpng。这让你不得不首先安装 NumPy,然后是 matplotlib 及其依赖项,最后是 descartes。
使用 pip 安装本身很简单,应该快速且无痛苦。如果 libgeos_c 没有正确安装,那么你可能需要安装 libgeos-dev 库。
安装 pyshp、geojson 和 pandas
这些特定的库是为了特定的格式而设计的,使得我们的生活和使用 GDAL 进行某些项目相比更加简单和方便。pyshp 将与 shapefiles 一起工作,geojson 与 GeoJSON 一起工作,而 pandas 则以结构化的方式处理所有其他文本数据类型。
pyshp 是纯 Python 编写的,用于导入和导出 shapefiles;你可以在github.com/GeospatialPython/pyshp找到 pyshp 库的源代码。pyshp 库的唯一目的是与 shapefiles 一起工作。GDAL 将用于处理我们大部分数据的输入/输出需求,但有时,当与 shapefiles 一起工作时,纯 Python 库会更简单。
geojson 是一个 Python 库的名称,也是一个格式,这使得理解它有点令人困惑。GeoJSON 格式 (geojson.org) 正变得越来越受欢迎,因此我们使用 Python geojson 库来处理其创建。如果你搜索 geojson,你会在 Python 包索引(PyPI)上找到它。正如你所期望的,这将帮助我们创建 GeoJSON 规范中支持的所有不同几何类型。
pandas (pandas.pydata.org) 是一个数据分析库,它以类似电子表格的方式组织你的数据,以便进行进一步的计算。由于我们的地理空间数据来自广泛的来源和格式,例如 CSV,pandas 有助于以最小的努力处理数据。
准备工作
使用以下命令进入你的虚拟环境:
$ workon pygeoan_cb
你的提示符现在应该看起来像这样:
(pygeoan_cb)mdiener@mdiener-VirtualBox:~$
如何操作...
以下是三个安装步骤:
-
Pyshp 将首先通过以下方式使用 pip 安装:
$ pip install pyshp -
接下来,将使用 pip 安装 geojson 库:
$ pip install geojson -
最后,pip 将安装 pandas:
$ pip install pandas
要测试 pyshp 的安装,请使用 import shapefile 类型。输出应类似于以下输出:
(pygeoan_cb)mdiener@mdiener-VirtualBox:~/venv$ python
Python 2.7.3 (default, Feb 27 2014, 19:58:35)
[GCC 4.6.3] on linux2
Type “help”, “copyright”, “credits”, or “license” for more information.
>> import shapefile
>> import geojson
>> import pandas
注意
import shapefile 语句导入了 pyshp 库;与其它库不同,导入名称与安装名称不同。
它是如何工作的...
如同在其他模块中看到的那样,我们使用了标准的安装 pip 软件包来执行安装。没有其他依赖项需要担心,这使得进度快速。
安装 SciPy、PySAL 和 IPython
SciPy 是一个包含 SciPy 库、matplotlib、pandas、SymPy 和 IPython 等库的 Python 库集合。SciPy 库本身用于许多操作,但我们特别感兴趣的是 空间 模块。此模块可以执行许多操作,包括运行最近邻查询。
PySAL 是一个用于空间分析的地理空间计算库。从 Python 代码中直接创建模型和运行模拟是 PySAL 提供的许多库功能之一。PySAL 是一个库,当与我们的可视化工具(如 matplotlib)结合使用时,为我们提供了一个强大的工具。
IPython 是一个用于控制台的 Python 解释器,它取代了您在终端运行和测试 Python 代码时可能习惯的正常 Python 控制台。这实际上只是一个具有一些酷功能的先进解释器,例如 Tab 自动完成,这意味着初学者可以通过输入一个字母并按 Tab 键来快速获取命令。IPython 笔记本可以帮助以网页形式共享代码,包括代码、图像等,而无需安装。
准备工作
我们之前查看的依赖关系丛林又回来了,我们需要在 Ubuntu 系统中使用 apt-get install 安装三个通用安装,如下所示:
$ sudo apt-get install libblas-dev liblapack-dev gfortran
注意
Windows 和 Mac 用户可以使用完整的安装程序(www.scipy.org/install.html),例如 Anaconda 或 Enthought Canopy,它将一次性为你完成所有安装依赖。
SciPy 安装使用了三个依赖项。PySAL 依赖于 SciPy,因此请确保首先安装 SciPy。只有 IPython 不需要额外的安装。
使用以下代码启动你的 Python 虚拟环境:
mdiener@mdiener-VirtualBox:~$ workon pygeoan_cb
(pygeoan_cb)mdiener@mdiener-VirtualBox:~$
如何操作...
让我们看看这些步骤:
-
首先,我们将安装 SciPy,因为 PySAL 依赖于它。安装需要一些时间;我的机器花费了 5 分钟,所以请休息一下:
$ pip install scipy -
PySAL 可以使用 pip 非常快速地安装:
$ pip install pysal -
如同往常,我们想看看一切是否正常工作,所以让我们按照以下方式启动 Python 壳:
(pygeoan_cb)mdiener@mdiener-VirtualBox:~$python >>> import scipy >>> import pysal >>> -
IPython 可以通过 pip 全局安装或安装在虚拟环境中,如下所示:
$ pip install ipython
它是如何工作的...
SciPy 和 PySAL 库都是为了帮助完成各种空间分析任务而设计的。工具的选择取决于手头的任务,所以请确保在命令提示符下检查哪个库提供了什么功能,如下所示:
>>> from scipy import spatial
>>> help(spatial)
输出应类似于以下截图所示:

安装 GDAL 和 OGR
格式转换是枯燥的、重复的,GDAL 库提供的许多功能之一,更不用说格式转换了。然而,GDAL 在其他地理空间功能方面也表现出色,例如获取 Shapefile 的当前投影或从高程数据生成等高线。所以,仅仅说 GDAL 是一个转换库是不准确的;它实际上要丰富得多。GDAL 的创始人 Frank Warmerdam 为启动该项目应得到认可,GDAL 项目现在是OSGEO(开源地理空间基金会,请参阅www.osgeo.org)的一部分。
注意
GDAL 安装包括 OGR;不需要额外安装。
目前,GDAL 涵盖了处理栅格数据,而 OGR 涵盖了处理矢量数据。随着 GDAL 2.x 版本的推出,栅格和矢量两个方面现在都合并为一个整体。GDAL 和 OGR 是地理空间数据转换的瑞士军刀,覆盖了 200 多种不同的空间数据格式。
准备工作
GDAL 在 Windows、Linux 或 OSX 上安装时并不总是那么友好。它有许多依赖项,安装方式也多种多样。描述并不总是非常直接。请记住,这种描述只是处理事情的一种方式,并不总是适用于所有机器,因此请参考在线说明以获取最新和最佳的系统配置方法。
首先,我们将在我们的机器上全局安装一些依赖项。依赖项安装完成后,我们将进入全局安装 Python 的 GDAL 全局站点包。
如何操作...
要将 GDAL 全局安装到我们的 Python 站点包中,我们将按照以下步骤进行:
-
安装构建和 XML 工具时使用以下命令:
$ sudo apt-get install -y build-essentiallibxml2-dev libxslt1-dev -
使用以下命令安装 GDAL 开发文件:
$ sudo apt-get install libgdal-dev # install is 125MB -
以下命令将在主 Python 包中安装 GDAL 包。这意味着 GDAL 将被全局安装。就我所知,GDAL 的全局安装通常不是坏事,因为没有向后不兼容的版本,这在当今时代是非常罕见的。在
virtualenv中直接且仅安装 GDAL 是非常痛苦的,如果你有兴趣尝试,我提供了一些链接供你尝试。$ sudo apt-get install python-gdal注意
如果您想在虚拟环境中尝试安装,请查看这个 Stack Overflow 问题:
gis.stackexchange.com/questions/28966/python-gdal-package-missing-header-file-when-installing-via-pip。 -
要在 Python 虚拟环境中获取 GDAL,我们只需要运行一个简单的 virtualevnwrapper 命令:
toggleglobalsitepackages确保您已激活虚拟环境,如下所示:
mdiener@mdiener-VirtualBox:~$ workon pygeoan_cb (pygeoan_cb)mdiener@mdiener-VirtualBox:~$ -
现在,在您的当前虚拟环境中激活全局 Python 站点包:
(pygeoan_cb)mdiener@mdiener-VirtualBox:~$ toggleglobalsitepackages enable global site-packages -
最后的检查是查看 GDAL 是否可用,如下所示:
$ python >>> import gdal >>> -
没有发现错误,GDAL 已准备好使用。
Windows 7 及以上用户应使用 OSGeo4W Windows 安装程序 (trac.osgeo.org/osgeo4w/)。在网页上找到以下部分,下载您的 32 位或 64 位 Windows 版本。遵循图形安装程序的说明,然后 GDAL 安装将完成。
小贴士
如果所有方法都失败,Windows 用户也可以直接从 www.gisinternals.com/sdk/ 获取二进制文件。此安装程序可以帮助避免可能出现的任何其他特定于 Windows 的问题,并且该网站可以帮助您走上正确的道路。
它是如何工作的...
GDAL 安装包括栅格(GDAL)和矢量(OGR)工具。在 GDAL 安装中包含五个模块,根据您的需求可以单独导入到您的项目中:
>>> from osgeo import gdal
>>> from osgeo import ogr
>>> from osgeo import osr
>>> from osgeo import gdal_array
>>> from osgeo import gdalconst
>>> python
>>> import osgeo
>>> help(osgeo)
要查看与您的 Python GDAL 安装一起包含的包,我们使用 Python 内置的帮助功能来列出 OSGeo 模块可以提供的内容。您应该看到以下内容:
NAME
osgeo - # __init__ for osgeo package.
FILE
/usr/lib/python2.7/dist-packages/osgeo/__init__.py
MODULE DOCS
http://docs.python.org/library/osgeo
PACKAGE CONTENTS
_gdal
_gdal_array
_gdalconst
_ogr
_osr
gdal
gdal_array
gdalconst
gdalnumeric
ogr
osr
DATA
__version__ = '1.10.0'
version_info = sys.version_info(major=2, minor=7, micro=3, releaseleve...
VERSION
1.10.0
(END)
在撰写本文时,GDAL 版本已提升到 2.0,在开发者领域,甚至在它被打印出来之前就已经是旧版本了。请注意,GDAL 2.0 存在兼容性问题,并且对于本书,建议使用 1.x.x 版本。
参考以下内容
www.gdal.org 的主页始终是关于其任何信息的最佳参考地点。OSGEO 将 GDAL 作为支持项目之一,您可以在 www.osgeo.org 上找到更多关于它的信息。
安装 GeoDjango 和 PostgreSQL 与 PostGIS
这是我们最终的安装方案,如果您到目前为止一直跟随,那么您就可以开始一个简单直接的 Django 之旅了。根据 Django 主页,Django 是为有截止日期的专业人士提供的 Web 框架。其空间部分可以在 GeoDjango 中找到。GeoDjango 是与每个 Django 安装一起安装的 contrib 模块,因此,您只需要安装 Django 就可以运行 GeoDjango。当然,“geo”有其依赖关系,这些依赖关系在前面的章节中已经满足。为了参考,请查看 Django 主页上的这份优秀的文档。
docs.djangoproject.com/en/dev/ref/contrib/gis/install/#ref-gis-install.
我们将使用 PostgreSQL 和 PostGIS,因为它们是开源行业常用的空间数据库。安装不是 100%必要的,但没有它们就没有真正的意义,因为你限制了你的操作,而且如果你计划将你的空间数据存储在空间数据库中,它们绝对是必需的。PostgreSQL 和 PostGIS 的组合是 GeoDjango 最常见空间数据库设置。这个安装肯定更复杂,可能会根据你的系统导致一些连接问题。
准备工作
要使用 GeoDjango,我们需要安装一个空间数据库,在我们的案例中,我们将使用带有 PostGIS 扩展的 PostgreSQL。GeoDjango 还支持 Oracle、Spatialite 和 MySQL。PostGIS 的依赖包括 GDAL、GEOS、PROJ.4、LibXML2 和 JSON-C。
按如下方式启动你的 Python 虚拟环境:
mdiener@mdiener-VirtualBox:~$ workon pygeoan_cb
(pygeoan_cb)mdiener@mdiener-VirtualBox:~$
小贴士
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册以直接将文件通过电子邮件发送给你。
如何操作...
按照以下步骤操作。这些步骤来自 Ubuntu Linux 的 PostgreSQL 主页:
-
使用标准的 gedit 文本编辑器创建一个名为
pgdg.list的新文件。该文件存储启动你的 Ubuntu 安装器包的命令:$ sudo gedit /etc/apt/sources.list.d/pgdg.list -
将此行添加到文件中,保存并关闭:
$ deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main -
现在,运行
wget命令添加密钥:$ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | \ sudo apt-key add - -
运行
update命令以实际更新你的安装器包:$ sudo apt-get update -
运行
install命令以实际安装 PostgreSQL 9.3:$ sudo apt-get install postgresql-9.3 -
要安装 PostGIS 2.1,我们将有一个未满足的依赖项
libgdal1,所以继续安装它:$ sudo apt-get install libgdal1 -
现在,我们可以在我们的机器上安装 PostGIS 2.1 for PostgreSQL 9.3:
$ sudo apt-get install postgresql-9.3-postgis-2.1 -
安装 PostgreSQL 头文件:
$ sudo apt-get install libpq-dev -
最后,使用以下贡献安装
contrib模块:$ sudo apt-get install postgresql-contrib -
安装 Python 数据库适配器
psycopg2,以便从 Python 连接到你的 PostgreSQL 数据库:$ sudo apt-get install python-psycopg2 -
现在,我们可以创建一个标准的 PostgreSQL 数据库如下:
(pygeoan_cb)mdiener@mdiener-VirtualBox:~$ createdb [NewDatabaseName] -
使用
psql命令行工具,我们可以创建一个 PostGIS 扩展到我们新创建的数据库,如下赋予它所有 PostGIS 功能:(pygeoan_cb)mdiener@mdiener-VirtualBox:~$ psql -d [NewDatabaseName] -c "CREATE EXTENSION postgis;" -
接下来,我们终于可以在激活的虚拟环境中直接一行安装 Django:
$ pip install django -
测试你的 Django 和 GDAL 安装,并始终尝试如下导入:
>>> from django.contrib.gis import gdal >>> gdal.HAS_GDAL True
Windows 用户应被指引到 EnterpriseDB 提供的 PostgreSQL Windows (www.postgresql.org/download/windows/)二进制文件,www.enterprisedb.com/products-services-training/pgdownload#windows。下载正确的版本并遵循安装说明。PostGIS 也包含在可以直接使用安装程序安装的扩展列表中。
它是如何工作的...
使用 apt-get Ubuntu 安装程序和 Windows 安装程序安装足够简单,以便 PostgreSQL、PostGIS 和 Django 能够正常运行。然而,安装程序的内部工作原理超出了本书的范围。
还有更多...
要总结所有已安装的库,请查看以下表格:
| 库名称 | 描述 | 安装原因 |
|---|---|---|
| NumPy | 这增加了对大型多维数组和矩阵的支持 | 它是许多其他库的要求 |
| pyproj | 这处理投影 | 它转换投影 |
| shapely | 这处理地理空间操作 | 它执行快速的几何操作和操作 |
| matplotlib | 这提供绘图库 | 它提供了结果的快速可视化 |
| descartes | 这使用 Shapely 或 GeoJSON 对象作为 matplotlib 路径和补丁 | 它快速绘制地理数据 |
| pandas | 这提供了高性能的数据结构和数据分析 | 它执行数据操作、CSV 创建和数据操作 |
| SciPy | 这提供了一组用于科学计算的 Python 库 | 它拥有最佳的工具集合 |
| PySAL | 这包含一个地理空间分析库 | 它执行大量的空间操作(可选) |
| IPython | 这提供了交互式 Python 计算 | 它是一个有用的笔记本,用于存储和保存你的脚本(可选) |
| Django | 这包含一个网络应用框架 | 它用于我们第十一章中的演示网络应用,使用 GeoDjango 进行网络分析 |
| pyshp | 这提供了纯 Python 的 shapefile 操作和生成 | 它有助于输入和输出 shapefiles |
| GeoJSON | 这包含空间数据的 JSON 格式 | 它促进了此格式的交换和发布 |
| PostgreSQL | 这是一个关系型数据库 | 它有助于存储空间数据 |
| PostGIS | 这是 PostgreSQL 的空间扩展 | 它在 PostgreSQL 中存储和执行地理数据的空间操作 |
第二章。处理投影
在本章中,我们将涵盖以下主题:
-
发现 Shapefile 或 GeoJSON 数据集的投影(s)
-
从 WMS 服务器列出投影(s)
-
如果 Shapefile 不存在,为其创建投影定义
-
批量设置一个充满 Shapefiles 文件夹的投影定义
-
将 Shapefile 从一个投影重新投影到另一个投影
简介
在我的看法中,处理投影并不太令人兴奋,但它们非常重要,并且您在任何应用程序中处理它们的能力至关重要。
本章的目标是提供一些常见的预数据筛选或转换步骤,以便将您的数据整理好,或者更好的是,为地理空间分析定位。我们无法总是对处于不同坐标系统中的多个数据集进行分析,而不存在得到不一致结果的风险,例如数据位置不准确。因此,当在全局范围内工作时,最好在相同的坐标系中工作,如 EPSG:4326,或者使用为您的地区提供最精确结果的本地坐标系。
欧洲石油调查组或EPSG代码已决定为所有坐标系分配一个数字代码,以简化投影信息的查找和共享。坐标系通过其定义来描述,这些定义存储在各种格式的文本文件中。这些文本文件旨在成为计算机可读格式,特别是为单个 GIS 桌面软件包,如 QGIS 或 ESRI ArcMap 或为您的 Web/脚本应用程序设计的。
EPSG 代码 4326 代表1984 年世界地理系统(WGS 84)是一个地理坐标系,具有经纬度(x,y)单位(参见图下所示)。地理坐标系将地球表示为一个球体,如图所示,测量单位是度。

图 1:地理坐标系(kartoweb.itc.nl/geometrics/coordinate%20systems/coordsys.html)
第二种坐标系是一种投影坐标系,它是一个具有恒定面积、长度或角度的二维平面,这些角度是在x和y网格上测量的。EPSG:3857 Pseudo-Mercator就是这样一种投影坐标系,其中单位是米,长度正确,但角度和面积是扭曲的。在任何给定的投影坐标系中,只有三个属性中的两个,即面积、距离或角度,可以在单个地图上正确表示。通用横轴墨卡托(UTM)坐标系将世界划分为 60 个区域(参见图下所示):

插图 2:投影坐标系 UTM (en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system#mediaviewer/File:Utm-zones.jpg)
注意
注意,你必须使用 workon pygeoan_cb 命令进入你的 Python 虚拟环境。
发现 Shapefile 或 GeoJSON 数据集的投影
记住,所有数据都是以坐标系存储的,无论数据源是什么。你的任务是使用本节中概述的简单方法来找出这一点。我们将查看两种不同的数据存储类型:一个 Shapefile 和一个 GeoJSON 文件。这两种格式包含几何形状,如点、线或多边形,以及它们相关的属性。例如,一棵树会被存储为一个具有属性(如高度、年龄和种类)的点几何形状。这些数据类型以不同的方式存储它们的投影数据,因此需要不同的方法来发现它们的投影信息。
现在快速介绍一下 Shapefile 是什么:Shapefile 不是一个单独的文件,而是一组至少三个文件,例如 .shp、.shx 和 .dbf,它们具有相同的名称。例如,world_borders.shp、world_borders.shx 和 world_borders.dbf 组成了一个文件。.shp 文件存储几何形状,.dbf 存储属性值的表,而 .shx 是连接几何形状到属性值的索引表,作为一个查找表。
Shapefile 应该附带一个非常重要的第四个文本文件,称为 world_borders.prj。.prj 代表 投影信息,并以纯文本格式包含 Shapefile 的投影定义。听起来可能很疯狂,但你仍然可以找到并下载今天仍在提供的大量数据,而这些数据没有这个 .prj 文件。你可以通过在文本编辑器中打开这个 .prj 文件来实现,例如 Sublime Text 或 Notepad++,在那里你可以阅读关于投影定义的内容,以确定文件的坐标系。
注意
.prj 文件是一个纯文本文件,如果你不小心,很容易为错误的坐标系生成。错误的投影定义可能会导致你的分析和转换出现问题。我们将看到如何正确评估 Shapefile 的投影信息。
GeoJSON 是一个存储在纯文本中的单个文件。GeoJSON 标准 (www.geojson.org) 基于 JSON 标准。根据我的经验,坐标参考信息通常 不 包含在内,默认为 WGS 84 和 EPSG:4326,其中坐标以 x、y、z 格式存储,并且在这个 确切顺序 中。
注意
对于 y,x 和 y 混合的情况可能发生,当这种情况发生时,你的数据很可能会最终出现在海洋中,所以请始终记住顺序很重要:
x = 经度
y = 纬度
z = 高度
如果 GeoJSON 的 CRS 信息以 FeatureCollection 的形式呈现,如下所示:
{
"type": "FeatureCollection",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
…
准备工作
首先,前往 github.com/mdiener21/python-geospatial-analysis-cookbook 并一次性下载整个源代码和地理数据。下载图标位于右下角,标记为 Download ZIP。如果你是 GitHub 用户,当然可以克隆仓库。请注意,这是一个超过 150 MB 的下载。在仓库内部,你会找到每个章节,包括以下三个文件夹:/geodata/ 用于存储数据,/code/ 用于存储已完成的代码脚本,以及一个名为 /working/ 的空文件夹,供你创建自己的代码脚本。结构如下所示:
/ch01/
–------/code
–------/geodata
–------/working
/ch02/
–------/code
–------/geodata
–------/working
...
本食谱中使用的数据来源是加拿大不列颠哥伦比亚省温哥华市,位于 data.vancouver.ca/datacatalogue/index.htm(温哥华学校)。
提示
当从互联网源下载数据时,总是要寻找有关投影信息的元数据描述,这样在开始处理数据之前,你就能对数据的历史和来源有所了解。如今,大多数数据都以 EPSG:4326 WGS 84 或 EPSG:3857 Web Pseudo-Mercator 的形式公开可用。来自政府资源的数据很可能存储在本地区域坐标系中。
如何操作...
我们将从我们的 Shapefile 开始,并使用导入 OGR 模块的 GDAL 库来识别其存储的坐标系:
注意
注意,我们假设你的 Shapefile 有一个 .prj 文件。如果没有,这个过程将无法工作。
-
在你的
/ch02/working/目录中创建一个名为ch02_01_show_shp_srs.py的新 Python 文件,并添加以下代码:#!/usr/bin/env python # -*- coding: utf-8 -*- from osgeo import ogr shp_driver = ogr.GetDriverByName('ESRI Shapefile') shp_dataset = shp_driver.Open(r'../geodata/schools.shp') shp_layer = shp_dataset.GetLayer() shp_srs = shp_layer.GetSpatialRef() print shp_srs -
现在保存文件,并在命令行中运行
ch02_01_show_shp_srs.py脚本:$ python ch02-01-show_shp_srs.py PROJCS["NAD_1983_UTM_Zone_10N", GEOGCS["GCS_North_American_1983", DATUM["North_American_Datum_1983", SPHEROID["GRS_1980",6378137,298.257222101]], PRIMEM["Greenwich",0], UNIT["Degree",0.017453292519943295]], PROJECTION["Transverse_Mercator"], PARAMETER["latitude_of_origin",0], PARAMETER["central_meridian",-123], PARAMETER["scale_factor",0.9996], PARAMETER["false_easting",500000], PARAMETER["false_northing",0], UNIT["Meter",1]]你应该在屏幕上看到前面的文本打印出来,显示
.prj投影的信息。注意
注意,我们也可以简单地使用文本编辑器打开
.prj文件,并查看这些信息。现在,我们将查看一个 GeoJSON 文件,看看是否有投影信息可用。
-
确定 GeoJSON 文件的坐标系稍微困难一些,因为我们必须做出两种假设,第一种情况是标准情况且最常见:
-
GeoJSON 内部没有明确定义任何 CRS,所以我们假设坐标系是 EPSG:4326 WGS 84。
-
坐标参考系统(CRS)被明确定义且正确无误。
-
-
在你的
/ch02/working/目录中创建一个名为ch02_02_show_geojson_srs.py的新 Python 文件,并添加以下代码:#!/usr/bin/env python # -*- coding: utf-8 -*- import json geojson_yes_crs = '../geodata/schools.geojson' geojson_no_crs = '../geodata/golfcourses_bc.geojson' with open(geojson_no_crs) as my_geojson: data = json.load(my_geojson) # check if crs is in the data python dictionary data # if yes print the crs to screen # else print NO to screen and print geojson data type if 'crs' in data: print "the crs is : " + data['crs']['properties']['name'] else: print "++++++ no crs tag in file+++++" print "++++++ assume EPSG:4326 ++++++" if "type" in data: print "current GeoJSON data type is :" + data['type'] -
脚本被设置为在 GeoJSON
golfcourses_bc.geojson文件中设置的geojson_no_crs变量上运行。这些数据来源于 OpenStreetMap,它使用位于overpass-turbo.eu/的 Overpass API 导出。现在,运行ch02_02_show_geojson_srs.py脚本,你应该会看到我们第一个文件的这个输出:$ python ch02_02_show_geojson_crs.py ++++++ no crs tag in file+++++ ++++++ assume EPSG:4326 ++++++ current GeoJSON data type is :FeatureCollection小贴士
如果我们的 GeoJSON 文件中没有 CRS,我们将假设它具有 EPSG:4326 的投影。为了检查这一点,你需要查看文件内列出的坐标,看看它们是否在范围内,例如
-180.0000、-90.0000、180.0000和90.0000。如果是这样,我们将假设数据集确实是 EPSG:4326,并在 QGIS 中打开数据以进行检查。 -
现在,进入代码并编辑第 10 行,将变量从
geojson_no_crs更改为geojson_yes_crs,然后重新运行ch02_02_show_geojson_srs.py代码文件:$ python ch02_02_show_geojson_crs.py the crs is : urn:ogc:def:crs:EPSG::26910你现在应该看到前面的输出打印在屏幕上。
它是如何工作的...
从 Shapefile 开始,我们使用了 OGR 库来帮助我们快速发现 Shapefile 的 EPSG 代码信息,如下所示:
-
按如下方式导入 OGR 模块:
from osgeo import ogr -
激活 OGR Shapefile 驱动器:
shp_driver = ogr.GetDriverByName('ESRI Shapefile') -
使用 OGR 打开 Shapefile:
shp_dataset = shp_driver.Open(r'../geodata/schools.shp') -
使用
GetLayer()方法访问图层信息:shp_layer = shp_dataset.GetLayer() -
现在我们可以使用
GetSpatialRef()函数获取坐标信息:shp_srs = shp_layer.GetSpatialRef() -
最后,在屏幕上打印空间参考系统:
print shp_srs
当我们使用 Python JSON 模块查找 crs 键并打印其值到屏幕上时,GeoJSON 文件有点难以处理,如果它存在的话。
注意
我们可以简单地将第一个示例代码替换为 GeoJSON 驱动器,我们会得到相同的结果。然而,并非所有 GeoJSON 文件都包含投影信息。OGR 驱动器默认会输出 WGS 84 作为坐标系,在我们的 no_geojson_crs.geojson 示例文件中,这是错误的。这可能会让新用户感到困惑。需要注意的是要检查你的数据,查看坐标值,看看它们是否在一个定义的坐标值范围内。要探索代码,或者如果你输入了一个你有的代码并想看到它在实时网络地图上覆盖的区域,请参考 epsg.io。
首先,我们将导入标准的 Python JSON 模块,并设置两个变量来存储我们的两个 GeoJSON 文件。然后,我们将打开一个文件,即 golfcourses_bc.geojson 文件,并将 GeoJSON 文件加载到 Python 对象中。然后,我们只需要检查 crs 键是否在 GeoJSON 中;如果是,我们将打印其值。如果不是,我们将在屏幕上简单地打印出 crs 不可用和 GeoJSON 数据类型。
GeoJSON 默认的坐标系是 WGS 84 EPSG:4326,这意味着我们处理的是经纬度值。这些值必须落在 -180.0000、-90.0000、180.0000 和 90.0000 的范围内才能符合条件。
更多...
这里有一些投影定义示例供你参考:
-
与 Shapefile 作为
ShapefileName.prj存储的 ESRI Well-Known Text 代码如下:GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -
与 EPSG:4326 坐标系统相同的 OGC Well-Known Text 代码如下:
GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]] -
Proj4 格式的代码,也显示了
EPSG:4326,如下所示:+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs
参见
在www.spatialreference.org的网页上,你可以通过简单地选择你喜欢的目标坐标系统,在地图上放大,然后复制和粘贴坐标来获取任何投影的坐标。稍后,我们将使用spatialreference.org/ API 来获取 EPSG 定义,为 Shapefile 创建我们自己的.prj文件。
从 WMS 服务器列出投影
Web Mapping Service(WMS),可以在en.wikipedia.org/wiki/Web_Map_Service找到,很有趣,因为大多数服务提供商提供的数据支持多个坐标系统,你可以指定你想要的哪一个。然而,你不能将 WMS 重新投影或转换到服务提供商不提供的其他系统,这意味着你只能使用提供的服务坐标系统。以下是一个 WMS getCapabilities请求的示例(gis.ktn.gv.at/arcgis/services/INSPIRE/INSPIRE/MapServer/WmsServer?service=wms&version=1.3.0&request=getcapabilities),显示了来自 WMS 服务的五个可用坐标系统列表:

准备工作
我们将使用的 WMS 服务 URL 是ogc.bgs.ac.uk/cgi-bin/BGS_1GE_Geology/wms?service=WMS&version=1.3.0&request=GetCapabilities。这是来自英国地质调查局,标题为OneGeology Europe geology。
小贴士
要获取全球可用的 WMS 服务器列表,请参考 Skylab Mobile Systems 的www.skylab-mobilesystems.com/en/wms_serverlist.html。还可以查看geopole.org/。
我们将使用一个名为 OWSLib 的库。这个库是一个处理 OGC 网络服务(如 WMS)的出色包,如下所示:
Pip install owslib
如何操作...
让我们按照以下步骤检索 WMS 服务器提供的投影,并将可用的 EPSG 代码打印到屏幕上:
-
在你的
/ch02/code/working/目录下创建一个名为ch02_03_show_wms_srs.py的新 Python 文件,并添加以下代码:#!/usr/bin/env python # -*- coding: utf-8 -*- from owslib.wms import WebMapService url = "http://ogc.bgs.ac.uk/cgi-bin/BGS_1GE_Geology/wms" get_wms_url = WebMapService(url) crs_list = get_wms_url.contents['GBR_Kilmarnock_BGS_50K_CompressibleGround'].crsOptions print crs_list -
现在,运行
ch02_03_show_wms_srs.py脚本,你应该会看到以下屏幕输出:$ python ch02_03_show_wms_srs.py ['EPSG:3857', 'EPSG:3034', 'EPSG:4326', 'EPSG:3031', 'EPSG:27700', 'EPSG:900913', 'EPSG:3413', 'CRS:84', 'EPSG:4258']
工作原理...
在 WMS 投影中确定信息涉及使用 OWSLib 库。这是一种相当强大的方式,可以从您的客户端获取各种 OGC 网络服务信息。代码简单地接收 WMS URL 以检索 WMS 信息。响应的内容被调用,我们能够访问crsOptions属性以列出所有可用的 EPSG 代码。
如果不存在,为 Shapefile 创建投影定义
您最近从互联网资源下载了一个 Shapefile,并看到没有包含.prj文件。然而,您知道数据存储在网站所述的 EPSG:4326 坐标系中。现在以下代码将创建一个新的.prj文件。
准备工作
使用workon pygeo_analysis_cookbook命令启动您的 Python 虚拟环境:
如何做到这一点...
在以下步骤中,我们将向您展示如何创建一个新的.prj文件以配合我们的 Shapefile。.prj扩展名对于桌面 GIS、网络服务或脚本执行的大多数空间操作都是必要的:
-
在您的
/ch02/code/working/目录中创建一个名为ch02_04_write_prj_file.py的新 Python 文件,并添加以下代码:#!/usr/bin/env python # -*- coding: utf-8 -*- import urllib import os def get_epsg_code(epsg): """ Get the ESRI formatted .prj definition usage get_epsg_code(4326) We use the http://spatialreference.org/ref/epsg/4326/esriwkt/ """ f=urllib.urlopen("http://spatialreference.org/ref/epsg/{0}/esriwkt/".format(epsg)) return (f.read()) # Shapefile filename must equal the new .prj filename shp_filename = "../geodata/UTM_Zone_Boundaries" # Here we write out a new .prj file with the same name # as our Shapefile named "schools" in this example with open("../geodata/{0}.prj".format(shp_filename), "w") as prj: epsg_code = get_epsg_code(4326) prj.write(epsg_code) print "done writing projection definition to EPSG: " + epsg_code -
现在,运行
ch02_04_write_prj_file.py脚本:$ python ch02_04_write_prj_file.py -
您应该看到以下屏幕输出:
done writing projection definition UTM_Zone_Boundaries.prj to EPSG:4326 -
在您的文件夹内,您应该看到一个与 Shapefile 同名的新
.prj文件被创建。
它是如何工作的...
我们首先编写了一个函数,通过传递 EPSG 代码值使用spatialreference.org/ API 来获取我们的投影定义文本。该函数使用esriwkt格式化样式返回 EPSG 代码信息的文本描述,这表示 ESRI Well-Known Text,这是 ESRI 软件用于存储.prj文件信息的格式。
然后,我们需要输入 Shapefile 名称,因为.prj文件的文件名必须与 Shapefile 名称相等。
在最后一步,我们将使用指定的shp_filename创建.prj文件,并调用我们编写的获取坐标参考系统文本定义的函数。
批量设置充满 Shapefile 的文件夹的投影定义
与单个 Shapefile 一起工作是可以的,但与成百上千个文件一起工作就完全是另一回事了。在这种情况下,我们需要自动化来快速完成任务。
我们有一个包含几个 Shapefile 的文件夹,这些 Shapefile 都在同一个坐标系中,但没有.prj文件。我们希望为当前目录中的每个 Shapefile 创建一个.prj文件。
此脚本是对先前代码示例的修改版本,可以将单个 Shapefile 的.prj文件写入可以运行在多个 Shapefile 上的批处理过程。
如何做到这一点...
我们有一个包含许多 Shapefile 的文件夹,我们希望为这个文件夹中的每个 Shapefile 创建一个新的.prj文件,让我们开始吧:
-
在你的
/ch02/code/working/目录下创建一个名为ch02_05_batch_shp_prj.py的新 Python 文件,并添加以下代码:#!/usr/bin/env python # -*- coding: utf-8 -*- import urllib import os from osgeo import osr def create_epsg_wkt_esri(epsg): """ Get the ESRI formatted .prj definition usage create_epsg_wkt(4326) We use the http://spatialreference.org/ref/epsg/4326/esriwkt/ """ spatial_ref = osr.SpatialReference() spatial_ref.ImportFromEPSG(epsg) # transform projection format to ESRI .prj style spatial_ref.MorphToESRI() # export to WKT wkt_epsg = spatial_ref.ExportToWkt() return wkt_epsg # Optional method to get EPGS as wkt from a web service def get_epsg_code(epsg): """ Get the ESRI formatted .prj definition usage get_epsg_code(4326) We use the http://spatialreference.org/ref/epsg/4326/esriwkt/ """ web_url = "http://spatialreference.org/ref/epsg/{0}/esriwkt/".format(epsg) f = urllib.urlopen(web_url) return f.read() # Here we write out a new .prj file with the same name # as our Shapefile named "schools" in this example def write_prj_file(folder_name, shp_filename, epsg): """ input the name of a Shapefile without the .shp input the EPSG code number as an integer usage write_prj_file(<ShapefileName>,<EPSG CODE>) """ in_shp_name = "/{0}.prj".format(shp_filename) full_path_name = folder_name + in_shp_name with open(full_path_name, "w") as prj: epsg_code = create_epsg_wkt_esri(epsg) prj.write(epsg_code) print ("done writing projection definition : " + epsg_code) def run_batch_define_prj(folder_location, epsg): """ input path to the folder location containing all of your Shapefiles usage run_batch_define_prj("../geodata/no_prj") """ # variable to hold our list of shapefiles shapefile_list = [] # loop through the directory and find shapefiles # for each found shapefile write it to a list # remove the .shp ending so we do not end up with # file names such as .shp.prj for shp_file in os.listdir(folder_location): if shp_file.endswith('.shp'): filename_no_ext = os.path.splitext(shp_file)[0] shapefile_list.append(filename_no_ext) # loop through the list of shapefiles and write # the new .prj for each shapefile for shp in shapefile_list: write_prj_file(folder_location, shp, epsg) # Windows users please use the full path # Linux users can also use full path run_batch_define_prj("c:/02_DEV/01_projects/04_packt/ch02/geodata/no_prj/", 4326)
它是如何工作的...
使用标准的urllib Python 模块,我们可以通过 Web 访问 EPSG 代码并将其写入.prj文件。我们需要创建一个包含我们想要定义.prj的 Shapefile 的列表,然后为列表中的每个 Shapefile 创建一个.prj文件。
get_epsg_code(epsg)函数返回我们需要的 ESPG 代码文本定义。write_prj_file(shp_filename, epsg)函数接受两个参数,Shapefile 名称和 EPSG 代码,将.prj文件写入磁盘。
接下来,我们将创建一个空列表来存储 Shapefile 列表,切换到存储 Shapefile 的目录,然后列出当前目录中所有现有的 Shapefile。
我们的for循环将 Shapefile 列表填充为不带.shp扩展名的文件名。最后,最后一个for循环将遍历每个 Shapefile 并调用我们的函数为列表中的每个 Shapefile 写入.prj文件。
从一个投影到另一个投影 Shapefile
处理来自多个来源的空间数据会导致数据很可能来自地球上的多个区域,具有多个坐标系。为了执行一致的空间分析,我们应该将所有输入数据转换到相同的坐标系。这意味着将你的 Shapefile 重新投影到所选的工作坐标系。
在这个菜谱中,我们将把单个 Shapefile 从 EPSG:4326 重新投影到网络墨卡托系统 EPSG:3857,以便在 Web 应用程序中使用。
如何操作...
我们的目标是将给定的 Shapefile 从一个坐标系重新投影到另一个坐标系;完成此操作的步骤如下:
-
在你的
/ch02/code/working/目录下创建一个名为ch02_06_re_project_shp.py的新 Python 文件,并添加以下代码:#!/usr/bin/env python # -*- coding: utf-8 -*- import ogr import osr import os shp_driver = ogr.GetDriverByName('ESRI Shapefile') # input SpatialReference input_srs = osr.SpatialReference() input_srs.ImportFromEPSG(4326) # output SpatialReference output_srs = osr.SpatialReference() output_srs.ImportFromEPSG(3857) # create the CoordinateTransformation coord_trans = osr.CoordinateTransformation(input_srs, output_srs) # get the input layer input_shp = shp_driver.Open(r'../geodata/UTM_Zone_Boundaries.shp') in_shp_layer = input_shp.GetLayer() # create the output layer output_shp_file = r'../geodata/UTM_Zone_Boundaries_3857.shp' # check if output file exists if yes delete it if os.path.exists(output_shp_file): shp_driver.DeleteDataSource(output_shp_file) # create a new Shapefile object output_shp_dataset = shp_driver.CreateDataSource(output_shp_file) # create a new layer in output Shapefile and define its geometry type output_shp_layer = output_shp_dataset.CreateLayer("basemap_3857", geom_type=ogr.wkbMultiPolygon) # add fields to the new output Shapefile # get list of attribute fields # create new fields for output in_layer_def = in_shp_layer.GetLayerDefn() for i in range(0, in_layer_def.GetFieldCount()): field_def = in_layer_def.GetFieldDefn(i) output_shp_layer.CreateField(field_def) # get the output layer's feature definition output_layer_def = output_shp_layer.GetLayerDefn() # loop through the input features in_feature = in_shp_layer.GetNextFeature() while in_feature: # get the input geometry geom = in_feature.GetGeometryRef() # reproject the geometry geom.Transform(coord_trans) # create a new feature output_feature = ogr.Feature(output_layer_def) # set the geometry and attribute output_feature.SetGeometry(geom) for i in range(0, output_layer_def.GetFieldCount()): output_feature.SetField(output_layer_def.GetFieldDefn(i).GetNameRef(), in_feature.GetField(i)) # add the feature to the shapefile output_shp_layer.CreateFeature(output_feature) # destroy the features and get the next input feature output_feature.Destroy() in_feature.Destroy() in_feature = in_shp_layer.GetNextFeature() # close the shapefiles input_shp.Destroy() output_shp_dataset.Destroy() spatialRef = osr.SpatialReference() spatialRef.ImportFromEPSG(3857) spatialRef.MorphToESRI() prj_file = open('UTM_Zone_Boundaries.prj', 'w') prj_file.write(spatialRef.ExportToWkt()) prj_file.close() -
现在,我们可以按照以下方式从命令行运行我们的代码:
$ python ch02_06_re_project_shp.py -
现在我们有一个名为
UTM_Zone_Boundaries_3857.shp的新 Shapefile,它位于EPSG:3857坐标系中,并准备好进一步使用。
它是如何工作的...
osgeo、ogr和osr模块承担了繁重的工作,重新投影 Shapefile 所需的代码相当冗长。它通过遍历每个几何形状并将其单独转换到新的坐标系来实现。
从 ESRI Shapefile 的驱动程序开始,我们将设置输入和输出空间参考系统(SRS),以便我们可以转换它们。
在变换每个几何形状时,我们需要将每个特征的几何形状及其属性从旧 Shapefile 复制到新 Shapefile 中。最后,我们将使用Destroy()函数关闭输入和输出 Shapefile。
相关内容
使用代码并不总是重新投影 Shapefile 的最佳或最快方式。另一种你可以使用的方法是ogr2ogr命令行工具,它将简单地在一行中重新投影 Shapefile。你可以将这个单行命令管道输入到 Python 脚本中,批量重新投影多个 Shapefile:
ogr2ogr -t_srs EPSG:4326 outputwith4236.shp input.shp
GDAL 库附带了一些非常实用和有帮助的命令行功能,值得检查一下。
第三章。将空间数据从一个格式移动到另一个格式
在本章中,我们将涵盖以下主题:
-
使用 ogr2ogr 将 Shapefile 转换为 PostGIS 表
-
使用 ogr2ogr 将 Shapefiles 文件夹批量导入 PostGIS
-
从 PostGIS 批量导出一系列表到 Shapefiles
-
将 OpenStreetMap(OSM)XML 转换为 Shapefile
-
将 Shapefile(矢量)转换为 GeoTiff(栅格)
-
使用 GDAL 将栅格(GeoTiff)转换为矢量(Shapefile)
-
从存储在 Microsoft Excel 中的点数据创建 Shapefile
-
将 ESRI ASCII DEM 转换为图像高度图
简介
地理空间数据有数百种格式,将数据从一种格式转换为另一种格式是一项简单的任务。在数据类型之间进行转换的能力,如栅格或矢量,属于数据处理任务,可用于更好的地理空间分析。以下是一个栅格和矢量数据集的示例,以便您可以查看我在谈论的内容:

最佳实践方法是运行存储在常见格式中的数据(如 PostgreSQL PostGIS 数据库或一组具有共同坐标系统的 Shapefiles)上的分析函数或模型。例如,对存储在多种格式中的输入数据进行分析也是可能的,但如果出现问题或结果不符合预期,您可能会发现问题的细节。
本章将探讨一些常见的数据格式,并演示如何使用最常用的工具将这些格式从一种转换为另一种。
使用 ogr2ogr 将 Shapefile 转换为 PostGIS 表
将数据从一种格式转换为另一种格式最简单的方法是直接使用随 GDAL 安装提供的ogr2ogr工具。这个强大的工具可以转换 200 多种地理空间格式。在这个解决方案中,我们将从 Python 脚本中执行ogr2ogr实用程序以执行通用的矢量数据转换。因此,Python 代码被用来执行这个命令行工具并传递变量,这样您就可以创建自己的数据导入或导出脚本。
如果你对编码并不特别感兴趣,只想完成工作以移动你的数据,使用这个工具也是推荐的。当然,纯 Python 解决方案是可能的,但它无疑更倾向于满足开发人员(或 Python 纯主义者)的需求。由于本书的目标读者是开发人员、分析师或研究人员,这种类型的配方既简单又易于扩展。
准备工作
要运行此脚本,您需要在您的系统上安装 GDAL 工具应用程序。Windows 用户可以访问 OSGeo4W(trac.osgeo.org/osgeo4w)并下载 32 位或 64 位 Windows 安装程序。只需双击安装程序即可启动脚本,如下所示:
-
导航到底部选项,高级安装 | 下一步。
-
点击下一步从互联网下载 GDAL 工具(第一个默认选项)。
-
点击下一步接受路径的默认位置或更改为你喜欢的位置。
-
点击下一步接受本地保存下载的位置(默认)。
-
点击下一步接受直接连接(默认)。
-
点击下一步选择默认下载站点。
-
现在,你终于可以看到菜单了。点击+打开Commandline_Utilities标签页,你应该能看到这个截图所示的内容:
![准备中]()
-
现在,选择gdal: The GDAL/OGR library and commandline tools来安装它。
-
点击下一步开始下载和安装。
Ubuntu/Linux 用户可以使用以下步骤安装 GDAL 工具:
-
执行以下简单的单行命令:
$ sudo apt-get install gdal-bin这将使你能够直接从终端执行
ogr2ogr。要导入的 Shapefile 位于你的
/ch02/geodata/文件夹中,如果你已经从 GitHubgithub.com/mdiener21/python-geospatial-analysis-cookbook/下载了整个源代码和代码。温哥华开放地理数据门户data.vancouver.ca/datacatalogue/index.htm是我们的数据源,它提供了一个本地自行车道的数据集。 -
接下来,让我们设置带有 PostGIS 扩展的 PostgreSQL 数据库。为此,我们首先创建一个新用户来管理我们的新数据库和表,如下所示:
Sudo su createuser –U postgres –P pluto -
为新角色输入密码。
-
再次输入新角色的密码。
-
为
postgres用户输入密码,因为你将使用此postgres用户创建用户。 -
–P选项会提示你为名为pluto的新用户设置密码。在以下示例中,我们的密码是stars;我建议为你的生产数据库使用一个更安全的密码。小贴士
Windows 用户可以导航到
c:\Program Files\PostgreSQL\9.3\bin\文件夹,并执行以下命令,然后按照之前的方式遵循屏幕上的说明:Createuser.exe –U postgres –P pluto -
要创建数据库,我们将使用与
postgres用户相同的createdb命令行来创建一个名为py_geoan_cb的数据库,并将pluto用户指定为数据库所有者。以下是执行此操作的命令:$ sudo su createdb –O pluto –U postgres py_geoan_cb小贴士
Windows 用户可以访问
c:\Program Files\PostgreSQL\9.3\bin\并执行以下createdb.exe命令:createdb.exe –O pluto –U postgres py_geoan_cb接下来,我们将为我们的新创建的数据库创建 PostGIS 扩展:
psql –U postgres -d py_geoan_cb -c "CREATE EXTENSION postgis;"Windows 用户也可以在
c:\Program Files\PostgreSQL\9.3\bin\文件夹中执行psql,如下所示:psql.exe –U postgres –d py_geoan_cb –c "CREATE EXTENSION postgis;" -
最后,我们将创建一个名为geodata的模式来存储我们新的空间表。在 PostgreSQL 的默认
public模式之外存储空间数据是常见的。$ sudo -u postgres psql -d py_geoan_cb -c "CREATE SCHEMA geodata AUTHORIZATION pluto;"小贴士
Windows 用户可以使用以下命令来完成此操作:
psql.exe –U postgres –d py_geoan_cb –c "CREATE SCHEMA geodata AUTHORIZATION pluto;"
如何操作...
-
现在,让我们开始将我们的 Shapefile 导入到 PostGIS 数据库中,这将自动从我们的 Shapefile 创建一个新表:
#!/usr/bin/env python # -*- coding: utf-8 -*- import subprocess # database options db_schema = "SCHEMA=geodata" overwrite_option = "OVERWRITE=YES" geom_type = "MULTILINESTRING" output_format = "PostgreSQL" # database connection string db_connection = """PG:host=localhost port=5432 user=pluto dbname=py_test password=stars""" # input shapefile input_shp = "../geodata/bikeways.shp" # call ogr2ogr from python subprocess.call(["ogr2ogr","-lco", db_schema, "-lco", overwrite_option, "-nlt", geom_type, "-f", output_format, db_connection, input_shp]) -
接下来,我们将从命令行调用我们的脚本:
$ python ch03-01_shp2pg.py
它是如何工作的...
我们首先导入标准的 Python subprocess模块,该模块将调用ogr2ogr命令行工具。接下来,我们将设置一系列变量,这些变量用作输入参数,并为 ogr2ogr 执行提供各种选项。
从SCHEMA=geodata的 PostgreSQL 数据库开始,我们为我们的新表设置了一个非默认的数据库模式。将空间数据表存储在公共模式之外的一个单独的模式中是一种最佳实践,公共模式是默认模式。这种做法将使备份和恢复变得容易得多,并使数据库组织得更好。
接下来,我们创建一个设置为yes的overwrite_option变量,这样我们就可以在创建时覆盖任何同名表。当您想完全用新数据替换表时,这很有用;否则,建议使用-append选项。我们还指定了几何类型,因为有时 ogr2ogr 并不总是能正确猜测我们的 Shapefile 的几何类型,所以设置这个值可以节省您这方面的担忧。
现在,我们将使用PostgreSQL关键字设置我们的output_format变量,告诉 ogr2ogr 我们希望将数据输出到 PostgreSQL 数据库。然后是db_connection变量,它指定了我们的数据库连接信息。我们绝对不能忘记数据库必须已经存在,以及geodata模式;否则,我们将得到一个错误。
最后的input_shp变量是我们 Shapefile 的完整路径,包括.shp文件扩展名。我们将调用 subprocess 模块,它将调用 ogr2ogr 命令行工具,并传递运行工具所需的变量选项。我们向该函数传递一个参数数组,数组中的第一个对象是 ogr2ogr 命令行工具的名称。在名称之后,我们在数组中传递一个选项,以完成调用。
注意
Subprocess 可以用来直接调用任何命令行工具。Subprocess 接受由空格分隔的参数列表。这种参数传递相当挑剔,所以请确保您紧跟其后,不要添加任何额外的空格或逗号。
最后但同样重要的是,我们需要从命令行执行我们的脚本,通过调用 Python 解释器并传递脚本实际上导入我们的 Shapefile。现在转到PgAdmin PostgreSQL 数据库查看器,看看是否成功。或者,更好的是,打开 Quantum GIS (www.qgis.org)并查看新创建的表。
参见
如果您想查看 ogr2ogr 命令可用的完整选项列表,只需在命令行中输入以下内容:
$ ogr2ogr –help
您将看到可用的完整选项列表。此外,请访问gdal.org/ogr2ogr.html以阅读可用的文档。
注意
对于那些好奇如何在不使用 Python 的情况下运行此调用的人来说,直接调用ogr2ogr的调用方式如下:
ogr2ogr -lco SCHEMA=geodata -nlt MULTILINE -f "Postgresql" PG:"host=localhost port=5432 user=postgres dbname=py_geoan_cb password=secret" /home/mdiener/ch03/geodata/bikeways.shp
使用 ogr2ogr 将 Shapefile 文件夹批量导入 PostGIS
我们希望扩展我们最后的脚本,以便遍历一个充满 Shapefiles 的文件夹并将它们导入到 PostGIS 中。大多数导入任务都涉及多个文件,因此这是一个非常实用的任务。
如何操作...
我们的脚本将以函数的形式重用之前的代码,这样我们就可以批量处理要导入到 PostgreSQL PostGIS 数据库的 Shapefiles 列表。
-
为了简化起见,我们将从单个文件夹创建我们的 Shapefiles 列表:
#!/usr/bin/env python # -*- coding: utf-8 -*- import subprocess import os import ogr def discover_geom_name(ogr_type): """ :param ogr_type: ogr GetGeomType() :return: string geometry type name """ return {ogr.wkbUnknown : "UNKNOWN", ogr.wkbPoint : "POINT", ogr.wkbLineString : "LINESTRING", ogr.wkbPolygon : "POLYGON", ogr.wkbMultiPoint : "MULTIPOINT", ogr.wkbMultiLineString : "MULTILINESTRING", ogr.wkbMultiPolygon : "MULTIPOLYGON", ogr.wkbGeometryCollection : "GEOMETRYCOLLECTION", ogr.wkbNone : "NONE", ogr.wkbLinearRing : "LINEARRING"}.get(ogr_type) def run_shp2pg(input_shp): """ input_shp is full path to shapefile including file ending usage: run_shp2pg('/home/geodata/myshape.shp') """ db_schema = "SCHEMA=geodata" db_connection = """PG:host=localhost port=5432 user=pluto dbname=py_geoan_cb password=stars""" output_format = "PostgreSQL" overwrite_option = "OVERWRITE=YES" shp_dataset = shp_driver.Open(input_shp) layer = shp_dataset.GetLayer(0) geometry_type = layer.GetLayerDefn().GetGeomType() geometry_name = discover_geom_name(geometry_type) print (geometry_name) subprocess.call(["ogr2ogr", "-lco", db_schema, "-lco", overwrite_option, "-nlt", geometry_name, "-skipfailures", "-f", output_format, db_connection, input_shp]) # directory full of shapefiles shapefile_dir = os.path.realpath('../geodata') # define the ogr spatial driver type shp_driver = ogr.GetDriverByName('ESRI Shapefile') # empty list to hold names of all shapefils in directory shapefile_list = [] for shp_file in os.listdir(shapefile_dir): if shp_file.endswith(".shp"): # apped join path to file name to outpout "../geodata/myshape.shp" full_shapefile_path = os.path.join(shapefile_dir, shp_file) shapefile_list.append(full_shapefile_path) # loop over list of Shapefiles running our import function for each_shapefile in shapefile_list: run_shp2pg(each_shapefile) print ("importing Shapefile: " + each_shapefile) -
现在,我们可以再次从命令行简单地运行我们的新脚本,如下所示:
$ python ch03-02_batch_shp2pg.py
它是如何工作的...
在这里,我们正在重用之前脚本中的代码,但已将其转换为名为run_shp2pg(input_shp)的 Python 函数,该函数接受一个参数,即我们想要导入的 Shapefile 的完整路径。输入参数必须包含 Shapefile 扩展名,.shp。
我们有一个辅助函数,它通过读取 Shapefile 要素层并输出几何类型作为字符串来获取几何类型,这样ogr命令就知道期待什么。这并不总是有效,可能会发生一些错误。–skipfailures选项将忽略插入过程中抛出的任何错误,并继续填充我们的表。
首先,我们需要定义包含所有待导入 Shapefiles 的文件夹。接下来,我们可以创建一个名为shapefile_list的空列表对象,它将保存我们想要导入的所有 Shapefiles 的列表。
第一个for循环使用标准 Python os.listdir()函数获取指定目录中所有 Shapefiles 的列表。我们不想获取这个文件夹中的所有文件。我们只想获取以.shp结尾的文件;因此,有一个if语句,如果文件以.shp结尾,则评估为True。一旦找到.shp文件,我们需要将文件路径和文件名连接起来,创建一个包含路径和 Shapefile 名称的单个字符串,即full_shapefile_path变量。在最后部分,我们将每个新文件及其附加路径添加到我们的shapefile_list列表对象中,以便我们可以遍历最终的列表。
现在,是时候遍历我们新列表中的每个 Shapefile,并对列表中的每个 Shapefile 运行我们的run_shp2pg(input_shp)函数,将其导入到我们的 PostgreSQL PostGIS 数据库中。
还有更多...
如果你有很多 Shapefiles(我的意思是真的很多,比如 100 个或更多),性能将是一个考虑因素,因此将需要很多具有空闲资源的机器。
从 PostGIS 批量导出表到 Shapefiles
现在,我们将改变方向,看看我们如何可以从 PostGIS 数据库批量导出一系列表到 Shapefiles 文件夹。我们将在 Python 脚本中使用 ogr2ogr 命令行工具,这样你就可以将其包含在你的应用程序编程工作流程中。在接近结尾的地方,你还可以看到所有这些是如何在一个单独的命令行中完成的。
如何操作...
-
以下脚本将触发
ogr2ogr命令并遍历表列表以将 Shapefile 格式导出到现有文件夹。因此,让我们看看如何按照以下步骤进行:#!/usr/bin/env python # -*- coding: utf-8 -*- # import subprocess import os # folder to hold output Shapefiles destination_dir = os.path.realpath('../geodata/temp') # list of postGIS tables postgis_tables_list = ["bikeways", "highest_mountains"] # database connection parameters db_connection = """PG:host=localhost port=5432 user=pluto dbname=py_geoan_cb password=stars active_schema=geodata""" output_format = "ESRI Shapefile" # check if destination directory exists if not os.path.isdir(destination_dir): os.mkdir(destination_dir) for table in postgis_tables_list: subprocess.call(["ogr2ogr", "-f", output_format, destination_dir, db_connection, table]) print("running ogr2ogr on table: " + table) else: print("oh no your destination directory " + destination_dir + " already exist please remove it then run again") # commandline call without using python will look like this # ogr2ogr -f "ESRI Shapefile" mydatadump \ # PG:"host=myhost user=myloginname dbname=mydbname password=mypassword" neighborhood parcels -
现在,我们将按照以下方式从命令行调用我们的脚本:
$ python ch03-03_batch_postgis2shp.py
它是如何工作的...
从简单的 subprocess 和 os 模块导入开始,我们立即定义了我们想要存储导出 Shapefiles 的目标目录。该变量后面跟着我们想要导出的表名列表。此列表只能包括位于同一 PostgreSQL 模式中的文件。该模式定义为 active_schema,这样 ogr2ogr 就知道在哪里找到要导出的表。
再次强调,我们将输出格式定义为ESRI Shapefile。现在,我们将检查目标文件夹是否存在。如果存在,我们将继续并调用我们的循环。然后,我们将遍历存储在 postgis_tables_list 变量中的表列表。如果目标文件夹不存在,您将在屏幕上看到错误信息。
更多内容...
编写应用程序并从脚本内部执行 ogr2ogr 命令确实既快又简单。另一方面,对于一次性工作,当导出 Shapefile 列表时,您只需执行命令行工具即可。为了以一行命令完成此操作,请参阅以下信息框。
注意
如果您只想执行一次而不在脚本环境中执行,以下是一个调用 ogr2ogr 批量 PostGIS 表到 Shapefiles 的一行示例:
ogr2ogr -f "ESRI Shapefile" /home/ch03/geodata/temp PG:"host=localhost user=pluto dbname=py_geoan_cb password=stars" bikeways highest_mountains
您想要导出的表列表位于末尾,由空格分隔。导出 Shapefiles 的目标位置是 ../geodata/temp。请注意,此 /temp 目录必须存在。
将 OpenStreetMap (OSM) XML 转换为 Shapefile
OpenStreetMap (OSM) 拥有丰富的免费数据,但为了与其他大多数应用程序一起使用,我们需要将其转换为其他格式,例如 Shapefile 或 PostgreSQL PostGIS 数据库。本食谱将使用 ogr2ogr 工具在 Python 脚本中为我们执行转换。这种方法的优点再次是简单性。
准备工作
要开始,您需要下载 OSM 数据,请访问 www.openstreetmap.org/export#map=17/37.80721/-122.47305 并将文件(.osm)保存到您的 /ch03/geodata 目录中。下载按钮位于左侧栏上,按下后应立即开始下载(参见图表)。我们正在测试的区域位于旧金山,就在金门大桥之前。

如果你选择从 OSM 下载另一个区域,请随意,但请确保你选择一个与我示例相似的小区域。如果你选择一个更大的区域,OSM 网络工具会给出警告并禁用下载按钮。原因很简单:如果数据集非常大,它可能更适合其他工具,例如osm2pgsql(wiki.openstreetmap.org/wiki/Osm2pgsql)进行转换。如果你需要获取大区域的 OSM 数据并将其导出为 Shapefile,建议使用其他工具,例如osm2pgsql,它首先将你的数据导入 PostgreSQL 数据库。然后,使用pgsql2shp工具从 PostGIS 数据库导出数据到 Shapefile。
小贴士
一个名为imposm的 Python 工具可以用来将 OSM 数据导入 PostGIS 数据库,并且可以在imposm.org/找到。它的第 2 版是用 Python 编写的,第 3 版是用go编程语言编写的,如果你想尝试这个,也可以。
如何操作...
使用以下步骤将 OpenStreetMap (OSM) XML 转换为 Shapefile:
-
使用子进程模块,我们将执行ogr2ogr将我们下载的 OSM 数据转换为新的 Shapefile:
#!/usr/bin/env python # -*- coding: utf-8 -*- # convert / import osm xml .osm file into a Shapefile import subprocess import os import shutil # specify output format output_format = "ESRI Shapefile" # complete path to input OSM xml file .osm input_osm = '../geodata/OSM_san_francisco_westbluff.osm' # Windows users can uncomment these two lines if needed # ogr2ogr = r"c:/OSGeo4W/bin/ogr2ogr.exe" # ogr_info = r"c:/OSGeo4W/bin/ogrinfo.exe" # view what geometry types are available in our OSM file subprocess.call([ogr_info, input_osm]) destination_dir = os.path.realpath('../geodata/temp') if os.path.isdir(destination_dir): # remove output folder if it exists shutil.rmtree(destination_dir) print("removing existing directory : " + destination_dir) # create new output folder os.mkdir(destination_dir) print("creating new directory : " + destination_dir) # list of geometry types to convert to Shapefile geom_types = ["lines", "points", "multilinestrings", "multipolygons"] # create a new Shapefile for each geometry type for g_type in geom_types: subprocess.call([ogr2ogr, "-skipfailures", "-f", output_format, destination_dir, input_osm, "layer", g_type, "--config","OSM_USE_CUSTOM_INDEXING", "NO"]) print("done creating " + g_type) # if you like to export to SPATIALITE from .osm # subprocess.call([ogr2ogr, "-skipfailures", "-f", # "SQLITE", "-dsco", "SPATIALITE=YES", # "my2.sqlite", input_osm]) -
现在,我们可以从命令行调用我们的脚本:
$ python ch03-04_osm2shp.py
前往你的../geodata文件夹查看新创建的 Shapefiles,并尝试在 Quantum GIS 中打开它们,Quantum GIS 是一款免费的 GIS 软件(www.qgis.org)。
它是如何工作的...
这个脚本应该很清晰,因为我们使用子进程模块调用来执行 ogr2ogr 命令行工具。我们将指定我们的 OSM 数据集作为输入文件,包括文件的完整路径。Shapefile 的名称不需要提供,因为 ogr2ogr 将输出一系列 Shapefiles,每个 Shapefile 根据在 OSM 文件中找到的几何类型分别对应一个。我们只需要指定我们希望 ogr2ogr 将 Shapefiles 导出到的文件夹名称,如果该文件夹不存在,则会自动创建。
注意
Windows 用户:如果你没有将 ogr2ogr 工具映射到你的环境变量中,你可以简单地取消第 16 行和第 17 行的注释,并将显示的路径替换为你机器上 Windows 可执行文件的路径。
第一次子进程调用会在屏幕上打印出我们 OSM 文件中找到的几何类型。这在大多数情况下很有用,可以帮助识别可用内容。Shapefiles 每个文件只能支持一种几何类型,这也是为什么 ogr2ogr 会输出一个包含多个 Shapefiles 的文件夹,每个 Shapefile 代表一个单独的几何类型。
最后,我们调用子进程来执行 ogr2ogr,传入输出文件类型为 ESRI Shapefile,输出文件夹和 OSM 数据集的名称。
将 Shapefile(矢量)转换为 GeoTiff(栅格)
在格式之间移动数据也包括从矢量到栅格或相反。在这个菜谱中,我们使用 Python 的gdal和ogr模块将数据从矢量(Shapefile)移动到栅格(GeoTiff)。
准备工作
我们需要再次进入我们的虚拟环境,所以启动它,这样我们就可以访问我们在第一章中安装的gdal和ogrPython 模块,设置您的地理空间 Python 环境。
如同往常,使用workon pygeoan_cb命令或此命令进入您的 Python 虚拟环境:
$ source venvs/pygeoan_cb/bin/activate
还需要一个 Shapefile,所以请确保下载源文件并访问/ch03/geodata文件夹(github.com/mdiener21/python-geospatial-analysis-cookbook/archive/master.zip)。
如何操作...
让我们深入进去,将我们的高尔夫球场多边形 Shapefile 转换为 GeoTif;下面是代码:
-
导入
ogr和gdal库,然后定义我们的输出像素大小以及分配给空值的值:#!/usr/bin/env python # -*- coding: utf-8 -*- from osgeo import ogr from osgeo import gdal # set pixel size pixel_size = 1 no_data_value = -9999 -
设置我们想要转换的输入 Shapefile,以及当脚本执行时将创建的新 GeoTiff 栅格:
# Shapefile input name # input projection must be in Cartesian system in meters # input wgs 84 or EPSG: 4326 will NOT work!!! input_shp = r'../geodata/ply_golfcourse-strasslach3857.shp' # TIF Raster file to be created output_raster = r'../geodata/ply_golfcourse-strasslach.tif' -
现在我们需要创建输入 Shapefile 对象,获取图层信息,并最终设置范围值:
# Open the data source get the layer object # assign extent coordinates open_shp = ogr.Open(input_shp) shp_layer = open_shp.GetLayer() x_min, x_max, y_min, y_max = shp_layer.GetExtent() -
在这里,我们需要计算分辨率距离到像素值的转换:
# calculate raster resolution x_res = int((x_max - x_min) / pixel_size) y_res = int((y_max - y_min) / pixel_size) -
我们的新栅格类型是 GeoTiff,因此我们必须明确告诉 GDAL 获取此驱动程序。然后,驱动程序能够通过传递文件名或我们想要创建的新栅格(称为x方向分辨率),然后是y方向分辨率,接着是波段数;在这种情况下,是 1。最后,我们设置了一种新的
GDT_Byte栅格类型:# set the image type for export image_type = 'GTiff' driver = gdal.GetDriverByName(image_type) new_raster = driver.Create(output_raster, x_res, y_res, 1, gdal.GDT_Byte) new_raster.SetGeoTransform((x_min, pixel_size, 0, y_max, 0, -pixel_size)) -
现在我们可以访问新的栅格波段,并为新的栅格分配无数据值和内部数据值。所有内部值都将接收一个值为 255,类似于我们在
burn_values变量中设置的值:# get the raster band we want to export too raster_band = new_raster.GetRasterBand(1) # assign the no data value to empty cells raster_band.SetNoDataValue(no_data_value) # run vector to raster on new raster with input Shapefile gdal.RasterizeLayer(new_raster, [1], shp_layer, burn_values=[255]) -
我们开始了;让我们运行这个脚本来看看我们的新栅格是什么样子:
$ python ch03-05_shp2raster.py
如果您使用QGIS(www.qgis.org)打开,我们的结果栅格应该看起来像以下截图所示:

它是如何工作的...
这个代码涉及几个步骤,所以请跟随,因为一些点可能会导致问题,如果你不确定要输入什么值。我们首先导入gdal和ogr模块,因为它们将通过输入 Shapefile(矢量)和输出 GeoTiff(栅格)为我们完成工作。
pixel_size变量非常重要,因为它将决定我们将创建的新栅格的大小。在这个例子中,我们只有两个多边形,所以我们设置pixel_size = 1以保持它们之间精细的边界。如果你有一个 Shapefile 中跨越全球的许多多边形,更明智的做法是将此值设置为 25 或更多。否则,你可能会得到一个 10GB 的栅格,你的机器将整夜运行!no_data_value是必需的,以告诉 GDAL 在输入多边形周围的空空间中设置什么值,我们将其设置为-9999以便于识别。
接下来,我们简单地设置输入的 Shapefile 存储在 EPSG:3857 Web Mercator 和输出 GeoTiff。如果你想使用其他数据集,请确保相应地更改文件名。我们首先使用 OGR 模块打开 Shapefile 并检索其层信息和范围信息。范围很重要,因为它用于计算输出栅格的宽度和高度值,这些值必须是整数,由x_res和y_res变量表示。
注意
注意,你的 Shapefile 投影必须是米为单位,而不是度。这一点非常重要,因为例如在 EPSG:4326, WGS 84 中,这将不会工作。原因在于坐标单位是经纬度。这意味着 WGS84 不是一个平面投影,不能直接绘制。我们的x_res和y_res值将评估为 0,因为我们无法使用度来获得真实的比例。这是由于我们无法简单地从坐标x中减去坐标y,因为单位是度而不是平面米投影。
现在,让我们继续到栅格设置,我们定义要导出的栅格类型为Gtiff。然后,我们将通过栅格类型获取正确的 GDAL 驱动程序。一旦设置栅格类型,我们就可以创建一个新的空栅格数据集,传入栅格文件名、宽度、栅格的像素高度、栅格波段数,以及最后在 GDAL 术语中的栅格类型,例如gdal.GDT_Byte。这五个参数是创建新栅格的必填项。
接下来,我们调用SetGeoTransform,它处理像素/行栅格空间和投影坐标空间之间的转换。我们希望激活波段 1,因为这是我们栅格中唯一的波段。然后,我们将所有围绕多边形的空空间分配为无数据值。
最后一步是调用gdal.RasterizeLayer()函数,并传入我们的新栅格、波段、Shapefile 以及分配给栅格内部的值。所有在多边形内部的像素将被分配值为 255。
参见
如果你感兴趣,可以访问gdal_rasterize命令行工具www.gdal.org/gdal_rasterize.html。你可以直接从命令行运行它。
使用 GDAL 将栅格(GeoTiff)转换为矢量(Shapefile)
我们已经看到了如何从矢量转换为栅格,现在是时候从栅格转换为矢量了。这种方法更为常见,因为我们的大部分矢量数据都来源于遥感数据,如卫星图像、正射影像或某些其他遥感数据集,如lidar。
准备工作
如同往常,在您的 Python 虚拟环境中输入workon pygeoan_cb命令:
$ source venvs/pygeoan_cb/bin/activate
如何操作...
这个食谱只需要四个步骤利用 OGR 和 GDAL,所以请为您的代码打开一个新文件:
-
导入
ogr和gdal模块,并直接打开我们想要转换的栅格,通过传递磁盘上的文件名并获取一个栅格波段:#!/usr/bin/env python # -*- coding: utf-8 -*- from osgeo import ogr from osgeo import gdal # get raster data source open_image = gdal.Open( "../geodata/cadaster_borders-2tone-black-white.png" ) input_band = open_image.GetRasterBand(3) -
将输出矢量文件设置为 Shapefile 格式,使用 output_shp,然后获取一个 Shapefile 驱动程序。现在,我们可以从我们的驱动程序创建输出,并创建一个图层,如下所示:
# create output data source output_shp = "../geodata/cadaster_raster" shp_driver = ogr.GetDriverByName("ESRI Shapefile") # create output file name output_shapefile = shp_driver.CreateDataSource( output_shp + ".shp" ) new_shapefile = output_shapefile.CreateLayer(output_shp, srs = None ) -
最后一步是运行
gdal.Polygonize函数,它通过将我们的栅格转换为矢量来完成繁重的工作,如下所示:gdal.Polygonize(input_band, None, new_shapefile, -1, [], callback=None) new_shapefile.SyncToDisk() -
按照以下方式执行新脚本:
$ python ch03-06_raster2shp.py
它是如何工作的...
在所有我们的食谱中,使用ogr和gdal的方式相似;我们必须定义输入并获取适当的文件驱动程序来打开文件。GDAL 库非常强大,我们只需一行代码就可以通过gdal.Polygonize函数将栅格转换为矢量。前面的代码仅仅是设置代码,用于定义我们想要使用哪种格式,然后设置适当的驱动程序来输入和输出我们的新文件。
从存储在 Microsoft Excel 中的点数据创建 Shapefile
Excel 文件现在非常普遍,分析师或开发者经常收到需要映射的 Excel 文件。当然,我们可以将其保存为.csv文件,然后使用伟大的 Python 标准csv模块,但这需要额外的手动步骤。我们将看看如何读取一个包含欧洲最高山脉列表的非常简单的 Excel 文件。这个数据集来源于www.geonames.org。
准备工作
我们将需要一个新 Python 库来读取 Microsoft Excel 文件,这个库是xlrd (www.python-excel.org)。
注意
这个库只能读取 Excel 文件;如果您想要写入 Excel 文件,请下载并安装xlwt。
首先,从您的workon pygeoan_cb Linux 机器启动虚拟环境,运行pip install xlrd,然后您就可以开始比赛了。
要写入新的 Shapefile,我们将使用我们在第一章中安装的 pyshp 库,这样就不需要做任何事情。
数据位于您的下载目录中的/ch03/geodata,在您完成这个食谱后,输出 Shapefile 也将被写入这个位置。
如何操作...
因此,让我们从一些代码开始:
-
首先导入
xlrd和 pyshp 模块;注意导入名称是shapefile,而不是模块名称所暗示的 pyshp:#!/usr/bin/env python # -*- coding: utf-8 -*- import xlrd import shapefile -
使用 xlrd 模块打开 Excel 文件,并创建一个变量来保存 Excel 工作表。我们通过索引号引用 Excel 文件中的第一个工作表,始终从第一个工作表的(0)开始:
excel_file = xlrd.open_workbook("../geodata/highest-mountains-europe.xlsx") # get the first sheet sh = excel_file.sheet_by_index(0) -
按照以下方式创建 Shapefile 对象:
w = shapefile.Writer(shapefile.POINT) -
定义新的 Shapefile 字段及其数据类型。F代表浮点数,C代表字符:
w.field('GeoNameId','F') w.field('Name', 'C') w.field('Country', 'C') w.field('Latitude', 'F') w.field('Longitude', 'F') w.field('Altitude', 'F') -
遍历 Excel 文件中的每一行,并创建几何值及其属性:
for row_number in range(sh.nrows): # skips over the first row since it is the header row if row_number == 0: continue else: x_coord = sh.cell_value(rowx=row_number, colx=4) y_coord = sh.cell_value(rowx=row_number, colx=3) w.point(x_coord, y_coord) w.record(GeoNameId=sh.cell_value(rowx=row_number, colx=0), Name=sh.cell_value(rowx=row_number, colx=1), Country=sh.cell_value(rowx=row_number, colx=2), Latitude=sh.cell_value(rowx=row_number, colx=3), Longitude=sh.cell_value(rowx=row_number, colx=4),Altitude=sh.cell_value(rowx=row_number, colx=5)) print "Adding row: " + str(row_number) + " creating mount: " + sh.cell_value(rowx=row_number, colx=1) -
最后,我们将在
/ch03/geodata文件夹中创建新的 Shapefile,如下所示:w.save('../geodata/highest-mountains') -
按照以下方式从命令行执行我们新的
ch03-07_excel2shp.py脚本:$ python ch03-07_excel2shp.py
它是如何工作的...
Python 代码的阅读方式类似于描述代码的工作方式,而且几乎所有的解释都非常简单。我们首先导入新的xlrd模块以及写入 Shapefile 所需的 Shapefile 模块。查看我们的 Excel 文件,我们可以看到哪些字段可用,并定位到x坐标(经度)和y坐标(纬度)的位置。这个位置索引号通过从 0 开始计数来记住起始点。
我们的 Excel 文件还有一个标题行,当然,这个标题行不应该包含在新数据属性中;这就是为什么我们要检查行号是否等于 0——即第一行——然后继续。continue 语句允许代码继续执行而不会出错,并进入else语句,在那里我们定义列的索引位置。每个列都使用pyshp语法引用,通过名称引用列,使代码更容易阅读。
我们调用w.point pyshp 函数来创建点几何形状,传入我们的 x 和 y 坐标作为浮点数。xlrd模块会自动将值转换为浮点数,这很方便。我们最终需要做的只是使用 pyshp 的保存函数将数据写入我们的/ch03/geodata文件夹。不需要添加.shp扩展名;pyshp 会为我们处理并输出.shp、.dbf和.shx。
注意
注意,.prj投影文件不会自动输出。如果您希望将投影信息一起导出,您需要手动创建它,如下所示:
# create the PRJ file
filename = 'highest-mountains'
prj = open("%s.prj" % filename, "w")
epsg = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]'
prj.write(epsg)
prj.close()
将 ESRI ASCII DEM 转换为图像高度图
为了让这一章有一个圆满的结尾,这里是我们迄今为止见过的最复杂的转换,也是最有趣的。输入是一个存储在ASCII格式中的高程数据集,更具体地说,是 Arc/Info ASCII Grid,简称 AAIGrid,文件扩展名为(.asc)。我们的输出是一个高度图图像(en.wikipedia.org/wiki/Heightmap)。高度图图像是一种存储高度高程为像素值的图像。高度图也简单地称为数字高程模型(DEM)。使用图像存储高程数据的优点是它是网络兼容的,我们可以使用它进行 3D 可视化,例如,如第十章中所示,可视化您的分析。
我们需要小心处理输出图像格式,因为仅仅存储 8 位图像将限制我们只能存储 0 到 255 的高度值,这通常是不够的。输出图像应存储至少 16 位,给我们一个从-32,767 到 32,767 的范围。如果我是正确的,地球上最高的山是珠穆朗玛峰,高度为 8,848 米,所以 16 位图像应该足够存储我们的高程数据。
准备工作
运行此练习需要一个 DEM,请确保您已下载了包含在github.com/mdiener21/python-geospatial-analysis-cookbook/archive/master.zip中的代码和地理数据,并下载所需的示例 DEM 进行处理。您不需要在虚拟环境中运行您的脚本,因为此脚本将执行标准 Python 模块和与 GDAL 一起安装的几个 GDAL 内置工具。这仅仅意味着您需要确保您的 GDAL 实用程序已正确安装并在您的机器上运行。(有关参考安装,请参阅第二章,处理投影。)
如何做到这一点...
我们将通过在 Python 脚本中调用由gdal安装的几个 GDAL 实用脚本来执行此脚本:
-
我们将首先导入
subprocess标准模块;这将用于执行我们的 GDAL 实用函数。然后,我们将设置基路径,我们将在这里存储我们的地理数据,包括输入文件、临时文件和输出文件:#!/usr/bin/env python # -*- coding: utf-8 -*- import subprocess from osgeo import gdal path_base = "../geodata/" -
使用伟大的 OSGeo4w 安装程序安装 GDAL 的 Windows 用户可能希望直接指定 GDAL 实用程序的路径,如果它不在 Windows 环境变量中,如下所示:
# gdal_translate converts raster data between different formats command_gdal_translate = "c:/OSGeo4W/bin/gdal_translate.exe" command_gdalinfo = "c:/OSGeo4W/bin/gdalinfo.exe" -
Linux 用户可以使用以下变量:
command_gdal_translate = "gdal_translate" command_gdalinfo = "gdalinfo" command_gdaldem = "gdaldem" -
我们将创建一组变量来存储我们的输入 DEM、输出文件、临时文件以及我们的最终输出文件。这些变量将基路径文件夹与文件名连接起来,如下所示:
orig_dem_asc = path_base + "original_dem.asc" temp_tiff = path_base + "temp_image.tif" output_envi = path_base + "final_envi.bin" -
然后,我们将调用
gdal_translate命令来创建我们的新临时 GeoTiff,如下所示:# transform dem to tiff dem2tiff = command_gdal_translate + " " + orig_dem_asc + " " + temp_tiff print ("now executing this command: " + dem2tiff) subprocess.call(dem2tiff.split(), shell=False) -
接下来,我们将打开临时 GeoTiff,读取关于 tiff 的信息,以找出存储在我们数据中的最小和最大高度值。这虽然不是完成脚本所必需的,但非常有用,可以帮助你识别最大和最小高度值:
ds = gdal.Open(temp_tiff, gdal.GA_ReadOnly) band = ds.GetRasterBand(1) print 'Band Type=', gdal.GetDataTypeName(band.DataType) min = band.GetMinimum() max = band.GetMaximum() if min is None or max is None: (min, max) = band.ComputeRasterMinMax(1) print 'Min=%.3f, Max=%.3f' % (min, max) min_elevation = str(int(round(min))) max_elevation = str(int(round(max))) -
然后,使用以下参数调用
gdal_translate工具,将缩放范围从原始的最小/最大值设置为新的范围,从 0 到 65,535 个值。指定-ot输出类型为 vENVI 格式,使用我们的临时 GeoTiff 作为输入:tif_2_envi = command_gdal_translate + " -scale -ot UInt16 -outsize 500 500 -of ENVI " \ + temp_tiff + " " + output_envi -
让我们从命令行运行我们新的
ch03-08_dem2heightmap.py脚本:subprocess.call(tif_2_envi.split(),shell=False) -
让我们从命令行运行我们新的
ch03-08_dem2heightmap.py脚本:python ch03-08_dem2heightmap.py
结果是,你会在/ch03/geodata/文件夹中找到一个名为.bin 的新文件,该文件存储了你的新的 ENVI 16 位图像,包括所有你的高程数据。现在,这个高度图可以用于你的 3D 软件,例如 Blender(www.blender.org)、Unity(www.unity3d.com),或者在一个更酷的 Web 应用程序中使用 JavaScript 库,如threejs。
它是如何工作的...
让我们从导入开始,然后指定我们的输入和输出存储的基本路径。之后,我们将看到实际使用的gdal_translate转换命令。Windows 和 Linux 的命令由你自己决定是否使用,这取决于你如何设置你的机器。然后,我们设置变量来定义输入 DEM、临时 GeoTiff 和输出 ENVI 高度图图像。
最后,我们可以使用gdal_translate工具将我们的 DEM ASCII 文件转换为 GeoTiff 格式的第一次转换。现在为了获取我们数据的一些信息,我们将最小和最大高度值打印到屏幕上。在转换过程中,这非常有用,可以让你检查输出数据是否确实包含了输入的高度值,并且在转换过程中没有出现错误。
最后,我们只需再次调用gdal_translate工具,将我们的 GeoTiff 转换为 ENVI 高度图图像。-scale参数没有参数时,会自动将我们的 16 位图像填充为从 0 到 65,535 的值。下一个参数是-ot,指定输出类型为 16 位,后面跟着-outsize 500 500,设置输出图像大小为 500 x 500 像素。最后,-of ENVI是我们的输出格式,后面跟着输入 GeoTiff 的名称和输出高度图的名称。
使用 DEM 时的一个典型工作流程如下:
-
下载一个 DEM,通常是一个非常大的文件,覆盖一个大的地理区域。
-
将 DEM 裁剪到较小的感兴趣区域。
-
将裁剪区域转换为另一种格式。
-
将 DEM 导出为高度图图像。
注意
我们介绍了 .split() 方法,它将返回一个由字符分隔的 Python 字符串列表。在我们的例子中,分隔字符是一个 单个空格 字符,但你也可以根据任何其他字符或字符组合进行分割(请参阅 Python 文档中的docs.python.org/2/library/string.html#string.split)。这有助于我们减少在代码中需要执行的连接操作数量。
第四章. 使用 PostGIS
在本章中,我们将涵盖以下主题:
-
执行 PostGIS ST_Buffer 分析查询并将其导出为 GeoJSON
-
查找点是否在多边形内部
-
使用 ST_Node 在交点处分割 LineStrings
-
检查 LineStrings 的有效性
-
执行空间连接并将点属性分配给多边形
-
使用 ST_Distance() 进行复杂的空间分析查询
简介
空间数据库不过是一个可以存储几何数据并在其最简单形式下执行空间查询的标准数据库。我们将探讨如何从我们的 Python 代码中运行空间分析查询、处理连接等,以及更多内容。你回答诸如“我想定位所有距离高尔夫球场 2 公里以内且距离公园不到 5 公里的酒店”这样的空间问题的能力,正是 PostGIS 发挥作用的地方。这种将请求链入模型的过程正是空间分析力量的体现。
我们将使用最受欢迎和功能强大的开源空间数据库 PostgreSQL,以及 PostGIS 扩展,包括超过 150 个函数。基本上,我们将获得一个功能齐全的 GIS,具有复杂的空间分析功能,适用于矢量和栅格数据,以及多种移动空间数据的方法。
如果你需要更多关于 PostGIS 的信息以及一本好书,请查看由 Paolo Corti 编著的 PostGIS Cookbook(可在 www.packtpub.com/big-data-and-business-intelligence/postgis-cookbook 购买)。这本书探讨了 PostGIS 的更广泛用途,并包括一个关于使用 Python 进行 PostGIS 编程的完整章节。
执行 PostGIS ST_Buffer 分析查询并将其导出为 GeoJSON
让我们从执行我们的第一个空间分析查询开始,该查询针对我们已运行的 PostgreSQL 和 PostGIS 数据库。目标是生成所有学校的 100 米缓冲区,并将新的缓冲多边形导出为 GeoJSON,包括学校的名称。最终结果将显示在这张地图上,可在 GitHub 上找到(github.com/mdiener21/python-geospatial-analysis-cookbook/blob/master/ch04/geodata/out_buff_100m.geojson)。
小贴士
使用 GitHub 快速可视化 GeoJSON 数据是一种快速简单的方法,无需编写任何代码即可创建网络地图。请注意,如果你使用的是公共免费的 GitHub 账户,那么数据将免费供其他人下载。私有 GitHub 账户意味着如果数据隐私或敏感性是一个问题,那么 GeoJSON 数据也将保持私有。

准备工作
要开始,我们将使用 PostGIS 数据库中的数据。我们将从访问我们上传到 PostGIS 的 schools 表开始,这是在 第三章,将空间数据从一个格式转换为另一个格式 中的 ogr2ogr 脚本中完成的批量导入文件夹。
连接到 PostgreSQL 和 PostGIS 数据库是通过 Psycopg 实现的,这是一个 Python DB API (initd.org/psycopg/)。我们已经在 第一章,设置你的地理空间 Python 环境 中安装了它,包括 PostgreSQL、Django 和 PostGIS。
对于所有后续的食谱,请进入你的虚拟环境 pygeoan_cb,这样你就可以使用此命令访问你的库:
workon pygeoan_cb
如何做到这一点...
-
长路并不那么长,所以请跟随:
#!/usr/bin/env python # -*- coding: utf-8 -*- import psycopg2 import json from geojson import loads, Feature, FeatureCollection # NOTE change the password and username # Database Connection Info db_host = "localhost" db_user = "pluto" db_passwd = "stars" db_database = "py_geoan_cb" db_port = "5432" # connect to DB conn = psycopg2.connect(host=db_host, user=db_user, port=db_port, password=db_passwd, database=db_database) # create a cursor cur = conn.cursor() # the PostGIS buffer query buffer_query = """SELECT ST_AsGeoJSON(ST_Transform( ST_Buffer(wkb_geometry, 100,'quad_segs=8'),4326)) AS geom, name FROM geodata.schools""" # execute the query cur.execute(buffer_query) # return all the rows, we expect more than one dbRows = cur.fetchall() # an empty list to hold each feature of our feature collection new_geom_collection = [] # loop through each row in result query set and add to my feature collection # assign name field to the GeoJSON properties for each_poly in dbRows: geom = each_poly[0] name = each_poly[1] geoj_geom = loads(geom) myfeat = Feature(geometry=geoj_geom, properties={'name': name}) new_geom_collection.append(myfeat) # use the geojson module to create the final Feature Collection of features created from for loop above my_geojson = FeatureCollection(new_geom_collection) # define the output folder and GeoJSon file name output_geojson_buf = "../geodata/out_buff_100m.geojson" # save geojson to a file in our geodata folder def write_geojson(): fo = open(output_geojson_buf, "w") fo.write(json.dumps(my_geojson)) fo.close() # run the write function to actually create the GeoJSON file write_geojson() # close cursor cur.close() # close connection conn.close()
它是如何工作的...
数据库连接正在使用 pyscopg2 模块,因此我们在开始时与 geojson 和标准的 json 模块一起导入库,以处理我们的 GeoJSON 导出。
我们创建连接后立即使用我们的 SQL 缓冲查询字符串。该查询使用了三个 PostGIS 函数。从内到外逐步工作,你会看到 ST_Buffer 函数接收学校点的几何形状,然后是 100 米的缓冲距离以及我们想要生成的圆段数量。然后 ST_Transform 函数将新创建的缓冲几何形状转换成 WGS84 坐标系统(EPSG: 4326),这样我们就可以在 GitHub 上显示它,GitHub 只显示 WGS84 和投影的 GeoJSON。最后,我们将使用 ST_asGeoJSON 函数将我们的几何形状导出为 GeoJSON 几何形状。
注意
PostGIS 不导出完整的 GeoJSON 语法,只以 GeoJSON 几何形状的形式导出几何形状。这就是为什么我们需要使用 Python geojson 模块来完成我们的 GeoJSON 的原因。
所有这些都意味着我们不仅对查询进行操作,而且我们还一次性指定了输出格式和坐标系。
接下来,我们将执行查询并使用 cur.fetchall() 获取所有返回的对象,这样我们就可以稍后遍历每个返回的缓冲多边形。我们的 new_geom_collection 列表将存储每个新的几何形状和特征名称。接下来,在 for 循环函数中,我们将使用 geojson 模块函数 loads(geom) 将我们的几何形状输入到一个 GeoJSON 几何对象中。这随后由 Feature() 函数创建我们的 GeoJSON 特征。然后它被用作 FeatureCollection 函数的输入,最终创建完成的 GeoJSON。
最后,我们需要将这个新的 GeoJSON 文件写入磁盘并保存。因此,我们将使用新的文件对象,在那里我们使用标准的 Python json.dumps 模块导出我们的 FeatureCollection。
我们将进行一些清理工作,以关闭游标对象和连接。Bingo!我们现在完成了,可以可视化我们的最终结果。
查找点是否在多边形内
多边形内点分析查询是一个非常常见的空间操作。此查询可以识别位于区域内的对象,例如多边形。在这个例子中,感兴趣的区域是围绕自行车道的 100 米缓冲多边形,我们希望定位所有位于这个多边形内的学校。
准备工作
在上一节中,我们使用了schools表来创建缓冲区。这次,我们将使用这个表作为我们的输入点表。我们在第三章中导入的bikeways表,即将空间数据从一种格式转换为另一种格式,将用作我们的输入线以生成一个新的 100 米缓冲多边形。但是,请确保您在本地 PostgreSQL 数据库中有这两个数据集。
如何做...
-
现在,让我们深入研究一些代码,以找到位于自行车道 100 米范围内的学校,以便找到多边形内的点:
#!/usr/bin/env python # -*- coding: utf-8 -*- import json import psycopg2 from geojson import loads, Feature, FeatureCollection # Database Connection Info db_host = "localhost" db_user = "pluto" db_passwd = "stars" db_database = "py_geoan_cb" db_port = "5432" # connect to DB conn = psycopg2.connect(host=db_host, user=db_user, port=db_port, password=db_passwd, database=db_database) # create a cursor cur = conn.cursor() # uncomment if needed # cur.execute("Drop table if exists geodata.bikepath_100m_buff;") # query to create a new polygon 100m around the bikepath new_bike_buff_100m = """ CREATE TABLE geodata.bikepath_100m_buff AS SELECT name, ST_Buffer(wkb_geometry, 100) AS geom FROM geodata.bikeways; """ # run the query cur.execute(new_bike_buff_100m) # commit query to database conn.commit() # query to select schools inside the polygon and output geojson is_inside_query = """ SELECT s.name AS name, ST_AsGeoJSON(ST_Transform(s.wkb_geometry,4326)) AS geom FROM geodata.schools AS s, geodata.bikepath_100m_buff AS bp WHERE ST_WITHIN(s.wkb_geometry, bp.geom); """ # execute the query cur.execute(is_inside_query) # return all the rows, we expect more than one db_rows = cur.fetchall() # an empty list to hold each feature of our feature collection new_geom_collection = [] def export2geojson(query_result): """ loop through each row in result query set and add to my feature collection assign name field to the GeoJSON properties :param query_result: pg query set of geometries :return: new geojson file """ for row in db_rows: name = row[0] geom = row[1] geoj_geom = loads(geom) myfeat = Feature(geometry=geoj_geom, properties={'name': name}) new_geom_collection.append(myfeat) # use the geojson module to create the final Feature # Collection of features created from for loop above my_geojson = FeatureCollection(new_geom_collection) # define the output folder and GeoJSon file name output_geojson_buf = "../geodata/out_schools_in_100m.geojson" # save geojson to a file in our geodata folder def write_geojson(): fo = open(output_geojson_buf, "w") fo.write(json.dumps(my_geojson)) fo.close() # run the write function to actually create the GeoJSON file write_geojson() export2geojson(db_rows)
您现在可以在 Mapbox 创建的一个很棒的网站上查看您新创建的 GeoJSON 文件,网址是www.geojson.io。只需将您的 GeoJSON 文件从 Windows 的 Windows Explorer 或 Ubuntu 的 Nautilus 拖放到www.geojson.io网页上,Bob's your uncle,您应该能看到大约 50 所学校,这些学校位于温哥华的自行车道 100 米范围内。

它是如何工作的...
我们将重用代码来建立数据库连接,所以这一点现在应该对您来说很熟悉。new_bike_buff_100m查询字符串包含我们生成围绕所有自行车道的 100 米缓冲多边形的查询。我们需要执行此查询并将其提交到数据库,以便我们可以访问这个新的多边形集作为我们实际查询的输入,该查询将找到位于这个新缓冲多边形内的学校(点)。
is_inside_query字符串实际上为我们做了艰苦的工作,通过从name字段选择值和从geom字段选择几何形状。几何形状被封装在另外两个 PostGIS 函数中,以便我们可以将数据作为 GeoJSON 在 WGS 84 坐标系中导出。这将是我们生成最终新的 GeoJSON 文件所需的输入几何形状。
WHERE子句使用ST_Within函数来查看一个点是否在多边形内,如果点在缓冲多边形内,则返回True。
现在,我们已经创建了一个新的函数,它只是封装了之前在执行 PostGIS ST_Buffer 分析查询并将其导出为 GeoJSON的配方中使用的导出 GeoJSON 代码。这个新的export2geojson函数只需一个 PostGIS 查询的输入,并输出一个 GeoJSON 文件。要设置新输出文件的名字和位置,只需在函数内替换路径和名称。
最后,我们只需要调用新的函数,使用包含我们学校列表的db_rows变量来导出 GeoJSON 文件,这些学校位于 100 米缓冲多边形内。
还有更多...
这个示例,找到所有位于自行车道 100 米范围内的学校,可以使用另一个名为ST_Dwithin的 PostGIS 函数来完成。
选择所有位于自行车道 100 米范围内的学校的 SQL 语句看起来像这样:
SELECT * FROM geodata.bikeways as b, geodata.schools as s where ST_DWithin(b.wkb_geometry, s.wkb_geometry, 100)
使用 ST_Node 在交叉口分割 LineStrings
处理道路数据通常是一件棘手的事情,因为数据的有效性和数据结构起着非常重要的作用。如果你想对你的道路数据做些有用的事情,比如构建一个路由网络,你首先需要准备数据。第一个任务通常是分割你的线条,这意味着在线条交叉的交叉口处分割所有线条,创建一个基础网络道路数据集。
注意
注意,这个菜谱将分割所有交叉口上的所有线条,无论是否例如,有一个道路-桥梁立交桥,不应该创建交叉口。
准备工作
在我们详细介绍如何做之前,我们将使用 OpenStreetMap(OSM)道路数据的一个小部分作为我们的示例。OSM 数据位于你的/ch04/geodata/文件夹中,名为vancouver-osm-data.osm。这些数据是从www.openstreetmap.org主页上使用位于页面顶部的导出按钮简单下载的:

OSM 数据不仅包含道路,还包含我选择的范围内所有其他点和多边形。感兴趣的区域再次是温哥华的 Burrard Street 桥。
我们需要提取所有道路并将它们导入我们的 PostGIS 表中。这次,让我们尝试直接从控制台使用ogr2ogr命令行上传 OSM 街道到我们的 PostGIS 数据库:
ogr2ogr -lco SCHEMA=geodata -nlt LINESTRING -f "PostgreSQL" PG:"host=localhost port=5432 user=pluto dbname=py_geoan_cb password=stars" ../geodata/vancouver-osm-data.osm lines -t_srs EPSG:3857
这假设你的 OSM 数据位于/ch04/geodata文件夹中,并且命令是在你位于/ch04/code文件夹时运行的。
现在这个非常长的东西意味着我们将连接到我们的 PostGIS 数据库作为输出,并将vancouver-osm-data.osm文件作为输入。创建一个名为lines的新表,并将输入的 OSM 投影转换为 EPSG:3857。所有从 OSM 导出的数据都在 EPSG:4326 中。当然,你可以保持在这个系统中,只需简单地删除命令行选项中的-t_srs EPSG:3857部分。
现在我们已经准备好在交叉口进行分割操作了。如果你愿意,可以打开数据在QGIS(量子 GIS)中。在 QGIS 中,你会看到道路数据并没有在所有交叉口处分割,就像这个截图所示:

这里,你可以看到McNicoll Avenue是一条单独的 LineString,横跨Cypress Street。完成我们的操作后,我们会看到McNicoll Avenue将在这个交叉口处被分割。
如何操作...
-
由于所有的工作都在一个 SQL 查询中完成,运行 Python 代码相当直接。所以请继续:
#!/usr/bin/env python # -*- coding: utf-8 -*- import psycopg2 import json from geojson import loads, Feature, FeatureCollection # Database Connection Info db_host = "localhost" db_user = "pluto" db_passwd = "stars" db_database = "py_geoan_cb" db_port = "5432" # connect to DB conn = psycopg2.connect(host=db_host, user=db_user, port=db_port, password=db_passwd, database=db_database) # create a cursor cur = conn.cursor() # drop table if exists # cur.execute("DROP TABLE IF EXISTS geodata.split_roads;") # split lines at intersections query split_lines_query = """ CREATE TABLE geodata.split_roads (ST_Node(ST_Collect(wkb_geometry)))).geom AS geom FROM geodata.lines;""" cur.execute(split_lines_query) conn.commit() cur.execute("ALTER TABLE geodata.split_roads ADD COLUMN id serial;") cur.execute("ALTER TABLE geodata.split_roads ADD CONSTRAINT split_roads_pkey PRIMARY KEY (id);") # close cursor cur.close() # close connection conn.close()![如何操作...]()
好吧,这相当简单,我们现在可以看到 McNicoll Avenue 在与 Cypress Street 的交点处被分割。
它是如何工作的...
从代码中我们可以看到,数据库连接保持不变,唯一的新事物就是创建交点的查询本身。在这里,使用了三个独立的 PostGIS 函数来获取我们的结果:
-
第一个函数,在查询中从内到外工作时,从
ST_Collect(wkb_geometry)开始。这仅仅是将我们的原始几何形状列作为输入。这里只是简单地将几何形状组合在一起。 -
接下来是使用
ST_Node(geometry)实际分割线段,输入新的几何形状集合并进行节点操作,这将在交点处分割我们的 LineStrings。 -
最后,我们将使用
ST_Dump()作为返回集合的函数。这意味着它基本上将所有的 LineString 几何形状集合爆炸成单个 LineStrings。查询末尾的.geom指定我们只想导出几何形状,而不是分割几何形状返回的数组数字。
现在,我们将执行并提交查询到数据库。提交是一个重要的部分,因为否则查询将会运行,但它实际上不会创建我们想要生成的新的表。最后但同样重要的是,我们可以关闭游标和连接。就是这样;我们现在有了分割的 LineStrings。
注意
注意,新的分割 LineStrings 不包含街道名称和其他属性。要导出名称,我们需要在数据上执行连接操作。这样的查询,包括在新建的 LineStrings 上的属性,可能看起来像这样:
CREATE TABLE geodata.split_roads_attributes AS SELECT
r.geom,
li.name,
li.highway
FROM
geodata.lines li,
geodata.split_roads r
WHERE
ST_CoveredBy(r.geom, li.wkb_geometry)
检查 LineStrings 的有效性
处理道路数据有许多需要注意的区域,其中之一就是无效的几何形状。我们的源数据是 OSM,因此是由一群未经 GIS 专业人员培训的用户收集的,这导致了错误。为了执行空间查询,数据必须是有效的,否则我们将得到有错误或根本没有结果的结果。
PostGIS 包含了 ST_isValid() 函数,该函数根据几何形状是否有效返回 True/False。还有一个 ST_isValidReason() 函数,它会输出几何形状错误的文本描述。最后,ST_isValidDetail() 函数将返回几何形状是否有效,以及几何形状错误的理由和位置。这三个函数都完成类似的任务,选择哪一个取决于你想要完成什么。
如何操作...
-
现在,为了确定
geodata.lines是否有效,我们将运行另一个查询,如果存在无效的几何形状,它将列出所有这些几何形状:#!/usr/bin/env python # -*- coding: utf-8 -*- import psycopg2 # Database Connection Info db_host = "localhost" db_user = "pluto" db_passwd = "stars" db_database = "py_geoan_cb" db_port = "5432" # connect to DB conn = psycopg2.connect(host=db_host, user=db_user, port=db_port, password=db_passwd, database=db_database) # create a cursor cur = conn.cursor() # the PostGIS buffer query valid_query = """SELECT ogc_fid, ST_IsValidDetail(wkb_geometry) FROM geodata.lines WHERE NOT ST_IsValid(wkb_geometry); """ # execute the query cur.execute(valid_query) # return all the rows, we expect more than one validity_results = cur.fetchall() print validity_results # close cursor cur.close() # close connection conn.close();
此查询应返回一个空 Python 列表,这意味着我们没有无效的几何形状。如果您的列表中有对象,那么您就会知道您需要做一些手动工作来纠正这些几何形状。您最好的选择是启动 QGIS 并使用数字化工具开始清理。
执行空间连接并将点属性分配给多边形
现在,我们将回到一些更多的高尔夫动作,我们想要执行一个空间属性连接。我们面临的情况是有一些多边形,在这种情况下,这些是以高尔夫球道的形式出现的,没有任何洞号。我们的洞号存储在一个点数据集中,该数据集位于每个洞的球道内。我们希望根据多边形内的位置为每个球道分配适当的洞号。
位于加利福尼亚州莫尼卡市的佩布尔海滩高尔夫球场的 OSM 数据是我们的源数据。这个高尔夫球场是 PGA 巡回赛上的顶级高尔夫球场之一,在 OSM 中得到了很好的映射。
小贴士
如果您对从 OSM 获取高尔夫球场数据感兴趣,建议您使用优秀的 Overpass API,网址为overpass-turbo.eu/。此网站允许您将 OSM 数据导出为 GeoJSON 或 KML 等格式。
要下载所有特定于高尔夫的 OSM 数据,您需要纠正标签。为此,只需将以下 Overpass API 查询复制并粘贴到左侧的查询窗口中,然后点击下载:
/*
This query looks for nodes, ways, and relations
using the given key/value combination.
Choose your region and hit the Run button above!
*/
[out:json][timeout:25];
// gather results
(
// query part for: "leisure=golf_course"
node"leisure"="golf_course";
way"leisure"="golf_course";
relation"leisure"="golf_course";
node"golf"="pin";
way"golf"="green";
way"golf"="fairway";
way"golf"="tee";
way"golf"="fairway";
way"golf"="bunker";
way"golf"="rough";
way"golf"="water_hazard";
way"golf"="lateral_water_hazard";
way"golf"="out_of_bounds";
way"golf"="clubhouse";
way"golf"="ground_under_repair";
);
// print results
out body;
>;
out skel qt;
准备工作
将我们的数据导入 PostGIS 将是执行空间查询的第一步。这次,我们将使用shp2pgsql工具将我们的数据导入,以改变一下方式,因为将数据导入 PostGIS 的方法有很多。shp2pgsql工具无疑是导入 Shapefiles 到 PostGIS 最经过测试和最常用的方法。让我们开始,再次执行此导入操作,直接从命令行运行此工具。
对于 Windows 用户,这应该可以工作,但请检查路径是否正确,或者shp2pgsql.exe是否已添加到您的系统路径变量中。这样做可以节省输入完整路径来执行操作。
注意
我假设您在/ch04/code文件夹中运行以下命令:
shp2pgsql -s 4326 ..\geodata\shp\pebble-beach-ply-greens.shp geodata.pebble_beach_greens | psql -h localhost -d py_geoan_cb -p 5432 -U pluto
在 Linux 机器上,您的命令基本上与 Windows 相同,没有长路径,前提是您在第一章 设置您的地理空间 Python 环境 中安装 PostGIS 时已设置好系统链接。
接下来,我们需要导入带有属性的点,让我们按照以下步骤进行:
shp2pgsql -s 4326 ..\geodata\shp\pebble-beach-pts-hole-num-green.shp geodata.pebble_bea-ch_hole_num | psql -h localhost -d py_geoan_cb -p 5432 -U postgres
那就是了!我们现在在我们的 PostGIS 模式geodata设置中有了点和多边形,这为我们的空间连接做好了准备。
如何操作...
-
核心工作再次在我们的 PostGIS 查询字符串内部完成,将属性分配给多边形,所以请跟随:
#!/usr/bin/env python # -*- coding: utf-8 -*- import psycopg2 # Database Connection Info db_host = "localhost" db_user = "pluto" db_passwd = "stars" db_database = "py_geoan_cb" db_port = "5432" # connect to DB conn = psycopg2.connect(host=db_host, user=db_user, port=db_port, password=db_passwd, database=db_database) # create a cursor cur = conn.cursor() # assign polygon attributes from points spatial_join = """ UPDATE geodata.pebble_beach_greens AS g SET name = h.name FROM geodata.pebble_beach_hole_num AS h WHERE ST_Contains(g.geom, h.geom); """ cur.execute(spatial_join) conn.commit() # close cursor cur.close() # close connection conn.close()
它是如何工作的...
查询非常直接;我们将使用UPDATE标准 SQL 命令来更新我们表geodata.pebble_beach_greens中名称字段中的值,这些值位于pebble_beach_hole_num表中。
我们接着设置geodata.pebble_beach_hole_num表中的名称值,其中字段名称也存在并包含我们需要的属性值。
我们的WHERE子句使用 PostGIS 查询ST_Contains,如果点位于我们的绿色区域内部,则返回True,如果是这样,它将更新我们的值。
这很简单,展示了空间关系强大的功能。
使用 ST_Distance()执行复杂的空间分析查询
现在,让我们检查一个更复杂的 PostGIS 查询,以激发我们的空间分析热情。我们想要定位所有位于国家公园或保护区内部或 5 公里范围内的高尔夫球场。此外,高尔夫球场必须在 2 公里范围内有城市。城市数据来自 OSM 中的标签,其中标签 place = city。
此查询的国家公园和保护区属于加拿大政府。我们的高尔夫球场和城市数据集来源于位于不列颠哥伦比亚省和艾伯塔省的 OSM。
准备工作
我们需要加拿大所有国家公园和保护区的数据,所以请确保它们位于/ch04/geodata/文件夹中。
原始数据位于ftp2.cits.rncan.gc.ca/pub/geott/frameworkdata/protected_areas/1M_PROTECTED_AREAS.shp.zip,如果您还没有从 GitHub 下载/geodata文件夹。
需要的其他数据集包括可以从 OSM 获取的城市和高尔夫球场。这两个文件是位于/ch04/geodata/文件夹中的 GeoJSON 文件,分别命名为osm-golf-courses-bc-alberta.geojson和osm-place-city-bc-alberta.geojson。
我们现在将导入下载的数据到我们的数据库中:
注意
确保你在运行以下命令时当前位于/ch04/code文件夹中;否则,根据需要调整路径。
-
从不列颠哥伦比亚省和艾伯塔省的 OSM 高尔夫球场开始,运行这个命令行调用 ogr2ogr。Windows 用户需要注意,他们可以将反斜杠切换为正斜杠,或者包含完整的路径到 GeoJSON:
ogr2ogr -f PostgreSQL PG:"host=localhost user=postgres port=5432 dbname=py_geoan_cb password=air" ../geodata/geojson/osm-golf-courses-bc-alberta.geojson -nln geodata.golf_courses_bc_alberta -
现在,我们将再次运行相同的命令来导入城市:
ogr2ogr -f PostgreSQL PG:"host=localhost user=postgres port=5432 dbname=py_geoan_cb password=air" ../geodata/geojson/osm-place-city-bc-alberta.geojson -nln geodata.cities_bc_alberta -
最后但同样重要的是,我们需要使用
shp2pgsql命令行导入加拿大的保护区和国家公园。在此,请注意,我们需要使用-W latin1选项来指定所需的编码。您获得的数据是整个加拿大,而不仅仅是 BC 和艾伯塔省:shp2pgsql -s 4326 -W latin1 ../geodata/shp/protarea.shp geodata.parks_pa_canada | psql -h localhost -d py_geoan_cb -p 5432 -U pluto
现在我们数据库中有所有三个表,我们可以执行我们的分析脚本。
如何做到这一点...
-
让我们看看代码的样子:
#!/usr/bin/env python # -*- coding: utf-8 -*- import psycopg2 import json import pprint from geojson import loads, Feature, FeatureCollection # Database Connection Info db_host = "localhost" db_user = "pluto" db_passwd = "stars" db_database = "py_geoan_cb" db_port = "5432" # connect to DB conn = psycopg2.connect(host=db_host, user=db_user, port=db_port, password=db_passwd, database=db_database) # create a cursor cur = conn.cursor() complex_query = """ SELECT ST_AsGeoJSON(st_centroid(g.wkb_geometry)) as geom, c.name AS city, g.name AS golfclub, p.name_en AS park, ST_Distance(geography(c.wkb_geometry), geography(g.wkb_geometry)) AS distance, ST_Distance(geography(p.geom), geography(g.wkb_geometry)) AS distance FROM geodata.parks_pa_canada AS p, geodata.cities_bc_alberta AS c JOIN geodata.golf_courses_bc_alberta AS g ON ST_DWithin(geography(c.wkb_geometry), geography(g.wkb_geometry),4000) WHERE ST_DWithin(geography(p.geom), geography(g.wkb_geometry),5000) """ # WHERE c.population is not null and e.name is not null # execute the query cur.execute(complex_query) # return all the rows, we expect more than one validity_results = cur.fetchall() # an empty list to hold each feature of our feature collection new_geom_collection = [] # loop through each row in result query set and add to my feature collection # assign name field to the GeoJSON properties for each_result in validity_results: geom = each_result[0] city_name = each_result[1] course_name = each_result[2] park_name = each_result[3] dist_city_to_golf = each_result[4] dist_park_to_golf = each_result[5] geoj_geom = loads(geom) myfeat = Feature(geometry=geoj_geom, properties={'city': city_name, 'golf_course': course_name, 'park_name': park_name, 'dist_to city': dist_city_to_golf, 'dist_to_park': dist_park_to_golf}) new_geom_collection.append(myfeat) # use the geojson module to create the final Feature Collection of features created from for loop above my_geojson = FeatureCollection(new_geom_collection) pprint.pprint(my_geojson) # define the output folder and GeoJSon file name output_geojson_buf = "../geodata/golfcourses_analysis.geojson" # save geojson to a file in our geodata folder def write_geojson(): fo = open(output_geojson_buf, "w") fo.write(json.dumps(my_geojson)) fo.close() # run the write function to actually create the GeoJSON file write_geojson() # close cursor cur.close() # close connection conn.close()
它是如何工作的...
让我们一步一步地通过 SQL 查询:
-
我们将从定义查询需要返回的列以及从哪些表中获取开始。在这里,我们将定义我们想要高尔夫球场的几何形状作为一个点、城市名称、高尔夫球场名称、公园名称、城市与高尔夫球场之间的距离,以及最终,公园与高尔夫球场之间的距离。我们返回的几何形状是高尔夫球场作为一个点,因此使用
ST_Centroid,它返回高尔夫球场的中心点,然后将其作为 GeoJSON 几何形状输出。 -
FROM子句设置了我们的公园和城市表,并使用SQL AS为它们分配一个别名。然后我们根据距离使用ST_DWithin()来JOIN高尔夫球场,以便我们可以定位城市与高尔夫球场之间小于 4 公里的距离。 -
WHERE子句中的ST_DWithin()强制执行最后一个要求,即公园与高尔夫球场之间的距离不能超过 5 公里。
SQL 完成了所有繁重的工作,以返回正确的空间分析结果。下一步是使用 Python 将我们的结果输出为有效的 GeoJSON,以便我们可以查看我们新发现的高尔夫球场。每个属性属性随后通过其在查询中的数组位置被识别,并为 GeoJSON 输出分配一个名称。最后,我们将输出一个.geojson文件,您可以直接在 GitHub 上可视化它,链接为github.com/mdiener21/python-geospatial-analysis-cookbook/blob/master/ch04/geodata/golfcourses_analysis.geojson。

第五章. 向量分析
在本章中,我们将涵盖以下主题:
-
将线字符串裁剪到感兴趣的区域
-
用线分割多边形
-
使用线性参照找到线上的点位置
-
将点捕捉到最近的线上
-
计算三维地面距离和总海拔升高
介绍
向量数据分析被应用于许多应用领域,从测量点 A 到点 B 的距离一直到复杂的路由算法。最早的 GIS 系统是基于向量数据和向量分析构建的,后来扩展到栅格域。在本章中,我们将从简单的向量操作开始,然后逐步深入到一个更复杂的模型,将各种向量方法串联起来,以提供回答我们空间问题的新的数据。
这种数据分析过程被分解为几个步骤,从输入数据集开始,对数据进行空间操作,如缓冲区分析,最后,我们将有一些输出,以新的数据集的形式。以下图表显示了分析流程在 simplest 模型形式中的流程:

将简单问题转换为空间操作方法和模型需要经验,并且不是一项简单的任务。例如,你可能会遇到一个简单的任务,比如,“识别并定位受洪水影响的住宅地块数量。”这会转化为以下内容:
-
首先,一个以洪水多边形形式存在的输入数据集,它定义了受影响洪水区域
-
其次,输入数据集表示地籍多边形
-
我们的空间操作是一个交集函数
-
所有这些都会导致一个新的多边形数据集
这将导致一个可能看起来像这样的空间模型:

为了解决更复杂的问题,空间建模简单地将更多的输入和更多的操作链在一起,这些操作输出新的数据,并输入到其他新的操作中。这最终导致一组或几组数据。
将线字符串裁剪到感兴趣的区域
一个涉及空间数据的工程项目通常在指定的边界区域内进行地理限制,即所谓的项目区域。输入数据可能来自多个来源,通常超出项目区域。移除这些多余数据有时对于加快空间处理速度至关重要,同时,它也减少了数据量。数据量的减少也可能导致二级加速,例如,减少数据传输或复制的耗时。
在这个菜谱中,我们将使用表示圆形 Shapefile 的边界多边形,然后移除所有超出这个圆的额外 LineStrings。
这个裁剪过程将移除所有位于裁剪区域之外的所有线条——即,我们的感兴趣的项目区域。
注意
一个名为clip的标准函数执行空间交集操作。这与正常的交集函数略有不同。裁剪将不会或不应保留附加到裁剪区域上的属性。裁剪涉及两个输入数据集;第一个定义了我们想要裁剪数据到的边界,第二个定义了将被裁剪的数据。这两个集合都包含属性,而裁剪边界的这些属性通常不包括在裁剪操作中。
新的裁剪数据将只包含原始输入数据集的属性,不包括裁剪多边形的所有属性。
intersection函数将找到重叠的几何形状,并仅输出圆内的线条。为了更好地演示这个概念,以下图形表示了我们将要实现的内容。
为了演示简单的裁剪操作,我们将使用一条 LineString 和一个定义裁剪边界的多边形,并执行快速交集操作。结果将类似于以下截图所示,您可以在浏览器中将其视为实时网络地图。请参考位于/code/html/ch05-01-clipping.html的 HTML 文件以查看结果。

当运行简单的intersection函数时,线条将被切割成两个新的 LineStrings,如前一个截图所示。
我们的第二个结果将使用两个代表输入的 Shapefiles。我们的真实数据OpenStreetMapconverted被转换为 Shapefile 格式,用于我们的输入和输出。圆定义了我们感兴趣的多边形区域,而道路 LineStrings 是我们想要裁剪的部分。我们的结果将以新的 Shapefile 的形式呈现,只显示圆内的道路。
准备工作
这个食谱分为两部分。第一部分是使用两个包含单个 LineString 和多边形的 GeoJSON 文件进行的简单裁剪演示。第二部分使用来自 OSM 的数据,可以在您的/ch05/geodata文件夹中找到,其中包含代表我们感兴趣区域的圆形多边形clip_area_3857.shp。roads_london_3857.shp文件代表我们将要裁剪到圆形多边形的线条输入 Shapefile。
为了可视化第一部分,我们在一个非常基础的 HTML 页面中使用了 leaflet JavaScript 库。然后,我们可以使用 QGIS 打开我们的第二个结果 Shapefile,以查看裁剪后的道路集合。
如何操作...
我们面前有两套代码示例。第一个是一个简单的自制的 GeoJSON 输入集,它被裁剪并输出为 GeoJSON 表示。然后,使用 Leaflet JS 的帮助,通过网页进行可视化。
第二个代码示例接受两个 Shapefiles,并返回一个裁剪后的 Shapefile,您可以使用 QGIS 查看。这两个示例都使用了相同的方法,并演示了裁剪函数的工作原理。
-
现在,让我们看看第一个代码示例:
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import json from shapely.geometry import asShape # define output GeoJSON file res_line_intersect = os.path.realpath("../geodata/ch05-01-geojson.js") # input GeoJSON features simple_line = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"line to clip"},"geometry":{"type":"LineString","coordinates":[[5.767822265625,50.14874640066278],[11.901806640625,50.13466432216696],[4.493408203125,48.821332549646634]]}}]} clip_boundary = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"Clipping boundary circle"},"geometry":{"type":"Polygon","coordinates":[[[6.943359374999999,50.45750402042058],[7.734374999999999,51.12421275782688],[8.96484375,51.316880504045876],[10.1513671875,51.34433866059924],[10.8544921875,51.04139389812637],[11.25,50.56928286558243],[11.25,49.89463439573421],[10.810546875,49.296471602658094],[9.6240234375,49.03786794532644],[8.1298828125,49.06666839558117],[7.5146484375,49.38237278700955],[6.8994140625,49.95121990866206],[6.943359374999999,50.45750402042058]]]}}]} # create shapely geometry from FeatureCollection # access only the geomety part of GeoJSON shape_line = asShape(simple_line['features'][0]['geometry']) shape_circle = asShape(clip_boundary['features'][0]['geometry']) # run the intersection shape_intersect = shape_line.intersection(shape_circle) # define output GeoJSON dictionary out_geojson = dict(type='FeatureCollection', features=[]) # generate GeoJSON features for (index_num, line) in enumerate(shape_intersect): feature = dict(type='Feature', properties=dict(id=index_num)) feature['geometry'] = line.__geo_interface__ out_geojson['features'].append(feature) # write out GeoJSON to JavaScript file # this file is read in our HTML and # displayed as GeoJSON on the leaflet map # called /html/ch05-01-clipping.html with open(res_line_intersect, 'w') as js_file: js_file.write('var big_circle = {0}'.format(json.dumps(clip_boundary))) js_file.write("\n") js_file.write('var big_linestring = {0}'.format(json.dumps(simple_line))) js_file.write("\n") js_file.write('var simple_intersect = {0}'.format(json.dumps(out_geojson)))这就结束了我们使用简单自制的 GeoJSON LineString 进行裁剪的第一次代码演示,该 LineString 裁剪到一个简单的多边形上。这个快速食谱可以在
/code/ch05-01-1_clipping_simple.py文件中找到。运行此文件后,您可以在本地网页浏览器中打开/code/html/ch05-01-clipping.html文件以查看结果。它通过定义一个输出 JavaScript 文件来实现,该文件用于可视化我们的裁剪结果。接下来是我们的输入裁剪区域和要裁剪的 LineString 作为 GeoJSON。我们将使用
ashape()函数将我们的 GeoJSON 转换为 shapely 几何对象,以便我们可以运行交点操作。然后,将结果交点几何对象从 shapely 几何对象转换为 GeoJSON 文件,并将其写入我们的输出 JavaScript 文件,该文件用于在.html文件内部使用 Leaflet 进行可视化。 -
要开始位于
/code/ch05-01-2_clipping.py文件中的第二个代码示例,我们将输入两个 Shapefiles,创建一组新的道路,这些道路裁剪到我们的圆形多边形上,并将它们作为 Shapefiles 导出:#!/usr/bin/env python # -*- coding: utf-8 -*- import shapefile import geojson import os # used to import dictionary data to shapely from shapely.geometry import asShape from shapely.geometry import mapping # open roads Shapefile that we want to clip with pyshp roads_london = shapefile.Reader(r"../geodata/roads_london_3857.shp") # open circle polygon with pyshp clip_area = shapefile.Reader(r"../geodata/clip_area_3857.shp") # access the geometry of the clip area circle clip_feature = clip_area.shape() # convert pyshp object to shapely clip_shply = asShape(clip_feature) # create a list of all roads features and attributes roads_features = roads_london.shapeRecords() # variables to hold new geometry roads_clip_list = [] roads_shply = [] # run through each geometry, convert to shapely geom and intersect for feature in roads_features: roads_london_shply = asShape(feature.shape.__geo_interface__) roads_shply.append(roads_london_shply) roads_intersect = roads_london_shply.intersection(clip_shply) # only export linestrings, shapely also created points if roads_intersect.geom_type == "LineString": roads_clip_list.append(roads_intersect) # open writer to write our new shapefile too pyshp_writer = shapefile.Writer() # create new field pyshp_writer.field("name") # convert our shapely geometry back to pyshp, record for record for feature in roads_clip_list: geojson = mapping(feature) # create empty pyshp shape record = shapefile._Shape() # shapeType 3 is linestring record.shapeType = 3 record.points = geojson["coordinates"] record.parts = [0] pyshp_writer._shapes.append(record) # add a list of attributes to go along with the shape pyshp_writer.record(["empty record"]) # save to disk pyshp_writer.save(r"../geodata/roads_clipped2.shp")
它是如何工作的...
对于这个食谱,我们将使用 Shapely 进行空间操作,并使用 pyshp 来读取和写入我们的 Shapefiles。
我们将开始导入用于演示项目区域的道路 LineStrings 和圆形多边形。我们将使用pyshp模块来处理 Shapefile 的输入/输出。Pyshp允许我们访问 Shapefile 的边界、要素几何、要素属性等。
我们的首要任务是将pyshp几何对象转换为 Shapely 可以理解的形式。我们将使用shape()函数获取pyshp几何对象,然后使用 Shapely 的asShape()函数。接下来,我们希望获取所有道路记录,以便我们可以使用shapeRecords()函数返回这些记录。
现在,我们将为实际裁剪做好准备,通过设置两个列表变量来存储我们的新数据。for循环遍历每个记录,即道路数据集中的每一行,使用geo_interface将其转换为 shapely 几何对象,并在 pyshp 函数中构建。然后,跟随实际的intersection shapely 函数,该函数只返回与我们的圆相交的几何对象。最后,我们将检查交点几何对象是否为 LineString。如果是,我们将将其追加到我们的输出列表中。
注意
在交点操作过程中,Shapely 将在一个几何集合中返回点和 LineStrings。这样做的原因是,如果两个 LineStrings 在末端接触,例如,或者相互重叠,它将生成一个点交点位置以及任何重叠的段。
最后,我们可以将我们的新数据集写入一个新的 Shapefile。使用 pyshp 的writer()函数,我们创建一个新的对象,并给它一个名为name的单个字段。遍历每个要素,我们可以使用 shapely 映射函数和一个空的 pyhsp 记录来创建一个 GeoJSON 对象,我们将在稍后将其添加到其中。我们希望添加来自 GeoJSON 的点坐标并将它们一起附加。
退出循环后,我们将我们的新 Shapefile roads_clipped.shp保存到磁盘。
使用线条分割多边形
通常,在 GIS 中,我们处理的数据以某种形式影响其他数据,这是由于它们固有的空间关系。这意味着我们需要处理一个数据集来编辑、更新甚至删除另一个数据集。一个典型的例子是行政边界,这是一个你无法在物理表面上看到的但会影响它所穿越的特征信息的多边形,例如湖泊。如果我们有一个湖泊多边形和一个行政边界,我们可能想知道每个行政边界属于多少平方米的湖泊。
另一个例子可能是一个包含一种树木种类且横跨河流的森林多边形。我们可能想知道河流两侧的面积。在第一种情况下,我们需要将我们的行政边界转换为 LineStrings,然后执行切割操作。
为了看到这看起来像什么,看看这个预告,看看结果将如何,因为我们都喜欢一个好的视觉效果。

准备工作
对于这个菜谱,我们还将再次使用我们之前菜谱中的 GeoJSON LineString 和 polygon。这些自制的几何形状将把我们的多边形切割成三个新的多边形。确保使用workon pygeoan_cb命令启动你的虚拟环境。
如何操作...
-
这个代码示例位于
/code/ch05-02_split_poly_with_line.py,如下所示:#!/usr/bin/env python # -*- coding: utf-8 -*- from shapely.geometry import asShape from shapely.ops import polygonize import json import os # define output GeoJSON file output_result = os.path.realpath("../geodata/ch05-02-geojson.js") # input GeoJSON features line_geojs = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"line to clip"},"geometry":{"type":"LineString","coordinates":[[5.767822265625,50.14874640066278],[11.901806640625,50.13466432216696],[4.493408203125,48.821332549646634]]}}]} poly_geojs = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"Clipping boundary circle"},"geometry":{"type":"Polygon","coordinates":[[[6.943359374999999,50.45750402042058],[7.734374999999999,51.12421275782688],[8.96484375,51.316880504045876],[10.1513671875,51.34433866059924],[10.8544921875,51.04139389812637],[11.25,50.56928286558243],[11.25,49.89463439573421],[10.810546875,49.296471602658094],[9.6240234375,49.03786794532644],[8.1298828125,49.06666839558117],[7.5146484375,49.38237278700955],[6.8994140625,49.95121990866206],[6.943359374999999,50.45750402042058]]]}}]} # create shapely geometry from FeatureCollection # access only the geomety part of GeoJSON cutting_line = asShape(line_geojs['features'][0]['geometry']) poly_to_split = asShape(poly_geojs['features'][0]['geometry']) # convert circle polygon to linestring of circle boundary bndry_as_line = poly_to_split.boundary # combine new boundary lines with the input set of lines result_union_lines = bndry_as_line.union(cutting_line) # re-create polygons from unioned lines new_polygons = polygonize(result_union_lines) # stores the final split up polygons new_cut_ply = [] # identify which new polygon we want to keep for poly in new_polygons: # check if new poly is inside original otherwise ignore it if poly.centroid.within(poly_to_split): print ("creating polgon") # add only polygons that overlap original for export new_cut_ply.append(poly) else: print ("This polygon is outside of the input features") # define output GeoJSON dictionary out_geojson = dict(type='FeatureCollection', features=[]) # generate GeoJSON features for (index_num, geom) in enumerate(new_cut_ply): feature = dict(type='Feature', properties=dict(id=index_num)) feature['geometry'] = geom.__geo_interface__ out_geojson['features'].append(feature) # write out GeoJSON to JavaScript file # this file is read in our HTML and # displayed as GeoJSON on the leaflet map # called /html/ch05-02.html with open(output_result, 'w') as js_file: js_file.write('var cut_poly_result = {0}'.format(json.dumps(out_geojson)))
它是如何工作的...
现在多边形的实际分割操作发生在我们的/ch05/code/ch05-02_split_poly_with_line.py脚本中。
基于 LineString 分割多边形的基本方法遵循这个简单的算法。首先,我们将我们的输入多边形转换为新的 LineString 数据集的边界。接下来,我们将我们想要用来切割新生成的多边形边界 LineStrings 的 LineString 组合起来。最后,我们使用polygonize方法根据新的 LineStrings 联合集重建多边形。
这种多边形重建的结果是在原始多边形外部创建的额外多边形。为了识别这些多边形,我们将使用一个简单的技巧。我们可以在每个新创建的多边形内部生成一个centroid点,然后检查这个点是否在原始多边形内部使用within谓词。如果点不在原始多边形内部,谓词返回False,我们不需要将这个多边形包含在我们的输出中。
使用线性参照找到线上的点位置
线性引用的使用非常广泛,从存储公交路线到石油和天然气管道。我们根据从线起始点的距离值定位任何位置的能力是通过插值方法实现的。我们想要在线的任何位置插值一个点位置。为了确定位置,我们将使用简单的数学来根据起始坐标的距离计算线上的位置。
对于我们的计算,我们将测量线的长度,并找到一个位于线起始点指定长度的坐标。然而,关于线起始点在哪里的问题很快就会出现。线的起始点是构成 LineString 的顶点数组中的第一个坐标,因为 LineString 不过是一系列点连在一起。
这将很好地引出我们的下一个菜谱,它稍微复杂一些。
如何做到这一点...
-
这是我们的最短代码片段;来看看:
#!/usr/bin/env python # -*- coding: utf-8 -*- from shapely.geometry import asShape import json import os from pyproj import Proj, transform # define the pyproj CRS # our output CRS wgs84 = Proj("+init=EPSG:4326") # output CRS pseudo_mercator = Proj("+init=EPSG:3857") def transform_point(in_point, in_crs, out_crs): """ export a Shapely geom to GeoJSON and transform to a new coordinate system with pyproj :param in_point: shapely geometry as point :param in_crs: pyproj crs definition :param out_crs: pyproj output crs definition :return: GeoJSON transformed to out_crs """ geojs_geom = in_point.__geo_interface__ x1 = geojs_geom['coordinates'][0] y1 = geojs_geom['coordinates'][1] # transform the coordinate x, y = transform(in_crs, out_crs, x1, y1) # create output new point new_point = dict(type='Feature', properties=dict(id=1)) new_point['geometry'] = geojs_geom new_coord = (x, y) # add newly transformed coordinate new_point['geometry']['coordinates'] = new_coord return new_point def transform_linestring(orig_geojs, in_crs, out_crs): """ transform a GeoJSON linestring to a new coordinate system :param orig_geojs: input GeoJSON :param in_crs: original input crs :param out_crs: destination crs :return: a new GeoJSON """ line_wgs84 = orig_geojs wgs84_coords = [] # transfrom each coordinate for x, y in orig_geojs['geometry']['coordinates']: x1, y1 = transform(in_crs, out_crs, x, y) line_wgs84['geometry']['coordinates'] = x1, y1 wgs84_coords.append([x1, y1]) # create new GeoJSON new_wgs_geojs = dict(type='Feature', properties={}) new_wgs_geojs['geometry'] = dict(type='LineString') new_wgs_geojs['geometry']['coordinates'] = wgs84_coords return new_wgs_geojs # define output GeoJSON file output_result = os.path.realpath("../geodata/ch05-03-geojson.js") line_geojs = {"type": "Feature", "properties": {}, "geometry": {"type": "LineString", "coordinates": [[-13643703.800790818,5694252.85913249],[-13717083.34794459,6325316.964654908]]}} # create shapely geometry from FeatureCollection shply_line = asShape(line_geojs['geometry']) # get the coordinates of each vertex in our line line_original = list(shply_line.coords) print line_original # showing how to reverse a linestring line_reversed = list(shply_line.coords)[::-1] print line_reversed # example of the same reversing function on a string for example hello = 'hello world' reverse_hello = hello[::-1] print reverse_hello # locating the point on a line based on distance from line start # input in meters = to 360 Km from line start point_on_line = shply_line.interpolate(360000) # transform input linestring and new point # to wgs84 for visualization on web map wgs_line = transform_linestring(line_geojs, pseudo_mercator, wgs84) wgs_point = transform_point(point_on_line, pseudo_mercator, wgs84) # write to disk the results with open(output_result, 'w') as js_file: js_file.write('var point_on_line = {0}'.format(json.dumps(wgs_point))) js_file.write('\n') js_file.write('var in_linestring = {0}'.format(json.dumps(wgs_line)))执行
/code/ch05-03_point_on_line.py文件后,当你用你的网络浏览器打开/code/html/ch05-03.html文件时,你应该会看到以下截图:![如何做到这一点...]()
如果你想要反转 LineString 的起始和结束点,你可以使用list(shply_line.coords)[::-1]代码来反转坐标顺序,如前述代码所示。
它是如何工作的...
这一切归结为执行一行代码来定位一个在特定距离上的线上的点。Shapely 插值函数为我们做了这件事。你所需要的只是 Shapely LineString 几何形状和一个距离值。距离值是 LineString 的 0,0 起始坐标的距离。
如果 LineString 的方向不是你想要测量的正确形式,请小心。这意味着你需要切换 LineString 的方向。看看line_reversed变量,它持有原始 LineString,顺序被反转。为了进行reverse操作,我们将使用简单的 Python 字符串操作[::-1]来反转我们的 LineString 列表。
你可以通过以下屏幕截图中的打印语句看到这一功能的作用:
[(-13643703.800790818, 5694252.85913249), (-13717083.34794459, 6325316.964654908)]
[(-13717083.34794459, 6325316.964654908), (-13643703.800790818, 5694252.85913249)]
参见
如果你想要更多关于线性引用的信息,ESRI 在resources.arcgis.com/en/help/main/10.1/0039/003900000001000000.htm和en.wikipedia.org/wiki/Linear_referencin提供了很好的用例和示例。
将点捕捉到最近的线上
建立在上一菜谱中获得的新知识的基础上,我们现在将解决另一个常见空间问题。这个超级常见空间任务是为所有想要将 GPS 坐标捕捉到现有道路上的 GPS 爱好者准备的。想象一下,你有一些 GPS 轨迹,你想要将这些坐标捕捉到你的基础道路数据集中。为了完成这个任务,我们需要将一个点(GPS 坐标)捕捉到一条线(道路)上。
geos库是Shapely构建的基础,可以轻松处理这个问题。我们将结合使用shapely.interpolate和shapely.project函数,通过线性引用将我们的点捕捉到线上的真实最近点。
如以下图所示,我们的输入点位于太阳图标上。绿色线是我们想要将点捕捉到最近位置的地方。带有点的灰色图标是我们的结果,它代表从原始 x 位置到线上的最近点。

如何做...
-
Shapely 非常适合将点捕捉到最近的线上,让我们开始吧:
#!/usr/bin/env python # -*- coding: utf-8 -*- from shapely.geometry import asShape import json import os from pyproj import Proj, transform # define the pyproj CRS # our output CRS wgs84 = Proj("+init=EPSG:4326") # output CRS pseudo_mercator = Proj("+init=EPSG:3857") def transform_point(in_point, in_crs, out_crs): """ export a Shapely geom to GeoJSON Feature and transform to a new coordinate system with pyproj :param in_point: shapely geometry as point :param in_crs: pyproj crs definition :param out_crs: pyproj output crs definition :return: GeoJSON transformed to out_crs """ geojs_geom = in_point.__geo_interface__ x1 = geojs_geom['coordinates'][0] y1 = geojs_geom['coordinates'][1] # transform the coordinate x, y = transform(in_crs, out_crs, x1, y1) # create output new point out_pt = dict(type='Feature', properties=dict(id=1)) out_pt['geometry'] = geojs_geom new_coord = (x, y) # add newly transformed coordinate out_pt['geometry']['coordinates'] = new_coord return out_pt def transform_geom(orig_geojs, in_crs, out_crs): """ transform a GeoJSON linestring or Point to a new coordinate system :param orig_geojs: input GeoJSON :param in_crs: original input crs :param out_crs: destination crs :return: a new GeoJSON """ wgs84_coords = [] # transfrom each coordinate if orig_geojs['geometry']['type'] == "LineString": for x, y in orig_geojs['geometry']['coordinates']: x1, y1 = transform(in_crs, out_crs, x, y) orig_geojs['geometry']['coordinates'] = x1, y1 wgs84_coords.append([x1, y1]) # create new GeoJSON new_wgs_geojs = dict(type='Feature', properties={}) new_wgs_geojs['geometry'] = dict(type='LineString') new_wgs_geojs['geometry']['coordinates'] = wgs84_coords return new_wgs_geojs elif orig_geojs['geometry']['type'] == "Point": x = orig_geojs['geometry']['coordinates'][0] y = orig_geojs['geometry']['coordinates'][1] x1, y1 = transform(in_crs, out_crs, x, y) orig_geojs['geometry']['coordinates'] = x1, y1 coord = x1, y1 wgs84_coords.append(coord) new_wgs_geojs = dict(type='Feature', properties={}) new_wgs_geojs['geometry'] = dict(type='Point') new_wgs_geojs['geometry']['coordinates'] = wgs84_coords return new_wgs_geojs else: print("sorry this geometry type is not supported") # define output GeoJSON file output_result = os.path.realpath("../geodata/ch05-04-geojson.js") line = {"type":"Feature","properties":{},"geometry":{"type":"LineString","coordinates":[[-49.21875,19.145168196205297],[-38.49609375,32.24997445586331],[-27.0703125,22.105998799750576]]}} point = {"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-33.57421875,32.54681317351514]}} new_line = transform_geom(line, wgs84, pseudo_mercator) new_point = transform_geom(point, wgs84, pseudo_mercator) shply_line = asShape(new_line['geometry']) shply_point = asShape(new_point['geometry']) # perform interpolation and project point to line pt_interpolate = shply_line.interpolate(shply_line.project(shply_point)) # print coordinates and distance to console print ("origin point coordinate") print (point) print ("interpolted point location") print (pt_interpolate) print "distance from origin to interploate point" print (shply_point.distance(pt_interpolate)) # convert new point to wgs84 GeoJSON snapped_pt = transform_point(pt_interpolate, pseudo_mercator, wgs84) # our original line and point are transformed # so here they are again in original coords # to plot on our map line_orig = {"type":"Feature","properties":{},"geometry":{"type":"LineString","coordinates":[[-49.21875,19.145168196205297],[-38.49609375,32.24997445586331],[-27.0703125,22.105998799750576]]}} point_orig = {"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[-33.57421875,32.54681317351514]}} # write to disk the results with open(output_result, 'w') as js_file: js_file.write('var input_pt = {0}'.format(json.dumps(snapped_pt))) js_file.write('\n') js_file.write('var orig_pt = {0}'.format(json.dumps(point_orig))) js_file.write('\n') js_file.write('var line = {0}'.format(json.dumps(line_orig)))
它是如何工作的...
我们将使用一种经过验证的方法,称为线性引用来完成这项工作。让我们从完成这项工作所需的导入开始,包括shapely.geometry asShape、json和pyproj。Pyproj 用于快速将我们的坐标从 EPSG: 4326 和 EPSG 3857 转换过来和转换回去。Shapely 在平面坐标上工作,不能直接使用lat/lon值。
从上一个菜谱扩展我们的函数,我们有了transform_point()函数和transform_geom()函数。transform_point()函数将 Shapely 几何体转换为 GeoJSON 并转换点坐标,而transform_geom()函数接收 GeoJSON 并转换为新的坐标系。这两个函数都使用 pyproj 来执行转换。
接下来,我们将定义我们的输出 GeoJSON 文件和输入线要素和点要素。然后,我们将执行我们的两个新转换函数,紧接着是将它们转换为 Shapely 几何对象。这个新的 Shapely 几何对象随后通过插值函数运行。
单独使用插值并不能回答我们的问题。我们需要将其与 Shapely 的project函数结合使用,该函数接收原始点并将其投影到线上。
然后,我们将结果打印到屏幕上,并创建一个新的 JavaScript 文件/geodata/ch05-04-geojson.js,用于在/code/html/ch05-04.html中查看。请打开浏览器中的 HTML 文件以查看结果。
查看你的控制台,以查看以下打印到控制台语句,显示点坐标和从原始点的距离:
>>> python python-geospatial-analysis-cookbook/ch05/code/ch05-04_snap_point2line.py
计算三维地面距离和总海拔升高
我们已经完成了在直线上找到点并返回直线上的点的工作,现在,是时候计算我们实际上沿着真实 3D 道路跑或骑行的真实地面 3D 距离了。还可以计算高程剖面,我们将在第七章 栅格分析 中看到这一点。
计算地面距离听起来很简单,但 3D 计算比 2D 计算更复杂。我们的 3D LineString 为组成 LineString 的每个顶点都有一个 z 坐标。因此,我们需要计算每对坐标之间的 3D 距离,即从输入 LineString 的顶点到顶点的距离。
计算两个 3D 笛卡尔坐标之间距离的数学方法相对简单,使用了毕达哥拉斯公式的 3D 形式:
3d_distance = 平方根 √ ( (x2 – x1) ² + (y2 – y1) ² + (z2 -z1)²)
这里是 Python 代码示例:
import math
3d_dist = math.sqrt((x2 – x1)**2 + (y2 – y1)**2 + (z2 – z1)**2 )
准备中
首先,我们将获取一些 3D 数据来进行分析,还有什么比分析 2014 年环法自行车赛第 16 赛段(卡卡斯/巴涅尔-德-卢什翁)的山地赛段更好的呢?这是一个真正的挑战。以下是从 www.letour.com 获取的一些统计数据,包括 237.5 公里的长度,迈克尔·罗杰斯的获胜时间为 6:07:10,平均速度为 38.811 公里/小时,最高点为 1753 米。您将在您的文件夹中找到这些数据,位于 /ch05/geodata/velowire_stage_16_27563_utf8.geojson。
原始的 KML 文件由 Thomas Vergouwen 慷慨提供(www.velowire.com),并且在他的许可下我们可以免费使用;谢谢,Thomas。原始数据位于 /ch05/geodata/velowire_stage_16-Carcassonne-Bagneres-de-Luchon.kml。将数据转换为 GeoJSON 并转换为 EPSG:27563 是通过使用 QGIS 的 另存为 功能完成的。
现在,根据《洛杉矶时报》网页(www.latimes.com/la-sp-g-tour-de-france-stage-elevation-profile-20140722-htmlstory.html),他们引用了 3895 米的高程上升。与 Strava 团队(blog.strava.com/tour-de-france-2014/)相比,他们声称上升了 4715 米。现在,谁是对的,这个 3D 中的 237.5 公里地面距离是多少?让我们找出答案!
这是官方的第 16 赛段的剖面图,供您欣赏:

为了让您了解准确和简化数据看起来像什么,请看看这个紫色(准确)标记的 velowire 网站(www.velowire.com)的 KML 和用黄色线条突出显示的 bikemap 网站的进度(简化)。如果你把差异加起来,长度和海拔对于 237.5 公里长的比赛来说都显著不同。在规划和对阵过程中,每米都很重要。在下面的屏幕截图中,你可以看到紫色标记的 velowire 网站的 KML 和用黄色线条突出显示的 bikemap 网站的进度:

数据来源:www.mapcycle.com.au/LeTour2014/#
如何操作...
我们将从遍历每个顶点并计算 LineString 中从一个顶点到另一个顶点的 3D 距离开始。每个顶点不过是一个具有x、y和z(3D 笛卡尔)值的点。
-
这里是计算每个顶点的代码:
#!/usr/bin/env python # -*- coding: utf-8 -*- import math import os from shapely.geometry import shape, Point import json def pairs(lst): """ yield iterator of two coordinates of linestring :param lst: list object :return: yield iterator of two coordinates """ for i in range(1, len(lst)): yield lst[i - 1], lst[i] def calc_3d_distance_2pts(x1, y1, z1, x2, y2, z2): """ :input two point coordinates (x1,y1,z1),(x2,y2,2) :param x1: x coordinate first segment :param y1: y coordiante first segment :param z1: z height value first coordinate :param x2: x coordinate second segment :param y2: y coordinate second segment :param z2: z height value second coordinate :return: 3D distance between two input 3D coordinates """ d = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2) return d def readin_json(jsonfile): """ input: geojson or json file """ with open(jsonfile) as json_data: d = json.load(json_data) return d geoj_27563_file = os.path.realpath("../geodata/velowire_stage_16_27563_utf8.geojson") print (geoj_27563_file) # create python dict type from geojson file object json_load = readin_json(geoj_27563_file) # set start lengths length_3d = 0.0 length_2d = 0.0 # go through each geometry in our linestring for f in json_load['features']: # create shapely shape from geojson s = shape(f['geometry']) # calculate 2D total length length_2d = s.length # set start elevation elevation_gain = 0 # go through each coordinate pair for vert_start, vert_end in pairs(s.coords): line_start = Point(vert_start) line_end = Point(vert_end) # create input coordinates x1 = line_start.coords[0][0] y1 = line_start.coords[0][1] z1 = line_start.coords[0][2] x2 = line_end.coords[0][0] y2 = line_end.coords[0][1] z2 = line_end.coords[0][2] # calculate 3d distance distance = calc_3d_distance_2pts(x1, y1, z1, x2, y2, z2) # sum distances from vertex to vertex length_3d += distance # calculate total elevation gain if z1 > z2: elevation_gain = ((z1 - z2) + elevation_gain ) z2 = z1 else: elevation_gain = elevation_gain # no height change z2 = z1 print ("total elevation gain is: {gain} meters".format(gain=str(elevation_gain))) # print coord_pair distance_3d = str(length_3d / 1000) distance_2d = str(length_2d / 1000) dist_diff = str(length_3d - length_2d) print ("3D line distance is: {dist3d} meters".format(dist3d=distance_3d)) print ("2D line distance is: {dist2d} meters".format(dist2d=distance_2d)) print ("3D-2D length difference: {diff} meters".format(diff=dist_diff))
它是如何工作的...
我们需要将存储在 EPSG: 4326 中的原始 KML 文件转换为平面坐标系,以方便我们的计算(参考即将到来的表格)。因此,我们将首先将 KML 转换为 EPSG: 27563 NTF Paris / Lambert Sud France。有关此信息的更多信息,请参阅epsg.io/27563。
首先,我们将定义三个用于计算的函数,从pairs()函数开始,它接受一个列表,然后使用 Python yield 生成器函数来产生两组值。第一组值是起始 x、y 和 z 坐标,第二组包括我们想要测量的坐标对的结束 x、y 和 z 坐标。
calc_3d_distancte_2pts()函数接受两个坐标对,包括重要的 z 值,并使用勾股定理计算 3D 空间中两点之间的距离。
我们的readin_json()函数输入一个文件路径,在这种情况下,我们可以将其指向存储在/ch05/geodata文件夹中的 GeoJSON 文件。这将返回一个 Python 字典对象,我们可以在接下来的几个步骤中使用它。
现在,让我们定义变量来保存我们的 GeoJSON 文件,加载此文件,并将起始 3D/2D 长度初始化为零。
接下来,我们将遍历 GeoJSON LineString 特征并将它们转换为 Shapely 对象,以便我们可以使用 Shapely 来告诉我们length_2d变量所使用的固有 2D 长度并读取坐标。这之后是我们的for循环,所有的动作都发生在这里。
通过遍历由我们的pairs()函数创建的新列表,我们可以遍历 LineString 的每个顶点。我们定义line_start和line_end变量来识别我们需要通过单个 LineString 特征访问的每个新线段的开头。然后,我们将通过解析我们的列表对象来定义我们的输入参数,使用标准的 Python 位置切片进行 3D 距离计算。最后,我们将调用calc_3d_distance_2pts()函数来给出我们的 3D 距离。
我们需要迭代地将从一个段落到下一个段落的距离相加。我们可以通过使用+=运算符将距离添加到我们的length_3d中来实现这一点。现在,length_3d变量在循环中的每个段落都会更新,从而给出我们想要的 3D 长度。
循环的剩余部分计算我们的海拔升高。我们的z1和z2海拔值需要不断比较,只有当下一个值大于上一个值时,才将总海拔升高累加。如果不是,将它们设置为相等,并继续到下一个z值。然后,如果没有任何变化,elevation_gain变量会不断更新为自己;否则,两个海拔之间的差值会被累加。
最后,我们将结果打印到屏幕上;它们应该看起来像这样:
total elevation gain is: 4322.0 meters
3D line distance is: 244.119162551
2D line distance is: 243.55802081
3D-2D length difference: 561.141741137 meters
通过将我们的数据转换并转换为 GeoJSON,根据我们的脚本,2D 长度为从 velowire KML 的 243.558 km,与官方比赛页面上的 237.5 km 相比,差异为 6.058 km。原始 EPSG:4326 的 KML 长度为 302.805 km,差异超过 65 km,因此需要进行转换。为了更好地比较,请查看以下表格:
| 来源 + EPSG | 2D 长度 | 3D 长度 | 差异 |
|---|---|---|---|
| Velowire EPSG:4326 | 302.805 km | 这不是计算得出的 | |
| Velowire EPSG:27563 | 243.558 km | 244.12 | 561.14 m |
| Mapcycle EPSG:4326 | 293.473 km | 此数据不可用 | |
| Mapcylce EPSG:27563 | 236.216 km | 此数据不可用 | |
| Letour 官方 | 237.500 km(近似) | 237.500 km(近似) |
海拔升高在不同来源之间也非常不同。
| 来源 | 海拔升高 |
|---|---|
Strava (blog.strava.com/) |
4715 m |
| Los Angeles Times | 3895 m |
| TrainingPeaks (www.trainingpeaks.com) | 3243 m |
| Velowire KML 数据分析 | 4322 m |
还有更多...
所有这些计算的准确性都基于原始的 KML 数据源。每个数据源都是由不同的人以及可能不同的方法推导出来的。你对你的数据源了解得越多,你对它的准确性了解得就越多。在这种情况下,我假设 Velowire 数据源是使用 Google Earth 手动数字化的。因此,准确性只能与底层 Google Earth 影像和坐标系统(EPSG:3857)的准确性相当。
第六章. 叠加分析
在本章中,我们将涵盖以下主题:
-
使用对称差操作在多边形中打孔
-
不合并的多边形联合
-
使用合并(溶解)的多边形联合
-
执行恒等函数(差集 + 交集)
简介
发现当两个数据集叠加在一起时它们在空间上的相互关系被称为叠加分析。叠加可以比作一张描图纸。例如,你可以在你的底图上叠加描图纸,看看哪些区域重叠。这个过程在过去和现在都是空间分析和建模的一个变革。因此,计算机辅助 GIS 计算可以自动识别两个几何集在空间上接触的位置。
本章的目标是让你对最常见的叠加分析函数有一个感觉,例如联合、交集和对称差。这些基于维度扩展的九交模型(DE-9IM),可以在en.wikipedia.org/wiki/DE-9IM找到,并描述了我们的可能叠加列表。我们在这里使用或命名的所有过程都是使用这九个谓词的组合推导出来的。

我们将在第九章中深入探讨这些拓扑规则,拓扑检查和数据验证。
使用对称差操作在多边形中打孔
为什么,哦,为什么我们要在多边形中打孔并创建一个甜甜圈?嗯,这是出于几个原因,例如,你可能想从与森林多边形重叠的湖泊多边形中移除,因为它位于森林中间,因此包含在你的面积计算中。
另一个例子是我们有一组代表高尔夫球道发球区的多边形,以及另一组代表与这些球道重叠的绿色区域的绿色多边形。我们的任务是计算正确的球道平方米数。绿色区域将在球道多边形中形成我们的甜甜圈。
这被翻译成空间操作术语,意味着我们需要执行一个对称差操作,或者在 ESRI 术语中,一个“擦除”操作。

准备工作
在这个例子中,我们将创建两组可视化来查看我们的结果。我们的输出将生成已知文本(WKT),它使用Openlayers 3网络地图客户端在你的浏览器中显示。
对于这个例子,请确保你已经下载了所有代码到 GitHub 提供的/ch06文件夹,并且这个文件夹结构包含以下文件:
code
¦ ch06-01_sym_diff.py
¦ foldertree.txt
¦ utils.py
¦
+---ol3
+---build
¦ ol-debug.js
¦ ol-deps.js
¦ ol.js
¦
+---css
¦ layout.css
¦ ol.css
¦
+---data
¦ my_polys.js
¦
+---html
¦ ch06-01_sym_diff.html
¦
+---js
¦ map_sym_diff.js
¦
+---resources
¦ jquery.min.js
¦ logo-32x32-optimized.png
¦ logo-32x32.png
¦ logo.png
¦ textured_paper.jpeg
¦
+---bootstrap
+---css
¦ bootstrap-responsive.css
¦ bootstrap-responsive.min.css
¦ bootstrap.css
¦ bootstrap.min.css
¦
+---img
¦ glyphicons-halflings-white.png
¦ glyphicons-halflings.png
¦
+---js
bootstrap.js
bootstrap.min.js
geodata
pebble-beach-fairways-3857.geojson
pebble-beach-greens-3857.geojson
results_sym_diff.js
在文件夹结构就绪的情况下,当你运行代码时,所有输入和输出都将找到它们正确的家。
如何做到这一点...
我们想像往常一样从命令行运行此代码,它将在您的虚拟环境中运行:
-
从您的
/ch06/code文件夹执行以下语句:>> python Ch06-01_sym_diff.py -
以下代码是 Shapely 中有趣操作发生的地方:
#!/usr/bin/env python # -*- coding: utf-8 -*- import json from os.path import realpath from shapely.geometry import MultiPolygon from shapely.geometry import asShape from shapely.wkt import dumps # define our files input and output locations input_fairways = realpath("../geodata/pebble-beach-fairways-3857.geojson") input_greens = realpath("../geodata/pebble-beach-greens-3857.geojson") output_wkt_sym_diff = realpath("ol3/data/results_sym_diff.js") # open and load our geojson files as python dictionary with open(input_fairways) as fairways: fairways_data = json.load(fairways) with open(input_greens) as greens: greens_data = json.load(greens) # create storage list for our new shapely objects fairways_multiply = [] green_multply = [] # create shapely geometry objects for fairways for feature in fairways_data['features']: shape = asShape(feature['geometry']) fairways_multiply.append(shape) # create shapely geometry objects for greens for green in greens_data['features']: green_shape = asShape(green['geometry']) green_multply.append(green_shape) # create shapely MultiPolygon objects for input analysis fairway_plys = MultiPolygon(fairways_multiply) greens_plys = MultiPolygon(green_multply) # run the symmetric difference function creating a new Multipolygon result = fairway_plys.symmetric_difference(greens_plys) # write the results out to well known text (wkt) with shapely dump def write_wkt(filepath, features): with open(filepath, "w") as f: # create a js variable called ply_data used in html # Shapely dumps geometry out to WKT f.write("var ply_data = '" + dumps(features) + "'") # write to our output js file the new polygon as wkt write_wkt(output_wkt_sym_diff, result)您的输出将保存在
/ch06/code/ol3/html/文件夹中,文件名为ch06-01_sym_diff.html。只需在您的本地网页浏览器中打开此文件,例如 Chrome、Firefox 或 Safari。我们的输出网络地图是通过根据我们的需求修改 Openlayers 3 示例代码页面创建的。生成的网络地图应在您的本地网页浏览器中显示以下地图:![如何做到这一点...]()
您现在可以清楚地看到航道内部有一个洞。
它是如何工作的...
首先,我们使用两个 GeoJSON 数据集作为我们的输入,它们都具有 EPSG: 3857,并源自 OSM EPSG: 4326。转换过程在此未涉及;有关如何在两个坐标系之间转换数据的更多信息,请参阅 第二章,处理投影。
我们的第一项任务是使用标准的 Python json 模块将两个 GeoJSON 文件读入 Python 字典对象。接下来,我们设置一些空列表,这些列表将存储 Shapely 几何对象列表,用作我们的输入以生成分析所需的 MultiPolygons。我们使用 Shapely 内置的 asShape() 函数创建 Shapely 几何对象,以便我们可以执行空间操作。这是通过访问字典的 ['geometry'] 元素来实现的。然后我们将每个几何形状追加到我们的空列表中。然后,这个列表被输入到 Shapely 的 MultiPolygon() 函数中,该函数将为我们创建一个 MultiPolygon,并用作我们的输入。
实际上运行我们的 symmetric_difference 过程发生在我们输入 fairways_plys MultiPolygon 作为输入,并传递参数 greens_ply MultiPolygon 时。输出存储在 result 变量中,它本身也是一个 MultiPolygon。别忘了,MultiPolygon 只是一个我们可以迭代的多边形列表。
接下来,我们将查看一个名为 write_wkt(filepath, features) 的函数。这个函数将我们的结果 MultiPolygon Shapely 几何形状输出到 Well Known Text (WKT) 格式。我们不仅输出这个 WKT,而是创建一个新的 JavaScript 文件,ol3/data/ch06-01_results_sym_diff.js,包含我们的 WKT 输出。代码输出一个字符串,创建一个名为 ply_data 的 JavaScript 变量。这个 ply_data 变量随后被用于位于 /ch06/code/ol3/html/sym_diff.html 的我们的 HTML 文件中,以使用 Openlayers 3 绘制我们的 WKT 向量层。然后我们调用我们的函数,它执行写入到 WKT JavaScript 文件的操作。
这个示例是第一个将我们的结果可视化为网络地图的示例。在 第十一章,使用 GeoDjango 进行网络分析中,我们将探索一个功能齐全的网络映射应用程序;对于那些迫不及待的人,您可能想要提前跳读。接下来的示例将继续使用 Openlayers 3 作为我们的数据查看器,而不再使用 Matplotlib。
最后,我们的简单一行对称差执行需要大量的辅助代码来处理导入 GeoJSON 数据和以可以显示 Openlayers 3 网络地图的格式导出结果。
不合并的合并多边形
为了演示合并的概念,我们将从一个 国家海洋和大气管理局 (NOAA) 的气象数据示例中获取例子。它提供了您下载数据的 Shapefiles 的令人惊叹的每分钟更新。我们将查看一周的天气预警集合,并将这些与州边界结合起来,以查看预警在州边界内确切发生的位置。

前面的截图显示了在 QGIS 中进行合并操作之前的多边形。
准备工作
确保您的虚拟环境始终处于运行状态,并运行以下命令:
$ source venvs/pygeo_analysis_cookbook/bin/activate
接下来,切换到您的 /ch06/code/ 文件夹以查找完成的代码示例,或者在 /ch06/ 工作文件夹中创建一个空文件,并按照代码进行操作。
如何操作...
pyshp 和 shapely 库是我们这个练习的两个主要工具:
-
您可以直接在命令提示符中运行此文件以查看结果,如下所示:
>> python ch06-02_union.py然后,您可以通过双击打开
/ch06/code/ol3/html/ch06-02_union.html文件夹中的结果,以在您的本地网络浏览器中启动它们。如果一切顺利,您应该看到以下网络地图:![如何操作...]()
-
现在,让我们看看使这一切发生的代码:
#!/usr/bin/env python # -*- coding: utf-8 -*- import json from os.path import realpath import shapefile # pyshp from geojson import Feature, FeatureCollection from shapely.geometry import asShape, MultiPolygon from shapely.ops import polygonize from shapely.wkt import dumps def create_shapes(shapefile_path): """ Convert Shapefile Geometry to Shapely MultiPolygon :param shapefile_path: path to a shapefile on disk :return: shapely MultiPolygon """ in_ply = shapefile.Reader(shapefile_path) # using pyshp reading geometry ply_shp = in_ply.shapes() ply_records = in_ply.records() ply_fields = in_ply.fields print ply_records print ply_fields if len(ply_shp) > 1: # using python list comprehension syntax # shapely asShape to convert to shapely geom ply_list = [asShape(feature) for feature in ply_shp] # create new shapely multipolygon out_multi_ply = MultiPolygon(ply_list) # # equivalent to the 2 lines above without using list comprehension # new_feature_list = [] # for feature in features: # temp = asShape(feature) # new_feature_list.append(temp) # out_multi_ply = MultiPolygon(new_feature_list) print "converting to MultiPolygon: " + str(out_multi_ply) else: print "one or no features found" shply_ply = asShape(ply_shp) out_multi_ply = MultiPolygon(shply_ply) return out_multi_ply def create_union(in_ply1, in_ply2, result_geojson): """ Create union polygon :param in_ply1: first input shapely polygon :param in_ply2: second input shapely polygon :param result_geojson: output geojson file including full file path :return: shapely MultiPolygon """ # union the polygon outer linestrings together outer_bndry = in_ply1.boundary.union(in_ply2.boundary) # rebuild linestrings into polygons output_poly_list = polygonize(outer_bndry) out_geojson = dict(type='FeatureCollection', features=[]) # generate geojson file output for (index_num, ply) in enumerate(output_poly_list): feature = dict(type='Feature', properties=dict(id=index_num)) feature['geometry'] = ply.__geo_interface__ out_geojson['features'].append(feature) # create geojson file on disk json.dump(out_geojson, open(result_geojson, 'w')) # create shapely MultiPolygon ply_list = [] for fp in polygonize(outer_bndry): ply_list.append(fp) out_multi_ply = MultiPolygon(ply_list) return out_multi_ply def write_wkt(filepath, features): """ :param filepath: output path for new JavaScript file :param features: shapely geometry features :return: """ with open(filepath, "w") as f: # create a JavaScript variable called ply_data used in html # Shapely dumps geometry out to WKT f.write("var ply_data = '" + dumps(features) + "'") def output_geojson_fc(shply_features, outpath): """ Create valid GeoJSON python dictionary :param shply_features: shapely geometries :param outpath: :return: GeoJSON FeatureCollection File """ new_geojson = [] for feature in shply_features: feature_geom_geojson = feature.__geo_interface__ myfeat = Feature(geometry=feature_geom_geojson, properties={'name': "mojo"}) new_geojson.append(myfeat) out_feat_collect = FeatureCollection(new_geojson) with open(outpath, "w") as f: f.write(json.dumps(out_feat_collect)) if __name__ == "__main__": # define our inputs shp1 = realpath("../geodata/temp1-ply.shp") shp2 = realpath("../geodata/temp2-ply.shp") # define outputs out_geojson_file = realpath("../geodata/res_union.geojson") output_union = realpath("../geodata/output_union.geojson") out_wkt_js = realpath("ol3/data/results_union.js") # create our shapely multipolygons for geoprocessing in_ply_1_shape = create_shapes(shp1) in_ply_2_shape = create_shapes(shp2) # run generate union function result_union = create_union(in_ply_1_shape, in_ply_2_shape, out_geojson_file) # write to our output js file the new polygon as wkt write_wkt(out_wkt_js, result_union) # write the results out to well known text (wkt) with shapely dump geojson_fc = output_geojson_fc(result_union, output_union)
它是如何工作的...
在代码开始部分快速浏览一下正在发生的事情应该有助于澄清。在我们的 Python 代码中,我们有四个函数和九个变量来分割输入和输出数据的负载。我们的代码运行发生在代码末尾的 if __name__ == "main": 调用中。我们开始定义两个变量来处理我们将要 合并 的输入。这两个是我们的输入 Shapefiles,其他三个输出是 GeoJSON 和 JavaScript 文件。
create_shapes() 函数将我们的 Shapefile 转换为 Shapely MultiPolygon 几何对象。在 Python 类内部,列表推导用于生成一个新列表,其中包含多边形对象,这些对象是我们用于创建输出 MultiPolygon 的输入多边形列表。接下来,我们将简单地运行这个函数,传入我们的输入 Shapefiles。
接下来是我们的create_union()函数,我们在这里进行真正的合并工作。我们首先将两个几何边界合并在一起,生成一个包含 LineStrings 的并集集合,代表输入多边形的边界。这样做的原因是我们不希望丢失两个多边形的几何形状,当直接传递给 Shapely 的合并函数时,它们将默认溶解成一个大的多边形。因此,我们需要使用polygonize() Shapely 函数重建多边形。
polygonize函数创建了一个 Python 生成器对象,而不是一个简单的几何对象。这是一个类似于列表的迭代器,我们需要遍历它以获取它为我们创建的各个多边形。
我们在下一个代码段中正是这样做的,使用 Python 的enumerate()函数为每个我们用作 id 字段的属性结果自动创建一个 ID。在我们的循环之后,我们使用标准的 Python json.dump()方法导出我们新创建的 GeoJSON 文件,并使用 Python 的open()方法以写入模式将其写入磁盘。
最后,在我们的create_union()函数中,我们准备输出我们的结果并集多边形作为一个 Shapely MultiPolygon 对象。这通过简单地遍历polygonize()迭代器,输出一个列表,该列表输入到 Shapely 的MultiPolygon()函数中。最后,我们执行合并函数,传入我们的两个输入几何形状,并指定输出 GeoJSON 文件。
因此,我们可以像在之前的练习中一样,使用一个名为write_wkt()的小函数在我们的网络地图中查看我们的结果。这个小小的函数接受我们想要创建的输出 JavaScript 文件的文件路径以及 MultiPolygon 结果的几何形状。Shapely 然后将几何形状以写入 JavaScript 文件的方式转换为 Well Known Text 格式。
最后,一个名为output_geojson_fc()的小函数被用来输出另一个 GeoJSON 文件,这次使用 Python 的geojson库。这仅仅展示了另一种创建 GeoJSON 文件的方法。由于 GeoJSON 是一个纯文本文件,因此根据您的个人编程偏好,可以以许多独特的方式创建它。
通过合并(溶解)合并多边形
为了展示合并的概念,我们将从 NOAA 气象数据中举一个例子。它提供了令人惊叹的逐分钟更新 Shapefiles,以满足您下载数据的愿望。我们将查看一周的天气预警收集,并将这些预警合并在一起,得到本周发布的总预警区域。
这里展示了我们期望的结果的概念可视化:

大部分数据位于佛罗里达州附近,但在夏威夷和加利福尼亚州也有一些多边形。要查看原始数据或寻找新数据,请查看以下链接:
如果你想查看州界,你可以在www.census.gov/geo/maps-data/data/cbf/cbf_state.html找到它们。
这里是佛罗里达州在联盟之前的数据样本的样子,它使用 QGIS 进行了可视化:

准备工作
需要遵循常规的业务顺序才能开始这段代码。启动你的虚拟环境,并检查你的数据是否全部下载并位于你的/ch06/geodata/文件夹中。如果一切准备就绪,就直接开始编写代码。
如何操作...
我们的数据至少有点杂乱,所以请按照我们的步骤概述的解决方案进行操作,以便我们能够处理并运行分析函数union:
# #!/usr/bin/env python
# -*- coding: utf-8 -*-
from shapely.geometry import MultiPolygon
from shapely.ops import cascaded_union
from os.path import realpath
from utils import create_shapes
from utils import out_geoj
from utils import write_wkt
def check_geom(in_geom):
"""
:param in_geom: input valid Shapely geometry objects
:return: Shapely MultiPolygon cleaned
"""
plys = []
for g in in_geom:
# if geometry is NOT valid
if not g.is_valid:
print "Oh no invalid geometry"
# clean polygon with buffer 0 distance trick
new_ply = g.buffer(0)
print "now lets make it valid"
# add new geometry to list
plys.append(new_ply)
else:
# add valid geometry to list
plys.append(g)
# convert new polygons into a new MultiPolygon
out_new_valid_multi = MultiPolygon(plys)
return out_new_valid_multi
if __name__ == "__main__":
# input NOAA Shapefile
shp = realpath("../geodata/temp-all-warn-week.shp")
# output union_dissolve results as GeoJSON
out_geojson_file = realpath("../geodata/ch06-03_union_dissolve.geojson")
out_wkt_js = realpath("ol3/data/ch06-03_results_union.js")
# input Shapefile and convert to Shapely geometries
shply_geom = create_shapes(shp)
# Check the Shapely geometries if they are valid if not fix them
new_valid_geom = check_geom(shply_geom)
# run our union with dissolve
dissolve_result = cascaded_union(new_valid_geom)
# output the resulting union dissolved polygons to GeoJSON file
out_geoj(dissolve_result, out_geojson_file)
write_wkt(out_wkt_js, dissolve_result)
你的结果网络地图将看起来像这样:

它是如何工作的...
我们开始越来越多地重用现在藏在我们/ch06/code/utils.py模块中的代码。正如你在导入中看到的那样,我们使用三个函数进行数据的标准输入和输出。主应用程序从定义我们的 NOAA 输入 Shapefile 和定义输出 GeoJSON 文件开始。然后,如果我们运行代码,它将由于数据有效性问题而崩溃。因此,我们创建了一个新函数来检查我们的输入数据中的无效几何形状。这个新函数将捕获这些无效几何形状并将它们转换为有效的多边形。
Shapely 有一个名为is_valid的几何属性,它访问 GEOS 引擎,根据 OGC 规范中的简单特征来检查几何的有效性。
小贴士
如果你正在寻找所有可能的无效数据可能性,你可以在开放地理空间联盟网站上找到更多信息。查看第28页的简单特征标准;你将在portal.opengeospatial.org/files/?artifact_id=25355找到无效多边形的示例。
这些异常的原因是,当数据重叠和处理时,几何形状会以不是总是最优的角度组合或切割。
最后,我们有了干净的数据可以工作,通过运行 Shapely 的cascaded_union()函数,这将溶解我们所有的重叠多边形。我们的结果多边形进一步推入我们的out_geoj()函数,该函数最终将新的几何形状写入我们/ch06/geodata文件夹中的磁盘。
执行身份函数(差集+交集)
在 ESRI 地理处理术语中,有一个名为identity的重叠功能。当你想要保留所有原始几何边界,并且仅与输入特征的重叠相结合时,这是一个非常有用的功能。

这归结为一个公式,需要同时调用difference和intersect。我们首先找到差集(输入特征 - 交集),然后添加交集以创建我们的结果如下:
(input feature – intersection) + intersection = result
如何做到这一点...
-
对于所有好奇的人,如果你想知道如何做到这一点,请输入以下代码;它将帮助你记忆肌肉:
##!/usr/bin/env python # -*- coding: utf-8 -*- from shapely.geometry import asShape, MultiPolygon from utils import shp2_geojson_obj, out_geoj, write_wkt from os.path import realpath def create_polys(shp_data): """ :param shp_data: input GeoJSON :return: MultiPolygon Shapely geometry """ plys = [] for feature in shp_data['features']: shape = asShape(feature['geometry']) plys.append(shape) new_multi = MultiPolygon(plys) return new_multi def create_out(res1, res2): """ :param res1: input feature :param res2: identity feature :return: MultiPolygon identity results """ identity_geoms = [] for g1 in res1: identity_geoms.append(g1) for g2 in res2: identity_geoms.append(g2) out_identity = MultiPolygon(identity_geoms) return out_identity if __name__ == "__main__": # out two input test Shapefiles shp1 = realpath("../geodata/temp1-ply.shp") shp2 = realpath("../geodata/temp2-ply.shp") # output resulting GeoJSON file out_geojson_file = realpath("../geodata/result_identity.geojson") output_wkt_identity = realpath("ol3/data/ch06-04_results_identity.js") # convert our Shapefiles to GeoJSON # then to python dictionaries shp1_data = shp2_geojson_obj(shp1) shp2_data = shp2_geojson_obj(shp2) # transform our GeoJSON data into Shapely geom objects shp1_polys = create_polys(shp1_data) shp2_polys = create_polys(shp2_data) # run the difference and intersection res_difference = shp1_polys.difference(shp2_polys) res_intersection = shp1_polys.intersection(shp2_polys) # combine the difference and intersection polygons into results result_identity = create_out(res_difference, res_intersection) # export identity results to a GeoJSON out_geoj(result_identity, out_geojson_file) # write out new JavaScript variable with wkt geometry write_wkt(output_wkt_identity, result_identity )现在生成的多边形可以在你的浏览器中可视化。现在只需打开
/ch06/code/ol3/html/ch06-04_identity.html文件,你将看到这张地图:![如何做到这一点...]()
它是如何工作的...
我们在我们的util.py工具文件中隐藏了两颗宝石,名为shp2_geojson_obj和out_geoj。第一个函数接收我们的 Shapefile 并返回一个 Python 字典对象。我们的函数实际上创建了一个有效的 GeoJSON,以 Python 字典的形式,可以很容易地使用标准的json.dumps()Python 模块转换为 JSON 字符串。
在处理完这些前置工作之后,我们可以跳到创建 Shapely 几何体,这些几何体可以用于我们的分析。create_polys()函数正是这样做的:它接收我们的几何体,返回一个MultiPolygon。这个MultiPolygon用于计算我们的差集和交集。
因此,最后,我们可以从 Shapely 的差集函数开始进行分析计算,使用我们的temp1-ply.shp作为输入特征,temp2-poly.shp作为身份特征。差集函数只返回不与另一个特征相交的输入特征的几何体。接下来,我们执行交集函数,它只返回两个输入之间的重叠几何体。
我们的配方几乎完成了;我们只需要将这两个新结果结合起来,以产生我们新的身份结果的多边形。create_out()函数接受两个参数,第一个是我们的输入特征,第二个是我们的结果交集特征。顺序非常重要;否则你的结果将被反转。所以请确保你输入正确的顺序。
我们遍历每个几何体,将它们组合成一个名为result_identity的复杂新MultiPolygon。然后将其泵入我们的out_geoj()函数,该函数将写入一个新的 GeoJSON 文件到你的/ch06/geodata文件夹。
我们的out_geoj()函数位于utils.py文件中,可能需要简要说明。输入是一个几何列表和输出 GeoJSON 文件在磁盘上的文件路径。我们简单地创建一个新的字典,然后遍历每个几何体,使用内置的 Shapely __geo_interface__将 Shapely 几何体导出到 GeoJSON 文件。
注意
如果你想了解__geo_interface__,请自行查阅并了解它是什么以及为什么它如此酷,请访问gist.github.com/sgillies/2217756。
对于那些正在寻找两个效用函数的各位,这里就是供您阅读的版本:
def shp2_geojson_obj(shapefile_path):
# open shapefile
in_ply = shapefile.Reader(shapefile_path)
# get a list of geometry and records
shp_records = in_ply.shapeRecords()
# get list of fields excluding first list object
fc_fields = in_ply.fields[1:]
# using list comprehension to create list of field names
field_names = [field_name[0] for field_name in fc_fields ]
my_fc_list = []
# run through each shape geometry and attribute
for x in shp_records:
field_attributes = dict(zip(field_names, x.record))
geom_j = x.shape.__geo_interface__
my_fc_list.append(dict(type='Feature', geometry=geom_j,
properties=field_attributes))
geoj_json_obj = {'type': 'FeatureCollection',
'features': my_fc_list}
return geoj_json_obj
def out_geoj(list_geom, out_geoj_file):
out_geojson = dict(type='FeatureCollection', features=[])
# generate geojson file output
for (index_num, ply) in enumerate(list_geom):
feature = dict(type='Feature', properties=dict(id=index_num))
feature['geometry'] = ply.__geo_interface__
out_geojson['features'].append(feature)
# create geojson file on disk
json.dump(out_geojson, open(out_geoj_file, 'w'))
第七章. 栅格分析
在本章中,我们将涵盖以下主题:
-
将 USGS ACSII CDED 格式的 DEM 加载到 PostGIS 中
-
创建高程剖面
-
使用 ogr 从您的 DEM 创建阴影栅格
-
从您的 DEM 生成坡度和方位图像
-
合并栅格以生成彩色地形图
简介
栅格分析的工作方式与矢量分析类似,但空间关系由栅格单元格的位置决定。我们的大部分栅格数据都是通过多种遥感技术收集的。在本章中,目标非常简单且专注于处理和围绕数字高程模型(DEM)。我们使用的 DEM 来自加拿大不列颠哥伦比亚省惠斯勒,这里是 2010 年冬季奥运会的举办地。我们的 DEM 是以 USGS ASCII CDED(.dem)格式存在的。DEM 是我们用于派生几个新的栅格数据集的源数据。与其他章节一样,我们将利用 Python 作为粘合剂来运行脚本,以实现栅格数据的处理流程。我们的数据可视化将通过 matplotlib 和 QGIS 桌面 GIS 来完成。
将 USGS ACSII CDED 格式的 DEM 加载到 PostGIS 中
导入和处理 PostGIS 中的 DEM 是本菜谱的主要内容。我们的旅程从一个充满点且以 USGS ASCII CDED 格式存储的文本文件开始(要了解更多关于此格式的详细信息,请自由查看www.gdal.org/frmt_usgsdem.html的文档页面)。ASCII 格式是众所周知且被许多桌面 GIS 应用程序作为直接数据源所接受的。您可以自由地使用 QGIS 打开您的 ASCII 文件来查看文件,并查看它为您创建的栅格表示。我们当前的任务是将此 DEM 文件导入 PostGIS 数据库,在 PostGIS 中创建一个新的 PostGIS 栅格数据集。我们通过使用与 PostGIS 安装一起安装的命令行工具raster2pgsql来完成此任务。如果您正在运行 PostgreSQL 9,则raster2pgsql工具位于 Windows 上的C:\Program Files\PostgreSQL\9.3\bin\。
准备工作
您的数据位于ch07/geodata/dem_3857.dem文件夹中。您可以自由地从 GeoGratis Canada 获取原始 DEM,这是不列颠哥伦比亚省惠斯勒山周围地区,请访问ftp2.cits.rncan.gc.ca/pub/geobase/official/cded/50k_dem/092/092j02.zip。
如果您还没有在第一章中创建您的Postgresql数据库,设置您的地理空间 Python 环境,请现在创建,然后继续启动虚拟环境以运行此脚本。
此外,请确保raster2pgsql命令在您的命令提示符中可用。如果不是,请在 Windows 上设置您的环境变量或在 Linux 机器上设置符号链接。
如何操作...
让我们继续到 /ch07/code/ch07-01_dem2postgis.py 文件中可以找到的有趣部分:
-
在
/ch07/code/ch07-01_dem2postgis.py文件中找到的代码如下:#!/usr/bin/env python # -*- coding: utf-8 -*- import subprocess import psycopg2 db_host = "localhost" db_user = "pluto" db_passwd = "secret" db_database = "py_geoan_cb" db_port = "5432" # connect to DB conn = psycopg2.connect(host=db_host, user=db_user, port=db_port, password=db_passwd, database=db_database) # create a cursor cur = conn.cursor() # input USGS ASCII DEM (and CDED) input_dem = "../geodata/dem_3857.dem" # create an sql file for loading into the PostGIS database raster # command line with options # -c create new table # -I option will create a spatial GiST index on the raster column # -C will apply raster constraints # -M vacuum analyse the raster table command = 'raster2pgsql -c -C -I -M ' + input_dem + ' geodata.dem_3857' # write the output to a file temp_sql_file = "temp_sql.sql" # open, create new file to write sql statements into with open(temp_sql_file, 'wb') as f: try: result = subprocess.call(command, stdout=f, shell=True) if result != 0: raise Exception('error code %d' % result) except Exception as e: print e # open the file full of insert statements created by raster2pgsql with open(temp_sql_file, 'r') as r: # run through and execute each line inside the temp sql file for sql_insert in r: cur.execute(sql_insert) print "please open QGIS >= 2.8.x and view your loaded DEM data"
它是如何工作的...
Python,再次成为我们的粘合剂,利用命令行工具的力量来完成脏活。这次,我们使用 Python 的 subprocess 模块来调用 raster2pgsql 命令行工具。然后 psycopg2 模块执行我们的 insert 语句。
从顶部开始,向下工作,我们看到 psycopg2 的数据库连接设置。我们的 DEM 输入路径设置为 input_dem 变量。然后,我们将命令行参数打包成一个名为 command 的单个字符串。然后通过 subprocess 运行它。单个命令行参数在代码注释中描述,更多信息选项可以直接在 postgis.refractions.net/docs/using_raster.xml.html#RT_Raster_Loader 找到。
现在命令已经准备好了,我们需要创建一个临时文件来存储 raster2pgsql 命令生成的 SQL insert 和 create 语句。使用 with open() 语法,我们创建我们的临时文件,然后使用 subprocess 调用命令。我们使用 stdout 来指定输出文件的路径。shell=True 参数附带一个 重要 警告。
注意
以下是从 Python 文档中摘取的 mention 警告:
Warning Executing shell commands that incorporate unsanitized input from an untrusted source makes a program vulnerable to shell injection, a serious security flaw which can result in arbitrary command execution. For this reason, the use of shell=True is strongly discouraged in cases where the command string is constructed from external input:
如果一切顺利,不应该出现任何异常,但如果出现了,我们会使用标准的 Python try 语句来捕获它们。
最后一步是打开新创建的包含插入语句的 SQL 文件,并使用 psycopg2 执行文件中的每一行。这将填充我们新创建的名为输入 DEM 文件名称的表。
打开 QGIS | 2.8.x 并查看你刚刚加载到 PostGIS 中的栅格。
小贴士
要在 QGIS 中打开栅格,我发现你需要打开 QGIS 附加的数据库管理器应用程序,连接到你的 Postgresql-PostGIS 数据库和模式。然后,你将看到新的栅格,你需要右键单击它将其添加到画布上。这将最终将栅格添加到你的 QGIS 项目中。
创建高程剖面
创建高程剖面在尝试可视化 3D 地形横截面或简单地查看自行车之旅的高程增益时非常有帮助。在这个例子中,我们将定义自己的 LineString 几何形状,并从位于我们沿线每 20 米处的 DEM 中提取高程值。分析将生成一个新的 CSV 文件,我们可以在 Libre Office Calc 或 Microsoft Excel 中打开它,将新数据可视化为折线图。
在 QGIS 内部看到的高程模型上方的线(二维视图)看起来像这样:

准备工作
此配方需要 GDAL 和 Shapely。请确保您已安装它们,并且正在您之前设置的 python 虚拟环境中运行它们。为了可视化您的最终 CSV 文件,您还必须安装 Libre Office Calc 或其他图表软件。执行此操作的代码位于/ch07/code/ch07-02_elev_profile.py。
如何做到这一点...
直接从您的命令行运行脚本将生成您的 CSV 文件,因此请阅读代码注释以了解生成我们新文件的所有细节,如下所示:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys, gdal, os
from gdalconst import GA_ReadOnly
from os.path import realpath
from shapely.geometry import LineString
def get_elevation(x_coord, y_coord, raster, bands, gt):
"""
get the elevation value of each pixel under
location x, y
:param x_coord: x coordinate
:param y_coord: y coordinate
:param raster: gdal raster open object
:param bands: number of bands in image
:param gt: raster limits
:return: elevation value of raster at point x,y
"""
elevation = []
xOrigin = gt[0]
yOrigin = gt[3]
pixelWidth = gt[1]
pixelHeight = gt[5]
px = int((x_coord - xOrigin) / pixelWidth)
py = int((y_coord - yOrigin) / pixelHeight)
for j in range(bands):
band = raster.GetRasterBand(j + 1)
data = band.ReadAsArray(px, py, 1, 1)
elevation.append(data[0][0])
return elevation
def write_to_csv(csv_out,result_profile_x_z):
# check if output file exists on disk if yes delete it
if os.path.isfile(csv_out):
os.remove(csv_out)
# create new CSV file containing X (distance) and Z value pairs
with open(csv_out, 'a') as outfile:
# write first row column names into CSV
outfile.write("distance,elevation" + "\n")
# loop through each pair and write to CSV
for x, z in result_profile_x_z:
outfile.write(str(round(x, 2)) + ',' + str(round(z, 2)) + '\n')
if __name__ == '__main__':
# set directory
in_dem = realpath("../geodata/dem_3857.dem")
# open the image
ds = gdal.Open(in_dem, GA_ReadOnly)
if ds is None:
print 'Could not open image'
sys.exit(1)
# get raster bands
bands = ds.RasterCount
# get georeference info
transform = ds.GetGeoTransform()
# line defining the the profile
line = LineString([(-13659328.8483806, 6450545.73152317), (-13651422.7820022, 6466228.25663444)])
# length in meters of profile line
length_m = line.length
# lists of coords and elevations
x = []
y = []
z = []
# distance of the topographic profile
distance = []
for currentdistance in range(0, int(length_m), 20):
# creation of the point on the line
point = line.interpolate(currentdistance)
xp, yp = point.x, point.y
x.append(xp)
y.append(yp)
# extraction of the elevation value from the MNT
z.append(get_elevation(xp, yp, ds, bands, transform)[0])
distance.append(currentdistance)
print (x)
print (y)
print (z)
print (distance)
# combine distance and elevation vales as pairs
profile_x_z = zip(distance,z)
csv_file = os.path.realpath('../geodata/output_profile.csv')
# output final csv data
write_to_csv(csv_file, profile_x_z)
它是如何工作的...
有两个函数用于创建我们的高程剖面。第一个get_elevation()函数返回每个波段中每个像素的单个高程值。这意味着我们的输入栅格可以包含多个数据波段。我们的第二个函数将我们的结果写入 CSV 文件。
get_elevation()函数创建一个高程值列表;为了实现这一点,我们需要从我们的输入高程栅格中提取一些细节。x和y起始坐标与栅格像素宽度和高度结合使用,以帮助我们找到我们的栅格中的像素。然后,这些信息与我们的输入x和y坐标一起处理,这些坐标是我们想要提取高程值的位置。
接下来,我们遍历我们的栅格中所有可用的波段,并找到位于输入x和y坐标处的每个波段的高程值。GDAL 的ReadAsArray函数找到这个位置,然后我们只需要获取第二个嵌套列表数组中的第一个对象。然后将此值附加到新的高程值列表中。
为了处理我们的数据,我们使用 Python 函数os.path.realpath()定义我们的栅格输入路径,该函数返回我们的输入的完整路径。GDAL 用于打开我们的 DEM 栅格并从我们的栅格返回波段数、x起始点、y起始点、像素宽度和像素高度信息。这些信息位于传递给我们的get_elevation()函数的 transform 变量中。
进一步工作,我们定义我们的输入 LineString。这个 LineString 定义了横截面剖面将要被提取的位置。为了处理我们的数据,我们希望在输入 LineString 上每 20 米提取一次高程值。这是在for循环中完成的,因为我们根据 LineString 的长度和我们的 20 米输入来指定范围。使用 Shapely 的Interpolate线性引用函数,我们然后每 20 米创建一个点对象。这些值随后存储在单独的x、y和z列表中,然后进行更新。z列表包含我们新的高程点列表。通过指定由我们的get_elevation()函数返回的列表中的第一个对象,可以收集个别的高程值。
要将这些数据汇总到一个 CSV 文件中,我们使用 Python 的zip函数将距离值与高程值合并。这创建了数据的最后两列,显示了从我们的 LineString 起点在x轴上的距离和y轴上的高程值。
在 Libre Office Calc 或 Microsoft Excel 中可视化结果非常简单。请打开位于您的/ch07/geodata/output_profile.csv文件夹中的输出 CSV 文件,并创建一个简单的折线图:

您生成的图表应类似于前一张截图所示。
要使用 Libre Office Calc 绘制图表,请参阅以下绘图选项:

使用 ogr 从您的 DEM 创建阴影栅格
我们的 DEM 可以作为许多类型派生栅格数据集的基础。其中之一是所谓的阴影栅格数据集。阴影栅格表示 3D 高程数据的 2D 视图,通过赋予灰度栅格阴影并使您能够看到地形的高低,从而产生 3D 效果。阴影是一个纯可视化辅助工具,用于创建外观良好的地图并在 2D 地图上显示地形。
创建阴影的纯 Python 解决方案由 Roger Veciana i Rovira 编写,您可以在geoexamples.blogspot.co.at/2014/03/shaded-relief-images-using-gdal-python.html找到它。在《使用 Python 进行地理空间分析》的第七章“Python 和高程数据”中,Joel Lawhead 也提供了一个很好的解决方案。如果您想了解 ESRI 对阴影的详细描述,请查看这个页面:webhelp.esri.com/arcgisdesktop/9.3/index.cfm?TopicName=How%20Hillshade%20works。gdaldem阴影命令行工具将被用来生成磁盘上的图像。

准备工作
本例的先决条件需要gdal(osgeo)、numpy和matplotlibPython 库。此外,您需要下载本书的数据文件夹,并确保/ch07/geodata文件夹可用于读写访问。我们将直接访问磁盘上的 USGS ASCII CDED DEM .dem文件以渲染阴影,因此请确保您有这个文件夹。代码执行将像往常一样在您的/ch07/code/文件夹中进行,该文件夹运行ch07-03_shaded_relief.pyPython 文件。因此,对于急于编码的开发者,请在命令行中按照以下方式尝试:
>> python ch07-03_shaded_relief.py
如何操作...
我们的 Python 脚本将执行几个数学运算,并调用 gdaldem 命令行工具,按照以下步骤生成输出:
-
代码中包含一些不是总是容易跟上的数学;灰度值的计算取决于高程及其周围的像素,所以请继续阅读:
#!/usr/bin/env python # -*- coding: utf-8 -*- from osgeo import gdal from numpy import gradient from numpy import pi from numpy import arctan from numpy import arctan2 from numpy import sin from numpy import cos from numpy import sqrt import matplotlib.pyplot as plt import subprocess def hillshade(array, azimuth, angle_altitude): """ :param array: input USGS ASCII DEM / CDED .dem :param azimuth: sun position :param angle_altitude: sun angle :return: numpy array """ x, y = gradient(array) slope = pi/2\. - arctan(sqrt(x*x + y*y)) aspect = arctan2(-x, y) azimuthrad = azimuth * pi / 180. altituderad = angle_altitude * pi / 180. shaded = sin(altituderad) * sin(slope)\ + cos(altituderad) * cos(slope)\ * cos(azimuthrad - aspect) return 255*(shaded + 1)/2 ds = gdal.Open('../geodata/092j02_0200_demw.dem') arr = ds.ReadAsArray() hs_array = hillshade(arr, 90, 45) plt.imshow(hs_array,cmap='Greys') plt.savefig('../geodata/hillshade_whistler.png') plt.show() # gdal command line tool called gdaldem # link http://www.gdal.org/gdaldem.html # usage: # gdaldem hillshade input_dem output_hillshade # [-z ZFactor (default=1)] [-s scale* (default=1)]" # [-az Azimuth (default=315)] [-alt Altitude (default=45)] # [-alg ZevenbergenThorne] [-combined] # [-compute_edges] [-b Band (default=1)] [-of format] [-co "NAME=VALUE"]* [-q] create_hillshade = '''gdaldem hillshade -az 315 -alt 45 ../geodata/092j02_0200_demw.dem ../geodata/hillshade_3857.tif''' subprocess.call(create_hillshade)
它是如何工作的...
阴影功能计算每个单元格的坡度和方向值,作为计算阴影灰度值的输入。azimuth变量定义了光线以度数击中我们的 DEM 的方向。反转和调整azimuth可以产生一些效果,例如山谷看起来像山,山看起来像山谷。我们的shaded变量持有阴影值作为数组,我们可以使用 matplotlib 进行绘图。
使用gdaldem命令行工具肯定比纯 Python 解决方案更健壮且更快。使用gdaldem,我们在磁盘上创建一个新的阴影 TIF 文件,可以用本地图像查看器打开,也可以拖放到 QGIS 中。QGIS 会自动拉伸灰度值,以便您可以看到您阴影的漂亮表示。
从您的 DEM 生成坡度和方向图像
坡度图非常有用,例如,可以帮助生物学家识别栖息地区域。某些物种只生活在非常陡峭的地区——例如高山山羊。坡度栅格可以快速识别潜在的栖息地区域。为了可视化这一点,我们使用 QGIS 显示我们的坡度图,它将类似于以下图像。白色区域表示较陡的区域,颜色越深,地形越平坦:

我们的方向图显示了表面朝向的方向——例如北、东、南和西——这以度数表示。在屏幕截图中,橙色区域代表温暖的朝南区域。朝北的侧面较冷,并且用我们颜色光谱的不同色调表示。为了达到这些颜色,我们将 QGIS 单波段伪彩色分类为五个连续类别,如下面的屏幕截图所示:

准备工作
确保您的/ch07/geodata文件夹已下载,并且加拿大不列颠哥伦比亚省惠斯勒的 DEM 文件092j02_0200_demw.dem可用。
如何操作...
-
我们使用
gdaldem命令行工具创建我们的坡度栅格。您可以调整此配方以批量生成多个 DEM 栅格的坡度图像。#!/usr/bin/env python # -*- coding: utf-8 -*- import subprocess # SLOPE # - To generate a slope map from any GDAL-supported elevation raster : # gdaldem slope input_dem output_slope_map" # [-p use percent slope (default=degrees)] [-s scale* (default=1)] # [-alg ZevenbergenThorne] # [-compute_edges] [-b Band (default=1)] [-of format] [-co "NAME=VALUE"]* [-q] create_slope = '''gdaldem slope ../geodata/092j02_0200_demw.dem ../geodata/slope_w-degrees.tif ''' subprocess.call(create_slope) # ASPECT # - To generate an aspect map from any GDAL-supported elevation raster # Outputs a 32-bit float raster with pixel values from 0-360 indicating azimuth : # gdaldem aspect input_dem output_aspect_map" # [-trigonometric] [-zero_for_flat] # [-alg ZevenbergenThorne] # [-compute_edges] [-b Band (default=1)] [-of format] [-co "NAME=VALUE"]* [-q] create_aspect = '''gdaldem aspect ../geodata/092j02_0200_demw.dem ../geodata/aspect_w.tif ''' subprocess.call(create_aspect)
它是如何工作的...
gdaldem命令行工具再次成为我们的工作马,我们只需要传递我们的 DEM 并指定一个输出文件。在代码内部,您会看到传递的参数包括-co compress=lzw,这可以显著减小图像的大小。我们的-p选项表示我们希望结果以百分比坡度表示,然后是输入 DEM 和我们的输出文件。
对于我们的gdaldem方向栅格,这次同样适用相同的压缩,并且不需要其他参数来生成方向栅格。要可视化方向栅格,请在 QGIS 中打开它,并分配一个颜色,如介绍中所述。
合并栅格以生成彩色高程图
使用gdaldem color-relief命令行生成彩色高程栅格是一行代码。如果你想要更直观的效果,我们将执行一个组合,包括坡度、阴影和一些颜色高程。我们的最终结果是单个新的栅格,表示层合并,以给出一个美观的高程视觉效果。结果看起来将类似于以下图像:

准备工作
对于这个练习,你需要安装包含gdaldem命令行工具的 GDAL 库。
如何操作...
-
让我们从使用
gdalinfo\ch07\code>gdalinfo ../geodata/092j02_0200_demw.dem命令行工具从我们的 DEM 中提取一些关键信息开始,如下所示:Driver: USGSDEM/USGS Optional ASCII DEM (and CDED) Files: ../geodata/092j02_0200_demw.dem ../geodata/092j02_0200_demw.dem.aux.xml Size is 1201, 1201 Coordinate System is: GEOGCS["NAD83", DATUM["North_American_Datum_1983", SPHEROID["GRS 1980",6378137,298.257222101, AUTHORITY["EPSG","7019"]], TOWGS84[0,0,0,0,0,0,0], AUTHORITY["EPSG","6269"]], PRIMEM["Greenwich",0, AUTHORITY["EPSG","8901"]], UNIT["degree",0.0174532925199433, AUTHORITY["EPSG","9108"]], AUTHORITY["EPSG","4269"]] Origin = (-123.000104166666630,50.250104166666667) Pixel Size = (0.000208333333333,-0.000208333333333) Metadata: AREA_OR_POINT=Point Corner Coordinates: Upper Left (-123.0001042, 50.2501042) (123d 0' 0.37"W, 50d15' 0.38"N) Lower Left (-123.0001042, 49.9998958) (123d 0' 0.37"W, 49d59'59.63"N) Upper Right (-122.7498958, 50.2501042) (122d44'59.62"W, 50d15' 0.38"N) Lower Right (-122.7498958, 49.9998958) (122d44'59.62"W, 49d59'59.63"N) Center (-122.8750000, 50.1250000) (122d52'30.00"W, 50d 7'30.00"N) Band 1 Block=1201x1201 Type=Int16, ColorInterp=Undefined Min=348.000 Max=2885.000 Minimum=348.000, Maximum=2885.000, Mean=1481.196, StdDev=564.262 NoData Value=-32767 Unit Type: m Metadata: STATISTICS_MAXIMUM=2885 STATISTICS_MEAN=1481.1960280116 STATISTICS_MINIMUM=348 STATISTICS_STDDEV=564.26229690401 -
这些关键信息随后被用来创建我们的颜色
ramp.txt文件。首先创建一个名为ramp.txt的新文本文件,并输入以下颜色代码:-32767 255 255 255 0 46 154 88 360 251 255 128 750 96 108 31 1100 148 130 55 2900 255 255 255 -
-32767值定义了我们的NODATA值,在白色(255 255 255)RGB 颜色中。现在,将ramp.txt文件保存在以下代码相同的文件夹中,该代码将生成新的栅格彩色高程:#!/usr/bin/env python # -*- coding: utf-8 -*- import subprocess dem_file = '../geodata/092j02_0200_demw.dem' hillshade_relief = '../geodata/hillshade.tif' relief = '../geodata/relief.tif' final_color_relief = '../geodata/final_color_relief.tif' create_hillshade = 'gdaldem hillshade -co compress=lzw -compute_edges ' + dem_file + ' ' + hillshade_relief subprocess.call(create_hillshade, shell=True) print create_hillshade cr = 'gdaldem color-relief -co compress=lzw ' + dem_file + ' ramp.txt ' + relief subprocess.call(cr) print cr merge = 'python hsv_merge.py ' + relief + ' ' + hillshade_relief + ' ' + final_color_relief subprocess.call(merge) print merge create_slope = '''gdaldem slope -co compress=lzw ../geodata/092j02_0200_demw.dem ../geodata/slope_w-degrees.tif ''' subprocess.call(create_slope)
工作原理...
我们需要链式连接一些命令和变量以获得期望的结果,使其看起来更好。开始我们的旅程,我们将从 DEM 中提取一些关键信息,以便我们能够创建一个颜色渐变,定义哪些颜色被分配给高程值。这个新的ramp.txt文件存储我们的颜色渐变值,然后由gdaldem color-relief命令使用。
代码首先定义了在整个脚本中需要的输入和输出变量。在前面的代码中,我们定义了输入DEM和三个输出.tif文件。
第一次调用将执行gdaldem hillshade命令以生成我们的阴影图。紧接着是gdaldem color-relief命令,创建基于我们定义的ramp.txt文件的好看的彩色栅格。ramp.txt文件包含 NODATA 值并将其设置为白色 RGB 颜色。五个类别基于 DEM 数据本身。
最终合并使用 Frank Warmerdam 的hsv_merge.py脚本完成,该脚本将我们的高程输出与生成的阴影栅格合并,留下我们的最终栅格。我们的结果是颜色高程图和阴影的漂亮组合。
第八章:网络路由分析
在本章中,我们将涵盖以下主题:
-
使用 pgRouting 找到 Dijkstra 最短路径
-
使用纯 Python 中的 NetworkX 找到 Dijkstra 最短路径
-
根据室内最短路径生成疏散多边形
-
从多边形创建中心线
-
在 3D 中构建室内路由系统
-
计算室内路线步行时间
简介
路由已成为全球道路网络导航设备上的常见功能。如果您想知道如何从点 A 开车到点 B,只需将起始地址和结束地址输入到您的导航软件中,它将在几秒钟内为您计算出最短路线。
这里有一个你可能遇到的场景:把我带到任何大学地理系的 Smith 教授办公室,以便参加我的会议。嗯,抱歉,我的导航软件上没有可用的路由网络。这是提醒您不要忘记在校园内询问您的会议地点方向。
本章全部关于路由,特别是从办公室 A33(位于建筑 E01 的第一层)到办公室 B55(位于建筑 P7 的第六层)的室内大型建筑群内的路由。

我们将探索pgRouting(PostgreSQL 的一个扩展)强大的路由功能。使用 pgRouting,我们可以使用 Dijkstra、A*和/或 K 最短路径算法中的任何一个来计算最短路径。除了 pgRouting,我们还将使用 NetworkX 库的纯 Python 解决方案,从相同的数据源生成路线。
注意
重要提示。请注意使用的输入网络数据集,并确保它位于 EPSG: 3857 坐标系中,这是一个几何笛卡尔米制系统。如果使用 EPSG: 4326 世界坐标系进行路由计算,则必须进行转换。此外,请注意,即使坐标存储在 EPSG: 3857 中,QGIS 也将 GeoJSON 坐标系解释为 EPSG: 4326!
使用 pgRouting 找到 Dijkstra 最短路径
现在有几个 Python 库,例如networkX和scikit-image,可以在栅格或 NumPy 数组上找到最短路径。我们希望专注于矢量源的路由并返回矢量数据集;因此,pgRouting 是我们自然的选择。虽然存在自定义 Python Dijkstra 或 A Star (A)* 最短路径算法,但找到一个在大网络上表现良好的算法是困难的。PostgreSQL 的pgRouting扩展被 OSM 和其他许多项目使用,并且经过了良好的测试。
我们的示例将让我们为了简单起见,从一个楼层的室内网络加载 Shapefile。室内网络由沿着建筑物走廊和开放步行空间的网络线组成,通常通向一扇门。
准备工作
对于这个菜谱,我们需要设置带有 pgRouting 扩展的 PostGIS 数据库。在 Windows 机器上,您可以通过下载 Postgresql 9.3 的 ZIP 文件来安装 pgRouting,网址为 winnie.postgis.net/download/windows/pg93/buildbot/。然后,将 ZIP 文件解压到 C:\Program Files\PostgreSQL\9.3\。
对于 Ubuntu Linux 用户,pgRouting 网站在 docs.pgrouting.org/2.0/en/doc/src/installation/index.html#ubuntu-debian 解释了详细信息。
要启用此扩展,您有几个选择。首先,如果您已按照 第一章 中所述设置 PostgreSQL,则可以运行命令行 psql 工具来激活扩展,如下所示:
> psql py_geoan_cb -c "create extension pgrouting"
您可以通过打开 py_geoan_cb 数据库,右键单击 Extensions,选择 New Extension..., 并在 Name 字段中向下滚动以找到 pgRouting 条目并选择它来使用 pgAdmin 用户工具。
现在我们需要一些数据进行路由计算。使用的数据是位于您 /ch08/geodata/shp/e01_network_lines_3857.shp 文件夹中的 Shapefile。请参阅 第三章,了解如何导入 Shapefile 或使用 shp2pgsql。以下是使用 ogr2ogr 导入 Shapefile 的命令行单行命令:
>ogr2ogr -a_srs EPSG:3857 -lco "SCHEMA=geodata" -lco "COLUMN_TYPES=type=varchar,type_id=integer" -nlt MULTILINESTRING -nln ch08_e01_networklines -f PostgreSQL "PG:host=localhost port=5432 user=pluto dbname=py_geoan_cb password=secret" geodata/shp/e01_network_lines_3857.shp
注意,您可以使用 第一章 中相同的用户名和密码,或者您自己的定义的用户名和密码。
对于 Windows 用户,您可能需要插入您的 Shapefile 的完整路径,这可能看起来像 c:\somepath\geodata\shp\e01_network_lines.shp。我们明确设置 EPSG:3857 Web Mercator 的输入,因为有时 ogr2ogr 估计的投影是错误的,这样就可以确保上传时是正确的。另一个需要注意的事项是我们还明确定义了输出表列类型,因为 ogr2ogr 使用数字字段来表示我们的整数,而这与 pgRouting 不兼容,所以我们明确传递了字段名称和字段类型的逗号分隔列表。
小贴士
要详细了解 ogr2ogr 的工作原理,请访问 gdal.org/ogr2ogr.html。
我们的新表包括两个字段,一个称为 type,另一个称为 type_id。type_id 变量将存储一个整数,用于识别我们所在的网络段类型,例如楼梯、室内路径或电梯。其余字段对于 pgRouting 是必要的,如以下代码所示,包括名为 source、target 和 cost 的列。source 和 target 列都需要是整数,而 cost 字段是双精度类型。这些类型是 pgRouting 函数的要求。
让我们继续添加这些字段到我们的 ch08_e01_networklines 表中,借助一些 SQL 查询:
ALTER TABLE geodata.ch08_e01_networklines ADD COLUMN source INTEGER;
ALTER TABLE geodata.ch08_e01_networklines ADD COLUMN target INTEGER;
ALTER TABLE geodata.ch08_e01_networklines ADD COLUMN cost DOUBLE PRECISION;
ALTER TABLE geodata.ch08_e01_networklines ADD COLUMN length DOUBLE PRECISION;
UPDATE geodata.ch08_e01_networklines set length = ST_Length(wkb_geometry);
一旦网络数据集有了新的列,我们需要运行创建拓扑的 pgr_createTopology() 函数。这个函数接受我们的网络数据集名称、容差值、几何字段名称和主键字段名称。该函数将在 LineString 交点处创建一个新的点表,即网络中的节点,它们具有相同的模式:
SELECT public.pgr_createTopology('geodata.ch08_e01_networklines',
0.0001, 'wkb_geometry', 'ogc_fid');
pgr_createTopology 函数的参数包括包含我们的成本和类型字段的网络线 LineStrings 的名称。第二个参数是米为单位的距离容差,然后是几何列的名称和我们的主键唯一标识符 ogc_fid。
现在我们已经设置了表和环境,这使我们能够实际创建最短路径,称为迪杰斯特拉路线。
要运行 Python 代码,请确保你已经按照第一章中描述的安装了 psycopg2 和 geojson 模块,设置你的地理空间 Python 环境。
如何操作...
-
查看以下代码并跟随操作:
#!/usr/bin/env python # -*- coding: utf-8 -*- import psycopg2 import json from geojson import loads, Feature, FeatureCollection db_host = "localhost" db_user = "pluto" db_passwd = "secret" db_database = "py_geoan_cb" db_port = "5432" # connect to DB conn = psycopg2.connect(host=db_host, user=db_user, port=db_port, password=db_passwd, database=db_database) # create a cursor cur = conn.cursor() start_x = 1587927.204 start_y = 5879726.142 end_x = 1587947.304 end_y = 5879611.257 # find the start node id within 1 meter of the given coordinate # used as input in routing query start point start_node_query = """ SELECT id FROM geodata.ch08_e01_networklines_vertices_pgr AS p WHERE ST_DWithin(the_geom, ST_GeomFromText('POINT(%s %s)',3857), 1);""" # locate the end node id within 1 meter of the given coordinate end_node_query = """ SELECT id FROM geodata.ch08_e01_networklines_vertices_pgr AS p WHERE ST_DWithin(the_geom, ST_GeomFromText('POINT(%s %s)',3857), 1); """ # get the start node id as an integer cur.execute(start_node_query, (start_x, start_y)) sn = int(cur.fetchone()[0]) # get the end node id as an integer cur.execute(end_node_query, (end_x, end_y)) en = int(cur.fetchone()[0]) # pgRouting query to return our list of segments representing # our shortest path Dijkstra results as GeoJSON # query returns the shortest path between our start and end nodes above # using the python .format string syntax to insert a variable in the query routing_query = ''' SELECT seq, id1 AS node, id2 AS edge, ST_Length(wkb_geometry) AS cost, ST_AsGeoJSON(wkb_geometry) AS geoj FROM pgr_dijkstra( 'SELECT ogc_fid as id, source, target, st_length(wkb_geometry) as cost FROM geodata.ch08_e01_networklines', {start_node},{end_node}, FALSE, FALSE ) AS dij_route JOIN geodata.ch08_e01_networklines AS input_network ON dij_route.id2 = input_network.ogc_fid ; '''.format(start_node=sn, end_node=en) # run our shortest path query cur.execute(routing_query) # get entire query results to work with route_segments = cur.fetchall() # empty list to hold each segment for our GeoJSON output route_result = [] # loop over each segment in the result route segments # create the list for our new GeoJSON for segment in route_segments: geojs = segment[4] geojs_geom = loads(geojs) geojs_feat = Feature(geometry=geojs_geom, properties={'nice': 'route'}) route_result.append(geojs_feat) # using the geojson module to create our GeoJSON Feature Collection geojs_fc = FeatureCollection(route_result) # define the output folder and GeoJSON file name output_geojson_route = "../geodata/ch08_shortest_path_pgrouting.geojson" # save geojson to a file in our geodata folder def write_geojson(): with open(output_geojson_route, "w") as geojs_out: geojs_out.write(json.dumps(geojs_fc)) # run the write function to actually create the GeoJSON file write_geojson() # clean up and close database curson and connection cur.close() conn.close() -
如果你在例如
pgAdmin中运行这个查询,得到的结果如下:![如何操作...]()
一条路线需要在地图上可视化,而不是表格中。将你新创建的
/ch08/geodata/ch08_shortest_path_pgrouting.geojson文件拖放到 QGIS 中。如果一切顺利,你应该看到这条漂亮的小线,不包括红色箭头和文本:

它是如何工作的...
我们的代码之旅从设置数据库连接开始,以便我们可以对上传的数据执行一些查询。
现在我们已经准备好运行一些路由,但是等等,我们如何设置我们想要路由的起点和终点?自然的方法是输入起点和终点的 x,y 坐标对。不幸的是,pgr_dijkstra() 函数只接受起点和终点节点 ID。这意味着我们需要从名为 ch08_e01_networklines_vertices_pgr 的新表中获取这些节点 ID。为了定位节点,我们使用一个简单的 PostGIS 函数 ST_Within() 来找到距离输入坐标一米内的最近节点。在这个查询中,我们的输入几何形状使用 ST_GeomFromText() 函数,这样你就可以清楚地看到 SQL 中的事情。现在,我们将执行我们的查询并将响应转换为整数值作为我们的节点 ID。这个节点 ID 就准备好输入到下一个和最终的查询中。
路由查询将为最终路线上的每个段落返回一个序列号、节点、边、成本和几何形状。创建的几何形状是使用 ST_AsGeoJSON() PostGIS 函数创建的 GeoJSON,该函数用于生成我们的最终 GeoJSON 输出路线。
pgRouting 的 pgr_dijkstra() 函数的输入参数包括一个 SQL 查询、起点节点 ID、终点节点 ID、有向值和一个 has_rcost 布尔值。我们将 directed 和 has_rcost 值设置为 False,同时传递 start_node 和 end_node ID。此查询在生成的路线 ID 和输入网络 ID 之间执行 JOIN 操作,以便我们有几何输出以进行可视化。
然后,我们的旅程以处理结果和创建我们的输出 GeoJSON 文件结束。路由查询返回了一个从起点到终点的不以单个 LineString 形式存在的单独段落的列表,而是一组许多 LineString。这就是为什么我们需要创建一个列表,并通过创建我们的 GeoJSON FeatureCollection 文件将每个路线段追加到列表中的原因。
在这里,我们使用 write_geojson() 函数输出我们的最终 GeoJSON 文件,名为 ch08_shortest_path_pgrouting.geojson。
注意
注意,这个 GeoJSON 文件位于 EPSG:3857 坐标系中,并被 QGIS 解释为 EPSG:4326,这是不正确的。用于路由的地理数据,如 OSM 数据和自定义数据集,有很多可能的错误、错误和不一致。请注意,这次魔鬼隐藏在数据的细节中,而不是代码中。
将你的 GeoJSON 文件拖放到 QGIS 中,看看你的最终路线看起来如何。
使用纯 Python 在 NetworkX 中找到 Dijkstra 最短路径
这个菜谱是一个纯 Python 解决方案,用于计算网络上的最短路径。NetworkX 是我们将使用的库,它包含许多算法来解决最短路径问题,包括 Dijkstra (networkx.github.io/)。NetworkX 依赖于 numpy 和 scipy 来执行一些图计算并帮助提高性能。在这个菜谱中,我们将仅使用 Python 库来创建基于我们之前菜谱中使用的相同输入 Shapefile 的最短路径。
准备工作
首先,使用以下pip安装程序在您的机器上安装NetworkX:
>> pip install networkx
对于网络图算法,NetworkX 需要numpy和scipy,因此请参阅第一章,设置您的地理空间 Python 环境,了解有关这些内容的说明。我们还使用 Shapely 生成我们的几何输出以创建 GeoJSON 文件,因此请检查您是否已安装 Shapely。一个隐藏的要求是 GDAL/OGR 在 NetworkX 的import Shapefile函数的后端使用。如前所述,在第一章中,您将找到有关此主题的说明。
表示我们网络的输入数据是一个位于/ch08/geodata/shp/e01_network_lines_3857.shp的 Shapefile,其中包含我们已准备好的用于路由的网络数据集,因此请确保您下载了本章。现在您已准备好运行示例。
如何操作...
-
您需要从命令行运行此代码以生成结果输出 GeoJSON 文件,您可以在 QGIS 中打开这些文件,因此请跟随操作:
#!/usr/bin/env python # -*- coding: utf-8 -*- import networkx as nx import numpy as np import json from shapely.geometry import asLineString, asMultiPoint def get_path(n0, n1): """If n0 and n1 are connected nodes in the graph, this function will return an array of point coordinates along the line linking these two nodes.""" return np.array(json.loads(nx_list_subgraph[n0][n1]['Json'])['coordinates']) def get_full_path(path): """ Create numpy array line result :param path: results of nx.shortest_path function :return: coordinate pairs along a path """ p_list = [] curp = None for i in range(len(path)-1): p = get_path(path[i], path[i+1]) if curp is None: curp = p if np.sum((p[0]-curp)**2) > np.sum((p[-1]-curp)**2): p = p[::-1, :] p_list.append(p) curp = p[-1] return np.vstack(p_list) def write_geojson(outfilename, indata): """ create GeoGJSOn file :param outfilename: name of output file :param indata: GeoJSON :return: a new GeoJSON file """ with open(outfilename, "w") as file_out: file_out.write(json.dumps(indata)) if __name__ == '__main__': # use Networkx to load a Noded shapefile # returns a graph where each node is a coordinate pair # and the edge is the line connecting the two nodes nx_load_shp = nx.read_shp("../geodata/shp/e01_network_lines_3857.shp") # A graph is not always connected, so we take the largest connected subgraph # by using the connected_component_subgraphs function. nx_list_subgraph = list(nx.connected_component_subgraphs(nx_load_shp.to_undirected()))[0] # get all the nodes in the network nx_nodes = np.array(nx_list_subgraph.nodes()) # output the nodes to a GeoJSON file to view in QGIS network_nodes = asMultiPoint(nx_nodes) write_geojson("../geodata/ch08_final_netx_nodes.geojson", network_nodes.__geo_interface__) # this number represents the nodes position # in the array to identify the node start_node_pos = 30 end_node_pos = 21 # Compute the shortest path. Dijkstra's algorithm. nx_short_path = nx.shortest_path(nx_list_subgraph, source=tuple(nx_nodes[start_node_pos]), target=tuple(nx_nodes[end_node_pos]), weight='distance') # create numpy array of coordinates representing result path nx_array_path = get_full_path(nx_short_path) # convert numpy array to Shapely Linestring out_shortest_path = asLineString(nx_array_path) write_geojson("../geodata/ch08_final_netx_sh_path.geojson", out_shortest_path.__geo_interface__)
它是如何工作的...
NetworkX 有一个名为read_shp的不错函数,可以直接输入 Shapefile。然而,要开始这样做,我们需要定义write_geojson函数以将我们的结果输出为 GeoJSON 文件。输入的 Shapefile 是一个完全连接的网络数据集。有时,您可能会发现您的输入没有连接,这个函数调用connected_component_subgraphs只使用这些连接的节点来找到节点。内部函数将我们的网络设置为无向。
注意
此函数不会创建一个连接的网络数据集;这项工作留给你在 QGIS 或其他桌面 GIS 软件中执行。一个解决方案是在 PostgreSQL 中使用 pgRouting 扩展提供的工具执行此操作。
现在,我们将生成网络上的节点并将它们导出为 GeoJSON。这当然不是必需的,但看到节点在地图上的位置以调试您的数据是很好的。如果在生成路线时出现任何问题,您可以非常快速地通过视觉识别它们。
接下来,我们设置起始节点和结束节点的数组位置以计算我们的路线。NetworkX 的shortest_path算法要求您定义源节点和目标节点。
小贴士
需要注意的一件事是,源和目标是在点数组内的坐标对。
尽管这个点数组很棒,但我们需要一个路径,因此接下来将讨论get_path和get_full_path函数。我们的get_path函数接受两个输入节点,即两个坐标对,并返回沿线的边坐标的 NumPy 数组。紧接着是get_full_path函数,它内部使用get_path函数输出所有路径和所有路径上的坐标的完整列表。
所有边和相应的坐标随后被追加到一个新的列表中,需要将其合并——因此,使用 NumPy 的 vstack 函数。在我们的 for 循环内部,我们遍历每条路径,获取边和坐标来构建我们的列表,然后将其连接在一起作为我们的最终 NumPy 数组输出。
Shapely 是与 NumPy 兼容构建的,因此有一个 asLineString() 函数可以直接输入坐标的 NumPy 数组。现在我们有了最终 LineString 路线的几何形状,可以使用我们的函数将其导出为 GeoJSON。

基于室内最短路径生成疏散多边形
例如,建筑师和交通规划师需要根据各种标准和安全政策来规划建筑所需的出入口位置和数量。一旦建筑建成,设施经理和安全团队通常无法获取这些信息。想象一下,你正在计划一个活动,并想查看在特定时间内可以疏散哪些区域,这些区域受建筑中出入口列表的限制。
在这个练习中,我们想在大型建筑内部的一个特定起点创建一些多边形,显示在 10、20、30 和 60 秒间隔内可以疏散哪些区域。我们假设人们以 5 公里/小时或 1.39 米/秒的速度行走,这是他们的正常行走速度。如果我们恐慌并奔跑,我们的正常奔跑速度将增加到 6.7 米/秒或 24.12 公里/小时。
我们的结果将生成一组多边形,代表基于建筑走廊的疏散区域。我们需要定义疏散开始的起始位置。我们计算的起始点等于之前配方中讨论的路线的起始点,使用纯 Python 中的 NetworkX 找到 Dijkstra 最短路径。

此图像显示了使用我们的脚本生成的结果多边形和点。结果使用 QGIS 进行了样式化和可视化。
准备工作
此示例使用我们之前配方中加载的网络数据,所以请确保你已经将此数据加载到你的本地 PostgreSQL 数据库中。在数据加载完成后,你将有两个表,geodata.ch08_e01_networklines_vertices_pgr 和 geodata.ch08_e01_networklines。结合这些表,你需要一个单独的新 Shapefile,用于我们的输入多边形,位于 /ch08/geodata/shp/e01_hallways_union_3857.shp,代表用于裁剪我们结果距离多边形的建筑走廊。
如何实现...
-
代码中有许多注释,用于提高清晰度,所以请阅读:
#!/usr/bin/env python # -*- coding: utf-8 -*- import psycopg2 import shapefile import json import shapely.geometry as geometry from geojson import loads, Feature, FeatureCollection from shapely.geometry import asShape # database connection db_host = "localhost" db_user = "pluto" db_passwd = "secret" db_database = "py_geoan_cb" db_port = "5432" # connect to DB conn = psycopg2.connect(host=db_host, user=db_user, port=db_port, password=db_passwd, database=db_database) cur = conn.cursor() def write_geojson(outfilename, indata): with open(outfilename, "w") as geojs_out: geojs_out.write(json.dumps(indata)) # center point for creating our distance polygons x_start_coord = 1587926.769 y_start_coord = 5879726.492 # query including two variables for the x, y POINT coordinate start_node_query = """ SELECT id FROM geodata.ch08_e01_networklines_vertices_pgr AS p WHERE ST_DWithin(the_geom, ST_GeomFromText('POINT({0} {1})',3857),1); """.format(x_start_coord, y_start_coord) # get the start node id as an integer # pass the variables cur.execute(start_node_query) start_node_id = int(cur.fetchone()[0]) combined_result = [] hallways = shapefile.Reader("../geodata/shp/e01_hallways_union_3857.shp") e01_hallway_features = hallways.shape() e01_hallway_shply = asShape(e01_hallway_features) # time in seconds evac_times = [10, 20, 30, 60] def generate_evac_polys(start_node_id, evac_times ): """ :param start_node_id: network node id to start from :param evac_times: list of times in seconds :return: none, generates GeoJSON files """ for evac_time in evac_times: distance_poly_query = """ SELECT seq, id1 AS node, cost, ST_AsGeoJSON(the_geom) FROM pgr_drivingDistance( 'SELECT ogc_fid AS id, source, target, ST_Length(wkb_geometry)/5000*60*60 AS cost FROM geodata.ch08_e01_networklines', {0}, {1}, false, false ) as ev_dist JOIN geodata.ch08_e01_networklines_vertices_pgr AS networklines ON ev_dist.id1 = networklines.id; """.format(start_node_id, evac_time) cur.execute(distance_poly_query) # get entire query results to work with distance_nodes = cur.fetchall() # empty list to hold each segment for our GeoJSON output route_results = [] # loop over each segment in the result route segments # create the list of our new GeoJSON for dist_node in distance_nodes: sequence = dist_node[0] # sequence number node = dist_node[1] # node id cost = dist_node[2] # cost value geojs = dist_node[3] # geometry geojs_geom = loads(geojs) # create geojson geom geojs_feat = Feature(geometry=geojs_geom, properties={'sequence_num': sequence, 'node':node, 'evac_time_sec':cost, 'evac_code': evac_time}) # add each point to total including all points combined_result.append(geojs_feat) # add each point for individual evacuation time route_results.append(geojs_geom) # geojson module creates GeoJSON Feature Collection geojs_fc = FeatureCollection(route_results) # create list of points for each evac time evac_time_pts = [asShape(route_segment) for route_segment in route_results] # create MultiPoint from our list of points for evac time point_collection = geometry.MultiPoint(list(evac_time_pts)) # create our convex hull polyon around evac time points convex_hull_polygon = point_collection.convex_hull # intersect convex hull with hallways polygon (ch = convex hull) cvex_hull_intersect = e01_hallway_shply.intersection(convex_hull_polygon) # export convex hull intersection to geojson cvex_hull = cvex_hull_intersect.__geo_interface__ # for each evac time we create a unique GeoJSON polygon output_ply = "../geodata/ch08-03_dist_poly_" + str(evac_time) + ".geojson" write_geojson(output_ply, cvex_hull) output_geojson_route = "../geodata/ch08-03_dist_pts_" + str(evac_time) + ".geojson" # save GeoJSON to a file in our geodata folder write_geojson(output_geojson_route, geojs_fc ) # create or set of evac GeoJSON polygons based # on location and list of times in seconds generate_evac_polys(start_node_id, evac_times) # final result GeoJSON final_res = FeatureCollection(combined_result) # write to disk write_geojson("../geodata/ch08-03_final_dist_poly.geojson", final_res) # clean up and close database cursor and connection cur.close() conn.close()
它是如何工作的...
代码从数据库模板代码和一个用于导出 GeoJSON 结果文件的函数开始。为了创建疏散多边形,我们需要一个输入,即我们网络中距离计算多边形的起始点。如前文所述,我们需要找到网络中距离我们的起始坐标最近的节点。因此,我们运行一个 SQL select查询来找到这个距离我们的坐标一米的节点。
接下来,我们定义一个combined_result变量,它将存储我们列表中所有指定疏散时间的可到达点。因此,它将每个疏散时间的每个结果存储在一个单独的输出中。
由于我们需要将输出多边形裁剪到走廊内,因此走廊的 Shapefile 被准备为 Shapely 几何形状。我们只对在指定的 10 秒、20 秒、30 秒和 60 秒时间尺度内可以疏散的区域感兴趣。如果区域在走廊之外,你位于建筑之外,换句话说,你很安全。
现在,我们将遍历我们的每个时间间隔,为列表中定义的每个时间创建单独的疏散多边形。pgRouting扩展包括一个名为pgr_drivingDistance()的函数,该函数返回一个列表,其中包含在指定成本内可到达的节点。此函数的参数包括返回id、source、target和cost列的SQL 查询。我们的最后四个参数包括表示start_node_id的%s变量,等于start_node_id。然后是存储在evac_time变量中的疏散时间(以秒为单位),后面跟着两个 false 值。这两个最后的 false 值用于有向路线或反向成本计算,我们不需要使用。
注意
在我们的案例中,成本是根据距离计算的时间值(以秒为单位)。我们假设你以 5 公里/小时的速度行走。成本计算为段长度(以米为单位)除以 5000 米乘以 60 分钟乘以 60 秒,以得出成本值。然后,我们传入起始节点 ID 以及我们指定的疏散时间(以秒为单位)。如果你想按分钟计算,只需从方程中移除一个乘以 60 即可。
每个节点的几何形状是通过在顶点表和具有节点 ID 的节点结果列表之间进行 SQL JOIN 操作来推导的。现在我们已经得到了每个节点在疏散时间内可到达的点的几何形状集合,是时候解析这个结果了。解析是创建我们的 GeoJSON 输出所必需的,它也将点输入到我们的组合输出中,即combined_result变量,以及使用 Shapely 中的凸包算法创建的个体疏散时间多边形。
提示
使用 alpha 形状可以创建更好的或更逼真的多边形。Alpha 形状从一组点形成多边形,紧贴每个点以保留更逼真的多边形,该多边形遵循点的形状。凸包只是确保所有点都在结果多边形内。要了解 alpha 形状,请查看肖恩·吉利斯在 sgillies.net/blog/1155/the-fading-shape-of-alpha/ 的这篇帖子,以及 blog.thehumangeo.com/2014/05/12/drawing-boundaries-in-python/ 的这篇帖子。
代码中包含的模块是名为 //ch08/code/alpha_shape.py 的 alpha 形状模块,如果您已经跟随教程进行,可以使用创建的输入数据点尝试,以创建一个更精确的多边形。
我们的 route_results 变量存储了用于创建单个凸包多边形的 GeoJSON 几何形状。然后,该变量用于填充每个疏散点集的点列表。它还提供了我们的 GeoJSON 导出的来源,创建 FeatureCollection。
最终的计算包括使用 Shapely 创建凸包多边形,紧接着与代表建筑走廊的输入 Shapefile 中的新凸包多边形相交。我们只对显示疏散区域感兴趣,这归结为仅显示建筑内部的区域,因此进行交集操作。
剩余的代码将我们的结果导出到 /ch08/geodata 文件夹中的 GeoJSON 文件。请打开此文件夹,并将 GeoJSON 文件拖放到 QGIS 中以可视化您的新结果。您需要获取以下文件:
-
ch08-03_dist_poly_10.geojson -
ch08-03_dist_poly_20.geojson -
ch08-03_dist_poly_30.geojson -
ch08-03_dist_poly_60.geojson -
ch08-03_final_dis_poly.geojson
从多边形创建中心线
对于任何路由算法要正常工作,我们需要一组网络 LineStrings 来执行最短路径查询。在这里,您当然有一些选择,您可以将这些选择下载到 OSM 数据中清理道路。其次,您可以数字化自己的网络线集合,或者第三,您可以尝试自动生成这些线。
生成此网络 LineString 的过程至关重要,它决定了我们可以生成的路线的质量和类型。在室内环境中,我们没有道路和街道名称;相反,我们有走廊、房间、休息室、电梯、坡道和楼梯。这些特征是我们的道路、桥梁和高速公路隐喻,我们希望为行人创建路线。
我们将向您展示如何从代表走廊的多边形创建基本的网络 LineStrings。

准备工作
这个练习要求我们以某种形式在数字上有一个计划,其中多边形表示走廊和其他人们可以行走的空间。我们的走廊多边形由奥地利克拉根福特市的 Alpen-Adria-Universität 提供的。多边形被简化以降低渲染时间。你的输入几何形状越复杂,处理所需的时间就越长。
我们正在使用 scipy、shapely 和 numpy 库,所以如果你还没有这样做,请阅读第一章,设置你的地理空间 Python 环境。在 /ch08/code/ 文件夹中,你可以找到包含 Centerline 类的 centerline.py 模块。这个模块包含了生成中心线的实际代码,并且被 ch08/code/ch08-04_centerline.py 模块导入。
如何做到这一点...
让我们深入一些代码:
注意
如果你决定立即运行以下代码,请注意,生成中心线是一个缓慢的过程,并且没有针对性能进行优化。在慢速机器上,这段代码可能需要运行 5 分钟,所以请耐心等待,并关注控制台,直到它显示 完成。
-
第一个任务是创建一个创建我们中心线的函数。这是 Filip Todic 原始
centerlines.py类的修改版本:#!/usr/bin/env python # -*- coding: utf-8 -*- from shapely.geometry import LineString from shapely.geometry import MultiLineString from scipy.spatial import Voronoi import numpy as np class Centerline(object): def __init__(self, inputGEOM, dist=0.5): self.inputGEOM = inputGEOM self.dist = abs(dist) def create_centerline(self): """ Calculates the centerline of a polygon. Densifies the border of a polygon which is then represented by a Numpy array of points necessary for creating the Voronoi diagram. Once the diagram is created, the ridges located within the polygon are joined and returned. Returns: a MultiLinestring located within the polygon. """ minx = int(min(self.inputGEOM.envelope.exterior.xy[0])) miny = int(min(self.inputGEOM.envelope.exterior.xy[1])) border = np.array(self.densify_border(self.inputGEOM, minx, miny)) vor = Voronoi(border) vertex = vor.vertices lst_lines = [] for j, ridge in enumerate(vor.ridge_vertices): if -1 not in ridge: line = LineString([ (vertex[ridge[0]][0] + minx, vertex[ridge[0]][1] + miny), (vertex[ridge[1]][0] + minx, vertex[ridge[1]][1] + miny)]) if line.within(self.inputGEOM) and len(line.coords[0]) > 1: lst_lines.append(line) return MultiLineString(lst_lines) def densify_border(self, polygon, minx, miny): """ Densifies the border of a polygon by a given factor (by default: 0.5). The function tests the complexity of the polygons geometry, i.e. does the polygon have holes or not. If the polygon doesn't have any holes, its exterior is extracted and densified by a given factor. If the polygon has holes, the boundary of each hole as well as its exterior is extracted and densified by a given factor. Returns: a list of points where each point is represented by a list of its reduced coordinates. Example: [[X1, Y1], [X2, Y2], ..., [Xn, Yn] """ if len(polygon.interiors) == 0: exterior_line = LineString(polygon.exterior) points = self.fixed_interpolation(exterior_line, minx, miny) else: exterior_line = LineString(polygon.exterior) points = self.fixed_interpolation(exterior_line, minx, miny) for j in range(len(polygon.interiors)): interior_line = LineString(polygon.interiors[j]) points += self.fixed_interpolation(interior_line, minx, miny) return points def fixed_interpolation(self, line, minx, miny): """ A helping function which is used in densifying the border of a polygon. It places points on the border at the specified distance. By default the distance is 0.5 (meters) which means that the first point will be placed 0.5 m from the starting point, the second point will be placed at the distance of 1.0 m from the first point, etc. Naturally, the loop breaks when the summarized distance exceeds the length of the line. Returns: a list of points where each point is represented by a list of its reduced coordinates. Example: [[X1, Y1], [X2, Y2], ..., [Xn, Yn] """ count = self.dist newline = [] startpoint = [line.xy[0][0] - minx, line.xy[1][0] - miny] endpoint = [line.xy[0][-1] - minx, line.xy[1][-1] - miny] newline.append(startpoint) while count < line.length: point = line.interpolate(count) newline.append([point.x - minx, point.y - miny]) count += self.dist newline.append(endpoint) return newline -
现在我们有一个创建中心线的函数,我们需要一些代码来导入 Shapefile 多边形,运行中心线脚本,并将我们的结果导出为 GeoJSON,以便我们可以在 QGIS 中查看:
#!/usr/bin/env python # -*- coding: utf-8 -*- import json import shapefile from shapely.geometry import asShape, mapping from centerline import Centerline def write_geojson(outfilename, indata): with open(outfilename, "w") as file_out: file_out.write(json.dumps(indata)) def create_shapes(shapefile_path): ''' Create our Polygon :param shapefile_path: full path to shapefile :return: list of Shapely geometries ''' in_ply = shapefile.Reader(shapefile_path) ply_shp = in_ply.shapes() out_multi_ply = [asShape(feature) for feature in ply_shp] print "converting to MultiPolygon: " return out_multi_ply def generate_centerlines(polygon_shps): ''' Create centerlines :param polygon_shps: input polygons :return: dictionary of linestrings ''' dct_centerlines = {} for i, geom in enumerate(polygon_shps): print " now running Centerline creation" center_obj = Centerline(geom, 0.5) center_line_shply_line = center_obj.create_centerline() dct_centerlines[i] = center_line_shply_line return dct_centerlines def export_center(geojs_file, centerlines): ''' Write output to GeoJSON file :param centerlines: input dictionary of linestrings :return: write to GeoJSON file ''' with open(geojs_file, 'w') as out: for i, key in enumerate(centerlines): geom = centerlines[key] newline = {'id': key, 'geometry': mapping(geom), 'properties': {'id': key}} out.write(json.dumps(newline)) if __name__ == '__main__': input_hallways = "../geodata/shp/e01_hallways_small_3857.shp" # run our function to create Shapely geometries shply_ply_halls = create_shapes(input_hallways) # create our centerlines res_centerlines = generate_centerlines(shply_ply_halls) print "now creating centerlines geojson" # define output file name and location outgeojs_file = '../geodata/04_centerline_results_final.geojson' # write the output GeoJSON file to disk export_center(outgeojs_file, res_centerlines)
它是如何工作的...
从包含 Centerline 类的 centerlines.py 开始,类内部有很多操作。我们使用 Voronoi 多边形并从中提取 脊 作为中心线。为了创建这些 Voronoi 多边形,我们需要将我们的多边形转换为表示内部和外部多边形边的 LineStrings。然后,这些边需要被转换为点以供 Voronoi 算法使用。这些点基于一个 densify 算法生成,该算法在多边形边缘每隔 0.5 米创建一个点,并围绕整个多边形。这有助于 Voronoi 函数创建多边形更精确的表示,从而提供更好的中心线。不利的一面是,这个距离设置得越高,所需的计算能力就越多。
然后,ch08-04_centerline.py 代码导入这个新的 Centerline 类,并使用我们的走廊多边形实际运行它。输入的多边形是通过 pyshp 从 Shapefile 中读取的。然后,我们生成的形状被泵入 generate_centerlines 函数,输出一个表示我们中心线的 LineStrings 字典。
然后,在遍历中心线时,我们将输出字典导出为 GeoJSON,使用标准的 json.dumps 函数将其导出到我们的文件中。
在 3D 中构建室内路由系统
如何通过一个或多个建筑或楼层进行路由是本食谱的主要内容。这当然是最复杂的情况,涉及复杂的数据收集、准备和实施过程。例如,我们无法深入所有复杂的数据细节,从 ACAD 到 PostGIS 的转换;相反,提供的是完成后的数据。
要创建室内路由应用程序,您需要一个已经数字化的路由网络线集,表示人们可以行走的地方。我们的数据代表了一所大学的第一层和第二层。以下截图显示的结果室内路线从第二层开始,沿着楼梯下到第一层,穿过整个建筑,再次上楼梯到第二层,最终到达我们的目的地。

准备工作
对于这个食谱,我们需要完成相当多的任务来准备室内 3D 路由。以下是一个快速的需求列表:
-
第一层的 Shapefile(
/ch08/geodata/shp/e01_network_lines_3857.shp)。 -
第二层的 Shapefile(
/ch08/geodata/shp/e02_network_lines_3857.shp)。 -
PostgreSQL DB 9.1 + PostGIS 2.1 和 pgRouting 2.0。这些都在本章开头的使用 pgRouting 找到 Dijkstra 最短路径食谱中安装了。
-
Python 模块,
psycopg2和geojson。
这里是需要我们执行的任务列表:
-
按以下方式导入第一层网络线的 Shapefile(如果已完成了导入此 Shapefile 的早期食谱,则跳过此步骤):
ogr2ogr -a_srs EPSG:3857 -lco "SCHEMA=geodata" -lco "COLUMN_TYPES=type=varchar,type_id=integer" -nlt MULTILINESTRING -nln ch08_e01_networklines -f PostgreSQL "PG:host=localhost port=5432 user=postgres dbname=py_geoan_cb password=air" geodata/shp/e01_network_lines_3857.shp -
按以下方式导入第二层网络线的 Shapefile:
ogr2ogr -a_srs EPSG:3857 -lco "SCHEMA=geodata" -lco "COLUMN_TYPES=type=varchar,type_id=integer" -nlt MULTILINESTRING -nln ch08_e02_networklines -f PostgreSQL "PG:host=localhost port=5432 user=postgres dbname=py_geoan_cb password=air" geodata/shp/e02_network_lines_3857.shp -
为第一层网络线分配路由列(如果已在之前的食谱中完成,则跳过此步骤):
ALTER TABLE geodata.ch08_e01_networklines ADD COLUMN source INTEGER; ALTER TABLE geodata.ch08_e01_networklines ADD COLUMN target INTEGER; ALTER TABLE geodata.ch08_e01_networklines ADD COLUMN cost DOUBLE PRECISION; ALTER TABLE geodata.ch08_e01_networklines ADD COLUMN length DOUBLE PRECISION; UPDATE geodata.ch08_e01_networklines set length = ST_Length(wkb_geometry); -
按以下方式为第二层网络线分配路由列:
ALTER TABLE geodata.ch08_e02_networklines ADD COLUMN source INTEGER; ALTER TABLE geodata.ch08_e02_networklines ADD COLUMN target INTEGER; ALTER TABLE geodata.ch08_e02_networklines ADD COLUMN cost DOUBLE PRECISION; ALTER TABLE geodata.ch08_e02_networklines ADD COLUMN length DOUBLE PRECISION; UPDATE geodata.ch08_e02_networklines set length = ST_Length(wkb_geometry); -
创建允许您在 3D 网络线上进行路由的 pgRouting 3D 函数。这两个 PostgreSQL 函数至关重要,因为它们反映了现在已转换为允许 3D 路由的原始 pgRouting 2D 函数。安装顺序也非常重要,所以请确保首先安装
pgr_pointtoid3d.sql!这两个 SQL 文件都位于您的/ch08/code/文件夹中:psql -U username -d py_geoan_cb -a -f pgr_pointtoid3d.sql -
接下来,安装
pgr_createTopology3d.sql。这是原始版本的修改版,现在使用我们新的pgr_pointtoid3d函数如下:psql -U username -d py_geoan_cb -a -f pgr_createTopology3d.sql -
现在我们需要将我们的两层楼网络线合并成一个单一的 3D LineString 表,我们将在该表上执行 3D 路由。这组 SQL 命令已为您存储在:
psql -U username -d py_geoan_cb -a -f indrz_create_3d_networklines.sql
理解 3D 路由表的精确创建非常重要,因为它允许 3D 路由查询。因此,我们的代码如下列出,带有 SQL 注释描述我们在每个步骤中做什么:
-- if not, go ahead and update
-- make sure tables dont exist
drop table if exists geodata.ch08_e01_networklines_routing;
drop table if exists geodata.ch08_e02_networklines_routing;
-- convert to 3d coordinates with EPSG:3857
SELECT ogc_fid, ST_Force_3d(ST_Transform(ST_Force_2D(st_geometryN(wkb_geometry, 1)),3857)) AS wkb_geometry,
type_id, cost, length, 0 AS source, 0 AS target
INTO geodata.ch08_e01_networklines_routing
FROM geodata.ch08_e01_networklines;
SELECT ogc_fid, ST_Force_3d(ST_Transform(ST_Force_2D(st_geometryN(wkb_geometry, 1)),3857)) AS wkb_geometry,
type_id, cost, length, 0 AS source, 0 AS target
INTO geodata.ch08_e02_networklines_routing
FROM geodata.ch08_e02_networklines;
-- fill the 3rd coordinate according to their floor number
UPDATE geodata.ch08_e01_networklines_routing SET wkb_geometry=ST_Translate(ST_Force_3Dz(wkb_geometry),0,0,1);
UPDATE geodata.ch08_e02_networklines_routing SET wkb_geometry=ST_Translate(ST_Force_3Dz(wkb_geometry),0,0,2);
UPDATE geodata.ch08_e01_networklines_routing SET length =ST_Length(wkb_geometry);
UPDATE geodata.ch08_e02_networklines_routing SET length =ST_Length(wkb_geometry);
-- no cost should be 0 or NULL/empty
UPDATE geodata.ch08_e01_networklines_routing SET cost=1 WHERE cost=0 or cost IS NULL;
UPDATE geodata.ch08_e02_networklines_routing SET cost=1 WHERE cost=0 or cost IS NULL;
-- update unique ids ogc_fid accordingly
UPDATE geodata.ch08_e01_networklines_routing SET ogc_fid=ogc_fid+100000;
UPDATE geodata.ch08_e02_networklines_routing SET ogc_fid=ogc_fid+200000;
-- merge all networkline floors into a single table for routing
DROP TABLE IF EXISTS geodata.networklines_3857;
SELECT * INTO geodata.networklines_3857 FROM
(
(SELECT ogc_fid, wkb_geometry, length, type_id, length*o1.cost as total_cost,
1 as layer FROM geodata.ch08_e01_networklines_routing o1) UNION
(SELECT ogc_fid, wkb_geometry, length, type_id, length*o2.cost as total_cost,
2 as layer FROM geodata.ch08_e02_networklines_routing o2))
as foo ORDER BY ogc_fid;
CREATE INDEX wkb_geometry_gist_index
ON geodata.networklines_3857 USING gist (wkb_geometry);
CREATE INDEX ogc_fid_idx
ON geodata.networklines_3857 USING btree (ogc_fid ASC NULLS LAST);
CREATE INDEX network_layer_idx
ON geodata.networklines_3857
USING hash
(layer);
-- create populate geometry view with info
SELECT Populate_Geometry_Columns('geodata.networklines_3857'::regclass);
-- update stairs, ramps and elevators to match with the next layer
UPDATE geodata.networklines_3857 SET wkb_geometry=ST_AddPoint(wkb_geometry,
ST_EndPoint(ST_Translate(wkb_geometry,0,0,1)))
WHERE type_id=3 OR type_id=5 OR type_id=7;
-- remove the second last point
UPDATE geodata.networklines_3857 SET wkb_geometry=ST_RemovePoint(wkb_geometry,ST_NPoints(wkb_geometry) - 2)
WHERE type_id=3 OR type_id=5 OR type_id=7;
-- add columns source and target
ALTER TABLE geodata.networklines_3857 add column source integer;
ALTER TABLE geodata.networklines_3857 add column target integer;
ALTER TABLE geodata.networklines_3857 OWNER TO postgres;
-- we dont need the temporary tables any more, delete them
DROP TABLE IF EXISTS geodata.ch08_e01_networklines_routing;
DROP TABLE IF EXISTS geodata.ch08_e02_networklines_routing;
-- remove route nodes vertices table if exists
DROP TABLE IF EXISTS geodata.networklines_3857_vertices_pgr;
-- building routing network vertices (fills source and target columns in those new tables)
SELECT public.pgr_createTopology3d('geodata.networklines_3857', 0.0001, 'wkb_geometry', 'ogc_fid');
哇,这需要处理很多东西,现在我们实际上已经准备好运行并创建一些 3D 路由了。太棒了!
如何操作...
-
让我们深入一些带有注释的代码,供你阅读愉快:
#!/usr/bin/env python # -*- coding: utf-8 -*- import psycopg2 import json from geojson import loads, Feature, FeatureCollection db_host = "localhost" db_user = "pluto" db_passwd = "secret" db_database = "py_geoan_cb" db_port = "5432" # connect to DB conn = psycopg2.connect(host=db_host, user=db_user, port=db_port, password=db_passwd, database=db_database) # create a cursor cur = conn.cursor() # define our start and end coordinates in EPSG:3857 # set start and end floor level as integer 0,1,2 for example x_start_coord = 1587848.414 y_start_coord = 5879564.080 start_floor = 2 x_end_coord = 1588005.547 y_end_coord = 5879736.039 end_floor = 2 # find the start node id within 1 meter of the given coordinate # select from correct floor level using 3D Z value # our Z Value is the same as the floor number as an integer # used as input in routing query start point start_node_query = """ SELECT id FROM geodata.networklines_3857_vertices_pgr AS p WHERE ST_DWithin(the_geom, ST_GeomFromText('POINT(%s %s)',3857), 1) AND ST_Z(the_geom) = %s;""" # locate the end node id within 1 meter of the given coordinate end_node_query = """ SELECT id FROM geodata.networklines_3857_vertices_pgr AS p WHERE ST_DWithin(the_geom, ST_GeomFromText('POINT(%s %s)',3857), 1) AND ST_Z(the_geom) = %s;""" # run our query and pass in the 3 variables to the query # make sure the order of variables is the same as the # order in your query cur.execute(start_node_query, (x_start_coord, y_start_coord, start_floor)) start_node_id = int(cur.fetchone()[0]) # get the end node id as an integer cur.execute(end_node_query, (x_end_coord, y_end_coord, end_floor)) end_node_id = int(cur.fetchone()[0]) # pgRouting query to return our list of segments representing # our shortest path Dijkstra results as GeoJSON # query returns the shortest path between our start and end nodes above # in 3D traversing floor levels and passing in the layer value = floor routing_query = ''' SELECT seq, id1 AS node, id2 AS edge, ST_Length(wkb_geometry) AS cost, layer, ST_AsGeoJSON(wkb_geometry) AS geoj FROM pgr_dijkstra( 'SELECT ogc_fid as id, source, target, st_length(wkb_geometry) AS cost, layer FROM geodata.networklines_3857', %s, %s, FALSE, FALSE ) AS dij_route JOIN geodata.networklines_3857 AS input_network ON dij_route.id2 = input_network.ogc_fid ; ''' # run our shortest path query cur.execute(routing_query, (start_node_id, end_node_id)) # get entire query results to work with route_segments = cur.fetchall() # empty list to hold each segment for our GeoJSON output route_result = [] # loop over each segment in the result route segments # create the list of our new GeoJSON for segment in route_segments: print segment seg_cost = segment[3] # cost value layer_level = segment[4] # floor number geojs = segment[5] # geojson coordinates geojs_geom = loads(geojs) # load string to geom geojs_feat = Feature(geometry=geojs_geom, properties={'floor': layer_level, 'cost': seg_cost}) route_result.append(geojs_feat) # using the geojson module to create our GeoJSON Feature Collection geojs_fc = FeatureCollection(route_result) # define the output folder and GeoJSON file name output_geojson_route = "../geodata/ch08_indoor_3d_route.geojson" # save geojson to a file in our geodata folder def write_geojson(): with open(output_geojson_route, "w") as geojs_out: geojs_out.write(json.dumps(geojs_fc)) # run the write function to actually create the GeoJSON file write_geojson() # clean up and close database curson and connection cur.close() conn.close()
它是如何工作的...
使用psycopg2模块,我们可以连接到数据库中我们新奇的表格并运行一些查询。第一个查询集基于x、y和Z海拔值找到起始和结束节点。Z值非常重要;否则,可能会选择错误的节点。Z值与层/楼层值一一对应。分配给我们的networklines_3857数据集的 3D 海拔数据对于一楼是简单的一米,对于二楼是两米。这样可以使事情简单且易于记忆,而不必实际使用楼层的高度,当然,如果你想的话,你也可以这样做。
由于我们的数据现在已经是 3D 的,我们的 3D 路由能够像任何其他正常的 2D 路由查询一样运行,这要归功于我们新增的两个 pgRouting 函数。查询通过,选择我们的数据,并返回一个漂亮的 GeoJSON 字符串。
你之前已经见过这段剩余的代码了。它将结果导出到磁盘上的 GeoJSON 文件中,这样你就可以在 QGIS 中打开它进行查看。我们已经成功地向新的 GeoJSON 文件添加了一些属性,包括楼层号、以距离为单位的成本以及识别一个段是室内路径还是楼梯形式的路径段类型。
计算室内路线步行时间
如果不让我们知道到达室内步行路径需要多长时间,我们的室内路由应用程序就不会完整,对吧?我们将创建几个小的函数,你可以将它们插入到前面的食谱中的代码中,以打印出路线步行时间。
如何实现...
-
不再拖延,让我们看看一些代码:
#!/usr/bin/env python # -*- coding: utf-8 -*- def format_walk_time(walk_time): """ takes argument: float walkTime in seconds returns argument: string time "xx minutes xx seconds" """ if walk_time > 0.0: return str(int(walk_time / 60.0)) + " minutes " + str(int(round(walk_time % 60))) + " seconds" else: return "Walk time is less than zero! Something is wrong" def calc_distance_walktime(rows): """ calculates distance and walk_time. rows must be an array of linestrings --> a route, retrieved from the DB. rows[5]: type of line (stairs, elevator, etc) rows[3]: cost as length of segment returns a dict with key/value pairs route_length, walk_time """ route_length = 0 walk_time = 0 for row in rows: route_length += row[3] #calculate walk time if row[5] == 3 or row[5] == 4: # stairs walk_speed = 1.2 # meters per second m/s elif row[5] == 5 or row[5] == 6: # elevator walk_speed = 1.1 # m/s else: walk_speed = 1.39 # m/s walk_time += (row[3] / walk_speed) length_format = "%.2f" % route_length real_time = format_walk_time(walk_time) print {"route_length": length_format, "walk_time": real_time} -
你的结果应该显示如下字典:
{'walk_time': '4 minutes 49 seconds', 'route_length': '397.19'}这里假设你已经将这些函数放入了前面的食谱中,并调用了函数将结果打印到控制台。
它是如何工作的...
我们有两个简单的函数来为我们的室内路线创建步行时间。第一个函数,称为format_walk_time(),简单地将结果时间转换为人类友好的形式,分别显示所需的分钟和秒。
第二个函数calc_distance_walktime()执行工作,期望一个包含距离的列表对象。然后,这个距离被加到每个路线段中,形成一个存储在route_length变量中的总距离值。然后,我们通过调用format_walk_time函数创建real_time变量,该函数传递秒数的walk_time值。
现在,你为你的应用程序拥有了一个复杂的室内路线,并指定了步行时间。
第九章. 拓扑检查和数据验证
本章将涵盖以下主题:
-
创建规则 - 多边形内只有一个点
-
一个点必须位于线的起始和结束节点上
-
线字符串不得重叠
-
线字符串不得有悬垂线
-
多边形质心必须位于线的一定距离内
介绍
拓扑规则允许你强制执行和测试不同几何集之间的空间关系。本章将构建一个开源的拓扑规则集,你可以从命令行运行或将其集成到你的 Python 程序中。
DE-9IM(九相交模型)描述的空间关系包括等于、不相交、相交、接触、交叉、包含、包含于和重叠。然而,这些关系的确切联系对于大多数初学者来说并不明确。我们指的是我们的几何类型(点、线字符串和多边形)的内部、边界和外部,这些类型直接用于执行拓扑检查。具体如下:
-
内部:这指的是整个形状除了其边界之外的部分。所有几何类型都有内部。
-
边界:这指的是线特征所有线性部分的端点或多边形的线性轮廓。只有线和多边形有边界。
-
外部:这指的是形状的外部区域。所有几何类型都有外部。介绍
下表以更正式的措辞总结了拓扑几何:
| 几何子类型 | 内部(I) | 边界(B) | 外部(E) |
|---|---|---|---|
| 点、多点 | 点或多个点 | 空集 | 不在内部或边界内的点 |
| 线字符串、线 | 移除边界点后留下的点 | 两个端点 | 不在内部或边界内的点 |
| 线性环 | 线性环上的所有点 | 空集 | 不在内部或边界内的点 |
| 多线字符串 | 移除边界点后留下的点 | 其元素曲线边界中的奇数个点 | 不在内部或边界内的点 |
| 多边形 | 环内的点 | 环集 | 不在内部或边界内的点 |
| 多多边形 | 环内的点 | 其多边形的环集 | 不在内部或边界内的点 |
主几何类型(如多边形、边界和外部)的定义由开放地理空间联盟(OGC)描述。
在下面的菜谱中,我们将探讨一些可以应用于任何项目的自定义拓扑规则,为你创建自己的规则集打下基础。
创建规则 - 多边形内只有一个点
在 GIS 历史很久以前,多边形内不出现多于一个点非常重要,因为一个多边形一个点是展示具有相关属性和 ID 的拓扑干净多边形的标准方式。今天,它对于许多其他原因仍然很重要,例如根据多边形内的点分配属性。我们必须在多边形和点之间执行空间连接来分配这些宝贵的属性。如果两个点位于一个多边形内,你将使用哪些属性?这个配方是关于创建一个规则来检查你的数据,以确保每个多边形内只有一个点。如果这个测试失败,你将得到一个错误列表;如果它通过,测试将返回True。

准备中
数据在这里再次扮演着核心角色,所以请检查你的/ch09/geodata/文件夹是否已准备好,包含两个包含topo_polys.shp和topo_points.shp的输入 Shapefiles。Shapely 库执行几何拓扑测试。如果你到目前为止一直跟着做,那么你已经安装了它;如果没有,现在通过参考第一章,设置你的地理空间 Python 环境来安装它。
如何做...
-
你现在将检查每个多边形是否包含一个点,方法如下:
#!/usr/bin/env python # -*- coding: utf-8 -*- # # for every polygon in a polygon layer there can only be # one point object located in each polygon # the number of points per polygon can be defined by the user from utils import shp2_geojson_obj from utils import create_shply_multigeom import json in_shp_poly = "../geodata/topo_polys.shp" in_shp_point = "../geodata/topo_points.shp" ply_geojs_obj = shp2_geojson_obj(in_shp_poly) pt_geojs_obj = shp2_geojson_obj(in_shp_point) shply_polys = create_shply_multigeom(ply_geojs_obj, "MultiPolygon") shply_points = create_shply_multigeom(pt_geojs_obj, "MultiPoint") def valid_point_in_poly(polys, points): """ Determine if every polygon contains max one point and that each point is not located on the EDGE or Vertex of the polygon :param point: Point data set :param poly: Polygon data set :return: True or False if False a dictionary containing polygon ids that contain no or multiple points """ pts_in_polys = [] pts_touch_plys = [] pts_plys_geom = [] pts_touch_geom = [] # check each polygon for number of points inside for i, poly in enumerate(polys): pts_in_this_ply = [] pts_touch_this_ply = [] for pt in points: if poly.touches(pt): pts_touch_this_ply.append( {'multipoint_errors_touches': pt.__geo_interface__, 'poly_id': i, 'point_coord': pt.__geo_interface__}) if poly.contains(pt): pts_in_this_ply.append({'multipoint_contains': pt.__geo_interface__}) pts_in_polys.append(len(pts_in_this_ply)) pts_touch_plys.append(len(pts_touch_this_ply)) # create list of point geometry errors pts_plys_geom.append(pts_in_this_ply) pts_touch_geom.append(pts_touch_this_ply) # identify if we have more than one point per polygon or # identify if no points are inside a polygon no_good = dict() all_good = True # loop over list containing the number of pts per polygon # each item in list is an integer representing the number # of points located inside a particular polygon [4,1,0] # represents 4 points in polygon 1, 1 point in poly 2, and # 0 points in polygon 3 for num, res in enumerate(pts_in_polys): if res == 1: # this polygon is good and only has one point inside # no points on the edge or on the vertex of polygon continue # no_good['poly num ' + str(num)] = "excellen only 1 point in poly" elif res > 1: # we have more than one point either inside, on edge # or vertex of a polygon no_good['poly num ' + str(num)] = str(res) + " points in this poly" all_good = False else: # last case no points in this polygon no_good['poly num ' + str(num)] = "No points in this poly" all_good = False if all_good: return all_good else: bad_list = [] for pt in pts_plys_geom: fgeom = {} for res in pt: if 'multipoint_contains' in res: hui = res['multipoint_contains'] print hui fgeom['geom'] = hui bad_list.append(fgeom) return bad_list # return no_good,pts_in_polys2 # [4,0,1] valid_res = valid_point_in_poly(shply_polys, shply_points) final_list = [] for res in valid_res: if 'geom' in res: geom = res['geom'] final_list.append(geom) final_gj = {"type": "GeometryCollection", "geometries": final_list} print json.dumps(final_gj) -
这结束了使用两个输入 Shapefiles 的实践测试。现在为了你的测试乐趣,这里有一个简单的单元测试,用于分解简单的点在多边形中的测试。以下测试代码位于
ch09/code/ch09-01_single_pt_test_in_poly.py文件中:# -*- coding: utf-8 -*- import unittest from shapely.geometry import Point from shapely.geometry import Polygon class TestPointPerPolygon(unittest.TestCase): def test_inside(self): ext = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] int = [(1, 1), (1, 1.5), (1.5, 1.5), (1.5, 1)] poly_with_hole = Polygon(ext,[int]) polygon = Polygon([(0, 0), (0, 10), (10, 10),(0, 10)]) point_on_edge = Point(5, 10) point_on_vertex = Point(10, 10) point_inside = Point(5, 5) point_outside = Point(20,20) point_in_hole = Point(1.25, 1.25) self.assertTrue(polygon.touches(point_on_vertex)) self.assertTrue(polygon.touches(point_on_edge)) self.assertTrue(polygon.contains(point_inside)) self.assertFalse(polygon.contains(point_outside)) self.assertFalse(point_in_hole.within(poly_with_hole)) if __name__ == '__main__': unittest.main()这个简单的测试应该能正常运行。如果你想要破坏它来查看会发生什么,请将最后的调用更改为以下内容:
self.assertTrue(point_in_hole.within(poly_with_hole) -
这将产生以下输出:
Failure Traceback (most recent call last): File "/home/mdiener/ch09/code/ch09-01_single_pt_test_in_poly.py", line 26, in test_inside self.assertTrue(point_in_hole.within(poly_with_hole)) AssertionError: False is not true
它是如何工作的...
我们有很多事情要测试,以确定多边形内是否只有一个点。我们将从定义的内部和外部开始。回顾本章的介绍,多边形的内部、外部和边界可以逻辑地定义。然后,我们明确地定义输入点的位置,即位于多边形内部且不在多边形边界、边或顶点上的点。此外,我们添加的准则是一个多边形只允许一个点,因此如果有0个或更多点位于任何给定的多边形内,将会产生错误。
我们的空间谓词包括接触以确定点是否在顶点或边上。如果接触返回True,则我们的点位于边或顶点上,这意味着它不在内部。这之后是contains方法,它检查点是否在我们的多边形内。在这里,我们检查多边形内是否没有超过一个点。
代码通过导入和转换 Shapefile 来处理 Shapely 模块执行的处理。当我们处理多边形时,我们创建几个列表来跟踪它们之间发现的关系类型,以便我们可以在最后将它们加起来,这样我们就可以计算是否有一个或多个点在单个多边形内部。
我们最后的代码段运行一系列简单的函数调用,测试点是否在多边形内部或外部的几个场景。最后的调用通过具有多个多边形和点的 Shapefiles 进行更现实的测试。这会返回 True 如果没有发现错误,或者返回一个 GeoJSON 打印输出,显示错误的位置。
一个点必须只位于线的起始和结束节点上
一个由连接边组成的路由网络可能包含一些与表示为点的道路交点相关的路由逻辑。当然,这些点必须精确地位于线的起始或结束位置,以便识别这些交叉口。一旦找到交叉口,可以在属性中应用各种规则来控制你的路由,例如。
一个典型的例子是将转弯限制建模为点:

如何做...
我们方便的 utils.py 模块位于 trunk 文件夹中,它帮助我们处理一些日常任务,例如导入 Shapefile 并将其转换为 Shapely 几何对象,以便我们进行处理。
-
现在让我们创建点检查代码如下:
#!/usr/bin/env python # -*- coding: utf-8 -*- from utils import shp2_geojson_obj from utils import create_shply_multigeom from utils import out_geoj from shapely.geometry import Point, MultiPoint in_shp_line = "../geodata/topo_line.shp" in_shp_point = "../geodata/topo_points.shp" # create our geojson like object from a Shapefile shp1_data = shp2_geojson_obj(in_shp_line) shp2_data = shp2_geojson_obj(in_shp_point) # convert the geojson like object to shapely geometry shp1_lines = create_shply_multigeom(shp1_data, "MultiLineString") shp2_points = create_shply_multigeom(shp2_data, "MultiPoint") def create_start_end_pts(lines): ''' Generate a list of all start annd end nodes :param lines: a Shapely geometry LineString :return: Shapely multipoint object which includes all the start and end nodes ''' list_end_nodes = [] list_start_nodes = [] for line in lines: coords = list(line.coords) line_start_point = Point(coords[0]) line_end_point = Point(coords[-1]) list_start_nodes.append(line_start_point) list_end_nodes.append(line_end_point) all_nodes = list_end_nodes + list_start_nodes return MultiPoint(all_nodes) def check_points_cover_start_end(points, lines): ''' :param points: Shapely point geometries :param lines:Shapely linestrings :return: ''' all_start_end_nodes = create_start_end_pts(lines) bad_points = [] good_points = [] if len(points) > 1: for pt in points: if pt.touches(all_start_end_nodes): print "touches" if pt.disjoint(all_start_end_nodes): print "disjoint" # 2 nodes bad_points.append(pt) if pt.equals(all_start_end_nodes): print "equals" if pt.within(all_start_end_nodes): print "within" # all our nodes on start or end if pt.intersects(all_start_end_nodes): print "intersects" good_points.append(pt) else: if points.intersects(all_start_end_nodes): print "intersects" good_points.append(points) if points.disjoint(all_start_end_nodes): print "disjoint" good_points.append(points) if len(bad_points) > 1: print "oh no 1 or more points are NOT on a start or end node" out_geoj(bad_points, '../geodata/points_bad.geojson') out_geoj(good_points, '../geodata/points_good.geojson') elif len(bad_points) == 1: print "oh no your input single point is NOT on start or end node" else: print "super all points are located on a start or end node" \ "NOTE point duplicates are NOT checked" check_points_cover_start_end(shp2_points, shp1_lines)
它是如何工作的...
你可以用多种不同的方法来解决这个问题。这种方法可能不是非常高效,但它演示了如何解决空间问题。
我们的逻辑从创建一个函数开始,用于找到输入 LineString 的所有真实起始和结束节点位置。Shapely 通过切片帮助我们获取每条线的第一个和最后一个坐标对,从而提供一些简单的列表。然后,这两个集合被合并成一个单独的列表持有者,以便检查所有节点。
第二个函数实际上执行检查,以确定我们的点是否位于主列表中的起始或结束节点。我们首先通过调用第一个函数来创建起始和结束节点的主列表,以便进行比较。现在,如果我们的输入有多个点,我们将遍历每个点并检查几个空间关系。其中只有两个真正有趣,那就是不相交和相交。这些通过显示哪些点是好的,哪些不是,来给出我们的答案。
注意
可以使用 within 语句代替 intersect,但之所以没有选择它,仅仅是因为它并不总是被初学者正确理解,而 intersects 似乎更容易理解。
剩余的检查只是将不良和良好点的列表导出到一个 GeoJSON 文件中,您可以在 QGIS 中打开它来可视化。
LineStrings 不能重叠
重叠的线通常很难找到,因为你无法在地图上看到它们。它们可能是故意的,例如,可能重叠的公交线路。这项练习旨在发现这些重叠的线,无论好坏。
以下图显示了两组输入线字符串,你可以清楚地看到它们重叠的地方,但这是一种地图学的视觉检查。我们需要在许多你无法如此清晰地看到的线路上工作。

如何做到这一点...
-
让我们深入代码:
#!/usr/bin/env python # -*- coding: utf-8 -*- from utils import shp2_geojson_obj from utils import create_shply_multigeom from utils import out_geoj in_shp_line = "../geodata/topo_line.shp" in_shp_overlap = "../geodata/topo_line_overlap.shp" shp1_data = shp2_geojson_obj(in_shp_line) shp2_data = shp2_geojson_obj(in_shp_overlap) shp1_lines = create_shply_multigeom(shp1_data, "MultiLineString") shp2_lines_overlap = create_shply_multigeom(shp2_data, "MultiLineString") overlap_found = False for line in shp1_lines: if line.equals(shp2_lines_overlap): print "equals" overlap_found = True if line.within(shp2_lines_overlap): print "within" overlap_found = True # output the overlapping Linestrings if overlap_found: print "now exporting overlaps to GeoJSON" out_int = shp1_lines.intersection(shp2_lines_overlap) out_geoj(out_int, '../geodata/overlapping_lines.geojson') # create final Linestring only list of overlapping lines # uses a pyhton list comprehension expression # only export the linestrings Shapely also creates 2 Points # where the linestrings cross and touch final = [feature for feature in out_int if feature.geom_type == "LineString"] # code if you do not want to use a list comprehension expresion # final = [] # for f in out_int: # if f.geom_type == "LineString": # final.append(f) # export final list of geometries to GeoJSON out_geoj(final, '../geodata/final_overlaps.geojson') else: print "hey no overlapping linestrings"
它是如何工作的...
重叠的线字符串有时是可取的,有时则不然。在这段代码中,你可以做一些简单的调整,并以 GeoJSON 的形式报告这两种情况。默认情况下,输出显示重叠线字符串的 GeoJSON 文件。
我们从将我们的 Shapefiles 转换为 Shapely 几何形状的样板代码开始,这样我们就可以使用我们的空间关系谓词来过滤掉重叠的部分。我们只需要两个谓词等于和包含来找到我们想要的东西。如果我们使用相交,这些可能会返回假阳性,因为crosses()和touches()也被检查了。
小贴士
我们还可以使用与contains()、crosses()、equals()、touches()和within()的 OR 运算等效的intersects谓词,如 Shapely 在线文档toblerity.org/shapely/manual.html#object.intersects中所述。
线字符串不能有悬垂
悬垂就像死胡同(道路)。你只能在一条线结束且不连接到另一段线的情况下找到它们。"悬在空中"指的是不连接到任何其他线字符串的线字符串。如果你想要确保道路网络是连通的,或者要确定街道应该如何汇合的地方,这些非常重要。
悬垂的更技术性的描述可以是这样一个边缘,其一个或两个端点不是另一个边缘端点的附属。

如何做到这一点...
-
你现在将按照以下方式检查你的线字符串集中的悬垂:
#!/usr/bin/env python # -*- coding: utf-8 -*- from utils import shp2_geojson_obj from utils import create_shply_multigeom from utils import out_geoj from shapely.geometry import Point in_shp_dangles = "../geodata/topo_dangles.shp" shp1_data = shp2_geojson_obj(in_shp_dangles) shp1_lines = create_shply_multigeom(shp1_data, "MultiLineString") def find_dangles(lines): """ Locate all dangles :param lines: list of Shapely LineStrings or MultiLineStrings :return: list of dangles """ list_dangles = [] for i, line in enumerate(lines): # each line gets a number # go through each line added first to second # then second to third and so on shply_lines = lines[:i] + lines[i+1:] # 0 is start point and -1 is end point # run through for start_end in [0, -1]: # convert line to point node = Point(line.coords[start_end]) # Return True if any element of the iterable is true. # https://docs.python.org/2/library/functions.html#any # python boolean evaluation comparison if any(node.touches(next_line) for next_line in shply_lines): continue else: list_dangles.append(node) return list_dangles # convert our Shapely MultiLineString to list list_lines = [line for line in shp1_lines] # find those dangles result_dangles = find_dangles(list_lines) # return our results if len(result_dangles) >= 1: print "yes we found some dangles exporting to GeoJSON" out_geoj(result_dangles, '../geodata/dangles.geojson') else: print "no dangles found"
它是如何工作的...
从一开始看,找到悬垂很容易,但实际上这比人们想象的要复杂一些。因此,为了清楚起见,让我们用伪代码解释一些悬垂识别的逻辑。
这些不是悬垂逻辑的一部分:
-
如果两条不同线的起始节点相等,则这不是悬垂
-
如果两条不同线的端节点相等,则这不是悬垂
-
如果一条线的起始节点等于另一条线的结束节点,则这不是悬垂
-
如果一条线的端节点等于另一条线的起始节点,则这不是悬垂
因此,我们需要遍历每个 LineString,并比较一个 LineString 的起始点和结束点与下一个 LineString 的起始点和结束点,使用 Shapely 的 touches() 方法检查它们是否接触。如果它们接触,我们继续下一个比较而不使用 break。它移动到 else 部分,在这里我们将捕获那些漂亮的悬垂线并将其追加到悬垂线列表中。
然后,我们只剩下最后一个有趣的决策:打印出我们没有任何悬垂线的确认,或者将悬垂线导出到 GeoJSON 以供视觉检查。
一个多边形的质心必须位于一条线的一定距离范围内
检查每个多边形的质心是否在到 LineString 的距离容差范围内。这种规则的一个示例用例可能是为路由网络定义从房间质心到最近路由网络线的捕捉容差(以米为单位)。这条线必须位于一定距离内;否则,无法生成路线,例如。以下截图显示了使用一些虚拟多边形和 LineString 的应用,用红色表示位于我们设定的 20000 米容差范围内的质心。这些多边形从威尼斯到维也纳分布得很远:
注意
如果你想要一些算法阅读材料,Paul Bourke 在 paulbourke.net/geometry/pointlineplane/ 提供了一篇不错的阅读材料。

如何去做...
-
此代码现在将自动找到距离容差范围外的质心:
#!/usr/bin/env python # -*- coding: utf-8 -*- from utils import shp2_geojson_obj from utils import create_shply_multigeom from utils import out_geoj in_shp_lines = "../geodata/topo_line.shp" shp1_data = shp2_geojson_obj(in_shp_lines) shp1_lines = create_shply_multigeom(shp1_data, "MultiLineString") in_shp_poly = "../geodata/topo_polys.shp" ply_geojs_obj = shp2_geojson_obj(in_shp_poly) shply_polys = create_shply_multigeom(ply_geojs_obj, "MultiPolygon") # nearest point using linear referencing # with interpolation and project # pt_interpolate = line.interpolate(line.project(point)) # create point centroids from all polygons # measure distance from centroid to nearest line segment def within_tolerance(polygons, lines, tolerance): """ Discover if all polygon centroids are within a distance of a linestring data set, if not print out centroids that fall outside tolerance :param polygons: list of polygons :param lines: list of linestrings :param tolerance: value of distance in meters :return: list of all points within tolerance """ # create our centroids for each polygon list_centroids = [x.centroid for x in polygons] # list to store all of our centroids within tolerance good_points = [] for centroid in list_centroids: for line in lines: # calculate point location on line nearest to centroid pt_interpolate = line.interpolate(line.project(centroid)) # determine distance between 2 cartesian points # that are less than the tolerance value in meters if centroid.distance(pt_interpolate) > tolerance: print "to far " + str(centroid.distance(pt_interpolate)) else: print "hey your in " + str(centroid.distance(pt_interpolate)) good_points.append(centroid) if len(good_points) > 1: return good_points else: print "sorry no centroids found within your tolerance of " + str(tolerance) # run our function to get a list of centroids within tolerance result_points = within_tolerance(shply_polys, shp1_lines, 20000) if result_points: out_geoj(result_points, '../geodata/centroids_within_tolerance.geojson') else: print "sorry cannot export GeoJSON of Nothing"
它是如何工作的...
我们的样板起始代码引入了一个多边形和一个 LineString Shapefile,这样我们就可以计算我们的质心和最短距离。这里的逻辑主要是我们需要首先为每个多边形创建一个质心列表,然后找到离这个质心最近的线上的点位置。当然,最后一步是计算这两个点之间的距离(以米为单位),并检查它是否小于我们指定的容差值。
大多数注释解释了细节,但实际计算到线的最短距离是使用 Shapely 的线性引用功能完成的。我们在 第五章 的 向量分析 中遇到了这个过程,使用我们的捕捉点到线。interpolate 和 project 函数负责找到线上的最近点。
通常,这会接着导出我们的结果到 GeoJSON,如果找到了具有指定容差值的任何点。
第十章. 可视化您的分析
在本章中,我们将涵盖以下主题:
-
使用 Folium 生成 leaflet 网络地图
-
设置 TileStache 以服务瓦片
-
使用 Three.js 可视化 DEM 数据
-
在 DEM 上覆盖正射影像
简介
地理空间分析最棒的部分是可视化。本章将介绍一些可视化分析结果的方法。到目前为止,我们已经使用了 QGIS、leaflet 和 Openlayers 3 来查看我们的结果。在这里,我们将专注于使用一些最新的库进行网络地图发布。
这段代码的大部分将混合 Python、JavaScript、HTML 和 CSS。
小贴士
可以在selection.datavisualization.ch/找到一系列令人惊叹的可视化技术和库。
使用 Folium 生成 leaflet 网络地图
使用自己的数据创建网络地图正变得越来越容易,随着每个新的网络地图库的出现。Folium (folium.readthedocs.org/) 是一个小的 Python 新项目,可以直接从 Python 代码创建简单的网络地图,利用 leaflet JavaScript 地图库。这仍然超过了一行,但只需 20 行以下的 Python 代码,你就可以让 Folium 为你生成一个漂亮的网络地图。
准备工作
Folium 需要 Jinja2 模板引擎和 Pandas 进行数据绑定。好的是,这两个都可以通过pip简单安装:
pip install jinja2
pip install pandas
关于使用 Pandas 的说明也可以在第一章中找到,设置您的地理空间 Python 环境。
如何做到...
-
现在请确保你处于
/ch10/code/文件夹中,以查看以下 Folium 的实时示例:#!/usr/bin/env python # -*- coding: utf-8 -*- import folium import pandas as pd # define the polygons states_geojson = r'us-states.json' # statistic data to connect to our polygons state_unemployment = r'../www/html/US_Unemployment_Oct2012.csv' # read the csv statistic data state_data = pd.read_csv(state_unemployment) # Let Folium determine the scale map = folium.Map(location=[48, -102], zoom_start=3, tiles="Stamen Toner") # create the leaflet map settings map.geo_json(geo_path=states_geojson, data=state_data, columns=['State', 'Unemployment'], threshold_scale=[5, 6, 7, 8, 9, 10], key_on='feature.id', fill_color='YlGn', fill_opacity=0.7, line_opacity=0.2, legend_name='Unemployment Rate (%)') # output the final map file map.create_map(path='../www/html/ch10-01_folium_map.html')
它是如何工作的...
Folium 使用 Jinja2 Python 模板引擎来渲染最终结果,并使用 Pandas 来绑定 CSV 统计数据。代码从导入和定义数据源开始。将显示美国州多边形的 GeoJSON 文件作为渐变图。渐变图是一种显示数据值,这些数据值被分类到一组定义的数据范围中,通常基于某种统计方法。在 GeoJSON 数据中有一个名为id的键字段,其值为美国州的缩写代码。这个id将空间数据绑定到包含相应id字段的统计 CSV 列,因此我们可以连接我们的两个数据集。
Folium 随后需要创建一个map对象,设置map中心坐标、缩放级别以及用于背景的基础瓦片地图。在我们的例子中,定义了Stamen Toner瓦片集。
接下来,我们定义将在我们的背景地图上出现的矢量 GeoJSON。我们需要传递我们源 GeoJSON 的路径以及引用我们的 CSV 文件列State和Unemployment的 Pandas 数据帧对象。然后,我们设置连接 CSV 与 GeoJSON 数据的链接键值。key_on参数读取特征数组中的id GeoJSON 属性键。
最后,我们设置颜色调色板为我们想要的颜色以及样式。图例是 D3 图例,它为我们自动创建并按分位数缩放。

设置 TileStache 以服务瓦片
一旦你有数据并想要将其放到网上,就需要某种服务器。TileStache 最初由 Michal Migurski 开发,是一个 Python 瓦片地图服务器,可以输出矢量瓦片。矢量瓦片是网络地图的未来,使网络地图应用超级快。最终,你将有一个运行并服务简单网络地图的TileStache实例。
准备工作
要在您的机器上运行 TileStache,需要一些要求,包括 Werkzeug、PIL、SimpleJson 和 Modestmaps,因此我们必须首先安装这些。让我们从运行我们的pip install命令开始,如下所示:
注意
要在完整的服务器上运行TileStache,例如 Nginx 或 Apache,并使用mod-python超出了本书的范围,但强烈推荐用于生产部署(有关更多信息,请参阅modpython.org/)。
pip install Werkzeug
pip install modestmaps
pip install simplejson
被称为Werkzeug的 Python 库(werkzeug.pocoo.org/)是我们测试应用的 WSGI 服务器。Mapnik 不是必需的,但你可以安装它来查看演示应用。
如何做...
-
现在,让我们从
github.com/TileStache/TileStache/archive/master.zip下载最新的代码作为 ZIP 文件。小贴士
如果你已安装,请使用命令行
git如下:$ git clone https://github.com/TileStache/TileStache.git -
将其解压到你的
/ch10/TileStache-master文件夹中。 -
通过进入你的
/ch10/TileStache-master/目录并输入以下命令行来测试和检查你的安装是否顺利:> python tilestache-server.py -c ../tilestache.cfg -
执行上述命令后,你应该会看到以下内容:
* Running on http://127.0.0.1:8080/ (Press CTRL+C to quit) -
现在,打开你的网络浏览器并输入
http://localhost:8080/;你应该会看到一些简单的文本,表明TileStache belows hello。 -
接下来,尝试输入
http://localhost:8080/osm/0/0/0.png;你会得到以下输出:![如何做...]()
这是你应该能够看到的全球地图。
-
要获取温哥华,不列颠哥伦比亚省的实时可滚动地图,请访问
http://localhost:8080/osm/preview.html#10/49.1725/-123.0719。
使用 Three.js 可视化 DEM 数据
你有一个很棒的 3D 数字高程模型(DEM),你可能想在网页上查看,所以你的选择仅限于你的想象和编程技能。在这个基于 Bjorn Sandvik 出色工作的例子中,我们将探讨操纵 DEM 以加载基于 Three.js 的 HTML 网页所需的方法。
提示
我强烈推荐的一个 QGIS 插件是qgis2threejs插件,由 Minoru Akagi 编写。Python 插件代码可在 GitHub 上找到,网址为github.com/minorua/Qgis2threejs,在那里你可以找到一个不错的gdal2threejs.py转换器。
生成的 3D DEM 网格可以在你的浏览器中查看:

准备工作
我们需要 Jinja2 作为我们的模板引擎(在本章的第一节中安装),来创建我们的 HTML。其余的要求包括 JavaScript 和我们的 3D DEM 数据。我们的 DEM 数据来自第七章,栅格分析,位于/ch07/geodata/dem_3857.dem文件夹,所以如果你还没有下载所有数据和代码,请现在就下载。
gdal_translate GDAL 可执行文件用于将我们的 DEM 转换为 ENVI .bin 16 位栅格。这个栅格将包含threejs库可以读取以创建 3D 网格的高程值。
提示
使用 IDE 并不总是必要的,但在这个案例中,PyCharm Pro IDE 很有帮助,因为我们正在使用 HTML、JavaScript 和 Python 来创建我们的结果。还有一个免费的 PyCharm 社区版,我也推荐,但它缺少 HTML、JavaScript 和 Jinja2 模板支持。
如果你在你的机器上下载了/ch10/www/js文件夹,那么 Three.js 就可用。如果没有,请现在就下载整个/ch10/www/文件夹。在里面,你会找到用于输出 HTML 和 Jinja2 使用的 Web 模板所需的文件夹。
如何做到...
-
我们将首先运行一个子进程调用,生成 Three.js 所需的带有高程数据的栅格。然后,我们将进入包含单个
Jinja2变量的 HTML 模板代码,如下所示:#!/usr/bin/env python # -*- coding: utf-8 -*- import subprocess from jinja2 import Environment, FileSystemLoader # Create our DEM # use gdal_translate command to create an image to store elevation values # -scale from 0 meters to 2625 meters # stretch all values to full 16bit 0 to 65535 # -ot is output type = UInt16 unsigned 16bit # -outsize is 200 x 200 px # -of is output format ENVI raster image .bin file type # then our input .tif with elevation # followed by output file name .bin subprocess.call("gdal_translate -scale 0 2625 0 65535 " "-ot UInt16 -outsize 200 200 -of ENVI " "../../ch07/geodata/dem_3857.tif " "../geodata/whistler2.bin") # create our Jinja2 HTML # create a standard Jinja2 Environment and load all files # located in the folder templates env = Environment(loader=FileSystemLoader(["../www/templates"])) # define which template we want to render template = env.get_template("base-3d-map.html") # path and name of input 16bit raster image with our elevation values dem_3d = "../../geodata/whistler2.bin" # name and location of the output HTML file we will generate out_html = "../www/html/ch10-03_dem3d_map.html" # dem_file is the variable name we use in our Jinja2 HTML template file result = template.render(title="Threejs DEM Viewer", dem_file=dem_3d) # write out our template to the HTML file on disk with open(out_html,mode="w") as f: f.write(result) -
我们的 Jinja2 HTML 模板代码只包含一个简单的变量,称为
{{ dem_3d }},这样你可以清楚地看到正在发生的事情:#!/usr/bin/env python <html lang="en"> <head> <title>DEM threejs Browser</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <style> body { margin: 0; overflow: hidden; }</style> </head> <body> <div id="dem-map"></div> <script src="img/three.min.js"></script> <script src="img/TrackballControls.js"></script> <script src="img/TerrainLoader.js"></script> <script> var width = window.innerWidth, height = window.innerHeight; var scene = new THREE.Scene(); var axes = new THREE.AxisHelper(200); scene.add(axes); var camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); camera.position.set(0, -50, 50); var renderer = new THREE.WebGLRenderer(); renderer.setSize(width, height); var terrainLoader = new THREE.TerrainLoader(); terrainLoader.load('{{ dem_3d }}', function(data) { var geometry = new THREE.PlaneGeometry(60, 60, 199, 199); for (var i = 0, l = geometry.vertices.length; i < l; i++) { geometry.vertices[i].z = data[i] / 65535 * 10; } var material = new THREE.MeshPhongMaterial({ color: 0xdddddd, wireframe: true }); var plane = new THREE.Mesh(geometry, material); scene.add(plane); }); var controls = new THREE.TrackballControls(camera); document.getElementById('dem-map').appendChild(renderer.domElement); render(); function render() { controls.update(); requestAnimationFrame(render); renderer.render(scene, camera); } </script> </body> </html>
它是如何工作的...
我们的gdal_translate通过将 DEM 数据转换为 Three.js 可以理解的栅格格式为我们做了艰苦的工作。Jinja2 模板 HTML 代码显示了所需的组件,从三个 JavaScript 文件开始。TerrainLoader.js读取这个二进制.bin格式栅格到 Three.js 地形中。
在我们的 HTML 文件中,JavaScript 代码展示了我们如何创建 Three.js 场景,其中最重要的部分是创建THREE.PlaneGeometry。在这个 JavaScript for循环中,我们为每个geometry.vertices分配高程高度,为每个顶点分配高程值对应的平坦平面。
接着使用 MeshPhongMaterial,这样我们就可以在屏幕上以线框的形式看到网格。要查看生成的 HTML 文件,您需要运行一个本地网络服务器,而对于这个,Python 内置了 SimpleHTTPServer。这可以通过以下 Python 命令在命令行中运行:
> python -m SimpleHTTPServer 8080
然后,访问您的浏览器并输入 http://localhost:8080/;选择 html 文件夹,然后点击 ch10-03_dem3d_map.html 文件。
小贴士
使用 PyCharm IDE,您可以直接在 PyCharm 中打开 HTML 文件,将鼠标移至打开文件的右上角,并选择一个浏览器,例如 Chrome,以打开一个新的 HTML 页面。PyCharm 将自动为您启动一个网络服务器,并在您选择的浏览器中显示 3D 地形。
在 DEM 上覆盖正射影像
这次,我们将通过将卫星影像覆盖到我们的 DEM 上,将我们之前的配方提升到新的水平,从而创建一个真正令人印象深刻的 3D 交互式网络地图。

您可以查看来自 geogratis.ca 的其他正射影像,geogratis.gc.ca/api/en/nrcan-rncan/ess-sst/77618678-421b-4a28-a0a5-b074e5f072ff.html。
准备工作
要直接在 DEM 上覆盖正射影像,我们需要确保输入的 DEM 和正射影像具有相同的范围和像素大小。对于这个练习,您需要完成前面的部分,并在 /ch10/geodata/092j02_1_1.tif 文件夹中准备好数据。这是我们将在 DEM 上覆盖的正射影像。
如何做到这一点...
-
让我们深入一些代码,这些代码充满了注释,以供您参考:
#!/usr/bin/env python # -*- coding: utf-8 -*- import subprocess from PIL import Image from jinja2 import Environment, FileSystemLoader # convert from Canada UTM http://epsg.io/3157/map to 3857 # transform the orthophto from epsg:3157 to epsg:3857 # cut the orthophoto to same extent of DEM subprocess.call("gdalwarp -s_srs EPSG:3157 -t_srs EPSG:3857 -overwrite " "-te -13664479.091 6446253.250 -13636616.770 6489702.670" "/geodata/canimage_092j02_tif/092j02_1_1.tif ../geodata/whistler_ortho.tif") # convert the new orthophoto into a 200 x 200 pixel image subprocess.call("gdal_translate -outsize 200 200 " "../geodata/whistler_ortho.tif " "../geodata/whistler_ortho_f.tif") # prepare to create new jpg output from .tif processed_ortho = '../geodata/whistler_ortho_f.tif' drape_texture = '../../geodata/whistler_ortho_f.jpg' # export the .tif to a jpg to make is smaller for web using pil Image.open(processed_ortho).save(drape_texture) # set Jinja2 env and load folder where templates are located env = Environment(loader=FileSystemLoader(["../www/templates"])) # assign template to our HTML file with our variable inside template = env.get_template( "base-3d-map-drape.html") # define the original DEM file dem_3d = "../../geodata/whistler2.bin" # location of new HTML file to be output out_html = "../www/html/ch10-04_dem3d_map_drape.html" # create the new output HTML object and set variable names result = template.render(title="Threejs DEM Drape Viewer", dem_file=dem_3d, texture_map=drape_texture) # write the new HTML file to disk with open(out_html,mode="w") as file: file.write(result) -
我们的 Jinja2 HTML 模板文件看起来是这样的:
<html lang="en"> <head> <title>DEM threejs Browser</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <style> body { margin: 0; overflow: hidden; }</style> </head> <body> <div id="dem-map"></div> <script src="img/three.min.js"></script> <script src="img/TrackballControls.js"></script> <script src="img/TerrainLoader.js"></script> <script> var width = window.innerWidth, height = window.innerHeight; var scene = new THREE.Scene(); scene.add(new THREE.AmbientLight(0xeeeeee)); var axes = new THREE.AxisHelper(200); scene.add(axes); var camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); camera.position.set(0, -50, 50); var renderer = new THREE.WebGLRenderer(); renderer.setSize(width, height); var terrainLoader = new THREE.TerrainLoader(); terrainLoader.load('{{ dem_file }}', function(data) { var geometry = new THREE.PlaneGeometry(60, 60, 199, 199); for (var i = 0, l = geometry.vertices.length; i < l; i++) { geometry.vertices[i].z = data[i] / 65535 * 10; } var material = new THREE.MeshPhongMaterial({ map: THREE.ImageUtils.loadTexture('{{ texture_map }}') }); var plane = new THREE.Mesh(geometry, material); scene.add(plane); }); var controls = new THREE.TrackballControls(camera); document.getElementById('dem-map').appendChild(renderer.domElement); render(); function render() { controls.update(); requestAnimationFrame(render); renderer.render(scene, camera); } </script> </body> </html>
它是如何工作的...
覆盖正射影像的主要方法与前面章节中看到的方法相同,只是在 Three.js 材质渲染的使用方式上略有不同。
数据准备再次扮演了最重要的角色,以确保一切顺利。在我们的 Python 代码 Ch10-04_drapeOrtho.py 中,我们使用 subprocess 调用来执行 gdalwarp 和 gdal_translate 命令行工具。首先使用 Gdalwarp 将原始正射影像从 EPSG:3157 转换为 EPSG:3857 Web Mercator 格式。同时,它也将原始栅格裁剪到与我们的 DEM 输入相同的范围。这个范围是通过读取 gdalinfo whistler.bin 栅格命令行调用来实现的。
然后,我们需要将栅格裁剪到适当的大小,并制作一个 200 x 200 像素的图像,以匹配我们的 DEM 大小。接着使用 PIL 将输出的 .tif 文件转换为更小的 .jpg 文件,这更适合网络演示和速度。
主要的腿部工作完成之后,我们可以使用 Jinja2 来创建我们的输出 HTML 模板,并传入两个dem_file变量,这些变量指向原始的 DEM。第二个变量名为texture_map,指向新创建的用于覆盖 DEM 的 whistler .jpg文件。
最终结果将写入/ch10/www/html/ch10-04_dem3d_map_drape.html文件夹,供你打开并在浏览器中查看。要查看此 HTML 文件,你需要从/ch10/www/目录启动本地 Web 服务器:
> python -m simpleHTTPServer 8080
然后,访问浏览器中的 http://localhost.8080/,你应该在 DEM 上看到一个覆盖的图像。
第十一章。使用 GeoDjango 进行网络分析
在本章中,我们将涵盖以下主题:
-
设置 GeoDjango 网络应用程序
-
创建室内网络路由服务
-
可视化室内路由服务
-
创建室内路线类型服务
-
从房间到房间的室内路线创建
简介
我们的最后一章完全关于将我们的分析扩展到使用 Django 网络框架的 Web 应用程序。一个标准的 Django 贡献包被称为 GeoDjango,位于 django/contrib/gis 包中。这是一个功能丰富的 GIS 工具集,用于地理空间 Web 应用程序开发。这里使用的空间库取决于您选择的空间数据库后端。对于 PostgreSQL,库需求包括 GEOS、PROJ.4 和 PostGIS。
Django 以其良好的文档而闻名,gis contrib 包的安装也不例外,它有一套您需要遵循的说明,网址为 docs.djangoproject.com/en/dev/ref/contrib/gis/。
由于 GeoDjango 是标准 Django 安装的一部分,您将看到第一步是安装 Django 框架。有关安装 GeoDjango、PostgreSQL 和 PostGIS 的任何参考,请参阅 第一章,设置您的地理空间 Python 环境。
设置 GeoDjango 网络应用程序
我们需要完成一些基本的 Django 基础工作,这将是一个对设置所需基本以启动 Django 网络应用程序的非常高级的概述。有关更多信息,请查看官方 Django 教程,网址为 docs.djangoproject.com/en/dev/intro/tutorial01/。
注意
如果您不熟悉 Django 或 GeoDjango,我强烈建议您阅读并完成在线教程,从 Django 开始,网址为 docs.djangoproject.com/en/dev/,然后是 GeoDjango 教程,网址为 docs.djangoproject.com/en/dev/ref/contrib/gis/tutorial/。对于本章,假设您熟悉 Django,已完成整个在线 Django 教程,因此熟悉 Django 概念。
准备工作
我们将使用 Django REST 框架 (www.django-rest-framework.org/) 构建一个路由网络服务。我们需要实现的是一个基本的网络服务,您可以使用 pip 来安装:
>pip install djangorestframework==3.1.3
这将安装版本 3.1.3,即最新版本。如果您想安装最新版本,只需输入以下命令,但请注意,它可能不适用于此示例:
>pip install djangorestframework
如何操作...
现在让我们使用 django-admin 工具创建一个 Django 项目,如下所示:
-
在命令行中,进入
/ch11/code目录并执行此命令:> django-admin startproject web_analysis -
现在,您将有一个
/ch11/code/web_analysis/web_analysis目录,并在其中,您将找到所有标准的基本 Django 组件。 -
要创建我们的网络服务,我们将把所有服务放置在一个名为
api的 Django 应用程序中。此应用程序将存储所有服务。创建此api应用程序就像输入以下代码一样简单:> cd web_analysis切换到新创建的
web_analysis目录:> django-admin startapp api现在创建一个名为 "api" 的新应用程序。
-
这将在
/ch11/code/web_analysis/api下创建一个新的文件夹,并在其中您将找到默认安装的 Django 应用程序文件。接下来,我们需要告诉 Django 关于 Django REST 框架、GeoDjango gis 应用程序以及我们的新api应用程序;我们在/ch11/code/web_analysis/web_analysis/settings.py文件中这样做。让我们将'django.contrib.gis'、'rest_framework'和'api'行添加到我们的INSTALLED_APPS变量中,如下所示:INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', #### GeoDjango Contrib APP # 'django.contrib.gis', #### third party apps 'rest_framework', ##### our local apps 'api', ) -
要启用 GeoDjango 空间模型和空间功能,
'django.contrib.gis'将允许我们访问丰富的地理空间框架。目前我们将其注释掉,因为我们不会在稍后使用它,但您可以随时取消注释,因为这不会造成任何伤害。此空间框架需要一个空间数据库,我们将使用带有 PostGIS 的 PostgreSQL 作为我们的后端。现在让我们在settings.py中更改数据库连接,如下所示:DATABASES = { 'default': { # PostgreSQL with PostGIS 'ENGINE': 'django.contrib.gis.db.backends.postgis', 'NAME': 'py_geoan_cb', # DB name 'USER': 'saturn', # DB user name 'PASSWORD': 'secret', # DB user password 'HOST': 'localhost', 'PORT': '5432', } }注意
此处数据库引用的是我们在第三章中创建的相同的 PostgreSQL + PostGIS 数据库,即 将空间数据从一种格式转换为另一种格式。如果您要跳过到这一部分,请访问第三章中的 使用 ogr2ogr 将 Shapefile 转换为 PostGIS 表 菜单,我们在那里创建了
py_geoan_cb数据库。 -
我们最终的
settings.py配置设置为将错误和异常记录到日志文件中,如果发生错误,将捕获错误。首先,我们将创建一个名为/web_analysis/logs的新文件夹,并添加两个新文件,分别命名为debug.log和verbose.log。我们将把发生的任何错误写入这两个文件,并记录请求或简单地打印错误到这些文件。因此,请将以下代码复制到/web_analysis/web_analysis/settings.py文件的底部,如下所示:LOGGING_CONFIG = None LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", 'datefmt' : "%d/%b/%Y %H:%M:%S" }, 'simple': { 'format': '%(levelname)s %(message)s' }, }, 'handlers': { 'file_verbose': { 'level': 'DEBUG', 'class': 'logging.FileHandler', 'filename': 'logs/verbose.log', 'formatter': 'verbose' }, 'file_debug': { 'level': 'DEBUG', 'class': 'logging.FileHandler', 'filename': 'logs/debug.log', 'formatter': 'verbose' }, }, 'loggers': { 'django': { 'handlers':['file_verbose'], 'propagate': True, 'level':'DEBUG', }, 'api': { 'handlers': ['file_debug'], 'propagate': True, 'level': 'DEBUG', }, } } import logging.config logging.config.dictConfig(LOGGING) -
接下来,让我们创建一个新的数据库用户和一个单独的 PostgreSQL 模式来存储所有我们的 Django 相关表;否则,所有新的 Django 表将自动创建在 PostgreSQL 默认模式 public 中。我们的新用户名为
saturn,可以使用secret密码登录。要创建新用户,您可以使用作为postgres用户运行的命令行工具:>createuser saturn您还可以使用免费的 PGAdmin 工具。在 Ubuntu 上,别忘了切换到
postgres用户,这将允许您在数据库上创建新用户。 -
现在,让我们创建一个新的模式名为
django,它将存储我们所有的 Django 应用程序表。使用 PGAdmin 或 SQL 命令来完成此操作,如下所示:CREATE SCHEMA django AUTHORIZATION saturn; -
使用这个新模式,我们只需要将 PostgreSQL
search_path变量顺序设置为将django模式作为第一优先级。为了完成这个任务,我们需要使用以下 SQLALTER ROLE命令:ALTER ROLE saturn SET search_path = django, geodata, public, topology; -
这设置了
search_path的顺序,将django作为第一个模式,geodata作为第二个,以此类推。这个顺序适用于saturn用户的所有数据库连接。当我们创建新的 Django 表时,它们现在将自动创建在django模式内。 -
现在让我们继续初始化我们的 Django 项目并创建所有表,如下所示:
> python manage.py migrate -
内置的 Django
manage.py命令调用migrate函数并一次性执行同步。接下来,让我们为我们的应用程序创建一个超级用户,该用户可以登录并完全控制整个网络应用程序。然后,按照命令行说明输入用户名、电子邮件和密码,如下所示:> python manage.py createsuperuser -
现在这些步骤都已完成,我们准备好真正做一些事情并构建我们的在线路由应用程序。为了测试一切是否正常工作,运行此命令:
> python manage.py runserver 8000 -
打开您的本地网络浏览器并查看 Django 默认欢迎页面。
创建室内网络路由服务
让我们把我们在第八章,网络路由分析中投入的所有努力都放到万维网上。我们的路由服务将简单地接受一个起点位置、一个 x、y 坐标对、楼层级别和目的地位置。然后室内路由服务将计算最短路径,并以 GeoJSON 文件的形式返回完整的路线。
准备工作。
为了规划前面的任务,让我们从高层次列出我们需要完成的内容,以便我们清楚我们的方向:
-
创建一个 URL 模式来调用路由服务。
-
创建一个视图来处理传入的 URL 请求并返回适当的 GeoJSON 路由网络响应:
-
接受传入的请求参数。
开始 x 坐标。
开始 y 坐标。
开始楼层编号。
结束 x 坐标。
结束 y 坐标。
结束楼层编号。
-
返回 GeoJSON LineString。
路径几何形状。
路径长度。
路径步行时间。
我们还需要让名为
saturn的新数据库用户能够访问在第八章,网络路由分析中创建的 PostgreSQL geodata 模式中的表。目前,只有名为postgres的用户是所有者和全能者。这需要改变,这样我们就可以继续前进,而无需重新创建我们在第八章,网络路由分析中创建的表。所以,让我们继续,简单地让saturn用户成为这些表的每个所有者,如下所示:ALTER TABLE geodata.ch08_e01_networklines OWNER TO saturn; ALTER TABLE geodata.ch08_e01_networklines_vertices_pgr OWNER TO saturn; ALTER TABLE geodata.ch08_e02_networklines OWNER TO saturn; ALTER TABLE geodata.ch08_e02_networklines_vertices_pgr OWNER TO saturn; ALTER TABLE geodata.networklines_3857 OWNER TO saturn; ALTER TABLE geodata.networklines_3857_vertices_pgr OWNER TO saturn;小贴士
如果您想允许
saturn用户和任何其他用户访问这些表,您可以创建一个 PostgreSQL 组角色,并将用户分配到该角色,如下所示:CREATE ROLE gis_edit VALID UNTIL 'infinity'; GRANT ALL ON SCHEMA geodata TO GROUP gis_edit; GRANT gis_edit TO saturn; GRANT ALL ON TABLE geodata.ch08_e01_networklines TO GROUP gis_edit; GRANT ALL ON TABLE geodata.ch08_e01_networklines_vertices_pgr TO GROUP gis_edit; GRANT ALL ON TABLE geodata.ch08_e02_networklines TO GROUP gis_edit; GRANT ALL ON TABLE geodata.ch08_e02_networklines_vertices_pgr TO GROUP gis_edit; GRANT ALL ON TABLE geodata.networklines_3857 TO GROUP gis_edit; GRANT ALL ON TABLE geodata.networklines_3857_vertices_pgr TO GROUP gis_edit; -
如何操作...
我们现在的代码在一个文件夹中,这种结构对所有 Django Web 项目都是通用的,因此遵循这些步骤应该是直截了当的:
-
让我们先连接我们的新 URL。请打开位于
ch11/code/web_analysis/文件夹中的urls.py文件。在文件中,您需要输入我们新网页的主要 URL 配置。当创建项目时,此文件会自动创建。如您所见,Django 填充了一些辅助文本,显示了基本配置选项。我们需要添加我们稍后将要使用的admin应用,以及我们新 API 的 URL。API 应用将拥有它自己的 URL 配置文件,正如您在api.urls引用中看到的,我们将创建它。/web_analysis/urls.py文件应如下所示:"""web_analysis URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/1.8/topics/http/urls/ Examples: Function views 1\. Add an import: from my_app import views 2\. Add a URL to urlpatterns: url(r'^$', views.home, name='home') Class-based views 1\. Add an import: from other_app.views import Home 2\. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') Including another URLconf 1\. Add an import: from blog import urls as blog_urls 2\. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) """ from django.conf.urls import include, url from django.contrib import admin urlpatterns = [ url(r'^admin/', include(admin.site.urls)), url(r'^api/', include('api.urls')), ] -
接下来,让我们创建
/web_analysis/api/urls.pyAPI URL。此文件不是自动生成的,所以我们现在将创建此文件。/api/urls.py文件的内容将如下所示:from django.conf.urls import patterns, url from rest_framework.urlpatterns import format_suffix_patterns urlpatterns = patterns('api.views', # ex valid call from to /api/directions/1587848.414,5879564.080,2&1588005.547,5879736.039,2 url(r'^directions/(?P<start_coord>[-]?\d+\.?\d+,\d+\.\d+),(?P<start_floor>\d+)&(?P<end_coord>[-]?\d+\.?\d+,\d+\.\d+),(?P<end_floor>\d+)/$', 'create_route', name='directions'), ) urlpatterns = format_suffix_patterns(urlpatterns) -
正则表达式看起来很复杂,就像大多数正则表达式一样。如果您需要一些帮助来理解它,请尝试参考
regex101.com/#python。请将此正则表达式粘贴到正则表达式字段中:(?P<start_coord>[-]?\d+\.?\d+,\d+\.\d+),(?P<start_floor>\d+)&(?P<end_coord>[-]?\d+\.?\d+,\d+\.\d+),(?P<end_floor>\d+) -
要测试您的 URL 字符串,只需将此文本粘贴到测试字符串字段中:
1587848.414,5879564.080,2&1588005.547,5879736.039,2 -
如果它以一些奇特的颜色亮起,那么您就可以开始了:
![如何操作...]()
Django 在 URL 配置中使用正则表达式非常方便,但并不总是容易阅读和明确。我们的 URL 以文本方式解释,如下所示:
/api/directions/start_x,start_y,start_floor&end_x,end_y,end_floor这是一个来自您开发机器的真实示例。当调用 URL 时,它看起来是这样的:
http://localhost:8000/api/directions/1587848.414,5879564.080,2&1588005.547,5879736.039,2起始和结束位置信息由一个
&符号分隔,而每个起始参数和结束参数的内容由逗号分隔。从现在开始,在复杂性方面,我们需要输入我们 API 的逻辑部分。Django 在视图中处理这一点。我们的
/web_analysis/api/views.py代码包含了处理请求和响应的代码。 -
主要的
def create_route函数应该看起来很熟悉,因为它直接来自第八章,网络路由分析,并进行了一些修改。创建了一个新的helper函数,称为find_closest_network_node。这个新函数比我们之前用来找到用户输入的任何给定x,y坐标的节点所用的 SQL 更健壮、更快:#!/usr/bin/env python # -*- coding: utf-8 -*- import traceback from django.http import HttpResponseNotFound from rest_framework.decorators import api_view from rest_framework.response import Response from geojson import loads, Feature, FeatureCollection import logging logger = logging.getLogger(__name__) from django.db import connection def find_closest_network_node(x_coord, y_coord, floor): """ Enter a given coordinate x,y and floor number and find the nearest network node to start or end the route on :param x_coord: float in epsg 3857 :param y_coord: float in epsg 3857 :param floor: integer value equivalent to floor such as 2 = 2nd floor :return: node id as an integer """ # connect to our Database logger.debug("now running function find_closest_network_node") cur = connection.cursor() # find nearest node on network within 200 m # and snap to nearest node query = """ SELECT verts.id as id FROM geodata.networklines_3857_vertices_pgr AS verts INNER JOIN (select ST_PointFromText('POINT(%s %s %s)', 3857)as geom) AS pt ON ST_DWithin(verts.the_geom, pt.geom, 200.0) ORDER BY ST_3DDistance(verts.the_geom, pt.geom) LIMIT 1;""" # pass 3 variables to our %s %s %s place holder in query cur.execute(query, (x_coord, y_coord, floor,)) # get the result query_result = cur.fetchone() # check if result is not empty if query_result is not None: # get first result in tuple response there is only one point_on_networkline = int(query_result[0]) return point_on_networkline else: logger.debug("query is none check tolerance value of 200") return False # use the rest_framework decorator to create our api # view for get, post requests @api_view(['GET', 'POST']) def create_route(request, start_coord, start_floor, end_coord, end_floor): """ Generate a GeoJSON indoor route passing in a start x,y,floor followed by & then the end x,y,floor Sample request: http:/localhost:8000/api/directions/1587848.414,5879564.080,2&1588005.547,5879736.039,2 :param request: :param start_coord: start location x,y :param start_floor: floor number ex) 2 :param end_coord: end location x,y :param end_floor: end floor ex) 2 :return: GeoJSON route """ if request.method == 'GET' or request.method == 'POST': cur = connection.cursor() # parse the incoming coordinates and floor using # split by comma x_start_coord = float(start_coord.split(',')[0]) y_start_coord = float(start_coord.split(',')[1]) start_floor_num = int(start_floor) x_end_coord = float(end_coord.split(',')[0]) y_end_coord = float(end_coord.split(',')[1]) end_floor_num = int(end_floor) # use our helper function to get vertices # node id for start and end nodes start_node_id = find_closest_network_node(x_start_coord, y_start_coord, start_floor_num) end_node_id = find_closest_network_node(x_end_coord, y_end_coord, end_floor_num) routing_query = ''' SELECT seq, id1 AS node, id2 AS edge, total_cost AS cost, layer, type_id, ST_AsGeoJSON(wkb_geometry) AS geoj FROM pgr_dijkstra( 'SELECT ogc_fid as id, source, target, st_length(wkb_geometry) AS cost, layer, type_id FROM geodata.networklines_3857', %s, %s, FALSE, FALSE ) AS dij_route JOIN geodata.networklines_3857 AS input_network ON dij_route.id2 = input_network.ogc_fid ; ''' # run our shortest path query if start_node_id or end_node_id: cur.execute(routing_query, (start_node_id, end_node_id)) else: logger.error("start or end node is None " + str(start_node_id)) return HttpResponseNotFound('<h1>Sorry NO start or end node' ' found within 200m</h1>') # get entire query results to work with route_segments = cur.fetchall() # empty list to hold each segment for our GeoJSON output route_result = [] # loop over each segment in the result route segments # create the list of our new GeoJSON for segment in route_segments: seg_cost = segment[3] # cost value layer_level = segment[4] # floor number seg_type = segment[5] geojs = segment[6] # geojson coordinates geojs_geom = loads(geojs) # load string to geom geojs_feat = Feature(geometry=geojs_geom, properties={'floor': layer_level, 'length': seg_cost, 'type_id': seg_type}) route_result.append(geojs_feat) # using the geojson module to create our GeoJSON Feature Collection geojs_fc = FeatureCollection(route_result) try: return Response(geojs_fc) except: logger.error("error exporting to json model: "+ str(geojs_fc)) logger.error(traceback.format_exc()) return Response({'error': 'either no JSON or no key params in your JSON'}) else: retun HttpResponseNotFound('<h1>Sorry not a GET or POST request</h1>')结果 API 调用有一个很好的 Web 界面,这是由Django REST 框架自动生成的,如下面的截图所示。您需要调用的 URL 也显示出来,并应返回 GeoJSON 结果。
![如何操作...]()
以下 URL 将返回 GeoJSON 到您的浏览器;在 Chrome 中,它通常只会显示为简单的文本。IE 用户可以通过在 Notepad++或本地文本编辑器中打开它来将其作为文件下载,以查看 GeoJSON 的内容:
http://localhost:8000/api/directions/1587848.414,5879564.080,2&1588005.547,5879736.039,2/?format=json
它是如何工作的...
我们的观点使用 Django REST 框架处理请求和响应。有两个函数执行所有艰苦的工作,而从未使用 Django 对象关系映射器(ORM)。这样做的原因有两个:首先,为了向您展示直接数据库使用的基本知识,而不需要太多的抽象和内部工作原理;其次,因为我们正在使用 PostGIS 的函数,这些函数不能直接通过 GeoDjango 的 ORM 直接使用,例如ST_3DDistance或ST_PointFromText。我们可以使用一些花哨的 Django 辅助函数,如.extra(),但这可能会让除了经验丰富的 Django 用户之外的所有人感到困惑。
让我们讨论第一个find_closest_network_node函数,它接受三个参数:x_coord、y_coord和floor。x和y坐标应该是双精度浮点值,而楼层是一个整数。我们的正则表达式 URL 将任何请求限制为数字,因此我们不需要在我们的代码中进行任何额外的格式检查。
查找最近节点并返回其 ID 的 SQL 查询将搜索半径限制在 200 米,这相当于一个巨大的房间或礼堂。然后,我们根据点之间的 3D 距离进行排序,并使用LIMIT将结果限制为一条,因为我们不是路由到多个位置。
这将数据传递给我们的第二个函数create_route,我们向其传递起始坐标、起始楼层整数、结束坐标和结束楼层编号。我们的 URL 在/web_analysis/api/urls.py中使用了名为groups的正则表达式,这与我们的函数请求参数中使用的名称相对应。这样做可以使事情更加明确,以便你知道查询中哪些值属于哪里。
我们首先解析传入的参数,以获取精确的浮点数和整数值,以便为我们的路由查询提供数据。路由查询本身与第八章网络路由分析保持不变,因此请参阅本章以获取更多详细信息。Django REST 框架的响应将 GeoJSON 发送回客户端,并且有将其作为纯文本返回的能力。
可视化室内路由服务
在创建了我们的精彩 API 之后,现在是时候在地图上可视化作为 GeoJSON 返回的室内路线了。我们将现在深入研究 Django 模板组件,以创建用于显示简单滑块网络地图的 HTML、JS 和 CSS,该地图使用 Openlayers 3.4.0 和 Bootstrap CSS。
我们的新网络地图将在地图上以美观的样式显示 GeoJSON,并附带一个菜单栏,我们将在其中包含后续功能。

准备工作
我们需要构建一些新的文件夹和文件来存储我们 Django 网络应用的新静态和模板内容。让我们从创建 /web_analysis/templates 文件夹开始,然后是 /web_analysis/static 文件夹。
在我们的 /static/ 文件夹中,我们将放置 JavaScript 和 CSS 文件的非动态内容。/templates/ 文件夹将存储用于创建我们网页的 HTML 模板文件。
接下来,让我们告诉 Django /web_analysis/settings.py 关于我们新模板文件夹的位置;将 os.path.join(BASE_DIR, 'templates') 值添加到此处显示的 'DIRS' 键中,以便 TEMPLATES 变量看起来像这样:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates'),],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
为了管理我们的地图,让我们创建一个名为 maps 的新 Django 应用,在那里我们可以存储所有我们的地图信息如下:
> python manage.py startapp maps
接下来,在 /web_analysis/web_analysis/settings.py INSTALLED APPS 变量中注册您的新应用,通过在 api 条目 'maps' 下方添加以下内容,在 'api' 条目下。
/maps/urls.py 文件不是自动创建的,所以现在让我们创建它并填充以下内容:
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
urlpatterns = patterns('maps.views',
# ex valid call from to /api/directions/1587848.414,5879564.080,2&1588005.547,5879736.039,2
url(r'^(?P<map_name>\w+)/$', 'route_map', name='route-map'),
)
urlpatterns = format_suffix_patterns(urlpatterns)
我们需要在主 /web_analysis/web_analysis/urls.py 中分配 maps/urls.py,这样我们就可以自由地为所有我们的映射需求创建任何 URL。
将以下行添加到 /web_analysis/web_analysis/urls.py 文件中,如下所示:
url(r'^maps/', include('maps.urls')),
这意味着我们 /maps/urls.py 中的所有 URL 都将以 http://localhost:8000/maps/ 开头。
我们现在准备好在 settings.py 中设置静态文件和静态内容,如下所示:
STATIC_URL = '/static/'
STATIC_FOLDER = 'static'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, STATIC_FOLDER),
]
# finds all static folders in all apps
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)
现在,您应该在 /static/ 文件夹中有以下文件夹和文件:
static
+---css
| bootstrap-responsive.min.css
| bootstrap.min.css
| custom-layout.css
| font-awesome.min.css
| ol.css
|
+---img
\---js
bootstrap.min.js
jquery-1.11.2.min.js
jquery.min.js
ol340.js
这应该足以设置您的 Django 项目,以便它能够提供静态地图。
如何做到这一点...
实际上提供地图服务需要我们创建一个 HTML 页面。我们使用内置的 Django 模板引擎构建两个 HTML 页面。第一个页面模板是 base.html,它将包含我们网络地图页面的基本内容,使其成为我们前端设计的重要组成部分。这个页面包含一系列块标签,每个标签对应一个单独的内容占位符。这使我们能够根据基础模板快速创建新的地图页面,从而设置我们的基本模板架构。
-
这里是
/templates/base.html文件:{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> {% block head %} <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="Sample Map"> <meta name="author" content="Michael Diener"> <meta charset="UTF-8"> <title>{% block title %}Default Title{% endblock %}</title> <script src="img/{% static "js/jquery-1.11.2.min.js" %}"></script> <link rel="stylesheet" href="{% static "css/bootstrap.min.css" %}"> <script src="img/{% static "js/bootstrap.min.js" %}"></script> <link rel="stylesheet" href="{% static "css/ol.css" %}" type="text/css"> <link rel="stylesheet" href="{% static "css/custom-layout.css" %}" type="text/css"> <script src="img/{% static "js/ol340.js" %}"></script> {% endblock head %} </head> <body> {% block body %} {% block nav %} <nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">Indoor Project</a> </div> <div id="navbar" class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li><a href="#about">About</a></li> <li><a href="#contact">Contact</a></li> </ul> </div><!--/.nav-collapse --> </div> </nav> {% endblock nav %} {% endblock body %} </body> </html> -
现在,让我们继续实际地图的设置。一个名为
/templates/route-map.html的新模板包含所有实际的 Django 模板块,这些块填充了以下 HTML 内容:{% extends "base.html" %} {% load staticfiles %} {% block title %}Simple route map{% endblock %} {% block body %} {{ block.super }} <div class="container-fluid"> <div class="row"> <div class="col-md-2"> <div id="directions" class="directions"> <form> <div class="radio"> <label> <input type="radio" name="typeRoute" id="routeTypeStandard" value="0" checked> Standard Route </label> </div> <div class="radio"> <label> <input type="radio" name="typeRoute" id="routeTypeBarrierFree" value="1"> Barrier Free Route </label> </div> <button type="submit" class="btn btn-default">Submit</button> <br> </form> </div> </div> <div class="col-md-10"> <div id="map" class="map"></div> </div> </div> </div> <script> var routeUrl = '/api/directions/1587848.414,5879564.080,2&1588005.547,5879736.039,2&' + sel_Val2 + '/?format=json'; map.getLayers().push(new ol.layer.Vector({ source: new ol.source.GeoJSON({url: routeUrl, crossDomain: true,}), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'blue', width: 4 }) }), title: "Route", name: "Route" })); }); var vectorLayer = new ol.layer.Vector({ source: new ol.source.GeoJSON({url: geojs_url}), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'red', width: 4 }) }), title: "Route", name: "Route" }); var map = new ol.Map({ layers: [ new ol.layer.Tile({ source: new ol.source.OSM() }), vectorLayer ], target: 'map', controls: ol.control.defaults({ attributionOptions: /** @type {olx.control.AttributionOptions} */ ({ collapsible: false }) }), view: new ol.View({ center: [1587927.09817072,5879650.90059265], zoom: 18 }) }); </script> {% endblock body %} -
为了使我们的应用实际上显示这些模板,我们需要创建一个视图。视图处理请求并返回
route-map.html。现在,我们的简单视图已经完成:from django.shortcuts import render def route_map(request): return render(request, 'route-map.html')
它是如何工作的...
从 base.html 模板开始,我们为地图制作设置了基本构建块。静态文件和资源被设置来处理我们的 JavaScript 和 CSS 代码的服务。base.html 文件设计为允许我们添加多个 HTML 页面之间共享的元素,例如 Microsoft PowerPoint 中的主页面。块越多,即占位符越多,您的基准就越好。
我们的route-map.html包含实际代码,通过调用它并使用预定义的、硬编码的from,to URL来引用我们的api:
var geojs_url = "http://localhost:8000/api/directions/1587898.414,5879564.080,1&1588005.547,5879736.039,2/?format=json"
/maps/views.py 代码是任何地图逻辑、变量或参数传递到模板的地方。在我们的代码中,我们只是接收一个请求并返回一个 HTML 页面。现在你有一个基本的室内路由服务和可视化客户端,可以向你的朋友展示。
创建室内路线类型服务
基于指定的类型值构建路线,例如无障碍路线或标准步行路线值,对用户来说非常好。如何构建不同的路线类型取决于与我们室内图中的方式连接的可用数据。此示例将允许用户选择无障碍路线,我们的服务将生成一条路径,避开如楼梯等障碍物:

准备工作
我们需要访问更多关于我们网络的数据,以便允许路由类型。路线类型基于网络线路类型,该类型存储在每个 LineString 上的属性。为了分类我们的路线类型,我们有以下查找表模式:
| 值 | 路线类型 |
|---|---|
0 |
室内路线 |
1 |
室外路线 |
2 |
电梯 |
3 |
楼梯 |
因此,我们想要避免任何楼梯段,这在技术上意味着避免type_id = 3。
备注
可选地,你可以创建一个查找表来存储所有可能的类型及其等效权重。这些值可以包含在总成本值的计算中,以影响路线结果。
现在,我们可以根据某些偏好来控制路线的生成。现在可以为偏好设置标准路线搜索,例如,根据你的需要选择走楼梯还是电梯:
ALTER TABLE geodata.ch08_e01_networklines ADD COLUMN total_cost double precision;
ALTER TABLE geodata.ch08_e02_networklines ADD COLUMN total_cost double precision;
update geodata.networklines_3857 set total_cost = st_length(wkb_geometry)*88
where type_id = 2;
update geodata.networklines_3857 set total_cost = st_length(wkb_geometry)*1.8
where type_id = 3;
如果你更新geodata.networklines_3857,请确保用户saturn是所有者或有访问权限;否则,你的 API 调用将失败。
如何操作...
从任何点到最短路径由一个基本属性cost控制。对于标准路线,成本等于段落的距离。我们寻找最短路径,这意味着找到到达目的地的最短路径。
为了控制路径,我们设置成本值。创建无障碍路线涉及将所有段类型设置为异常高的stairs值,从而使路径,即距离,变得非常大,因此被排除在最短路径路由查找过程中。我们的另一种选择是在查询中添加一个WHERE子句,并且只接受type_id不等于3的值,这意味着它不是楼梯类型。我们将在即将到来的代码中使用此选项。
因此,我们的数据需要保持清洁,以便我们可以为我们的网络线路中的特定段类型分配特定的成本。
现在,我们需要添加一个新参数来捕获路线类型:
-
我们将更新
/api/views.py函数,create route(),并添加一个名为route_type的新参数。接下来是实际需要接受这个新参数的查询。我们设置了一个名为barrierfree_q的新变量来保存我们将添加到原始查询中的WHERE子句:def create_route(request, start_coord, start_floor, end_coord, end_floor, route_type): base_route_q = """SELECT ogc_fid as id, source, target, total_cost AS cost, layer, type_id FROM geodata.networklines_3857""" # set default query barrierfree_q = "WHERE 1=1" if route_type == "1": # exclude all networklines of type stairs barrierfree_q = "WHERE type_id not in (3,4)" routing_query = ''' SELECT seq, id1 AS node, id2 AS edge, ST_Length(wkb_geometry) AS cost, layer, type_id, ST_AsGeoJSON(wkb_geometry) AS geoj FROM pgr_dijkstra(' {normal} {type}', %s, %s, FALSE, FALSE ) AS dij_route JOIN geodata.networklines_3857 AS input_network ON dij_route.id2 = input_network.ogc_fid ; '''.format(normal=base_route_q, type=barrierfree_q) -
我们将更新我们的
/api/urls.py以输入新的 URL 参数route_type。新添加的命名组正则表达式自然被称为route_type,并且只接受从 0 到 9 的数字。因此,这当然也限制了你有 10 种路由类型。所以,如果你想添加更多类型,你需要更新你的regex如下:from django.conf.urls import patterns, url from rest_framework.urlpatterns import format_suffix_patterns urlpatterns = patterns('api.views', # ex valid call from to /api/directions/1587848.414,5879564.080,2&1588005.547,5879736.039,2 url(r'^directions/(?P<start_coord>[-]?\d+\.?\d+,\d+\.\d+),(?P<start_floor>\d+)&(?P<end_coord>[-]?\d+\.?\d+,\d+\.\d+),(?P<end_floor>\d+)&(?P<route_type>[0-9])/$', 'create_route', name='directions'), ) urlpatterns = format_suffix_patterns(urlpatterns) -
/maps/views.py函数也需要进行改进,以便我们可以传递参数。现在,它将接受在/api/urls.py中定义的route_type:from django.shortcuts import render def route_map(request, route_type = "0"): return render(request, 'route-map.html', {'route_type': route_type}) -
是时候更新
route-map.html以包括单选按钮,允许用户选择 标准路线 或 无障碍路线。地图将在你点击路由类型单选按钮后立即更新路线:{% extends "base.html" %} {% load staticfiles %} {% block title %}Simple route map{% endblock %} {% block body %} {{ block.super }} <div class="container-fluid"> <div class="row"> <div class="col-md-2"> <div id="directions" class="directions"> <form> <div class="radio"> <label> <input type="radio" name="typeRoute" id="routeTypeStandard" value="0" checked> Standard Route </label> </div> <div class="radio"> <label> <input type="radio" name="typeRoute" id="routeTypeBarrierFree" value="1"> Barrier Free Route </label> </div> <button type="submit" class="btn btn-default">Submit</button> <br> </form> </div> </div> <div class="col-md-10"> <div id="map" class="map"></div> </div> </div> </div> <script> var url_base = "/api/directions/"; var start_coord = "1587848.414,5879564.080,2"; var end_coord = "1588005.547,5879736.039,2"; var r_type = {{ route_type }}; var geojs_url = url_base + start_coord + "&" + end_coord + "&" + sel_Val + '/?format=json'; var sel_Val = $( "input:radio[name=typeRoute]:checked" ).val(); $( ".radio" ).change(function() { map.getLayers().pop(); var sel_Val2 = $( "input:radio[name=typeRoute]:checked" ).val(); var routeUrl = '/api/directions/1587848.414,5879564.080,2&1588005.547,5879736.039,2&' + sel_Val2 + '/?format=json'; map.getLayers().push(new ol.layer.Vector({ source: new ol.source.GeoJSON({url: routeUrl, crossDomain: true,}), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'blue', width: 4 }) }), title: "Route", name: "Route" })); }); var vectorLayer = new ol.layer.Vector({ source: new ol.source.GeoJSON({url: geojs_url}), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'red', width: 4 }) }), title: "Route", name: "Route" }); var map = new ol.Map({ layers: [ new ol.layer.Tile({ source: new ol.source.OSM() }), vectorLayer ], target: 'map', controls: ol.control.defaults({ attributionOptions: /** @type {olx.control.AttributionOptions} */ ({ collapsible: false }) }), view: new ol.View({ center: [1587927.09817072,5879650.90059265], zoom: 18 }) }); </script> {% endblock body %}对于
type = 0或使用楼梯的路线,我们的结果应该看起来像这样:![如何操作...]()
无障碍路线将使用
type = 1,这意味着强制使用电梯并避免所有楼梯。你的结果应该看起来像这样:![如何操作...]()
工作原理...
在这里需要理解的主要部分是,我们需要在我们的 API 调用中添加一个选项路由类型。这个 API 调用必须接受一个特定的路由类型,我们将其定义为从 0 到 9 的数字。然后,这个路由类型数字被作为参数传递到我们的 URL 中,api/views.py 执行调用。然后 API 根据路由类型生成一个新的路线。
我们的所有更改都是在 /api/view.py 代码中进行的,现在包括一个 SQL WHERE 子句,并排除 type_id = 3 的 networklines——即楼梯。这个查询更改使我们的应用保持快速,实际上并没有增加任何 Django 中间件代码在我们的视图中。
前端需要用户选择一个路由类型,默认路由类型设置为标准值,例如 0,就像楼梯的情况一样。这个默认类型被使用,因为在大多数室内环境中,楼梯通常更短。当然,你可以在任何时候将其默认值更改为任何值或标准。使用单选按钮来限制选择为标准路线或无障碍路线。选择路由类型后,地图会自动删除旧路线并创建新路线。
从房间到房间创建室内路线
在多层室内路由网络应用中,从房间 A 到房间 B 的路由类型将汇集我们到目前为止的所有工作。我们将导入一些房间数据并利用我们的网络,然后允许用户选择一个房间,从一个房间到下一个房间进行路由,并选择一种路由类型。

准备工作
我们需要导入一楼和二楼的一组房间多边形,如下所示:
-
按如下方式导入一楼房间多边形的 Shapefile:
ogr2ogr -a_srs EPSG:3857 -lco "SCHEMA=geodata" -lco "COLUMN_TYPES=name=varchar,room_num=integer,floor=integer" -nlt POLYGON -nln ch11_e01_roomdata -f PostgreSQL "PG:host=localhost port=5432 user=saturn dbname=py_geoan_cb password=secret" e01_room_data.shp -
按如下方式导入二楼房间多边形的 Shapefile:
ogr2ogr -a_srs EPSG:3857 -lco "SCHEMA=geodata" -lco "COLUMN_TYPES=name=varchar,room_num=integer,floor=integer" -nlt POLYGON -nln ch11_e02_roomdata -f PostgreSQL "PG:host=localhost port=5432 user=saturn dbname=py_geoan_cb password=secret" e02_room_data.shp -
创建一个新的 PostgreSQL 视图以合并所有新的房间数据到一个表中,这样我们就可以一次性查询所有房间:
CREATE OR REPLACE VIEW geodata.search_rooms_v AS SELECT floor, wkb_geometry, room_num FROM geodata.ch11_e01_roomdata UNION SELECT floor, wkb_geometry, room_num FROM geodata.ch11_e02_roomdata ; ALTER TABLE geodata.search_rooms_v OWNER TO saturn;
如何操作...
为了允许用户从 A 到 B 进行路由,我们需要启用从字段到字段的路线,如下所示:
-
创建一个新的 URL 以接受起始和结束房间号的新参数。例如,第一个 URL 将看起来像
http://localhost:8000/api/directions/10010&20043&0,这意味着从房间号10010到房间号20042使用标准路由类型等于零的路由。注意
第二个 URL 是一个额外的函数,您可以通过传入房间号来调用它,以仅返回房间中心坐标,例如:
http://localhost:8000/directions/10010。视图中的此函数 不存在,留作您的作业。
url(r'^directions/(?P<start_room_num>\d{5})&(?P<end_room_num>\d{5})&(?P<route_type>[0-9])/$', 'route_room_to_room', name='route-room-to-room'), url(r'^directions/(?P<room_num>\d{5})/$', 'get_room_centroid_node', name='room-center'), -
创建一个新的
/api/views.py函数来查找房间中心坐标,并返回到networklines上最近的节点:def get_room_centroid_node(room_number): ''' Find the room center point coordinates and find the closest route node point :param room_number: integer value of room number :return: Closest route node to submitted room number ''' room_center_q = """SELECT floor, ST_asGeoJSON(st_centroid(wkb_geometry)) AS geom FROM geodata.search_rooms_v WHERE room_num = %s;""" cur = connection.cursor() cur.execute(room_center_q, (room_number,)) res = cur.fetchall() res2 = res[0] room_floor = res2[0] room_geom_x = json.loads(res2[1]) room_geom_y = json.loads(res2[1]) x_coord = float(room_geom_x['coordinates'][0]) y_coord = float(room_geom_y['coordinates'][1]) room_node = find_closest_network_node(x_coord, y_coord, room_floor) try: return room_node except: logger.error("error get room center " + str(room_node)) logger.error(traceback.format_exc()) return {'error': 'error get room center'} -
在
/api/views.py内部构建该函数以接受起始节点 ID、结束节点 ID 和路由类型,然后返回最终路由的 GeoJSON,如下所示:def run_route(start_node_id, end_node_id, route_type): ''' :param start_node_id: :param end_node_id: :param route_type: :return: ''' cur = connection.cursor() base_route_q = """SELECT ogc_fid AS id, source, target, total_cost AS cost, layer, type_id FROM geodata.networklines_3857""" # set default query barrierfree_q = "WHERE 1=1" if route_type == "1": # exclude all networklines of type stairs barrierfree_q = "WHERE type_id not in (3,4)" routing_query = ''' SELECT seq, id1 AS node, id2 AS edge, ST_Length(wkb_geometry) AS cost, layer, type_id, ST_AsGeoJSON(wkb_geometry) AS geoj FROM pgr_dijkstra(' {normal} {type}', %s, %s, FALSE, FALSE ) AS dij_route JOIN geodata.networklines_3857 AS input_network ON dij_route.id2 = input_network.ogc_fid ; '''.format(normal=base_route_q, type=barrierfree_q) # run our shortest path query if start_node_id or end_node_id: cur.execute(routing_query, (start_node_id, end_node_id)) else: logger.error("start or end node is None " + str(start_node_id)) return HttpResponseNotFound('<h1>Sorry NO start or end node' ' found within 200m</h1>') # get entire query results to work with route_segments = cur.fetchall() # empty list to hold each segment for our GeoJSON output route_result = [] # loop over each segment in the result route segments # create the list of our new GeoJSON for segment in route_segments: seg_cost = segment[3] # cost value layer_level = segment[4] # floor number seg_type = segment[5] geojs = segment[6] # geojson coordinates geojs_geom = loads(geojs) # load string to geom geojs_feat = Feature(geometry=geojs_geom, properties={'floor': layer_level, 'length': seg_cost, 'type_id': seg_type}) route_result.append(geojs_feat) # using the geojson module to create our GeoJSON Feature Collection geojs_fc = FeatureCollection(route_result) return geojs_fc -
最后,我们可以创建一个函数,我们的 API 将调用它来生成响应:
@api_view(['GET', 'POST']) def route_room_to_room(request, start_room_num, end_room_num, route_type): ''' Generate a GeoJSON route from room number to room number :param request: GET or POST request :param start_room_num: an integer room number :param end_room_num: an integer room number :param route_type: an integer room type :return: a GeoJSON linestring of the route ''' if request.method == 'GET' or request.method == 'POST': start_room = int(start_room_num) end_room = int(end_room_num) start_node_id = get_room_centroid_node(start_room) end_node_id = get_room_centroid_node(end_room) res = run_route(start_node_id, end_node_id, route_type) try: return Response(res) except: logger.error("error exporting to json model: " + str(res)) logger.error(traceback.format_exc()) return Response({'error': 'either no JSON or no key params in your JSON'}) else: return HttpResponseNotFound('<h1>Sorry not a GET or POST request</h1>') -
在
/api/urls.py中添加一个 URL 以访问所有可用的房间列表:url(r'^rooms/$', 'room_list', name='room-list'), -
创建一个 API 服务以返回所有房间号的 JSON 数组。此数组用于自动完成字段、
route-from和route-to。我们使用 Twitter 的Typeahead.jsJavaScript 库来处理我们的自动完成下拉类型提示。作为用户,您只需输入 1,例如,所有以 1 开头的房间将显示为10010(请在此处查看twitter.github.io/typeahead.js/examples/):@api_view(['GET', 'POST']) def room_list(request): ''' http://localhost:8000/api/rooms :param request: no parameters GET or POST :return: JSON Array of room numbers ''' cur = connection.cursor() if request.method == 'GET' or request.method == 'POST': room_query = """SELECT room_num FROM geodata.search_rooms_v""" cur.execute(room_query) room_nums = cur.fetchall() room_num_list = [] for x in room_nums: v = x[0] room_num_list.append(v) try: return Response(room_num_list) except: logger.error("error exporting to json model: " + str(room_num_list)) logger.error(traceback.format_exc()) return Response({'error': 'either no JSON or no key params in your JSON'}) -
我们最终的
base.html模板已经完成,包含从房间到房间最终路由所需的所有调料,如下所示:{% load staticfiles %} <!DOCTYPE html> <html lang="en"> <head> {% block head %} <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="Sample Map"> <meta name="author" content="Michael Diener"> <meta charset="UTF-8"> <title>{% block title %}Default Title{% endblock %}</title> <script src="img/{% static "js/jquery-1.11.2.min.js" %}"></script> <link rel="stylesheet" href="{% static "css/bootstrap.min.css" %}"> <script src="img/{% static "js/bootstrap.min.js" %}"></script> <link rel="stylesheet" href="{% static "css/ol.css" %}" type="text/css"> <link rel="stylesheet" href="{% static "css/custom-layout.css" %}" type="text/css"> <script src="img/{% static "js/ol340.js" %}"></script> {% endblock head %} </head> <body> {% block body %} {% block nav %} <nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="http://www.indrz.com" target="_blank">Indoor Project</a> </div> <div id="navbar" class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li><a href="#about" target="_blank">About</a></li> <li><a href="https://github.com/mdiener21/" target="_blank">Contact</a></li> </ul> </div><!--/.nav-collapse --> </div> </nav> {% endblock nav %} {% endblock body %} </body> </html> -
现在,我们将创建最终的
route-map.html模板及其相关的 JavaScript,如下所示:{% extends "base.html" %} {% load staticfiles %} {% block title %}Simple route map{% endblock %} {% block head %} {{ block.super }} <script src="img/{% static "js/bloodhound.min.js" %}"></script> <script src="img/{% static "js/typeahead.bundle.min.js" %}"></script> {% endblock head %} {% block body %} {{ block.super }} <div class="container-fluid"> <div class="row"> <div class="col-md-2"> <div id="directions" class="directions"> <form id="submitForm"> <div id="rooms-prefetch" class="form-group"> <label for="route-to">Route From:</label> <input type="text" class="typeahead form-control" id="route-to" placeholder="Enter Room Number"> </div> <div id="rooms-prefetch" class="form-group"> <label for="route-from">Route To:</label> <input type="text" class="typeahead form-control" id="route-from" placeholder="Enter Room Number"> </div> <div class="radio"> <label> <input type="radio" name="typeRoute" id="routeTypeStandard" value="0" checked> Standard Route </label> </div> <div class="radio"> <label> <input type="radio" name="typeRoute" id="routeTypeBarrierFree" value="1"> Barrier Free Route </label> </div> <button id="enterRoute" type="submit" class="btn btn-default">Go !</button> <br> </form> </div> </div> <div class="col-md-10"> <div id="map" class="map"></div> </div> </div> </div> <script> {% include 'routing.js' %} </script> <script> var roomNums = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.whitespace, queryTokenizer: Bloodhound.tokenizers.whitespace, prefetch: 'http://localhost:8000/api/rooms/?format=json' }); // passing in `null` for the `options` arguments will result in the default // options being used $('#rooms-prefetch .typeahead').typeahead(null, { name: 'countries', limit: 100, source: roomNums }); $( "#submitForm" ).submit(function( event ) { {# alert( "Handler for .submit() called." );#} var startNum = $('#route-from').val(); var endNum = $('#route-to').val(); var rType = $( "input:radio[name=typeRoute]:checked" ).val(); addRoute(startNum, endNum, rType); event.preventDefault(); }); </script> {% endblock body %} -
我们的
maps/templates/routing.js包含调用路由 API 所需的函数,如下所示:var url_base = "/api/directions/"; var start_coord = "1587848.414,5879564.080,2"; var end_coord = "1588005.547,5879736.039,2"; var sel_Val = $( "input:radio[name=typeRoute]:checked" ).val(); var geojs_url = url_base + start_coord + "&" + end_coord + "&" + sel_Val + '/?format=json'; // uncomment this code if you want to reactivate // the quick static demo switcher //$( ".radio" ).change(function() { // map.getLayers().pop(); // var sel_Val2 = $( "input:radio[name=typeRoute]:checked" ).val(); // var routeUrl = '/api/directions/1587848.414,5879564.080,2&1588005.547,5879736.039,2&' + sel_Val2 + '/?format=json'; // // map.getLayers().push(new ol.layer.Vector({ // source: new ol.source.GeoJSON({url: routeUrl}), // style: new ol.style.Style({ // stroke: new ol.style.Stroke({ // color: 'blue', // width: 4 // }) // }), // title: "Route", // name: "Route" // })); // //}); var vectorLayer = new ol.layer.Vector({ source: new ol.source.GeoJSON({url: geojs_url}), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'red', width: 4 }) }), title: "Route", name: "Route" }); var map = new ol.Map({ layers: [ new ol.layer.Tile({ source: new ol.source.OSM() }) , vectorLayer ], target: 'map', controls: ol.control.defaults({ attributionOptions: /** @type {olx.control.AttributionOptions} */ ({ collapsible: false }) }), view: new ol.View({ center: [1587927.09817072,5879650.90059265], zoom: 18 }) }); function addRoute(fromNumber, toNumber, routeType) { map.getLayers().pop(); console.log("addRoute big"+ String(fromNumber)); var baseUrl = 'http://localhost:8000/api/directions/'; var geoJsonUrl = baseUrl + fromNumber + '&' + toNumber + '&' + routeType +'/?format=json'; console.log("final url " + geoJsonUrl); map.getLayers().push(new ol.layer.Vector({ source: new ol.source.GeoJSON({url: geoJsonUrl}), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'purple', width: 4 }) }), title: "Route", name: "Route" })); } -
现在,输入
1并查看自动完成功能;然后,选择 路由到 并输入2以查看二楼选项。最后,点击 GO! 并见证魔法发生:![如何操作...]()
它是如何工作的...
每一步的内部工作原理都得到了展示,因此我们将逐一过一遍。我们首先从导入新的房间数据集开始,作为我们新的室内路由工具的基本起始点和结束点。我们使用一些新的 URL 布局我们的 API 的顶层结构,定义了我们如何调用新的路由服务,并定义了这些变量。我们的正则表达式处理了在 URL 中传入的正确数据类型,没有任何异常。
这些 URL 模式随后被 api/views.py 使用,以接受传入的房间号和路由类型,生成我们的新路由。这一生成过程被拆分成几个函数以提高可用性。get_room_centroid_node() 函数是必要的,这样我们就可以找到房间的中心点,然后在网络上找到下一个最近的节点。我们也可以简单地使用多边形几何来找到最近的节点,但如果房间很大且入口彼此靠近,这可能会导致歧义。centroid 方法更加可靠,并且不会增加太多开销。
run_route() 函数实际上运行了我们之前创建的 find_closest_network_node() 函数,使得它们能够很好地协同工作。run_route 函数随后根据传入的起始节点 ID、结束节点 ID 和路由类型生成我们的 GeoJSON 结果。
route_room_to_room() 函数很小,因为其他函数已经完成了大部分工作。它只是输入由我们的 API 调用调用的 URL 参数,如 http://localhost:8000/api/directions/10010&20043&0 中所见。第 6 步之后的最终步骤是用户界面。我们需要向用户提供一个可供路由的房间列表。/api/rooms URL 正好提供了这个功能,返回一个包含房间号的 JSON 数组。输入字段是带有 Twitter Typeahead.js 和 Bloodhound.js 的 Bootstrap 输入,用于预取远程数据。作为用户,你只需输入一个数字,然后就会出现一个列表。关于 JavaScript 方面的更详细说明超出了本书的范围,但幸运的是,这些内容被保持在最低限度。
总而言之,你现在拥有了一个功能齐全的室内地图网络应用程序,它包含一组基本的室内 3D 路由功能,你可以随时扩展。
附录 A. 其他地理空间 Python 库
我们已经介绍了很多库和示例,但我们并没有涵盖所有内容。这个附录旨在快速浏览其他在 Python 地理空间工作环境中扮演特殊角色的库。这个列表绝对不完整,我在写作时还没有有幸与所有这些库合作。
列表是一个进一步阅读和实验的资源,希望它能为你解决特定问题提供正确的方向。每个库的描述都以官方库名称开头,后面跟着简短描述和网页链接:
| 库名称 | 描述 | 网站 |
|---|---|---|
| Rtree | 这是一个libspatialindex的 Python 包装器,提供了高级空间索引功能 |
toblerity.org/rtree |
| rasterio | 这是一个 Mapbox 创建工具,旨在以更简单的方式处理栅格数据 | github.com/mapbox/rasterio |
| Fiona | 这专注于以标准的 Python I/O 风格读取和写入数据 | toblerity.org/fiona |
| geopy | 这有助于 Python 中的地理编码 | www.geopy.org |
| PyQGIS | 这是 QGIS(以前称为 Quantum GIS)的 Python 接口,有助于扩展 QGIS 等 | pythongisbook.com |
| GeoPandas | 这是一个 pandas 库的扩展,用于处理地理空间数据库 | geopandas.org/ |
| MapFish | 这是 Python 的地理空间 Web 框架 | mapfish.org |
| PyWPS | 这个客户端与各种开放地理空间标准服务进行交互 | pywps.wald.intevation.org |
| pycsw | 这提供了一个元数据目录接口 | pycsw.org |
| GeoNode | 这为 Web 提供 Python 地理空间内容管理,基于 Django Web 框架和 GeoServer 构建 | geonode.org |
| mapnik | 这是一个用于创建网络瓦片缓存的地图可视化库 | mapnik.org |
| cartopy | 这是在 Python-shapely 中简化地图制作的工具 | scitools.org.uk/cartopy |
| Kartograph | 这创建 SVG 地图或网络地图 | kartograph.org |
| basemap | 这是 matplotlib 与 descartes 结合的扩展 | matplotlib.org/basemap |
| SciPy | 这是一个用于科学数据分析的 Python 库集合,可以捆绑安装或作为单独的安装提供 | www.scipy.org |
| GeoAlchemy | 这是一个空间扩展到 SQLAlchemy,与空间数据库 PostGIS 一起工作 | geoalchemy.org |
| pyspatialite | 这有助于您处理地理空间数据的 spatialite 数据库 | pypi.python.org/pypi/pyspatialite |
| gpxpy | 这有助于在 Python 友好的格式中处理标准 GPX 格式的 GPS 数据 | www.trackprofiler.com/gpxpy/index.html |
| ShaPy | 这是一个没有依赖项的 Shapely 的纯 Python 版本 | github.com/karimbahgat/Shapy |
| pyshp | 这使用纯 Python 读取和写入 Shapefiles | github.com/GeospatialPython/pyshp |
| TileCache | 这是一个 WMS-C(目录)瓦片映射服务器(TMS)的实现 | tilecache.org |
| TileStache | 这是一个基于 Python 的服务器应用程序,可以根据渲染的地理数据提供地图瓦片 | www.tilestache.org |
| FeatureServer | 这是一个 RESTful 功能服务,通过 HTTP 帮助轻松获取、编辑、删除和更新网络上的功能 | featureserver.org |
| GeoScript | 这是一个 Python 的实现,为其他脚本语言和 Python 提供空间分析功能;Python 就是其中之一;它与 Shapely 类似 | www.geoscript.org |
| karta | 这是一个地理分析的瑞士军刀 | ironicmtn.com/karta |
附录 B. 地图图标库
寻找完美的地图图标集是困难的。以下列表提供了一些较好的地图符号,供您的网络地图应用程序使用:
| Library name | 描述 | 网站 |
|---|---|---|
| map-icons | 这是一个与 Google Maps API 和 Google Places API 一起使用的图标字体,使用 SVG 标记和图标标签 | map-icons.com/ |
| Maki | 这为网络制图创建 Mapbox 像素完美的图标 | www.mapbox.com/maki |
| map icons | 这专注于以标准的 Python IO 风格读取和写入数据 | mapicons.mapsmarker.com/ |
| Integration and Application Network | 这创建了 2782 个自定义矢量符号 | ian.umces.edu/symbols/ |
| OSM icons | 这是一个用于 OSM 地图的免费 SVG 图标集 | osm-icons.org/wiki/Icons |
| OSGeo 地图符号集 | 这是一个地图图标链接的集合 | wiki.osgeo.org/wiki/OSGeo_map_symbol_set |
| SJJB collection | 这是一个 PD/CC0 SVG 地图图标和生成 PNG 图标的工具集 | github.com/twain47/Open-SVG-Map-Icons 和 www.sjjb.co.uk/mapicons/contactsheet |
| OSM map-icons | 这创建了一个 OpenStreetMap 图标集 | github.com/openstreetmap/map-icons/tree/master/svg |
| opensreetmap-carto | 安迪·艾伦创建了这些 PNG 格式的地图图标集 | github.com/gravitystorm/openstreetmap-carto/tree/master/symbols |















浙公网安备 33010602011771号