GIS底层 | Shapefile是怎么设计出来的

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的字段设计

文件头:

文件头共17个字段,其中包括7个大端序,10个小端序字段。这里分别解释一下这几类字段:
  • 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文件表示数据功能的增强而启用。

    记录头:

记录头只包含两个字段,占用固定的8个字节空间:
  • 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底层直通
转载于微信公众号:GIS底层直通

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值