天天看點

GIS底層 | Shapefile是怎麼設計出來的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檔案的結構:

GIS底層 | Shapefile是怎麼設計出來的GIS底層 | Shapefile是怎麼設計出來的

檔案讀取為位元組流後,就像一把尺子,尺子的每個刻度區間代表一定的意義。而所說的字段就代表每一個刻度區間。**字段(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的字段設計

檔案頭:

GIS底層 | Shapefile是怎麼設計出來的GIS底層 | 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類型在下表列出。
GIS底層 | Shapefile是怎麼設計出來的GIS底層 | Shapefile是怎麼設計出來的
  • Bounding Box 代表shp的圖形範圍,也就是我們之前定義的Extent。至于Bounding Box的後四位字段針對存儲三維等其他字段,現暫不考慮使用。
  • Unused 表示暫未用到的字段,可随着shp檔案表示資料功能的增強而啟用。

    記錄頭:

GIS底層 | Shapefile是怎麼設計出來的GIS底層 | Shapefile是怎麼設計出來的

記錄頭隻包含兩個字段,占用固定的8個位元組空間:

  • Record Number 記錄号,辨別這一記錄的位置。讀取記錄号,可以實作圖形的定位。
  • Content Length 記錄長度。用于辨別目前記錄的長度。與File Length機關一緻,需要乘2換算為位元組。

    記錄内容(點):

GIS底層 | Shapefile是怎麼設計出來的GIS底層 | Shapefile是怎麼設計出來的

說到記錄内容,需要注意的是,記錄内容不再像檔案頭或記錄頭一樣結構固定了。需要結合上邊的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”:

GIS底層 | Shapefile是怎麼設計出來的GIS底層 | Shapefile是怎麼設計出來的

現在來定義其點選事件:

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檔案顯示

GIS底層 | Shapefile是怎麼設計出來的GIS底層 | Shapefile是怎麼設計出來的

shp檔案的的主要設計結構就是這樣,Shapefile可以表示很多種圖形類型,更多的圖形類型可以按照技術文檔繼續展開。今天針對的是點shp的讀取,線面shp檔案的讀取将在下篇推出。

看好關注,下期見!

上一篇:這位同學,請回答電子地圖與紙質地圖的差別!

GIS底層 | Shapefile是怎麼設計出來的GIS底層 | Shapefile是怎麼設計出來的

轉載于微信公衆号:GIS底層直通

繼續閱讀