shapefile(.shp)空间数据格式详细说明

shapefile(.shp)空间数据格式详细说明

shp空间数据格式详细说明

  • shapefile(.shp)空间数据格式详细说明
    • 写在前面
    • 一.主文件的组织
      • 1.主文件头
      • 2.主文件记录
        • (1)记录头
        • (2)主文件记录内容
          • 1)空 shape
          • 2)Point (点)
          • 3)MultiPoint (多点)
          • 4) Arc(弧段)
          • 5)Polygon(面)
    • 二.索引文件的组织
      • 1.索引文件文件头
      • 2.索引记录
    • 三.属性文件的组织
      • 1.属性文件的文件头
      • 2.属性文件的实体信息
      • 3.读取属性文件的示例:

写在前面

  本文只做记录,参考资料均来源于网络,版权归原作者所有。
  ESRI 的 shape 文件由一个主文件(.shp)、一个索引文件(.shx)和一个 dBASE 表(.dbf)构成。主文件(.shp)是一个可变记录长度的随机文件,文件中的每个记录描述一个包含多个顶点的 shape。在索引文件(.shx)中,每个记录内容包含着与主文件中记录相对应的从主文件开始处的偏移量。dBASE 表(.dbf)中包含着与每个要素相对应的一条要素属性记录。几何数据与属性的一一对应关系是基于记录号来对应的。dBASE 文件中属性记录的顺序必须与主文件中的记录顺序相同。

一.主文件的组织

  主文件(.shp)用于记录空间坐标信息,包含一个固定长度的文件头,在文件头的后面存储着可变长度的记录。每个可变长度记录由一个固定长度的记录头和跟随其后的可变长度记录内容组成。
shapefile(.shp)空间数据格式详细说明_第1张图片

1.主文件头

  主文件头长度为 100 字节,一共有9个int型和8个double型数据。
注:表中最后4个加星号特别标示的四个数据只有当这个Shapefile文件包含Z方向坐标或者具有Measure值时才有值,否则为0.0。所谓Measure值,是用于存储需要的附加数据,可以用来记录各种数据,例如权值、道路长度等信息。
shapefile(.shp)空间数据格式详细说明_第2张图片
Shapefile文件所支持的几何类型:
注:对于一个不是记录Null Shape 类型的Shapefile文件,它所记录的空间目标的几何类型必须一致,不能在一个Shapefile文件中同时记录两种不同类型的几何目标。
shapefile(.shp)空间数据格式详细说明_第3张图片
读取坐标文件(.shp)的文件头的代码如下:

void OnReadShp(CString ShpFileName)
{
       FILE*   m_ShpFile_fp;       //****Shp文件指针
//打开坐标文件
       if((m_ShpFile_fp=fopen(ShpFileName,"rb"))==NULL)
       {
              return;
       }
       //读取坐标文件头的内容 开始
       int FileCode;
       int Unused;
       int FileLength;
       int Version;
       int ShapeType;
       double Xmin;
       double Ymin;
       double Xmax;
       double Ymax;
       double Zmin;
       double Zmax;
       double Mmin;
       double Mmax;
       fread(&FileCode,     sizeof(int),   1,m_ShpFile_fp);
       FileCode  = OnChangeByteOrder (FileCode);
       for(i=0;i<5;i++)
              fread(&Unused,sizeof(int),   1,m_ShpFile_fp);
       fread(&FileLength,   sizeof(int),   1,m_ShpFile_fp);
       FileLength      = OnChangeByteOrder (FileLength);
       fread(&Version,          sizeof(int),   1,m_ShpFile_fp);
       fread(&ShapeType,    sizeof(int),   1,m_ShpFile_fp);
       fread(&Xmin,         sizeof(double),1,m_ShpFile_fp);
       fread(&Ymin,         sizeof(double),1,m_ShpFile_fp);
       fread(&Xmax,         sizeof(double),1,m_ShpFile_fp);
       fread(&Ymax,         sizeof(double),1,m_ShpFile_fp);
       fread(&Zmin,         sizeof(double),1,m_ShpFile_fp);
       fread(&Zmax,        sizeof(double),1,m_ShpFile_fp);
       fread(&Mmin,         sizeof(double),1,m_ShpFile_fp);
       fread(&Mmax,         sizeof(double),1,m_ShpFile_fp);
       //读取坐标文件头的内容 结束
       //根据几何类型读取实体信息
       ……
}

