GIS底層 | Shapefile是怎麼設計出來的
轉載于微信公衆号:GIS底層直通
手寫地理資訊元件系列 第5篇
Shapefile的資料結構與讀取
難度指數:★★★☆☆
前情回顧
前文中,我們基于螢幕坐标變換的知識,推導出了地圖縮放的計算等式。通過動态的計算地圖視窗的角點坐标,實作了地圖的4方向平移和縮放。
地圖元件經過多次的增強和改造,已經從第一篇的GIS小玩具,初步成長為一個可用的地圖程式。我一直認同資料是程式的血液。沒有資料,程式就失去了意義。是以今天就來挖一挖GIS系統中被我們司空見慣,最具代表性的資料格式Shapefile,究竟來自于一種怎樣的設計
Shapefile的結構定義
Shapefile是ESRI(美國環境系統研究所公司)定義推出的一種矢量資料存儲格式。按照其官方介紹,一份空間資料描述一般包括三個檔案:
- .shp 矢量資料檔案,用于存儲矢量資料坐标
- .dbf 屬性表檔案,用于存儲屬性資料
- .shx 空間索引檔案,用于存儲幾何圖形在shp檔案中的位置索引
今天我們着重挖掘.shp矢量資料的結構,看看幾何圖形在檔案中是怎麼存的。
根據ESRI的Shapefile技術白皮書介紹,shp檔案是由一個檔案頭,多個記錄頭和多個記錄内容組成。
檔案頭是一個位于檔案首端,固定長度(100位元組)的一段連續位元組序列。主要用來存儲該shp檔案的描述資訊,檔案大小,版本等。
記錄頭用于存儲每個圖形記錄的描述資訊。
記錄内容主要用來存儲圖形坐标。一個記錄頭 + 一個記錄内容,構成一條矢量資料的記錄。
下面是shp檔案的結構:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbwxCdh1mcvZ2LcV2Zh1Wa9M3clN2byBXLzN3btg3PBRkTygzUVpFZyIlRaBTYqx2Ra5kVwUlMJ1mUwoEWVFWOHJlex0WW1hmaZBnRuNmMOxmU5V1akhXRFpVUKtGZ59GSjFjStJWYKdUTCJVbZBHaWlVN1s2VLZlalhEcyo1ToNzU0gmVP1EZXNFasd0VN50VhZHaHVGasd0YFxmbVZVO5pVdCNDW2wWbZRXMywUdO1GTqx2RjhXNpVGcKdlY0lTeMZTTINGMShUYvwlbj5yZtlmbkN3YuQnclZnbvN2Ztl2Lc9CX6MHc0RHaiojIsJye.jpg)
檔案讀取為位元組流後,就像一把尺子,尺子的每個刻度區間代表一定的意義。而所說的字段就代表每一個刻度區間。**字段(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底層直通