分享

土地报备坐标txt文件转shp遇到的坑以及该功能的 Python(Arcpy) 实现

 GIS荟 2021-09-19

前言:在各种项目中,将国土报备坐标 txt 文本转换为 shp 是很常见的,一般都是通过 ArcGIS 添加XY数据的方式来实现。但是效率较低,同时存在着导出的 shp 线段出错、到处飞线的情况。所以这里结合 Python 不仅能快速将 txt 文件转换为 shp 文件,同时还能解决遇到的各种奇怪问题...

一. 使用 Python(ArcPy) 绘制shp

1.什么是ArcPy

ArcPy 是一个安装 ArcGIS 会附带的站点包,通过 Python 实现。简言之,能通过 Python 直接调用 arcpy 执行地理数据分析、数据转换、数据管理和地图自动化以及多种客制化的需求。

ArcGIS 帮助链接:https://desktop./zh-cn/arcmap/10.3/analyze/arcpy/what-is-arcpy-.htm

需要注意的是 ArcPy 是通过 Python2 实现的,不兼容 Python3!

需要注意的是 ArcPy 是通过 Python2 实现的,不兼容 Python3!

需要注意的是 ArcPy 是通过 Python2 实现的,不兼容 Python3!

除非使用 ArcGIS pro,要花钱哦!


2.如何构造shp(面)

主要涉及类

Array 类

ArcPy 中自带的数组对象,可包含点和数组,用于构造几何对象。

官方文档:https://desktop./zh-cn/arcmap/10.3/analyze/arcpy-classes/array.htm

Polygon 类

可以使用 Polygon 类从头开始创建几何对象。接收 Array 类中数据然后使用 Polygon 类完成要素类的创建。

官方文档:https://desktop./zh-cn/arcmap/10.3/analyze/arcpy-classes/polygon.htm

构造面

点连成线,线组成了一个面。只需在 arcpy.Polygon() 类中传入一个点集就能形成一个面,点集(数组)通过 arcpy.Array() 构造传入(最后一个点不必与首个点相同,就算没有也会自动闭合)。

[[20.0, 20.0],[30.0, 20.0],
[30.0, 10.0],[20.0, 10.0]]

多个面就是多个点集。两个点集就是两个面、两个图形。

[
  [[5.0, 3.0],[3.0, 3.0],
    [3.0, 5.0],[5.0, 3.0]],
   
  [[7.0, 5.0],[5.0, 5.0],
    [5.0, 7.0],[7.0, 5.0]]
]

那么如果是环岛、孔洞等图形呢?

不过是多个点集,当某个点集在另一个点集范围内时,arcpy.Polygon() 类可以自动把重叠部分完全扣除掉,实现环岛、孔洞等。所以使用该方法绘制图形是非常方便的。

[
  [[40.0, 40.0], [50.0, 40.0], [50.0, 30.0],
  [40.0, 30.0]],
 
  [[45.0, 35.0], [48.0, 35.0], [48.0, 36.0],
  [45.0, 36.0]]
]

以下是 使用 arcpy.Array() 和 arcpy.Polygon() 实现简单的点集转 shp 代码示例:

# -*- coding:utf-8 -*-
# ---------------------------------------------------------------------------
# Author: Lcc hygnic
# Created on: 2020/10/16 15:55
# Reference:
"""
Description:Python2.7
Usage:
"""
# ---------------------------------------------------------------------------
import arcpy
import os


def draw_poly(coord_list, sr, y, x):
   """
  创建多边形
  coord_list(List):多个点组成的坐标
  sr: 投影系
  y(Int): y坐标列
  x(Int): x坐标列
  """
   parts = arcpy.Array()
   yuans = arcpy.Array()
   yuan = arcpy.Array()
   for part in coord_list:
       for pnt in part:
           if pnt:
               yuan.add(arcpy.Point(pnt[y], pnt[x]))
           else:
               # null point - we are at the start of a new ring
               yuans.add(yuan)
               yuan.removeAll()
       # we have our last ring, add it
       yuans.add(yuan)
       yuan.removeAll()
       # if we only have one ring: remove nesting
       if len(yuans) == 1:
           yuans = yuans.getObject(0)
       parts.add(yuans)
       yuans.removeAll()
   # 只有一个,单个图形
   if len(parts) == 1:
       parts = parts.getObject(0)
   return arcpy.Polygon(parts, sr)