2.主文件记录

  每个记录段分为记录头和记录内容两部分。

(1)记录头

  每个记录的记录头都储存着记录号和记录内容的长度,记录头的长度是 8 个字节,记
录号从 1 开始。字节位置是相对于记录头算起的。记录头的内容包括记录号(Record Number)和坐标记录长度(Content Length) 两个记录项。它们的位序都是big。记录号(Record Number)和坐标记录长度(Content Length) 两个记录项都是int型,并且shapefile文件中的记录号都是从1开始的。

(2)主文件记录内容

  Shape 文件的记录内容由 shape 类型和其后面的 shape 的几何数据组成。记录内容的长度取决于 part 的数量和 shape 的顶点数目。对于每个 shape 类型,我们先给出它的描述,然后是它在磁盘上的记录内容。position 是从记录内容的开始算起的。记录内容包括目标的几何类型(ShapeType)和具体的坐标记录(X、Y) ,记录内容因要素几何类型的不同其具体的内容及格式都有所不同。
具体分为以下几种:

1)空 shape

  Shape 类型为 0 表示一个没有几何类据的空的(Null)shape。每个要素类型(point、line、polygon 等等)都支持“空状态”-在同一个 shape 文件中“有点”(have point)和“无点”(null point)都是合法的。空 shape 经常作为位置标志符来使用,在创建 shape 文件时使用它,稍后就会被填充为几何数据。
shapefile(.shp)空间数据格式详细说明_第4张图片

2)Point (点)

  一个 Point 由一对双精度坐标组成,存储顺序为 X,Y。
shapefile(.shp)空间数据格式详细说明_第5张图片

Point
{
Double X // X coordinate
Double Y // Y coordinate
}

下面是读取点状目标的记录内容的代码:


OnReadPointShp(CString ShpFileName)
{
       //打开坐标文件
       ……
       //读取坐标文件头的内容 开始
       ……
       //读取点状目标的实体信息
       int RecordNumber;
       int ContentLength;
       int num   =0;
       while((fread(&RecordNumber,    sizeof(int),   1,ShpFile_fp)!=0))
       {
              num++;
              fread(&ContentLength,sizeof(int),   1,ShpFile_fp);
              RecordNumber      = OnChangeByteOrder (RecordNumber);
              ContentLength       = OnChangeByteOrder (ContentLength);
              int shapeType;
              double x;
double y;
              fread(&shapeType, sizeof(int),   1,ShpFile_fp);
              fread(&x, sizeof(double),   1,ShpFile_fp);
              fread(&y, sizeof(double),   1,ShpFile_fp);
}
}
3)MultiPoint (多点)

一个 MultiPoint 描述一组点, 具体如下:
shapefile(.shp)空间数据格式详细说明_第6张图片

MultiPoint
{
Double[4] Box // Bounding Box
Integer NumPoints // Number of Points
Point[NumPoints] Points // The Points in the set
}

Bounding box 的存储顺是 Xmin, Ymin, Xmax, Ymax.

4) Arc(弧段)

  一条弧段是一个按次序排列的顶点序列,包含一个或几个 part,一个 part 是由两个或两个以上的点连接而成的序列,Part 之间互相连接或不连接均可,Part 之间可以交叉也可以不交叉。Shapefile允许出现多个坐标完全相同的连续点,当读取文件时一定要注意这种情况,但是不允许出现某个退化的、长度为0的子线段出现。
shapefile(.shp)空间数据格式详细说明_第7张图片

Arc
{
Double[4] Box // Bounding Box
Integer NumParts // Number of Parts
Integer NumPoints // Total Number of Points
Integer[NumParts] Parts // Index to first Point in Part
Point[NumPoints] Points // Points for all parts
}

各个字段描述如下:
  Box: Arc 的 Bounding box,按照 Xmin, Ymin, Xmax, Ymax 的顺序存储。
  NumParts: Arc 中 Part 的数目。
  NumPoints: 所有 Part 的总点数。
  Parts: 长度为 NumParts 的数组,存储着每个 part(注:原文中是 polyline)的起始点在 points 数组中的索引,索引从 0 开始。
  Points: 长度为 NumPoints 的数组,按顺序存储构成 Arc 的所有 Part 的点。组成序号为 2 的 Part 的点紧接着序号为 1 的,依此类推。数组 Parts 中存储着每个 Part 起点的数组索引。在 points 数组中,各 Part 之间没有分隔符。

