C++读取shp文件并使用画笔工具绘制出地图
shp文件格式
坐标文件 (.shp) 用于记录空间坐标信息。它由头文件和实体信息两部分构成,具体如下表。
文件头 | |
---|---|
记录头 | 记录内容 |
记录头 | 记录内容 |
记录头 | 记录内容 |
具体文件结构也不多说,见链接:shapefile文件
shp文件的数值的位序有 little 和 big 的区别,二者的区别在于它们位序的顺序相反。一个位序为 big 的数据,如果我们想得到它的真实数值,需要将它的位序转换成 little即可。转换原理非常简单,就是交换字节顺序,下面实现的在两者间进行转换的程序,代码如下:
#ifndef OnChangeByteOrder
#define OnChangeByteOrder(x) ((((x)&0xFF)<<24) \
|(((x)>>24)&0xFF) \
|(((x)&0x0000FF00)<<8) \
|(((x)&0x00FF0000)>>8) )//这个将big转化为little
#endif
//本文以下代码运行需要头文件和自行定义的结构体和数组
#include "stdafx.h"
#include "iostream"
#include "atlstr.h"
#include "vector"
using namespace std;
struct Point
{
double dx;
double dy;
};
vector<Point> Points;
vector<vector<Point>> PolyLine;
vector<vector<Point>> PolyGonS;
读取shp头文件
对于一个不是记录 Null Shape 类型的 Shapefile 文件,它所记录的空间目标的几何类型必须一致,不能在一个 Shapefile 文件中同时记录两种不同类型的几何目标。读取坐标文件( .shp )的文件头的代码如下:
int OnReadShp(FILE* m_ShpFile_fp) {
//m_ShpFile_fp为指向shp文件的指针
// 读取坐标文件头的内容开始
int FileCode;
int Unused;
int FileLength;
int Version;
int ShapeType;
double Xmin;
double Ymin;
double Xmax;
double Ymax;
double Zmin;
double Zmax;
double Mmin;
double Mmax;
fread(&FileCode, sizeof(int), 1, m_ShpFile_fp);
FileCode = OnChangeByteOrder(FileCode);
for (int i = 0;i<5;i++)
fread(&Unused, sizeof(int), 1, m_ShpFile_fp);
fread(&FileLength, sizeof(int), 1, m_ShpFile_fp);
FileLength = OnChangeByteOrder(FileLength);
fread(&Version, sizeof(int), 1, m_ShpFile_fp);
fread(&ShapeType, sizeof(int), 1, m_ShpFile_fp);
fread(&Xmin, sizeof(double), 1, m_ShpFile_fp);
fread(&Ymin, sizeof(double), 1, m_ShpFile_fp);
fread(&Xmax, sizeof(double), 1, m_ShpFile_fp);
fread(&Ymax, sizeof(double), 1, m_ShpFile_fp);
fread(&Zmin, sizeof(double), 1, m_ShpFile_fp);
fread(&Zmax, sizeof(double), 1, m_ShpFile_fp);
fread(&Mmin, sizeof(double), 1, m_ShpFile_fp);
fread(&Mmax, sizeof(double), 1, m_ShpFile_fp);
// 读取坐标文件头的内容结束
return ShapeType; //可以返回自己需要的数据
}
实体信息的内容
实体信息负责记录坐标信息,它以记录段为基本单位,每一个记录段记录一个地理实体目标的坐标信息,每个记录段分为记录头和记录内容两部分。
记录头的内容包括记录号( Record Number )和坐标记录长度 (Content Length) 两个记录项。它们的位序都是 big 。记录号( Record Number )和坐标记录长度 (Content Length) 两个记录项都是 int型,并且 shapefile 文件中的记录号都是从 1 开始的。
记录内容包括目标的几何类型( ShapeType )和具体的坐标记录 (X 、 Y) ,记录内容因要素几何类型的不同其具体的内容及格式都有所不同。下面分别介绍点状目标( Point )、线状目标( PolyLine )和面状目标( Polygon )三种几何类型的 .shp 文件的记录内容:
点状目标
shapefile 中的点状目标由一对 X 、 Y 坐标构成,坐标值为双精度型( double )。点状目标的记录内
容如下表 :
下面是 读取点状目标的记录内容的代码:
void OnReadPointShp(FILE *ShpFile_fp) {
int RecordNumber;
int ContentLength;
int num = 0;
Points.clear();
while ((fread(&RecordNumber, sizeof(int), 1, ShpFile_fp) != 0))
{
num++;
fread(&ContentLength, sizeof(int), 1, ShpFile_fp);
RecordNumber = OnChangeByteOrder(RecordNumber);
ContentLength = OnChangeByteOrder(ContentLength);
int shapeType;
Point p;
fread(&shapeType, sizeof(int), 1, ShpFile_fp);
fread(&p.dx, sizeof(double), 1, ShpFile_fp);
fread(&p.dy, sizeof(double), 1, ShpFile_fp);
Points.push_back(p);
}
return;
}
线状目标
shapefile 中的线状目标是由一系列点坐标串构成,一个线目标可能包括多个子线段,子线段之间可以是相离的,同时子线段之间也可以相交。 Shapefile 允许出现多个坐标完全相同的连续点,当读取文件时一定要注意这种情况,但是不允许出现某个退化的、长度为 0 的子线段出现。
具体的数据结构如下:
PolyLine
{
Double[4] Box // 当前线状目标的坐标范围
Integer NumParts // 当前线目标所包含的子线段的个数
Integer NumPoints // 当前线目标所包含的顶点个数
Integer[NumParts] Parts // 每个子线段的第一个坐标点在 Points 的位置
Point[NumPoints] Points // 记录所有坐标点的数组
}
这些记录项的具体含义如下:
Box 记录了当前的线目标的坐标范围,它是一个 double 型的数组,按照 Xmin 、 Ymin 、 Xmax 、Ymax 的顺序记录了坐标范围;
NumParts 记录了当前线目标所包含的子线段的个数;
NumPoints 记录了当前线目标的坐标点总数;
Parts 记录了每个子线段的第一个坐标点在坐标数组 points中的位置,以便读取数据;
Points 是用于存放当前线目标的 X 、 Y 坐标的数组。
下面是读取线状目标的记录内容的代码:
void OnReadLineShp(FILE *ShpFile_fp) {
int RecordNumber;
int ContentLength;
int num = 0;
while ((fread(&RecordNumber, sizeof(int), 1, ShpFile_fp) != 0))
{
fread(&ContentLength, sizeof(int), 1, ShpFile_fp);
RecordNumber = OnChangeByteOrder(RecordNumber);
ContentLength = OnChangeByteOrder(ContentLength);
int shapeType;
double Box[4];
int NumParts;
int NumPoints;
int *Parts;
fread(&shapeType, sizeof(int), 1, ShpFile_fp);
// 读 Box
for (int i = 0;i<4;i++)
fread(Box + i, sizeof(double), 1, ShpFile_fp);
// 读 NumParts 和NumPoints
fread(&NumParts, sizeof(int), 1, ShpFile_fp);
fread(&NumPoints, sizeof(int), 1, ShpFile_fp);
// 读 Parts 和 Points
Parts = new int[NumParts];
for (int i = 0;i<NumParts;i++)
fread(Parts + i, sizeof(int), 1, ShpFile_fp);
int pointNum;
vector<Point> ly; //储存一条折线上所有的点
for (int i = 0;i<NumParts;i++)
{
ly.clear();
if (i != NumParts - 1)
pointNum = Parts[i + 1] - Parts[i];
else
pointNum = NumPoints - Parts[i];
Point p; //暂时存点
for (int j = 0;j<pointNum;j++)
{
fread(&p.dx, sizeof(double), 1, ShpFile_fp);
fread(&p.dy, sizeof(double), 1, ShpFile_fp);
ly.push_back(p);
//temp[j].dx = PointsX[j];
//temp[j].dy = PointsY[j];
}
PolyLine.push_back(ly);
}
delete[] Parts;
}
}
面状目标
shapefile 中的面状目标是由多个子环构成,每个子环是由至少四个顶点构成的封闭的、无自相交现象的环。对于含有岛的多边形,构成它的环有内外环之分,每个环的顶点的排列顺序或者方向说明了这个环到底是内环还是外环。一个内环的顶点是按照逆时针顺序排列的;而对于外环,它的顶点排列顺序是顺时针方向。如果一个多边形只由一个环构成,那么它的顶点排列顺序肯定是顺时针方向。每条多边形记录的数据结构与线目标的数据结构完全相同。
Polygon
{
Double[4] Box // 当前面状目标的坐标范围
Integer NumParts // 当前面目标所包含的子环的个数
Integer NumPoints // 构成当前面状目标的所有顶点的个数
Integer[NumParts] Parts // 每个子环的第一个坐标点在 Points 的位置
Point[NumPoints] Points // 记录所有坐标点的数组
}
对于一个 shapefile 中的多边形,它必须满足下面三个条件:
构成多边形的每个子环都必须是闭合的,即每个子环的第一个顶点跟最后一个顶点是同一个点;每个子环在 Points 数组中的排列顺序并不重要,但每个子环的顶点必须按照一定的顺序连续排列;存储在 shapefile 中的多边形必须是干净的。所谓一个干净的多边形,它必须满足两点:
没有自相交现象。这就要求任何一个子环不能跟其它的子环相交,共线的现 象也将被当作相交。但是允许两个子环的顶点重合;
对于一个不含岛的多边形或者是含岛的多边形的外环,它们的顶点排列顺序必须是顺时针方向;而对于内环,它的排列顺序必须是逆时针方向。
下面是读取面状目标的记录内容的代码:
void OnReadAreaShp(FILE *m_ShpFile_fp) {
int RecordNumber;
int ContentLength;
while ((fread(&RecordNumber, sizeof(int), 1, m_ShpFile_fp) != 0))
{
fread(&ContentLength, sizeof(int), 1, m_ShpFile_fp);
RecordNumber = OnChangeByteOrder(RecordNumber);
ContentLength = OnChangeByteOrder(ContentLength);
int shapeType;
double Box[4];
int NumParts;
int NumPoints;
int *Parts;
fread(&shapeType, sizeof(int), 1, m_ShpFile_fp);
// 读 Box
for (int i = 0;i<4;i++)
fread(Box + i, sizeof(double), 1, m_ShpFile_fp);
// 读 NumParts 和 NumPoints
fread(&NumParts, sizeof(int), 1, m_ShpFile_fp);
fread(&NumPoints, sizeof(int), 1, m_ShpFile_fp);
// 读 Parts 和 Points
Parts = new int[NumParts];
for (int i = 0;i<NumParts;i++)
fread(Parts + i, sizeof(int), 1, m_ShpFile_fp);
int pointNum;
vector<Point> ly; //储存一条折线上所有的点
for (int i = 0;i<NumParts;i++)
{
ly.clear();
if (i != NumParts - 1)
pointNum = Parts[i + 1] - Parts[i];
else
pointNum = NumPoints - Parts[i];
Point p; //暂时存点
for (int j = 0;j<pointNum;j++)
{
fread(&p.dx, sizeof(double), 1, m_ShpFile_fp);
fread(&p.dy, sizeof(double), 1, m_ShpFile_fp);
ly.push_back(p);
}
PolyGonS.push_back(ly);
}
delete[] Parts;
}
}
最后附上mian函数名改名的调用函数名readw(),最后可以改成.h文件使用MFC调用。
int readw(CString ss)
{
Points.clear();
PolyLine.clear();
PolyGonS.clear();
FILE *p;
// 打开坐标文件
if ((p = fopen(ss, "rb")) == NULL)
{
cout << "文件打开失败" << endl;
return 0;
}
int shapetype = OnReadShp(p);
switch (shapetype) {
case 0: cout << "空" << endl;break;
case 1:
OnReadPointShp(p);
case 3:
OnReadLineShp(p);
case 5:
OnReadAreaShp(p);
}
return 0;
}
使用MFC读取数组数据并使用画笔绘图
自行新建按钮,以及输入的文本框。由于对MFC使用还不是很了解,只能使用如此拙劣的放缩+移动手法,给各位爷乐乐。以下代码不可直接运行,需要改成与自己新建的MFC所对应的按钮名,工具名等等。MFC调用按钮代码如下(需要将以上代码整合并添加至MFC,以及添加文件头):
{
// TODO: 在此添加控件通知处理程序代码
CEdit* pBoxOne;
pBoxOne = (CEdit*)GetDlgItem(IDC_EDIT1);//IDC_EDIT1名称需要改成自己需要获取的数据的工具名
CString str;
pBoxOne->GetWindowText(str);
if (str == "") str = '1';
double out = _tstof(LPCTSTR(str));
CRect rect;
GetClientRect(&rect);//获取工作区长宽
//int rx = (rect.Width() ) / 2;
//int ry = (rect.Height() ) / 2;
int rx = 0; //自行更改数值
int ry = (rect.Height());
CEdit* pBoxOne2;
pBoxOne2 = (CEdit*)GetDlgItem(IDC_EDIT2);//同上
CString str2;
pBoxOne2->GetWindowText(str2);
if (str2 == "") str2 = '0';
double x = rx + _tstof(LPCTSTR(str2));
CEdit* pBoxOne3;
pBoxOne3 = (CEdit*)GetDlgItem(IDC_EDIT3);//同上
CString str3;
pBoxOne3->GetWindowText(str3);
if (str3 == "") str3 = '0';
double y = ry + _tstof(LPCTSTR(str3));
//以上都可不用,看自己实现
for (int i = 0; i < Points.size(); i++) {
//画点
CClientDC dc(this);
dc.SetPixel(x + Points[i].dx * out, y - Points[i].dy * out, RGB(255, 0, 0));//红色
}
for (int i = 0; i < PolyLine.size();i++) {
//画线
for (int j = 0; j < PolyLine[i].size();j++) {
CClientDC dc(this);
CPen pen(PS_SOLID, 1, RGB(0, 255, 0));//绿色
dc.SelectObject(&pen);
dc.MoveTo(x + PolyLine[i][j].dx * out, y - PolyLine[i][j].dy * out);
if (j < PolyLine[i].size() - 1)
dc.LineTo(x + PolyLine[i][j + 1].dx * out, y - PolyLine[i][j + 1].dy * out);
}
}
CArray<CPoint, CPoint>*myArray;//存储坐标点
myArray = new CArray<CPoint, CPoint>;
for (int i = 0; i < PolyGonS.size();i++) {
CPoint cp;
for (int j = 0; j < PolyGonS[i].size();j++) {
//int out = 1;
cp.x = x + PolyGonS[i][j].dx * out;
cp.y = y - PolyGonS[i][j].dy * out;
myArray->Add(cp);
}
int nSize = myArray->GetSize();
CPoint *pt = new CPoint[nSize];
for (int i = 0;i<nSize;i++)
{
pt[i] = myArray->GetAt(i);
}
CClientDC dc(this);
CBrush brush(RGB(0, 0, 255)), *pOldBrush;//以lan画刷为例
pOldBrush = dc.SelectObject(&brush);
dc.Polygon(pt, nSize);
myArray->RemoveAll();
delete pt;
}
}
结果图:
Point为红色,Polyline为绿色,Polygon为蓝色。