C++读取.shp文件

1. shape文件格式

1.1 Shape文件初览
根据 ESRI Shapefile Technical Description 文件描述,一个完整的Shapefile最重要的三个文件,分别是 ***.shp, **.shx, .dbf。三者的意思分别是: MainFile(主文件),IndexFile(索引文件),dBaseFile(属性文件)。MainFile(.shp)主文件用来存储点线面三种基本几何类型。IndexFile(.shx)索引文件存储的是对应的主文件中,每一条记录的起始点在主文件中的位置。dBaseFile(.dbf)属性文件存储的是每个点的属性。
所有的文件都是以
二进制
方式存储,所以在使用C++进行文件读取的时候,需要在ifstream对象初始化的时候加入
ios::binary
,表明文件是以二进制方式打开。打开方式如下:

ifstream inFile("C:\\Users\\Administrator\\Desktop\\test.shp", ios::binary | ios::in )

1.2 .shp文件
每一个 .shp 文件都分为两个部分,一部分是100字节的文件头,另一部分是需要用到的数据内容,被称为数据记录。
1.2.1 .shp文件头
从文件的第1个字节起一直到100字节,是文件头的内容,在数据读取的时候却是从0-99,共100个字节。就跟数组的索引一样,从0开始,以下说的文件指针都是按照数组索引来说的。
从100字节到文件尾为数据记录的内容。如图,一个.shp文件前100字节为文件头,后面的内容都是数据记录
关于文件头的描述,可以参见下图:
图中最左边一列是字节号,比如第7行的filelength,字节号为24,那么想读取filelength的内容,就只需要将文件指针移到24就行。
上图中,最右边一列是计算机字节序模式。笔者经过查找资料,得知不同的计算机系统,所用的字节序可能不同。Big是大端序,Little是小端序。关于大端序小端序,这里不做多解释,只贴出一段字节序转换的代码,可以直接拿过去用:

template<class T>
T ByteTrans(T m)
{
    
    
    //联合体内所有的数据公用一个内存
    union n_
    {
    
    
        T n;
        char mem[sizeof(T)];
    };
    n_ big, small;
    big.n = m;
    for (int i = 0; i < sizeof(T); i++)
    {
    
    
        small.mem[i] = big.mem[sizeof(T) - i - 1];
    }
    return small.n;
}

文件头中最重要的几个数据是,ShapeType,Box。这里的ShapeType是地图类型,对应的是点线面三种几何元素。Box是当前这个.shp文件中记录的所有点的范围,包括X的最大最小值,Y的最大最小值(如果有Z和M,还有Z和M的最大最小值,但是到目前为止,笔者还没见过带有M的.shp文件)。
关于文件头中的ShapeType,可以参见下图:
点(Point)、线(Polyline)、面(Polygon)对应ShapeType的值分别为1、3、5

再是文件的数据记录。
1.2.2 .shp文件的数据记录
从.shp文件的第101个字节开始,即文件指针为100的地方开始,一直到文件尾,是.shp文件的数据记录。
每一条数据记录同样包含两个部分。第一个部分是包含两个大端序的integer的内容,被称为记录头,这个是定长的,只有8个字节,记录头以大端序存储。另一个部分是不定长,即变长的记录内容,所有的数据都以小端序存储。
记录头当中的第一个大端序的integer是当前记录的编号,从1开始。第二个大端序的integer是记录长(RecordLength)。定长的记录头一般没什么用。
变长的记录内容当中首先是地图类型(ShapeType),再是真实的点记录。对于点、线、面三种几何元素,它们在.shp文件中的存储方式不同,具体可见下表:

ShapeType 存储格式
1(点) X(double) ,Y(double)
2(线) double Box[4],int NumParts,int NumPoints ,int Parts[NumParts],double [NumPoints[X,Y]]
3(面) double Box[4],int NumParts,int NumPoints ,int Parts[NumParts],double [NumPoints[X,Y]]

对于点来说,记录内容里就只有ShapeType(value=1)和两个double值,分别是X坐标和Y坐标。
对于线和面,除了ShapeType之外,第一个double Box[4]是指当前记录中所有点的范围,存储方式为Xmin,Ymin,Xmax,Ymax。
NumParts为当前记录中线的段数(或面的环数)。
NumPoints为当前记录中的点数。
int Parts[NumParts]为存储每一段线(或环)的起点的文件指针索引。
double [NumPoints[X,Y]]为点的X、Y坐标,按顺序存储,如X1,Y1,X2,Y2,X3,Y3…一直到当前记录尾。
1.2.3 小结
.shp文件的介绍就到这里。
1.3 .shx文件
.shx文件为索引文件,包括100字节的文件头和定长的记录。
文件头和.shp文件相同。定长记录为8个字节,两个int类型的数据。如下图:
Offset为偏移量,是当前记录的第一个字节在文件中的位置,后面的ContentLength与.shp文件中每一条记录对应的ContentLength相同
1.4 .dbf文件
暂时不知道格式咋样,后面用到的时候再更新。

2. C++类设计

