文件型GIS数据的读写操作入门

来源:互联网 发布:时间序列预测 python 编辑:程序博客网 时间:2024/06/06 13:58

基于文件的数据管理方法

GIS 空间数据具有数据量大,访问的局部性和随机性强等特点。一般来说,其数据管理方式有文件和数据库两种。为了具备更好的并发性和安全性等优势,大型GIS 软件往往选择商业数据库作为底层数据访问平台,部署成本较高。文件型管理方式则具有速度快、使用灵活、容易实现和对运行环境要求较低等特点,比较适合小型 GIS 平台的应用特点。

         

对于以文件形式存放的数据,最简单的操作方法是一次全部读进内存,对象化成一个个要素( Feature )并利用数组或链表进行管理。但是对于较大的数据文件来说,一次全部读取需要很长的时间,并且很可能出现内存溢出。


磁盘文件实际上是二进制流( Stream )的一种存储形式,类似于内存块。因此 我们可以用随机读写的方式打开数据文件,用游标的相对偏移来定位并读写所需要的数据。对于读进内存中的数据,操作完毕后随即丢弃,这样就可以在有限的内存空间中高速操作很大的数据文件。


对于在Win32平台下的文件随机读写方式 ,我们可以利用 File Mapping 机制,以操纵内存指针( memset() memcpy() 等C标准库函数 )的方式来简化编程。Win32系统会 自动把这些操作转换为磁盘文件的读写动作,并提供缓存机制大幅度提高存取效率 (其具体用法请参考 Win32 API 手册

 

 

 

Tabby's EasyMap源代码中的CFileMapStream类包装了File Mapping的相关函数,但在实际操作过程中发现,当文件很大(超过500-600MB)时系统会出错,尚未找到原因。因此以同样的接口格式提供了CSimpleFileStream类,作为不使用File Mapping机制的随机文件读写替代方案

 

 

空间要素的组织方式

我们通常把具有相同几何类型、相同属性结构的要素组成要素类( FeatureClass ),一种比较简单的实现方法是用两个文件(数据文件和索引文件)来完成对每个要素类的存储。每个要素以流的形式顺序存放在数据文件中,而将每个要素流在数据文件中起始位置的偏移量(以及其它的索引信息)单独放在相对轻量的索引文件中。在加载某个要素类的时候,首先将其索引文件读取到内存,然后对数据文件建立内存映射。当读取某一个要素的时候,可以先从内存中的索引找到该要素的偏移量,然后就可以存取内存映射文件(数据文件)中该偏移量所对应的数据。

 

要素类的文件结构示意图

图1 要素类的文件结构示意图

 

补充一点,这里的数据文件与索引文件是分离的,而Tabby's EasyMap的.esd文件格式使用了一些技巧,在不影响功能和性能的前提下将两个文件合二为一,此处不再详述。

 

每个要素对象化后都具有 ID 、最小外接矩形( MBR )、几何信息( Geometry )、属性信息( Attribute )等内容。这些内容在数据文件中存储时,必须转化成具有特定结构的要素流,以便于用一个指针从起始位置方便的读取,典型的要素流具有以下结构:

 

要素流的内存块结构示意图

图2 要素流的内存块结构示意图

 

空间要素的更新方式

添加要素的时候,需要先记录下当前数据文件的末尾偏移量+ 1 ,作为新要素在数据文件中的偏移量,并向索引文件中添加一个节点以记录该偏移量,然后将要素流追加到数据文件末尾;删除要素的时候,为了避免移动大块的数据,我们只需要简单的删除掉索引文件中对应的节点;同样,修改要素的时候,并不修改数据文件中原有的要素内容,而将新内容追加在数据文件的末尾,然后修改索引文件中对应的节点偏移量即可。


这种增量追加处理方式会带来一个问题,即任何编辑操作都增加了数据文件的长度。准确的说,在执行了多次删除和修改操作后,数据文件中就会有大量冗余信息,如果想去掉这些冗余信息就需要花额外的时间对数据文件进行压缩处理。但是,这种增量追加处理方式实现起来简单、高效,并且有一个很大的优点:由于每一次修改结果都被记录了下来,因此可以很容易的实现 Undo Redo 操作。对于以浏览、查询为主的简单应用来说,这是比较合适的解决方案。

 

Tabby's EasyMap源代码中的CSlimLayer、CSlimFeature类实现了以上内容。

 

对于一个实用的空间数据引擎,其索引文件中光保存每个要素的偏移量是不够的,往往还需要保存空间索引信息,稍后讨论空间索引技术。

 

 

空间查询与空间索引

空间查询( Spatial Query )是获取空间数据的手段,所以查询效率直接体现了空间数据引擎的优劣。为了提高查询引擎在大范围、大数据量环境下的工作效率,我们通常会面向二维逻辑空间引入空间索引( Spatial Index )机制,并将查询分为两个级别:第一级是高速的粗略查询,第二级是相对耗时的精确查询。

 

CellQuadTree 空间索引算法

空间索引的算法有很多种,比较流行的有四叉树算法、 R 树算法等。这些算法各有优劣,但其目的都是保证无论面对多大的数据量或空间范围,依旧能维持较好的局部查询性能。


固定格网划分的四叉树( CellQuadTree )算法具有实现简单、查询速度快、索引结构稳固等特点,缺点是必须事先依靠经验确定四叉树的范围和级数。 CellQuadTree 算法的 基本思想是将二维空间的整个有效矩形区域递规四分成小的矩形子区域,每个矩形子区域构成四叉树的节点:

 

当n=2时四叉树的结构示意图

图3 当 n=2 时四叉树的结构示意图

 

n CellQuadTree 的叶结点所对应的逻辑空间将形成一个 2n*2n 的网格:

 

四叉树节点与空间区域的对应关系

图4 四叉树节点与空间区域的对应关系

 

补充一点,四叉树节点编号的编码方法有很多, 对于特殊的存储体系结构通常需要特殊的编码方式(例如用于SQL查询的 线性可排序四叉树 )。 然而 在基本的CellQuadTree算法中, 节点编号 并没有实际意义,因此这里不作展开讨论。


四叉树的每个节点都记录了一组空间要素(的 ID ),通常用一个数组 (vector) 或列表 (list) 来实现,此外还要记录其 4 个子节点的指针:

class CQuadNode

    {

        CQuadNode* pLeaf1;       // 4 个成员用于记录其四个子节点

        CQuadNode* pLeaf2;         // 如果为 NULL 则代表自己为叶节点

        CQuadNode* pLeaf3;

        CQuadNode* pLeaf4;

        WKSRect m_Extent;             // 本节点的空间范围

         list<SIItem> m_ItemList;     // 记录挂在本节点上的要素(的 ID

    };

创建了一个四叉树后,就可以用它来管理空间要素了。首先根据每个要素对象的空间位置计算出其 MBR ,然后从四叉树的根节点开始,逐级比较该节点是否完全包含要素的 MBR 。如果完全包含,则继续比较其四个子节点,如果遍历到了某一个节点,其四个子节点都无法完全包含要素的 MBR (或是已经遍历到了四叉树的叶节点),就将要素(的 ID )其挂在该节点上。因此,节点与要素之间的关系是 1 对多。


以图 4 中的折线为例:折线 A B MBR 分别被节点 5 6 所完全包含,由于节点 5 6 是叶节点,因此折线 A 被挂在节点 5 上,折线 B 被挂在节点 6 上;折线 C MBR 被节点 2 完全包含,因此继续遍历节点 2 的四个子节点,发现都无法完全包含,因此折线 C 被挂在节点 2 上;同样的道理,折线 D 被挂在根节点 0 上。其它的要素对象也用同样的方式挂在合适的节点上。在实际编程的时候,这个步骤可以用简单的递规算法实现。

 

四叉树与各个空间要素的关系创建完毕后就可以用于空间查询,每次查询的时候只需要从根节点开始,将查询条件(通常是点或矩形)循环和每个节点及其子节点所对应的矩形子区域做匹配,如果匹配成功就取出挂在该节点上的所有要素(的 ID )。这样一来,只要要素对象在空间位置上分布得比较均匀并且四叉树级数设计得合理,即使在要素数量和范围比较大的时候也能保证较好的查询效率。

 

Tabby's EasyMap源代码中的CSingleQuadTree类实现了四叉树空间索引的功能。

 

 

空间索引的边界问题

单个四叉树索引的主要弱点,就是有效范围必须在创建的时候指定并且不可修改。如果在编辑操作时有要素超出了范围,除非修改参数重建整个索引,否则它无法正确的被挂在索引结构中。为了有效的支持大范围编辑操作,我们可以引入一个单级格网索引,其每个网格都由一个四叉树索引组成。每个网格都具有唯一的 Grid ID Grid ID 的编码原则是能快速求出逻辑空间中的任何一个点坐标属于哪个网格(四叉树)。当进行编辑操作的时候,首先判断要素所在的位置是否已经创建了四叉树,若没有就先创建,这样就可以保证新增或修改过的要素对象总能被正确的挂到某个四叉树节点上。然而,对于那些跨越单个网格边界的要素依旧无法挂在某个四叉树中,只能单独存放在一个列表中,导致每次空间查询都必须遍历这个列表。当然,只要网格大小设定得合理,跨边界的要素通常不会太多,也就不会对查询性能造成大的冲击。


Grid ID 的编码方案有很多。为了简化编程,我们可以事先确定整个逻辑空间的中心网格,以它为原点,用 short 型整数坐标 (x y) 对所有网格编号,例如中心网格的编号是( 0, 0 ),它左边的网格编号是( -1, 0 ),它上方的网格编号是( 0, 1 …… 依次类推。网格的 Grid ID long 型)则以 y 值作为高 2 字节、以 x 值为低 2 字节组合而成,其计算公式为:

 

Grid ID(x, y)   =   (y << 16) | (0x0000ffff & x)

 

Tabby's EasyMap源代码中的CMultiQuadTree类实现了可扩展边界的格网索引功能。

 

空间查询方式

 

由于空间索引的存在,空间数据引擎的查询操作往往被设计为粗查和精查两个级别。粗查的结果集是检索空间索引的结果,它并不精确(有冗余信息),其目的是以最快的速度将要素数量限制在一个合理的范围内,以减少第二级查询的运算强度。

 

通常,在绘制要素类的时候(对检索速度要求很高,但对检索精度要求不高),就是用当前的地图显示范围(矩形)执行粗查操作,并将粗查的结果(要素对象中的几何坐标信息)通过实地- 屏幕坐标变换后绘制出来。

 

精查操作实际上是取出粗查结果集内的要素几何信息,再和查询条件进行精确的空间运算,因此查询速度相对比较慢,但输出的结果集是精确的。在实际工程项目中用到的空间查询操作一般都是精确查询。

 

 

总结

 

以上内容简单讨论了空间数据文件的存储格式、访问方式与空间索引的基本原理,这些技术构成了Tabby's EasyMap数据组织与访问的基础,希望对初学者能有所帮助。

 

 

原创粉丝点击