zoukankan      html  css  js  c++  java
  • 一步一步手写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

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

  • 相关阅读:
    1.Oracle实例和Oracle数据库(Oracle体系结构)
    04.SQL基础-->分组与分组函数
    SYSAUX表空间满的解决方法
    Linux 软件安装到 /usr,/usr/local/ 还是 /opt 目录?
    Python学习-八周五次课(12月15日)
    ELK安装
    Python学习-八周二次课(12月12日)
    Python学习-八周一次课(12月11日)
    Python学习——七周四次课(12月7日)
    Python学习-复习7次课(12月4日)
  • 原文地址:https://www.cnblogs.com/ATtuing/p/10807472.html
Copyright © 2011-2022 走看看