1.开篇

      
大学毕业工作已经两年了,上学那会就很想研读一份开源GIS的源码,苦于自己知识和理解有限,而市面上也没有什么由浅入深讲解开源gis原理的书籍,大多都是开源项目简介以及项目的简单应用。对于初级程序员想读懂一个成熟的GIS开源项目的困难点主要有三点,1.开发经验和gis原理理解不足。2.一个开源项目是一个循序渐进的过程,如果不是从项目很小的时候跟进,等项目持续更新几年后逻辑就会变得很复杂,小白很难通过Dbug调试来理清楚整个项目的原理。3.GIS属于小行业,国内基本没有国人主导的开源GIS项目,这方面的文章书籍比较欠缺。所以我的想法是通过重写一个GIS项目,还原到项目的初始状态,由浅入深人讲解GIS原理,对于GIS开发从业者不仅要知其然,更要知其所以然。了解GIS底层的开发技术提升自己的竞争力。

     我的文章思路,代码上传到我的github上面,github地址:https://github.com/HuHongYong/ATtuingMap
<https://github.com/HuHongYong/ATtuingMap>,欢迎大家star
一下,每一篇文章对应的代码生成released版本,方便后期找到文章对应的版本版本代码,如下:


<https://img2018.cnblogs.com/blog/821154/201905/821154-20190504081804907-335123301.png>

2.开源gis项目的选择

      本次开源项目的选择是SharpMap <https://github.com/SharpMap/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功能。项目的核心结构如下:


<https://img2018.cnblogs.com/blog/821154/201905/821154-20190504081806121-524662507.png>

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
<http://baike.baidu.com/link?url=Gy1FDhZrVwR2_mC9RAkXOUvQ3EbmmDeDKqetmIN-or3s4hnKIt3z-aAOycQZCCSF-eIijCL1urFdO1QeYFqjOa>
文件由ESRI开发,一个ESRI(Environmental Systems Research
Institute)的shape文件包括一个主文件,一个索引文件,和一个dBASE
<http://baike.baidu.com/view/684011.htm>表。其中主文件的后缀就是.shp。

      .shp文件包含文件头,数据记录两部分。文件头是固定的100个字节组成,结构如下,数据记录包含着坐标记录信息是文件的数据核心。


<https://img2018.cnblogs.com/blog/821154/201905/821154-20190504081807181-2113667695.jpg>

       .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.空间坐标外包矩形宽高比(宽/高)>画布屏幕坐标宽高比(宽/高) 如下图


<https://img2018.cnblogs.com/blog/821154/201905/821154-20190504081808208-47091991.jpg>

该状态下展示就以坐标点左右方向占满整个宽度,真实坐标点转为屏幕坐标点的函数。
/// <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.空间坐标外包矩形宽高比(宽/高)<画布屏幕坐标宽高比(宽/高) 如下图


<https://img2018.cnblogs.com/blog/821154/201905/821154-20190504081809397-734626840.jpg>

该状态下展示就以坐标点左右方向占满整个宽度,真实坐标点转为屏幕坐标点的函数。
/// <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
<https://github.com/HuHongYong/ATtuingMap> 

作者:ATtuing

出处:http://www.cnblogs.com/ATtuing <http://www.cnblogs.com/ATtuing>

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信