2.1 思路
shapefile是地图类型的文件,一张地图包含多个图层,每一个图层都包含点线面。从点线面出发,可以得知,点组成线,线组成面。同时,点可以组成图层,线和面也都可以。如图:
在这里插入图片描述
所以点线面都是对象,Layer也是对象,多个Layer组成的Map也是对象。
因此,类之间的关系就出来了。
2.2 代码实现
因为点线面三个类都有一个共同点,就是存储点的坐标,放到文件里面,还有NumParts,NumPoints,Box等共同属性。因此,在设计点线面的类之前,还需要抽象出来一个Shape类。
*[HTML]:本文所有的代码都是已经在QT上运行成功的代码,是从我自己的项目中直接拿出来复制粘贴的,所以代码运行应该是没什么问题。
2.2.1 Shape类
代码如下:

#include <vector>

struct strXY
{
    
    
    double dX;
    double dY;
};

class Shape
{
    
    
public:
    int _vParts[1000];
    int _iNumParts;
    int _iShapeType;
    double _dBox[4];
    std::vector<strXY> _vPoints;

    virtual void toShape(double, double) = 0;
};

2.2.2 Point类
代码如下:

class Point :public Shape
{
    
    
public:
    ~Point();
    void toShape(double, double);
}

2.2.3 LinePolygon类
由于Polyline和Polygon都有一定的共同属性,这些共同属性就是每一条记录的点数和范围。
代码如下:

#include "shape.h"

class LinePolygon :public Shape
{
    
    
public:
    int _iNumPnts;
    double _dBox[4];

    virtual void toShape(double, double) = 0;
};

2.2.4 Polyline类
代码如下:

#include "linepolygon.h"

class Polyline :public LinePolygon
{
    
    
public:
    void toShape(double, double);
};

2.2.5 Polygon类
代码如下:

#include "linepolygon.h"

class Polygon :public LinePolygon
{
    
    
public:
    void toShape(double, double);
};

2.2.6 Layer类
每一个文件都是一个图层,每一个图层都包含某种几何元素,当多个图层放到一起的时候,就有一些抽象属性,如下:

    int _iShpTyp;  // 地图类型
    double _dBox[4];  // 边界
    int _iRecnt;  // 记录数
    string _sfilename;  // 文件名
    vector<Shape*> _vShape;  // 记录的集合

而且图层需要跟文件打交道,所以还需要有读文件的动作,即一系列读文件的函数。如下:

    bool loadFile(string);
    void readFilehead(ifstream&);
    void toPoint(ifstream& inFile,int iIndex);
    void toPolyline(ifstream& inFile,int iIndex);
    void toPolygon(ifstream& inFile,int iIndex);
    void toPointZ(ifstream& inFile,int iIndex);
    void readRecContent(string);
    int getReCnt(string);

所以整个Layer头文件就是:

#include <fstream>
#include <vector>
#include "shape.h"
#include "point.h"
#include "polyline.h"
#include "polygon.h"

using namespace std;

class Layer
{
    
    
protected:
    void readFilehead(ifstream&);

    void toPoint(ifstream& inFile, int iIndex);
    void toPolyline(ifstream& inFile, int iIndex);
    void toPolygon(ifstream& inFile, int iIndex);
    void toPointZ(ifstream& inFile, int iIndex);

    void readRecContent(string);
    int getReCnt(string);
public:
    Layer();
    ~Layer();

    bool loadFile(string);

    Point* _pPoint;
    Polyline* _pLine;
    Polygon* _pPolygon;
    int _iShpTyp;  // 地图类型
    double _dBox[4];  // 边界
    int _iRecnt;  // 记录数
    string _sfilename;  // 文件名
    vector<Shape*> _vShape;  // 记录的集合
    double _dRecordBox[50000][4];
};

2.2.7 Map类
一个Map对象包含很多个Layer,所以像Layer包含很多个Shape一样,Map类的设计可以类比,如下:

#include <vector>
#include "layer.h"
#include "box.h"
#include <QPoint>
#include <QPolygon>

class Map
{
    
    
public:
    Map();
    Map(string);

    // 自定义函数部分
    void addFile(string);  // 向地图里添加图层
    void addFile(string*, int);  // 重载addFile函数

    // 属性
    double _dBox[4];  // 整个地图里面的Box,是所有的Box求并之后的结果
    std::vector<Layer*> _vMap;  // 用来存储所有的图层

protected:
    Box _box;
    void setBox();
};

2.2.8 小结
类的语法很简单,但是怎么把抽象的概念引入到实际的对象中,并将它们的共同点进行抽象描述,这就非常难了。
如老师所说,在进行类的设计的时候,只抓住一点,那就是简单直接。
任重而道远,C++搞坏程序员脑子。

3. 整体代码

这里就不多说了,直接上代码会非常长,所以想看源码的可以跳转下载
至于为什么要提供源码,有什么好东西不能自己一个人吃独食不是,开源它不香嘛!

鄙视敝帚自珍,拥抱开源世界,欧耶!

猜你喜欢

转载自blog.csdn.net/GeomasterYi/article/details/106434452