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字节到文件尾为数据记录的内容。
关于文件头的描述,可以参见下图:
上图中,最右边一列是计算机字节序模式。笔者经过查找资料,得知不同的计算机系统,所用的字节序可能不同。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,可以参见下图:
再是文件的数据记录。
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类型的数据。如下图:
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. 整体代码
这里就不多说了,直接上代码会非常长,所以想看源码的可以跳转下载。
至于为什么要提供源码,有什么好东西不能自己一个人吃独食不是,开源它不香嘛!