GIS底层 | Shapefile是怎么设计出来的
转载于微信公众号:GIS底层直通
手写地理信息组件系列 第5篇
Shapefile的数据结构与读取
难度指数:★★★☆☆
前情回顾
前文中,我们基于屏幕坐标变换的知识,推导出了地图缩放的计算等式。通过动态的计算地图窗口的角点坐标,实现了地图的4方向平移和缩放。
地图组件经过多次的增强和改造,已经从第一篇的GIS小玩具,初步成长为一个可用的地图程序。我一直认同数据是程序的血液。没有数据,程序就失去了意义。所以今天就来挖一挖GIS系统中被我们司空见惯,最具代表性的数据格式Shapefile,究竟来自于一种怎样的设计
Shapefile的结构定义
Shapefile是ESRI(美国环境系统研究所公司)定义推出的一种矢量数据存储格式。按照其官方介绍,一份空间数据描述一般包括三个文件:
-
.shp 矢量数据文件,用于存储矢量数据坐标
-
.dbf 属性表文件,用于存储属性数据
-
.shx 空间索引文件,用于存储几何图形在shp文件中的位置索引
今天我们着重挖掘.shp矢量数据的结构,看看几何图形在文件中是怎么存的。
根据ESRI的Shapefile技术白皮书介绍,shp文件是由一个文件头,多个记录头和多个记录内容组成。
文件头是一个位于文件首端,固定长度(100字节)的一段连续字节序列。主要用来存储该shp文件的描述信息,文件大小,版本等。
记录头用于存储每个图形记录的描述信息。
记录内容主要用来存储图形坐标。一个记录头 + 一个记录内容,构成一条矢量数据的记录。
下面是shp文件的结构:
文件读取为字节流后,就像一把尺子,尺子的每个刻度区间代表一定的意义。而所说的字段就代表每一个刻度区间。**字段(field)是文件中一段连续的字节序列,每个字段都有其字段位置(Potition)、字段类型(Type)、字段值(Value)和字节序(Byte Order)**规则。
-
Position: 字段的字节偏移量。用于描述字段在文件字节数组中的位置。如文件头第一个字段“FileCode”,位于第0~3位,可以称为这个字段位于第0位,长度共4个字节。每个字段的Position减去上一个字段的Position,就是这个字段的字节长度。
-
Type: 字段类型。字段存储数据的类型,与字段长度有关。如表中长度为4的字段,存储的是Integer类型,因为Int32占据的是4字节的空间。
-
Value: 对应字段类型的字段值。这里需要注意的是,字段值的单位需要明确,不能凭主观臆断。
-
Byte Order : 字节序。分为小端序(LittleEndian)和大端序(BigEndian)。端序是与硬件体系结构相关而与操作系统无关的概念,目前基本上所有x86系列的PC机都是小端序。关于端序的概念不做过多解读。**这里只需要简单记住两点:**一是,端序表示字节的排列顺序,如果一个小端序字节数组是{1,2,3,4},那么大端序数组就是它的反序排列:{4,3,2,1}。另一个是Shapefile中,管理类的字段一般是Big大端序的,其余都是小端序。读取数据时要注意做转换。
Shapefile的字段设计
文件头:-
FileCode 值通常是9994,可以用来判断文件格式是否正确。值得注意的是,FileCode是大端序,在判断shp文件是否合法时,需要转换小端序后与9994作比较。
-
File Length 顾名表示整个文件的长度,所以有些选手在读取这个字段的时候,就直接读取为字节数,然后换算为B、KB,这肯定是不对的。根据官方的介绍,这个FileLength指的是16位字的个数,其实这个表述也并不是很直观,很多介绍也都是简单复制而略过。经过寻找多方资料,整理出的这个说法还是很好理解的:FileLength不是以字节为单位,而是以“字”为单位的。两个字节称为一个“字(Word)”,而一个字节等于8位,所以也就称作“16位字”。如果需要换算为字节,将其值乘以2即可。
-
Version 版本号,一般为1000。
-
ShapeType 图形类型。例如点线面不同的shp文件类型就是在这里读取到的。目前已被定义的Shape类型在下表列出。
-
Bounding Box 代表shp的图形范围,也就是我们之前定义的Extent。至于Bounding Box的后四位字段针对存储三维等其他字段,现暂不考虑使用。
-
Unused 表示暂未用到的字段,可随着shp文件表示数据功能的增强而启用。
记录头:
-
Record Number 记录号,标识这一记录的位置。读取记录号,可以实现图形的定位。
-
Content Length 记录长度。用于标识当前记录的长度。与File Length单位一致,需要乘2换算为字节。
记录内容(点):
说到记录内容,需要注意的是,记录内容不再像文件头或记录头一样结构固定了。需要结合上边的ShapeType表而展开,每种图形的表示方法都不一样。
今天就以简单的点做一个示例,为GIS组件实现一个读取从Shapefile读取点实体的功能。
从Shapefile读取点实体
按照上边的分析,先定义一个枚举ShapeType,用于标识图形类型。
public enum ShapeType
{
NullShape = 0,//空图形
Point = 1,
Line = 3,
Polygon = 5
}
按照文件头字段列表定义,设计文件头类:
public class ShapeFileHeader
{
private Int32 fileLength = -1;
private ShapeType shapeType;
private Extent extent;
public Int32 FileLength
{
get { return fileLength; }
}
public ShapeType ShapeType
{
get { return shapeType; }
}
public Extent Extent
{
get { return extent; }
}
public ShapeFileHeader(BinaryReader br)
{
Int32 fileCode = ShapeFile.SwapByteOrder(br.ReadInt32());
if (fileCode != 9994)
{
throw (new Exception("Invalid Shapefile!"));
}
//跳过5个Unused字段
br.ReadBytes(20);
//文件长度(单位"字")
fileLength = ShapeFile.SwapByteOrder(br.ReadInt32());
//文件版本号,一般为1000
int version = br.ReadInt32();
//图形类型,点线面..
shapeType = (ShapeType)br.ReadInt32();
//图形范围
double minX = br.ReadDouble();
double minY = br.ReadDouble();
double maxX = br.ReadDouble();
double maxY = br.ReadDouble();
//涉及三维图形等字段,未使用
double minZ = br.ReadDouble();
double maxZ = br.ReadDouble();
double minM = br.ReadDouble();
double maxM = br.ReadDouble();
//左下角点
Vertex lbVtx = new Vertex(minX, minY);
//右上角点
Vertex rtVtx = new Vertex(maxX, maxY);
extent = new Extent(lbVtx, rtVtx);
}
}
字节流按序读取,读取文件头完成后,顺序读取记录头:
public class ShapeRecordHeader
{
//记录的顺序号
private int recordNumber;
//记录内容的长度(单位"字")
private int recordLength;
public int RecordNumber
{
get { return recordNumber; }
}
public int RecordLength
{
get { return recordLength; }
}
public ShapeRecordHeader(BinaryReader br)
{
recordNumber = ShapeFile.SwapByteOrder(br.ReadInt32());
recordLength = ShapeFile.SwapByteOrder(br.ReadInt32());
}
}
文件头和记录头定义后,就已经可以获取很多信息了,下面定义Shapefile类,这个类目前只设计于点实体读取。
public class ShapeFile
{
private Extent extent;
public Extent Extent
{
get { return extent; }
}
//字节顺序反转
public static Int32 SwapByteOrder(Int32 i)
{
var buffer = BitConverter.GetBytes(i);
Array.Reverse(buffer, 0, buffer.Length);
return BitConverter.ToInt32(buffer, 0);
}
public List<Point> ReadShapeFile(String filePath)
{
FileStream fs = File.Open(filePath, FileMode.Open);
BinaryReader reader = new BinaryReader(fs);
//读取文件头
ShapeFileHeader shpHeader = new ShapeFileHeader(reader);
extent = shpHeader.Extent;
List<Point> points = new List<Point>();
//字节流读取结束标志为-1
while (reader.PeekChar() != -1)
{
//读取记录头
ShapeRecordHeader recHeader = new ShapeRecordHeader(reader);
Point p = ReadPoint(reader);
points.Add(p);
}
reader.Close();
fs.Close();
return points;
}
//读取点
public Point ReadPoint(BinaryReader br)
{
//记录内容的第一个Integer代表图形的类型
ShapeType type = (ShapeType)br.ReadInt32();
if (type == ShapeType.NullShape)
return null;
double x = br.ReadDouble();
double y = br.ReadDouble();
Point p = new Point(new Vertex(x, y));
return p;
}
}
至此,shp点读取的功能代码已经成型了,现在进行最后的调用:
窗口再次添加一个按钮“打开Shp”:
现在来定义其点击事件:
private void btn_OpenShp_Click(object sender, EventArgs e)
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "(*.shp)|*.shp";
if (ofd.ShowDialog() == DialogResult.OK)
{
ShapeFile shp = new ShapeFile();
List<Point> points = shp.ReadShapeFile(ofd.FileName);
//Extent写入文本框
txtBox_minX.Text = shp.Extent.MinX.ToString();
txtBox_minY.Text = shp.Extent.MinY.ToString();
txtBox_maxX.Text = shp.Extent.MaxX.ToString();
txtBox_maxY.Text = shp.Extent.MinY.ToString();
mapExtent = shp.Extent;
map.Update(mapExtent, this.ClientRectangle);
Graphics graphics = this.CreateGraphics();
graphics.Clear(this.BackColor);
foreach (Point p in points)
{
p.Draw(graphics, map);
}
}
}
点shp文件显示
shp文件的的主要设计结构就是这样,Shapefile可以表示很多种图形类型,更多的图形类型可以按照技术文档继续展开。今天针对的是点shp的读取,线面shp文件的读取将在下篇推出。
看好关注,下期见!
转载于微信公众号:GIS底层直通