if __name__ == '__main__':
   # 点集,将之前的示例的三个图形放到一起
   poly = [
      [[20.0, 20.0], [30.0, 20.0],
        [30.0, 10.0], [20.0, 10.0]],
       
      [[5.0, 3.0], [3.0, 3.0],
        [3.0, 5.0], [5.0, 3.0]],
      [[7.0, 5.0], [5.0, 5.0],
        [5.0, 7.0], [7.0, 5.0]],
       
      [[40.0, 40.0], [50.0, 40.0],
        [50.0, 30.0], [40.0, 30.0]],
      [[45.0, 35.0], [48.0, 35.0],
        [48.0, 36.0], [45.0, 36.0]]
  ]
   
   arcpy.env.overwriteOutput = True
   output_folder = os.getcwd()
   # 创建空白 shp
   name = "NewShp"
   # ▶注释1◀
   blank_shp = arcpy.CreateFeatureclass_management(
       output_folder, name, "Polygon", spatial_reference=None)
   # 创建、写入面
   Rows = arcpy.da.InsertCursor(blank_shp, "SHAPE@")
   f = poly
   p = draw_poly(f, sr=None, y=0, x=1)
   Rows.insertRow([p])
   write_sth = "Output path: " + os.path.join(output_folder, name)
   print write_sth
   del Rows

运行该程序会创建一个名为 NewShp.shp 的文件。

▶注释1◀:

arcpy.CreateFeatureclass_management() 创建一个空要素类,该方法可以传入参考系参数 spatial_reference。在这里没有设置参考系,如果之后需要的话,需要使用定义投影工具定义投影系或者提前就设置好投影系。


二. 土地报备坐标txt文件(坐标交换数据)

数据主要依据标准《勘测定界界址点坐标交换格式》📚

我们拿到的 TXT 文件中的数据基本按照以下标准:

附录一:《勘测定界界址点坐标交换格式》

...

坐标交换格式具有两种格式,分别如下:
文本格式
[属性描述]
格式版本号=
数据产生单位=
数据产生日期=
坐标系=
几度分带=
投影类型=
计量单位=
带号=
精度=
转换参数=X平移,Y平移,Z平移,X旋转,Y旋转,Z旋转,尺度参数
[地块坐标]
界址点数,地块面积,地块编号,地块名称,记录图形属性(点、线、面),图幅号,地块用途,地类编码,@
{点号,地块圈号,X坐标,Y坐标
      ...
      ...
点号,地块圈号,X坐标,Y坐标}
界址点数,地块面积,地块编号,地块名称,记录图形属性(点、线、面),图幅号,地块用途,地类编码,@
{点号,地块圈号,X坐标,Y坐标
      ...
      ...
点号,地块圈号,X坐标,Y坐标}
注意:
所有的逗号分隔符都必须是英文输入法状态下的逗号;地块圈号不能小于零;数据产生日期的格式为:2000-12-12;坐标系为54北京坐标系或80国家大地坐标系;投影类型为高斯克吕格或等角多圆锥;几度分带为3或6;带号、精度、转换参数、界址点数、地块面积、地块圈号,X坐标,Y坐标必须为数字型;且不能用该(9999,000,000)方式表示;地块编号、地块名称、记录图形属性(点、线、面)、图幅号、地块用途、地类编码、点号的每项里不能含有“,” 、“@”符号。

点号中不能有字母,如J01等,建议为1。(预审报盘坐标格式要求)

  ...