下面是读取线状目标的记录内容的代码:

OnReadLineShp(CString ShpFileName)
{
       //打开坐标文件
       ……
       //读取坐标文件头的内容 开始
       ……
       //读取线状目标的实体信息
       int RecordNumber;
       int ContentLength;
       int num   =0;
       while((fread(&RecordNumber,    sizeof(int),   1,ShpFile_fp)!=0))
       {
              fread(&ContentLength,sizeof(int),   1,ShpFile_fp);
              RecordNumber      = OnChangeByteOrder (RecordNumber);
              ContentLength       = OnChangeByteOrder (ContentLength);
              int shapeType;
              double Box[4];
              int NumParts;
              int NumPoints;
              int *Parts;
              fread(&shapeType,    sizeof(int),   1,ShpFile_fp);
              //读Box
              for(i=0;i<4;i++)
                     fread(Box+i,     sizeof(double),1,ShpFile_fp);
              //读NumParts和NumPoints
              fread(&NumParts,     sizeof(int),   1,ShpFile_fp);
              fread(&NumPoints,    sizeof(int),   1,ShpFile_fp);
              //读Parts和Points
              Parts       =new int[NumParts];
              for(i=0;i<NumParts;i++)
                     fread(Parts+i,   sizeof(int),   1,ShpFile_fp);
              int pointNum;
              for(i=0;i<NumParts;i++)
              {
                     if(i!=NumParts-1)
                            pointNum       =Parts[i+1]-Parts[i];
                     else
                            pointNum       =NumPoints-Parts[i];
                     double *PointsX;
                     double *PointsY;
                     
                     PointsX =new double[pointNum];
                     PointsY =new double[pointNum];
                     
                     for(j=0;j<pointNum;j++)
                     {
                            fread(PointsX+j, sizeof(double),1,ShpFile_fp);
                            fread(PointsY+j, sizeof(double),1,ShpFile_fp);
                     }
                     delete[] PointsX;
                     delete[] PointsY;
              }
              delete[] Parts;
       }
}
5)Polygon(面)

  Polygon 由一个或多个环组成。环是一个由 4 个或 4 个以上的顺序连接的点构成的闭
合的、非自相交的回路。Polygon 可以包含多个外部环。顶点的顺序或方向表明环的哪一
侧是处于 Polygon 内部的。沿着一个环的顶点顺序前进,前进方向的右侧就是这个环所在
的 Polygon。在 Polygon 中由顶点组成的洞是逆时针方向的。单一且闭合的 Polygon 的结
点顺序总是顺时针方向的。组成 Polygon 的 ring 就是 Polygon 的 Part。
shapefile(.shp)空间数据格式详细说明_第8张图片

Polygon
{
Double[4] Box // Bounding Box
Integer NumParts // Number of Parts
Integer NumPoints // Total Number of Points
Integer[NumParts] Parts // Index to First Point in Part
Point[NumPoints] Points // Points for All Parts
}

Polygon 中各个字段的描述如下:
  Box: Polygon 的封装边界,存储顺序为:Xmin, Ymin, Xmax, Ymax。
  NumParts: Polygon 中环的个数。
  NumPoints: 构成所有环的点的数目。
  Parts: 长度为 NumParts 的数组,存储着每个环的首点在 Points 数组中的索引,数
组索引从 0 开始。
  Points: 长度为 NumPoints 的数组。构成 Polygon 的每个环的点,按照首尾相连的顺序存储的。组成序号为 2 的环的点紧接着序号为 1 的环,依此类推。数组Parts 中存储着每个环的起点的数组索引。组成不同环的点之间没有分隔符。

对于一个shapefile中的多边形,它必须满足下面三个条件:
  (1)构成多边形的每个子环都必须是闭合的,即每个子环的第一个顶点跟最后一个顶点是同一个点;
  (2)每个子环在Points数组中的排列顺序并不重要,但每个子环的顶点必须按照一定的顺序连续排列;
  (3)存储在shapefile 中的多边形必须是干净的。所谓一个干净的多边形,它必须满足两点:
    1)没有自相交现象。这就要求任何一个子环不能跟其它的子环相交,共线的现 象也将被当作相交。但是允许两个子环的顶点重合;
    2) 对于一个不含岛的多边形或者是含岛的多边形的外环,它们的顶点排列顺序必须是顺时针方向;而对于内环,它的排列顺序必须是逆时针方向。所谓的“脏多边形”就是指顶点排列顺序为顺时针的内环。

