本帖最后由 hygnic 于 2021-4-26 19:11 编辑
Python和ArcGIS自动化制图完全指南(三):分配模板
​前言:在《指南》第二章中,我们制作了3种不同尺寸的 mxd 模板,就是为了适配不同的制图单位,比如适配自贡市的 mxd 模板就不适用于广元市(广元市面积大)。而如何让计算机给不同制图单位分配大小合适的模板就是本章需要解决的问题 。 |
​
1.模板信息标准化
根据第二章制作的 mxd 模板文件的名字生成标准化的匹配信息,该信息可以辅助程序对每一个制图单位匹配大小合适的 mxd 模板。
为什么要将模板信息标准化? 因为程序无法知道你做的 mxd 模板的大小是多少,也不知道不同的模板对应的是哪个大小。 所以我们要将模板信息存进程序中,让程序知道有这么一个模板。 你可以在程序运行前输入每一个模板的名称及其对应的大小,但是这太麻烦了,如果有很多模板呢?如果你输入错误呢? 所以我们要让电脑自己知道模板的大小,这就需要我们严格按照我们第二章的规定:mxd 模板命名按 宽x高.mxd 来。 比如:1080x700.mxd、1080x1300.mxd、1180x900.mxd(单位是毫米)。 程序会自动识别、收集宽度和高度数据,减少人工操作。
之后收集到的信息样式如下,它表示 pagesize1 这个英文字符表示大小1080和700;pagesize2 表示1080和1300;pagesize3 表示1180和900。
- {
- "pagesize1":(1080,700),
- "pagesize2":(1080,1300),
- "pagesize3":(1180,900)
- }
复制代码
​Note: 有 Python 编程基础的读者看到这里就不会陌生啦,这里的匹配信息就是 Python中 的一种数据结构:字典。 |
​
根据输入文件夹中的模板名称,来生成上述标准匹配信息的代码 C1 (../main/part1.py)如下:
- # -*- coding:utf-8 -*-
- # ----------------------------------------------------------
- # Author: LiaoChenchen
- # Created on: 2021/1/29 17:34
- # ----------------------------------------------------------
- from __future__ import absolute_import
- from __future__ import unicode_literals
- from __future__ import print_function
- from __future__ import division
- import arcpy
- import os
- from os.path import splitext as st
- from os import listdir as ld
- ​
- def size_creator(path):
- """
- 根据输入的模板文件地址生成标准格式的字典。
- :param path: {String} mxd file path
- :return:
- """
- print("请确保模板文件夹中不存在无关文件!")
- m_dict = {}
- counter = 1
- for m_name in [st(x)[0] for x in ld(path) if ".mxd" or ".MXD" in x]:
- width, height = m_name.split("x")
- name = "pagesize{}".format(counter)
- m_dict[name] = (int(width), int(height))
- counter+=1
- return m_dict
复制代码
最后返回的 m_dict 就是标准化后的匹配信息,程序后面会用到。
​
​
2.最小边界几何
现在我们知道了模板的大小,接下来肯定是获取制图单位的大小。这样才能互相适配。 比如下图有17个地级市,也是17个制图单位,如何知道它们的宽边和长边呢?
那么可以使用最小边界几何工具来帮助我们确定。 最小边界几何——面积最小的情况下,能完全覆盖多边形的封闭面。如下图的所示:浅绿色框就是每一个制图单位的最小矩形边界几何。
生成最小边界几何可以直接计算出长边和短边, 通过调用 ArcPy 方法 arcpy.MinimumBoundingGeometry_management 可以计算生成最小边界几何图层,该图层自带两个字段,即 MBG_Width 和 MBG_Length 字段,前者表示短边,后者表示长边。
​Note: 该工具位于 数据管理工具->要素->最小边界几何;
官方在线文档:
https://desktop.arcgis.com/zh-cn/arcmap/10.3/tools/data-management-toolbox/minimum-bounding-geometry.htm |
​
使用 MappingIndex 图层生成最小边界几何及其相关操作的代码 C2 (../main/part1.py)如下:
- # -*- coding:utf-8 -*-
- # ----------------------------------------------------------
- # Author: LiaoChenchen
- # Created on: 2021/1/29 17:34
- # ----------------------------------------------------------
- from __future__ import absolute_import
- from __future__ import unicode_literals
- from __future__ import print_function
- from __future__ import division
- import arcpy
- import os
- from os.path import splitext as st
- from os import listdir as ld
- ​
- """_______global_values_______"""
- # 地址
- mxd_template = "tempMXDS" # 模板文件位置
- output_dir = "out" # 制图输出位置
- gdb_path = "arcpy指南.gdb" # 数据库地址
- # 重要常量
- FIELD = "CITY" # 检索字段
- MI = "MappingIndex" # 制图索引文件名称
- SCALE = 200000 # 制图的比例尺
- """_______global_values_______"""
- ​
- arcpy.env.overwriteOutput = True
- arcpy.env.workspace = gdb_path
- ​
- def check_field(layer, field):
- """
- 检查一个要素文件中是否存在field字段
- """
- fields = arcpy.ListFields(layer)
- return field in [_.name for _ in fields]
- ​
- ​
- class PageSizeMatch(object):
- """
- 适配页面大小
- """
- def __init__(self, feature_name, field, mxd_template_d):
- """
- :param feature_name: {String} # 制图索引图层的名字
- :param field: {String} field = "CITY" # 检索字段
- :param mxd_template_d: {Dict} 模板匹配信息(字典)
- """
- self.f = feature_name # 制图索引图层的名字
- self.field = field
- self.m_d = mxd_template_d
- self.minimum_bounding() # 制作最小几何边界图层
- true_height = self.check_width_height() # 获取真实的高度信息
- # 将高度信息更新入制图索引图层(MappingIndex)
- self.update_width_height(true_height)
- self.update_page_size(SCALE)
-
-
- def minimum_bounding(self):
- """
- 1.make MinimumBoundingGeometry feature layer
- 2.add PAGRSIZE field
- :return:
- """
- if not check_field(MI,"MBG_Width"): #▶注释1◀
- #▶注释2◀
- mbe = arcpy.MinimumBoundingGeometry_management
- mbe(self.f, "m_out", "ENVELOPE", "LIST", self.field, True)
- print("Complete MinimumBoundingGeometry")
- arcpy.Delete_management(self.f,"FeatureClass")#▶注释3◀
- arcpy.Rename_management("m_out",self.f,"FeatureClass")
-
- if not check_field(self.f,"PAGESIZE"): #▶注释4◀
- # 没有计算过最小几何边界
- print('Add field PAGESIZE')
- arcpy.AddField_management(
- self.f, "PAGESIZE", "TEXT", field_length = 100)
复制代码
由于该代码是从完整代码剪切下来的,所以有一部分暂时无关的代码片段。 我们先看 check_field 和 minimum_bounding 方法。 前者位于上述代码片段的前部分,用于检查一个矢量要素文件中是否存在某个字段。 后者位于上述代码片段的最后,用于计算生成最小边界几何及其相关操作。
▶注释1◀: 使用了自定义 check_field 方法检查 MappingIndex (关于该图层的来历请看《指南》第二章) 是否有 MBG_Width 字段,因为生成的最小边界几何图层会自动创建 MBG_Width 字段。 所以能通过检查字段来确认是否已经执行了 minimum_bounding 方法,生成了最小边界几何图层。
▶注释2◀: Python 中函数也是对象,我们使用一个较简短的变量 mbe 来指向 arcpy.MinimumBoundingGeometry_management 方法的内存地址,然后使用 mbe () 来执行该函数。 避免一行代码写的过长。
▶注释3◀: 替换图层。 先将原 MappingIndex 矢量文件删除,然后将生成的最小边界几何矢量数据重命名为 “MappingIndex”。以此实现对 mxd 模板文件中 MappingIndex 图层的替换(将其源文件换为最小边界几何图层)。
▶注释4◀: 添加字段 PAGESIZE。
执行完上述操作后,重新打开 MappingIndex 的属性表1:
​表1
​
Note:MBG_Width 和 MBG_Length 的单位为米。 |
​
​
​
3.更新MBG_Width 和 MBG_Length 字段
——将 MBG_Width 和 MBG_Length 字段更新为宽和高的关系! 经过第二步,我们获取了各个制图单位的短边(MBG_Width)和长边(MBG_Length)。从表1可以看到每一个制图单位的 MBG_Width 都大于 MBG_Length。 但是仅仅通过短边、长边是无法完全区分出地图形状的。 无法区分出横卧状和长条状,如下所示。你能分出下面那个图形是正确的吗?只根据短边和长边信息的话,下面两种都是正确的。但是答案只有一个。 分不清这两种情况的话,自然也无法分配大小比例适合的 mxd 模板。
所以只有知道短边是高还是宽的时候,才能知道那个是正确的。
如何得知短边是高还是宽呢? 将面转成点的时候,一个矩形会生成5个点,五个点分别是 右下角点、右上角点、左上角点、左下角点、右下角点(和第一点重叠)。右下角点、右上角点 两点 y 坐标的差值就是该矩形的高。 如果该差值等于 MBG_Width 字段值,那么短边就是高,上图第二种情况是正确的。反之,差值等于 MBG_Length 字段值,长边是高,那么第一种情况是正确的。
要素折点转点通过 ArcPy 中的 FeatureVerticesToPoints_management 方法实现。
识别出高和宽的代码 C3 (../main/part1.py)如下:
- # -*- coding:utf-8 -*-
- # ----------------------------------------------------------
- # Author: LiaoChenchen
- # Created on: 2021/1/29 17:34
- # ----------------------------------------------------------
- from __future__ import absolute_import
- from __future__ import unicode_literals
- from __future__ import print_function
- from __future__ import division
- import arcpy
- import os
- from os.path import splitext as st
- from os import listdir as ld
- ​
- ​
- class PageSizeMatch(object):
-
- """
- ... 省略部分代码
- """
- ​
- def check_width_height(self): #▶注释1◀
- # 使用 “要素折点转点要素” 工具,将最小边界几何转变成点;
- # 每一个最小边界几何都会生成5个点:左下,左上,右上,右下,左下;
- # 前两个点的差值就是高度
- # 返回值为高度的字典:{地级市名称:高度}
- fea_v = "feature_vertices" # feature name
- short_f = arcpy.FeatureVerticesToPoints_management
- short_f(self.f, fea_v, "ALL")
- cursor = arcpy.da.SearchCursor(
- fea_v, [self.field, "SHAPE@Y"]
- )
- cursor2l = [(x[0],x[1]) for x in cursor] # 转换成列表
- del cursor
- #▶注释2◀
- cursor2l_1, cursor2l_2 = cursor2l[::5], cursor2l[1::5]
- height_info = {}
- for i in xrange(len(cursor2l_1)):#▶注释2◀
- height = cursor2l_2[1] - cursor2l_1[1]
- height_info[cursor2l_2[0]] = abs(height)
- return height_info
- #▶注释4◀
- def update_width_height(self, height_infomation):
- """
- 该方法将 "MBG_Width","MBG_Length"两字段更新为宽和高
- :param height_infomation: {Dict} 包含了制图单位的高度信息
- :return:
- """
- _field = [self.field,"MBG_Width", "MBG_Length"]
- with arcpy.da.UpdateCursor(self.f, _field) as cursor:
- for row in cursor:
- name, width, height = row
- #▶注释5◀
- if round(height_infomation[name], 2)==round(width, 2):
- row[1] = height
- row[2] = width
- cursor.updateRow(row)
- print("update width&height completly!")
复制代码
​
省略了 PageSizeMatch 类中 C2 代码部分。
▶注释1◀: check_width_height 使用 “要素折点转点要素” 工具,将最小边界几何转变成点。 该方法完成要素折点转点的工作,同时计算出每个制图单元的高是多少。
▶注释2◀: 每个矩形都会生成5个按顺序排列的点,cursor2l[::5] 使用切片每隔5个数取第一个;cursor2l[1::5] 使用切片每隔5个数取第二个。
▶注释3◀: 这个 for 循环是将 y 轴差值和制图单位名称组合成字典。
▶注释4◀: height_infomation 形参接收上面 check_width_height 方法的返回值。该方法会把 MappingIndex 图层中的 MBG_Width 和 MBG_Length 的值分别更新为宽和高。
▶注释5◀: 如果短边等于高度,字段 MBG_Width 和 MBG_Length 的意义从原来的短边和长边变成宽和高,那么原来 MBG_Width 和 MBG_Length 的值就需要互换。
更新后的效果: MappingIndex 图层属性表如下表所示: ​表2
​
把表2和表1进行比较,可以发现不少城市的 MBG_Width 的 MBG_Length 都发生了互换。比如自贡市
​自贡市边界图:自贡市明显呈横卧状,宽大于高
​
​
​
4.更新PAGESIZE
顺利完成本章前3步后,下一步就是更新 PAGESIZE 字段。
在第一部分,我们获得了 mxd 模板的信息。 在二、三部分,成功获得了各个制图单位的准确宽、高数据。 那么接下来就是将这两套数据相比较,给每个制图单位分配大小合适的 mxd 模板。 最后将匹配结果储存到 PAGESIZE 字段。
实现匹配和 PAGESIZE 字段更新的代码 C4 (../main/part1.py)如下:
- # -*- coding:utf-8 -*-
- # ----------------------------------------------------------
- # Author: LiaoChenchen
- # Created on: 2021/1/29 17:34
- # ----------------------------------------------------------
- from __future__ import absolute_import
- from __future__ import unicode_literals
- from __future__ import print_function
- from __future__ import division
- import arcpy
- import os
- from os.path import splitext as st
- from os import listdir as ld
- ​
- ​
- def select_template_size(size, template_size):
- """
- :param size: 宽和高组成的列表
- such as:[659.8490915000066, 822.3146429999917]
- :param template_size: 制图模板大小
- :return: 返回制图模板大小的名称(键),
- 如果找不到适合的制图模板就返回 -1
- """
-
- map_w, map_h = size[0], size[1]
- map_div = map_w / map_h
- # 符合该制图单位的模板大小的字典
- template_size_fit = {
- k:(v[0], v[1], v[0]*v[1], v[0]/v[1]) for k,v
- in template_size.items() if v[0]>map_w and v[1]>map_h
- } #▶注释6◀
- d_len = len(template_size_fit)
- # 字典转列表
- d2l = zip(template_size_fit.keys(), template_size_fit.values())
- # 按元组中第三个数大小排序(按面积大小)
- d2l_sorted = sorted(d2l, key=lambda x: x[1][2])
- if d_len > 2: #▶注释7◀
- two_remaind = d2l_sorted[:2]
- # (u'pagesize3', (1380, 850, 1173000, 1.6235294117647059))
- res = min(two_remaind, key=lambda x: abs(x[1][3]-map_div))
- return res[0] # u'pagesize3'
- elif d_len==2:
- res = d2l_sorted[0]
- return res[0]
- elif d_len==1:
- return d2l_sorted[0][0]
- else:
- # info="存在超出页面大小的制图单位"
- return -1
-
-
- class PageSizeMatch(object):
- """
- ... 省略部分代码
- """
-
- def update_page_size(self,scale):
- """
- 更新填充字段 "PAGESIZE" 的值
- :param scale: {Int} 比例大小
- :return:
- """
- _field = ["MBG_Width", "MBG_Length", "PAGESIZE"]
- with arcpy.da.UpdateCursor(self.f, _field) as cursor:
- for row in cursor:
- row_p = [row[0], row[1]]
- new_row = [x/scale*1000 for x in row_p]
- # PAGESIZE1 or -1
- pgs_name = select_template_size(new_row, self.m_d)
- if pgs_name != -1:
- p_size = self.m_d[pgs_name] # (1180, 900)
- # update PAGESIZE field values 1180x900
- #▶注释8◀
- row[2] = "{}x{}".format(p_size[0],p_size[1])
- else:
- print("存在超出所有模板页面大小的制图单位")
- cursor.updateRow(row)
- print("update PAGESIZE completly!")
复制代码
类方法 update_page_size 则用于更新 PAGESIZE 字段。 具体的匹配方式是由 select_template_size 方法来实现。该方法接收两个参数,一个宽和高组成的列表(单位毫米),另一个参数就是第一步获得的模板标准化信息(字典)。
其内部的思路是:首先筛选出可以容下制图单位的 mxd 模板。 如果满足条件的 mxd 模板大于三个,在▶注释7◀中你可以看到,这时就会启用 面积 指标,按大小排序,取最小的两个模板。 再使用 宽高比例 指标,从两个模板中选出 宽高比例 最接近制图单位的 mxd 模板。
▶注释6◀: 字典推导式,生成如这样的字典:{... , "达州市" : (宽, 高,面积大小,宽高比例) , ...}。 ... v[0]>map_w and v[1]>map_h... 这段代码做了初步的筛选,筛选出满足该制图单位宽和高的 mxd模板。同时这里引入了 面积 和 宽高比例,这两个指标用于进一步的判断和筛选。
▶注释8◀:更新 PAGESIZE 字段,其形式如1180x900。
更新后的 PAGESIZE 字段:
更新后的 PAGESIZE 字段值即是我们为每个制图单位分配的模板大小。
​
​
总结
该章节主要分为了4部分。
那么下一章节进入到使用 ArcPy 控制 mxd 自动更新和出图。 ​Note:该章节完整详细代码见下载文件 ../main/part1.py |
​
​
​
结束语
下载: 演示文件数据 源代码 《指南》文档PDF小册子,便于电脑查看
关注公众号 GIS荟 回复:自动化制图 ,获取所有下载!
​
​分享GIS,不止于Python。荟GIS精粹,关注我,带你飞!(长按扫码也行)
​
|