本文节选自作者新书《GIS基础原理与技术实践》第4章,系统讲解矢量数据开发基础,涵盖 OGR/GDAL 使用、坐标转换、拓扑判断等实战内容。

4.4 矢量数据开发基础
通过之前的介绍,相信读者已经对于GIS的矢量数据有了一个全面的认识,尽管可能不是那么深入。但是没有关系,我们可以在实际的开发过程中加深对其的认识。
4.4.1 第三方开源库OGR/GDAL
在第3章的时候,我们小试牛刀,通过第三方开源库PROJ/GDAL很简单的就实现了地理空间参考系统的相互转换。在这里同样如此,我们可以通过GDAL的OGR组件,轻松实现GIS矢量数据的基础开发。
OGR曾经是一个独立的矢量IO库,表示OpenGIS Simple Features Reference Implementation的意思,但其实OGR不完全符合OGC的Simple Feature标准规范,因此未被批准作为该规范的参考实现(当然也非常接近规范了)。从GDAL2.0开始,GDAL和OGR组件被集成在一起。
OGR/GDAL提供了非常强大的矢量读写能力,支持市面上绝大多数矢量数据格式。在第3章我们介绍GDAL的时候就提到过,GDAL是一个GIS数据抽象库。所谓数据抽象,是指无论是哪一种具体的矢量数据格式,GDAL都会将其抽象成数据集对象(Dataset),从而可以支持读取、写出和处理操作。常用的矢量数据格式如下:
- DXF/DWG:应用最广泛的几何图形数据格式,在CAD领域内用的非常多,但是缺点是缺少地理信息。
- ESRI Shapefile:GIS中最常见的矢量数据格式,几乎所有的商业和开源GIS软件都支持。除了几何信息之外,还包含空间信息和属性信息。
- GeoJSON:一种通过JSON来表述要素的矢量数据格式,因而很容易被JavaScript解析和处理,适用于Web端的轻量化应用。
- KML:Google提出的一种基于XML标准来描述地理空间信息的数据格式(包括点、线、面、多边形和模型等),并且已经被OGC认定为开放地理信息编码标准。
在GDAL中,对数据格式的支持被封装成驱动(dirver),包括上述格式在内的数据驱动要么内嵌在GDAL中,要么通过另外的第三方库来支持。当然这些我们可以暂时不用关心,直接使用本书Github主页代码仓库中的GDAL即可。
4.4.2 矢量数据的读取、处理和写入
了解了第三方库OGR/GDAL,接下来我们就通过其实现关于GIS矢量数据的基础开发。一个很容易理解的常识是,包含GIS矢量数据在内的任何数据的操作都离不开三个过程:读取、处理和写入。因此,这里我们结合前面空间坐标参考转换的实践,实现一个简单的实例:读取一个面的矢量,然后对其进行空间参考的转换,最后重新写出一个矢量。具体的实现代码如下:
cpp
#include <ogrsf_frmts.h>
#include <Eigen/Eigen>
#include <iostream>
#include <vector>
using namespace std;
using namespace Eigen;
using Point = Vector3d;
using Line = vector<Point>;
using Polygon = vector<Line>;
vector<Polygon> polygonData;
OGRSpatialReference srcFileSpatialReference;
OGRSpatialReference dstFileSpatialReference;
void OgrRing2Line(OGRLinearRing *ogrLinearRing, Line &line) {
for (int i = 0; i < ogrLinearRing->getNumPoints(); i++) {
line.emplace_back(ogrLinearRing->getX(i), ogrLinearRing->getY(i),
ogrLinearRing->getZ(i));
}
}
void OgrPolygon2Polygon(OGRPolygon *ogrPolygon, Polygon &polygon) {
//外环
Line line;
OgrRing2Line(ogrPolygon->getExteriorRing(), line);
polygon.push_back(line);
//内环
for (int ri = 0; ri < ogrPolygon->getNumInteriorRings(); ri++) {
Line line;
OgrRing2Line(ogrPolygon->getInteriorRing(ri), line);
polygon.push_back(line);
}
}
bool ReadShp() {
string srcFile = getenv("GISBasic");
srcFile = srcFile + "/../Data/Vector/multipolygons.shp";
GDALDataset *poDS = (GDALDataset *)GDALOpenEx(srcFile.c_str(), GDAL_OF_VECTOR,
NULL, NULL, NULL);
if (!poDS) {
printf("无法读取该文件,请检查数据是否存在问题!");
return false;
}
if (poDS->GetLayerCount() < 1) {
printf("该文件的层数小于1,请检查数据是否存在问题!");
return false;
}
//原始数据空间参考
char *pszWKT = nullptr;
poDS->GetLayer(0)->GetSpatialRef()->exportToWkt(&pszWKT);
srcFileSpatialReference.importFromWkt(pszWKT);
CPLFree(pszWKT);
pszWKT = nullptr;
for (int li = 0; li < poDS->GetLayerCount(); li++) {
OGRLayer *poLayer = poDS->GetLayer(li); //读取层
poLayer->ResetReading();
//输出字段名
OGRFeatureDefn *poFDefn = poLayer->GetLayerDefn();
int n = poFDefn->GetFieldCount();
for (int iField = 0; iField < n; iField++) {
OGRFieldDefn *OGRFieldDefn = poFDefn->GetFieldDefn(iField);
cout << OGRFieldDefn->GetNameRef() << endl;
}
//遍历特征
OGRFeature *poFeature = nullptr;
while ((poFeature = poLayer->GetNextFeature()) != nullptr) {
OGRGeometry *geometry = poFeature->GetGeometryRef();
OGRwkbGeometryType geometryType = geometry->getGeometryType();
switch (geometryType) {
case wkbPolygon:
case wkbPolygonM:
case wkbPolygonZM: {
OGRPolygon *ogrPolygon = dynamic_cast<OGRPolygon *>(geometry);
if (!ogrPolygon) {
continue;
}
Polygon polygon;
OgrPolygon2Polygon(ogrPolygon, polygon);
polygonData.push_back(polygon);
break;
}
case wkbMultiPolygon:
case wkbMultiPolygonM:
case wkbMultiPolygonZM: {
OGRMultiPolygon *ogrMultiPolygon =
dynamic_cast<OGRMultiPolygon *>(geometry);
if (!ogrMultiPolygon) {
continue;
}
for (int gi = 0; gi < ogrMultiPolygon->getNumGeometries(); gi++) {
OGRPolygon *ogrPolygon =
dynamic_cast<OGRPolygon *>(ogrMultiPolygon->getGeometryRef(gi));
if (!ogrPolygon) {
continue;
}
Polygon polygon;
OgrPolygon2Polygon(ogrPolygon, polygon);
polygonData.push_back(polygon);
}
break;
}
default: {
printf("未处理的特征类型\n");
break;
}
}
//输出每个字段的值
for (int iField = 0; iField < n; iField++) {
cout << poFeature->GetFieldAsString(iField) << " ";
}
cout << endl;
OGRFeature::DestroyFeature(poFeature);
}
}
GDALClose(poDS);
poDS = nullptr;
return true;
}
void Convert() {
dstFileSpatialReference.importFromEPSG(3857);
dstFileSpatialReference.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
srcFileSpatialReference.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
OGRCoordinateTransformation *src2DstTransformation =
OGRCreateCoordinateTransformation(&srcFileSpatialReference,
&dstFileSpatialReference);
if (!src2DstTransformation) {
return;
}
for (auto &polygon : polygonData) {
for (auto &line : polygon) {
for (auto &point : line) {
src2DstTransformation->Transform(1, point.data(), point.data() + 1,
point.data() + 2);
}
}
}
}
bool CreateField(OGRLayer *poLayer) {
// 字符串
OGRFieldDefn oField1("Type", OFTString);
oField1.SetWidth(8);
if (poLayer->CreateField(&oField1) != OGRERR_NONE) {
printf("Creating Name field failed.\n");
return false;
}
// 浮点数
OGRFieldDefn oField2("Area", OFTReal);
oField2.SetPrecision(3);
if (poLayer->CreateField(&oField2) != OGRERR_NONE) {
printf("Creating Name field failed.\n");
return false;
}
// 整型
OGRFieldDefn oField3("VertexCount", OFTInteger);
if (poLayer->CreateField(&oField3) != OGRERR_NONE) {
printf("Creating Name field failed.\n");
return false;
}
return true;
}
bool WriteGeoJson() {
string dstFile = getenv("GISBasic");
dstFile = dstFile + "/../Data/Out.geoJson";
//创建
GDALDriver *driver = GetGDALDriverManager()->GetDriverByName("GeoJSON");
if (!driver) {
printf("Get Driver GeoJSON Error!\n");
return false;
}
GDALDataset *dataset =
driver->Create(dstFile.c_str(), 0, 0, 0, GDT_Unknown, NULL);
OGRLayer *poLayer = dataset->CreateLayer(
"FirstLayer", &dstFileSpatialReference, wkbPolygon, NULL);
if (!CreateField(poLayer)) {
return false;
}
//创建特征
for (const auto &polygon : polygonData) {
OGRFeature ogrFeature(poLayer->GetLayerDefn());
OGRPolygon ogrPolygon;
int vertexNum = 0;
for (const auto &line : polygon) {
OGRLinearRing ogrRing;
for (const auto &point : line) {
ogrRing.addPoint(point.x(), point.y(), point.z());
vertexNum++;
}
ogrPolygon.addRing(&ogrRing);
}
ogrFeature.SetGeometry(&ogrPolygon);
ogrFeature.SetField("Type", "Polygon");
ogrFeature.SetField("Area", ogrPolygon.get_Area());
ogrFeature.SetField("VertexCount", vertexNum);
if (poLayer->CreateFeature(&ogrFeature) != OGRERR_NONE) {
printf("Failed to create feature.\n");
return false;
}
}
//释放
GDALClose(dataset);
dataset = nullptr;
return true;
}
int main() {
GDALAllRegister();
CPLSetConfigOption("GDAL_FILENAME_IS_UTF8", "NO"); //支持中文路径
CPLSetConfigOption("SHAPE_ENCODING", ""); //解决中文乱码问题
if (!ReadShp()) {
return 1;
}
Convert();
WriteGeoJson();
}
在这里,读取的矢量是一个Shapefile文件,包含的都是多边形的要素,空间参考为WGS84地理坐标系(EPSG:4326),显示如下图4.14所示:

经过空间参考转换后,空间参考为Web墨卡托投影坐标系(EPSG:3857),且生成的是一个GeoJson文件。如下图4.15所示,可以看到在空间坐标参考转换之后形状似乎"变窄"了一点,这是地图投影变换的特性决定的(参考第2章介绍的知识)。

这段代码的关键在于如下几点:
-
读取矢量时,矢量数据被读取成数据集对象GDALDataset,GDALDataset管理图层对象OGRLayer,OGRLayer管理特征类对象OGRFeature,OGRFeature则包含几何对象类OGRGeometry。OGRGeometry是一个抽象父类,有意义的是其表达具体的几何对象的子类,比如OGRPolygon代表多边形几何对象。OGRPolygon又是由1个外环和多个内环(环线:OGRLinearRing)组成。矢量就是这样一层层抽象组合表达出来的。
-
在进行数据处理也就是空间参考转换之前,我们把读取的数据放到我们自己的数据容器中:
cppusing Point = Vector3d; using Line = vector<Point>; using Polygon = vector<Line>; vector<Polygon> polygonData;真实的GIS数据处理开发要求是千变万化的,操作的往往不是读取组件(这里指OGR)定义的数据结构对象。我们可以将其读取在成自定义的数据结构中,以方便我们进行更加通用的操作,或者进行并行优化。在这里我们遍历了自定义的数据结构对象中的顶点,逐点进行空间坐标参考的转换。进行其他的数据处理也是如此,根据自己的需要操作自己的数据结构容器。
-
写出矢量时,将处理结束的自定义数据结构对象恢复成数据集对象GDALDataset(读取矢量的逆操作)。另外,需要关注的是GDAL的数据驱动GDALDriver,其是通过名称来创建对应格式的矢量文件,例如这里传入到名称是GeoJson:
cppGDALDriver *driver = GetGDALDriverManager()->GetDriverByName("GeoJSON"); if (!driver) { printf("Get Driver GeoJSON Error!\n"); return false; } -
矢量数据的属性表数据与数据库中的表结构比较类似,每个字段具有自己的定义属性,例如数据类型、长度、精度等。并且,每一个特征都对应了一行记录。因而属性表的操作还是挺复杂的,往往与业务操作相关联。这里仅仅只是输出显示了读取的属性表信息,写入了自己创建的字段与值。读取和写入的属性表数据如下图4.16、图4.17所示:


4.4.3 空间拓扑关系的判断
对矢量要素进行空间拓扑关系的判断也是GIS开发中常用的功能。在4.3.3节中介绍了OGC的Simple Feature标准规范中拓扑关系,OGR/GDAL对其做了具体的实现。任何继承了OGRGeometry的几何对象类(如OGRPoint、OGRPolygon)都可以调用标准中定义的接口(二元谓词),判断该几何对象与另外一个几何对象的拓扑关系。在如下实例中,判断了空间中某点与某多边形是否存在包含(Contains)关系:
cpp
#include <ogrsf_frmts.h>
#include <iostream>
using namespace std;
int main() {
OGRLinearRing linearRing;
linearRing.addPoint(268.28, 784.75);
linearRing.addPoint(153.98, 600.60);
linearRing.addPoint(274.63, 336.02);
linearRing.addPoint(623.88, 401.64);
linearRing.addPoint(676.80, 634.47);
linearRing.addPoint(530.75, 822.85);
linearRing.closeRings();
OGRPolygon polygon;
polygon.addRing(&linearRing);
cout << "点A是否在多边形内:";
OGRPoint pointA(407.98, 579.43);
cout << polygon.Contains(&pointA) << endl;
cout << "点B是否在多边形内:";
OGRPoint pointB(678.92, 482.07);
cout << polygon.Contains(&pointB) << endl;
}
运行的结果如下所示:
text
点A是否在多边形内:1
点B是否在多边形内:0
在4.3.3节中介绍过,包含(Contains)与内含(Within)是一对相反的关系。在这里我们当然也可以调用OGRPoint的内含(Within)接口,会返回与上述同样的结果。不止如此,其他空间拓扑关系的接口也是这样调用,读者可以根据自己的需求尝试一二,这里只是抛砖引玉。
本文节选自作者新书《GIS基础原理与技术实践》第3章。书中系统讲解 GIS 核心理论与多语言实战,适合开发者与高校师生。