例子:(该例子中数据经过处理,如果使用该数据生成shp,结果可能不符合预期)
[属性描述]
格式版本号=
数据产生单位=国土资源部
数据产生日期=2020-9-10
坐标系=2000国家大地坐标系
几度分带=6
投影类型=高斯克吕格
计量单位=米
带号=35
精度=0.001
转换参数=0,0,0,0,0,0,0
[地块坐标]
8137,559.4952,1,火星乡地球村改造项目,面,,,,@
J1,1,3433398.731421,35572460.011417
J2,1,3433413.686436,35572361.307118
J3,1,3433422.659303,35572214.746216
J4,1,3433425.650143,35572095.104671
J5,1,3433431.631890,35571897.696121
J6,1,3433446.586915,35571804.9739
J7,1,3433437.613643,35571673.368316
J8,1,3433377.792817,35571493.906440
J1,1,3433398.731421,35572460.011417
18400,998.7312,1,火星乡地球村改造项目,面,,,,@
J1,1,3437490.7824,35577811.1468
J2,1,3437488.2225,35577838.46
J3,1,3437488.2225,35577864.9191
J4,1,3437503.5857,35577884.5502
J5,1,3437514.6817,35577899.9133
J6,1,3437514.6818,35577928.0804
J7,1,3437506.9998,35577940.0295
J8,1,3437506.5118,35577944.1285
J9,1,3437503.9028,35577960.8496
J1,1,3437490.7824,35577811.1468

数据结构说明:


三.转换过程中的坑

那么把土地报备坐标 txt 文件转换成 shp,主要做的就是 把TXT文件中的点正确的分成一个个点集(数组),然后传入 arcpy.Polygon() 类就行了。

然而在实际操作中,由于各种原因,有很多坑,故可能出现下面这些情况:

这种情况出现的原因通常只有一个,就是没有正确的把一个个点集分开,导致本应该闭合的一个图形没有结束,然后最后一个点又连接到了下一个图形,出现这种线“乱飞乱连”的错误情况。

而为什么没有正确把点集分开,是因为对数据的理解不全面(或者说数据有几种样式,txt 文本自己本人从相关官网下载,不存在其他人私自修改的可能),上文所说的标准是我在网上找到的,感觉说的不甚清楚且不全。

实际在实践中,出现了以下几种样式(情况):


情况一

无地块描述,如 18400,998.7312,1,火星乡地球村改造项目,面,,,,@

这种用 @ 符号结尾的段落,是每一个单独地块都有的描述符,本可以作为地块之间区分的标志物。然而实际上部分 txt 文件中直接就没有地块描述符 @ ,就如下图两个单独地块之间没有用地块描述符 @ 分开。


情况二

序号混乱。

下图的这种情况可以理解为多部件(一个地块实际是由两个单独地块组成的),就像这样

如果第二个部件在第一个部件范围内,就会变成孔洞、环岛,就像这样

从 txt 文本可以看到部件从1变成2,序号是连续的,从J1019 到 J1020(中间的 J1 是部件1的起始点,可有可无,在这里不影响)。

然后奇怪的事情来了,或者说,“另一种情况吧”。

下图也是多部件,部件从1变成2,但是在这个 txt 文本中, 序号没有紧接上面从 J6077 到 J6078,而是变成了 J1,然后 J2,重新开始了。


情况三

更多的奇怪情况。

从上面来看,由于序号规则不详、不完善或者说数据错误,非常容易混淆多部件和地块之间的编号。

更糟糕的是,我还遇到过以下几种情况,正如下面三个不同 txt 文本的截图所示:

从左到右,第一个是直接开头序号都不为1了,直接从 J9677 开始;

第二个是部件开头部不为1,为0;

第三个是部件计数跳跃,直接从1变成174,完全不知道为什么会这样!


四.解决思路 🤔

那么如何处理这些情况呢?咋一看上去非常复杂。

实际上非常简单,那就是不去纠结部件和地块序号,直接全部当成不同的地块处理(相当于有多部件的地块被打散了,这完全不影响出图,反正在一张shp文件上)。

到了这里,我已经逐渐理解一切,关键在于序号列和部件列,一旦序号列从1重新开始或者部件发生改变,数据就进行分割。

分好的数据(一个地块)是一个列表,然后一个 txt 文件中的所有数据组成一个大列表进行处理。


五.实现代码

完整实现代码(脚本形式): ✌️好耶