典型多边形示例:
  下图中这个多边形包括一个岛,所有顶点的个数为8。NumParts等于2,NumPoints等于10。请注意内环(岛)的顶点的排列顺序是逆时针的。
shapefile(.shp)空间数据格式详细说明_第9张图片

shapefile(.shp)空间数据格式详细说明_第10张图片
下面是读取Polygon(面)的记录内容的代码:

void OnReadAreaShp(CString ShpFileName)
{
       //打开坐标文件
       ……
       //读取坐标文件头的内容 开始
       ……
       //读取面状目标的实体信息
       int RecordNumber;
       int ContentLength;
       while((fread(&RecordNumber,    sizeof(int),   1,m_ShpFile_fp)!=0))
       {
              fread(&ContentLength,sizeof(int),   1,m_ShpFile_fp);
              RecordNumber      = OnChangeByteOrder (RecordNumber);
              ContentLength       = OnChangeByteOrder (ContentLength);
              int shapeType;
              double Box[4];
              int NumParts;
              int NumPoints;
              int *Parts;
              fread(&shapeType,    sizeof(int),   1,m_ShpFile_fp);
              //读Box
              for(i=0;i<4;i++)
                     fread(Box+i,     sizeof(double),1,m_ShpFile_fp);
              //读NumParts和NumPoints
              fread(&NumParts,     sizeof(int),   1,m_ShpFile_fp);
              fread(&NumPoints,    sizeof(int),   1,m_ShpFile_fp);
              //读Parts和Points
              Parts       =new int[NumParts];
              for(i=0;i<NumParts;i++)
                     fread(Parts+i,   sizeof(int),   1,m_ShpFile_fp);
              int pointNum;
              int xx;
              int yy;
              for(i=0;i<NumParts;i++)
              {
                     if(i!=NumParts-1)
                            pointNum       =Parts[i+1]-Parts[i];
                     else
                            pointNum       =NumPoints-Parts[i];
                     
                     double *PointsX;
                     double *PointsY;
                     
                     PointsX =new double[pointNum];
                     PointsY =new double[pointNum];
                     
                     for(j=0;j<pointNum;j++)
                     {
                            fread(PointsX+j, sizeof(double),1,m_ShpFile_fp);
                            fread(PointsY+j, sizeof(double),1,m_ShpFile_fp);
                     }
                     delete[] PointsX;
                     delete[] PointsY;
              }
              delete[] Parts;
       }
}

二.索引文件的组织

  索引文件(.shx)由一个长度为 100 字节的文件头引导,后面是一系列长度为 8 字节的记录。
shapefile(.shp)空间数据格式详细说明_第11张图片

1.索引文件文件头

  索引文件文件头的组织形式与上面的主文件文件头的描述是一样的。文件头中存储的文件长度是以 16 字节表示的文件的总长度(文件头的 50 个 16 字节 加上记录个数的 4 倍)。

2.索引记录

  索引文件中的第 I 个记录存储着第 I 个记录在主文件中的偏移量和内容长度。表中显示了文件头中的各个字段以及它们的位置、值、类型、字节顺序。表中的位置是从索引文件记录的开始算起的。
shapefile(.shp)空间数据格式详细说明_第12张图片

  一个记录在主文件中的偏移量是用 16 字节表示的,它表示从主文件开始至这个记录记录头第一个字节的 word 个数。因此,主文件中的第一个记录的偏移量是 50。索引记录中存储的内容长度与主文件中记录头中存储的数值相同。

下面是一段读取索引文件的代码:

void OnReadShx(CString ShxFileName)
{
       FILE*   m_ShxFile_fp;       //****Shx文件指针
       //打开索引文件
       if((m_ShxFile_fp=fopen(ShxFileName,"rb"))==NULL)
       {
              return;
       }
       //读取索引文件头的内容 开始
       int FileCode;
       int Unused;
       int FileLength;
       int Version;
       int ShapeType;
       double Xmin;
       double Ymin;
       double Xmax;
       double Ymax;
       double Zmin;
       double Zmax;
       double Mmin;
       double Mmax;
       fread(&FileCode,     sizeof(int),   1,m_ShxFile_fp);
       FileCode  = OnChangeByteOrder (FileCode);
       for(i=0;i<5;i++)
              fread(&Unused,sizeof(int),   1,m_ShxFile_fp);
       fread(&FileLength,   sizeof(int),   1,m_ShxFile_fp);
       FileLength      = OnChangeByteOrder (FileLength);
       fread(&Version,          sizeof(int),   1,m_ShxFile_fp);
       fread(&ShapeType,    sizeof(int),   1,m_ShxFile_fp);
       fread(&Xmin,         sizeof(double),1,m_ShxFile_fp);
       fread(&Ymin,         sizeof(double),1,m_ShxFile_fp);
       fread(&Xmax,         sizeof(double),1,m_ShxFile_fp);
       fread(&Ymax,         sizeof(double),1,m_ShxFile_fp);
       fread(&Zmin,         sizeof(double),1,m_ShxFile_fp);
       fread(&Zmax,        sizeof(double),1,m_ShxFile_fp);
       fread(&Mmin,         sizeof(double),1,m_ShxFile_fp);
       fread(&Mmax,         sizeof(double),1,m_ShxFile_fp);
       //读取索引文件头的内容 结束
       
              int Offset, ContentLength;
       //读取实体信息
       while((fread(&Offset,    sizeof(int),   1, m_ShxFile_fp)!=0))
       {
              fread(&ContentLength,sizeof(int),   1, m_ShxFile_fp);
              Offset            = OnChangeByteOrder (Offset);
              ContentLength       = OnChangeByteOrder (ContentLength); 
       }
}

三.属性文件的组织

  dBASE 文件(.dbf)中包含任何需要的要素属性或可供其它表连接的属性关键字。它是标准的 DBF 格式文件,广泛应用于诸多的 Windows 和 DOS 平台上基于表格的应用程序。各类字段都可被引入到表中。
shapefile(.shp)空间数据格式详细说明_第13张图片

1.属性文件的文件头

  其中文件头部分的长度是不定长的,它主要对DBF文件作了一些总体说明,其中最主要的是对这个DBF文件的记录项的信息进行了详细地描述,比如对每个记录项的名称、数据类型、长度等信息都有具体的说明。

2.属性文件的实体信息

  实体信息部分就是一条条属性记录,每条记录都是由若干个记录项构成,因此只要依次循环读取每条记录就可以了。

3.读取属性文件的示例:

假设要读取一个名为soil的dbf文件(存储了土地利用信息),它含有8个记录项。

下面是读取这个dbf文件的代码:

void OnReadDbf(CString DbfFileName)
{
       FILE*   m_DbfFile_fp;       //****Dbf文件指针
       //打开dbf文件
       if((m_DbfFile_fp=fopen(DbfFileName,"rb"))==NULL)
       {
              return;
       }
       int i,j;
       //****读取dbf文件的文件头  开始
       BYTE version;
       fread(&version,     1,   1,m_DbfFile_fp);
       
       BYTE date[3];
       for(i=0;i<3;i++)
       {
              fread(date+i,     1,   1,m_DbfFile_fp);
       }
       
       int RecordNum;            //******
       fread(&RecordNum,         sizeof(int),   1,m_DbfFile_fp);
       short HeaderByteNum;
       fread(&HeaderByteNum,  sizeof(short), 1,m_DbfFile_fp);
       short RecordByteNum
       fread(&RecordByteNum,  sizeof(short), 1,m_DbfFile_fp);
       short Reserved1;           
       fread(&Reserved1,    sizeof(short), 1,m_DbfFile_fp);
       
       BYTE Flag4s;
       fread(&Flag4s,                 sizeof(BYTE),  1,m_DbfFile_fp);
       BYTE EncrypteFlag;
       fread(&EncrypteFlag,            sizeof(BYTE),  1,m_DbfFile_fp);
       
       for(i=0;i<3;i++)
       {
              fread(&Unused,        sizeof(int),   1,m_DbfFile_fp);
       }
       BYTE MDXFlag;
       fread(&MDXFlag,     sizeof(BYTE),  1,m_DbfFile_fp);
       
       BYTE LDriID;
       fread(&LDriID,                sizeof(BYTE),  1,m_DbfFile_fp);
       short Reserved2;
       fread(&Reserved2,    sizeof(short), 1,m_DbfFile_fp);
       BYTE name[11];
       BYTE fieldType;
       int Reserved3;
       BYTE fieldLength;
       BYTE decimalCount;
       short Reserved4;
       BYTE workID;
       short Reserved5[5];
       BYTE mDXFlag1;
       int fieldscount;
       fieldscount = (HeaderByteNum - 32) / 32;
       //读取记录项信息-共有8个记录项
       for(i=0;i< HeaderByteNum;i++)
       {
              //FieldName----11   bytes
              fread(name,    11, 1,m_DbfFile_fp);
              //FieldType----1     bytes
              fread(&fieldType,   sizeof(BYTE), 1,m_DbfFile_fp);
              //Reserved3----4     bytes
              Reserved3      =0;
              fread(&Reserved3, sizeof(int),  1,m_DbfFile_fp);
              //FieldLength--1     bytes
              fread(&fieldLength,sizeof(BYTE), 1,m_DbfFile_fp);
              //DecimalCount-1   bytes
              fread(&decimalCount,sizeof(BYTE), 1,m_DbfFile_fp);
              //Reserved4----2     bytes
              Reserved4      =0;
              fread(&Reserved4, sizeof(short),  1,m_DbfFile_fp);
              //WorkID-------1    bytes
              fread(&workID,            sizeof(BYTE), 1,m_DbfFile_fp);
              //Reserved5----10   bytes
              for(j=0;j<5;j++)
              {
                     fread(Reserved5+j,sizeof(short),  1,m_DbfFile_fp);
              }
              //MDXFlag1-----1  bytes
              fread(&mDXFlag1,       sizeof(BYTE), 1,m_DbfFile_fp);
       }
       BYTE terminator;
       fread(&terminator,        sizeof(BYTE), 1,m_DbfFile_fp);
       //读取dbf文件头结束
       double Area,Perimeter,Centroid_y,Centroid_x;
       int Soils_,Soils_id;
       CString Soil_code,suit;
       BYTE   deleteFlag;
       char media[31];
       //读取dbf文件记录  开始
       for(i=0;i<RecordNum;i++)
       {
              fread(&deleteFlag,  sizeof(BYTE), 1,m_DbfFile_fp);
              //读取 Area double
              for(j=0;j<31;j++)
                     fread(media+j, sizeof(char), 1,m_DbfFile_fp);
              Area =atof(media);
              //读取 Perimeter double
              for(j=0;j<31;j++)
                     fread(media+j, sizeof(char), 1,m_DbfFile_fp);
              Perimeter =atof(media);
              //读取 soils_  int
              for(j=0;j<31;j++)
                     strcpy(media+j,"");
              for(j=0;j<11;j++)
                     fread(media+j, sizeof(char), 1,m_DbfFile_fp);
              Soils_      =atoi(media);
              //读取 Soils_id  int
              for(j=0;j<31;j++)
                     strcpy(media+j,"");
              for(j=0;j<11;j++)
                     fread(media+j, sizeof(char), 1,m_DbfFile_fp);
              Soils_id   =atoi(media);
              //读取 soil_code string
              for(j=0;j<31;j++)
                     strcpy(media+j,"");
              for(j=0;j<3;j++)
                     fread(media+j, sizeof(char), 1,m_DbfFile_fp);
              Soil_code       =media;
              //读取 suit string
              for(j=0;j<31;j++)
                     strcpy(media+j,"");
              for(j=0;j<1;j++)
                     fread(media+j, sizeof(char), 1,m_DbfFile_fp);
              suit =media;
              //读取 Centroid_y double
              for(j=0;j<31;j++)
                     strcpy(media+j,"");
              for(j=0;j<31;j++)
                     fread(media+j, sizeof(char), 1,m_DbfFile_fp);
              Centroid_y      =atof(media);
              //读取 Centroid_x double
              for(j=0;j<31;j++)
                     strcpy(media+j,"");
              for(j=0;j<31;j++)
                     fread(media+j, sizeof(char), 1,m_DbfFile_fp);
              Centroid_x      =atof(media);
       }
       //读取dbf文件记录  结束
}

你可能感兴趣的:(三维开发,GIS数据格式,shp)