一步一步手寫GIS開源項目-(1)500行代碼實現基礎GIS展示功能

1.開篇

       大學畢業工作已經兩年了,上學那會就很想研讀一份開源GIS的源碼,苦於自己知識和理解有限,而市面上也沒有什麼由淺入深講解開源gis原理的書籍,大多都是開源項目簡介以及項目的簡單應用。對於初級程序員想讀懂一個成熟的GIS開源項目的困難點主要有三點,1.開發經驗和gis原理理解不足。2.一個開源項目是一個循序漸進的過程,如果不是從項目很小的時候跟進,等項目持續更新幾年後邏輯就會變得很複雜,小白很難通過Dbug調試來理清楚整個項目的原理。3.GIS屬於小行業,國內基本沒有國人主導的開源GIS項目,這方面的文章書籍比較欠缺。所以我的想法是通過重寫一個GIS項目,還原到項目的初始狀態,由淺入深人講解GIS原理,對於GIS開發從業者不僅要知其然,更要知其所以然。瞭解GIS底層的開發技術提升自己的競爭力。

     我的文章思路,代碼上傳到我的github上面,github地址:https://github.com/HuHongYong/ATtuingMap,歡迎大家star 一下,每一篇文章對應的代碼生成released版本,方便後期找到文章對應的版本版本代碼,如下:

image

2.開源gis項目的選擇

      本次開源項目的選擇是SharpMap,選擇這個項目的原因主要有兩點,1.SharpMap是一套簡單易用的小型GIS平臺,核心代碼1萬多行,可以用於開發桌面GIS應用和簡單的BS程序。支持多種GIS數據格式,支持空間查詢,可輸出精美的地圖。可瞭解GIS核心原理。2.開發語言.net,本人工作中使用的核心開發語言,而且現在.net core 3.0開始支持winform、wpf等windows桌面開發技術的跨平臺應用的開發,當然這個講原理就不使用.net core 了,等.net core桌面開發穩定以後可以做一下跨平臺。

3.開源GIS最簡單實現

     本節500行編寫了GIS基本框架,開發環境是vs2017,使用.net framework4.5 ,實現了讀取shp點數據,並實現點數據的渲染,以及屏幕座標向空間座標轉換的基礎GIS功能。項目的核心結構如下:

微信截圖_20190502095532

1.Map  只包含一個Map類,是該GIS項目的核心類。

2.Geometries 所有點、線、面等幾何類的定義和幾何類的方法,這些類都繼承了抽象類Geometry,有利於與幾何類的擴展,移植,複用。

3.Layers 地圖的圖層類,該類的核心成員就是下面的4、5、6,構成了一個圖層對象的整體。

4.Rendering 提供了用於繪製空間數據的功能,使用c#System.Drawing.Graphics進行渲染繪製。

5.Styles  提供圖層的樣式,例如點的大小、顏色等。

6.Data 數據的讀取接口。例如shapefile文件的讀取。

7.Utilities 工具類,一些公用的方法。

4.shp點文件的解析與讀取 

       shape文件由ESRI開發,一個ESRI(Environmental Systems Research Institute)的shape文件包括一個主文件,一個索引文件,和一個dBASE表。其中主文件的後綴就是.shp。

      .shp文件包含文件頭,數據記錄兩部分。文件頭是固定的100個字節組成,結構如下,數據記錄包含着座標記錄信息是文件的數據核心。

未命名文件

       .shp文件頭解析,使用的是c# BinaryReader讀取字節流,brShapeFile.BaseStream.Seek(36, 0)這個方法是指定位到第36個字節,接着brShapeFile.ReadInt32()是指從第37個字節開始讀4個字節。

/// <summary>
/// 讀取和解析.shp文件的文件頭
/// </summary>
private void ParseHeader (){
     fsShapeFile = new FileStream(_Filename, FileMode.Open, FileAccess.Read);
     brShapeFile = new BinaryReader(fsShapeFile, Encoding.Unicode);
     brShapeFile.BaseStream.Seek(0, 0);
     //讀取四個字節,檢查文件頭
     if (brShapeFile.ReadInt32() != 170328064)
     {
         //文件真實的編碼是9994,
         //170328064的16進製爲0x0a27,交換字節順序後就是0x270a,十進制就是9994了
         throw (new ApplicationException("無效的Shapefile文件 (.shp)"));
     }
     //五個沒有被使用的int32整數
     brShapeFile.BaseStream.Seek(24, 0);
     //獲取文件長度,包括文件頭
     _FileSize = 2 * SwapByteOrder(brShapeFile.ReadInt32());
     //讀取幾何類型
     _ShapeType = (ShapeTypeEnum)brShapeFile.ReadInt32();
     //讀取數據的外包矩形
     brShapeFile.BaseStream.Seek(36, 0);
     _Envelope = new BoundingBox(brShapeFile.ReadDouble(), brShapeFile.ReadDouble(), brShapeFile.ReadDouble(),
                     brShapeFile.ReadDouble());

    //通過.shp文件獲取數據條數
     // 跳過文件頭讀取
     brShapeFile.BaseStream.Seek(100, 0);
     // 幾何數據記錄開始位置
     long offset = 100;

    //遍歷數據建立功能包含在數據文件的數量
     while (offset < _FileSize)
     {
         ++_FeatureCount;

        brShapeFile.BaseStream.Seek(offset + 4, 0); //跳過長度
         int data_length = 2 * SwapByteOrder(brShapeFile.ReadInt32());

        if ((offset + data_length) > _FileSize)
         {
             --_FeatureCount;
         }

        offset += data_length; // 添加記錄數據長度
         offset += 8; //  +添加每條數據記錄頭的大小
     }
     _OffsetOfRecord = new int[_FeatureCount];
     //brShapeFile.Close();
     //fsShapeFile.Close();
}

       讀取數據記錄,由於本次不讀取.shx索引文件,我們讀取數據生成一個索引數組,方便我們讀取數據,通過索引值來讀取對應得點數據,代碼如下:

/// <summary>
/// 生成矢量文件索引
/// </summary>
private void PopulateIndexes() {
     //記錄當前位置的指針
     long old_position = brShapeFile.BaseStream.Position;
     //跳過文件頭
     brShapeFile.BaseStream.Seek(100, 0);
     //矢量文件記錄開始位置
     long offset = 100;
     for (int x = 0; x < _FeatureCount; ++x)
     {
         _OffsetOfRecord[x] = (int)offset;

        brShapeFile.BaseStream.Seek(offset + 4, 0); //跳過的長度
         int data_length = 2 * SwapByteOrder(brShapeFile.ReadInt32());
         offset += data_length; // 添加記錄數據長度
         offset += 8; //   +添加每條數據記錄頭的大小
     }

    // 返回指針的原始位置
     brShapeFile.BaseStream.Seek(old_position, 0);

}
/// <summary>
/// 從.shp文件中讀取並解析幾何對象
/// </summary>
/// <param name="oid"></param>
/// <returns></returns>
private Geometry ReadGeometry(int oid) {
     brShapeFile.BaseStream.Seek(_OffsetOfRecord[oid] + 8, 0);
     ShapeTypeEnum type = (ShapeTypeEnum)brShapeFile.ReadInt32(); //Shape type
     if (type== ShapeTypeEnum.Null)
     {
         return null;
     }
     if (type==ShapeTypeEnum.Point)
     {
         return new Point(brShapeFile.ReadDouble(), brShapeFile.ReadDouble());
     }
     else
     {
         throw (new ApplicationException("Shapefile 文件類型 " + _ShapeType.ToString() + " 不支持"));
     }

}

5.屏幕座標與空間座標轉換

      這一節主要講一下如何全圖展現所有數據。全圖顯示存在兩種狀態

1.空間座標外包矩形寬高比(寬/高)>畫布屏幕座標寬高比(寬/高) 如下圖

test

該狀態下展示就以座標點左右方向佔滿整個寬度,真實座標點轉爲屏幕座標點的函數。

/// <summary>
/// 空間座標轉屏幕座標
/// </summary>
/// <param name="p"></param>
/// <param name="map"></param>
/// <returns></returns>
public static PointF WorldtoMap(Point p, Map map)
{
     PointF result = new System.Drawing.Point();

    //在該種情況下,求出的height值爲,除去上下空白座標的高度,如上圖所示
     double height = (map.Zoom * map.Size.Height) / map.Size.Width;
     double left = map.Center.X - map.Zoom * 0.5;
     double top = map.Center.Y + height * 0.5 * map.PixelAspectRatio;
     result.X = (float)((p.X - left) / map.PixelWidth);
     result.Y = (float)((top - p.Y) / map.PixelHeight);
     return result;
}

2.空間座標外包矩形寬高比(寬/高)<畫布屏幕座標寬高比(寬/高) 如下圖

test1

該狀態下展示就以座標點左右方向佔滿整個寬度,真實座標點轉爲屏幕座標點的函數。

/// <summary>
/// 空間座標轉屏幕座標
/// </summary>
/// <param name="p"></param>
/// <param name="map"></param>
/// <returns></returns>
public static PointF WorldtoMap(Point p, Map map)
{
    PointF result = new System.Drawing.Point();

    //在該種情況下,求出的height值爲,整個屏幕高度,如上圖所示
     double height = (map.Zoom * map.Size.Height) / map.Size.Width;
     double left = map.Center.X - map.Zoom * 0.5;
     double top = map.Center.Y + height * 0.5 * map.PixelAspectRatio;
     result.X = (float)((p.X - left) / map.PixelWidth);
     result.Y = (float)((top - p.Y) / map.PixelHeight);
     return result;
}

6.繪製點座標

使用c#System.Drawing.Graphics進行渲染繪製點。

      /// <summary>
       /// 在地圖上繪製點
       /// </summary>
       public static void DrawPoint(Graphics g, Point point, Brush b, float size,  Map map) {
           if (point == null)
               return;
           PointF pp = Transform.WorldtoMap(point, map);
           Matrix startingTransform = g.Transform;
           float width = size;
           float height = size;
           g.FillEllipse(b, (int)pp.X - width / 2,
                       (int)pp.Y - height / 2 , width, height);
       }

7總結

      第一節簡單的講了一下.shp數據的讀取,以及全圖情況下空間座標與屏幕座標相互轉換。當然只講了核心功能,具體不明白的可以調試代碼進行自己探索。下一節主要講一下GIS平移縮放問題核心功能。

github項目地址:https://github.com/HuHongYong/ATtuingMap 

作者:ATtuing

出處:http://www.cnblogs.com/ATtuing

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章