# -*- coding:utf-8 -*-# ---------------------------------------------------------------------------# arcgis 10.3# Author: hygnic lcc# Created on: 2020/4/8 16:33# Reference:"""Description: 将国土土地报备坐标txt文本处理生成shp文件原始数据如下:   [属性描述]   格式版本号=   数据产生单位=   数据产生日期=2077   坐标系=2000ff   几度分带=   投影类型=高斯克吕格   计量单位=米   带号=ffff   精度=ffff   转换参数=fffffff   [地块坐标]   41,0,1,0,面,0,0,,@ - J1,1,3.0, 8.0   J2,1,1.0, 8.0   J3,1,2.0, 10.0   J4,1,3.0, 8.0   555,0,1,0,面,0,0,,@   J1,1,9.0, 11.0   J2,1,9.0, 8.0   J3,1,6.0, 8.0   J4,1,6.0, 11.0   J1,2,7.0, 10.0 -   J2,2,7.0, 9.0 -   J3,2,8.0, 9.0 -   J4,2,8.0, 10.0处理为如下列表:       [           [           [3.0, 8.0],           [1.0, 8.0],           [2.0, 10.0],           [3.0, 8.0]           ]           ,           [           [9.0, 11.0],           [9.0, 8.0],           [6.0, 8.0],           [6.0, 11.0],           [9.0, 11.0]           ]           ,           [           [7.0, 10.0],           [7.0, 9.0],           [8.0, 9.0],           [8.0, 10.0],           [7.0, 10.0]           ]       ]每一个会闭合的线都组成一个单独的列表,防止因为首尾不接导致闭合面错误Usage:"""# ---------------------------------------------------------------------------import reimport arcpyimport osdef points_genarator(txt_file): """ points_genarator(txt_file)   return list txt_file:文本文件地址 将txt转换为可以使用的点集列表 """ fffs = open(txt_file, "r") input_data = fffs.readlines() # input_data.remove("\n") # 去除换行符 # input_data = [x.strip() for x in input_data] # input_data = input_data[2:] # 去除前13行 #▶注释1◀ 去除带@的行 input_data = [x.strip() for x in input_data if "@" not in x] # 去除非J行 while True:  # 去除前13行 # 如果首行字符不在下列元组中 first = ("J", "1", "0", "2", "3", "4", "5", "6", "7", "8", "9") if input_data[0][0] not in first: del input_data[0] # elif : # del input_data[0] else: break # input_data = input_data[12:] # 去除前13行 fffs.close() # 每一根闭合线单独组成一个列表 line_closed = [] # 多根线条组成面 polygon_list = [] #▶注释2◀ while input_data: row = input_data.pop(0) # ['J1', '1', '3405133.8969', '35353662.0113'] row1 = row.split(",") # 去除字母和数字组合中的字母 如 "J1" 只保留 "1" row_num = re.findall(r'[0-9]+|[a-z]+', row1[0])  # ['1'] row_num2 = row1[1]  # '1' 文本最后有空行会报错 if not line_closed:  # 为空时 line_closed.append(row1) flag1 = int(row_num[0])  # 1 第一列数字 # 第二列数字 flag2 = row1[1]  # '1' elif int(row_num[0]) > flag1: if row_num2 != flag2:  # 第二列出现不一样的,新一轮开始 flag2 = row_num2 polygon_list.append(line_closed) line_closed = [] # 未开始新一轮就接着上一个,如果开始新一轮那么这里就是第一个起始点 line_closed.append(row1) elif int(row_num[0]) == flag1:  # 新一轮开始 polygon_list.append(line_closed) line_closed = [] line_closed.append(row1) if line_closed: polygon_list.append(line_closed) return polygon_listdef draw_poly(coord_list, sr, y, x): """ 创建多边形 coord_list(List):多个点组成的坐标 sr: 投影系 y(Int): y坐标列 x(Int): x坐标列 """ parts = arcpy.Array() yuans = arcpy.Array() yuan = arcpy.Array() for part in coord_list: for pnt in part: if pnt: yuan.add(arcpy.Point(pnt[y], pnt[x])) else: # null point - we are at the start of a new ring yuans.add(yuan) yuan.removeAll() # we have our last ring, add it yuans.add(yuan) yuan.removeAll() # if we only have one ring: remove nesting if len(yuans) == 1: yuans = yuans.getObject(0) parts.add(yuans) yuans.removeAll() # 只有一个,单个图形 if len(parts) == 1: parts = parts.getObject(0) return arcpy.Polygon(parts, sr)def main(info, txt_folder, output_folder): """ 功能实现的主函数 info(List): 存放信息的列表,作为独立脚本使用时没啥用 txt_folder(Unicode/String): 包含txt文件的文件夹 output_folder(Unicode/String): 导出文件夹 """ arcpy.env.overwriteOutput = True txts = os.listdir(txt_folder) for txt in txts: if txt[-3:].lower() == "txt": txt_p = os.path.join(txt_folder, txt) f = points_genarator(txt_p) name = os.path.splitext(os.path.basename(txt_p))[0] # 创建空白shp blank_shp = arcpy.CreateFeatureclass_management( output_folder,name, "Polygon", spatial_reference=None) # create the polygons and write them Rows = arcpy.da.InsertCursor(blank_shp, "SHAPE@") #▶注释3◀ p = draw_poly(f, sr=None, y=3, x=2) Rows.insertRow([p]) del Rows write_sth = "Export succeed: " + os.path.join( output_folder, name) print write_sth info.append(write_sth) # 给新建的shp文件添加 MC 字段和值 shp_name = name + ".shp" newfield_name = "MC" fresh_layer = arcpy.mapping.Layer( os.path.join(output_folder, shp_name)) arcpy.AddField_management( fresh_layer, newfield_name, "TEXT", field_length=100) cursor2 = arcpy.da.UpdateCursor(fresh_layer,                                            newfield_name) for roww in cursor2: roww[0] = name cursor2.updateRow(roww) del cursor2if __name__ == '__main__': """脚本单独使用""" """----------------------------------------------""" """---------------------PARA---------------------""" arcpy.env.overwriteOutput = True #▶注释4◀ dir_p = ur"G:\高标准分布图\xx县\测试"  # txt文件夹 output_dir = ur"G:\高标准分布图\xx县\文件夹"# 输出文件夹 """----------------------------------------------""" """----------------------------------------------""" infos = [] main(infos, dir_p, output_dir)

points_genarator 方法将输入的 txt  文件正确分割成多个点集(数组);然后使用 draw_poly 方法将该点集绘制成图形。

▶注释1◀:

由于我们不再详细区分多部件等,所以不再需要描述符 @,况且有的 txt 文件直接都没有该描述符。所以直接将存在 @ 的行删除。

▶注释2◀:

这个循环中的代码看上去比较复杂,其作用是判断每一行的部件或者序号的顺序是否发生改变,从来将不同点集直接分开。

▶注释3◀:

文本中的第三行和第四行对应的不是 x y 坐标,而是 y x 坐标。所以 y=3, x=2。

▶注释4◀:

输入 txt 文件所在地址和输出的 shp 文件的地址,然后运行即可。

Note: 之前有的朋友问我为什么运行报错,我检查了很久,最后发现 txt 文件最后有空行。所以如果有报错的话可以先检查一下是否有空行,一般从自然资源局等官网下载下来的 txt 文件是没有的空行的。

正确分割数据得到的正确 shp 文件与错误 shp 文件的对比:


结束语

参考文章

关于构造面:https://gis./questions/14952/arcgis-10-0-arcpy-how-to-create-a-polygon-geometry-from-inner-and-outer-ring-po

使用版本

ArcGIS10.3 Python2.7

源代码及其PDF文档下载地址:

链接:https://pan.baidu.com/s/1ihBKcufLuRkAmXlJFFsDDA 提取码:3t9e

如果下载地址失效的话,可以回复 d1 获取最新下载地址。


分享GIS,不止于Python。
很难的教程、有趣的文章、好看的地图,这里都有!
荟GIS精粹,关注公众号:GIS荟 ,带你飞!

很难的文章系列:

...

不难的有趣文章系列:

  • 《从地图发现世界》——从地图,发现奇特的、美丽的、我的世界。(持续更新中!)

  • 《制图艺术》——制作不出优美的地图?进来看看是不是缺点东西(持续更新中!)

...

更多文章可以使用搜索哦

欢迎交流

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多