初始化

This commit is contained in:
2026-04-22 12:27:49 +08:00
commit 4857cb6e45
73 changed files with 20927 additions and 0 deletions

6
tools/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""
ArcGIS Pro工具集
包含各种地图处理和栅格处理工具
"""
__version__ = '1.1.0'

View File

@@ -0,0 +1,54 @@
'''
ArcGis 字段计算器 模块代码
'''
codeblock_dltb_yjdl = """
def calculate_yjdl(dlbm):
if str(dlbm).startswith('01'):
return '耕地'
elif str(dlbm).startswith('02'):
return '园地'
elif str(dlbm).startswith('03'):
return '林地'
elif str(dlbm).startswith('04'):
return '草地'
else:
return '其他'"""
codeblock_dltb_ejdl = """
def calculate_ejdl(dlbm,dlmc):
if str(dlbm).startswith('03'):
return '林地'
elif str(dlbm).startswith('04'):
return '草地'
elif str(dlbm).startswith('12'):
return '其他'
elif str(dlbm).startswith('0101'):
return '水田'
elif str(dlbm).startswith('0102'):
return '水浇地'
elif str(dlbm).startswith('0103'):
return '旱地'
elif str(dlbm).startswith('0201'):
return '果园'
elif str(dlbm).startswith('0202'):
return '茶园'
elif str(dlbm).startswith('0203'):
return '橡胶园'
elif str(dlbm).startswith('0204'):
return '其他园地'
else:
return dlmc"""
codeblock_cal_shfj = """
def calculate_shfj(girdcode):
if int(girdcode) == 1:
return "重度酸化"
elif int(girdcode) == 2:
return "中度酸化"
elif int(girdcode) == 3:
return "轻度酸化"
elif int(girdcode) == 4:
return "弱酸化"
else: # dPH
return "其他"
"""

View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
'''
公共配置
- 地区分组
- 土壤性质字典
- 土壤性质分级字典
- 土壤性质分级标准字典
'''
# 地区分组
guangxi_region = ['广西壮族自治区', '北海市', '海城区', '银海区', '铁山港区', '苍梧县', '容县', '靖西市', '兴宁区', '邕宁区', '武鸣区', '天峨县', '平南县', '港南区', '来宾市']
yunnan_region = ['云南省', '西畴县', '马关县', '澜沧县', '双江县', '永德县', '寻甸县', '罗平县', '丘北县', '永仁县', '南华县', '双柏县', '武定县', '祥云县', '楚雄彝族自治州']
# 土壤性质字典
soil_prop_dict = {
"AB": "有效硼",
"ACU": "有效铜",
"AMN": "有效锰",
"AMO": "有效钼",
"AS1": "有效硫",
"AZN": "有效锌",
"CEC": "阳离子交换量",
"ECA": "交换性钙",
"EMG": "交换性镁",
"TSE": "全硒",
"TN": "全氮",
"TP": "全磷",
"TK": "全钾",
"AFE": "有效铁",
"AK": "速效钾",
"AP": "有效磷",
"TRRZ": "土壤容重",
"OM": "有机质",
"FL": "粉粒含量",
"NL": "黏粒含量",
"SL": "砂粒含量",
"PH": "土壤 pH",
"YXTCHD": "有效土层厚度",
"GZCHD": "耕作层厚度",
"TRZD": "土壤质地",
"TRZD12": "土壤质地",
"LSFD": "砾石丰度",
"三普PH": "三普PH",
"二普PH": "二普PH",
"测土PH": "测土PH",
"二普-三普": "二普-三普",
"测土-三普": "测土-三普",
"二普-测土": "二普-测土"
}

125
tools/config/custom_sort.py Normal file
View File

@@ -0,0 +1,125 @@
'''
自定义排序yl_order-亚类排序ts_order-土属排序
'''
yl_order = [
"典型砖红壤",
"典型赤红壤",
"典型红壤",
"黄红壤",
"红壤性土",
"典型黄壤",
"漂洗黄壤",
"表潜黄壤",
"黄壤性土",
"暗黄棕壤",
"典型新积土",
"滨海风沙土",
"红色石灰土",
"黑色石灰土",
"棕色石灰土",
"黄色石灰土",
"酸性紫色土",
"中性紫色土",
"石灰性紫色土",
"硅质岩粗骨土",
"典型潮土",
"灰潮土",
"山地灌丛草甸土",
"泥炭沼泽土",
"盐化沼泽土",
"滨海潮滩盐土",
"含盐酸性硫酸盐土",
"潴育水稻土",
"淹育水稻土",
"渗育水稻土",
"潜育水稻土",
"漂洗水稻土",
"咸酸水稻土",
]
ts_order = [
"涂砂质砖红壤",
"暗泥质砖红壤",
"麻砂质砖红壤",
"砂泥质砖红壤",
"红泥质赤红壤",
"暗泥质赤红壤",
"麻砂质赤红壤",
"硅质赤红壤",
"砂泥质赤红壤",
"泥质赤红壤",
"灰泥质赤红壤",
"红泥质红壤",
"麻砂质红壤",
"砂泥质红壤",
"泥质红壤",
"麻砂质黄红壤",
"硅质黄红壤",
"砂泥质黄红壤",
"灰泥质黄红壤",
"泥砂质红壤性土",
"麻砂质黄壤",
"砂泥质黄壤",
"麻砂质漂洗黄壤",
"砂泥质漂洗黄壤",
"砂泥质表潜黄壤",
"砂泥质黄壤性土",
"灰泥质黄壤性土",
"麻砂质暗黄棕壤",
"砂泥质暗黄棕壤",
"山洪土",
"滨海固定风沙土",
"红色石灰土",
"黑色石灰土",
"棕色石灰土",
"黄色石灰土",
"酸紫砂土",
"酸紫壤土",
"酸紫黏土",
"紫泥土",
"灰紫壤土",
"灰紫泥土",
"白粉土",
"潮壤土",
"石灰性灰潮壤土",
"灰潮砂土",
"灰潮壤土",
"山地灌丛草甸砂土",
"山地灌丛草甸壤土",
"泥炭沼泽土",
"盐化沼泽土",
"涂砂盐土",
"涂泥盐土",
"含盐酸性硫酸盐土",
"潮泥田",
"潮泥砂田",
"涂泥田",
"麻砂泥田",
"砂泥田",
"鳝泥田",
"灰泥田",
"紫泥田",
"红泥田",
"白粉泥田",
"暗泥田",
"浅潮泥田",
"浅潮泥砂田",
"浅暗泥田",
"浅麻砂泥田",
"浅砂泥田",
"浅鳝泥田",
"浅灰泥田",
"浅紫泥田",
"浅白粉泥田",
"浅红泥田",
"渗潮泥田",
"渗砂泥田",
"渗紫泥田",
"青潮泥田",
"青灰泥田",
"青红泥田",
"烂泥田",
"泥炭土田",
"漂红泥田",
"咸酸田",
]

View File

@@ -0,0 +1,141 @@
'''
用于pandas dataframe中字段计算
'''
# 计算一级土地利用类型
def calculate_yjdl(dlbm):
if str(dlbm).startswith('01'):
return '耕地'
elif str(dlbm).startswith('02'):
return '园地'
elif str(dlbm).startswith('03'):
return '林地'
elif str(dlbm).startswith('04'):
return '草地'
else:
return '其他'
# 计算二级土地利用类型
def calculate_ejdl(dlbm):
dlbm_str = str(dlbm).strip()
if dlbm_str.startswith('0101'):
return '水田'
elif dlbm_str.startswith('0102'):
return '水浇地'
elif dlbm_str.startswith('0103'):
return '旱地'
elif dlbm_str.startswith('0201'):
return '果园'
elif dlbm_str.startswith('0202'):
return '茶园'
elif dlbm_str.startswith('0203'):
return '橡胶园'
elif dlbm_str.startswith('0204'):
return '其他园地'
elif dlbm_str.startswith('03'):
return '林地'
elif dlbm_str.startswith('04'):
return '草地'
elif dlbm_str.startswith('12'):
return '其他'
else:
return '未分类'
# 计算母岩母质
def calculate_muyan(soil_name):
soil_name = str(soil_name) # 确保为字符串
# 将"紫"放在最前面
if "" in soil_name:
return "紫色砂页岩"
elif "红砂" in soil_name:
return "第三纪红砂岩"
elif "麻砂" in soil_name:
return "花岗岩或花岗片麻岩"
elif "涂砂" in soil_name:
return "砂质浅海沉积物"
elif "暗泥" in soil_name:
return "玄武岩、火山灰(渣)"
elif "砂泥" in soil_name:
return "砂页岩、砂岩、砂砾岩"
elif "" in soil_name or "白粉泥" in soil_name:
return "石英砂岩、石英岩、硅质岩"
elif "灰泥" in soil_name or "石灰" in soil_name:
return "石灰岩、白云岩、大理岩"
elif "磷灰" in soil_name:
return "磷灰岩"
elif "红泥" in soil_name:
return "第四纪红色黏土"
elif "红土" in soil_name:
return "第三纪红色黏土"
elif "风砂" in soil_name:
return "风积砂"
elif "潮泥砂" in soil_name or "泥砂" in soil_name or "新积土" in soil_name:
return "洪积物"
elif "淡涂泥" in soil_name:
return "河口相沉积物"
elif "涂泥" in soil_name:
return "海相沉积物"
elif "黄泥" in soil_name:
return "古老洪冲积物"
# 将容易误匹配的放在最后
elif "潮泥" in soil_name or "潮砂" in soil_name or "潮土" in soil_name:
return "冲积物"
elif "" in soil_name or "鳝泥" in soil_name:
return "片岩、板岩、千枚岩、页岩、泥岩"
else:
return "其他" # 或者返回空值 ""
def calculate_muzhi(value):
if value in ["第三纪红砂岩", "花岗岩或花岗片麻岩", "玄武岩、火山灰(渣)",
"砂页岩、砂岩、砂砾岩", "片岩、板岩、千枚岩、页岩、泥岩", "石英砂岩、石英岩、硅质岩",
"石灰岩、白云岩、大理岩", "磷灰岩", "紫色砂页岩", "第三纪红色黏土", "风积砂"]:
return "残坡积物"
elif value in ["冲积物", "第四纪红色黏土", "洪积物", "海相沉积物", "古老洪冲积物", "河口相沉积物", "砂质浅海沉积物"]:
return "第四纪松散沉积物"
else:
return "未知"
"""
"粉(砂)质黏壤土": "1",
"粉(砂)质黏土": "2",
"粉(砂)质壤土": "3",
"黏壤土": "4",
"黏土": "5",
"壤土": "6",
"壤质黏土": "7",
"砂土及壤质砂土": "8",
"砂质黏壤土": "9",
"砂质黏土": "10",
"砂质壤土": "11",
"重黏土": "12"
"""
def cal_trzd(value):
if value == "砂土及壤质砂土":
return 1
elif value == "砂质壤土":
return 2
elif value == "壤土":
return 3
elif value == "粉砂质壤土":
return 4
elif value == "砂质黏壤土":
return 5
elif value == "黏壤土":
return 6
elif value == "粉砂质黏壤土":
return 7
elif value == "砂质黏土":
return 8
elif value == "壤质黏土":
return 9
elif value == "粉砂质黏土":
return 10
elif value == "黏土":
return 11
elif value == "重黏土":
return 12
else:
return 0

View File

@@ -0,0 +1,315 @@
{
"export_config": {
"AB": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图",
"分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法",
"项目分级": "有效硼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.002.00",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"ACU": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铜\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1.80",
"等级二": "1.001.80",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"AMN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锰\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "20.030.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AMO": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图",
"分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法",
"项目分级": "有效钼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "0.20",
"等级二": "0.150.20",
"等级三": "0.100.15",
"等级四": "0.050.10",
"等级五": "≤0.05"
}
},
"AS1": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "有效硫\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "30.040.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"AZN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锌\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "2.003.00",
"等级三": "1.002.00",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"CEC": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图",
"分析方法": "分析方法EDTA-乙酸铵交换法",
"项目分级": "阳离子\n交换量级别",
"分级标准": "分级标准/\n(cmol/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"ECA": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "交换性钙\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1800",
"等级二": "13001800",
"等级三": "8001300",
"等级四": "300800",
"等级五": "≤300"
}
},
"EMG": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "交换性镁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "250",
"等级二": "150250",
"等级三": "70150",
"等级四": "3070",
"等级五": "≤30"
}
},
"TSE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图",
"分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法",
"项目分级": "全硒\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "0.403.00",
"等级三": "0.170.40",
"等级四": "≤0.17"
}
},
"TN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图",
"分析方法": "分析方法:自动定氮仪法",
"项目分级": "全氮\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.502.00",
"等级三": "1.001.50",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"TP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全磷\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "1.50",
"等级二": "1.001.50",
"等级三": "0.601.00",
"等级四": "0.400.60",
"等级五": "≤0.40"
}
},
"TK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全钾\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AFE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "10.020.0",
"等级三": "8.010.0",
"等级四": "3.08.0",
"等级五": "≤3.0"
}
},
"AK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图",
"分析方法": "分析方法:乙酸钠浸提-火焰光度法",
"项目分级": "速效钾\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "150",
"等级二": "100150",
"等级三": "75100",
"等级四": "5075",
"等级五": "≤50"
}
},
"AP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图",
"分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法",
"项目分级": "有效磷\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "20.040.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "30.035.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"FL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "粉粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤20",
"等级二": "2040",
"等级三": "4060",
"等级四": "6080",
"等级五": "80"
}
},
"NL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "黏粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"SL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "砂粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "土壤pH\n级别",
"分级标准": "分级标准/\npH值",
"标准等级": {
"等级一": "6.07.0",
"等级二": "7.07.5,\n5.56.0",
"等级三": "7.58.0,\n5.05.5",
"等级四": "8.08.5,\n4.55.0",
"等级五": "8.5,\n≤4.5"
}
},
"YXTCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "有效土层\n厚度级别",
"分级标准": "分级标准/\ncm",
"标准等级": {
"等级一": "80",
"等级二": "7080",
"等级三": "6070",
"等级四": "5060",
"等级五": "≤50"
}
},
"GZCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "耕作层\n厚度级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "25.0",
"等级二": "20.025.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"TRRZ": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图",
"分析方法": "分析方法:热鼓风加热环刀法",
"项目分级": "土壤容重\n级别",
"分级标准": "分级标准/\n(g/cm³)",
"标准等级": {
"等级一": "1.001.20",
"等级二": "1.201.30",
"等级三": "1.301.40,\n0.901.0",
"等级四": "1.401.50",
"等级五": "1.50,\n≤0.90"
}
}
}
}

View File

@@ -0,0 +1,328 @@
{
"export_config": {
"AB": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图",
"分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法",
"项目分级": "有效硼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.002.00",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"ACU": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铜\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1.80",
"等级二": "1.001.80",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"AMN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锰\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "20.030.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AMO": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图",
"分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法",
"项目分级": "有效钼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "0.20",
"等级二": "0.150.20",
"等级三": "0.100.15",
"等级四": "0.050.10",
"等级五": "≤0.05"
}
},
"AS1": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "有效硫\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "30.040.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"AZN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锌\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "2.003.00",
"等级三": "1.002.00",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"CEC": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图",
"分析方法": "分析方法EDTA-乙酸铵交换法",
"项目分级": "阳离子\n交换量级别",
"分级标准": "分级标准/\n(cmol/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"ECA": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图(地块)",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性钙\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1000",
"等级二": "5001000",
"等级三": "200500",
"等级四": "50200",
"等级五": "≤50"
}
},
"EMG": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图(地块)",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性镁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "200",
"等级二": "100200",
"等级三": "50100",
"等级四": "2550",
"等级五": "≤25"
}
},
"TSE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图",
"分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法",
"项目分级": "全硒\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "0.403.00",
"等级三": "0.170.40",
"等级四": "≤0.17"
}
},
"TN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图",
"分析方法": "分析方法:自动定氮仪法",
"项目分级": "全氮\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.502.00",
"等级三": "1.001.50",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"TP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全磷\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "1.50",
"等级二": "1.001.50",
"等级三": "0.601.00",
"等级四": "0.400.60",
"等级五": "≤0.40"
}
},
"TK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全钾\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AFE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "10.020.0",
"等级三": "8.010.0",
"等级四": "3.08.0",
"等级五": "≤3.0"
}
},
"AK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图",
"分析方法": "分析方法:乙酸钠浸提-火焰光度法",
"项目分级": "速效钾\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "150",
"等级二": "100150",
"等级三": "75100",
"等级四": "5075",
"等级五": "≤50"
}
},
"AP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图",
"分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法",
"项目分级": "有效磷\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "20.040.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "30.035.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"FL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "粉粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤20",
"等级二": "2040",
"等级三": "4060",
"等级四": "6080",
"等级五": "80"
}
},
"NL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "黏粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"SL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "砂粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.07.0",
"等级二": "7.07.5,\n5.56.0",
"等级三": "7.58.0,\n5.05.5",
"等级四": "8.08.5,\n4.55.0",
"等级五": "8.5,\n≤4.5"
}
},
"YXTCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "有效土层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "80",
"等级二": "7080",
"等级三": "6070",
"等级四": "5060",
"等级五": "≤50"
}
},
"GZCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "耕作层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "25.0",
"等级二": "20.025.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"TRRZ": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图",
"分析方法": "分析方法:热鼓风加热环刀法",
"项目分级": "土壤容重\n级别",
"分级标准": "分级标准/\n(g/cm³)",
"标准等级": {
"等级一": "1.001.20",
"等级二": "1.201.30",
"等级三": "1.301.40,\n0.901.0",
"等级四": "1.401.50",
"等级五": "1.50,\n≤0.90"
}
},
"TRZD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"黏壤质": "1",
"黏质": "2",
"壤质": "3",
"砂壤质": "4",
"砂质": "5"
}
}
}
}

View File

@@ -0,0 +1,328 @@
{
"export_config": {
"AB": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图(地块)",
"分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法",
"项目分级": "有效硼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.002.00",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"ACU": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铜\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1.80",
"等级二": "1.001.80",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"AMN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锰\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "20.030.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AMO": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图(地块)",
"分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法",
"项目分级": "有效钼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "0.20",
"等级二": "0.150.20",
"等级三": "0.100.15",
"等级四": "0.050.10",
"等级五": "≤0.05"
}
},
"AS1": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图(地块)",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "有效硫\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "30.040.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"AZN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锌\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "2.003.00",
"等级三": "1.002.00",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"CEC": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图(地块)",
"分析方法": "分析方法EDTA-乙酸铵交换法",
"项目分级": "阳离子\n交换量级别",
"分级标准": "分级标准/\n(cmol/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"ECA": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图(地块)",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性钙\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1000",
"等级二": "5001000",
"等级三": "200500",
"等级四": "50200",
"等级五": "≤50"
}
},
"EMG": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图(地块)",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性镁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "200",
"等级二": "100200",
"等级三": "50100",
"等级四": "2550",
"等级五": "≤25"
}
},
"TSE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图(地块)",
"分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法",
"项目分级": "全硒\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "0.403.00",
"等级三": "0.170.40",
"等级四": "≤0.17"
}
},
"TN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图(地块)",
"分析方法": "分析方法:自动定氮仪法",
"项目分级": "全氮\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.502.00",
"等级三": "1.001.50",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"TP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图(地块)",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全磷\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "1.50",
"等级二": "1.001.50",
"等级三": "0.601.00",
"等级四": "0.400.60",
"等级五": "≤0.40"
}
},
"TK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图(地块)",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全钾\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AFE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "10.020.0",
"等级三": "8.010.0",
"等级四": "3.08.0",
"等级五": "≤3.0"
}
},
"AK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图(地块)",
"分析方法": "分析方法:乙酸钠浸提-火焰光度法",
"项目分级": "速效钾\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "150",
"等级二": "100150",
"等级三": "75100",
"等级四": "5075",
"等级五": "≤50"
}
},
"AP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图(地块)",
"分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法",
"项目分级": "有效磷\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "20.040.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图(地块)",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "30.035.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"FL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "粉粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤20",
"等级二": "2040",
"等级三": "4060",
"等级四": "6080",
"等级五": "80"
}
},
"NL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "黏粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"SL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "砂粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图地块",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.07.0",
"等级二": "7.07.5,\n5.56.0",
"等级三": "7.58.0,\n5.05.5",
"等级四": "8.08.5,\n4.55.0",
"等级五": "8.5,\n≤4.5"
}
},
"YXTCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图(地块)",
"分析方法": "分析方法:剖面法",
"项目分级": "有效土层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "80",
"等级二": "7080",
"等级三": "6070",
"等级四": "5060",
"等级五": "≤50"
}
},
"GZCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图(地块)",
"分析方法": "分析方法:剖面法",
"项目分级": "耕作层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "25.0",
"等级二": "20.025.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"TRRZ": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图(地块)",
"分析方法": "分析方法:热鼓风加热环刀法",
"项目分级": "土壤容重\n级别",
"分级标准": "分级标准/\n(g/cm³)",
"标准等级": {
"等级一": "1.001.20",
"等级二": "1.201.30",
"等级三": "1.301.40,\n0.901.0",
"等级四": "1.401.50",
"等级五": "1.50,\n≤0.90"
}
},
"TRZD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "1",
"等级二": "2",
"等级三": "3",
"等级四": "4",
"等级五": "5"
}
}
}
}

View File

@@ -0,0 +1,328 @@
{
"export_config": {
"AB": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图",
"分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法",
"项目分级": "有效硼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1.00",
"等级二": "0.801.00",
"等级三": "0.500.80",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"ACU": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铜\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.002.00",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"AMN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锰\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "15.030.0",
"等级三": "5.015.0",
"等级四": "1.05.0",
"等级五": "≤1.0"
}
},
"AMO": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图",
"分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法",
"项目分级": "有效钼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "0.20",
"等级二": "0.150.20",
"等级三": "0.100.15",
"等级四": "0.050.10",
"等级五": "≤0.05"
}
},
"AS1": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "有效硫\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "30.040.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"AZN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锌\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "1.003.00",
"等级三": "0.501.00",
"等级四": "0.300.50",
"等级五": "≤0.30"
}
},
"CEC": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图",
"分析方法": "分析方法EDTA-乙酸铵交换法",
"项目分级": "阳离子\n交换量级别",
"分级标准": "分级标准/\n(cmol/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "20.030.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"ECA": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性钙\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1500",
"等级二": "10001500",
"等级三": "5001000",
"等级四": "200500",
"等级五": "≤200"
}
},
"EMG": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性镁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "200",
"等级二": "150200",
"等级三": "100150",
"等级四": "50100",
"等级五": "≤50"
}
},
"TSE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图",
"分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法",
"项目分级": "全硒\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "0.403.00",
"等级三": "0.170.40",
"等级四": "≤0.17"
}
},
"TN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图",
"分析方法": "分析方法:自动定氮仪法",
"项目分级": "全氮\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.502.00",
"等级三": "1.001.50",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"TP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全磷\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "1.00",
"等级二": "0.801.00",
"等级三": "0.600.80",
"等级四": "0.400.60",
"等级五": "≤0.40"
}
},
"TK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全钾\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AFE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "10.020.0",
"等级三": "5.010.0",
"等级四": "3.05.0",
"等级五": "≤3.0"
}
},
"AK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图",
"分析方法": "分析方法:乙酸钠浸提-火焰光度法",
"项目分级": "速效钾\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "150",
"等级二": "100150",
"等级三": "75100",
"等级四": "5075",
"等级五": "≤50"
}
},
"AP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图",
"分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法",
"项目分级": "有效磷\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "25.040.0",
"等级三": "15.025.0",
"等级四": "5.015.0",
"等级五": "≤5.0"
}
},
"OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "25.035.0",
"等级三": "15.025.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"FL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "粉粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤20",
"等级二": "2040",
"等级三": "4060",
"等级四": "6080",
"等级五": "80"
}
},
"NL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "黏粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"SL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "砂粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.07.0",
"等级二": "7.07.5,\n5.56.0",
"等级三": "7.58.0,\n5.05.5",
"等级四": "8.08.5,\n4.55.0",
"等级五": "8.5,\n≤4.5"
}
},
"YXTCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "有效土层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "80",
"等级二": "7080",
"等级三": "6070",
"等级四": "5060",
"等级五": "≤50"
}
},
"GZCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "耕作层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "25.0",
"等级二": "20.025.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"TRRZ": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图",
"分析方法": "分析方法:热鼓风加热环刀法",
"项目分级": "土壤容重\n级别",
"分级标准": "分级标准/\n(g/cm³)",
"标准等级": {
"等级一": "1.101.25",
"等级二": "1.251.35,\n1.001.10",
"等级三": "1.351.45",
"等级四": "1.451.55,\n0.901.00",
"等级五": "1.55,\n≤0.90"
}
},
"TRZD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"黏壤质": "1",
"黏质": "2",
"壤质": "3",
"砂壤质": "4",
"砂质": "5"
}
}
}
}

View File

@@ -0,0 +1,30 @@
{
"AB": ">2.00;1.002.00;0.501.00;0.200.50;≤0.20",
"ACU": ">1.80;1.001.80;0.501.00;0.200.50;≤0.20",
"AMN": ">30.0;20.030.0;10.020.0;5.010.0;≤5.0",
"AMO": ">0.20;0.150.20;0.100.15;0.050.10;≤0.05",
"AS1": ">40.0;30.040.0;20.030.0;10.020.0;≤10.0",
"AZN": ">3.00;2.003.00;1.002.00;0.501.00;≤0.50",
"CEC": ">20.0;15.020.0;10.015.0;5.010.0;≤5.0",
"ECA": ">4.99;2.504.99;1.002.50;0.251.00;≤0.25",
"EMG": ">1.64;0.821.64;0.410.82;0.210.41;≤0.21",
"TSE": ">3.00;0.403.00;0.170.40;≤0.17",
"TN": ">2.00;1.502.00;1.001.50;0.501.00;≤0.50",
"TP": ">1.50;1.001.50;0.601.00;0.400.60;≤0.40",
"TK": ">20.0;15.020.0;10.015.0;5.010.0;≤5.0",
"AFE": ">20.0;10.020.0;8.010.0;3.08.0;≤3.0",
"AK": ">150;100150;75100;5075;≤50",
"AP": ">40.0;20.040.0;10.020.0;5.010.0;≤5.0",
"OM": ">35.0;30.035.0;20.030.0;10.020.0;≤10.0",
"FL": "≤15;1530;3045;4575;75",
"NL": "≤15;1525;2545;4565;65",
"SL": "≤30;3040;4055;5585;85",
"PH": "6.07.0;7.07.5,5.56.0;7.58.0,5.05.5;8.08.5,4.55.0;8.5,≤4.5",
"YXTCHD": ">100;80100;6080;4060;≤40",
"GZCHD": ">25.0;20.025.0;15.020.0;10.015.0;≤10.0",
"TRRZ": "1.001.20;1.201.30;1.301.40,0.901.0;1.401.50;1.50,≤0.90",
"TRZD": "1;2;3;4;5",
"二普PH": ">6.5;5.56.5;4.55.5;≤4.5",
"三普PH": ">6.5;5.56.5;4.55.5;≤4.5",
"酸化PH": ">1.0;0.51.0;0.30.5;-0.30.3;-10-0.3"
}

View File

@@ -0,0 +1,468 @@
{
"export_config": {
"AB": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图",
"分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法",
"项目分级": "有效硼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.002.00",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"ACU": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铜\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1.80",
"等级二": "1.001.80",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"AMN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锰\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "20.030.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AMO": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图",
"分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法",
"项目分级": "有效钼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "0.20",
"等级二": "0.150.20",
"等级三": "0.100.15",
"等级四": "0.050.10",
"等级五": "≤0.05"
}
},
"AS1": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "有效硫\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "30.040.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"AZN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锌\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "2.003.00",
"等级三": "1.002.00",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"CEC": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图",
"分析方法": "分析方法EDTA-乙酸铵交换法",
"项目分级": "阳离子\n交换量级别",
"分级标准": "分级标准/\n(cmol/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"ECA": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性钙\n含量级别",
"分级标准": "分级标准/\ncmol(½Ca²⁺)/kg",
"标准等级": {
"等级一": "4.99",
"等级二": "2.504.99",
"等级三": "1.002.50",
"等级四": "0.251.00",
"等级五": "≤0.25"
}
},
"EMG": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性镁\n含量级别",
"分级标准": "分级标准/\ncmol(½Mg²⁺)/kg",
"标准等级": {
"等级一": "1.64",
"等级二": "0.821.64",
"等级三": "0.410.82",
"等级四": "0.210.41",
"等级五": "≤0.21"
}
},
"TSE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图",
"分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法",
"项目分级": "全硒\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "0.403.00",
"等级三": "0.170.40",
"等级四": "≤0.17"
}
},
"TN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图",
"分析方法": "分析方法:自动定氮仪法",
"项目分级": "全氮\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.502.00",
"等级三": "1.001.50",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"TP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全磷\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "1.50",
"等级二": "1.001.50",
"等级三": "0.601.00",
"等级四": "0.400.60",
"等级五": "≤0.40"
}
},
"TK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全钾\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AFE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "10.020.0",
"等级三": "8.010.0",
"等级四": "3.08.0",
"等级五": "≤3.0"
}
},
"AK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图",
"分析方法": "分析方法:乙酸钠浸提-火焰光度法",
"项目分级": "速效钾\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "150",
"等级二": "100150",
"等级三": "75100",
"等级四": "5075",
"等级五": "≤50"
}
},
"AP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图",
"分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法",
"项目分级": "有效磷\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "20.040.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "30.035.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"FL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "粉粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1530",
"等级三": "3045",
"等级四": "4575",
"等级五": "75"
}
},
"NL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "黏粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"SL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "砂粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤30",
"等级二": "3040",
"等级三": "4055",
"等级四": "5585",
"等级五": "85"
}
},
"PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "≤4.5",
"等级二": "4.55.0",
"等级三": "5.05.5",
"等级四": "5.56.0",
"等级五": "6.07.0",
"等级六": "7.07.5",
"等级七": "7.58.0",
"等级八": "8.08.5",
"等级九": "8.5"
}
},
"YXTCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "有效土层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "100",
"等级二": "80100",
"等级三": "6080",
"等级四": "4060",
"等级五": "≤40"
}
},
"GZCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "耕作层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "25.0",
"等级二": "20.025.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"TRRZ": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图",
"分析方法": "分析方法:热鼓风加热环刀法",
"项目分级": "土壤容重\n级别",
"分级标准": "分级标准/\n(g/cm³)",
"标准等级": {
"等级一": "≤0.90",
"等级二": "0.901.0",
"等级三": "1.001.20",
"等级四": "1.201.30",
"等级五": "1.301.40",
"等级六": "1.401.50",
"等级七": "1.50"
}
},
"LSFD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砾石丰度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "砾石丰度\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤0",
"等级二": "05",
"等级三": "515",
"等级四": "1550",
"等级五": "50"
}
},
"TRZD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"砂质": "1",
"砂壤质": "2",
"壤质": "3",
"黏壤质": "4",
"黏质": "5"
}
},
"TRZD12": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"砂土及壤质砂土": "1",
"砂质壤土": "2",
"壤土": "3",
"粉(砂)质壤土": "4",
"砂质黏壤土": "5",
"黏壤土": "6",
"粉(砂)质黏壤土": "7",
"砂质黏土": "8",
"壤质黏土": "9",
"粉(砂)质黏土": "10",
"黏土": "11",
"重黏土": "12"
}
},
"二普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"测土PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"三普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"酸化PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"重度酸化": "1.0",
"中度酸化": "0.51.0",
"轻度酸化": "0.30.5",
"弱酸化": "0.10.3",
"碱化": "-100.1"
}
},
"二普-三普_PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"重度酸化": "1.0",
"中度酸化": "0.51.0",
"轻度酸化": "0.30.5",
"弱酸化": "0.10.3",
"碱化": "-100.1"
}
},
"测土-三普_PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"重度酸化": "1.0",
"中度酸化": "0.51.0",
"轻度酸化": "0.30.5",
"弱酸化": "0.10.3",
"碱化": "-100.1"
}
},
"二普-测土_PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"重度酸化": "1.0",
"中度酸化": "0.51.0",
"轻度酸化": "0.30.5",
"弱酸化": "0.10.3",
"碱化": "-100.1"
}
},
"专题图OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "≤10.0",
"等级二": "10.020.0",
"等级三": "20.030.0",
"等级四": "30.040.0",
"等级五": "40.0"
}
}
}
}

View File

@@ -0,0 +1,404 @@
{
"export_config": {
"AB": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图(地块)",
"分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法",
"项目分级": "有效硼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.002.00",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"ACU": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铜\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1.80",
"等级二": "1.001.80",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"AMN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锰\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "20.030.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AMO": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图(地块)",
"分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法",
"项目分级": "有效钼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "0.20",
"等级二": "0.150.20",
"等级三": "0.100.15",
"等级四": "0.050.10",
"等级五": "≤0.05"
}
},
"AS1": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图(地块)",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "有效硫\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "30.040.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"AZN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锌\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "2.003.00",
"等级三": "1.002.00",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"CEC": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图(地块)",
"分析方法": "分析方法EDTA-乙酸铵交换法",
"项目分级": "阳离子\n交换量级别",
"分级标准": "分级标准/\n(cmol/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"ECA": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图(地块)",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性钙\n含量级别",
"分级标准": "分级标准/\ncmol(½Ca²⁺)/kg",
"标准等级": {
"等级一": "4.99",
"等级二": "2.504.99",
"等级三": "1.002.50",
"等级四": "0.251.00",
"等级五": "≤0.25"
}
},
"EMG": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图(地块)",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性镁\n含量级别",
"分级标准": "分级标准/\ncmol(½Mg²⁺)/kg",
"标准等级": {
"等级一": "1.64",
"等级二": "0.821.64",
"等级三": "0.410.82",
"等级四": "0.210.41",
"等级五": "≤0.21"
}
},
"TSE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图(地块)",
"分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法",
"项目分级": "全硒\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "0.403.00",
"等级三": "0.170.40",
"等级四": "≤0.17"
}
},
"TN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图(地块)",
"分析方法": "分析方法:自动定氮仪法",
"项目分级": "全氮\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.502.00",
"等级三": "1.001.50",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"TP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图(地块)",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全磷\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "1.50",
"等级二": "1.001.50",
"等级三": "0.601.00",
"等级四": "0.400.60",
"等级五": "≤0.40"
}
},
"TK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图(地块)",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全钾\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AFE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "10.020.0",
"等级三": "8.010.0",
"等级四": "3.08.0",
"等级五": "≤3.0"
}
},
"AK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图(地块)",
"分析方法": "分析方法:乙酸钠浸提-火焰光度法",
"项目分级": "速效钾\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "150",
"等级二": "100150",
"等级三": "75100",
"等级四": "5075",
"等级五": "≤50"
}
},
"AP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图(地块)",
"分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法",
"项目分级": "有效磷\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "20.040.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图(地块)",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "30.035.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"FL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "粉粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1530",
"等级三": "3045",
"等级四": "4575",
"等级五": "75"
}
},
"NL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "黏粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"SL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "砂粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤30",
"等级二": "3040",
"等级三": "4055",
"等级四": "5585",
"等级五": "85"
}
},
"PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图地块",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "≤4.5",
"等级二": "4.55.0",
"等级三": "5.05.5",
"等级四": "5.56.0",
"等级五": "6.07.0",
"等级六": "7.07.5",
"等级七": "7.58.0",
"等级八": "8.08.5",
"等级九": "8.5"
}
},
"YXTCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图(地块)",
"分析方法": "分析方法:剖面法",
"项目分级": "有效土层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "100",
"等级二": "80100",
"等级三": "6080",
"等级四": "4060",
"等级五": "≤40"
}
},
"GZCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图(地块)",
"分析方法": "分析方法:剖面法",
"项目分级": "耕作层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "25.0",
"等级二": "20.025.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"TRRZ": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图(地块)",
"分析方法": "分析方法:热鼓风加热环刀法",
"项目分级": "土壤容重\n级别",
"分级标准": "分级标准/\n(g/cm³)",
"标准等级": {
"等级一": "≤0.90",
"等级二": "0.901.0",
"等级三": "1.001.20",
"等级四": "1.201.30",
"等级五": "1.301.40",
"等级六": "1.401.50",
"等级七": "1.50"
}
},
"LSFD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砾石丰度分布图(地块)",
"分析方法": "分析方法:剖面法",
"项目分级": "砾石丰度\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤0",
"等级二": "05",
"等级三": "515",
"等级四": "1550",
"等级五": "50"
}
},
"TRZD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"砂质": "1",
"砂壤质": "2",
"壤质": "3",
"黏壤质": "4",
"黏质": "5"
}
},
"TRZD12": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"砂土及壤质砂土": "1",
"砂质壤土": "2",
"壤土": "3",
"粉(砂)质壤土": "4",
"砂质黏壤土": "5",
"黏壤土": "6",
"粉(砂)质黏壤土": "7",
"砂质黏土": "8",
"壤质黏土": "9",
"粉(砂)质黏土": "10",
"黏土": "11",
"重黏土": "12"
}
},
"二普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤pH分布图地块",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"三普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图地块",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"酸化PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图(地块)",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"重度酸化": "1.0",
"中度酸化": "0.51.0",
"轻度酸化": "0.30.5",
"未酸化": "-0.30.3",
"碱化": "-10-0.3"
}
}
}
}

View File

@@ -0,0 +1,418 @@
{
"export_config": {
"AB": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图",
"分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法",
"项目分级": "有效硼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1.00",
"等级二": "0.81.00",
"等级三": "0.500.80",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"ACU": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铜\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.002.00",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"AMN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锰\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "15.030.0",
"等级三": "5.015.0",
"等级四": "1.05.0",
"等级五": "≤1.0"
}
},
"AMO": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图",
"分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法",
"项目分级": "有效钼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "0.20",
"等级二": "0.150.20",
"等级三": "0.100.15",
"等级四": "0.050.10",
"等级五": "≤0.05"
}
},
"AS1": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "有效硫\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "30.040.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"AZN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锌\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "1.003.00",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"CEC": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图",
"分析方法": "分析方法EDTA-乙酸铵交换法",
"项目分级": "阳离子\n交换量级别",
"分级标准": "分级标准/\n(cmol/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "20.030.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"ECA": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性钙\n含量级别",
"分级标准": "分级标准/\ncmol(½Ca²⁺)/kg",
"标准等级": {
"等级一": "7.49",
"等级二": "4.997.49",
"等级三": "2.504.99",
"等级四": "1.002.50",
"等级五": "≤1.00"
}
},
"EMG": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性镁\n含量级别",
"分级标准": "分级标准/\ncmol(½Mg²⁺)/kg",
"标准等级": {
"等级一": "1.64",
"等级二": "1.231.64",
"等级三": "0.821.23",
"等级四": "0.410.82",
"等级五": "≤0.41"
}
},
"TSE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图",
"分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法",
"项目分级": "全硒\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "0.403.00",
"等级三": "0.170.40",
"等级四": "≤0.17"
}
},
"TN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图",
"分析方法": "分析方法:自动定氮仪法",
"项目分级": "全氮\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.502.00",
"等级三": "1.001.50",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"TP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全磷\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "1.00",
"等级二": "0.801.00",
"等级三": "0.600.80",
"等级四": "0.400.60",
"等级五": "≤0.40"
}
},
"TK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全钾\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AFE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "10.020.0",
"等级三": "5.010.0",
"等级四": "3.05.0",
"等级五": "≤3.0"
}
},
"AK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图",
"分析方法": "分析方法:乙酸钠浸提-火焰光度法",
"项目分级": "速效钾\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "150",
"等级二": "100150",
"等级三": "75100",
"等级四": "5075",
"等级五": "≤50"
}
},
"AP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图",
"分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法",
"项目分级": "有效磷\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "25.040.0",
"等级三": "15.025.0",
"等级四": "5.015.0",
"等级五": "≤5.0"
}
},
"OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "25.035.0",
"等级三": "15.025.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"FL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "粉粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1530",
"等级三": "3045",
"等级四": "4575",
"等级五": "75"
}
},
"NL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "黏粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"SL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "砂粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤30",
"等级二": "3040",
"等级三": "4055",
"等级四": "5585",
"等级五": "85"
}
},
"PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "≤4.5",
"等级二": "4.55.0",
"等级三": "5.05.5",
"等级四": "5.56.0",
"等级五": "6.07.0",
"等级六": "7.07.5",
"等级七": "7.58.0",
"等级八": "8.08.5",
"等级九": "8.5"
}
},
"YXTCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "有效土层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "100",
"等级二": "80100",
"等级三": "6080",
"等级四": "4060",
"等级五": "≤40"
}
},
"GZCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "耕作层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "25.0",
"等级二": "20.025.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"TRRZ": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图",
"分析方法": "分析方法:热鼓风加热环刀法",
"项目分级": "土壤容重\n级别",
"分级标准": "分级标准/\n(g/cm³)",
"标准等级": {
"等级一": "≤0.90",
"等级二": "0.901.00",
"等级三": "1.001.10",
"等级四": "1.101.25",
"等级五": "1.251.35",
"等级六": "1.351.45",
"等级七": "1.451.55",
"等级八": "1.55"
}
},
"LSFD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砾石丰度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "砾石丰度\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤0",
"等级二": "05",
"等级三": "515",
"等级四": "1550",
"等级五": "50"
}
},
"TRZD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分类分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n分类",
"分级标准": "分级标准/\n ",
"标准等级": {
"砂质": "1",
"砂壤质": "2",
"壤质": "3",
"黏壤质": "4",
"黏质": "5"
}
},
"TRZD12": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"砂土及壤质砂土": "1",
"砂质壤土": "2",
"壤土": "3",
"粉(砂)质壤土": "4",
"砂质黏壤土": "5",
"黏壤土": "6",
"粉(砂)质黏壤土": "7",
"砂质黏土": "8",
"壤质黏土": "9",
"粉(砂)质黏土": "10",
"黏土": "11",
"重黏土": "12"
}
},
"二普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"三普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"酸化PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"重度酸化": "1.0",
"中度酸化": "0.51.0",
"轻度酸化": "0.30.5",
"未酸化": "-0.30.3",
"碱化": "-10-0.3"
}
},
"专题图OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "≤10.0",
"等级二": "10.020.0",
"等级三": "20.030.0",
"等级四": "30.040.0",
"等级五": "40.0"
}
}
}
}

View File

@@ -0,0 +1,405 @@
{
"export_config": {
"AB": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图(地块)",
"分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法",
"项目分级": "有效硼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1.00",
"等级二": "0.81.00",
"等级三": "0.500.80",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"ACU": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铜\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.002.00",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"AMN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锰\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "15.030.0",
"等级三": "5.015.0",
"等级四": "1.05.0",
"等级五": "≤1.0"
}
},
"AMO": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图(地块)",
"分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法",
"项目分级": "有效钼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "0.20",
"等级二": "0.150.20",
"等级三": "0.100.15",
"等级四": "0.050.10",
"等级五": "≤0.05"
}
},
"AS1": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图(地块)",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "有效硫\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "30.040.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"AZN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锌\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "1.003.00",
"等级三": "0.501.00",
"等级四": "0.200.50",
"等级五": "≤0.20"
}
},
"CEC": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图(地块)",
"分析方法": "分析方法EDTA-乙酸铵交换法",
"项目分级": "阳离子\n交换量级别",
"分级标准": "分级标准/\n(cmol/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "20.030.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"ECA": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图(地块)",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性钙\n含量级别",
"分级标准": "分级标准/\ncmol(½Ca²⁺)/kg",
"标准等级": {
"等级一": "7.49",
"等级二": "4.997.49",
"等级三": "2.504.99",
"等级四": "1.002.50",
"等级五": "≤1.00"
}
},
"EMG": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图(地块)",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性镁\n含量级别",
"分级标准": "分级标准/\ncmol(½Mg²⁺)/kg",
"标准等级": {
"等级一": "1.64",
"等级二": "1.231.64",
"等级三": "0.821.23",
"等级四": "0.410.82",
"等级五": "≤0.41"
}
},
"TSE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图(地块)",
"分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法",
"项目分级": "全硒\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "0.403.00",
"等级三": "0.170.40",
"等级四": "≤0.17"
}
},
"TN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图(地块)",
"分析方法": "分析方法:自动定氮仪法",
"项目分级": "全氮\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.502.00",
"等级三": "1.001.50",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"TP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图(地块)",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全磷\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "1.00",
"等级二": "0.801.00",
"等级三": "0.600.80",
"等级四": "0.400.60",
"等级五": "≤0.40"
}
},
"TK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图(地块)",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全钾\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "15.020.0",
"等级三": "10.015.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"AFE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "10.020.0",
"等级三": "5.010.0",
"等级四": "3.05.0",
"等级五": "≤3.0"
}
},
"AK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图(地块)",
"分析方法": "分析方法:乙酸钠浸提-火焰光度法",
"项目分级": "速效钾\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "150",
"等级二": "100150",
"等级三": "75100",
"等级四": "5075",
"等级五": "≤50"
}
},
"AP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图(地块)",
"分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法",
"项目分级": "有效磷\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "25.040.0",
"等级三": "15.025.0",
"等级四": "5.015.0",
"等级五": "≤5.0"
}
},
"OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图(地块)",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "25.035.0",
"等级三": "15.025.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"FL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "粉粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1530",
"等级三": "3045",
"等级四": "4575",
"等级五": "75"
}
},
"NL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "黏粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"SL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "砂粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤30",
"等级二": "3040",
"等级三": "4055",
"等级四": "5585",
"等级五": "85"
}
},
"PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图地块",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "≤4.5",
"等级二": "4.55.0",
"等级三": "5.05.5",
"等级四": "5.56.0",
"等级五": "6.07.0",
"等级六": "7.07.5",
"等级七": "7.58.0",
"等级八": "8.08.5",
"等级九": "8.5"
}
},
"YXTCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图(地块)",
"分析方法": "分析方法:剖面法",
"项目分级": "有效土层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "100",
"等级二": "80100",
"等级三": "6080",
"等级四": "4060",
"等级五": "≤40"
}
},
"GZCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图(地块)",
"分析方法": "分析方法:剖面法",
"项目分级": "耕作层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "25.0",
"等级二": "20.025.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"TRRZ": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图(地块)",
"分析方法": "分析方法:热鼓风加热环刀法",
"项目分级": "土壤容重\n级别",
"分级标准": "分级标准/\n(g/cm³)",
"标准等级": {
"等级一": "≤0.90",
"等级二": "0.901.00",
"等级三": "1.001.10",
"等级四": "1.101.25",
"等级五": "1.251.35",
"等级六": "1.351.45",
"等级七": "1.451.55",
"等级八": "1.55"
}
},
"LSFD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砾石丰度分布图(地块)",
"分析方法": "分析方法:剖面法",
"项目分级": "砾石丰度\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤0",
"等级二": "05",
"等级三": "515",
"等级四": "1550",
"等级五": "50"
}
},
"TRZD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分类分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n分类",
"分级标准": "分级标准/\n ",
"标准等级": {
"砂质": "1",
"砂壤质": "2",
"壤质": "3",
"黏壤质": "4",
"黏质": "5"
}
},
"TRZD12": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"砂土及壤质砂土": "1",
"砂质壤土": "2",
"壤土": "3",
"粉(砂)质壤土": "4",
"砂质黏壤土": "5",
"黏壤土": "6",
"粉(砂)质黏壤土": "7",
"砂质黏土": "8",
"壤质黏土": "9",
"粉(砂)质黏土": "10",
"黏土": "11",
"重黏土": "12"
}
},
"二普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤pH分布图地块",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"三普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图地块",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"酸化PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图(地块)",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"重度酸化": "1.0",
"中度酸化": "0.51.0",
"轻度酸化": "0.30.5",
"未酸化": "-0.30.3",
"碱化": "-10-0.3"
}
}
}
}

View File

@@ -0,0 +1,365 @@
{
"export_config": {
"AB": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图",
"分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法",
"项目分级": "有效硼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.002.00",
"等级三": "0.501.00",
"等级四": "0.250.50",
"等级五": "≤0.25"
}
},
"ACU": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铜\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1.80",
"等级二": "1.001.80",
"等级三": "0.201.00",
"等级四": "0.100.20",
"等级五": "≤0.10"
}
},
"AMN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锰\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "50.0",
"等级二": "15.050.0",
"等级三": "7.015.0",
"等级四": "3.07.0",
"等级五": "≤3.0"
}
},
"AMO": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图",
"分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法",
"项目分级": "有效钼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "0.20",
"等级二": "0.150.20",
"等级三": "0.100.15",
"等级四": "0.050.10",
"等级五": "≤0.05"
}
},
"AS1": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "有效硫\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "30.040.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"AZN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锌\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "2.003.00",
"等级三": "1.002.00",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"CEC": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图",
"分析方法": "分析方法EDTA-乙酸铵交换法",
"项目分级": "阳离子\n交换量级别",
"分级标准": "分级标准/\n(cmol/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "20.030.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"ECA": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性钙\n含量级别",
"分级标准": "分级标准/\ncmol(½Ca²⁺)/kg",
"标准等级": {
"等级一": "5.99",
"等级二": "3.995.99",
"等级三": "2.503.99",
"等级四": "1.002.50",
"等级五": "≤1.00"
}
},
"EMG": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性镁\n含量级别",
"分级标准": "分级标准/\ncmol(½Mg²⁺)/kg",
"标准等级": {
"等级一": "2.47",
"等级二": "1.642.47",
"等级三": "0.821.64",
"等级四": "0.410.82",
"等级五": "≤0.41"
}
},
"TSE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图",
"分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法",
"项目分级": "全硒\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "0.403.00",
"等级三": "0.170.40",
"等级四": "≤0.17"
}
},
"TN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图",
"分析方法": "分析方法:自动定氮仪法",
"项目分级": "全氮\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.502.00",
"等级三": "1.001.50",
"等级四": "0.751.00",
"等级五": "≤0.75"
}
},
"TP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全磷\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "1.00",
"等级二": "0.801.00",
"等级三": "0.600.80",
"等级四": "0.400.60",
"等级五": "≤0.40"
}
},
"TK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全钾\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "25.0",
"等级二": "20.025.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"AFE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "10.020.0",
"等级三": "4.510.0",
"等级四": "2.54.5",
"等级五": "≤2.5"
}
},
"AK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图",
"分析方法": "分析方法:乙酸钠浸提-火焰光度法",
"项目分级": "速效钾\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "150",
"等级二": "125150",
"等级三": "100125",
"等级四": "75100",
"等级五": "≤75"
}
},
"AP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图",
"分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法",
"项目分级": "有效磷\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "25.035.0",
"等级三": "15.025.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "25.035.0",
"等级三": "15.025.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"FL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "粉粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1530",
"等级三": "3045",
"等级四": "4575",
"等级五": "75"
}
},
"NL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "黏粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"SL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "砂粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤30",
"等级二": "3040",
"等级三": "4055",
"等级四": "5585",
"等级五": "85"
}
},
"PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.57.5",
"等级二": "5.56.5",
"等级三": "7.58.5",
"等级四": "4.55.5",
"等级五": "8.5,\n≤4.5"
}
},
"YXTCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "有效土层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "100",
"等级二": "80100",
"等级三": "6080",
"等级四": "4060",
"等级五": "≤40"
}
},
"GZCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图",
"分析方法": "分析方法:剖面法",
"项目分级": "耕作层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "20.0",
"等级二": "16.020.0",
"等级三": "12.016.0",
"等级四": "8.012.0",
"等级五": "≤8.0"
}
},
"TRRZ": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图",
"分析方法": "分析方法:热鼓风加热环刀法",
"项目分级": "土壤容重\n级别",
"分级标准": "分级标准/\n(g/cm³)",
"标准等级": {
"等级一": "1.001.20",
"等级二": "1.201.30,\n0.901.00",
"等级三": "1.301.40",
"等级四": "1.401.50",
"等级五": "1.50,\n≤0.90"
}
},
"TRZD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"黏壤质": "1",
"黏质": "2",
"壤质": "3",
"砂壤质": "4",
"砂质": "5"
}
},
"二普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"三普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"酸化PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"重度酸化": "1.0",
"中度酸化": "0.51.0",
"轻度酸化": "0.30.5",
"未酸化": "-0.30.3",
"碱化": "-10-0.3"
}
}
}
}

View File

@@ -0,0 +1,365 @@
{
"export_config": {
"AB": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图(地块)",
"分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法",
"项目分级": "有效硼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.002.00",
"等级三": "0.501.00",
"等级四": "0.250.50",
"等级五": "≤0.25"
}
},
"ACU": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铜\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "1.80",
"等级二": "1.001.80",
"等级三": "0.201.00",
"等级四": "0.100.20",
"等级五": "≤0.10"
}
},
"AMN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锰\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "50.0",
"等级二": "15.050.0",
"等级三": "7.015.0",
"等级四": "3.07.0",
"等级五": "≤3.0"
}
},
"AMO": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图(地块)",
"分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法",
"项目分级": "有效钼\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "0.20",
"等级二": "0.150.20",
"等级三": "0.100.15",
"等级四": "0.050.10",
"等级五": "≤0.05"
}
},
"AS1": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图(地块)",
"分析方法": "分析方法:电感耦合等离子体发射光谱法",
"项目分级": "有效硫\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "40.0",
"等级二": "30.040.0",
"等级三": "20.030.0",
"等级四": "10.020.0",
"等级五": "≤10.0"
}
},
"AZN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效锌\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "2.003.00",
"等级三": "1.002.00",
"等级四": "0.501.00",
"等级五": "≤0.50"
}
},
"CEC": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图(地块)",
"分析方法": "分析方法EDTA-乙酸铵交换法",
"项目分级": "阳离子\n交换量级别",
"分级标准": "分级标准/\n(cmol/kg)",
"标准等级": {
"等级一": "30.0",
"等级二": "20.030.0",
"等级三": "10.020.0",
"等级四": "5.010.0",
"等级五": "≤5.0"
}
},
"ECA": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图(地块)",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性钙\n含量级别",
"分级标准": "分级标准/\ncmol(½Ca²⁺)/kg",
"标准等级": {
"等级一": "5.99",
"等级二": "3.995.99",
"等级三": "2.503.99",
"等级四": "1.002.50",
"等级五": "≤1.00"
}
},
"EMG": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图(地块)",
"分析方法": "分析方法:原子吸收分光光度法",
"项目分级": "交换性镁\n含量级别",
"分级标准": "分级标准/\ncmol(½Mg²⁺)/kg",
"标准等级": {
"等级一": "2.47",
"等级二": "1.642.47",
"等级三": "0.821.64",
"等级四": "0.410.82",
"等级五": "≤0.41"
}
},
"TSE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图(地块)",
"分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法",
"项目分级": "全硒\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "3.00",
"等级二": "0.403.00",
"等级三": "0.170.40",
"等级四": "≤0.17"
}
},
"TN": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图(地块)",
"分析方法": "分析方法:自动定氮仪法",
"项目分级": "全氮\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "2.00",
"等级二": "1.502.00",
"等级三": "1.001.50",
"等级四": "0.751.00",
"等级五": "≤0.75"
}
},
"TP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图(地块)",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全磷\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "1.00",
"等级二": "0.801.00",
"等级三": "0.600.80",
"等级四": "0.400.60",
"等级五": "≤0.40"
}
},
"TK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图(地块)",
"分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法",
"项目分级": "全钾\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "25.0",
"等级二": "20.025.0",
"等级三": "15.020.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"AFE": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图(地块)",
"分析方法": "分析方法二乙三胺五乙酸DTPA浸提法",
"项目分级": "有效铁\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "20.0",
"等级二": "10.020.0",
"等级三": "4.510.0",
"等级四": "2.54.5",
"等级五": "≤2.5"
}
},
"AK": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图(地块)",
"分析方法": "分析方法:乙酸钠浸提-火焰光度法",
"项目分级": "速效钾\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "150",
"等级二": "125150",
"等级三": "100125",
"等级四": "75100",
"等级五": "≤75"
}
},
"AP": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图(地块)",
"分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法",
"项目分级": "有效磷\n含量级别",
"分级标准": "分级标准/\n(mg/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "25.035.0",
"等级三": "15.025.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"OM": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图(地块)",
"分析方法": "分析方法:重铬酸钾氧化-容量法",
"项目分级": "有机质\n含量级别",
"分级标准": "分级标准/\n(g/kg)",
"标准等级": {
"等级一": "35.0",
"等级二": "25.035.0",
"等级三": "15.025.0",
"等级四": "10.015.0",
"等级五": "≤10.0"
}
},
"FL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "粉粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1530",
"等级三": "3045",
"等级四": "4575",
"等级五": "75"
}
},
"NL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "黏粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤15",
"等级二": "1525",
"等级三": "2545",
"等级四": "4565",
"等级五": "65"
}
},
"SL": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "砂粒含量\n级别",
"分级标准": "分级标准/\n(%)",
"标准等级": {
"等级一": "≤30",
"等级二": "3040",
"等级三": "4055",
"等级四": "5585",
"等级五": "85"
}
},
"PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图地块",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.57.5",
"等级二": "5.56.5",
"等级三": "7.58.5",
"等级四": "4.55.5",
"等级五": "8.5,\n≤4.5"
}
},
"YXTCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图(地块)",
"分析方法": "分析方法:剖面法",
"项目分级": "有效土层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "100",
"等级二": "80100",
"等级三": "6080",
"等级四": "4060",
"等级五": "≤40"
}
},
"GZCHD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图(地块)",
"分析方法": "分析方法:剖面法",
"项目分级": "耕作层厚度\n级别",
"分级标准": "分级标准/\n(cm)",
"标准等级": {
"等级一": "20.0",
"等级二": "16.020.0",
"等级三": "12.016.0",
"等级四": "8.012.0",
"等级五": "≤8.0"
}
},
"TRRZ": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图(地块)",
"分析方法": "分析方法:热鼓风加热环刀法",
"项目分级": "土壤容重\n级别",
"分级标准": "分级标准/\n(g/cm³)",
"标准等级": {
"等级一": "1.001.20",
"等级二": "1.201.30,\n0.901.00",
"等级三": "1.301.40",
"等级四": "1.401.50",
"等级五": "1.50,\n≤0.90"
}
},
"TRZD": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图(地块)",
"分析方法": "分析方法:吸管法",
"项目分级": "土壤质地\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"黏壤质": "1",
"黏质": "2",
"壤质": "3",
"砂壤质": "4",
"砂质": "5"
}
},
"二普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤pH分布图地块",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"三普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图地块",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "6.5",
"等级二": "5.56.5",
"等级三": "4.55.5",
"等级四": "≤4.5"
}
},
"酸化PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图(地块)",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"重度酸化": "1.0",
"中度酸化": "0.51.0",
"轻度酸化": "0.30.5",
"未酸化": "-0.30.3",
"碱化": "-10-0.3"
}
}
}
}

View File

@@ -0,0 +1,30 @@
{
"AB": ">1.00;0.81.00;0.500.80;0.200.50;≤0.20",
"ACU": ">2.00;1.002.00;0.501.00;0.200.50;≤0.20",
"AMN": ">30.0;15.030.0;5.015.0;1.05.0;≤1.0",
"AMO": ">0.20;0.150.20;0.100.15;0.050.10;≤0.05",
"AS1": ">40.0;30.040.0;20.030.0;10.020.0;≤10.0",
"AZN": ">3.00;1.003.00;0.501.00;0.200.50;≤0.20",
"CEC": ">30.0;20.030.0;15.020.0;10.015.0;≤10.0",
"ECA": ">7.49;4.997.49;2.504.99;1.002.50;≤1.00",
"EMG": ">1.64;1.231.64;0.821.23;0.410.82;≤0.41",
"TSE": ">3.00;0.403.00;0.170.40;≤0.17",
"TN": ">2.00;1.502.00;1.001.50;0.501.00;≤0.50",
"TP": ">1.00;0.801.00;0.600.80;0.400.60;≤0.40",
"TK": ">20.0;15.020.0;10.015.0;5.010.0;≤5.0",
"AFE": ">20.0;10.020.0;5.010.0;3.05.0;≤3.0",
"AK": ">150;100150;75100;5075;≤50",
"AP": ">40.0;25.040.0;15.025.0;5.015.0;≤5.0",
"OM": ">35.0;25.035.0;15.025.0;10.015.0;≤10.0",
"FL": "≤15;1530;3045;4575;75",
"NL": "≤15;1525;2545;4565;65",
"SL": "≤30;3040;4055;5585;85",
"PH": "6.07.0;7.07.5,5.56.0;7.58.0,5.05.5;8.08.5,4.55.0;8.5,≤4.5",
"YXTCHD": ">100;80100;6080;4060;≤40",
"GZCHD": ">25.0;20.025.0;15.020.0;10.015.0;≤10.0",
"TRRZ": "1.101.25;1.251.35,1.001.10;1.351.45;1.451.55,0.901.00;1.55,≤0.90",
"TRZD": "1;2;3;4;5",
"二普 PH": ">6.5;5.56.5;4.55.5;≤4.5",
"三普 PH": ">6.5;5.56.5;4.55.5;≤4.5",
"酸化 PH": ">1.0;0.51.0;0.30.5;-0.30.3;-10-0.3"
}

View File

@@ -0,0 +1,91 @@
{
"export_config": {
"三普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤三普pH值分级分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "≤4.5",
"等级二": "4.55.0",
"等级三": "5.05.5",
"等级四": "5.56.5",
"等级五": "6.57.5",
"等级六": "7.58.5",
"等级七": "8.59.0",
"等级八": "9.0"
}
},
"二普PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤二普pH值分级分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "≤4.5",
"等级二": "4.55.0",
"等级三": "5.05.5",
"等级四": "5.56.5",
"等级五": "6.57.5",
"等级六": "7.58.5",
"等级七": "8.59.0",
"等级八": "9.0"
}
},
"测土PH": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}测土配方施肥pH值分级分布图",
"分析方法": "分析方法:电位法",
"项目分级": "pH\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"等级一": "≤4.5",
"等级二": "4.55.0",
"等级三": "5.05.5",
"等级四": "5.56.5",
"等级五": "6.57.5",
"等级六": "7.58.5",
"等级七": "8.59.0",
"等级八": "9.0"
}
},
"二普-三普": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤酸化等级分布图(土壤二普-土壤三普)",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"弱酸化": "0.10.3",
"轻度酸化": "0.30.5",
"中度酸化": "0.51.0",
"重度酸化": "1.0",
"其它": "-100.1"
}
},
"二普-测土": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤酸化等级分布图(土壤二普-测土配方施肥)",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"弱酸化": "0.10.3",
"轻度酸化": "0.30.5",
"中度酸化": "0.51.0",
"重度酸化": "1.0",
"其它": "-100.1"
}
},
"测土-三普": {
"项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤酸化等级分布图(测土配方施肥-土壤三普)",
"分析方法": "分析方法:电位法",
"项目分级": "酸化\n级别",
"分级标准": "分级标准/\n ",
"标准等级": {
"弱酸化": "0.10.3",
"轻度酸化": "0.30.5",
"中度酸化": "0.51.0",
"重度酸化": "1.0",
"其它": "-100.1"
}
}
}
}

4
tools/core/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
"""
核心功能模块
包含独立的功能脚本,可以在独立进程中运行
"""

View File

View File

@@ -0,0 +1,610 @@
# -*- coding: utf-8 -*-
import os
import arcpy
import pandas as pd
import numpy as np
from collections import OrderedDict
from openpyxl import Workbook
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
from tools.config.arcgis_field_cal_code import codeblock_cal_shfj, codeblock_dltb_ejdl, codeblock_dltb_yjdl
from tools.core.utils.excel_utils import ExcelStyleUtils
yjdl_order = ["耕地", "园地", "林地", "草地", "其他"]
ejdl_order = ["水田", "旱地", "水浇地", "果园", "茶园", "橡胶园", "其他园地"]
# --- 2. 辅助函数 ---
# 等级计算
def get_acidification_degree(delta_ph):
"""根据ΔpH值判断酸化程度"""
if pd.isna(delta_ph) or delta_ph == 0:
return "-"
# 请根据您的实际分级标准调整这里的阈值
if delta_ph > 1.0:
return "重度酸化"
elif 0.5 < delta_ph <= 1.0:
return "中度酸化"
elif 0.3 < delta_ph <= 0.5:
return "轻度酸化"
elif -0.3 <= delta_ph <= 0.3:
return "未酸化"
else: # dPH < -0.3
return "碱化"
# --- 3. 数据处理与分析 均值---
def process_data_for_table5_3(gdb_path, mean_table_name, sample_table_name):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("【最终版 v2】开始处理数据...")
def clean_df(df, columns):
# ... (此函数不变)
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# --- a. 处理样点数据,计算“样点均值” ---
print("--> 步骤1: 计算样点均值...")
sample_table_path = os.path.join(gdb_path, sample_table_name)
sample_fields = ['YJDL', 'EJDL', 'dPH']
df_samples = pd.DataFrame(arcpy.da.TableToNumPyArray(sample_table_path, sample_fields,'dPH>0.3', skip_nulls=False))
df_samples = clean_df(df_samples, ['YJDL', 'EJDL'])
# 按 YJDL, EJDL 分组,计算 dPH 的均值
df_sample_means = df_samples.groupby(['YJDL', 'EJDL'])['dPH'].mean().reset_index()
df_sample_means.rename(columns={'dPH': '样点均值'}, inplace=True)
print("样点均值计算完成。")
# --- b. 处理制图数据,获取“制图均值”和“制图样点数” ---
print("--> 步骤2: 获取制图均值和样点数...")
mean_table_path = os.path.join(gdb_path, mean_table_name)
# **【核心修改】: 增加读取 COUNT 字段**
mean_fields = ['YJDL', 'EJDL', 'MEAN', 'COUNT']
df_map_data = pd.DataFrame(arcpy.da.TableToNumPyArray(mean_table_path, mean_fields, skip_nulls=False))
df_map_data = clean_df(df_map_data, ['YJDL', 'EJDL'])
df_map_data.rename(columns={'MEAN': '制图均值', 'COUNT': '制图样点数'}, inplace=True)
print("制图数据获取完成。")
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['YJDL', 'EJDL']],
df_map_data[['YJDL', 'EJDL']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['YJDL', 'EJDL'], how='left')
# **【核心修改】: 合并整个 df_map_data而不仅仅是均值列**
df_final = pd.merge(df_final, df_map_data, on=['YJDL', 'EJDL'], how='left')
# --- d. 计算酸化程度 ---
print("--> 步骤4: 计算酸化程度...")
# **【核心修改】: 在计算酸化程度之前,先过滤掉不展示的行**
# 我们只对 dPH 在酸化范围内 ( > 0.3) 的数据感兴趣
# 但为了计算合计,我们需要保留所有数据,所以这一步只计算,不删除
df_final['酸化程度_样本'] = df_final['样点均值'].apply(get_acidification_degree)
df_final['酸化程度_制图'] = df_final['制图均值'].apply(get_acidification_degree)
# (可选) 按“一级地类”和“二级地类”排序
in_ejdl_order = ejdl_order + [x for x in df_final['EJDL'].unique() if x not in ejdl_order]
df_final["YJDL"] = pd.Categorical(df_final['YJDL'], categories=yjdl_order, ordered=True)
df_final["EJDL"] = pd.Categorical(df_final['EJDL'], categories=in_ejdl_order, ordered=True)
df_final.sort_values(['YJDL', 'EJDL'], inplace=True)
print("数据处理流程完成!")
return df_final
# --- 4. Excel 制表 均值---
def write_to_excel_table5_3(df, output_path):
"""
将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同土地利用类型pH变化统计"
# --- b. 绘制表头 ---
ws.merge_cells('A1:B1'); ws['A1'] = '土地利用类型'
ws.merge_cells('C1:F1'); ws['C1'] = 'ΔpH'
ws['A2'] = '一级'
ws['B2'] = '二级'
ws['C2'] = '样点均值'
ws['D2'] = '酸化程度'
ws['E2'] = '制图均值'
ws['F2'] = '酸化程度'
# --- c. 填充数据 ---
current_row = 3
# **【核心修改】: 先对整个DataFrame进行过滤只保留需要展示的行**
# 只有当“样点酸化程度”或“制图酸化程度”不为“未酸化”、“碱化”或“-”时,才展示该行
acid_levels_to_show = ["轻度酸化", "中度酸化", "重度酸化"]
df_to_write = df[
df['酸化程度_样本'].isin(acid_levels_to_show) |
df['酸化程度_制图'].isin(acid_levels_to_show)
].copy() # 使用 .copy() 避免 SettingWithCopyWarning
for yl, group_yl_df in df_to_write.groupby('YJDL', sort=False, observed=False):
print(f"正在写入一级地类: {yl}...")
yl_start_row = current_row
# 遍历该一级地类下的所有“二级地类”
for _, row_data in group_yl_df.iterrows():
ws.cell(row=current_row, column=2).value = row_data['EJDL']
# 填充样点数据
sample_mean = row_data.get('样点均值')
if pd.notna(sample_mean):
ws.cell(row=current_row, column=3).value = f"{sample_mean:.2f}" if sample_mean > 0.3 else "-"
ws.cell(row=current_row, column=4).value = row_data.get('酸化程度_样本', '-') if sample_mean > 0.3 else "-"
else:
ws.cell(row=current_row, column=3).value = "-"
ws.cell(row=current_row, column=4).value = "-"
# 填充制图数据
map_mean = row_data.get('制图均值')
if pd.notna(map_mean):
ws.cell(row=current_row, column=5).value = f"{map_mean:.2f}" if map_mean > 0.3 else "-"
ws.cell(row=current_row, column=6).value = row_data.get('酸化程度_制图', '-') if map_mean > 0.3 else "-"
else:
ws.cell(row=current_row, column=5).value = "-"
ws.cell(row=current_row, column=6).value = "-"
current_row += 1
# 计算并写入“合计”行
if ws.cell(row=current_row-1, column=2).value in ["林地", "草地", "其他"]:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=yl_start_row, end_column=2)
ws.cell(row=yl_start_row, column=1).value = yl
continue
ws.cell(row=current_row, column=2).value = '合计'
# 计算合计行的均值 (均值的均值)
total_sample_mean = group_yl_df['样点均值'].mean()
if pd.notna(total_sample_mean):
ws.cell(row=current_row, column=3).value = f"{total_sample_mean:.2f}"
ws.cell(row=current_row, column=4).value = get_acidification_degree(total_sample_mean)
else:
ws.cell(row=current_row, column=3).value = "-"
ws.cell(row=current_row, column=4).value = "-"
# b. **【核心修正】: 计算合计行的“制图均值”(加权平均)**
# 准备加权平均的分子和分母
weighted_sum = 0
total_count = 0
# 遍历当前一级地类分组中的每一行
for _, row in group_yl_df.iterrows():
mean_val = row.get('制图均值')
count_val = row.get('制图样点数')
# 只有当均值和样点数都存在且有效时,才参与计算
if pd.notna(mean_val) and pd.notna(count_val) and count_val > 0:
weighted_sum += mean_val * count_val # Σ (mean * count)
total_count += count_val # Σ (count)
# 计算加权平均值
weighted_avg = (weighted_sum / total_count) if total_count > 0 else 0
if weighted_avg > 0:
ws.cell(row=current_row, column=5).value = f"{weighted_avg:.2f}"
ws.cell(row=current_row, column=6).value = get_acidification_degree(weighted_avg)
else:
ws.cell(row=current_row, column=5).value = "-"
ws.cell(row=current_row, column=6).value = "-"
# 合并“一级地类”单元格
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
current_row += 1
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
# --- d. 应用样式和调整列宽 ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}{current_row-1}')
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}2', header_font)
print("正在自动调整列宽...")
# 自动调整列宽
ExcelStyleUtils.auto_adjust_column_width(ws)
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
# --- 2. 数据处理与分析 (使用 Pandas) ---
def process_data_for_table5_4(gdb_path, area_table_name, sample_table_name, target_area_dict):
"""
【最终修正版 v2】: 先建立统一的层级结构,再分别合并统计结果。
"""
print("【最终修正版 v2】开始处理数据...")
def clean_df(df, columns):
# ... (此函数不变)
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# --- a. 从两个表中提取并建立唯一的 (YJDL, EJDL) 层级结构 "骨架" ---
print("--> 步骤1: 建立统一的层级结构...")
sample_table_path = os.path.join(gdb_path, sample_table_name)
area_table_path = os.path.join(gdb_path, area_table_name)
df_samples_raw = pd.DataFrame(arcpy.da.TableToNumPyArray(sample_table_path, ['YJDL', 'EJDL'], skip_nulls=False))
df_area_raw = pd.DataFrame(arcpy.da.TableToNumPyArray(area_table_path, ['YJDL', 'EJDL'], skip_nulls=False))
# 清理并合并两个表中的 (YJDL, EJDL) 组合
df_samples_raw = clean_df(df_samples_raw, ['YJDL', 'EJDL'])
df_area_raw = clean_df(df_area_raw, ['YJDL', 'EJDL'])
# 使用 concat 连接两个DataFrame然后用 drop_duplicates 去除重复的组合
df_skeleton = pd.concat([df_samples_raw, df_area_raw]).drop_duplicates().reset_index(drop=True)
if df_skeleton.empty:
print("警告: 无法从源数据中建立任何有效的 (YJDL, EJDL) 层级结构。")
return pd.DataFrame(), {}
print(f"已建立包含 {len(df_skeleton)} 个唯一土壤类型的层级结构。")
# --- b. 独立统计样点数据 ---
print("--> 步骤2: 独立统计样点数据...")
df_samples = pd.DataFrame(arcpy.da.TableToNumPyArray(sample_table_path, ['EJDL', 'YJDL', 'dPH'], skip_nulls=False))
df_samples = clean_df(df_samples, ['YJDL', 'EJDL'])
if not df_samples.empty:
# ... (统计逻辑不变)
bins = [-np.inf, -0.3, 0.3, 0.5, 1.0, np.inf]
labels = ["碱化", "未酸化", "轻度酸化", "中度酸化", "重度酸化"]
df_samples['SHFJ'] = pd.cut(df_samples['dPH'], bins=bins, labels=labels, right=True)
sample_counts = df_samples.groupby(['YJDL', 'EJDL', 'SHFJ'], observed=False).size().reset_index(name='样点数')
ts_total_samples = sample_counts.groupby(['YJDL', 'EJDL'])['样点数'].transform('sum')
sample_counts['样点占比'] = (sample_counts['样点数'] / ts_total_samples) * 100
df_sample_stats = sample_counts.pivot_table(
index=['YJDL', 'EJDL'], columns='SHFJ', values=['样点数', '样点占比'], fill_value=0, observed=False
).reset_index()
df_sample_stats.columns = [f'{col[0]}_{col[1]}'.strip('_') if col[1] else col[0] for col in df_sample_stats.columns]
# 将样点统计结果合并到骨架上
df_final = pd.merge(df_skeleton, df_sample_stats, on=['YJDL', 'EJDL'], how='left')
else:
df_final = df_skeleton.copy()
# --- c. 独立统计面积数据 ---
print("--> 步骤3: 独立统计面积数据...")
df_area = pd.DataFrame(arcpy.da.TableToNumPyArray(area_table_path, ['EJDL', 'YJDL', 'SHFJ', 'AREA'], skip_nulls=False))
df_area = clean_df(df_area, ['YJDL', 'EJDL'])
if not df_area.empty:
# 计算平差系数
landuse_types = {'耕地':'01', '园地':'02', '林地':'03', '草地':'04', '其他':'12'}
df_area['AREA_MU'] = df_area['AREA'] * 0.0015
yjdl_area = df_area.groupby(['YJDL'])['AREA_MU'].sum().reset_index()
yjdl_area.columns = ['YJDL', 'ORIGINAL_TOTAL_MU']
adjustment_factors = []
for _, row in yjdl_area.iterrows():
yjdl = row['YJDL']
original_total = row['ORIGINAL_TOTAL_MU']
target_total = target_area_dict.get(landuse_types[yjdl], original_total) # 如果没有指定,就用原始面积
adjustment_factor = target_total / original_total
adjustment_factors.append({
'YJDL': yjdl,
'原始总面积_亩': original_total,
'目标总面积_亩': target_total,
'平差系数': adjustment_factor
})
factor_df = pd.DataFrame(adjustment_factors)
# 4. 对每个二级地类应用平差系数
# 合并原始数据和平差系数
df_with_factors = df_area.merge(factor_df[['YJDL', '平差系数']], on='YJDL')
df_with_factors['制图面积_亩'] = df_with_factors['AREA_MU'] * df_with_factors['平差系数']
ts_total_area = df_with_factors.groupby(['YJDL', 'EJDL'])['制图面积_亩'].transform('sum')
df_with_factors['面积占比'] = (df_with_factors['制图面积_亩'] / ts_total_area) * 100
df_area_stats = df_with_factors.pivot_table(
index=['YJDL', 'EJDL'], columns='SHFJ', values=['制图面积_亩', '面积占比'], fill_value=0
).reset_index()
df_area_stats.columns = [f'{col[0]}_{col[1]}'.strip('_') if col[1] else col[0] for col in df_area_stats.columns]
# 将面积统计结果合并到 df_final 上
# 注意,这里我们合并到已经包含样点数据的 df_final 上
df_final = pd.merge(df_final, df_area_stats, on=['YJDL', 'EJDL'], how='left')
# --- d. 最后清理和构建映射 ---
df_final.fillna(0, inplace=True)
print("--> 步骤4: 自动构建层级结构...")
dynamic_soil_mapping = df_final.groupby('YJDL')['EJDL'].unique().apply(list).to_dict()
dynamic_soil_mapping = OrderedDict(sorted(dynamic_soil_mapping.items(),key=lambda item: yjdl_order.index(item[0])))
in_ejdl_order = ejdl_order + [x for x in df_final['EJDL'].unique() if x not in ejdl_order]
for yl in dynamic_soil_mapping:
# dynamic_soil_mapping[yl].sort()
dynamic_soil_mapping[yl] = sorted( dynamic_soil_mapping[yl], key=lambda x: in_ejdl_order.index(x))
print("数据处理流程完成!")
return df_final, dynamic_soil_mapping
# --- 3. Excel 制表 面积---
def write_to_excel_table5_4(df, soil_mapping, output_path):
"""
【最终修正版】: 将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel将创建一个空的报告。")
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同类型土壤酸化程度统计"
# --- b. 绘制表头 (不变) ---
ws.merge_cells('A1:B1'); ws['A1'] = '土地利用类型'
ws['A2'] = '一级'
ws['B2'] = '二级'
acid_levels = ['轻度酸化', '中度酸化', '重度酸化']
all_possible_levels = ['碱化', '未酸化', '轻度酸化', '中度酸化', '重度酸化']
acid_level_headers = ['轻度酸化(0.3<ΔpH≤0.5)', '中度酸化(0.5<ΔpH≤1.0)', '重度酸化(ΔpH>1.0)']
col_start = 3
for header in acid_level_headers:
ws.merge_cells(start_row=1, start_column=col_start, end_row=1, end_column=col_start + 3)
ws.cell(row=1, column=col_start).value = header
ws.cell(row=2, column=col_start).value = '样点数/个'
ws.cell(row=2, column=col_start + 1).value = '占比/%'
ws.cell(row=2, column=col_start + 2).value = '制图面积/亩'
ws.cell(row=2, column=col_start + 3).value = '占比/%'
col_start += 4
# --- c. 填充数据 (完全重构的逻辑) ---
current_row = 3
# 使用 .groupby('YJDL', sort=False) 来保证我们之前设置的排序顺序
for yl, ts_list in soil_mapping.items():
# **【关键】** group_yl 是一个只包含当前一级地类数据的子DataFrame
# 我们可以安全地在这个子DataFrame上进行迭代和计算
print(f"正在写入一级地类: {yl}...")
yl_start_row = current_row
# 筛选出当前一级地类的所有数据
group_yl_df = df[df['YJDL'] == yl]
# 1. 遍历该一级地类下的所有“二级地类”并写入数据
for ts in ts_list:
ws.cell(row=current_row, column=2).value = ts
# 在子集中查找当前二级地类的数据行
row_data = group_yl_df[group_yl_df['EJDL'] == ts]
# --- 填充单元格的逻辑开始 ---
col_start = 3 # 从第 C 列开始填充
# 检查是否找到了该土属的数据
if not row_data.empty:
# 如果找到了数据 (row_data 不为空),我们就获取这一行的数据
# .iloc[0] 获取第一行(也是唯一一行)的数据,作为一个 Series 对象
data_series = row_data.iloc[0]
# 遍历每一个酸化等级,填充对应的四列数据
for level in acid_levels:
# 1. 构建要从 data_series 中查找的列名
sample_col = f'样点数_{level}'
sample_pct_col = f'样点占比_{level}'
area_col = f'制图面积_亩_{level}'
area_pct_col = f'面积占比_{level}'
# 2. 从 data_series 中安全地获取值
# 使用 .get(key, default_value) 的好处是,如果列名不存在,它会返回默认值(0),而不会报错
sample_val = data_series.get(sample_col, 0)
sample_pct_val = data_series.get(sample_pct_col, 0)
area_val = data_series.get(area_col, 0)
area_pct_val = data_series.get(area_pct_col, 0)
# 3. 将获取到的值填入单元格
# - 对于数值我们判断它是否大于0。如果是就填入数值否则填入 "-"
# - 对于样点数,我们将其转为整数
# - 对于占比和面积,我们保留两位小数
# 样点数/个
ws.cell(row=current_row, column=col_start).value = int(sample_val) if sample_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=col_start + 1).value = f"{sample_pct_val:.2f}%" if sample_val > 0 else "-"
# 制图面积/万亩
ws.cell(row=current_row, column=col_start + 2).value = f"{area_val:.0f}" if area_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=col_start + 3).value = f"{area_pct_val:.2f}%" if area_val > 0 else "-"
# 移动到下一个酸化等级的起始列
col_start += 4
else:
# 如果没有找到该土属的数据 (row_data 为空)
# 这意味着该土属在源数据中不存在任何样点或面积信息
# 我们将整行所有统计单元格都填充为 "-"
# acid_levels 列表包含3个等级每个等级4列总共12列
for _ in range(len(acid_levels) * 4):
ws.cell(row=current_row, column=col_start).value = "-"
col_start += 1
# --- 填充单元格的逻辑结束 ---
# 完成一行填充后行号加1为下一行做准备
current_row += 1
# 2. 计算并写入这个一级地类的“合计”行
if ws.cell(row=current_row-1, column=2).value in ["林地","草地", "其他"]:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=yl_start_row, end_column=2)
ws.cell(row=yl_start_row, column=1).value = yl
continue
ws.cell(row=current_row, column=2).value = '合计'
# 计算总样点数和总面积,仅针对当前 group_yl
yl_grand_total_samples = 0
for lvl in all_possible_levels:
if f'样点数_{lvl}' in group_yl_df:
yl_grand_total_samples += group_yl_df[f'样点数_{lvl}'].sum()
yl_grand_total_area = 0
for lvl in all_possible_levels:
if f'制图面积_亩_{lvl}' in group_yl_df:
yl_grand_total_area += group_yl_df[f'制图面积_亩_{lvl}'].sum()
col_start = 3
for level in acid_levels:
sample_sum = group_yl_df.get(f'样点数_{level}', 0).sum()
col_name = f'制图面积_亩_{level}'
area_sum = group_yl_df[col_name].sum() if col_name in group_yl_df else 0
# area_sum = group_yl_df.get(f'制图面积_亩_{level}', 0).sum()
sample_perc = (sample_sum / yl_grand_total_samples * 100) if yl_grand_total_samples > 0 else 0
area_perc = (area_sum / yl_grand_total_area * 100) if yl_grand_total_area > 0 else 0
ws.cell(row=current_row, column=col_start).value = int(sample_sum) if sample_sum > 0 else "-"
ws.cell(row=current_row, column=col_start + 1).value = f"{sample_perc:.2f}%" if sample_sum > 0 else "-"
ws.cell(row=current_row, column=col_start + 2).value = f"{area_sum:.0f}" if area_sum > 0 else "-"
ws.cell(row=current_row, column=col_start + 3).value = f"{area_perc:.2f}%" if area_sum > 0 else "-"
col_start += 4
# 3. 合并“一级地类”单元格
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
current_row += 1
# --- a. 定义样式 (不变) ---
header_font = Font(name='等线', size=11, bold=True)
# --- d. 应用样式和调整列宽 (最终健壮版) ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}{current_row-1}')
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}2', header_font)
print("正在自动调整列宽...")
# 调整列宽
ExcelStyleUtils.auto_adjust_column_width(ws)
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
def main(gdb_path:str, ph_features:str,dltb_class_feature:str, shph_tif:str, output_path:str,target_areas_dict:dict):
try:
# --- 1. 用户配置 ---
# 输出配置
output_excel_path = os.path.join(output_path, "土地利用类型酸化统计表.xlsx") # 生成的Excel报告文件路径
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
sample_table_name = "历史样点PH信息_Table" # 图2: 样点信息表名
in_zone_feature = dltb_class_feature # 地类图斑
in_class_feature = ph_features # 已重分类好的酸化PH图层
in_value_raster = shph_tif # 赋值栅格,酸化PH栅格
out_table_area = r"土地利用类型_酸化面积表" # 输出的面积统计表名
out_table_mean = r"土地利用类型_酸化均值表" # 输出的均值表名
print("开始处理数据...")
if not arcpy.Exists(out_table_area):
# 判断输入表是否存在SHFJ字段
try:
if not arcpy.ListFields(in_zone_feature, "EJDL"):
arcpy.management.CalculateField(in_zone_feature, "EJDL", "calculate_ejdl(!DLBM!,!DLMC!)", "PYTHON3", codeblock_dltb_ejdl)
arcpy.management.CalculateField(in_zone_feature, "YJDL", "calculate_yjdl(!DLBM!)", "PYTHON3", codeblock_dltb_yjdl)
if not arcpy.ListFields(in_class_feature, "SHFJ"):
arcpy.management.CalculateField(in_class_feature, "SHFJ", "calculate_shfj(!gridcode!)", "PYTHON3", codeblock_cal_shfj)
except Exception as e:
print(f"计算SHFJ字段时发生错误: {e}")
# 拿到地类图斑的坐标系
desc = arcpy.Describe(in_zone_feature)
spatial_ref = desc.spatialReference
# 1.用arcpy.analysis.TabulateIntersection进行交集制表,面积使用地类图斑投影坐标系下面积
with arcpy.EnvManager(outputCoordinateSystem=spatial_ref):
arcpy.analysis.TabulateIntersection(
in_zone_feature,
["YJDL", "EJDL"],
in_class_feature,
out_table_area,
"SHFJ",
out_units="SQUARE_METERS",
)
if not arcpy.Exists(out_table_mean):
# 判断输入表是否存在YJDL_EJDL字段
if not arcpy.ListFields(in_zone_feature, "YJDL_EJDL"):
# 如果不存在,则添加该字段
arcpy.management.AddField(in_zone_feature, "YJDL_EJDL", "TEXT")
# 计算YJDL_EJDL字段的值
arcpy.management.CalculateField(in_zone_feature,"YJDL_EJDL","!YJDL! + '_' + !EJDL!","PYTHON3")
# 2.用arcpy.sa.ZonalStatisticsAsTable进行区域统计
mean_table = arcpy.sa.ZonalStatisticsAsTable(
in_zone_feature, "YJDL_EJDL", in_value_raster, out_table_mean, "DATA", "MEAN"
)
# 2.1 添加土壤类型字段并计算
arcpy.management.AddFields(
out_table_mean,
[["YJDL", "TEXT"],["EJDL", "TEXT"]],
)
arcpy.management.CalculateField(mean_table, "YJDL", "!YJDL_EJDL!.split('_')[0]", "PYTHON3")
arcpy.management.CalculateField(mean_table, "EJDL", "!YJDL_EJDL!.split('_')[1]", "PYTHON3")
# 生成表5.4的面积统计Excel报告
final_dataframe, soil_structure = process_data_for_table5_4(gdb_path, out_table_area, sample_table_name,target_areas_dict)
write_to_excel_table5_4(final_dataframe, soil_structure, output_excel_path)
# 生成表5.3的均值统计Excel报告
final_mean_dataframe = process_data_for_table5_3(gdb_path, out_table_mean, sample_table_name)
write_to_excel_table5_3(final_mean_dataframe, output_excel_path.replace(".xlsx", "_mean.xlsx"))
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
import gc
gc.collect()
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# main()

View File

@@ -0,0 +1,629 @@
# -*- coding: utf-8 -*-
import os
import arcpy
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
from tools.config.arcgis_field_cal_code import codeblock_cal_shfj
from tools.core.utils.excel_utils import ExcelStyleUtils
from tools.config.custom_sort import yl_order, ts_order
# --- 2. 辅助函数 ---
# 获取要素类各酸化等级面积
def get_acid_area_by_group(target_area_df):
try:
# 转为numpy数组供pandas统计使用
df = target_area_df.copy()
area_by_group = df.groupby("SHFJ")["AREA_MU"].sum()
for key in area_by_group.keys():
area_by_group[key] = area_by_group[key]
return area_by_group.to_dict()
except Exception as e:
print(f"计算面积时出错: {str(e)}")
return None
def apply_adjustment_by_each_level(df, target_area_dict):
"""
对DataFrame中的面积数据按每一个酸化等级独立进行平差。
参数:
df (pd.DataFrame): 包含面积统计的DataFrame。
target_area_dict (dict): 每个酸化等级的目标总面积字典。
例如: {'轻度酸化': 10000.0, '中度酸化': 8000.0, ...}
"""
print("\n开始按每个酸化等级独立进行平差...")
df_adjusted = df.copy()
for level, target_area in target_area_dict.items():
col_name = f'制图面积_亩_{level}'
adjusted_col_name = f'平差后面积_亩_{level}'
if col_name not in df.columns:
print(f"警告: 未找到列 '{col_name}',跳过该等级平差。")
if adjusted_col_name not in df_adjusted.columns:
df_adjusted[adjusted_col_name] = 0 # 创建一个空列
continue
# a. 计算该等级的实际总面积
actual_area = df_adjusted[col_name].sum()
if actual_area > 0:
# b. 计算误差
error = target_area - actual_area
print(f"等级 '{level}': 目标面积={target_area:.2f}, 实际面积={actual_area:.2f}, 误差={error:.2f}")
# c. 按比例分配误差
adjustment = error * (df_adjusted[col_name] / actual_area)
df_adjusted[adjusted_col_name] = df_adjusted[col_name] + adjustment
df_adjusted[adjusted_col_name] = df_adjusted[adjusted_col_name].clip(lower=0)
else:
df_adjusted[adjusted_col_name] = df_adjusted[col_name]
print("按每个酸化等级独立平差完成。")
return df_adjusted
# 获取酸化程度
def get_acidification_degree(delta_ph):
"""根据ΔpH值判断酸化程度"""
if pd.isna(delta_ph) or delta_ph == 0:
return "-"
# 请根据您的实际分级标准调整这里的阈值
if delta_ph > 1.0:
return "重度酸化"
elif 0.5 < delta_ph <= 1.0:
return "中度酸化"
elif 0.3 < delta_ph <= 0.5:
return "轻度酸化"
elif -0.3 <= delta_ph <= 0.3:
return "未酸化"
else: # dPH < -0.3
return "碱化"
# --- 3. 数据处理与分析 均值表---
def process_data_for_table5_5(gdb_path, mean_table_name, sample_table_name):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("【最终版 v2】开始处理数据...")
def clean_df(df, columns):
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# --- a. 处理样点数据,计算“样点均值” ---
print("--> 步骤1: 计算样点均值...")
sample_table_path = os.path.join(gdb_path, sample_table_name)
sample_fields = ['YL', 'TS', 'dPH']
df_samples = pd.DataFrame(arcpy.da.TableToNumPyArray(sample_table_path, sample_fields, 'dPH>0.3', skip_nulls=False))
df_samples = clean_df(df_samples, ['YL', 'TS'])
# 按 YL, TS 分组,计算 dPH 的均值
df_sample_means = df_samples.groupby(['YL', 'TS'])['dPH'].mean().reset_index()
df_sample_means.rename(columns={'dPH': '样点均值'}, inplace=True)
print("样点均值计算完成。")
# --- b. 处理制图数据,获取“制图均值”和“制图样点数” ---
print("--> 步骤2: 获取制图均值和样点数...")
mean_table_path = os.path.join(gdb_path, mean_table_name)
mean_fields = ['YL', 'TS', 'MEAN', 'COUNT']
df_map_data = pd.DataFrame(arcpy.da.TableToNumPyArray(mean_table_path, mean_fields, skip_nulls=False))
df_map_data = clean_df(df_map_data, ['YL', 'TS'])
df_map_data.rename(columns={'MEAN': '制图均值', 'COUNT': '制图样点数'}, inplace=True)
print("制图数据获取完成。")
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['YL', 'TS']],
df_map_data[['YL', 'TS']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['YL', 'TS'], how='left')
# **【核心修改】: 合并整个 df_map_data而不仅仅是均值列**
df_final = pd.merge(df_final, df_map_data, on=['YL', 'TS'], how='left')
# --- d. 计算酸化程度 ---
print("--> 步骤4: 计算酸化程度...")
# **【核心修改】: 在计算酸化程度之前,先过滤掉不展示的行**
# 我们只对 dPH 在酸化范围内 ( > 0.3) 的数据感兴趣
# 但为了计算合计,我们需要保留所有数据,所以这一步只计算,不删除
df_final['酸化程度_样本'] = df_final['样点均值'].apply(get_acidification_degree)
df_final['酸化程度_制图'] = df_final['制图均值'].apply(get_acidification_degree)
# (可选) 按“亚类”和“土属”排序
in_yl_order = yl_order + [x for x in df_final['YL'].unique() if x not in yl_order]
in_ts_order = ts_order + [x for x in df_final['TS'].unique() if x not in ts_order]
df_final["YL"] = pd.Categorical(df_final['YL'], categories=in_yl_order, ordered=True)
df_final["TS"] = pd.Categorical(df_final['TS'], categories=in_ts_order, ordered=True)
df_final.sort_values(['YL', 'TS'], inplace=True)
print("数据处理流程完成!")
return df_final
# --- 4. Excel 制表 均值表---
def write_to_excel_table5_5(df, output_path):
"""
将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同类型土壤pH变化统计"
# --- b. 绘制表头 ---
ws.merge_cells('A1:A2'); ws['A1'] = '亚类'
ws.merge_cells('B1:B2'); ws['B1'] = '土属'
ws.merge_cells('C1:F1'); ws['C1'] = 'ΔpH'
ws['C2'] = '样点均值'
ws['D2'] = '酸化程度'
ws['E2'] = '制图均值'
ws['F2'] = '酸化程度'
# --- c. 填充数据 ---
current_row = 3
# **【核心修改】: 先对整个DataFrame进行过滤只保留需要展示的行**
# 只有当“样点酸化程度”或“制图酸化程度”不为“未酸化”、“碱化”或“-”时,才展示该行
acid_levels_to_show = ["轻度酸化", "中度酸化", "重度酸化"]
df_to_write = df[
df['酸化程度_样本'].isin(acid_levels_to_show) |
df['酸化程度_制图'].isin(acid_levels_to_show)
].copy() # 使用 .copy() 避免 SettingWithCopyWarning
for yl, group_yl_df in df_to_write.groupby('YL', observed=True, sort=False):
print(f"正在写入亚类: {yl}...")
yl_start_row = current_row
# 遍历该亚类下的所有“土属”
for _, row_data in group_yl_df.iterrows():
ws.cell(row=current_row, column=2).value = row_data['TS']
# 填充样点数据
sample_mean = row_data.get('样点均值')
if pd.notna(sample_mean):
ws.cell(row=current_row, column=3).value = f"{sample_mean:.2f}" if sample_mean > 0.3 else "-"
ws.cell(row=current_row, column=4).value = row_data.get('酸化程度_样本', '-') if sample_mean > 0.3 else "-"
else:
ws.cell(row=current_row, column=3).value = "-"
ws.cell(row=current_row, column=4).value = "-"
# 填充制图数据
map_mean = row_data.get('制图均值')
if pd.notna(map_mean):
ws.cell(row=current_row, column=5).value = f"{map_mean:.2f}" if map_mean > 0.3 else "-"
ws.cell(row=current_row, column=6).value = row_data.get('酸化程度_制图', '-') if map_mean > 0.3 else "-"
else:
ws.cell(row=current_row, column=5).value = "-"
ws.cell(row=current_row, column=6).value = "-"
current_row += 1
# 计算并写入“合计”行
ws.cell(row=current_row, column=2).value = '合计'
# 计算合计行的均值 (均值的均值)
total_sample_mean = group_yl_df['样点均值'].mean()
if pd.notna(total_sample_mean):
ws.cell(row=current_row, column=3).value = f"{total_sample_mean:.2f}"
ws.cell(row=current_row, column=4).value = get_acidification_degree(total_sample_mean)
else:
ws.cell(row=current_row, column=3).value = "-"
ws.cell(row=current_row, column=4).value = "-"
# b. **【核心修正】: 计算合计行的“制图均值”(加权平均)**
# 准备加权平均的分子和分母
weighted_sum = 0
total_count = 0
# 遍历当前亚类分组中的每一行
for _, row in group_yl_df.iterrows():
mean_val = row.get('制图均值')
count_val = row.get('制图样点数')
# 只有当均值和样点数都存在且有效时,才参与计算
if pd.notna(mean_val) and pd.notna(count_val) and count_val > 0:
weighted_sum += mean_val * count_val # Σ (mean * count)
total_count += count_val # Σ (count)
# 计算加权平均值
weighted_avg = (weighted_sum / total_count) if total_count > 0 else 0
if weighted_avg > 0:
ws.cell(row=current_row, column=5).value = f"{weighted_avg:.2f}"
ws.cell(row=current_row, column=6).value = get_acidification_degree(weighted_avg)
else:
ws.cell(row=current_row, column=5).value = "-"
ws.cell(row=current_row, column=6).value = "-"
# 合并“亚类”单元格
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
current_row += 1
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
# --- d. 应用样式和调整列宽 (最终健壮版) ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}{current_row-1}')
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}2', header_font)
print("正在自动调整列宽...")
# 自动调整列宽
ExcelStyleUtils.auto_adjust_column_width(ws)
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
# --- 2. 数据处理与分析 (面积统计表) ---
def process_data_final(gdb_path, area_table_name, sample_table_name):
"""
【最终修正版 v2】: 先建立统一的层级结构,再分别合并统计结果。
"""
print("【最终修正版 v2】开始处理数据...")
def clean_df(df, columns):
# ... (此函数不变)
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# --- a. 从两个表中提取并建立唯一的 (YL, TS) 层级结构 "骨架" ---
print("--> 步骤1: 建立统一的层级结构...")
sample_table_path = os.path.join(gdb_path, sample_table_name)
area_table_path = os.path.join(gdb_path, area_table_name)
df_samples_raw = pd.DataFrame(arcpy.da.TableToNumPyArray(sample_table_path, ['YL', 'TS'], skip_nulls=False))
df_area_raw = pd.DataFrame(arcpy.da.TableToNumPyArray(area_table_path, ['YL', 'TS'], skip_nulls=False))
# 清理并合并两个表中的 (YL, TS) 组合
df_samples_raw = clean_df(df_samples_raw, ['YL', 'TS'])
df_area_raw = clean_df(df_area_raw, ['YL', 'TS'])
# 使用 concat 连接两个DataFrame然后用 drop_duplicates 去除重复的组合
df_skeleton = pd.concat([df_samples_raw, df_area_raw]).drop_duplicates().reset_index(drop=True)
if df_skeleton.empty:
print("警告: 无法从源数据中建立任何有效的 (YL, TS) 层级结构。")
return pd.DataFrame(), {}
print(f"已建立包含 {len(df_skeleton)} 个唯一土壤类型的层级结构。")
# --- b. 独立统计样点数据 ---
print("--> 步骤2: 独立统计样点数据...")
df_samples = pd.DataFrame(arcpy.da.TableToNumPyArray(sample_table_path, ['TS', 'YL', 'dPH'], skip_nulls=False))
df_samples = clean_df(df_samples, ['YL', 'TS'])
if not df_samples.empty:
bins = [-np.inf, -0.3, 0.3, 0.5, 1.0, np.inf]
labels = ["碱化", "未酸化", "轻度酸化", "中度酸化", "重度酸化"]
df_samples['SHFJ'] = pd.cut(df_samples['dPH'], bins=bins, labels=labels, right=True)
sample_counts = df_samples.groupby(['YL', 'TS', 'SHFJ'], observed=False).size().reset_index(name='样点数')
ts_total_samples = sample_counts.groupby(['YL', 'TS'])['样点数'].transform('sum')
sample_counts['样点占比'] = (sample_counts['样点数'] / ts_total_samples) * 100
df_sample_stats = sample_counts.pivot_table(
index=['YL', 'TS'], columns='SHFJ', values=['样点数', '样点占比'], fill_value=0, observed=False
).reset_index()
df_sample_stats.columns = [f'{col[0]}_{col[1]}'.strip('_') if col[1] else col[0] for col in df_sample_stats.columns]
# 将样点统计结果合并到骨架上
df_final = pd.merge(df_skeleton, df_sample_stats, on=['YL', 'TS'], how='left')
else:
df_final = df_skeleton.copy()
# --- c. 独立统计面积数据 ---
print("--> 步骤3: 独立统计面积数据...")
df_area = pd.DataFrame(arcpy.da.TableToNumPyArray(area_table_path, ['TS', 'YL', 'SHFJ', 'AREA'], skip_nulls=False))
df_area = clean_df(df_area, ['YL', 'TS'])
if not df_area.empty:
df_area['制图面积_亩'] = df_area['AREA'] * 0.0015
ts_total_area = df_area.groupby(['YL', 'TS'])['制图面积_亩'].transform('sum')
df_area['面积占比'] = (df_area['制图面积_亩'] / ts_total_area) * 100
df_area_stats = df_area.pivot_table(
index=['YL', 'TS'], columns='SHFJ', values=['制图面积_亩', '面积占比'], fill_value=0
).reset_index()
df_area_stats.columns = [f'{col[0]}_{col[1]}'.strip('_') if col[1] else col[0] for col in df_area_stats.columns]
# 将面积统计结果合并到 df_final 上
# 注意,这里我们合并到已经包含样点数据的 df_final 上
df_final = pd.merge(df_final, df_area_stats, on=['YL', 'TS'], how='left')
# --- d. 最后清理和构建映射 ---
df_final.fillna(0, inplace=True)
print("--> 步骤4: 自动构建层级结构...")
in_yl_order = yl_order + [x for x in df_final['YL'].unique() if x not in yl_order]
in_ts_order = ts_order + [x for x in df_final['TS'].unique() if x not in ts_order]
df_final["YL"] = pd.Categorical(df_final['YL'], categories=in_yl_order, ordered=True)
df_final["TS"] = pd.Categorical(df_final['TS'], categories=in_ts_order, ordered=True)
df_final.sort_values(['YL', 'TS'], inplace=True)
dynamic_soil_mapping = df_final.groupby('YL', observed=True)['TS'].unique().apply(list).to_dict()
# for yl in dynamic_soil_mapping:
# dynamic_soil_mapping[yl].sort()
print("数据处理流程完成!")
return df_final, dynamic_soil_mapping
# --- 3. Excel 制表 面积统计表 ---
def write_to_excel(df, soil_mapping, output_path):
"""
【最终修正版】: 将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel将创建一个空的报告。")
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同类型土壤酸化程度统计"
# --- b. 绘制表头 (不变) ---
ws.merge_cells('A1:A2'); ws['A1'] = '亚类'
ws.merge_cells('B1:B2'); ws['B1'] = '土属'
acid_levels = ['轻度酸化', '中度酸化', '重度酸化']
all_possible_levels = ['碱化', '未酸化', '轻度酸化', '中度酸化', '重度酸化']
acid_level_headers = ['轻度酸化(0.3<ΔpH≤0.5)', '中度酸化(0.5<ΔpH≤1.0)', '重度酸化(ΔpH>1.0)']
col_start = 3
for header in acid_level_headers:
ws.merge_cells(start_row=1, start_column=col_start, end_row=1, end_column=col_start + 3)
ws.cell(row=1, column=col_start).value = header
ws.cell(row=2, column=col_start).value = '样点数/个'
ws.cell(row=2, column=col_start + 1).value = '占比/%'
ws.cell(row=2, column=col_start + 2).value = '制图面积/亩'
ws.cell(row=2, column=col_start + 3).value = '占比/%'
col_start += 4
# --- c. 填充数据 (完全重构的逻辑) ---
current_row = 3
# 使用 .groupby('YL', sort=False) 来保证我们之前设置的排序顺序
for yl, ts_list in soil_mapping.items():
# **【关键】** group_yl 是一个只包含当前亚类数据的子DataFrame
# 我们可以安全地在这个子DataFrame上进行迭代和计算
print(f"正在写入亚类: {yl}...")
yl_start_row = current_row
# 筛选出当前亚类的所有数据
group_yl_df = df[df['YL'] == yl]
# 1. 遍历该亚类下的所有“土属”并写入数据
for ts in ts_list:
ws.cell(row=current_row, column=2).value = ts
# 在子集中查找当前土属的数据行
row_data = group_yl_df[group_yl_df['TS'] == ts]
# --- 填充单元格的逻辑开始 ---
col_start = 3 # 从第 C 列开始填充
# 检查是否找到了该土属的数据
if not row_data.empty:
# 如果找到了数据 (row_data 不为空),我们就获取这一行的数据
# .iloc[0] 获取第一行(也是唯一一行)的数据,作为一个 Series 对象
data_series = row_data.iloc[0]
# 遍历每一个酸化等级,填充对应的四列数据
for level in acid_levels:
# 1. 构建要从 data_series 中查找的列名
sample_col = f'样点数_{level}'
sample_pct_col = f'样点占比_{level}'
area_col = f'平差后面积_亩_{level}'
area_pct_col = f'面积占比_{level}'
# 2. 从 data_series 中安全地获取值
# 使用 .get(key, default_value) 的好处是,如果列名不存在,它会返回默认值(0),而不会报错
sample_val = data_series.get(sample_col, 0)
sample_pct_val = data_series.get(sample_pct_col, 0)
area_val = data_series.get(area_col, 0)
area_pct_val = data_series.get(area_pct_col, 0)
# 3. 将获取到的值填入单元格
# - 对于数值我们判断它是否大于0。如果是就填入数值否则填入 "-"
# - 对于样点数,我们将其转为整数
# - 对于占比和面积,我们保留两位小数
# 样点数/个
ws.cell(row=current_row, column=col_start).value = int(sample_val) if sample_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=col_start + 1).value = f"{sample_pct_val:.2f}%" if sample_val > 0 else "-"
# 制图面积/亩
ws.cell(row=current_row, column=col_start + 2).number_format = "0.00"
ws.cell(row=current_row, column=col_start + 2).value = f"{area_val:.0f}" if area_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=col_start + 3).value = f"{area_pct_val:.2f}%" if area_val > 0 else "-"
# 移动到下一个酸化等级的起始列
col_start += 4
else:
# 如果没有找到该土属的数据 (row_data 为空)
# 这意味着该土属在源数据中不存在任何样点或面积信息
# 我们将整行所有统计单元格都填充为 "-"
# acid_levels 列表包含3个等级每个等级4列总共12列
for _ in range(len(acid_levels) * 4):
ws.cell(row=current_row, column=col_start).value = "-"
col_start += 1
# --- 填充单元格的逻辑结束 ---
# 完成一行填充后行号加1为下一行做准备
current_row += 1
# 2. 计算并写入这个亚类的“合计”行
ws.cell(row=current_row, column=2).value = '合计'
# 计算总样点数和总面积,仅针对当前 group_yl
yl_grand_total_samples = 0
for lvl in all_possible_levels:
if f'样点数_{lvl}' in group_yl_df:
yl_grand_total_samples += group_yl_df[f'样点数_{lvl}'].sum()
yl_grand_total_area = 0
for lvl in all_possible_levels:
if f'制图面积_亩_{lvl}' in group_yl_df:
yl_grand_total_area += group_yl_df[f'制图面积_亩_{lvl}'].sum()
col_start = 3
for level in acid_levels:
sample_sum = group_yl_df.get(f'样点数_{level}', 0).sum()
col_name = f'制图面积_亩_{level}'
area_sum = group_yl_df[col_name].sum() if col_name in group_yl_df else 0
# area_sum = group_yl_df.get(f'平差后面积_亩_{level}', 0).sum()
sample_perc = (sample_sum / yl_grand_total_samples * 100) if yl_grand_total_samples > 0 else 0
area_perc = (area_sum / yl_grand_total_area * 100) if yl_grand_total_area > 0 else 0
ws.cell(row=current_row, column=col_start).value = int(sample_sum) if sample_sum > 0 else "-"
ws.cell(row=current_row, column=col_start + 1).value = f"{sample_perc:.2f}%" if sample_sum > 0 else "-"
ws.cell(row=current_row, column=col_start + 2).value = f"{area_sum:.0f}" if area_sum > 0 else "-"
ws.cell(row=current_row, column=col_start + 3).value = f"{area_perc:.2f}%" if area_sum > 0 else "-"
col_start += 4
# 3. 合并“亚类”单元格
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
current_row += 1
# --- a. 定义样式 (不变) ---
header_font = Font(name='等线', size=11, bold=True)
# d. 应用样式和调整列宽
max_col = 2 + len(acid_levels) * 4
if current_row > 1: # 确保有数据才应用样式
ExcelStyleUtils.set_style(ws, f'A1:{get_column_letter(max_col)}{current_row-1}')
ExcelStyleUtils.set_style(ws, f'A1:{get_column_letter(max_col)}2', header_font)
# 调整列宽
ExcelStyleUtils.auto_adjust_column_width(ws)
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
def main(gdb_path, trlx_polygon, sh_ph_polygon, ph_raster, output_path, target_areas_df):
try:
# --- 1. 用户配置 ---
sample_table_name = "历史样点PH信息_Table" # 图2: 样点信息表名
# 输出配置
output_excel_path = os.path.join(output_path, "土壤类型酸化统计表.xlsx") # 生成的Excel报告文件路径
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
in_zone_feature = trlx_polygon # 土壤类型图
# in_class_feature = sh_ph_polygon # 已重分类好的酸化PH图层
in_class_feature = "最小面积统计单元"
in_value_raster = ph_raster # 酸化PH栅格
dltb_ph_statstable = "土地利用类型_酸化面积表" # 土壤类型_酸化面积表gdb table
out_table_area = r"土壤类型_酸化面积表" # 输出的交集表名
out_table_mean = r"土壤类型_酸化均值表" # 输出的均值表名
print("开始处理数据...")
if not arcpy.Exists(out_table_area):
# 判断输入表是否存在SHFJ字段
try:
arcpy.management.CalculateField(in_class_feature, "SHFJ", "calculate_shfj(!gridcode!)", "PYTHON3", codeblock_cal_shfj)
except Exception as e:
print(f"计算SHFJ字段时发生错误: {e}")
# 1.用arcpy.analysis.TabulateIntersection进行交集制表
arcpy.analysis.TabulateIntersection(
in_zone_feature,
["TS", "YL"],
in_class_feature,
out_table_area,
"SHFJ",
out_units="SQUARE_METERS",
)
if not arcpy.Exists(out_table_mean):
# 判断输入表是否存在YL_TS字段
if not arcpy.ListFields(in_zone_feature, "YL_TS"):
# 如果不存在,则添加该字段
arcpy.management.AddField(in_zone_feature, "YL_TS", "TEXT")
# 计算YL_TS字段的值
arcpy.management.CalculateField(in_zone_feature,"YL_TS","!YL! + '_' + !TS!","PYTHON3")
# 2.用arcpy.sa.ZonalStatisticsAsTable进行区域统计
mean_table = arcpy.sa.ZonalStatisticsAsTable(
in_zone_feature, "YL_TS", in_value_raster, out_table_mean, "DATA", "MEAN"
)
# 2.1 添加土壤类型字段并计算
arcpy.management.AddFields(
out_table_mean,
[["YL", "TEXT"],["TS", "TEXT"]],
)
arcpy.management.CalculateField(mean_table, "YL", "!YL_TS!.split('_')[0]", "PYTHON3")
arcpy.management.CalculateField(mean_table, "TS", "!YL_TS!.split('_')[1]", "PYTHON3")
# 生成表5.4的面积统计Excel报告
final_dataframe, soil_structure = process_data_final(gdb_path, out_table_area, sample_table_name)
# 统计地类图斑酸化总面积亩
each_acid_area = get_acid_area_by_group(target_areas_df)
print(f"容县土壤类型图斑总 acid 总面积(亩):{each_acid_area}")
# 执行平差计算
if each_acid_area:
adjusted_dataframe = apply_adjustment_by_each_level(final_dataframe, each_acid_area)
print("使用平差值进行修正!")
write_to_excel(adjusted_dataframe, soil_structure, output_excel_path)
else:
print("未使用平差值进行修正!")
write_to_excel(final_dataframe, soil_structure, output_excel_path)
# 生成表5.4的均值统计Excel报告
final_mean_dataframe = process_data_for_table5_5(gdb_path, out_table_mean, sample_table_name)
write_to_excel_table5_5(final_mean_dataframe, output_excel_path.replace(".xlsx", "_mean.xlsx"))
# adjusted_dataframe.to_csv(output_excel_path.replace(".xlsx", "_adjusted.csv"), index=False)
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
import gc
gc.collect()
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# main()

View File

@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
import sys
import arcpy
from pathlib import Path
sys.path.append(str(Path(__file__).parent))
from tools.config.arcgis_field_cal_code import codeblock_dltb_ejdl, codeblock_dltb_yjdl
def export_to_points(ph_points, dltb_features, trlx_features, xzq_features, assign_raster, workspace):
# --- 1. 设置工作空间和变量 ---
# 请根据您的实际情况修改以下路径
arcpy.env.workspace = workspace
arcpy.env.overwriteOutput = True
# 输入的要素类
input_features = ph_points # 历史样点PH数据
join_features_list = [trlx_features,xzq_features,dltb_features] # 连接图层 (规划分区)
# 输出的要素类
final_output_fc = "历史样点PH信息_Table"
# --- 3. 主处理逻辑 ---
try:
print("开始处理赋值样点PH信息...")
target_features = f"in_memory/temp_sample_raster"
# 将栅格数据提取至历史PH样点
arcpy.sa.ExtractValuesToPoints(
in_point_features=input_features,
in_raster=assign_raster,
out_point_features=target_features,
interpolate_values="NONE",
add_attributes="VALUE_ONLY"
)
print("开始计算地类一二级类别...")
# 计算地类图斑一级、二级类别
try:
arcpy.management.CalculateField(dltb_features, "EJDL", "calculate_ejdl(!DLBM!,!DLMC!)", "PYTHON3", codeblock_dltb_ejdl)
arcpy.management.CalculateField(dltb_features, "YJDL", "calculate_yjdl(!DLBM!)", "PYTHON3", codeblock_dltb_yjdl)
arcpy.management.CalculateField(dltb_features, "YJDLBM", "!DLBM![:2]", "PYTHON3")
raster_path = Path(assign_raster)
# if "二普" in raster_path.stem or "测土" in raster_path.stem:
arcpy.management.CalculateField(target_features, "dPH", "!RASTERVALU!-!PH!", "PYTHON3", field_type="DOUBLE")
# else:
# arcpy.management.CalculateField(target_features, "dPH", "!PH!-!RASTERVALU!", "PYTHON3", field_type="DOUBLE")
except Exception as e:
print(e)
# --- 2. 定义要保留的字段 ---
# 这是一个非常清晰的配置方式:指定每个图层要保留的字段列表
fields_to_keep = {
target_features: ["PH", "RASTERVALU", "dPH"],
trlx_features: ["YL", "TS"],
xzq_features: ["XZQMC"],
dltb_features: ["YJDL", "EJDL"]
}
print("开始配置字段映射...")
# 初始化当前的目标图层,最开始是原始的目标图层
current_target = target_features
# 存储所有中间生成的临时文件,以便最后清理
temp_outputs = []
temp_outputs.append(target_features)
# 获取目标图层的所有字段,以便在后续迭代中保留
retained_fields = fields_to_keep.get(target_features, [])
# 迭代处理每一个连接图层
for i, join_features in enumerate(join_features_list):
print(f"\n--- 开始处理第 {i+1} 个连接图层: {join_features} ---")
# 检查连接图层是否存在
if not arcpy.Exists(join_features):
print(f"警告: 连接图层 '{join_features}' 不存在,将跳过此连接。")
continue
# --- 配置 FieldMappings ---
field_mappings = arcpy.FieldMappings()
# a. 保留已经存在于 current_target 中的字段
# 这些字段是在之前的迭代中保留下来的
for field_name in retained_fields:
try:
field_map = arcpy.FieldMap()
field_map.addInputField(current_target, field_name)
field_mappings.addFieldMap(field_map)
except Exception:
# 如果字段在之前的某个步骤中未能成功添加,这里会捕获异常
print(f"注意: 在图层 '{current_target}' 中未找到字段 '{field_name}',可能在之前的步骤中已被跳过。")
# b. 从当前的 join_features 中添加新字段
fields_from_current_join = fields_to_keep.get(join_features, [])
for field_name in fields_from_current_join:
try:
field_map = arcpy.FieldMap()
field_map.addInputField(join_features, field_name)
field_map.mergeRule = "First" # 对所有连接字段使用 "First" 规则
field_mappings.addFieldMap(field_map)
except Exception as e:
print(f"警告: 添加字段 '{field_name}' (来自 '{join_features}') 时出错,将跳过。错误信息: {e}")
# 如果本次迭代没有有效的字段映射,则跳过
if field_mappings.fieldCount == 0:
print(f"警告: 对于连接图层 '{join_features}' 没有有效的字段可以添加,跳过此连接。")
continue
# 定义本次连接的临时输出名
# 使用 in_memory 工作空间可以提高性能
temp_output = f"in_memory/temp_join_{i}"
temp_outputs.append(temp_output)
print(f"执行空间连接: '{current_target}' + '{join_features}' -> '{temp_output}'")
# 执行空间连接
arcpy.analysis.SpatialJoin(
target_features=current_target,
join_features=join_features,
out_feature_class=temp_output,
join_operation="JOIN_ONE_TO_ONE",
join_type="KEEP_ALL",
field_mapping=field_mappings,
match_option="INTERSECT"
)
# 更新 current_target 为本次操作的输出,以便下一次迭代使用
current_target = temp_output
# 更新已保留字段列表,为下一次迭代做准备
retained_fields.extend(fields_from_current_join)
print(f"连接成功。目前已保留的字段: {retained_fields}")
# --- 4. 保存最终结果并清理 ---
# 将最后一个临时输出复制或重命名为最终结果
if arcpy.Exists(current_target):
print(f"\n所有连接完成。将最终结果 '{current_target}' 保存为 '{final_output_fc}'...")
# arcpy.management.CopyFeatures(current_target, final_output_fc)
arcpy.conversion.ExportTable(current_target, final_output_fc)
print("最终结果已保存。")
# 验证输出字段
output_fields = [f.name for f in arcpy.ListFields(final_output_fc)]
print(f"最终输出的字段为: {output_fields}")
else:
print("警告: 没有任何连接操作成功执行,未生成最终输出。")
except arcpy.ExecuteError:
print("\n--- ArcPy 执行错误 ---")
print(arcpy.GetMessages(2))
except Exception as e:
print(f"\n--- 发生未预料的错误 ---")
print(e)
finally:
# 清理所有中间生成的临时文件
print("\n开始清理临时文件...")
for temp_file in temp_outputs:
if arcpy.Exists(temp_file):
arcpy.management.Delete(temp_file)
print(f"已删除临时文件: {temp_file}")
print("清理完成。")

View File

@@ -0,0 +1,641 @@
# -*- coding: utf-8 -*-
import os
import arcpy
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
from tools.config.arcgis_field_cal_code import codeblock_cal_shfj
from tools.core.utils.excel_utils import ExcelStyleUtils
yjdl_order = ["耕地", "园地", "林地", "草地", "其他"]
ejdl_order = ["水田", "旱地", "水浇地", "果园", "茶园", "橡胶园", "其他园地"]
# --- 2. 辅助函数 ---
# 等级计算
def get_acidification_degree(delta_ph):
"""根据ΔpH值判断酸化程度"""
if pd.isna(delta_ph) or delta_ph == 0:
return "-"
# 请根据您的实际分级标准调整这里的阈值
if delta_ph > 1.0:
return "重度酸化"
elif 0.5 < delta_ph <= 1.0:
return "中度酸化"
elif 0.3 < delta_ph <= 0.5:
return "轻度酸化"
elif 0.1 < delta_ph <= 0.3:
return "弱酸化"
else: # dPH < -0.3
return "其他"
# --- 3. 数据处理与分析 均值---
def process_data_for_table5_7(gdb_path, mean_table_name, sample_table_name):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("开始处理数据...")
def clean_df(df, columns):
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# --- a. 处理样点数据,计算“样点均值” ---
print("--> 步骤1: 计算样点均值...")
sample_table_path = os.path.join(gdb_path, sample_table_name)
sample_fields = ['XZQMC','YJDL','EJDL', 'dPH']
df_samples = pd.DataFrame(arcpy.da.TableToNumPyArray(sample_table_path, sample_fields, 'dPH>0.3', skip_nulls=False))
df_samples = clean_df(df_samples, ['XZQMC','YJDL', 'EJDL'])
# 按 YJDL, EJDL 分组,计算 dPH 的均值
df_sample_means = df_samples.groupby(['XZQMC'])['dPH'].mean().reset_index()
df_sample_means.rename(columns={'dPH': '样点均值'}, inplace=True)
print("样点均值计算完成。")
# --- b. 处理制图数据,获取“制图均值”和“制图样点数” ---
print("--> 步骤2: 获取制图均值和样点数...")
mean_table_path = os.path.join(gdb_path, mean_table_name)
mean_fields = ['XZQMC', 'MEAN', 'COUNT']
df_map_data = pd.DataFrame(arcpy.da.TableToNumPyArray(mean_table_path, mean_fields, skip_nulls=False))
df_map_data = clean_df(df_map_data, ['XZQMC'])
df_map_data.rename(columns={'MEAN': '制图均值', 'COUNT': '制图样点数'}, inplace=True)
print("制图数据获取完成。")
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['XZQMC']],
df_map_data[['XZQMC']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['XZQMC'], how='left')
# **【核心修改】: 合并整个 df_map_data而不仅仅是均值列**
df_final = pd.merge(df_final, df_map_data, on=['XZQMC'], how='left')
# --- d. 计算酸化程度 ---
print("--> 步骤4: 计算酸化程度...")
# **【核心修改】: 在计算酸化程度之前,先过滤掉不展示的行**
# 我们只对 dPH 在酸化范围内 ( > 0.3) 的数据感兴趣
# 但为了计算合计,我们需要保留所有数据,所以这一步只计算,不删除
df_final['酸化程度_样本'] = df_final['样点均值'].apply(get_acidification_degree)
df_final['酸化程度_制图'] = df_final['制图均值'].apply(get_acidification_degree)
df_final.sort_values(['XZQMC'], inplace=True)
print("数据处理流程完成!")
return df_final
# --- 4. Excel 制表 均值---
def write_to_excel_table5_7(df, output_path):
"""
将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同土地利用类型pH变化统计"
# --- b. 绘制表头 ---
ws.merge_cells('A1:A2'); ws['A1'] = '乡镇/街道'
ws.merge_cells('B1:E1'); ws['B1'] = 'ΔpH'
ws['B2'] = '样点均值'
ws['C2'] = '酸化程度'
ws['D2'] = '制图均值'
ws['E2'] = '酸化程度'
# --- c. 填充数据 ---
current_row = 3
# **【核心修改】: 先对整个DataFrame进行过滤只保留需要展示的行**
acid_levels_to_show = ["弱酸化", "轻度酸化", "中度酸化", "重度酸化", "其他"]
df_to_write = df[
df['酸化程度_样本'].isin(acid_levels_to_show) | df['酸化程度_制图'].isin(acid_levels_to_show)
].copy() # 使用 .copy() 避免 SettingWithCopyWarning
for _, row_data in df_to_write.iterrows():
print(f"正在写入一级地类...")
# 写入数据”
ws.cell(row=current_row, column=1).value = row_data['XZQMC']
# 填充样点数据
sample_mean = row_data.get('样点均值')
if pd.notna(sample_mean):
ws.cell(row=current_row, column=2).value = f"{sample_mean:.2f}" if sample_mean > 0.3 else "-"
ws.cell(row=current_row, column=3).value = row_data.get('酸化程度_样本', '-') if sample_mean > 0.3 else "-"
else:
ws.cell(row=current_row, column=2).value = "-"
ws.cell(row=current_row, column=3).value = "-"
# 填充制图数据
map_mean = row_data.get('制图均值')
if pd.notna(map_mean):
ws.cell(row=current_row, column=4).value = f"{map_mean:.2f}" if map_mean > 0.3 else "-"
ws.cell(row=current_row, column=5).value = row_data.get('酸化程度_制图', '-') if map_mean > 0.3 else "-"
else:
ws.cell(row=current_row, column=4).value = "-"
ws.cell(row=current_row, column=5).value = "-"
current_row += 1
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
# --- d. 应用样式和调整列宽 ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}{current_row-1}')
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}2', header_font)
print("正在自动调整列宽...")
# 设置列宽
ExcelStyleUtils.auto_adjust_column_width(ws)
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
# --- 2. 数据处理与分析 面积 各乡镇---
def process_data_for_table5_4(gdb_path, area_table_name, target_area_dict):
"""
【最终修正版 v2】: 先建立统一的层级结构,再分别合并统计结果。
"""
print("【最终修正版 v2】开始处理数据...")
def clean_df(df, columns):
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# --- a. 独立统计面积数据 ---
print("--> 步骤1: 独立统计面积数据...")
area_table_path = os.path.join(gdb_path, area_table_name)
df_area = pd.DataFrame(arcpy.da.TableToNumPyArray(area_table_path, ['XZQMC', 'SHFJ', 'AREA'], skip_nulls=False))
df_area = clean_df(df_area, ['XZQMC'])
df_final = pd.DataFrame()
if not df_area.empty:
# 计算平差系数
target_shfj_areas = target_area_dict.groupby(['SHFJ'])['AREA_MU'].sum().reset_index()
original_shfj_areas = df_area.groupby(['SHFJ'])['AREA'].sum().reset_index()
original_shfj_areas['AREA_MU'] = original_shfj_areas['AREA'] * 0.0015
adjustment_factors = []
for index, row in original_shfj_areas.iterrows():
shfj = row['SHFJ']
area_mu = row['AREA_MU']
adjustment_factor = target_shfj_areas[target_shfj_areas['SHFJ'] == shfj]['AREA_MU'].values[0] / area_mu
adjustment_factors.append({
'SHFJ': shfj,
'平差系数':adjustment_factor
})
factor_df = pd.DataFrame(adjustment_factors)
df_sh_area = df_area.merge(factor_df[['SHFJ', '平差系数']], on='SHFJ')
df_sh_area['制图面积_亩'] = df_sh_area['AREA'] * 0.0015 * df_sh_area['平差系数']
ts_total_area = df_sh_area.groupby(['XZQMC'])['制图面积_亩'].transform('sum')
df_sh_area['面积占比'] = (df_sh_area['制图面积_亩'] / ts_total_area) * 100
df_area_stats = df_sh_area.pivot_table(
index=['XZQMC'], columns='SHFJ', values=['制图面积_亩', '面积占比'], fill_value=0
).reset_index()
df_area_stats.columns = [f'{col[0]}_{col[1]}'.strip('_') if col[1] else col[0] for col in df_area_stats.columns]
df_final = df_area_stats
print("--> 步骤2: 计算酸化面积合计...")
# 定义属于酸化类别的面积列
acidic_area_cols = [
'制图面积_亩_轻度酸化',
'制图面积_亩_中度酸化',
'制图面积_亩_重度酸化'
]
# 确保这些列存在于DataFrame中不存在的列用0代替
for col in acidic_area_cols:
if col not in df_final.columns:
df_final[col] = 0
# 将这三列相加,得到合计值
df_final['酸化面积合计_亩'] = df_final[acidic_area_cols].sum(axis=1)
# --- d. 最后清理和构建映射 ---
df_final.fillna(0, inplace=True)
print("数据处理流程完成!")
return df_final
# --- 3. Excel 制表 面积---
def write_to_excel_table5_4(df, output_path):
"""
【最终修正版】: 将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel将创建一个空的报告。")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同乡镇酸化面积统计"
ws['A1'] = "没有有效的统计数据。"
wb.save(output_path)
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同乡镇酸化面积统计"
# --- b. 绘制表头 (不变) ---
ws.merge_cells('A1:A2'); ws['A1'] = '乡镇/街道'
acid_levels = ['弱酸化', '轻度酸化', '中度酸化', '重度酸化', '其他']
# acid_level_headers = ['0.1<ΔpH≤0.3', '0.3<ΔpH≤0.5', '0.5<ΔpH≤1.0', 'ΔpH>1.0', '其他']
# all_possible_levels = ['碱化', '未酸化', '轻度酸化', '中度酸化', '重度酸化']
acid_level_headers = ['弱酸化(0.1<ΔpH≤0.3)','轻度酸化(0.3<ΔpH≤0.5)', '中度酸化(0.5<ΔpH≤1.0)', '重度酸化(ΔpH>1.0)', '其他(未酸化)']
col_start = 2
for header in acid_level_headers:
ws.merge_cells(start_row=1, start_column=col_start, end_row=1, end_column=col_start + 1)
ws.cell(row=1, column=col_start).value = header
ws.cell(row=2, column=col_start).value = '面积/亩'
ws.cell(row=2, column=col_start + 1).value = '占比/%'
col_start += 2
# 增加合计列的表头**
total_col = col_start # 记录合计列的列号
ws.merge_cells(start_row=1, start_column=total_col, end_row=2, end_column=total_col)
ws.cell(row=1, column=total_col).value = '酸化面积合计'
# --- c. 填充数据 (完全重构的逻辑) ---
current_row = 3
# **【核心修改】: 不再需要 group_yl_df直接遍历整个 df**
# 假设 df 已经按 XZQMC 排序(如果需要的话)
df_sorted = df.sort_values('XZQMC').reset_index(drop=True)
for index, row_data in df_sorted.iterrows():
ws.cell(row=current_row, column=1).value = row_data['XZQMC']
col_start = 2
for level in acid_levels:
area_col = f'制图面积_亩_{level}'
area_pct_col = f'面积占比_{level}'
area_val = row_data.get(area_col, 0)
area_pct_val = row_data.get(area_pct_col, 0)
ws.cell(row=current_row, column=col_start).value = f"{area_val:.0f}" if area_val > 0 else "-"
ws.cell(row=current_row, column=col_start + 1).value = f"{area_pct_val:.2f}%" if area_val > 0 else "-"
col_start += 2
# **【核心修改】: 填充酸化面积合计列的值**
total_area_val = row_data.get('酸化面积合计_亩', 0)
ws.cell(row=current_row, column=total_col).value = f"{total_area_val:.0f}" if total_area_val > 0 else "-"
current_row += 1
# **(可选) 增加一个所有乡镇的“总合计”行**
# print("--> 计算并写入总合计行...")
# ws.cell(row=current_row, column=1).value = '总合计'
# col_start = 2
# for level in acid_levels:
# area_col = f'制图面积_亩_{level}'
# area_sum = df_sorted.get(area_col, 0).sum()
# # 总合计行的占比是相对于所有乡镇的总面积
# grand_total_area = df_sorted[[f'制图面积_亩_{lvl}' for lvl in all_possible_levels if f'制图面积_亩_{lvl}' in df_sorted]].sum().sum()
# area_perc = (area_sum / grand_total_area * 100) if grand_total_area > 0 else 0
# ws.cell(row=current_row, column=col_start).value = f"{area_sum:.2f}" if area_sum > 0 else "-"
# ws.cell(row=current_row, column=col_start + 1).value = f"{area_perc:.2f}" if area_sum > 0 else "-"
# col_start += 2
# grand_total_acidic_area = df_sorted['酸化面积合计_亩'].sum()
# ws.cell(row=current_row, column=total_col).value = f"{grand_total_acidic_area:.2f}" if grand_total_acidic_area > 0 else "-"
# current_row += 1
# --- a. 定义样式 (不变) ---
header_font = Font(name='等线', size=11, bold=True)
# --- d. 应用样式和调整列宽 (最终健壮版) ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}{current_row-1}')
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}2', header_font)
print("正在自动调整列宽...")
# 设置列宽
ExcelStyleUtils.auto_adjust_column_width(ws)
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
# 步骤5.3: 生成表5.3 - 总表数据处理
def process_data_for_table5_2(gdb_path, area_table_name, sample_table_name, target_area_dict:pd.DataFrame):
def clean_df(df, columns):
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# --- a. 从两个表中提取并建立唯一的 (YJDL, EJDL) 层级结构 "骨架" ---
print("--> 步骤1: 建立统一的层级结构...")
sample_table_path = os.path.join(gdb_path, sample_table_name)
area_table_path = os.path.join(gdb_path, area_table_name)
# --- b. 独立统计样点数据 ---
print("--> 步骤2: 独立统计样点数据...")
df_samples = pd.DataFrame(arcpy.da.TableToNumPyArray(sample_table_path, ['XZQMC', 'dPH'], skip_nulls=False))
df_samples = clean_df(df_samples, ['XZQMC'])
if not df_samples.empty:
bins = [-np.inf, 0.1, 0.3, 0.5, 1.0, np.inf]
labels = ["其他", "弱酸化", "轻度酸化", "中度酸化", "重度酸化"]
df_samples['SHFJ'] = pd.cut(df_samples['dPH'], bins=bins, labels=labels, right=True)
sample_counts = df_samples.groupby(['SHFJ'], observed=False).size().reset_index(name='样点数')
sample_counts = sample_counts.merge(df_samples.groupby(['SHFJ'], observed=False)['dPH'].mean(), on='SHFJ')
ts_total_samples = sample_counts['样点数'].sum()
sample_counts['样点占比'] = (sample_counts['样点数'] / ts_total_samples) * 100
# print(sample_counts)
# --- c. 独立统计面积数据 ---
print("--> 步骤3: 独立统计面积数据...")
df_area = pd.DataFrame(arcpy.da.TableToNumPyArray(area_table_path, ['XZQMC', 'SHFJ', 'AREA'], skip_nulls=False))
df_area = clean_df(df_area, ['XZQMC'])
if not df_area.empty:
# 计算平差系数
target_shfj_areas = target_area_dict.groupby(['SHFJ'])['AREA_MU'].sum().reset_index()
original_shfj_areas = df_area.groupby(['SHFJ'])['AREA'].sum().reset_index()
original_shfj_areas['AREA_MU'] = original_shfj_areas['AREA'] * 0.0015
adjustment_factors = []
for index, row in original_shfj_areas.iterrows():
shfj = row['SHFJ']
area_mu = row['AREA_MU']
adjustment_factor = target_shfj_areas[target_shfj_areas['SHFJ'] == shfj]['AREA_MU'].values[0] / area_mu
adjustment_factors.append({
'SHFJ': shfj,
'平差系数':adjustment_factor
})
factor_df = pd.DataFrame(adjustment_factors)
df_sh_area = df_area.merge(factor_df[['SHFJ', '平差系数']], on='SHFJ')
df_sh_area['制图面积_亩'] = df_sh_area['AREA'] * 0.0015 * df_sh_area['平差系数']
df_area_counts = df_sh_area.groupby(['SHFJ'], observed=False)[['制图面积_亩']].sum()
ts_total_area = df_area_counts['制图面积_亩'].sum()
df_area_counts['面积占比'] = (df_area_counts['制图面积_亩'] / ts_total_area) * 100
df_final = pd.merge(sample_counts, df_area_counts, on=['SHFJ'], how='left')
# # --- d. 最后清理和构建映射 ---
df_final.fillna(0, inplace=True)
return df_final
# --- 3. Excel 制表 总表---
def write_to_excel_table5_2(df, df_mean, output_path):
"""
【最终修正版】: 将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel将创建一个空的报告。")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws['A1'] = "没有有效的统计数据。"
wb.save(output_path)
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "行政区酸化程度等级分布及占比"
# --- b. 绘制表头 (不变) ---
ws.merge_cells('A1:B1'); ws['A1'] = '酸化程度'
ws.merge_cells('C1:D1'); ws['C1'] = '样点统计'
ws.merge_cells('E1:F1'); ws['E1'] = '制图统计'
ws.merge_cells('A8:B8'); ws['A8'] = '总计'
ws.merge_cells('A9:B9'); ws['A9'] = '全县酸化样点ΔpH 均值'
ws.merge_cells('A10:B10'); ws['A10'] = '全县酸化制图ΔpH 均值'
ws['A2'] = '分级'; ws['B2'] = '值域'
ws['C2'] = '数量/个'; ws['D2'] = '占比'
ws['E2'] = '面积/亩'; ws['F2'] = '占比'
acid_levels = ['弱酸化', '轻度酸化', '中度酸化', '重度酸化', '其他']
acid_level_headers = ['0.1<ΔpH≤0.3', '0.3<ΔpH≤0.5', '0.5<ΔpH≤1.0', 'ΔpH>1.0', '未酸化']
# --- c. 填充数据 ---
current_row = 3
# 1. 遍历该一级地类下的所有“二级地类”并写入数据
for index,level in enumerate(acid_levels):
ws.cell(row=current_row, column=1).value = level
ws.cell(row=current_row, column=2).value = acid_level_headers[index]
# 在子集中查找当前二级地类的数据行
row_data = df[df['SHFJ'] == level]
# --- 填充单元格的逻辑开始 ---
col_start = 3 # 从第 C 列开始填充
# 检查是否找到了该土属的数据
if not row_data.empty:
data_series = row_data.iloc[0]
# 1. 构建要从 data_series 中查找的列名
sample_col = f'样点数'
sample_pct_col = f'样点占比'
area_col = f'制图面积_亩'
area_pct_col = f'面积占比'
# 2. 从 data_series 中安全地获取值
sample_val = data_series.get(sample_col, 0)
sample_pct_val = data_series.get(sample_pct_col, 0)
area_val = data_series.get(area_col, 0)
area_pct_val = data_series.get(area_pct_col, 0)
# 3. 将获取到的值填入单元格
ws.cell(row=current_row, column=col_start).value = f"{sample_val:.0f}" if sample_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=col_start + 1).value = f"{sample_pct_val:.2f}%" if sample_val > 0 else "-"
# 制图面积/亩
ws.cell(row=current_row, column=col_start + 2).value = f"{area_val:.0f}" if area_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=col_start + 3).value = f"{area_pct_val:.2f}%" if area_val > 0 else "-"
# 移动到下一个酸化等级的起始列
col_start += 2
else:
for _ in range(4):
ws.cell(row=current_row, column=col_start).value = "-"
col_start += 1
current_row += 1
# 合计单元格填充
mask = df["SHFJ"].isin(acid_levels)
df_acid = df[mask]
weighted_avg = (df_acid["dPH"] * df_acid["样点数"]).sum() / df_acid["样点数"].sum()
mean_msk = df_mean["酸化程度_制图"].isin(acid_levels)
df_mean_acid = df_mean[mean_msk]
weighted_mean = (df_mean_acid["制图均值"] * df_mean_acid["制图样点数"]).sum() / df_mean_acid["制图样点数"].sum()
ws.merge_cells('C9:F9')
ws.merge_cells('C10:F10')
ws['C8'] = df[df['SHFJ'].isin(acid_levels)]['样点数'].sum()
ws['D8'] = f"{df[df['SHFJ'].isin(acid_levels)]['样点占比'].sum():.2f}%"
ws['E8'] = f"{df[df['SHFJ'].isin(acid_levels)]['制图面积_亩'].sum():.0f}"
ws['F8'] = f"{df[df['SHFJ'].isin(acid_levels)]['面积占比'].sum():.2f}%"
ws['C9'] = f"{weighted_avg:.2f}" # type: ignore
ws['C10'] = f"{weighted_mean:.2f}"
# --- a. 定义样式 (不变) ---
header_font = Font(name='宋体', size=11)
# --- d. 应用样式和调整列宽 (最终健壮版) ---
if current_row > 1: # 确保有数据才应用样式
ExcelStyleUtils.set_style(ws, f'A1:F10')
ExcelStyleUtils.set_style(ws, f'A1:F2', header_font)
print("正在自动调整列宽...")
# 设置列宽
ExcelStyleUtils.auto_adjust_column_width(ws)
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
def main(gdb_path, xzq_features, ph_features, dltb_features, sh_ph_tif, output_path,target_areas_dict:dict):
try:
# --- 1. 用户配置 ---
# 输出配置
output_excel_path = os.path.join(output_path,"乡镇街道酸化统计表.xlsx") # 生成的Excel报告文件路径
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
sample_table_name = "历史样点PH信息_Table" # 图2: 样点信息表名
in_zone_feature = xzq_features # 规划分区图层
in_class_feature = ph_features # 已重分类好的酸化PH图层
dltb_class_feature = dltb_features
in_value_raster = sh_ph_tif # 赋值栅格
out_feature_class = "最小面积统计单元"
out_table_area = r"行政区划_酸化面积表" # 输出的交集表名
out_table_mean = r"行政区划_酸化均值表" # 输出的均值表名
print("开始处理数据...")
if not arcpy.Exists(out_feature_class):
# 判断输入表是否存在SHFJ字段
try:
arcpy.management.CalculateField(in_class_feature, "SHFJ", "calculate_shfj(!gridcode!)", "PYTHON3", codeblock_cal_shfj)
except Exception as e:
print(f"计算SHFJ字段时发生错误: {e}")
arcpy.analysis.Intersect(
in_features=[dltb_class_feature, in_class_feature],
out_feature_class=out_feature_class,
join_attributes="ALL",
output_type="INPUT"
)
if not arcpy.Exists(out_table_area):
# 1.用arcpy.analysis.TabulateIntersection进行交集制表
arcpy.analysis.TabulateIntersection(
in_zone_feature,
["XZQMC"],
out_feature_class,
out_table_area,
"SHFJ",
out_units="SQUARE_METERS",
)
if not arcpy.Exists(out_table_mean):
# 2.用arcpy.sa.ZonalStatisticsAsTable进行区域统计
arcpy.sa.ZonalStatisticsAsTable(
in_zone_feature, "XZQMC", in_value_raster, out_table_mean, "DATA", "MEAN"
)
# 计算按地类平差后的各酸化等级面积
if arcpy.Exists(out_feature_class):
df = pd.DataFrame(arcpy.da.TableToNumPyArray(out_feature_class, ["YJDL", "SHFJ", "Shape_Area"]))
df_area = df.groupby(["YJDL", "SHFJ"]).agg({"Shape_Area": "sum"}).reset_index()
yjdl_area = df_area.groupby(['YJDL'])['Shape_Area'].sum().reset_index()
landuse_types = {'耕地':'01', '园地':'02', '林地':'03', '草地':'04', '其他':'12'}
adjustment_factors = []
for _, row in yjdl_area.iterrows():
yjdl = row['YJDL']
original_total = row['Shape_Area'] * 0.0015
target_total = target_areas_dict.get(landuse_types[yjdl], original_total)
adjustment_factor = target_total / original_total
adjustment_factors.append({
'YJDL': yjdl,
'平差系数': adjustment_factor
})
factor_df = pd.DataFrame(adjustment_factors)
df_with_factors = df_area.merge(factor_df[['YJDL', '平差系数']], on='YJDL')
df_with_factors['AREA_MU'] = df_with_factors['Shape_Area'] * df_with_factors['平差系数'] * 0.0015
# print(df_with_factors)
# 生成表5.4的面积统计Excel报告
final_area_dataframe = process_data_for_table5_4(gdb_path, out_table_area, df_with_factors)
write_to_excel_table5_4(final_area_dataframe, output_excel_path)
# 生成表5.3的均值统计Excel报告
final_mean_dataframe = process_data_for_table5_7(gdb_path, out_table_mean, sample_table_name)
write_to_excel_table5_7(final_mean_dataframe, output_excel_path.replace(".xlsx", "_mean.xlsx"))
# 生成总表5.2的统计Excel报告
final_dataframe = process_data_for_table5_2(gdb_path, out_table_area, sample_table_name, df_with_factors)
write_to_excel_table5_2(final_dataframe, final_mean_dataframe, output_excel_path.replace(".xlsx", "_total.xlsx"))
return df_with_factors
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
import gc
gc.collect()
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# main()

View File

@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
"""
输入重分类后栅格转面要素类、乡镇边界面要素类、地类图斑要素类;
按一级地类统计土壤属性面积 和 按乡镇统计土壤属性面积;
将统计结果导出为Excel表格
将Excel表格转换为jpg图片
"""
import json
import os
from pathlib import Path
import sys
import traceback
import argparse
import win32com.client as win32
import pythoncom
import time
sys.path.append(str(Path(__file__).parent))
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="将Excel表格转换为jpg图片")
parser.add_argument("--settings_path", required=True, help="配置文件路径")
args = parser.parse_args()
if args.settings_path:
with open(args.settings_path, 'r', encoding="utf-8") as settings_file:
settings = json.load(settings_file)
area_stat_settings = settings.get("area_stat_settings", {})
else:
print_status("错误: 未找到有效配置文件")
sys.exit(1)
return area_stat_settings
def print_status(message):
"""
输出状态信息到标准输出,用于 GUI 实时显示
格式: STATUS: <message>
"""
print(f"STATUS:{message}")
sys.stdout.flush() # 确保立即输出
def print_result(success, output_path="", error_message=""):
"""
输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果
格式: RESULT:True|<output_path>|
格式: RESULT:False||<error_message>
"""
if success:
print(f"RESULT:True|{output_path}|")
else:
# 在错误信息中替换换行符,避免干扰解析
cleaned_error_message = error_message.replace('\n', ' ').replace('\r', '')
print(f"RESULT:False||{cleaned_error_message}")
sys.stdout.flush() # 确保立即输出
def export_excel_to_image(excel_path, sheet_name, output_path, range_address=None):
# 检查 Excel 文件是否存在
if not os.path.exists(excel_path):
print(f"错误: Excel 文件 '{excel_path}' 不存在。请检查路径。")
return
# 确保输出目录存在
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# 初始化 COM 库
pythoncom.CoInitialize()
try:
# 1. 获取 Excel 应用程序对象
excel = win32.Dispatch("Excel.Application")
excel.Visible = False # 不显示 Excel 窗口
excel.DisplayAlerts = False # 不显示任何警告或提示框
# 2. 打开工作簿
workbook_obj = excel.Workbooks.Open(excel_path)
# 3. 选择工作表
try:
sheet = workbook_obj.Sheets(sheet_name)
except Exception:
print(f"错误: 工作簿 '{os.path.basename(excel_path)}' 中找不到工作表 '{sheet_name}'")
# 尝试选择第一个工作表作为备用
if workbook_obj.Sheets.Count > 0:
sheet = workbook_obj.Sheets(1)
print(f"改为导出第一个工作表 '{sheet.Name}'")
else:
raise ValueError("工作簿中没有可用的工作表。")
# 4. 选择要复制的区域
if range_address:
try:
range_obj = sheet.Range(range_address)
except Exception:
print(f"警告: 指定的导出范围 '{range_address}' 无效或不存在,将导出 UsedRange。")
range_obj = sheet.UsedRange
else:
range_obj = sheet.UsedRange
# 选中区域(确保焦点)
range_obj.Select()
# 5. 将选定区域复制为图片
range_obj.CopyPicture(Format=1, Appearance=2) # xlBitmap = 1, xlScreen = 2
# 6. 临时创建ChartObject在当前工作表
chart_width = range_obj.Width * (300/72) # 将点转换为厘米
chart_height = range_obj.Height * (300/65) # 将点转换为厘米
temp_chart_obj = sheet.ChartObjects().Add(0, 0, chart_width, chart_height).Chart
temp_chart_obj.Paste()
# 7. 导出图表为图片文件
temp_chart_obj.Export(output_path, FilterName="JPG")
print(f"图片已成功导出到 '{output_path}'")
# 8. 删除临时图表对象
sheet.ChartObjects(sheet.ChartObjects().Count).Delete()
# 9. 关闭工作簿,不保存更改
workbook_obj.Close(False)
except Exception as e:
print(f"处理 Excel 时发生错误: {e}")
print("请确保已安装 Microsoft Excel 应用程序,并且 Excel 文件路径、工作表名称正确。")
finally:
# 确保 Excel 应用程序被关闭
if excel:
try:
excel.Quit()
except Exception as e:
print(f"关闭 Excel 应用程序时发生错误: {e}")
# 释放 COM 对象
pythoncom.CoUninitialize()
def main():
params = None
try:
# 1. 解析参数
params = parse_arguments()
output_path = params["batch_output_folder"]
for excel_file in os.listdir(output_path):
time.sleep(1.5)
if excel_file.endswith(".xlsx"):
excel_file_path = os.path.join(output_path, excel_file)
output_jpg_path = os.path.join(output_path, excel_file.replace(".xlsx", ".jpg"))
export_excel_to_image(excel_file_path, "综合统计表", output_jpg_path)
print_status(f"已处理文件: {excel_file}")
except Exception as e:
error_msg = f"主函数错误: {str(e)}\n{traceback.format_exc()}"
print_status(error_msg)
print_result(False, error_message=error_msg)
finally:
sys.exit(0)
if __name__ == '__main__':
print_status("开始执行")
main()

310
tools/core/export_layout.py Normal file
View File

@@ -0,0 +1,310 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
导出布局脚本
此脚本可以独立运行用于导出布局不依赖于PyQt6线程
"""
import os
import sys
import json
import arcpy
import argparse
def log(message):
"""日志输出函数"""
print(message)
sys.stdout.flush() # 确保立即输出
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description='导出布局')
parser.add_argument('--mode', choices=['single', 'batch'], default='single', help='导出模式')
parser.add_argument('--aprx_file_list', help='ArcGIS Pro工程文件路径')
parser.add_argument('--input_aprx_folder', help='批量模式下的工程文件夹路径')
parser.add_argument('--output_image_path', required=True, help='输出路径')
parser.add_argument('--export_format', default='PDF', help='导出格式')
parser.add_argument('--resolution', type=int, default=300, help='分辨率(DPI)')
parser.add_argument('--use_multiprocessing', action='store_true', help='是否使用多进程')
parser.add_argument('--process_count', type=int, default=2, help='进程数')
parser.add_argument('--image_force_regenerate', help='输出文件名')
args = parser.parse_args()
# 处理图层列表参数从JSON字符串转换为列表
try:
# 尝试将字符串解析为JSON
if args.aprx_file_list.startswith('[') and args.aprx_file_list.endswith(']'):
args.aprx_file_list = json.loads(args.aprx_file_list)
else:
# 如果不是JSON格式则假定是单个图层或逗号分隔的列表
if ',' in args.aprx_file_list:
cleaned = args.aprx_file_list.strip("[]")
args.aprx_file_list = [aprx_file_list.strip() for aprx_file_list in cleaned.split(',')]
else:
args.aprx_file_list = [args.aprx_file_list]
except json.JSONDecodeError:
args.aprx_file_list = [args.aprx_file_list]
return args
def get_file_extension(format_name):
"""根据格式名称获取文件扩展名"""
format_dict = {
"PDF": ".pdf",
"PNG": ".png",
"JPG": ".jpg",
"JPEG": ".jpg",
"TIFF": ".tif",
"EPS": ".eps",
"SVG": ".svg",
"AI": ".ai"
}
return format_dict.get(format_name.upper(), ".pdf")
def export_layout(params):
"""导出布局"""
aprx = None
try:
# 获取参数
log(f"开始导出布局...")
aprx_path = params['aprx_path']
output_folder = params['output_path']
export_format = params.get('export_format', 'PDF')
resolution = params.get('resolution', 300)
output_name = params.get('output_name', '')
# 确保输出文件夹存在
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# 打开地图文档
try:
log(f"打开地图文档: {aprx_path}")
aprx = arcpy.mp.ArcGISProject(aprx_path) # type: ignore
except Exception as e:
raise Exception(f"无法打开地图文档: {str(e)}")
# 获取布局
layouts = aprx.listLayouts()
if not layouts:
raise Exception("地图文档中没有布局")
# 如果未指定输出名称,则使用地图文档名称
if not output_name:
output_name = os.path.splitext(os.path.basename(aprx_path))[0]
# 获取文件扩展名
file_ext = get_file_extension(export_format)
# 导出每个布局
exported_files = []
for layout in layouts:
layout_name = layout.name
output_file = os.path.join(output_folder, f"{output_name}{file_ext}")
log(f"导出布局 {layout_name}{output_file}")
try:
if export_format.upper() == "PDF":
# 导出PDF
layout.exportToPDF(output_file, resolution=resolution)
elif export_format.upper() in ["PNG", "JPG", "JPEG", "TIFF"]:
# 导出栅格图像
layout.exportToJPEG(output_file, resolution=resolution,jpeg_quality=85) if export_format.upper() in ["JPG", "JPEG"] else None
layout.exportToPNG(output_file, resolution=resolution) if export_format.upper() == "PNG" else None
layout.exportToTIFF(output_file, resolution=resolution) if export_format.upper() == "TIFF" else None
elif export_format.upper() in ["EPS", "SVG"]:
# 导出矢量图像
layout.exportToEPS(output_file, resolution=resolution) if export_format.upper() == "EPS" else None
layout.exportToSVG(output_file, resolution=resolution) if export_format.upper() == "SVG" else None
else:
# 默认导出PDF
layout.exportToPDF(output_file, resolution=resolution)
exported_files.append(output_file)
log(f"成功导出布局 {layout_name}{output_file}")
except Exception as e:
log(f"导出布局 {layout_name} 失败: {str(e)}")
return {
'exported_files': exported_files,
'count': len(exported_files)
}
except Exception as e:
log(f"导出布局失败: {str(e)}")
raise
finally:
# 释放资源
if aprx:
del aprx
arcpy.management.ClearWorkspaceCache()
def batch_export_layout(params):
"""批量导出布局"""
try:
# 获取参数
log(f"开始批量导出布局...")
aprx_files = params['aprx_files']
output_folder = params['output_path']
export_format = params.get('export_format', 'PDF')
resolution = params.get('resolution', 300)
# 确保输出文件夹存在
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# 记录导出结果
all_exported_files = []
success_count = 0
failed_count = 0
# 逐个处理aprx文件
for aprx_path in aprx_files:
try:
file_name = os.path.splitext(os.path.basename(aprx_path))[0]
log(f"\n处理文件: {file_name}")
# 准备参数
export_params = {
'aprx_path': aprx_path,
'output_path': output_folder,
'export_format': export_format,
'resolution': resolution,
'output_name': file_name
}
# 调用导出布局函数
result = export_layout(export_params)
all_exported_files.extend(result['exported_files'])
success_count += result['count']
log(f"文件 {file_name} 处理完成")
except Exception as e:
log(f"处理文件 {os.path.basename(aprx_path)} 失败: {str(e)}")
failed_count += 1
# 返回结果
log(f"\n批量导出完成")
log(f"成功: {success_count} 个布局")
log(f"失败: {failed_count} 个文件")
return {
'exported_files': all_exported_files,
'count': success_count,
'success_count': success_count,
'failed_count': failed_count
}
except Exception as e:
log(f"批量导出布局失败: {str(e)}")
raise
def export_worker(aprx_path, output_path, export_format, resolution):
"""子进程专用工作函数(保持最小化参数)"""
try:
# 每个子进程独立初始化ArcPy环境
import arcpy
arcpy.env.overwriteOutput = True
result = export_layout({
'aprx_path': aprx_path,
'output_path': output_path,
'export_format': export_format,
'resolution': resolution
})
return (True, aprx_path, result)
except Exception as e:
return (False, aprx_path, str(e))
def main():
"""主函数"""
try:
args = parse_arguments()
if len(args.aprx_file_list) == 1:
aprx_file = args.aprx_file_list[0]
if not os.path.exists(aprx_file):
log(f"所选文件{aprx_file}不存在,请确认")
return 1
# 准备参数
params = {
'aprx_path': aprx_file,
'output_path': args.output_image_path,
'export_format': args.export_format,
'resolution': args.resolution
}
# 调用导出函数
result = export_layout(params)
log(f"导出完成,成功导出 {result['count']} 个布局")
elif len(args.aprx_file_list) >1:
if not args.input_aprx_folder:
log("批量导出模式需要指定aprx_folder参数")
return 1
# 查找所有aprx文件
aprx_files = []
valied_files = []
failed_files = []
for file in args.aprx_file_list:
if not os.path.exists(file) and file.lower().endswith('.aprx'):
failed_files.append(os.path.basename(file))
continue
aprx_files.append(file)
valied_files.append(os.path.basename(file))
if not aprx_files:
log(f"在指定文件夹中未找到aprx文件: {args.input_aprx_folder}")
return 1
log(f"找到 {len(valied_files)} 个有效aprx文件: {', '.join(valied_files)}\n")
log(f"{len(failed_files)} 个无效文件: {', '.join(failed_files)}")
if args.use_multiprocessing and args.process_count > 1 and len(aprx_files)>1:
from multiprocessing import Pool
tasks = [(aprx_file, args.output_image_path, args.export_format, args.resolution) for aprx_file in aprx_files]
with Pool(min(int(args.process_count), len(tasks))) as p:
results = p.starmap(export_worker, tasks)
for success, aprx_path, result in results:
if success:
log(f"成功导出布局 {aprx_path}{result['exported_files']}") # type: ignore
else:
log(f"导出布局 {aprx_path} 失败: {result}")
return 0
else:
# 准备参数
params = {
'aprx_files': aprx_files,
'output_path': args.output_image_path,
'export_format': args.export_format,
'resolution': args.resolution
}
# 调用批量导出函数
result = batch_export_layout(params)
log(f"批量导出完成,成功导出 {result['count']} 个布局")
return 0
else:
log("请选择要处理的aprx文件")
return 0
except Exception as e:
log(f"导出失败: {str(e)}")
return 1
if __name__ == "__main__":
sys.exit(main())

572
tools/core/export_map_v1.py Normal file
View File

@@ -0,0 +1,572 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
导出地图脚本
此脚本可以独立运行,用于导出地图,支持批量导出
"""
import os
import sys
import json
import time
import arcpy
import argparse
from pathlib import Path
from collections import defaultdict
sys.path.append(str(Path(__file__).parent))
from utils import common_utils
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="导出地图工具")
parser.add_argument("--config_file", required=True, help="配置文件路径")
parser.add_argument("--county_name", required=True, help="区县名称")
parser.add_argument("--polygon_list", required=True, help="要导出的图层列表JSON格式字符串")
parser.add_argument("--template_aprx_file", required=True, help="模板文件路径")
parser.add_argument("--output_path", required=True, help="输出路径")
parser.add_argument("--data_source_path", required=True, help="数据源路径")
parser.add_argument("--symbol_path", required=True, help="符号文件路径")
parser.add_argument("--force_regenerate", action="store_true", help="强制重新生成工程文件")
parser.add_argument("--pic_path", required=True, help="图片输入路径")
# 解析参数
args = parser.parse_args()
# 处理图层列表参数从JSON字符串转换为列表
try:
# 尝试将字符串解析为JSON
if args.polygon_list.startswith('[') and args.polygon_list.endswith(']'):
args.polygon_list = json.loads(args.polygon_list)
else:
# 如果不是JSON格式则假定是单个图层或逗号分隔的列表
if ',' in args.polygon_list:
cleaned = args.polygon_list.strip("[]")
args.polygon_list = [layer.strip() for layer in cleaned.split(',')]
else:
args.polygon_list = [args.polygon_list]
except json.JSONDecodeError:
args.polygon_list = [args.polygon_list]
return args
def print_status(message):
"""
输出状态信息到标准输出,用于 GUI 实时显示
格式: STATUS: <message>
"""
print(f"STATUS:{message}")
sys.stdout.flush() # 确保立即输出
def print_result(success, output_path="", error_message=""):
"""
输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果
格式: RESULT:True|<output_path>|
格式: RESULT:False||<error_message>
"""
if success:
print(f"RESULT:True|{output_path}|")
else:
# 在错误信息中替换换行符,避免干扰解析
cleaned_error_message = error_message.replace('\n', ' ').replace('\r', '')
print(f"RESULT:False||{cleaned_error_message}")
sys.stdout.flush() # 确保立即输出
def log_arcpy_message(message):
"""输出 ArcPy 产生的 geoprocessing 消息"""
# 可以在这里进一步处理或过滤 ArcPy 消息
if message.type == 'Message':
print_status(f"ArcPy消息: {message.message}")
elif message.type == 'Warning':
print_status(f"ArcPy警告: {message.message}")
elif message.type == 'Error':
# 对于错误,也可以记录到标准错误
print_status(f"ArcPy错误: {message.message}")
sys.stderr.write(f"ArcPyError:{message.message}\n")
sys.stderr.flush()
def area_statistics_by_field(layer, field_name="GRIDCODE", area_unit="HECTARES"):
"""计算要素的面积统计信息"""
area_stats = defaultdict(float, {1: 0.00, 2: 0.00, 3: 0.00, 4: 0.00, 5: 0.00})
try:
# 检查图层是否有效
if not layer or not layer.isFeatureLayer:
raise ValueError("输入图层无效或不是要素图层")
# 检查字段是否存在
if field_name not in [f.name for f in arcpy.ListFields(layer)]:
raise ValueError(f"字段 '{field_name}' 在图层中不存在")
# 判断坐标系类型
desc = arcpy.Describe(layer.dataSource)
is_geographic = desc.spatialReference.type == "Geographic"
if is_geographic:
print_status("图层坐标系为地理坐标系,计算面积可能不准确")
# 创建游标遍历要素
with arcpy.da.SearchCursor(layer, ["SHAPE@", field_name]) as cursor:
for row in cursor:
geometry = row[0]
value = row[1]
# 计算面积
area = geometry.getArea("GEODESIC" if is_geographic else "PLANAR", area_unit)
# 根据分类进行统计
if value in area_stats:
area_stats[value] += area
else:
area_stats[value] = area
return area_stats
except Exception as e:
raise Exception(f"计算面积统计信息失败: {str(e)}")
def update_text_elements(layout, config_data, county_name):
"""更新布局中的文本元素"""
# 更新标题和其他文本元素
for element_name, element_content in config_data.items():
if element_name == "项目名称":
new_text = element_content.replace('{区县占位符}', county_name)
text_element = layout.listElements("TEXT_ELEMENT", element_name)[0]
text_element.text = new_text
if element_name == "分析方法":
text_element = layout.listElements("TEXT_ELEMENT", element_name)[0]
text_element.text = element_content
print_status(f"文本元素更新成功")
def update_data_source(layer, layer_name, new_data_source):
"""更新图层数据源"""
try:
# 确保路径分隔符正确
new_data_source = os.path.normpath(new_data_source)
layer_path = os.path.join(new_data_source, layer_name)
# 检查数据源是否存在
if not arcpy.Exists(layer_path):
raise ValueError(f"数据源不存在: {layer_path}")
# 更新数据源
cp = layer.connectionProperties
cp["connection_info"]["database"] = new_data_source
cp["dataset"] = layer_name
cp["workspace_factory"] = "File Geodatabase"
layer.updateConnectionProperties(layer.connectionProperties, cp)
print_status(f"数据源更新成功")
except Exception as e:
print_status(f"更新数据源失败: {str(e)}")
raise
def update_symbol_system(layer, layer_name, symbol_path):
"""
更新符号系统
1.ApplySymbologyFromLayer循环更新不起作用还没找到原因
2.替代方案: lf = arcpy.mp.LayerFile(symbol_file)
layer.symbology = lf.listLayers()[0].symbology
"""
try:
# 获取符号文件
if os.path.isfile(symbol_path) and symbol_path.endswith('.lyr'):
symbol_file = symbol_path
elif os.path.isdir(symbol_path):
symbol_file = None
for file in os.listdir(symbol_path):
if (file.endswith('.lyr') or file.endswith('.lyrx')) and layer_name in file:
symbol_file = os.path.join(symbol_path, file)
break
if not symbol_file:
raise FileNotFoundError(f"符号系统中未找到匹配 {layer_name} 的.lyr文件")
else:
raise FileNotFoundError("符号系统路径必须是有效的.lyr文件或文件夹")
# 更新符号系统
lf = arcpy.mp.LayerFile(symbol_file) # type: ignore
layer.symbology = lf.listLayers()[0].symbology
# arcpy.management.ApplySymbologyFromLayer(layer, symbol_file, update_symbology="MAINTAIN")
print_status(f"符号系统更新成功")
except Exception as e:
print_status(f"更新符号系统失败: {str(e)}")
# 添加注记
def add_annotation(map_scale, map_obj, label_layer,layer_name, new_data_source):
"""
标注转注记
标注存在缓存机制和layer.name绑定需修改layer.name
"""
try:
# 检查图层是否为空
if label_layer is None:
print_status(f"图层对象为空,无法进行标注转注记")
return
# 转换为注记
anno_layer_name = f"{layer_name}_GDBAnno"
output_anno = os.path.join(new_data_source, anno_layer_name)
try:
# 1. 从地图中移除所有相关图层
for lyr in map_obj.listLayers("*GDBAnno"):
map_obj.removeLayer(lyr)
label_layer.showLabels = False
# 将新生成的注记图层添加到地图中
if arcpy.Exists(output_anno):
new_anno_layer = map_obj.addDataFromPath(output_anno)
map_obj.moveLayer(label_layer, new_anno_layer)
print_status(f" 成功添加注记图层: {new_anno_layer.name}")
else:
raise FileNotFoundError(f"注记图层不存在: {output_anno}")
except Exception as e:
print_status(f"执行标注转注记工具时出错: {str(e)}")
raise Exception(f"执行标注转注记工具时出错: {str(e)}")
except Exception as e:
print_status(f"标注转注记失败: {str(e)}")
# 打印详细错误信息
import traceback
print_status(traceback.format_exc())
def label_to_annotation(map_scale, map_obj, label_layer,layer_name, new_data_source):
"""
标注转注记
标注存在缓存机制和layer.name绑定需修改layer.name
"""
try:
# 检查图层是否为空
if label_layer is None:
print_status(f"图层对象为空,无法进行标注转注记")
return
# 尝试开启标注
if label_layer.supports("SHOWLABELS"):
try:
label_layer.showLabels = True
except Exception as e:
print_status(f"启用标注失败: {str(e)}")
# 转换为注记
anno_layer_name = f"{layer_name}_GDBAnno"
output_anno = os.path.join(new_data_source, anno_layer_name)
try:
# 1. 从地图中移除所有相关图层
for lyr in map_obj.listLayers("*GDBAnno"):
map_obj.removeLayer(lyr)
if arcpy.Exists(output_anno):
# 2. 强制释放工作空间锁
arcpy.management.ClearWorkspaceCache(new_data_source)
# 3. 带重试机制的要素类删除
max_retries = 5
for attempt in range(max_retries):
try:
arcpy.management.Delete(output_anno)
except arcpy.ExecuteError as e:
if "000464" in str(e) and attempt < max_retries - 1:
time.sleep((attempt + 1) * 3)
arcpy.management.ClearWorkspaceCache(new_data_source)
continue
raise
# 4. 最终存在性检查
if arcpy.Exists(output_anno):
raise RuntimeError("无法彻底删除旧注记要素类")
except Exception as e:
raise Exception(f"清除旧注记失败,{str(e)}")
try:
# 使用ConvertLabelsToAnnotation工具
arcpy.cartography.ConvertLabelsToAnnotation(
input_map=map_obj,
conversion_scale=map_scale,
output_geodatabase=new_data_source,
anno_suffix="_GDBAnno",
extent="DEFAULT",
output_group_layer=f"{layer_name}GDBAnno",
which_layers="SINGLE_LAYER",
single_layer=label_layer
)
print_status(f"标注转注记成功")
# 关闭原始标注显示(可选)
label_layer.showLabels = False
# 将新生成的注记图层添加到地图中
if arcpy.Exists(output_anno):
new_anno_layer = map_obj.addDataFromPath(output_anno)
map_obj.moveLayer(label_layer, new_anno_layer)
print_status(f" 成功添加注记图层: {new_anno_layer.name}")
else:
raise FileNotFoundError(f"注记图层不存在: {output_anno}")
except Exception as e:
print_status(f"执行标注转注记工具时出错: {str(e)}")
raise Exception(f"执行标注转注记工具时出错: {str(e)}")
except Exception as e:
print_status(f"标注转注记失败: {str(e)}")
# 打印详细错误信息
import traceback
print_status(traceback.format_exc())
def export_map(params):
"""导出地图"""
start_time = time.time()
aprx = None
try:
# 获取参数
export_config = params['export_config']
county_name = params['county_name']
template_aprx_file = params['template_aprx_file']
output_path = params['output_path']
data_source_path = params['data_source_path']
symbol_path = params['symbol_path']
force_regenerate = params.get('force_regenerate', False) # 是否强制重新生成工程文件
polygon_list = params['polygon_list']
pic_path = params.get('pic_path', None)
# 结果记录
success_count = 0
# 确保输出文件夹存在
if not os.path.exists(output_path):
os.makedirs(output_path)
# 创建工作空间
workspace_path = os.path.join(output_path, f"{county_name}_工作空间")
if not os.path.exists(workspace_path) or force_regenerate:
if os.path.exists(workspace_path):
print_status(f"强制重新生成,删除现有工作空间:{workspace_path}")
# 删除旧工作空间可能需要arcpy函数
os.makedirs(workspace_path, exist_ok=True)
print_status(f"创建工作空间:{workspace_path}")
# 工程模板不存在则返回
if not arcpy.Exists(template_aprx_file):
raise Exception("模板文件不存在")
# 设置工作空间
orginal_workspace = arcpy.env.workspace
arcpy.env.workspace = workspace_path
aprx = arcpy.mp.ArcGISProject(template_aprx_file) # type: ignore
# 获取指定布局
target_layout = None
layout_name = "属性图模板"
target_layout = aprx.listLayouts(layout_name)[0]
if not target_layout:
raise Exception(f"未找到布局: {layout_name}")
# 获取当前地图比例尺
map_frame = target_layout.listElements("MAPFRAME_ELEMENT", "地图框")[0]
# if isinstance(map_frame, arcpy.mp.MapFrame):
map_scale = map_frame.camera.scale
# else:
# raise Exception("地图框元素不存在")
# 获取指定地图
target_map = None
map_name = "土壤属性图层"
target_map = aprx.listMaps(map_name)[0]
if not target_map:
raise Exception(f"未找到地图: {map_name}")
# 获取指定图层
target_layer = None
target_layer_name = "属性图"
try:
target_layer = target_map.listLayers(target_layer_name)[0]
if not target_layer:
raise Exception(f"未找到图层: {target_layer_name}")
except Exception as e:
raise Exception(f"错误信息: {str(e)}")
# 循环处理每个图层
for layer_name in polygon_list:
print_status(f"===== 开始处理要素: {layer_name} =====")
config_key = common_utils.get_config_key(layer_name)
try:
# 更新图层名
target_layer.name = config_key
# 获取图层配置
single_export_config = export_config.get(config_key, {})
if not single_export_config:
print_status(f"警告: 未找到 {layer_name} 的配置信息, 将处理下一个")
continue
# 生成输出文件名和路径
temp_file_name = single_export_config['项目名称'].split('\n')[1]
file_name = temp_file_name.replace('{区县占位符}', county_name)
output_path = os.path.join(workspace_path, f"{file_name}.aprx")
# 检查工程文件是否已存在
if os.path.exists(output_path) and not force_regenerate:
print_status(f"工程文件已存在: {file_name}.aprx, 将直接使用")
success_count += 1
continue
# 检查数据源是否存在
data_layer_path = os.path.join(data_source_path, layer_name)
if not arcpy.Exists(data_layer_path):
print_status(f"警告: 数据源不存在: {layer_name}要素,跳过此图层")
continue
# 更新数据源
if data_source_path:
try:
update_data_source(target_layer, layer_name, data_source_path)
except Exception as e:
print_status(f"更新数据源时出错: {str(e)},跳过此图层")
continue
# 更新符号系统
if os.path.exists(symbol_path):
try:
update_symbol_system(target_layer, config_key, symbol_path)
except Exception as e:
print_status(f"更新符号系统时出错: {str(e)},但将继续处理")
# 更新文本元素
if target_layout:
try:
update_text_elements(target_layout, single_export_config, county_name)
except Exception as e:
print_status(f"更新文本元素时出错: {str(e)},但将继续处理")
# 替换图片
try:
for pic in os.listdir(pic_path):
if pic.endswith(".jpg") and pic.startswith(config_key):
pic_file = os.path.join(pic_path, pic)
break
pic_element = target_layout.listElements("PICTURE_ELEMENT", "统计图片")[0]
# if isinstance(pic_element, arcpy.mp.PictureElement):
pic_element.sourceImage = pic_file
print_status(f"图片替换成功,{pic_file}")
# else:
# print_status(f"未找到统计图片元素,无法替换图片")
except Exception as e:
print_status(f"替换图片时出错: {str(e)},但将继续处理")
# 如果存在注记 直接添加;如果不存在,则尝试转注记
try:
add_annotation(map_scale, target_map, target_layer, config_key, data_source_path)
except Exception as e:
print_status(f"添加注记失败: {str(e)},但将继续处理")
# 标注转注记
# try:
# label_to_annotation(map_scale, target_map, target_layer, config_key, data_source_path)
# except Exception as e:
# print_status(f"标注转注记失败: {str(e)},但将继续处理")
# 保存工程文件
aprx.saveACopy(output_path)
print_status(f"成功保存工程文件: {output_path}")
# 记录结果
success_count += 1
arcpy.management.ClearWorkspaceCache()
except Exception as e:
print_status(f"处理图层 {config_key} 时出错: {str(e)}")
continue
print_status(f"===== 导出处理完成 =====")
# 结束时间
end_time = time.time()
elapsed_time = end_time - start_time
print_status(f"共处理 {len(polygon_list)} 个图层,成功 {success_count} 个,耗时:{elapsed_time:.2f}")
return success_count
except Exception as e:
print_status(f"导出过程中出错:{str(e)}")
import traceback
print_status(traceback.format_exc())
return 0
finally:
# 确保释放资源
arcpy.management.ClearWorkspaceCache()
arcpy.env.workspace = orginal_workspace
if 'target_layer' in locals():
del target_layer
if 'target_map' in locals():
del target_map
if 'target_layout' in locals():
del target_layout
if 'aprx' in locals():
del aprx
def main():
"""主函数"""
try:
# args1 = {
# 'config_file': 'D:/arcpystudy/ArcGisPro/tools/ui/raster_test_config.json',
# 'county_name': '澜沧拉祜族自治县',
# 'polygon_list': ['AB_processed.shp'],
# 'template_aprx_file': r'D:/工作/ArcGisPro/澜沧县模板/澜沧县模板/澜沧县模板.aprx',
# 'output_path': 'D:/工作/三普成果编制/澜沧2/成果图',
# 'data_source_path': 'D:/工作/三普成果编制/澜沧2/@矢量数据',
# 'symbol_path': 'D:/工作/ArcGisPro/澜沧县模板/2.配色/华南配色',
# 'force_regenerate': True,
# 'pic_path': 'D:/工作/三普成果编制/澜沧2/@基础数据/面积统计表格'
# }
# # 将字典转为对象
# args = argparse.Namespace(**args1)
# 解析命令行参数
args = parse_arguments()
# 执行导出
with open(args.config_file,'r',encoding='utf-8') as f:
config = json.load(f)
params = {
"export_config": config["export_config"],
"county_name": args.county_name,
"polygon_list":args.polygon_list,
"template_aprx_file": args.template_aprx_file,
"output_path": args.output_path,
"data_source_path": args.data_source_path,
"symbol_path": args.symbol_path,
"force_regenerate":args.force_regenerate,
"pic_path":args.pic_path
}
success_count = export_map(params)
# 返回结果
if success_count > 0:
return 0
else:
print_status("没有导出任何图层")
return 1
except Exception as e:
print_status(f"错误:{str(e)}")
import traceback
print_status(traceback.format_exc())
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,475 @@
# -*- coding: utf-8 -*-
"""
栅格处理模块: 提供栅格重分类、栅格转矢量和小面积图斑消除功能
设计用于通过 QProcess 调用,接收命令行参数,并通过标准输出返回结果和状态。
"""
import argparse
import json
import os
import random
import sys
import time
import traceback
import arcpy
import uuid
from pathlib import Path
from tools.core.utils.os_utils import temp_files_processor
try:
from utils import common_utils
except ImportError:
print("错误: 未找到 utils 模块。请确保 utils.py 文件存在或已添加到 PYTHONPATH。")
sys.exit(1)
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description='处理栅格数据:重分类、转矢量、消除小图斑')
parser.add_argument('--input_raster', required=True, help='输入栅格文件路径')
parser.add_argument('--settings_path', required=True, help='配置文件路径')
args = parser.parse_args()
if args.settings_path:
with open(args.settings_path, 'r', encoding="utf-8") as settings_file:
settings = json.load(settings_file)
raster_settings = settings.get("raster_settings", {})
if raster_settings:
standards_dict_path = raster_settings.get("config_file_path", "")
with open(standards_dict_path, 'r', encoding="utf-8") as standards_file:
standards_dict = json.load(standards_file)
raster_name = Path(args.input_raster).stem
remap_table = common_utils.create_remap_table(standards_dict['export_config'][raster_name]["标准等级"])
raster_settings["remap_table"] = remap_table
raster_settings["input_raster"] = args.input_raster
else:
print("错误: 未找到有效配置文件")
sys.exit(1)
return raster_settings
def print_status(message):
"""
输出状态信息到标准输出,用于 GUI 实时显示
格式: STATUS: <message>
"""
print(f"STATUS:{message}")
sys.stdout.flush() # 确保立即输出
def print_result(success, output_path="", error_message=""):
"""
输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果
格式: RESULT:True|<output_path>|
格式: RESULT:False||<error_message>
"""
if success:
print(f"RESULT:True|{output_path}|")
else:
# 在错误信息中替换换行符,避免干扰解析
cleaned_error_message = error_message.replace('\n', ' ').replace('\r', '')
print(f"RESULT:False||{cleaned_error_message}")
sys.stdout.flush() # 确保立即输出
def log_arcpy_message(message):
"""输出 ArcPy 产生的 geoprocessing 消息"""
# 可以在这里进一步处理或过滤 ArcPy 消息
if message.type == 'Message':
print_status(f"ArcPy消息: {message.message}")
elif message.type == 'Warning':
print_status(f"ArcPy警告: {message.message}")
elif message.type == 'Error':
# 对于错误,也可以记录到标准错误
print_status(f"ArcPy错误: {message.message}")
sys.stderr.write(f"ArcPyError:{message.message}\n")
sys.stderr.flush()
# --- 核心处理函数 ---
def reclassify_raster(input_raster, remap_table, temp_files_to_clean):
"""
根据重分类映射表重分类栅格数据,结果存储在内存中。
参数:
input_raster (str): 输入栅格路径
remap_table (list): 重分类映射表,格式为 [[from, to, new_value], ...]
temp_files_to_clean (list): 用于收集临时文件路径的列表
返回:
str: 内存栅格路径
"""
print_status(f"开始重分类栅格: {input_raster}")
try:
# 确保remap_table中的new_value是整数并确保格式正确
corrected_remap_table = []
for item in remap_table:
try:
if len(item) == 3:
from_value, to_value, new_value = item
# 尝试转换为浮点数以处理范围值
try:
from_value = float(from_value)
except (ValueError, TypeError):
print_status(f"警告: 跳过无效的重分类项起始值: {item[0]}")
continue
if isinstance(to_value, (int, float)) and to_value == float('inf'):
to_value = 10000 # 用一个很大的数值代替
elif isinstance(to_value, (int, float)) and to_value == float('-inf'):
to_value = -10000 # 用一个很小的数值代替
else:
try:
to_value = float(to_value)
except (ValueError, TypeError):
print_status(f"警告: 跳过无效的重分类项结束值: {item[1]}")
continue
try:
new_value = int(new_value)
except (ValueError, TypeError):
print_status(f"警告: 跳过无效的重分类项新值: {item[2]}")
continue
# 验证范围的有效性
if from_value > to_value:
print_status(f"警告: 跳过无效范围: {from_value} > {to_value}")
continue
corrected_remap_table.append([from_value, to_value, new_value])
else:
print_status(f"警告: 跳过无效的重分类项格式: {item}")
except Exception as e: # 捕获更广泛的异常
print_status(f"处理重分类项 {item} 时出错: {e}")
if not corrected_remap_table:
raise ValueError("重分类映射表为空或无效,无法执行重分类。")
# 创建重分类对象
remap = arcpy.sa.RemapRange(corrected_remap_table)
# 执行重分类到内存
# 检查输入栅格是否存在
if not arcpy.Exists(input_raster):
raise FileNotFoundError(f"输入栅格不存在: {input_raster}")
# 设置Snap Raster和Mask环境如果需要的话
# arcpy.env.snapRaster = input_raster
# arcpy.env.mask = input_raster # 如果需要按栅格范围裁剪
out_raster = arcpy.sa.Reclassify(input_raster, "VALUE", remap, "DATA")
# 保存到内存工作空间
temp_reclass_raster_mem = f"in_memory/reclass_int_{uuid.uuid4().hex[:8]}"
# 在返回之前保存到内存,并添加到清理列表
out_raster.save(temp_reclass_raster_mem)
temp_files_to_clean.append(temp_reclass_raster_mem)
print_status(f"重分类已完成: {temp_reclass_raster_mem}")
return temp_reclass_raster_mem
except Exception as e:
print_status(f"重分类过程出错: {str(e)}")
# 记录ArcPy消息
for msg in arcpy.GetMessages(2).split('\n'):
if msg:
print_status(f"ArcPy重分类错误详情: {msg}")
raise # 重新抛出异常
def eliminate_small_polygons(input_polygon, output_polygon, min_area, area_unit, temp_files_to_clean, current_iter=1, max_iterations=8, start_area=1000):
"""
递归消除小于指定面积的多边形,直至没有小图斑或达到最大迭代次数。
参数:
input_polygon (str): 输入多边形要素类路径
output_polygon (str): 最终输出多边形要素类路径
min_area (float): 最小面积阈值
area_unit (str): 面积单位
max_iterations (int): 最大递归次数
current_iter (int): 当前迭代次数
temp_files_to_clean (list): 用于收集临时文件路径的列表
返回:
str: 最终输出多边形要素类路径
"""
# print_status(f"开始第 {current_iter} 次消除小图斑...")
# 检查输入多边形是否存在
if not arcpy.Exists(input_polygon):
raise FileNotFoundError(f"输入多边形不存在: {input_polygon}")
try:
# 如果是第一次迭代,检查输出文件是否已存在,并删除
if current_iter == 1 and arcpy.Exists(output_polygon):
try:
arcpy.management.Delete(output_polygon)
print_status(f"已删除现有输出文件: {output_polygon}")
except Exception as delete_err:
print_status(f"警告: 无法删除现有输出文件 {output_polygon}: {str(delete_err)}")
#==================增加逐面积消除================
# 计算当前迭代的面积阈值
current_threshold = start_area * (2 ** (current_iter - 1))
# 如果当前阈值超过最终阈值,使用最终阈值
if current_threshold > min_area:
current_threshold = min_area
#==================增加逐面积消除================
# 创建临时内存图层以便选择
temp_layer = f"input_lyr_{uuid.uuid4().hex[:8]}"
arcpy.management.MakeFeatureLayer(input_polygon, temp_layer)[0]
# print_status(f"已创建临时图层: {temp_layer_name}")
# 添加或检查面积字段
area_field_name = "TEMP_AREA"
fields = [f.name for f in arcpy.ListFields(temp_layer)]
if area_field_name not in fields:
arcpy.management.AddField(temp_layer, area_field_name, "DOUBLE")
# print_status(f"已添加临时面积字段: {area_field_name}")
# 计算面积
arcpy.management.CalculateGeometryAttributes(
temp_layer,
[[area_field_name, "AREA"]],
None,
area_unit,
None,
"SAME_AS_INPUT"
)
# print_status("面积计算完成.")
# 选择小于阈值的多边形
selection_query = f"{arcpy.AddFieldDelimiters(temp_layer, area_field_name)} < {current_threshold}"
# print_status(f"选择查询: {selection_query}")
arcpy.management.SelectLayerByAttribute(temp_layer, "NEW_SELECTION", selection_query)
# 检查选中的要素数量
count = int(arcpy.management.GetCount(temp_layer).getOutput(0))
# print_status(f"发现 {count} 个小于 {min_area} {area_unit} 的小图斑.")
# 判断是否停止迭代
if count == 0 or current_iter >= max_iterations:
# print_status(f"复制最终结果到: {output_polygon}")
arcpy.management.CopyFeatures(input_polygon, output_polygon)
# 删除临时面积字段 (可选,如果需要保持输出干净)
# if area_field_name in fields: # 仅在字段是我们添加的情况下删除
# arcpy.management.DeleteField(output_polygon, area_field_name)
return output_polygon
else:
# print_status(f"执行消除操作...")
temp_eliminate_output = f"in_memory/temp_eliminate_{uuid.uuid4().hex[:8]}"
temp_files_to_clean.append(temp_eliminate_output) # 添加到临时文件列表
# 执行消除操作
arcpy.Eliminate_management(temp_layer, temp_eliminate_output, "LENGTH")
# print_status(f"执行融合操作...")
temp_dissolve_output = f"in_memory/dissolve_polygons_{uuid.uuid4().hex[:8]}"
temp_files_to_clean.append(temp_dissolve_output)
# 添加融合字段
dissolve_fields = ["gridcode"]
arcpy.management.Dissolve(temp_eliminate_output, temp_dissolve_output, dissolve_fields, multi_part="SINGLE_PART")
# print_status(f"融合结果已保存到内存: {temp_dissolve_output}")
# 递归调用,使用融合后的结果作为下一次迭代的输入
return eliminate_small_polygons(
temp_dissolve_output, # 下一次迭代使用临时融合输出作为输入
output_polygon,
min_area,
area_unit,
temp_files_to_clean,
current_iter + 1,
max_iterations,
start_area
)
except Exception as e:
print_status(f"消除小面积多边形过程出错 (迭代 {current_iter}): {str(e)}")
# 记录ArcPy消息
for msg in arcpy.GetMessages(2).split('\n'):
if msg:
print_status(f"ArcPy消除错误详情: {msg}")
raise # 重新抛出异常
finally:
# 确保删除临时图层,即使出错
if 'temp_layer' in locals() and arcpy.Exists(temp_layer):
try:
arcpy.management.Delete(temp_layer)
# print_status(f"已删除临时图层: {temp_layer.name}")
except Exception as delete_layer_err:
print_status(f"警告: 无法删除临时图层 {temp_layer}: {str(delete_layer_err)}")
# --- 主处理逻辑 ---
def main():
"""主函数:解析参数,执行处理流程,输出结果和状态"""
params = None
temp_files_to_clean = []
original_workspace = None
try:
# 1. 解析参数
params = parse_arguments()
input_raster = params["input_raster"]
raster_name = Path(input_raster).stem
input_folder = params["input_folder"]
output_folder = params["batch_output_folder"]
clip_features = params["clip_features"]
clip_enabled = params["clip_enabled"]
remap_table = params["remap_table"]
min_area = params["min_area"]
area_unit = params["area_unit"]
simplify = params["simplify"]
# print_status(f"解析参数完成: {params}")
# 2. 设置工作空间和环境
original_workspace = arcpy.env.workspace
arcpy.env.workspace = input_folder
arcpy.env.overwriteOutput = True
print_status(f"ArcPy 工作空间设置为: {arcpy.env.workspace}")
# 3. 校验输入/输出路径
if not arcpy.Exists(input_raster):
raise FileNotFoundError(f"输入栅格文件不存在: {input_raster}")
# 创建输出文件夹
if not os.path.exists(output_folder):
os.makedirs(output_folder)
print_status(f"已创建输出文件夹: {output_folder}")
# 创建面积统计用文件夹
disk_output_path = os.path.join(output_folder, "面积统计用栅格面")
time.sleep(random.random())
if not os.path.exists(disk_output_path):
os.makedirs(disk_output_path)
print_status(f"已创建面积统计文件夹: {disk_output_path}")
if clip_enabled and not arcpy.Exists(clip_features):
raise FileNotFoundError(f"裁剪要素类不存在: {clip_features}")
# 4. 定义中间和最终输出路径
# 根据输出文件夹类型确定文件扩展名和命名方式
output_is_workspace = common_utils.get_data_type(output_folder) in ["Workspace", "FeatureDataset", "Geodatabase"]
if output_is_workspace:
# 输出到地理数据库或要素数据集
final_output_path = os.path.join(os.path.dirname(output_folder), f"{raster_name}_eliminate.shp")
# final_output_path = f"{raster_name}_processed"
else:
# 输出到文件夹 (例如 Shapefile)
final_output_path = os.path.join(output_folder, f"{raster_name}_eliminate.shp")
# final_output_path = f"{raster_name}_processed.shp"
# 5. 执行重分类 (如果 remap_table 存在)
if remap_table:
reclassed_raster = reclassify_raster(input_raster, remap_table, temp_files_to_clean)
arcpy.Raster(reclassed_raster).save(os.path.join(output_folder,f"{Path(input_raster).stem}分级后.tif"))
# 6. 栅格转多边形
print_status(f"开始栅格转多边形: {reclassed_raster}")
temp_polygon_output = os.path.join("in_memory", f"raster_to_polygon_{uuid.uuid4().hex[:8]}")
temp_files_to_clean.append(temp_polygon_output)
simplify_value = "SIMPLIFY" if simplify else "NO_SIMPLIFY"
arcpy.conversion.RasterToPolygon(reclassed_raster, temp_polygon_output, simplify_value, "VALUE")
print_status(f"栅格转多边形完成,结果在内存: {temp_polygon_output}")
# 将内存中的要素类保存到硬盘
disk_output_polygon = os.path.join(disk_output_path, f"{raster_name}_reclassed_polygon.shp")
arcpy.CopyFeatures_management(temp_polygon_output, disk_output_polygon)
print_status(f"已将重分类转面结果保存到硬盘: {disk_output_polygon}")
# 7. 裁剪 (如果 clip_features 存在)
current_polygon_source = temp_polygon_output
if clip_enabled:
print_status(f"开始裁剪要素类: {current_polygon_source} using {clip_features}")
temp_cliped_polygon_output = os.path.join("in_memory", f"cliped_{uuid.uuid4().hex[:8]}")
temp_files_to_clean.append(temp_cliped_polygon_output)
arcpy.analysis.Clip(current_polygon_source, clip_features, temp_cliped_polygon_output)
current_polygon_source = temp_cliped_polygon_output
print_status(f"裁剪完成,结果在内存: {current_polygon_source}")
# 多部件至单部件 (通常在裁剪后进行,确保每个要素都是独立的单部件)
print_status(f"开始多部件至单部件转换: {current_polygon_source}")
temp_multi_to_single_output = os.path.join("in_memory", f"single_{uuid.uuid4().hex[:8]}")
temp_files_to_clean.append(temp_multi_to_single_output)
arcpy.management.MultipartToSinglepart(current_polygon_source, temp_multi_to_single_output)
current_polygon_source = temp_multi_to_single_output
print_status(f"多部件至单部件转换完成,结果在内存: {current_polygon_source}")
# 8. 消除小面积图斑
print_status(f"开始消除小面积图斑: {current_polygon_source} (阈值: {min_area} {area_unit})")
eliminate_small_polygons(
current_polygon_source,
final_output_path, # 直接传递最终输出路径
min_area,
area_unit,
temp_files_to_clean # 传递临时文件列表
)
print_status("消除小面积图斑完成.")
# 9. 最终清理和输出结果
print_status("处理流程全部完成.")
# 清理在内存或临时位置生成的中间文件
temp_files_processor.clean_up_temp_files(temp_files=temp_files_to_clean, workspace=original_workspace)
# 验证最终输出文件是否存在
if arcpy.Exists(final_output_path):
print_result(True, final_output_path, "")
else:
raise Exception(f"处理完成,但最终输出文件 {final_output_path} 不存在。")
except FileNotFoundError as fnf_e:
error_msg = f"文件不存在错误: {str(fnf_e)}"
print_status(error_msg)
print_result(False, "", error_msg)
except ValueError as ve:
error_msg = f"参数错误或数据校验失败: {str(ve)}"
print_status(error_msg)
print_result(False, "", error_msg)
except arcpy.ExecuteError:
# 捕获 ArcPy 执行错误
error_msg = f"ArcPy 执行错误: {arcpy.GetMessages(2)}"
print_status(error_msg)
sys.stderr.write(f"ArcPyExecuteError:{arcpy.GetMessages(2)}\n") # 记录到标准错误
print_result(False, "", error_msg)
except Exception as e:
# 捕获其他未预料的错误
error_msg = f"发生未预料的错误: {str(e)}\n{traceback.format_exc()}"
print_status(error_msg)
sys.stderr.write(f"UnexpectedError:{error_msg}\n") # 记录到标准错误
print_result(False, "", error_msg)
finally:
# 确保在任何情况下都尝试清理(尽管在 except 块中也调用了)
# 这里的调用是最后的保障,如果 except 块中的清理失败了
print_status("脚本结束,执行最终清理...")
temp_files_processor.clean_up_temp_files(temp_files=temp_files_to_clean, workspace=original_workspace)
print_status("最终清理完成.")
sys.exit(0) # 正常退出脚本
if __name__ == "__main__":
print_status("脚本开始执行...")
main()

View File

@@ -0,0 +1,392 @@
# -*- coding: utf-8 -*-
import os
import re
import arcpy
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font, Border, Side, Alignment
from openpyxl.utils import get_column_letter
from tools.core.utils import arcgis_utils, common_utils
from tools.core.utils.os_utils import temp_files_processor
trzd5_order = ["砂质", "砂壤质", "壤质", "黏壤质", "黏质"]
trzd12_order = ["砂土及壤质砂土", "砂质壤土", "壤土", "粉砂质壤土", "砂质黏壤土", "黏壤土", "粉砂质黏壤土", "砂质黏土", "壤质黏土", "粉砂质黏土", "黏土", "重黏土"]
# --- 2. 辅助函数 ---
# 判断单元格类型
def get_merge_type(merged_range):
"""
判断合并类型
返回: 'row'(行合并), 'column'(列合并), 'both'(行列合并)或 None不是合并单元格
"""
if not merged_range:
return None
min_row, max_row = merged_range.min_row, merged_range.max_row
min_col, max_col = merged_range.min_col, merged_range.max_col
if max_row > min_row and max_col > min_col:
return 'both' # 同时跨行和跨列
elif max_row > min_row:
return 'row' # 行合并(垂直合并)
elif max_col > min_col:
return 'column' # 列合并(水平合并)
else:
return None # 实际上不是合并单元格
# 计算属性等级
def get_prop_level(prop_level):
"""根据输入值判断 返回等级"""
if pd.isna(prop_level) or str(prop_level) == "0":
return "-"
# 请根据您的实际分级标准调整这里的阈值
if str(prop_level) == "8" or prop_level == '砂土及壤质砂土':
return "砂质"
elif str(prop_level) == "11" or prop_level == '砂质壤土':
return "砂壤质"
elif str(prop_level) in ["6","3"] or prop_level in ['粉砂质壤土', '壤土']:
return "壤质"
elif str(prop_level) in ["1","4","9"] or prop_level in ['粉砂质年壤土', '黏壤土', '砂质黏壤土']:
return "黏壤质"
elif str(prop_level) in ["2","5","7","10","12"] or prop_level in ['粉砂质黏土', '黏土', '壤质黏土', '砂质黏土', '重黏土']:
return "黏质"
else:
return "-"
# 等级计算
def process_soil_dataframe(df:pd.DataFrame, level_config, target_prop):
"""
处理土壤数据DataFrame添加分级列
"""
result_df = df.copy()
if level_config and target_prop in df.columns:
grade_standards = level_config["标准等级"]
grade_column = "GRIDCODE"
# 使用向量化方法(性能更好)
result_df[grade_column] = common_utils.vectorized_grade_assignment(
df[target_prop].values, grade_standards
)
# 统计分级结果
result_df['YJDL'] = result_df['TDLYLX'].str[:2]
return result_df
# --- 3. 数据处理与分析 均值---
def process_data_for_table1(gdb_path, soil_prop_feature_name, df_origin_area, target_areas_dict,xzqmc,is_by_xzq, prop_config=None):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("开始处理数据...")
def clean_df(df, columns):
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# ==a. 处理样点数据,计算“样点均值” ---
print("--> 步骤1: 计算样点均值...")
field_name = soil_prop_feature_name
sample_table_path = os.path.join(gdb_path, soil_prop_feature_name)
sample_fields = ['TDLYLX', field_name]
df_samples = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(sample_table_path, sample_fields, skip_nulls=False))
df_samples = clean_df(df_samples, [field_name])
processed_df = process_soil_dataframe(df_samples, prop_config, field_name) # 返回具有属性分级的列
processed_df['GRIDCODE'] = processed_df['GRIDCODE'].astype('int')
processed_df['属性分级'] = processed_df['GRIDCODE'].apply(get_prop_level)
# 计算全部样点均值、中位值、范围
processed_df[field_name] = processed_df[field_name].astype('float')
# ===处理样点数据,计算 各分级样点数
df_sample_means = processed_df.groupby(['属性分级','GRIDCODE']).size().reset_index(name='样点数')
df_sample_means['样点数占比'] = df_sample_means['样点数'] / df_sample_means['样点数'].sum() * 100
print("样点数计算完成。")
# ==处理制图数据,获各等级制图面积
# print(df_origin_area)
df_origin_area['YJDL'] = df_origin_area['YJDL_EJDL'].str.split('_').str[0]
df_map_data = df_origin_area.groupby(["XZQMC","YJDL", "GRIDCODE"]).agg({"temp_area": "sum"}).reset_index()
# print(df_map_data)
try:
if is_by_xzq:
df_map_data['adjusted_area'] = df_map_data['temp_area']
df_map_data['adjustment_factor'] = 1.0
# 获取所有存在的行政区和地类
existing_districts = df_map_data['XZQMC'].unique()
# 检查目标字典中的行政区是否存在
missing_districts = []
tt = [td for td in target_areas_dict.keys()]
for ed in existing_districts:
if ed not in tt:
missing_districts.append(ed)
# 如果有行政区不存在,返回原始数据并提示
if missing_districts:
print(f"警告:平差数据中不存在行政区: {missing_districts},未进行平差")
# 计算每个行政区每个地类的原始总面积
original_totals = df_map_data.groupby(['XZQMC', 'YJDL'])['temp_area'].sum()
# 对每个行政区的每个地类进行平差
for xzqmc, landuse_targets in target_areas_dict.items():
for yjdl, target_area in landuse_targets.items():
# 检查该行政区是否有此地类数据
if (xzqmc, yjdl) in original_totals.index and original_totals[(xzqmc, yjdl)] > 0:
adjustment_factor = target_area / original_totals[(xzqmc, yjdl)]
# 应用平差系数
mask = (df_map_data['XZQMC'] == xzqmc) & (df_map_data['YJDL'] == yjdl)
df_map_data.loc[mask, 'temp_area'] = df_map_data.loc[mask, 'temp_area'] * adjustment_factor
df_map_data.loc[mask, 'adjustment_factor'] = adjustment_factor
# print(f"{xzqmc} - 地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}")
else:
# 用df_target_area按YJDL进行平差计算
original_totals = df_map_data.groupby('YJDL')['temp_area'].sum().to_dict()
# 对每个地类进行平差
target_area_dict = target_areas_dict.get(xzqmc,"")
# print(target_areas_dict)
for yjdl, target_area in target_area_dict.items():
if (yjdl in original_totals and original_totals[yjdl] > 0) or target_area > 0:
adjustment_factor = target_area / original_totals[yjdl]
# 应用平差系数
mask = df_map_data['YJDL'] == yjdl
df_map_data.loc[mask, 'temp_area'] = df_map_data.loc[mask, 'temp_area'] * adjustment_factor
df_map_data.loc[mask, 'adjustment_factor'] = adjustment_factor
# print(f"地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}")
except Exception as e:
print(f"平差处理失败: {e}")
# print(df_map_data)
df_map_data['面积_亩'] = df_map_data['temp_area']
df_map_data['属性分级'] = df_map_data['GRIDCODE'].apply(get_prop_level)
df_map_areas = df_map_data.groupby(['属性分级','GRIDCODE'])['面积_亩'].sum().reset_index(name='制图面积')
# 面积平差
df_map_areas['制图面积_平差后'] = df_map_areas['制图面积']
# ===计算面积占比
df_map_areas['面积占比'] = df_map_areas['制图面积_平差后'] / df_map_areas['制图面积_平差后'].sum() * 100
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['属性分级','GRIDCODE']],
df_map_areas[['属性分级','GRIDCODE']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['属性分级','GRIDCODE'], how='left')
df_final = pd.merge(df_final, df_map_areas, on=['属性分级','GRIDCODE'], how='left')
# print(df_final)
# (可选) 按“一级地类”和“二级地类”排序
df_final["属性分级"] = pd.Categorical(df_final['属性分级'], categories=trzd5_order, ordered=True)
# df_final["EJDL"] = pd.Categorical(df_final['EJDL'], categories=in_ejdl_order, ordered=True)
df_final.sort_values(['属性分级','GRIDCODE'], inplace=True)
print("数据处理流程完成!")
# print(df_final)
return df_final
# --- 3. Excel 制表 总表---
def write_to_excel_table1(df:pd.DataFrame, output_path, prop_config):
"""
【最终修正版】: 将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel将创建一个空的报告。")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws['A1'] = "没有有效的统计数据。"
wb.save(output_path)
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "行政区酸化程度等级分布及占比"
# --- a. 定义样式 (不变) ---
header_font = Font(name='宋体', size=11)
cell_font = Font(name='宋体', size=11)
center_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
def apply_style(cell_range, font, alignment=None, border=None):
for row in ws[cell_range]:
for cell in row:
cell.font = font
if alignment: cell.alignment = alignment
if border: cell.border = border
# --- b. 绘制表头 (不变) ---
ws.merge_cells('A1:B1'); ws['A1'] = '土壤三普分类'
ws.merge_cells('C1:D1'); ws['C1'] = '样点统计'
ws.merge_cells('E1:F1'); ws['E1'] = '制图统计'
ws['A2'] = '类别'; ws['B2'] = '名称'
ws['C2'] = '数量/个'; ws['D2'] = '占比%'
ws['E2'] = '面积/亩'; ws['F2'] = '占比%'
level_dict = prop_config['标准等级']
# 创建两个列表来分别存储上段和下段范围
upper_ranges = {value: key for key, value in level_dict.items()}
# --- c. 填充数据 ---
current_row = 3
df_to_write = df.copy() # 使用 .copy() 避免 SettingWithCopyWarning
for yl, group_yl_df in df_to_write.groupby('属性分级', sort=False, observed=False):
yl_start_row = current_row
# 1. 遍历该一级地类下的所有“二级地类”并写入数据
for _, row_data in group_yl_df.iterrows():
ws.cell(row=current_row, column=2).value = upper_ranges.get(str(row_data['GRIDCODE']), '-')
# --- 填充单元格的逻辑开始 ---
col_start = 3 # 从第 C 列开始填充
# 检查是否找到了该土属的数据
if not row_data.empty:
# 1. 构建要从 data_series 中查找的列名
sample_col = f'样点数'
sample_pct_col = f'样点数占比'
area_col = f'制图面积_平差后'
area_pct_col = f'面积占比'
# 2. 从 data_series 中安全地获取值
sample_val = row_data.get(sample_col, 0)
sample_pct_val = row_data.get(sample_pct_col, 0)
area_val = row_data.get(area_col, 0)
area_pct_val = row_data.get(area_pct_col, 0)
# 3. 将获取到的值填入单元格
ws.cell(row=current_row, column=col_start).value = f"{sample_val:.0f}" if sample_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=col_start + 1).value = f"{sample_pct_val:.1f}" if sample_val > 0 else "-"
# 制图面积/亩
ws.cell(row=current_row, column=col_start + 2).value = f"{area_val:.0f}" if area_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=col_start + 3).value = f"{area_pct_val:.1f}" if area_val > 0 else "-"
# 移动到下一个酸化等级的起始列
col_start += 2
else:
for _ in range(4):
ws.cell(row=current_row, column=col_start).value = "-"
col_start += 1
current_row += 1
# 合并“一级地类”单元格
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row-1, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
# 2. 填充总计行
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=2)
ws.cell(row=current_row, column=1).value = '全区'
ws.cell(row=current_row, column=3).value = df['样点数'].sum()
ws.cell(row=current_row, column=4).value = '100'
ws.cell(row=current_row, column=5).value = f"{df['制图面积_平差后'].sum():.0f}"
ws.cell(row=current_row, column=6).value = '100'
# --- d. 应用样式和调整列宽 (最终健壮版) ---
if current_row > 1: # 确保有数据才应用样式
apply_style(f'A1:F{current_row}', cell_font, center_align, thin_border)
apply_style(f'A1:F2', header_font)
print("正在自动调整列宽...")
dims = {}
for row in ws.rows:
for cell in row:
if cell.value:
merged_range = next((range for range in ws.merged_cells.ranges if cell.coordinate in range), None)
if get_merge_type(merged_range) == 'column':
continue
cell_len = 0.7 * len(re.findall('([\u4e00-\u9fa5])', str(cell.value))) + len(str(cell.value))
dims[cell.column] = max(dims.get(cell.column, 0), cell_len)
# 设置列宽
for col, value in dims.items():
ws.column_dimensions[get_column_letter(int(col))].width = value + 5
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
def main(gdb_path, soil_prop_name, reclassed_features_path, dltb_features, output_path, target_area_dict,xzqmc, prop_config):
try:
# --- 1. 用户配置 ---
# 输出配置
temp_files = []
output_excel_path = os.path.join(output_path, f"{soil_prop_name}土壤分级分布.xlsx") # 生成的Excel报告文件路径
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
print("开始处理数据...")
is_by_xzq = False if xzqmc not in ["北海市","来宾市","楚雄自治州"] else True
# out_table_mean = r"in_memory/out_table_mean"
temp_out_feature_class = r"in_memory/temp_out_feature_class"
temp_out_tables_area = r"in_memory/temp_out_tables_area"
# temp_files.append(out_table_mean)
temp_files.append(temp_out_tables_area)
# 求地类图斑和重分类栅格面的交集
arcpy.analysis.Intersect(
in_features=[dltb_features,reclassed_features_path],
out_feature_class=temp_out_feature_class,
join_attributes="ALL",
output_type="INPUT"
)
# 行政区划和相交结果进行交集制表
arcpy.analysis.TabulateIntersection(
in_zone_features="行政区划", # 乡镇边界
zone_fields="XZQMC",
in_class_features=temp_out_feature_class,
out_table=temp_out_tables_area,
class_fields="gridcode;YJDL_EJDL",
out_units="SQUARE_METERS"
)
clipped_table_df = arcgis_utils.read_arcgis_table(temp_out_tables_area)
# 生成表1 土壤属性分级分布 的统计Excel报告
final_dataframe = process_data_for_table1(gdb_path, soil_prop_name, clipped_table_df, target_area_dict,xzqmc,is_by_xzq, prop_config)
write_to_excel_table1(final_dataframe, output_excel_path, prop_config)
# return df_with_factors
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
temp_files_processor.clean_up_temp_files(temp_files)
import gc
gc.collect()
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# main()

View File

@@ -0,0 +1,360 @@
# -*- coding: utf-8 -*-
import os
import re
import arcpy
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font, Border, Side, Alignment
from openpyxl.utils import get_column_letter
from tools.core.utils import arcgis_utils, common_utils
from tools.core.utils.os_utils import temp_files_processor
# --- 2. 辅助函数 ---
# 判断单元格类型
def get_merge_type(merged_range):
"""
判断合并类型
返回: 'row'(行合并), 'column'(列合并), 'both'(行列合并)或 None不是合并单元格
"""
if not merged_range:
return None
min_row, max_row = merged_range.min_row, merged_range.max_row
min_col, max_col = merged_range.min_col, merged_range.max_col
if max_row > min_row and max_col > min_col:
return 'both' # 同时跨行和跨列
elif max_row > min_row:
return 'row' # 行合并(垂直合并)
elif max_col > min_col:
return 'column' # 列合并(水平合并)
else:
return None # 实际上不是合并单元格
# 计算属性等级
def get_prop_level(prop_level):
"""根据输入值判断 返回等级"""
if pd.isna(prop_level) or prop_level == 0:
return "-"
# 请根据您的实际分级标准调整这里的阈值
if int(prop_level) == 5 or prop_level == "砂质":
return "砂质"
elif int(prop_level) == 4 or prop_level == "砂壤质":
return "砂壤质"
elif int(prop_level) == 3 or prop_level == "壤质":
return "壤质"
elif int(prop_level) == 1 or prop_level == "黏壤质":
return "黏壤质"
elif int(prop_level) == 2 or prop_level == "黏质":
return "黏质"
else:
return "-"
# 等级计算
def process_soil_dataframe(df:pd.DataFrame, level_config, target_prop):
"""
处理土壤数据DataFrame添加分级列
"""
result_df = df.copy()
if level_config and target_prop in df.columns:
grade_standards = level_config["标准等级"]
grade_column = "GRIDCODE"
# 使用向量化方法(性能更好)
result_df[grade_column] = common_utils.vectorized_grade_assignment(
df[target_prop].values, grade_standards
)
# 统计分级结果
result_df['YJDL'] = result_df['TDLYLX'].str[:2]
return result_df
# --- 3. 数据处理与分析 均值---
def process_data_for_table1(gdb_path, soil_prop_feature_name, df_origin_area, target_areas_dict,xzqmc,is_by_xzq, prop_config=None):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("开始处理数据...")
def clean_df(df, columns):
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# ==a. 处理样点数据,计算“样点均值” ---
print("--> 步骤1: 计算样点均值...")
field_name = soil_prop_feature_name
sample_table_path = os.path.join(gdb_path, soil_prop_feature_name)
sample_fields = ['TDLYLX', field_name]
df_samples = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(sample_table_path, sample_fields, skip_nulls=False))
df_samples = clean_df(df_samples, [field_name])
processed_df = df_samples.copy()
processed_df['属性分级'] = processed_df[field_name]
# ===处理样点数据,计算 各分级样点数
df_sample_means = processed_df.groupby(['属性分级']).size().reset_index(name='样点数')
df_sample_means['样点数占比'] = df_sample_means['样点数'] / df_sample_means['样点数'].sum() * 100
print("样点数计算完成。")
# ==处理制图数据,获各等级制图面积
# print(df_origin_area)
df_origin_area['YJDL'] = df_origin_area['YJDL_EJDL'].str.split('_').str[0]
df_map_data = df_origin_area.groupby(["XZQMC","YJDL", "GRIDCODE"]).agg({"temp_area": "sum"}).reset_index()
# print(df_map_data)
try:
if is_by_xzq:
df_map_data['adjusted_area'] = df_map_data['temp_area']
df_map_data['adjustment_factor'] = 1.0
# 获取所有存在的行政区和地类
existing_districts = df_map_data['XZQMC'].unique()
# 检查目标字典中的行政区是否存在
missing_districts = []
tt = [td for td in target_areas_dict.keys()]
for ed in existing_districts:
if ed not in tt:
missing_districts.append(ed)
# 如果有行政区不存在,返回原始数据并提示
if missing_districts:
print(f"警告:平差数据中不存在行政区: {missing_districts},未进行平差")
# 计算每个行政区每个地类的原始总面积
original_totals = df_map_data.groupby(['XZQMC', 'YJDL'])['temp_area'].sum()
# 对每个行政区的每个地类进行平差
for xzqmc, landuse_targets in target_areas_dict.items():
for yjdl, target_area in landuse_targets.items():
# 检查该行政区是否有此地类数据
if (xzqmc, yjdl) in original_totals.index and original_totals[(xzqmc, yjdl)] > 0:
adjustment_factor = target_area / original_totals[(xzqmc, yjdl)]
# 应用平差系数
mask = (df_map_data['XZQMC'] == xzqmc) & (df_map_data['YJDL'] == yjdl)
df_map_data.loc[mask, 'temp_area'] = df_map_data.loc[mask, 'temp_area'] * adjustment_factor
df_map_data.loc[mask, 'adjustment_factor'] = adjustment_factor
# print(f"{xzqmc} - 地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}")
else:
# 用df_target_area按YJDL进行平差计算
original_totals = df_map_data.groupby('YJDL')['temp_area'].sum().to_dict()
# 对每个地类进行平差
target_area_dict = target_areas_dict.get(xzqmc,"")
# print(target_areas_dict)
for yjdl, target_area in target_area_dict.items():
if (yjdl in original_totals and original_totals[yjdl] > 0) or target_area > 0:
adjustment_factor = target_area / original_totals[yjdl]
# 应用平差系数
mask = df_map_data['YJDL'] == yjdl
df_map_data.loc[mask, 'temp_area'] = df_map_data.loc[mask, 'temp_area'] * adjustment_factor
df_map_data.loc[mask, 'adjustment_factor'] = adjustment_factor
# print(f"地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}")
except Exception as e:
print(f"平差处理失败: {e}")
# print(df_map_data)
df_map_data['面积_亩'] = df_map_data['temp_area']
df_map_data['属性分级'] = df_map_data['GRIDCODE'].apply(get_prop_level)
df_map_areas = df_map_data.groupby(['属性分级'])['面积_亩'].sum().reset_index(name='制图面积')
# 面积平差
df_map_areas['制图面积_平差后'] = df_map_areas['制图面积']
# ===计算面积占比
df_map_areas['面积占比'] = df_map_areas['制图面积_平差后'] / df_map_areas['制图面积_平差后'].sum() * 100
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['属性分级']],
df_map_areas[['属性分级']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['属性分级'], how='left')
df_final = pd.merge(df_final, df_map_areas, on=['属性分级'], how='left')
# print(df_final)
df_final.sort_values(['属性分级'], inplace=True)
print("数据处理流程完成!")
# print(df_final)
return df_final
# --- 3. Excel 制表 总表---
def write_to_excel_table1(df:pd.DataFrame, output_path, prop_config):
"""
【最终修正版】: 将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel将创建一个空的报告。")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws['A1'] = "没有有效的统计数据。"
wb.save(output_path)
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "土壤质地分类分布"
# --- a. 定义样式 (不变) ---
header_font = Font(name='宋体', size=11)
cell_font = Font(name='宋体', size=11)
center_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
def apply_style(cell_range, font, alignment=None, border=None):
for row in ws[cell_range]:
for cell in row:
cell.font = font
if alignment: cell.alignment = alignment
if border: cell.border = border
# --- b. 绘制表头 (不变) ---
ws.merge_cells('A1:A2'); ws['A1'] = '土壤质地类别'
ws.merge_cells('B1:C1'); ws['B1'] = '样点统计'
ws.merge_cells('D1:E1'); ws['D1'] = '制图统计'
ws['B2'] = '数量/个'; ws['C2'] = '占比%'
ws['D2'] = '面积/亩'; ws['E2'] = '占比%'
level_dict = prop_config['标准等级']
# 创建两个列表来分别存储上段和下段范围
upper_ranges = {value: key for key, value in level_dict.items()}
# --- c. 填充数据 ---
current_row = 3
df_to_write = df.copy() # 使用 .copy() 避免 SettingWithCopyWarning
for index, row_data in df_to_write.iterrows():
# 检查是否找到了该土属的数据
if not row_data.empty:
# 1. 构建要从 data_series 中查找的列名
sample_col = f'样点数'
sample_pct_col = f'样点数占比'
area_col = f'制图面积_平差后'
area_pct_col = f'面积占比'
# 2. 从 data_series 中安全地获取值
row_name = row_data.get('属性分级', "")
sample_val = row_data.get(sample_col, 0)
sample_pct_val = row_data.get(sample_pct_col, 0)
area_val = row_data.get(area_col, 0)
area_pct_val = row_data.get(area_pct_col, 0)
ws.cell(row=current_row, column=1).value = f"{row_name}" if row_name else "-"
# 3. 将获取到的值填入单元格
ws.cell(row=current_row, column=2).value = f"{sample_val:.0f}" if sample_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=3).value = f"{sample_pct_val:.1f}" if sample_val > 0 else "-"
# 制图面积/亩
ws.cell(row=current_row, column=4).value = f"{area_val:.0f}" if area_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=5).value = f"{area_pct_val:.1f}" if area_val > 0 else "-"
current_row += 1
# 2. 填充总计行
ws.cell(row=current_row, column=1).value = '全区'
ws.cell(row=current_row, column=2).value = df['样点数'].sum()
ws.cell(row=current_row, column=3).value = '100'
ws.cell(row=current_row, column=4).value = f"{df['制图面积_平差后'].sum():.0f}"
ws.cell(row=current_row, column=5).value = '100'
# --- d. 应用样式和调整列宽 (最终健壮版) ---
if current_row > 1: # 确保有数据才应用样式
apply_style(f'A1:E{current_row}', cell_font, center_align, thin_border)
apply_style(f'A1:E2', header_font)
print("正在自动调整列宽...")
dims = {}
for row in ws.rows:
for cell in row:
if cell.value:
merged_range = next((range for range in ws.merged_cells.ranges if cell.coordinate in range), None)
if get_merge_type(merged_range) == 'column':
continue
cell_len = 0.7 * len(re.findall('([\u4e00-\u9fa5])', str(cell.value))) + len(str(cell.value))
dims[cell.column] = max(dims.get(cell.column, 0), cell_len)
# 设置列宽
for col, value in dims.items():
ws.column_dimensions[get_column_letter(int(col))].width = value + 5
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
def main(gdb_path, soil_prop_name, reclassed_features_path, dltb_features, output_path, target_area_dict,xzqmc, prop_config):
try:
# --- 1. 用户配置 ---
# 输出配置
temp_files = []
output_excel_path = os.path.join(output_path, f"{soil_prop_name}土壤分级分布.xlsx") # 生成的Excel报告文件路径
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
print("开始处理数据...")
is_by_xzq = False if xzqmc not in ["北海市","来宾市","楚雄自治州"] else True
# out_table_mean = r"in_memory/out_table_mean"
temp_out_feature_class = r"in_memory/temp_out_feature_class"
temp_out_tables_area = r"in_memory/temp_out_tables_area"
# temp_files.append(out_table_mean)
temp_files.append(temp_out_tables_area)
# 求地类图斑和重分类栅格面的交集
arcpy.analysis.Intersect(
in_features=[dltb_features,reclassed_features_path],
out_feature_class=temp_out_feature_class,
join_attributes="ALL",
output_type="INPUT"
)
# 行政区划和相交结果进行交集制表
arcpy.analysis.TabulateIntersection(
in_zone_features="行政区划", # 乡镇边界
zone_fields="XZQMC",
in_class_features=temp_out_feature_class,
out_table=temp_out_tables_area,
class_fields="gridcode;YJDL_EJDL",
out_units="SQUARE_METERS"
)
clipped_table_df = arcgis_utils.read_arcgis_table(temp_out_tables_area)
# 生成表1 土壤属性分级分布 的统计Excel报告
final_dataframe = process_data_for_table1(gdb_path, soil_prop_name, clipped_table_df, target_area_dict,xzqmc,is_by_xzq, prop_config)
write_to_excel_table1(final_dataframe, output_excel_path, prop_config)
# return df_with_factors
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
temp_files_processor.clean_up_temp_files(temp_files)
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# main()

View File

@@ -0,0 +1,513 @@
# -*- coding: utf-8 -*-
import os
import re
import arcpy
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font
from tools.core.utils import arcgis_utils, common_utils
from tools.core.utils.os_utils import temp_files_processor
from tools.core.utils.excel_utils import ExcelStyleUtils
# --- 2. 辅助函数 ---
xn_region = ['天峨县', '寻甸县', '罗平县', '丘北县', '永仁县', '南华县', '双柏县', '武定县', '祥云县', '楚雄彝族自治州']
hn_region = ['北海市', '海城区', '银海区', '铁山港区', '港南区', '容县', '平南县', '兴宁区', '武鸣区', '邕宁区', '苍梧县', '靖西市', '西畴县', '马关县', '澜沧县', '双江县', '永德县']
# 计算属性等级
def get_prop_level(prop_level):
"""根据输入值判断 返回等级"""
if pd.isna(prop_level) or prop_level == 0:
return "-"
# 请根据您的实际分级标准调整这里的阈值
if int(prop_level) == 1 or int(prop_level) == 6 or prop_level == '等级一':
return "Ⅰ级"
elif int(prop_level) == 2 or int(prop_level) == 7 or prop_level == '等级二':
return "Ⅱ级"
elif int(prop_level) == 3 or int(prop_level) == 8 or prop_level == '等级三':
return "Ⅲ级"
elif int(prop_level) == 4 or int(prop_level) == 9 or prop_level == '等级四':
return "Ⅳ级"
elif int(prop_level) == 5 or int(prop_level) == 10 or prop_level == '等级五':
return "Ⅴ级"
else:
return "-"
def get_prop_level_for_pH(prop_level):
if pd.isna(prop_level) or prop_level == 0:
return "-"
if int(prop_level) == 5 or prop_level == "等级五":
return "Ⅰ级"
elif int(prop_level) in [4, 6] or prop_level in ["等级四", "等级六"]:
return "Ⅱ级"
elif int(prop_level) in [3, 7] or prop_level in ["等级三", "等级七"]:
return "Ⅲ级"
elif int(prop_level) in [2, 8] or prop_level in ["等级二", "等级八"]:
return "Ⅳ级"
elif int(prop_level) in [1, 9] or prop_level in ["等级一", "等级九"]:
return "Ⅴ级"
else:
return "-"
def get_prop_level_for_hn_TRRZ(prop_level):
if pd.isna(prop_level) or prop_level == 0:
return "-"
if int(prop_level) == 3 or prop_level == "等级三":
return "Ⅰ级"
elif int(prop_level) == 4 or prop_level == "等级四":
return "Ⅱ级"
elif int(prop_level) in [2, 5] or prop_level in ["等级二", "等级五"]:
return "Ⅲ级"
elif int(prop_level) == 6 or prop_level == "等级六":
return "Ⅳ级"
elif int(prop_level) in [1, 7] or prop_level in ["等级一", "等级七"]:
return "Ⅴ级"
else:
return "-"
def get_prop_level_for_xn_TRRZ(prop_level):
if pd.isna(prop_level) or prop_level == 0:
return "-"
if int(prop_level) == 4 or prop_level == "等级四":
return "Ⅰ级"
elif int(prop_level) in [3,5] or prop_level in ["等级三", "等级五"]:
return "Ⅱ级"
elif int(prop_level) == 6 or prop_level == "等级六":
return "Ⅲ级"
elif int(prop_level) in [2, 7] or prop_level in ["等级二", "等级七"]:
return "Ⅳ级"
elif int(prop_level) in [1, 8] or prop_level in ["等级一", "等级八"]:
return "Ⅴ级"
else:
return "-"
# 等级计算
def process_soil_dataframe(df:pd.DataFrame, level_config, target_prop):
"""
处理土壤数据DataFrame添加分级列
"""
result_df = df.copy()
if level_config and target_prop in df.columns:
grade_standards = level_config["标准等级"]
grade_column = "GRIDCODE"
# 使用向量化方法(性能更好)
result_df[grade_column] = common_utils.vectorized_grade_assignment(
df[target_prop].values, grade_standards
)
# 统计分级结果
result_df['YJDL'] = result_df['TDLYLX'].str[:2]
return result_df
# --- 3. 数据处理与分析 均值---
def process_data_for_table1(gdb_path, soil_prop_feature_name, df_origin_area, target_areas_dict,xzqmc,is_by_xzq, prop_config=None):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("开始处理数据...")
def clean_df(df, columns):
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# ==a. 处理样点数据,计算“样点均值” ---
print("--> 步骤1: 计算样点均值...")
field_name = soil_prop_feature_name
sample_table_path = os.path.join(gdb_path, soil_prop_feature_name)
sample_fields = ['TDLYLX', field_name]
df_samples = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(sample_table_path, sample_fields, skip_nulls=False))
df_samples = clean_df(df_samples, [field_name])
processed_df = process_soil_dataframe(df_samples, prop_config, field_name) # 返回具有属性分级的列
processed_df['GRIDCODE'] = processed_df['GRIDCODE'].astype('int')
if soil_prop_feature_name == 'PH':
processed_df['属性分级'] = processed_df['GRIDCODE'].apply(get_prop_level_for_pH)
elif soil_prop_feature_name == 'TRRZ' and xzqmc in hn_region:
processed_df['属性分级'] = processed_df['GRIDCODE'].apply(get_prop_level_for_hn_TRRZ)
elif soil_prop_feature_name == 'TRRZ' and xzqmc in xn_region:
processed_df['属性分级'] = processed_df['GRIDCODE'].apply(get_prop_level_for_xn_TRRZ)
else:
processed_df['属性分级'] = processed_df['GRIDCODE'].apply(get_prop_level)
# 计算全部样点均值、中位值、范围
processed_df[field_name] = processed_df[field_name].astype('float')
stat_sample = {
'min': processed_df[field_name].min(),
'max': processed_df[field_name].max(),
'mean':processed_df[field_name].mean(),
'median': processed_df[field_name].median(),
}
# ===处理样点数据,计算 各分级样点数
df_sample_means = processed_df.groupby(['属性分级','GRIDCODE']).size().reset_index(name='样点数')
df_sample_means['样点数占比'] = df_sample_means['样点数'] / df_sample_means['样点数'].sum() * 100
print("样点数计算完成。")
# ==处理制图数据,获各等级制图面积
# print(df_origin_area)
df_origin_area['YJDL'] = df_origin_area['YJDL_EJDL'].str.split('_').str[0]
# 定义需要过滤地类的属性列表
filtered_props = ['ECA', 'EMG', 'ACU', 'AZN', 'AFE', 'AMN', 'AMO', 'AB', 'AS1', 'TSE']
# 如果当前属性在列表中,则只统计耕地和园地
if soil_prop_feature_name in filtered_props:
farmland_yjdl = ['耕地', '园地'] # 01: 耕地, 02: 园地
df_origin_area = df_origin_area[df_origin_area['YJDL'].isin(farmland_yjdl)]
print(f"过滤制图数据仅统计耕地和园地YJDL in {farmland_yjdl}")
# 如果土壤属性为GZCHD则只需要耕地的面积统计
if soil_prop_feature_name in ['GZCHD']:
df_origin_area = df_origin_area[df_origin_area['YJDL'] == '耕地']
print(f"过滤制图数据GZCHD仅统计耕地")
df_map_data = df_origin_area.groupby(["XZQMC","YJDL", "GRIDCODE"]).agg({"temp_area": "sum"}).reset_index()
# print(df_map_data)
try:
if is_by_xzq:
df_map_data['adjusted_area'] = df_map_data['temp_area']
df_map_data['adjustment_factor'] = 1.0
# 获取所有存在的行政区和地类
existing_districts = df_map_data['XZQMC'].unique()
# 检查目标字典中的行政区是否存在
missing_districts = []
tt = [td for td in target_areas_dict.keys()]
for ed in existing_districts:
if ed not in tt:
missing_districts.append(ed)
# 如果有行政区不存在,返回原始数据并提示
if missing_districts:
print(f"警告:平差数据中不存在行政区: {missing_districts},未进行平差")
# 计算每个行政区每个地类的原始总面积
original_totals = df_map_data.groupby(['XZQMC', 'YJDL'])['temp_area'].sum()
# 对每个行政区的每个地类进行平差
for xzqmc, landuse_targets in target_areas_dict.items():
for yjdl, target_area in landuse_targets.items():
# 检查该行政区是否有此地类数据
if (xzqmc, yjdl) in original_totals.index and original_totals[(xzqmc, yjdl)] > 0:
adjustment_factor = target_area / original_totals[(xzqmc, yjdl)]
# 应用平差系数
mask = (df_map_data['XZQMC'] == xzqmc) & (df_map_data['YJDL'] == yjdl)
df_map_data.loc[mask, 'temp_area'] = df_map_data.loc[mask, 'temp_area'] * adjustment_factor
df_map_data.loc[mask, 'adjustment_factor'] = adjustment_factor
# print(f"{xzqmc} - 地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}")
else:
# 用df_target_area按YJDL进行平差计算
original_totals = df_map_data.groupby('YJDL')['temp_area'].sum().to_dict()
# 对每个地类进行平差
target_area_dict = target_areas_dict.get(xzqmc,"")
# print(target_areas_dict)
for yjdl, target_area in target_area_dict.items():
if (yjdl in original_totals and original_totals[yjdl] > 0) or target_area > 0:
adjustment_factor = target_area / original_totals[yjdl]
# 应用平差系数
mask = df_map_data['YJDL'] == yjdl
df_map_data.loc[mask, 'temp_area'] = df_map_data.loc[mask, 'temp_area'] * adjustment_factor
df_map_data.loc[mask, 'adjustment_factor'] = adjustment_factor
# print(f"地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}")
except Exception as e:
print(f"平差处理失败: {e}")
# print(df_map_data)
df_map_data['面积_亩'] = df_map_data['temp_area']
if soil_prop_feature_name == 'PH':
df_map_data['属性分级'] = df_map_data['GRIDCODE'].apply(get_prop_level_for_pH)
elif soil_prop_feature_name == 'TRRZ' and xzqmc in hn_region:
df_map_data['属性分级'] = df_map_data['GRIDCODE'].apply(get_prop_level_for_hn_TRRZ)
elif soil_prop_feature_name == 'TRRZ' and xzqmc in xn_region:
df_map_data['属性分级'] = df_map_data['GRIDCODE'].apply(get_prop_level_for_xn_TRRZ)
else:
df_map_data['属性分级'] = df_map_data['GRIDCODE'].apply(get_prop_level)
df_map_areas = df_map_data.groupby(['属性分级','GRIDCODE'])['面积_亩'].sum().reset_index(name='制图面积')
# 面积平差
df_map_areas['制图面积_平差后'] = df_map_areas['制图面积']
# ===计算面积占比
df_map_areas['面积占比'] = df_map_areas['制图面积_平差后'] / df_map_areas['制图面积_平差后'].sum() * 100
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['属性分级','GRIDCODE']],
df_map_areas[['属性分级','GRIDCODE']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['属性分级','GRIDCODE'], how='left')
df_final = pd.merge(df_final, df_map_areas, on=['属性分级','GRIDCODE'], how='left')
# print(df_final)
df_final.sort_values(['属性分级'], inplace=True)
print("数据处理流程完成!")
# print(df_final)
return df_final, stat_sample
# --- 3. Excel 制表 总表---
def write_to_excel_table1(df:pd.DataFrame, output_path, prop_config, soil_prop_tif, stat_sample):
"""
【最终修正版】: 将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel将创建一个空的报告。")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws['A1'] = "没有有效的统计数据。"
wb.save(output_path)
return
# 全区制图统计
"""
try:
raster = arcpy.Raster(soil_prop_tif)
# 转换为numpy数组进行计算
array = arcpy.RasterToNumPyArray(raster,nodata_to_value=9999)
# 过滤掉NoData值
# 过滤NoData值和9999值
array = array[~np.isnan(array)] # 过滤NoData
array = array[array != 9999] # 过滤9999
array = array.astype(np.float64)
stats = {
'min': round(np.min(array),2),
'max': round(np.max(array),2),
'mean': round(np.mean(array),2),
'median': round(np.median(array),2),
'std': round(np.std(array),2)
}
except Exception as e:
print(f"错误: {e}")
"""
# 全区样点统计
stats = stat_sample
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "行政区酸化程度等级分布及占比"
# 获取属性单位
special_prop = ['耕作层厚度','阳离子','有机质','pH','有效磷','速效钾','交换性钙','交换性镁','有效硫','有效铁','有效锰','有效硅']
fsn_props = ['砂粒含量','粉粒含量','黏粒含量','有效土层厚度']
prop_name_str = prop_config.get('项目分级','')
if prop_name_str:
split_name = prop_name_str.split('\n')[0].strip()
if split_name in special_prop:
prop_name = '1f'
elif split_name in fsn_props:
prop_name = '0f'
else:
prop_name = '2f'
else:
prop_name = '1f'
# print(prop_name_str, prop_name)
prop_unit_str = prop_config.get('分级标准', '')
if prop_unit_str:
prop_unit = prop_unit_str.split('\n')[1].strip()
else:
prop_unit = ''
# --- b. 绘制表头 (不变) ---
ws.merge_cells('A1:B1'); ws['A1'] = '土壤三普分级'
ws.merge_cells('C1:D1'); ws['C1'] = '样点统计'
ws.merge_cells('E1:F1'); ws['E1'] = '制图统计'
ws['A2'] = '分级'; ws['B2'] = '值域/' + prop_unit if prop_unit else '值域'
ws['C2'] = '数量/个'; ws['D2'] = '占比%'
ws['E2'] = '面积/亩'; ws['F2'] = '占比%'
acid_levels = ['Ⅰ级','Ⅱ级', 'Ⅲ级', 'Ⅳ级', 'Ⅴ级']
level_dict = prop_config['标准等级']
# 创建两个列表来分别存储上段和下段范围
upper_ranges = {}
lower_ranges = {}
# 遍历排序后的等级
for i, (level, ranges) in enumerate(sorted(level_dict.items(), key=lambda x: list(level_dict.keys()).index(x[0])), 1):
# 分割范围字符串
range_list = [r.strip() for r in ranges.split(',')]
if len(range_list) >= 1:
upper_ranges[i] = range_list[0]
if len(range_list) >= 2:
# 计算下段范围的索引(原始索引 + 等级总数)
lower_index = i + len(level_dict)
lower_ranges[lower_index] = range_list[1]
# 合并结果
upper_ranges.update(lower_ranges)
# --- c. 填充数据 ---
current_row = 3
df_to_write = df.copy() # 使用 .copy() 避免 SettingWithCopyWarning
for yl, group_yl_df in df_to_write.groupby('属性分级', sort=False, observed=False):
yl_start_row = current_row
# 1. 遍历该一级地类下的所有“二级地类”并写入数据
for _, row_data in group_yl_df.iterrows():
ws.cell(row=current_row, column=2).value = upper_ranges.get(row_data['GRIDCODE'], '-')
# --- 填充单元格的逻辑开始 ---
col_start = 3 # 从第 C 列开始填充
# 检查是否找到了该土属的数据
if not row_data.empty:
# 1. 构建要从 data_series 中查找的列名
sample_col = f'样点数'
sample_pct_col = f'样点数占比'
area_col = f'制图面积_平差后'
area_pct_col = f'面积占比'
# 2. 从 data_series 中安全地获取值
sample_val = row_data.get(sample_col, 0)
sample_pct_val = row_data.get(sample_pct_col, 0)
area_val = row_data.get(area_col, 0)
area_pct_val = row_data.get(area_pct_col, 0)
# 3. 将获取到的值填入单元格
ws.cell(row=current_row, column=col_start).value = f"{sample_val:.0f}" if sample_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=col_start + 1).value = f"{sample_pct_val:.1f}" if sample_val > 0 else "-"
# 制图面积/亩
ws.cell(row=current_row, column=col_start + 2).value = f"{area_val:.0f}" if area_val > 0 else "-"
# 占比/%
ws.cell(row=current_row, column=col_start + 3).value = f"{area_pct_val:.1f}" if area_val > 0 else "-"
# 移动到下一个酸化等级的起始列
col_start += 2
else:
for _ in range(4):
ws.cell(row=current_row, column=col_start).value = "-"
col_start += 1
current_row += 1
# 合并“一级地类”单元格
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row-1, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
# 2. 填充总计行
ws.cell(row=current_row, column=1).value = '全区'
ws.cell(row=current_row, column=2).value = '-'
ws.cell(row=current_row, column=3).value = df['样点数'].sum()
ws.cell(row=current_row, column=4).value = '100'
ws.cell(row=current_row, column=5).value = f"{df['制图面积_平差后'].sum():.0f}"
ws.cell(row=current_row, column=6).value = '100'
# 3. 合计单元格填充
ws.merge_cells(f'B{current_row + 1}:F{current_row + 1}')
ws.cell(row=current_row + 1, column=1).value = '全区均值'
ws.cell(row=current_row + 1, column=2).value = f'{stats["mean"]:.{prop_name}}'
ws.merge_cells(f'B{current_row + 2}:F{current_row + 2}')
ws.cell(row=current_row + 2, column=1).value = '全区中位值'
ws.cell(row=current_row + 2, column=2).value = f'{stats["median"]:.{prop_name}}'
ws.merge_cells(f'B{current_row + 3}:F{current_row + 3}')
ws.cell(row=current_row + 3, column=1).value = '全区范围'
ws.cell(row=current_row + 3, column=2).value = f'{stats["min"]:.{prop_name}} {stats["max"]:.{prop_name}}'
# --- a. 定义样式 ---
header_font = Font(name='宋体', size=11, bold=True)
# --- d. 应用样式和调整列宽 (最终健壮版) ---
if current_row > 1: # 确保有数据才应用样式
ExcelStyleUtils.set_style(ws, f'A1:F{current_row+3}')
ExcelStyleUtils.set_style(ws, f'A1:F2', header_font)
# 调整列宽
ExcelStyleUtils.auto_adjust_column_width(ws)
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
def main(gdb_path, soil_prop_name, reclassed_features_path, dltb_features, soil_prop_tif, output_path, target_area_dict,xzqmc, prop_config):
try:
# --- 1. 用户配置 ---
# 输出配置
temp_files = []
output_excel_path = os.path.join(output_path, f"{soil_prop_name}土壤分级分布.xlsx") # 生成的Excel报告文件路径
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
print("开始处理数据...")
is_by_xzq = False if xzqmc not in ["北海市","来宾市","楚雄自治州"] else True
# out_table_mean = r"in_memory/out_table_mean"
temp_out_feature_class = r"in_memory/temp_out_feature_class"
temp_out_tables_area = r"in_memory/temp_out_tables_area"
# temp_files.append(out_table_mean)
temp_files.append(temp_out_tables_area)
# if not arcpy.Exists(out_table_mean):
# # 2.用arcpy.sa.ZonalStatisticsAsTable 以表格进行分区统计
# arcpy.sa.ZonalStatisticsAsTable(
# dltb_features, "YJDL_EJDL", soil_prop_tif, out_table_mean, "DATA", "MEAN"
# )
# arcpy.management.CalculateField(out_table_mean, "YJDL", "!YJDL_EJDL!.split('_')[0]", "PYTHON3")
# arcpy.management.CalculateField(out_table_mean, "EJDL", "!YJDL_EJDL!.split('_')[1]", "PYTHON3")
# 求地类图斑和重分类栅格面的交集
arcpy.analysis.Intersect(
in_features=[dltb_features,reclassed_features_path],
out_feature_class=temp_out_feature_class,
join_attributes="ALL",
output_type="INPUT"
)
# 行政区划和相交结果进行交集制表
arcpy.analysis.TabulateIntersection(
in_zone_features="行政区划", # 乡镇边界
zone_fields="XZQMC",
in_class_features=temp_out_feature_class,
out_table=temp_out_tables_area,
class_fields="gridcode;YJDL_EJDL",
out_units="SQUARE_METERS"
)
clipped_table_df = arcgis_utils.read_arcgis_table(temp_out_tables_area)
# 生成表1 土壤属性分级分布 的统计Excel报告
final_dataframe,stat = process_data_for_table1(gdb_path, soil_prop_name, clipped_table_df, target_area_dict,xzqmc,is_by_xzq, prop_config)
write_to_excel_table1(final_dataframe, output_excel_path, prop_config, soil_prop_tif, stat)
# return df_with_factors
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
temp_files_processor.clean_up_temp_files(temp_files)
import gc
gc.collect()
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# main()

View File

@@ -0,0 +1,315 @@
# -*- coding: utf-8 -*-
import os
import re
import arcpy
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font, Border, Side, Alignment
from openpyxl.utils import get_column_letter
from tools.config.pandas_field_cal_func import calculate_ejdl, calculate_yjdl
from tools.core.utils.os_utils import temp_files_processor
yjdl_order = ["耕地", "园地", "林地", "草地", "其他"]
ejdl_order = ["水田", "旱地", "水浇地", "果园", "茶园", "橡胶园", "其他园地"]
# 土壤12级地质类别
trzd_order = ['砂土及壤质砂土', '砂质壤土','壤土','粉(砂)质壤土','砂质黏壤土','黏壤土','粉(砂)质黏壤土','砂质黏土','壤质黏土','粉(砂)质黏土','黏土','重黏土']
# --- 2. 辅助函数 ---
# 判断单元格类型
def get_merge_type(merged_range):
"""
判断合并类型
返回: 'row'(行合并), 'column'(列合并), 'both'(行列合并)或 None不是合并单元格
"""
if not merged_range:
return None
min_row, max_row = merged_range.min_row, merged_range.max_row
min_col, max_col = merged_range.min_col, merged_range.max_col
if max_row > min_row and max_col > min_col:
return 'both' # 同时跨行和跨列
elif max_row > min_row:
return 'row' # 行合并(垂直合并)
elif max_col > min_col:
return 'column' # 列合并(水平合并)
else:
return None # 实际上不是合并单元格
# --- 3. 数据处理与分析 均值---
def process_data_for_table2(gdb_path, soil_prop_feature_name, df_dltb, target_areas_df):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("开始处理数据...")
def clean_df(df, columns):
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# ==a. 处理样点数据,计算样点数 ---
print("--> 步骤1: 计算样点均值...")
field_name = soil_prop_feature_name
sample_table_path = os.path.join(gdb_path, soil_prop_feature_name)
sample_fields = ['TDLYLX', field_name]
df_samples = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(sample_table_path, sample_fields, skip_nulls=False))
df_samples = clean_df(df_samples, [field_name])
df_samples["YJDL"] = df_samples['TDLYLX'].apply(calculate_yjdl)
df_samples["EJDL"] = df_samples['TDLYLX'].apply(calculate_ejdl)
df_samples["GRIDCODE"] = df_samples[field_name].astype(int)
# 按 YJDL, EJDL 分组,计算 属性 的均值
df_sample_means = df_samples.groupby(['YJDL', 'EJDL', 'GRIDCODE']).size().reset_index(name="样点数")
total_sample_count = df_sample_means['样点数'].sum()
df_sample_means['样点数占比'] = df_sample_means['样点数'] / total_sample_count
# ==b. 处理制图数据,获各等级制图面积
df_dltb["YJDL"] = df_dltb['YJDL_EJDL'].apply(lambda x: x.split('_')[0])
df_dltb["EJDL"] = df_dltb["YJDL_EJDL"].apply(lambda x: x.split('_')[1])
df_dltb.columns = df_dltb.columns.str.upper()
df_dltb = clean_df(df_dltb, ['YJDL', 'EJDL'])
df_map_data = df_dltb.groupby(["YJDL","EJDL", "GRIDCODE"]).agg({"AREA": "sum"}).reset_index()
df_map_data['制图面积_原始'] = df_map_data['AREA'] * 0.0015 # 单位:亩
# df_map_data['面积占比'] = df_map_data['制图面积'] / df_map_data['制图面积'].sum()
# 第二步:整理目标面积表(确保字段名统一)
target_areas_df = target_areas_df.copy()
target_areas_df.columns = target_areas_df.columns.str.strip() # 去除字段名空格
# 重置索引确保EJDL是列而不是索引
if 'EJDL' not in target_areas_df.columns:
target_areas_df = target_areas_df.reset_index()
target_areas_df.rename(columns={'index': 'EJDL'}, inplace=True)
# 确保面积字段为数值型
target_areas_df['面积'] = pd.to_numeric(target_areas_df['面积'], errors='coerce').fillna(0)
# 第三步:按二级地类分组计算平差系数
# 先计算每个二级地类的原始合计面积
ejdl_original_sum = df_map_data.groupby('EJDL')['制图面积_原始'].sum().reset_index()
ejdl_original_sum.rename(columns={'制图面积_原始': '原始合计面积'}, inplace=True)
# 合并目标面积
ejdl_adj = pd.merge(ejdl_original_sum, target_areas_df, on='EJDL', how='left')
ejdl_adj.rename(columns={'面积': '目标合计面积'}, inplace=True)
# 填充无目标面积的二级地类(目标面积=原始面积,平差系数=1
ejdl_adj['目标合计面积'] = ejdl_adj['目标合计面积'].fillna(ejdl_adj['原始合计面积'])
# 计算平差系数(目标面积 / 原始面积避免除以0
ejdl_adj['平差系数'] = ejdl_adj['目标合计面积'] / ejdl_adj['原始合计面积'].replace(0, 1)
ejdl_adj['平差系数'] = ejdl_adj['平差系数'].fillna(1) # 极端情况填充1
# 第四步:应用平差系数到每个质地级别的制图面积
df_map_data = pd.merge(df_map_data, ejdl_adj[['EJDL', '平差系数']], on='EJDL', how='left')
df_map_data['平差系数'] = df_map_data['平差系数'].fillna(1) # 未匹配到的二级地类系数=1
# 计算平差后的制图面积
df_map_data['制图面积'] = df_map_data['制图面积_原始'] * df_map_data['平差系数']
# 重新计算面积占比(基于平差后的面积)
total_adjusted_area = df_map_data['制图面积'].sum()
df_map_data['面积占比'] = df_map_data['制图面积'] / total_adjusted_area
df_map_data = clean_df(df_map_data, ['YJDL', 'EJDL'])
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['YJDL', 'EJDL', 'GRIDCODE']],
df_map_data[['YJDL', 'EJDL', 'GRIDCODE']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['YJDL', 'EJDL', 'GRIDCODE'], how='left')
df_final = pd.merge(df_final, df_map_data, on=['YJDL', 'EJDL', 'GRIDCODE'], how='left')
# (可选) 按“一级地类”和“二级地类”排序
in_ejdl_order = ejdl_order + [x for x in df_final['EJDL'].unique() if x not in ejdl_order]
df_final["YJDL"] = pd.Categorical(df_final['YJDL'], categories=yjdl_order, ordered=True)
df_final["EJDL"] = pd.Categorical(df_final['EJDL'], categories=in_ejdl_order, ordered=True)
df_final["GRIDCODE"] = pd.Categorical(df_final['GRIDCODE'], categories=sorted(df_final['GRIDCODE'].unique()), ordered=True)
df_final.sort_values(['YJDL', 'EJDL', 'GRIDCODE'], inplace=True)
print("数据处理流程完成!")
return df_final
# 写入EXCEL 表2
def write_to_excel_table2(df, output_path, prop_config):
"""
将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同土地利用类型属性变化统计"
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
cell_font = Font(name='等线', size=11)
center_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
def apply_style(cell_range, font, alignment=None, border=None):
for row in ws[cell_range]:
for cell in row:
cell.font = font
if alignment: cell.alignment = alignment
if border: cell.border = border
# --- b. 绘制表头 ---
ws.merge_cells('A1:B1'); ws['A1'] = '土地利用类型'
ws.merge_cells('C1:E1'); ws['C1'] = '样点统计'
ws.merge_cells('F1:G1'); ws['F1'] = '制图统计'
ws['A2'] = '一级'
ws['B2'] = '二级'
ws['C2'] = '质地类型'
ws['D2'] = '数量/个'
ws['E2'] = '占比%'
ws['F2'] = '面积/亩'
ws['G2'] = '占比%'
level_dict = prop_config['标准等级']
# 创建两个列表来分别存储上段和下段范围
upper_ranges = {value: key for key, value in level_dict.items()}
# --- c. 填充数据 ---
current_row = 3
df_to_write = df.copy() # 使用 .copy() 避免 SettingWithCopyWarning
for yl, group_yl_df in df_to_write.groupby('YJDL', sort=False, observed=False):
if group_yl_df.empty:
continue
print(f"正在写入一级地类: {yl}...")
yl_start_row = current_row
# 按二级地类分组
for ej, group_ej_df in group_yl_df.groupby('EJDL', sort=False, observed=False):
if group_ej_df.empty:
continue
print(f"正在写入二级地类: {ej}...")
ej_start_row = current_row
# 按“土壤质地分级”分组
for idx, row_data in group_ej_df.iterrows():
# 填充土壤质地分类
ws.cell(row=current_row, column=3).value = upper_ranges.get(str(row_data['GRIDCODE']), '-')
# 填充样点数据
ws.cell(row=current_row, column=4).value = row_data['样点数'] if not np.isnan(row_data['样点数']) else '-'
ws.cell(row=current_row, column=5).value = round(row_data['样点数占比']*100, 2) if not np.isnan(row_data['样点数占比']) else '-'
# 填充制图数据
ws.cell(row=current_row, column=6).value = round(row_data['制图面积'], 0) if not np.isnan(row_data['制图面积']) else '-'
ws.cell(row=current_row, column=7).value = round(row_data['面积占比']*100, 2) if not np.isnan(row_data['面积占比']) else '-'
current_row += 1
# 合并二级地类单元格
if ej_start_row <= current_row:
ws.merge_cells(start_row=ej_start_row, start_column=2, end_row=current_row-1, end_column=2)
ws.cell(row=ej_start_row, column=2).value = ej
# 一级地类合计行
ws.merge_cells(start_row=current_row, start_column=2, end_row=current_row, end_column=3)
ws.cell(row=current_row, column=2).value = '合计'
ws.cell(row=current_row, column=4).value = round(group_yl_df['样点数'].sum(), 0) if not np.isnan(group_yl_df['样点数'].sum()) else '-'
ws.cell(row=current_row, column=5).value = round(group_yl_df['样点数占比'].sum()*100, 2) if not np.isnan(group_yl_df['样点数占比'].sum()) else '-'
ws.cell(row=current_row, column=6).value = round(group_yl_df['制图面积'].sum(), 0) if not np.isnan(group_yl_df['制图面积'].sum()) else '-'
ws.cell(row=current_row, column=7).value = round(group_yl_df['面积占比'].sum()*100, 2) if not np.isnan(group_yl_df['面积占比'].sum()) else '-'
# 合并一级地类单元格(修正合并范围)
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
current_row += 1
# --- 5. 全区汇总行 ---
ws.cell(row=current_row, column=1).value = '全区汇总'
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=3)
ws.cell(row=current_row, column=4).value = round(df_to_write['样点数'].sum(), 0) if not np.isnan(df_to_write['样点数'].sum()) else '-'
ws.cell(row=current_row, column=5).value = round(df_to_write['样点数占比'].sum()*100, 2) if not np.isnan(df_to_write['样点数占比'].sum()) else '-'
ws.cell(row=current_row, column=6).value = round(df_to_write['制图面积'].sum(), 0) if not np.isnan(df_to_write['制图面积'].sum()) else '-'
ws.cell(row=current_row, column=7).value = round(df_to_write['面积占比'].sum()*100, 2) if not np.isnan(df_to_write['面积占比'].sum()) else '-'
# --- d. 应用样式和调整列宽 ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
apply_style(f'A1:{max_col_letter}{current_row}', cell_font, center_align, thin_border)
apply_style(f'A1:{max_col_letter}2', header_font)
print("正在自动调整列宽...")
dims = {}
for row in ws.rows:
for cell in row:
if cell.value:
merged_range = next((range for range in ws.merged_cells.ranges if cell.coordinate in range), None)
if get_merge_type(merged_range) == 'column':
continue
cell_len = 0.7 * len(re.findall('([\u4e00-\u9fa5])', str(cell.value))) + len(str(cell.value))
dims[cell.column] = max(dims.get(cell.column, 0), cell_len)
# 设置列宽
for col, value in dims.items():
ws.column_dimensions[get_column_letter(int(col))].width = value + 5
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
# def main(gdb_path, soil_prop_name, dltb_features, reclassed_feature, output_path,target_areas_df, prop_config):
# print(target_areas_df)
# df = pd.read_csv(r"D:\ProgramData\ArcGis_Py\测试数据.csv")
# output_path = r"E:\@三普属性图出图\测试\AAA.xlsx"
# write_to_excel_table2(df,output_path,prop_config)
def main(gdb_path, soil_prop_name, dltb_features, reclassed_feature, output_path,target_areas_df, prop_config):
try:
# --- 1. 用户配置 ---
# 输出配置
temp_files = []
output_excel_path = os.path.join(output_path, f"{soil_prop_name}土地利用类型土壤.xlsx") # 生成的Excel报告文件路径
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
print("开始处理数据...")
out_table_mean = r"in_memory/out_table_mean"
temp_files.append(out_table_mean)
if not arcpy.Exists(out_table_mean):
# 2.使用交集制表计算每个TRZD的面积
arcpy.analysis.TabulateIntersection(dltb_features, "YJDL_EJDL", reclassed_feature, out_table_mean, "gridcode", out_units="SQUARE_METERS")
dltb_df = pd.DataFrame(arcpy.da.TableToNumPyArray(out_table_mean, ["YJDL_EJDL", "gridcode", "AREA"]))
# 生成表1 土壤属性分级分布 的统计Excel报告
final_dataframe = process_data_for_table2(gdb_path, soil_prop_name, dltb_df, target_areas_df)
write_to_excel_table2(final_dataframe, output_excel_path, prop_config)
# return df_with_factors
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
temp_files_processor.clean_up_temp_files(temp_files)
import gc
gc.collect()
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# df = pd.read_csv(r"D:\ProgramData\ArcGis_Py\测试数据.csv")
# output_path = r"E:\@三普属性图出图\测试\AAA.xlsx"
# write_to_excel_table2(df,output_path)

View File

@@ -0,0 +1,336 @@
# -*- coding: utf-8 -*-
import os
import re
from matplotlib.artist import get
import arcpy
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font, Border, Side, Alignment
from openpyxl.utils import get_column_letter
from tools.config.pandas_field_cal_func import calculate_ejdl, calculate_yjdl
from tools.core.utils.os_utils import temp_files_processor
yjdl_order = ["耕地", "园地", "林地", "草地", "其他"]
ejdl_order = ["水田", "旱地", "水浇地", "果园", "茶园", "橡胶园", "其他园地"]
# 土壤12级地质类别
trzd_order = ['黏壤质','黏质','壤质','砂壤质','砂质']
# --- 2. 辅助函数 ---
def get_prop_level(prop_level):
"""根据输入值判断 返回等级"""
if pd.isna(prop_level) or prop_level == 0:
return "-"
# 请根据您的实际分级标准调整这里的阈值
if int(prop_level) == 5 or prop_level == "砂质":
return "砂质"
elif int(prop_level) == 4 or prop_level == "砂壤质":
return "砂壤质"
elif int(prop_level) == 3 or prop_level == "壤质":
return "壤质"
elif int(prop_level) == 1 or prop_level == "黏壤质":
return "黏壤质"
elif int(prop_level) == 2 or prop_level == "黏质":
return "黏质"
else:
return "-"
# 判断单元格类型
def get_merge_type(merged_range):
"""
判断合并类型
返回: 'row'(行合并), 'column'(列合并), 'both'(行列合并)或 None不是合并单元格
"""
if not merged_range:
return None
min_row, max_row = merged_range.min_row, merged_range.max_row
min_col, max_col = merged_range.min_col, merged_range.max_col
if max_row > min_row and max_col > min_col:
return 'both' # 同时跨行和跨列
elif max_row > min_row:
return 'row' # 行合并(垂直合并)
elif max_col > min_col:
return 'column' # 列合并(水平合并)
else:
return None # 实际上不是合并单元格
# --- 3. 数据处理与分析 均值---
def process_data_for_table2(gdb_path, soil_prop_feature_name, df_dltb, target_areas_df):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("开始处理数据...")
def clean_df(df, columns):
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# ==a. 处理样点数据,计算样点数 ---
print("--> 步骤1: 计算样点均值...")
field_name = soil_prop_feature_name
sample_table_path = os.path.join(gdb_path, soil_prop_feature_name)
sample_fields = ['TDLYLX', field_name]
df_samples = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(sample_table_path, sample_fields, skip_nulls=False))
df_samples = clean_df(df_samples, [field_name])
df_samples["YJDL"] = df_samples['TDLYLX'].apply(calculate_yjdl)
df_samples["EJDL"] = df_samples['TDLYLX'].apply(calculate_ejdl)
df_samples["GRIDCODE"] = df_samples[field_name]
# 按 YJDL, EJDL 分组,计算 属性 的均值
df_sample_means = df_samples.groupby(['YJDL', 'EJDL', 'GRIDCODE']).size().reset_index(name="样点数")
total_sample_count = df_sample_means['样点数'].sum()
df_sample_means['样点数占比'] = df_sample_means['样点数'] / total_sample_count
# ==b. 处理制图数据,获各等级制图面积
df_dltb["YJDL"] = df_dltb['YJDL_EJDL'].apply(lambda x: x.split('_')[0])
df_dltb["EJDL"] = df_dltb["YJDL_EJDL"].apply(lambda x: x.split('_')[1])
df_dltb.columns = df_dltb.columns.str.upper()
df_dltb = clean_df(df_dltb, ['YJDL', 'EJDL'])
df_dltb['GRIDCODE'] = df_dltb['GRIDCODE'].apply(get_prop_level)
df_map_data = df_dltb.groupby(["YJDL","EJDL", "GRIDCODE"]).agg({"AREA": "sum"}).reset_index()
df_map_data['制图面积_原始'] = df_map_data['AREA'] * 0.0015 # 单位:亩
# df_map_data['面积占比'] = df_map_data['制图面积'] / df_map_data['制图面积'].sum()
# 第二步:整理目标面积表(确保字段名统一)
target_areas_df = target_areas_df.copy()
target_areas_df.columns = target_areas_df.columns.str.strip() # 去除字段名空格
# 重置索引确保EJDL是列而不是索引
if 'EJDL' not in target_areas_df.columns:
target_areas_df = target_areas_df.reset_index()
target_areas_df.rename(columns={'index': 'EJDL'}, inplace=True)
# 确保面积字段为数值型
target_areas_df['面积'] = pd.to_numeric(target_areas_df['面积'], errors='coerce').fillna(0)
# 第三步:按二级地类分组计算平差系数
# 先计算每个二级地类的原始合计面积
ejdl_original_sum = df_map_data.groupby('EJDL')['制图面积_原始'].sum().reset_index()
ejdl_original_sum.rename(columns={'制图面积_原始': '原始合计面积'}, inplace=True)
# 合并目标面积
ejdl_adj = pd.merge(ejdl_original_sum, target_areas_df, on='EJDL', how='left')
ejdl_adj.rename(columns={'面积': '目标合计面积'}, inplace=True)
# 填充无目标面积的二级地类(目标面积=原始面积,平差系数=1
ejdl_adj['目标合计面积'] = ejdl_adj['目标合计面积'].fillna(ejdl_adj['原始合计面积'])
# 计算平差系数(目标面积 / 原始面积避免除以0
ejdl_adj['平差系数'] = ejdl_adj['目标合计面积'] / ejdl_adj['原始合计面积'].replace(0, 1)
ejdl_adj['平差系数'] = ejdl_adj['平差系数'].fillna(1) # 极端情况填充1
# 第四步:应用平差系数到每个质地级别的制图面积
df_map_data = pd.merge(df_map_data, ejdl_adj[['EJDL', '平差系数']], on='EJDL', how='left')
df_map_data['平差系数'] = df_map_data['平差系数'].fillna(1) # 未匹配到的二级地类系数=1
# 计算平差后的制图面积
df_map_data['制图面积'] = df_map_data['制图面积_原始'] * df_map_data['平差系数']
# 重新计算面积占比(基于平差后的面积)
total_adjusted_area = df_map_data['制图面积'].sum()
df_map_data['面积占比'] = df_map_data['制图面积'] / total_adjusted_area
df_map_data = clean_df(df_map_data, ['YJDL', 'EJDL'])
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['YJDL', 'EJDL', 'GRIDCODE']],
df_map_data[['YJDL', 'EJDL', 'GRIDCODE']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['YJDL', 'EJDL', 'GRIDCODE'], how='left')
df_final = pd.merge(df_final, df_map_data, on=['YJDL', 'EJDL', 'GRIDCODE'], how='left')
# (可选) 按“一级地类”和“二级地类”排序
in_ejdl_order = ejdl_order + [x for x in df_final['EJDL'].unique() if x not in ejdl_order]
df_final["YJDL"] = pd.Categorical(df_final['YJDL'], categories=yjdl_order, ordered=True)
df_final["EJDL"] = pd.Categorical(df_final['EJDL'], categories=in_ejdl_order, ordered=True)
df_final["GRIDCODE"] = pd.Categorical(df_final['GRIDCODE'], categories=sorted(df_final['GRIDCODE'].unique()), ordered=True)
df_final.sort_values(['YJDL', 'EJDL', 'GRIDCODE'], inplace=True)
print("数据处理流程完成!")
return df_final
# 写入EXCEL 表2
def write_to_excel_table2(df, output_path, prop_config):
"""
将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同土地利用类型属性变化统计"
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
cell_font = Font(name='等线', size=11)
center_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
def apply_style(cell_range, font, alignment=None, border=None):
for row in ws[cell_range]:
for cell in row:
cell.font = font
if alignment: cell.alignment = alignment
if border: cell.border = border
# --- b. 绘制表头 ---
ws.merge_cells('A1:B1'); ws['A1'] = '土地利用类型'
ws.merge_cells('C1:E1'); ws['C1'] = '样点统计'
ws.merge_cells('F1:G1'); ws['F1'] = '制图统计'
ws['A2'] = '一级'
ws['B2'] = '二级'
ws['C2'] = '质地类型'
ws['D2'] = '数量/个'
ws['E2'] = '占比%'
ws['F2'] = '面积/亩'
ws['G2'] = '占比%'
level_dict = prop_config['标准等级']
# 创建两个列表来分别存储上段和下段范围
upper_ranges = {value: key for key, value in level_dict.items()}
# --- c. 填充数据 ---
current_row = 3
df_to_write = df.copy() # 使用 .copy() 避免 SettingWithCopyWarning
for yl, group_yl_df in df_to_write.groupby('YJDL', sort=False, observed=False):
if group_yl_df.empty:
continue
print(f"正在写入一级地类: {yl}...")
yl_start_row = current_row
# 按二级地类分组
for ej, group_ej_df in group_yl_df.groupby('EJDL', sort=False, observed=False):
if group_ej_df.empty:
continue
print(f"正在写入二级地类: {ej}...")
ej_start_row = current_row
# 按“土壤质地分级”分组
for idx, row_data in group_ej_df.iterrows():
# 填充土壤质地分类
ws.cell(row=current_row, column=3).value = str(row_data['GRIDCODE'])
# 填充样点数据
ws.cell(row=current_row, column=4).value = row_data['样点数'] if not np.isnan(row_data['样点数']) else '-'
ws.cell(row=current_row, column=5).value = round(row_data['样点数占比']*100, 2) if not np.isnan(row_data['样点数占比']) else '-'
# 填充制图数据
ws.cell(row=current_row, column=6).value = round(row_data['制图面积'], 0) if not np.isnan(row_data['制图面积']) else '-'
ws.cell(row=current_row, column=7).value = round(row_data['面积占比']*100, 2) if not np.isnan(row_data['面积占比']) else '-'
current_row += 1
# 合并二级地类单元格
if ej_start_row <= current_row:
ws.merge_cells(start_row=ej_start_row, start_column=2, end_row=current_row-1, end_column=2)
ws.cell(row=ej_start_row, column=2).value = ej
# 一级地类合计行
ws.merge_cells(start_row=current_row, start_column=2, end_row=current_row, end_column=3)
ws.cell(row=current_row, column=2).value = '合计'
ws.cell(row=current_row, column=4).value = round(group_yl_df['样点数'].sum(), 0) if not np.isnan(group_yl_df['样点数'].sum()) else '-'
ws.cell(row=current_row, column=5).value = round(group_yl_df['样点数占比'].sum()*100, 2) if not np.isnan(group_yl_df['样点数占比'].sum()) else '-'
ws.cell(row=current_row, column=6).value = round(group_yl_df['制图面积'].sum(), 0) if not np.isnan(group_yl_df['制图面积'].sum()) else '-'
ws.cell(row=current_row, column=7).value = round(group_yl_df['面积占比'].sum()*100, 2) if not np.isnan(group_yl_df['面积占比'].sum()) else '-'
# 合并一级地类单元格(修正合并范围)
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
current_row += 1
# --- 5. 全区汇总行 ---
ws.cell(row=current_row, column=1).value = '全区汇总'
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=3)
ws.cell(row=current_row, column=4).value = round(df_to_write['样点数'].sum(), 0) if not np.isnan(df_to_write['样点数'].sum()) else '-'
ws.cell(row=current_row, column=5).value = round(df_to_write['样点数占比'].sum()*100, 2) if not np.isnan(df_to_write['样点数占比'].sum()) else '-'
ws.cell(row=current_row, column=6).value = round(df_to_write['制图面积'].sum(), 0) if not np.isnan(df_to_write['制图面积'].sum()) else '-'
ws.cell(row=current_row, column=7).value = round(df_to_write['面积占比'].sum()*100, 2) if not np.isnan(df_to_write['面积占比'].sum()) else '-'
# --- d. 应用样式和调整列宽 ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
apply_style(f'A1:{max_col_letter}{current_row}', cell_font, center_align, thin_border)
apply_style(f'A1:{max_col_letter}2', header_font)
print("正在自动调整列宽...")
dims = {}
for row in ws.rows:
for cell in row:
if cell.value:
merged_range = next((range for range in ws.merged_cells.ranges if cell.coordinate in range), None)
if get_merge_type(merged_range) == 'column':
continue
cell_len = 0.7 * len(re.findall('([\u4e00-\u9fa5])', str(cell.value))) + len(str(cell.value))
dims[cell.column] = max(dims.get(cell.column, 0), cell_len)
# 设置列宽
for col, value in dims.items():
ws.column_dimensions[get_column_letter(int(col))].width = value + 5
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
# def main(gdb_path, soil_prop_name, dltb_features, reclassed_feature, output_path,target_areas_df, prop_config):
# print(target_areas_df)
# df = pd.read_csv(r"D:\ProgramData\ArcGis_Py\测试数据.csv")
# output_path = r"E:\@三普属性图出图\测试\AAA.xlsx"
# write_to_excel_table2(df,output_path,prop_config)
def main(gdb_path, soil_prop_name, dltb_features, reclassed_feature, output_path,target_areas_df, prop_config):
try:
# --- 1. 用户配置 ---
# 输出配置
temp_files = []
output_excel_path = os.path.join(output_path, f"{soil_prop_name}土地利用类型土壤.xlsx") # 生成的Excel报告文件路径
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
print("开始处理数据...")
out_table_mean = r"in_memory/out_table_mean"
temp_files.append(out_table_mean)
if not arcpy.Exists(out_table_mean):
# 2.使用交集制表计算每个TRZD的面积
arcpy.analysis.TabulateIntersection(dltb_features, "YJDL_EJDL", reclassed_feature, out_table_mean, "gridcode", out_units="SQUARE_METERS")
dltb_df = pd.DataFrame(arcpy.da.TableToNumPyArray(out_table_mean, ["YJDL_EJDL", "gridcode", "AREA"]))
# 生成表1 土壤属性分级分布 的统计Excel报告
final_dataframe = process_data_for_table2(gdb_path, soil_prop_name, dltb_df, target_areas_df)
write_to_excel_table2(final_dataframe, output_excel_path, prop_config)
# return df_with_factors
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
temp_files_processor.clean_up_temp_files(temp_files)
import gc
gc.collect()
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# df = pd.read_csv(r"D:\ProgramData\ArcGis_Py\测试数据.csv")
# output_path = r"E:\@三普属性图出图\测试\AAA.xlsx"
# write_to_excel_table2(df,output_path)

View File

@@ -0,0 +1,328 @@
# -*- coding: utf-8 -*-
import os
import re
import arcpy
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
from tools.config.pandas_field_cal_func import calculate_ejdl, calculate_yjdl
from tools.core.utils.os_utils import temp_files_processor
from tools.core.utils.excel_utils import ExcelStyleUtils
yjdl_order = ["耕地", "园地", "林地", "草地", "其他"]
ejdl_order = ["水田", "旱地", "水浇地", "果园", "茶园", "橡胶园", "其他园地"]
# --- 3. 数据处理与分析 均值---
def process_data_for_table2(gdb_path, soil_prop_feature_name, df_dltb, target_areas_df):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("开始处理数据...")
def clean_df(df, columns):
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# ==a. 处理样点数据,计算“样点均值” ---
print("--> 步骤1: 计算样点均值...")
field_name = soil_prop_feature_name
sample_table_path = os.path.join(gdb_path, soil_prop_feature_name)
sample_fields = ['TDLYLX', field_name]
df_samples = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(sample_table_path, sample_fields, skip_nulls=False))
df_samples = clean_df(df_samples, [field_name])
df_samples["YJDL"] = df_samples['TDLYLX'].apply(calculate_yjdl)
df_samples["EJDL"] = df_samples['TDLYLX'].apply(calculate_ejdl)
df_samples[field_name] = df_samples[field_name].astype(float)
# 按 YJDL, EJDL 分组,计算 属性 的均值
df_sample_means = df_samples.groupby(['YJDL', 'EJDL'])[field_name].agg(['count', 'max', 'min', 'mean']).reset_index()
# ==b. 处理制图数据,获各等级制图面积
df_dltb["YJDL"] = df_dltb['YJDL_EJDL'].apply(lambda x: x.split('_')[0])
df_dltb["EJDL"] = df_dltb["YJDL_EJDL"].apply(lambda x: x.split('_')[1])
df_dltb = clean_df(df_dltb, ['YJDL', 'EJDL'])
df_dltb.rename(columns={'MEAN': '制图均值', 'COUNT': '制图样点数'}, inplace=True)
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['YJDL', 'EJDL']],
df_dltb[['YJDL', 'EJDL']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['YJDL', 'EJDL'], how='left')
df_final = pd.merge(df_final, df_dltb, on=['YJDL', 'EJDL'], how='left')
df_final = pd.merge(df_final, target_areas_df, on=['EJDL'], how='left')
# (可选) 按“一级地类”和“二级地类”排序
in_ejdl_order = ejdl_order + [x for x in df_final['EJDL'].unique() if x not in ejdl_order]
df_final["YJDL"] = pd.Categorical(df_final['YJDL'], categories=yjdl_order, ordered=True)
df_final["EJDL"] = pd.Categorical(df_final['EJDL'], categories=in_ejdl_order, ordered=True)
df_final.sort_values(['YJDL', 'EJDL'], inplace=True)
print("数据处理流程完成!")
return df_final
# 写入EXCEL 表2
def write_to_excel_table2(df, output_path, prop_config:dict, soil_prop_name: str = ''):
"""
将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同土地利用类型属性变化统计"
# 获取属性单位
special_prop = ['耕作层厚度','阳离子','有机质','pH','有效磷','速效钾','交换性钙','交换性镁','有效硫','有效铁','有效锰','有效硅','全钾']
fsn_props = ['砂粒含量','粉粒含量','黏粒含量','有效土层厚度']
prop_name_str = prop_config.get('项目分级','')
if prop_name_str:
split_name = prop_name_str.split('\n')[0].strip()
if split_name in special_prop:
prop_name = '1f'
elif split_name in fsn_props:
prop_name = '0f'
else:
prop_name = '2f'
else:
prop_name = '1f'
prop_unit_str = prop_config.get('分级标准', '')
if prop_unit_str:
prop_unit = prop_unit_str.split('\n')[1].strip()
else:
prop_unit = ''
# --- b. 绘制表头 ---
ws.merge_cells('A1:B1'); ws['A1'] = '土地利用类型'
ws.merge_cells('C1:E1'); ws['C1'] = '样点统计'
ws.merge_cells('F1:G1'); ws['F1'] = '制图统计'
ws['A2'] = '一级'
ws['B2'] = '二级'
ws['C2'] = '均值/' + prop_unit
ws['D2'] = '范围/' + prop_unit
ws['E2'] = '数量/个'
ws['F2'] = '均值/' + prop_unit
ws['G2'] = '面积/亩'
# --- c. 填充数据 ---
current_row = 3
df_to_write = df.copy() # 使用 .copy() 避免 SettingWithCopyWarning
filtered_props = ['ECA', 'EMG', 'ACU', 'AZN', 'AFE', 'AMN', 'AMO', 'AB', 'AS1', 'TSE']
for yl, group_yl_df in df_to_write.groupby('YJDL', sort=False, observed=False):
print(f"正在写入一级地类: {yl}...")
yl_start_row = current_row
# 遍历该一级地类下的所有“二级地类”
for _, row_data in group_yl_df.iterrows():
ws.cell(row=current_row, column=2).value = row_data['EJDL']
# 填充样点数据
sample_mean = row_data.get('mean')
if pd.notna(sample_mean):
ws.cell(row=current_row, column=3).value = f"{sample_mean:.{prop_name}}"
ws.cell(row=current_row, column=4).value = f"{row_data.get('min', '-'):.{prop_name}}{row_data.get('max', '-'):.{prop_name}}"
ws.cell(row=current_row, column=5).value = row_data.get('count', '-')
else:
ws.cell(row=current_row, column=3).value = "-"
ws.cell(row=current_row, column=4).value = "-"
ws.cell(row=current_row, column=5).value = "-"
# 填充制图数据
map_mean = row_data.get('制图均值')
if pd.notna(map_mean):
ws.cell(row=current_row, column=6).value = f"{map_mean:.{prop_name}}"
ws.cell(row=current_row, column=7).value = f"{row_data.get('面积', '-'):.0f}"
else:
ws.cell(row=current_row, column=6).value = "-"
ws.cell(row=current_row, column=7).value = "-"
current_row += 1
# 计算并写入“合计”行
if ws.cell(row=current_row-1, column=2).value in ["林地", "草地", "其他"]:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=yl_start_row, end_column=2)
ws.cell(row=yl_start_row, column=1).value = yl
if soil_prop_name in filtered_props:
ws.cell(row=yl_start_row, column=6).value = "-"
ws.cell(row=yl_start_row, column=7).value = "-"
continue
ws.cell(row=current_row, column=2).value = '合计'
# 计算合计行的均值 (均值的均值)
total_count = group_yl_df['count'].sum()
weighted_sum = group_yl_df['mean']*group_yl_df['count']
if not weighted_sum.empty and total_count != 0:
total_sample_mean = weighted_sum.sum()/group_yl_df['count'].sum()
else:
total_sample_mean = None
min_min, max_max = group_yl_df['min'].min(), group_yl_df['max'].max()
if pd.notna(total_sample_mean):
ws.cell(row=current_row, column=3).value = f"{total_sample_mean:.{prop_name}}"
ws.cell(row=current_row, column=4).value = f"{min_min:.{prop_name}}{max_max:.{prop_name}}"
ws.cell(row=current_row, column=5).value = f"{total_count:.0f}"
else:
ws.cell(row=current_row, column=3).value = "-"
ws.cell(row=current_row, column=4).value = "-"
ws.cell(row=current_row, column=5).value = "-"
# b. **【核心修正】: 计算合计行的“制图均值”(加权平均)**
# 准备加权平均的分子和分母
weighted_sum = 0
total_count = 0
# 遍历当前一级地类分组中的每一行
for _, row in group_yl_df.iterrows():
mean_val = row.get('制图均值')
count_val = row.get('制图样点数')
# 只有当均值和样点数都存在且有效时,才参与计算
if pd.notna(mean_val) and pd.notna(count_val) and count_val > 0:
weighted_sum += mean_val * count_val # Σ (mean * count)
total_count += count_val # Σ (count)
# 计算加权平均值
weighted_avg = (weighted_sum / total_count) if total_count > 0 else 0
total_area = group_yl_df['面积'].sum()
if weighted_avg > 0:
ws.cell(row=current_row, column=6).value = f"{weighted_avg:.{prop_name}}"
ws.cell(row=current_row, column=7).value = f"{total_area:.0f}"
else:
ws.cell(row=current_row, column=6).value = "-"
ws.cell(row=current_row, column=7).value = "-"
# 合并“一级地类”单元格
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
current_row += 1
# 计算全区的均值、范围、数量
if soil_prop_name in filtered_props:
# 只基于耕地和园地计算全区统计
df_for_total = df_to_write[df_to_write['YJDL'].isin(['耕地', '园地'])].copy()
print(f"全区统计过滤仅基于耕地和园地YJDL in ['耕地', '园地']")
else:
df_for_total = df_to_write.copy()
# 使用 df_for_total 进行后续计算
total_weighted_sum = df_for_total['mean'] * df_for_total['count']
total_counts = df_for_total['count'].sum()
if total_counts > 0:
total_mean = total_weighted_sum.sum() / total_counts
else:
total_mean = None
if not df_for_total.empty:
total_range = f"{df_for_total['min'].min():.{prop_name}}{df_for_total['max'].max():.{prop_name}}"
total_zhitu_weighted_sum = df_for_total['制图均值']*df_for_total['面积']
total_areas = df_for_total['面积'].sum()
if total_areas > 0:
total_zhitu_mean = total_zhitu_weighted_sum.sum() / total_areas
else:
total_zhitu_mean = None
else:
total_range = "-"
total_zhitu_mean = None
total_areas = 0
# 填充全区统计行
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=2)
ws.cell(row=current_row, column=1).value = '全区'
if pd.notna(total_mean):
ws.cell(row=current_row, column=3).value = f"{total_mean:.{prop_name}}"
else:
ws.cell(row=current_row, column=3).value = "-"
ws.cell(row=current_row, column=4).value = total_range
ws.cell(row=current_row, column=5).value = f"{total_counts:.0f}" if total_counts > 0 else "-"
if pd.notna(total_zhitu_mean):
ws.cell(row=current_row, column=6).value = f"{total_zhitu_mean:.{prop_name}}"
else:
ws.cell(row=current_row, column=6).value = "-"
ws.cell(row=current_row, column=7).value = f"{total_areas:.0f}" if total_areas > 0 else "-"
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
# --- d. 应用样式和调整列宽 ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
ExcelStyleUtils.set_style(ws,f'A1:{max_col_letter}{current_row}')
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}2', header_font)
print("正在自动调整列宽...")
# 设置列宽
ExcelStyleUtils.auto_adjust_column_width(ws)
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
def main(gdb_path, soil_prop_name, dltb_features, soil_prop_tif, output_path,target_areas_df, prop_config):
try:
# --- 1. 用户配置 ---
# 输出配置
temp_files = []
output_excel_path = os.path.join(output_path, f"{soil_prop_name}土地利用类型土壤.xlsx") # 生成的Excel报告文件路径
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
print("开始处理数据...")
out_table_mean = r"in_memory/out_table_mean"
temp_files.append(out_table_mean)
if not arcpy.Exists(out_table_mean):
# 2.用arcpy.sa.ZonalStatisticsAsTable 以表格进行分区统计
arcpy.sa.ZonalStatisticsAsTable(dltb_features, "YJDL_EJDL", soil_prop_tif, out_table_mean, "DATA", "MEAN")
dltb_df = pd.DataFrame(arcpy.da.TableToNumPyArray(out_table_mean, ["YJDL_EJDL", "MEAN", "COUNT"]))
# 生成表1 土壤属性分级分布 的统计Excel报告
final_dataframe = process_data_for_table2(gdb_path, soil_prop_name, dltb_df, target_areas_df)
# final_dataframe = process_data_for_table5_2(gdb_path, out_table_area, sample_table_name, df_with_factors)
write_to_excel_table2(final_dataframe, output_excel_path, prop_config, soil_prop_name)
# return df_with_factors
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
temp_files_processor.clean_up_temp_files(temp_files)
import gc
gc.collect()
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# main()

View File

@@ -0,0 +1,446 @@
# -*- coding: utf-8 -*-
import os
import re
import arcpy
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font, Border, Side, Alignment
from openpyxl.utils import get_column_letter
from tools.config.pandas_field_cal_func import calculate_muyan, calculate_muzhi
from tools.config.custom_sort import yl_order, ts_order
from tools.core.utils.os_utils import temp_files_processor
# --- 2. 辅助函数 ---
# 判断单元格类型
def get_merge_type(merged_range):
"""
判断合并类型
返回: 'row'(行合并), 'column'(列合并), 'both'(行列合并)或 None不是合并单元格
"""
if not merged_range:
return None
min_row, max_row = merged_range.min_row, merged_range.max_row
min_col, max_col = merged_range.min_col, merged_range.max_col
if max_row > min_row and max_col > min_col:
return 'both' # 同时跨行和跨列
elif max_row > min_row:
return 'row' # 行合并(垂直合并)
elif max_col > min_col:
return 'column' # 列合并(水平合并)
else:
return None # 实际上不是合并单元格
# --- 3. 数据处理与分析 均值---
def process_data_for_table3(soil_prop_name, df_trlx_sample, df_trlx_zhitu, df_trlx, target_areas_df):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("开始处理数据...")
def clean_df(df, columns) -> pd.DataFrame:
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# ==a. 处理样点数据,计算“样点均值” ---
print("--> 步骤1: 计算样点均值...")
field_name = soil_prop_name
sample_fields = ['YL', 'TS', field_name]
df_samples = clean_df(df_trlx_sample, sample_fields)
df_samples["GRIDCODE"] = df_samples[field_name].astype(int)
# 通过土属计算母岩母质
df_samples['母岩'] = df_samples['TS'].apply(calculate_muyan)
df_samples['母质'] = df_samples['母岩'].apply(calculate_muzhi)
# 按 YJDL, EJDL 分组
df_sample_means = df_samples.groupby(['YL', 'TS', 'GRIDCODE']).size().reset_index(name="样点数")
total_sample_count = df_sample_means['样点数'].sum()
df_sample_means['样点数占比'] = df_sample_means['样点数'] / total_sample_count
# df_sample_mymz = df_samples.groupby(['母质', '母岩', 'TZ'])[field_name].agg(['count', 'mean', 'median']).reset_index()
# print(df_sample_mymz)
# ==b. 处理制图数据,获各等级制图面积
df_trlx_zhitu["YL"] = df_trlx_zhitu['YL_TS'].apply(lambda x: x.split('_')[0])
df_trlx_zhitu["TS"] = df_trlx_zhitu["YL_TS"].apply(lambda x: x.split('_')[1])
df_trlx_zhitu.columns = df_trlx_zhitu.columns.str.upper()
df_trlx_zhitu = clean_df(df_trlx_zhitu, ['YL', 'TS'])
df_map_data = df_trlx_zhitu.groupby(["YL","TS", "GRIDCODE"]).agg({"AREA": "sum"}).reset_index()
df_map_data['制图面积_原始'] = df_map_data['AREA'] * 0.0015 # 单位:亩
# ==c. 处理制图数据,获各等级制图面积
df_trlx = clean_df(df_trlx, ['YL', 'TS'])
df_trlx["面积"] = df_trlx["Shape@Area"] * 0.0015
# 拿到目标df总面积计算比例进行平差
target_areas = target_areas_df['面积'].sum()
original_area = df_trlx['面积'].sum()
adjusted_area_yz = target_areas / original_area
df_trlx["面积"] = df_trlx["面积"] * adjusted_area_yz
df_trlx_area = df_trlx.groupby(['YL', 'TS'])['面积'].sum().reset_index()
df_trlx_area['面积'] = pd.to_numeric(df_trlx_area['面积'], errors='coerce').fillna(0)
# ==========================
# 第三步:按二级地类分组计算平差系数
# 先计算每个二级地类的原始合计面积
ts_original_sum = df_map_data.groupby('TS')['制图面积_原始'].sum().reset_index()
ts_original_sum.rename(columns={'制图面积_原始': '原始合计面积'}, inplace=True)
# 合并目标面积
ts_adj = pd.merge(ts_original_sum, df_trlx_area, on='TS', how='left')
ts_adj.rename(columns={'面积': '目标合计面积'}, inplace=True)
# 填充无目标面积的二级地类(目标面积=原始面积,平差系数=1
ts_adj['目标合计面积'] = ts_adj['目标合计面积'].fillna(ts_adj['原始合计面积'])
# 计算平差系数(目标面积 / 原始面积避免除以0
ts_adj['平差系数'] = ts_adj['目标合计面积'] / ts_adj['原始合计面积'].replace(0, 1)
ts_adj['平差系数'] = ts_adj['平差系数'].fillna(1) # 极端情况填充1
# 第四步:应用平差系数到每个质地级别的制图面积
df_map_data = pd.merge(df_map_data, ts_adj[['TS', '平差系数']], on='TS', how='left')
df_map_data['平差系数'] = df_map_data['平差系数'].fillna(1) # 未匹配到的二级地类系数=1
# 计算平差后的制图面积
df_map_data['制图面积'] = df_map_data['制图面积_原始'] * df_map_data['平差系数']
# 重新计算面积占比(基于平差后的面积)
total_adjusted_area = df_map_data['制图面积'].sum()
df_map_data['面积占比'] = df_map_data['制图面积'] / total_adjusted_area
df_map_data = clean_df(df_map_data, ['YL', 'TS'])
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['YL', 'TS', 'GRIDCODE']],
df_map_data[['YL', 'TS', 'GRIDCODE']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['YL', 'TS', 'GRIDCODE'], how='left')
df_final = pd.merge(df_final, df_map_data, on=['YL', 'TS', 'GRIDCODE'], how='left')
# (可选) 按“亚类”和“土属”排序
in_yl_order = yl_order + [x for x in df_final['YL'].unique() if x not in yl_order]
in_ts_order = ts_order + [x for x in df_final['TS'].unique() if x not in ts_order]
df_final["YL"] = pd.Categorical(df_final['YL'], categories=in_yl_order, ordered=True)
df_final["TS"] = pd.Categorical(df_final['TS'], categories=in_ts_order, ordered=True)
df_final["GRIDCODE"] = pd.Categorical(df_final['GRIDCODE'], categories=sorted(df_final['GRIDCODE'].unique()), ordered=True)
df_final.sort_values(['YL', 'TS', 'GRIDCODE'], inplace=True)
print("数据处理流程完成!")
return df_final
# return df_final, df_sample_mymz
# 写入EXCEL 表2
def write_to_excel_table3(df, output_path, prop_config:dict):
"""
将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同土壤类型属性变化统计"
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
cell_font = Font(name='等线', size=11)
center_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
def apply_style(cell_range, font, alignment=None, border=None):
for row in ws[cell_range]:
for cell in row:
cell.font = font
if alignment: cell.alignment = alignment
if border: cell.border = border
# --- b. 绘制表头 ---
ws.merge_cells('A1:B1'); ws['A1'] = '土壤类型'
ws.merge_cells('C1:E1'); ws['C1'] = '样点统计'
ws.merge_cells('F1:G1'); ws['F1'] = '制图统计'
ws['A2'] = '亚类'
ws['B2'] = '土属'
ws['C2'] = '质地类型'
ws['D2'] = '数量/个'
ws['E2'] = '占比%'
ws['F2'] = '面积/亩'
ws['G2'] = '占比%'
level_dict = prop_config['标准等级']
# 创建两个列表来分别存储上段和下段范围
upper_ranges = {value: key for key, value in level_dict.items()}
# --- c. 填充数据 ---
current_row = 3
df_to_write = df.copy() # 使用 .copy() 避免 SettingWithCopyWarning
for yl, group_yl_df in df_to_write.groupby('YL', sort=False, observed=True):
if group_yl_df.empty:
continue
print(f"正在写入亚类: {yl}...")
yl_start_row = current_row
# 按二级地类分组
for ts, group_ts_df in group_yl_df.groupby('TS', sort=False, observed=False):
if group_ts_df.empty:
continue
print(f"正在写入二级地类: {ts}...")
ts_start_row = current_row
# 遍历该亚类下下的所有“土属”
for _, row_data in group_ts_df.iterrows():
ws.cell(row=current_row, column=3).value = upper_ranges.get(str(row_data['GRIDCODE']), '-')
# 填充样点数据
ws.cell(row=current_row, column=4).value = row_data['样点数'] if not np.isnan(row_data['样点数']) else '-'
ws.cell(row=current_row, column=5).value = round(row_data['样点数占比']*100, 2) if not np.isnan(row_data['样点数占比']) else '-'
# 填充制图数据
ws.cell(row=current_row, column=6).value = round(row_data['制图面积'], 0) if not np.isnan(row_data['制图面积']) else '-'
ws.cell(row=current_row, column=7).value = round(row_data['面积占比']*100, 2) if not np.isnan(row_data['面积占比']) else '-'
current_row += 1
# 合并二级地类单元格
if ts_start_row <= current_row:
ws.merge_cells(start_row=ts_start_row, start_column=2, end_row=current_row-1, end_column=2)
ws.cell(row=ts_start_row, column=2).value = ts
# 合并“一级地类”单元格
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row-1, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
# 计算全区的均值、范围、数量
total_areas = df_to_write['制图面积'].sum()
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=3)
ws.cell(row=current_row, column=1).value = '全区'
ws.cell(row=current_row, column=4).value = df_to_write['样点数'].sum()
ws.cell(row=current_row, column=5).value = round(df_to_write['样点数占比'].sum()*100, 2)
ws.cell(row=current_row, column=6).value = round(total_areas, 0)
ws.cell(row=current_row, column=7).value = round(df_to_write['面积占比'].sum()*100, 2)
# --- d. 应用样式和调整列宽 ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
apply_style(f'A1:{max_col_letter}{current_row}', cell_font, center_align, thin_border)
apply_style(f'A1:{max_col_letter}2', header_font)
print("正在自动调整列宽...")
dims = {}
for row in ws.rows:
for cell in row:
if cell.value:
merged_range = next((range for range in ws.merged_cells.ranges if cell.coordinate in range), None)
if get_merge_type(merged_range) == 'column':
continue
cell_len = 0.7 * len(re.findall('([\u4e00-\u9fa5])', str(cell.value))) + len(str(cell.value))
dims[cell.column] = max(dims.get(cell.column, 0), cell_len)
# 设置列宽
for col, value in dims.items():
ws.column_dimensions[get_column_letter(int(col))].width = value + 5
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
# 母岩母质表
def write_to_excel_table4(df:pd.DataFrame, output_path, prop_config):
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "母岩母质土壤属性统计"
# 获取属性单位
special_prop = ['耕作层厚度','阳离子','有机质','pH','有效磷','速效钾','交换性钙','交换性镁','有效硫','有效铁','有效锰','有效硅','全钾','沙粒','粉粒','粘粒']
prop_name_str = prop_config.get('项目分级','')
if prop_name_str:
prop_name = prop_name_str.split('\n')[0].strip() in special_prop
else:
prop_name = False
prop_unit_str = prop_config.get('分级标准', '')
if prop_unit_str:
prop_unit = prop_unit_str.split('\n')[1].strip()
else:
prop_unit = ''
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
cell_font = Font(name='等线', size=11)
center_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
def apply_style(cell_range, font, alignment=None, border=None):
for row in ws[cell_range]:
for cell in row:
cell.font = font
if alignment: cell.alignment = alignment
if border: cell.border = border
# 写入表头
headers = ['母岩母质','', '土种类型', '样点统计', '']
ws.append(headers)
ws.append(['', '', '', f'均值/{prop_unit}', '数量/个'])
# 合并表头单元格
ws.merge_cells('A1:B2') # 母岩母质
ws.merge_cells('C1:C2') # 土种类型
ws.merge_cells('D1:E1') # 样点统计
current_row = 3
# 按母质和母岩进行分组
grouped = df.groupby(['母质', '母岩']).agg({
'TZ': lambda x: ''.join(x), # 将土种名称用逗号连接
'mean': 'mean', # 计算均值
'count': 'sum' # 计算总数
}).reset_index()
parent_materials = grouped['母质'].unique()
for parent_material in parent_materials:
parent_material_row = current_row
if parent_material == '未知':
continue
material_group = grouped[grouped['母质'] == parent_material]
# 写入母岩母质分组(只在第一行显示)
first_row_in_group = True
for _, row_data in material_group.iterrows():
if first_row_in_group:
# 第一行显示母岩母质名称
ws.cell(row=current_row, column=1, value=parent_material)
first_row_in_group = False
else:
# 后续行留空
ws.cell(row=current_row, column=1, value='')
# 写入母岩类型
ws.cell(row=current_row, column=2, value=row_data['母岩'])
# 写入土种类型(所有土种用逗号连接)
ws.cell(row=current_row, column=3, value=row_data['TZ'])
# 写入统计数据
ws.cell(row=current_row, column=4, value=round(row_data['mean'], 1))
ws.cell(row=current_row, column=5, value=row_data['count'])
current_row += 1
# 合并母岩母质分组
if parent_material_row < current_row:
ws.merge_cells(start_row=parent_material_row, start_column=1, end_row=current_row - 1, end_column=1)
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=3)
ws.cell(row=current_row, column=1, value='全区')
# --- d. 应用样式和调整列宽 ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
apply_style(f'A1:{max_col_letter}{current_row}', cell_font, center_align, thin_border)
apply_style(f'A1:{max_col_letter}2', header_font)
# 设置列宽
ws.column_dimensions["A"].width = 20
ws.column_dimensions["B"].width = 20
ws.column_dimensions["C"].width = 30
ws.column_dimensions["D"].width = 20
ws.column_dimensions["E"].width = 20
# 保存文件
wb.save(output_path)
print(f"数据已成功写入到 {output_path}")
def main(gdb_path, soil_prop_name, trlx_features, reclassed_feature, output_path,target_areas_df, prop_config):
try:
# --- 1. 用户配置 ---
# 输出配置
temp_files = []
output_excel_path = os.path.join(output_path,f"{soil_prop_name}不同土壤类型土壤.xlsx") # 生成的Excel报告文件路径
# output_excel4_path = os.path.join(output_path,f"{soil_prop_name}不同母岩母质土壤属性.xlsx")
soil_prop_features = os.path.join(gdb_path,soil_prop_name)
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
print("开始处理数据...")
temp_out_features = r"in_memory/temp_out_type_features"
out_table_mean = r"in_memory/out_table_type_mean"
temp_files.append(temp_out_features)
temp_files.append(out_table_mean)
# 2. 用样点进行空间连接到土壤类型图斑
fields_to_keep = {
soil_prop_features: [soil_prop_name],
trlx_features: ["YL", "TS", "TZ"],
}
field_mappings = arcpy.FieldMappings()
for join_features in fields_to_keep.keys():
for field_name in fields_to_keep[join_features]:
try:
field_map = arcpy.FieldMap()
field_map.addInputField(join_features, field_name)
field_map.mergeRule = "First" # 对所有连接字段使用 "First" 规则
field_mappings.addFieldMap(field_map)
except Exception as e:
print(f"警告: 添加字段 '{field_name}' (来自 '{join_features}') 时出错,将跳过。错误信息: {e}")
# 空间连接
arcpy.analysis.SpatialJoin(soil_prop_features, trlx_features, temp_out_features, "JOIN_ONE_TO_ONE", "KEEP_ALL",field_mappings, "INTERSECT")
# 3. 交集制表计算每个TRZD的面积
arcpy.analysis.TabulateIntersection(trlx_features, "YL_TS", reclassed_feature, out_table_mean, "gridcode", out_units="SQUARE_METERS")
trlx_zhitu_df = pd.DataFrame(arcpy.da.TableToNumPyArray(out_table_mean, ["YL_TS", "gridcode", "AREA"]))
trlx_sample_df = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(temp_out_features, ["YL", "TS", "TZ", soil_prop_name]))
# 获取土壤类型图斑面积
trlx_area_df = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(trlx_features, ["YL", "TS", "Shape@Area"]))
# 处理表3数据
final_dataframe = process_data_for_table3(soil_prop_name,trlx_sample_df, trlx_zhitu_df, trlx_area_df, target_areas_df)
# print(final_dataframe)
# 生成表3
write_to_excel_table3(final_dataframe, output_excel_path, prop_config)
# 母岩母质表
# write_to_excel_table4(df_mymz, output_excel4_path, prop_config)
# return df_with_factors
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
temp_files_processor.clean_up_temp_files(temp_files)
import gc
gc.collect()
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# main()

View File

@@ -0,0 +1,465 @@
# -*- coding: utf-8 -*-
import os
import re
import arcpy
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font, Border, Side, Alignment
from openpyxl.utils import get_column_letter
from tools.config.pandas_field_cal_func import calculate_muyan, calculate_muzhi
from tools.config.custom_sort import yl_order, ts_order
from tools.core.utils.os_utils import temp_files_processor
# --- 2. 辅助函数 ---
def get_prop_level(prop_level):
"""根据输入值判断 返回等级"""
if pd.isna(prop_level) or prop_level == 0:
return "-"
# 请根据您的实际分级标准调整这里的阈值
if int(prop_level) == 5 or prop_level == "砂质":
return "砂质"
elif int(prop_level) == 4 or prop_level == "砂壤质":
return "砂壤质"
elif int(prop_level) == 3 or prop_level == "壤质":
return "壤质"
elif int(prop_level) == 1 or prop_level == "黏壤质":
return "黏壤质"
elif int(prop_level) == 2 or prop_level == "黏质":
return "黏质"
else:
return "-"
# 判断单元格类型
def get_merge_type(merged_range):
"""
判断合并类型
返回: 'row'(行合并), 'column'(列合并), 'both'(行列合并)或 None不是合并单元格
"""
if not merged_range:
return None
min_row, max_row = merged_range.min_row, merged_range.max_row
min_col, max_col = merged_range.min_col, merged_range.max_col
if max_row > min_row and max_col > min_col:
return 'both' # 同时跨行和跨列
elif max_row > min_row:
return 'row' # 行合并(垂直合并)
elif max_col > min_col:
return 'column' # 列合并(水平合并)
else:
return None # 实际上不是合并单元格
# --- 3. 数据处理与分析 均值---
def process_data_for_table3(soil_prop_name, df_trlx_sample, df_trlx_zhitu, df_trlx, target_areas_df):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("开始处理数据...")
def clean_df(df, columns) -> pd.DataFrame:
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# ==a. 处理样点数据,计算“样点均值” ---
print("--> 步骤1: 计算样点均值...")
field_name = soil_prop_name
sample_fields = ['YL', 'TS', field_name]
df_samples = clean_df(df_trlx_sample, sample_fields)
df_samples["GRIDCODE"] = df_samples[field_name]
# 通过土属计算母岩母质
df_samples['母岩'] = df_samples['TS'].apply(calculate_muyan)
df_samples['母质'] = df_samples['母岩'].apply(calculate_muzhi)
# 按 YJDL, EJDL 分组
df_sample_means = df_samples.groupby(['YL', 'TS', 'GRIDCODE']).size().reset_index(name="样点数")
total_sample_count = df_sample_means['样点数'].sum()
df_sample_means['样点数占比'] = df_sample_means['样点数'] / total_sample_count
# df_sample_mymz = df_samples.groupby(['母质', '母岩', 'TZ'])[field_name].agg(['count', 'mean', 'median']).reset_index()
# print(df_sample_mymz)
# ==b. 处理制图数据,获各等级制图面积
df_trlx_zhitu["YL"] = df_trlx_zhitu['YL_TS'].apply(lambda x: x.split('_')[0])
df_trlx_zhitu["TS"] = df_trlx_zhitu["YL_TS"].apply(lambda x: x.split('_')[1])
df_trlx_zhitu.columns = df_trlx_zhitu.columns.str.upper()
df_trlx_zhitu = clean_df(df_trlx_zhitu, ['YL', 'TS'])
df_trlx_zhitu['GRIDCODE'] = df_trlx_zhitu['GRIDCODE'].apply(get_prop_level)
df_map_data = df_trlx_zhitu.groupby(["YL","TS", "GRIDCODE"]).agg({"AREA": "sum"}).reset_index()
df_map_data['制图面积_原始'] = df_map_data['AREA'] * 0.0015 # 单位:亩
# ==c. 处理制图数据,获各等级制图面积
df_trlx = clean_df(df_trlx, ['YL', 'TS'])
df_trlx["面积"] = df_trlx["Shape@Area"] * 0.0015
# 拿到目标df总面积计算比例进行平差
target_areas = target_areas_df['面积'].sum()
original_area = df_trlx['面积'].sum()
adjusted_area_yz = target_areas / original_area
df_trlx["面积"] = df_trlx["面积"] * adjusted_area_yz
df_trlx_area = df_trlx.groupby(['YL', 'TS'])['面积'].sum().reset_index()
df_trlx_area['面积'] = pd.to_numeric(df_trlx_area['面积'], errors='coerce').fillna(0)
# ==========================
# 第三步:按二级地类分组计算平差系数
# 先计算每个二级地类的原始合计面积
ts_original_sum = df_map_data.groupby('TS')['制图面积_原始'].sum().reset_index()
ts_original_sum.rename(columns={'制图面积_原始': '原始合计面积'}, inplace=True)
# 合并目标面积
ts_adj = pd.merge(ts_original_sum, df_trlx_area, on='TS', how='left')
ts_adj.rename(columns={'面积': '目标合计面积'}, inplace=True)
# 填充无目标面积的二级地类(目标面积=原始面积,平差系数=1
ts_adj['目标合计面积'] = ts_adj['目标合计面积'].fillna(ts_adj['原始合计面积'])
# 计算平差系数(目标面积 / 原始面积避免除以0
ts_adj['平差系数'] = ts_adj['目标合计面积'] / ts_adj['原始合计面积'].replace(0, 1)
ts_adj['平差系数'] = ts_adj['平差系数'].fillna(1) # 极端情况填充1
# 第四步:应用平差系数到每个质地级别的制图面积
df_map_data = pd.merge(df_map_data, ts_adj[['TS', '平差系数']], on='TS', how='left')
df_map_data['平差系数'] = df_map_data['平差系数'].fillna(1) # 未匹配到的二级地类系数=1
# 计算平差后的制图面积
df_map_data['制图面积'] = df_map_data['制图面积_原始'] * df_map_data['平差系数']
# 重新计算面积占比(基于平差后的面积)
total_adjusted_area = df_map_data['制图面积'].sum()
df_map_data['面积占比'] = df_map_data['制图面积'] / total_adjusted_area
df_map_data = clean_df(df_map_data, ['YL', 'TS'])
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['YL', 'TS', 'GRIDCODE']],
df_map_data[['YL', 'TS', 'GRIDCODE']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['YL', 'TS', 'GRIDCODE'], how='left')
df_final = pd.merge(df_final, df_map_data, on=['YL', 'TS', 'GRIDCODE'], how='left')
# (可选) 按“亚类”和“土属”排序
in_yl_order = yl_order + [x for x in df_final['YL'].unique() if x not in yl_order]
in_ts_order = ts_order + [x for x in df_final['TS'].unique() if x not in ts_order]
df_final["YL"] = pd.Categorical(df_final['YL'], categories=in_yl_order, ordered=True)
df_final["TS"] = pd.Categorical(df_final['TS'], categories=in_ts_order, ordered=True)
df_final["GRIDCODE"] = pd.Categorical(df_final['GRIDCODE'], categories=sorted(df_final['GRIDCODE'].unique()), ordered=True)
df_final.sort_values(['YL', 'TS', 'GRIDCODE'], inplace=True)
print("数据处理流程完成!")
return df_final
# return df_final, df_sample_mymz
# 写入EXCEL 表2
def write_to_excel_table3(df, output_path, prop_config:dict):
"""
将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同土壤类型属性变化统计"
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
cell_font = Font(name='等线', size=11)
center_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
def apply_style(cell_range, font, alignment=None, border=None):
for row in ws[cell_range]:
for cell in row:
cell.font = font
if alignment: cell.alignment = alignment
if border: cell.border = border
# --- b. 绘制表头 ---
ws.merge_cells('A1:B1'); ws['A1'] = '土壤类型'
ws.merge_cells('C1:E1'); ws['C1'] = '样点统计'
ws.merge_cells('F1:G1'); ws['F1'] = '制图统计'
ws['A2'] = '亚类'
ws['B2'] = '土属'
ws['C2'] = '质地类型'
ws['D2'] = '数量/个'
ws['E2'] = '占比%'
ws['F2'] = '面积/亩'
ws['G2'] = '占比%'
level_dict = prop_config['标准等级']
# 创建两个列表来分别存储上段和下段范围
upper_ranges = {value: key for key, value in level_dict.items()}
# --- c. 填充数据 ---
current_row = 3
df_to_write = df.copy() # 使用 .copy() 避免 SettingWithCopyWarning
for yl, group_yl_df in df_to_write.groupby('YL', sort=False, observed=True):
if group_yl_df.empty:
continue
print(f"正在写入亚类: {yl}...")
yl_start_row = current_row
# 按二级地类分组
for ts, group_ts_df in group_yl_df.groupby('TS', sort=False, observed=False):
if group_ts_df.empty:
continue
print(f"正在写入二级地类: {ts}...")
ts_start_row = current_row
# 遍历该亚类下下的所有“土属”
for _, row_data in group_ts_df.iterrows():
ws.cell(row=current_row, column=3).value = row_data['GRIDCODE']
# 填充样点数据
ws.cell(row=current_row, column=4).value = row_data['样点数'] if not np.isnan(row_data['样点数']) else '-'
ws.cell(row=current_row, column=5).value = round(row_data['样点数占比']*100, 2) if not np.isnan(row_data['样点数占比']) else '-'
# 填充制图数据
ws.cell(row=current_row, column=6).value = round(row_data['制图面积'], 0) if not np.isnan(row_data['制图面积']) else '-'
ws.cell(row=current_row, column=7).value = round(row_data['面积占比']*100, 2) if not np.isnan(row_data['面积占比']) else '-'
current_row += 1
# 合并二级地类单元格
if ts_start_row <= current_row:
ws.merge_cells(start_row=ts_start_row, start_column=2, end_row=current_row-1, end_column=2)
ws.cell(row=ts_start_row, column=2).value = ts
# 合并“一级地类”单元格
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row-1, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
# 计算全区的均值、范围、数量
total_areas = df_to_write['制图面积'].sum()
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=3)
ws.cell(row=current_row, column=1).value = '全区'
ws.cell(row=current_row, column=4).value = df_to_write['样点数'].sum()
ws.cell(row=current_row, column=5).value = round(df_to_write['样点数占比'].sum()*100, 2)
ws.cell(row=current_row, column=6).value = round(total_areas, 0)
ws.cell(row=current_row, column=7).value = round(df_to_write['面积占比'].sum()*100, 2)
# --- d. 应用样式和调整列宽 ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
apply_style(f'A1:{max_col_letter}{current_row}', cell_font, center_align, thin_border)
apply_style(f'A1:{max_col_letter}2', header_font)
print("正在自动调整列宽...")
dims = {}
for row in ws.rows:
for cell in row:
if cell.value:
merged_range = next((range for range in ws.merged_cells.ranges if cell.coordinate in range), None)
if get_merge_type(merged_range) == 'column':
continue
cell_len = 0.7 * len(re.findall('([\u4e00-\u9fa5])', str(cell.value))) + len(str(cell.value))
dims[cell.column] = max(dims.get(cell.column, 0), cell_len)
# 设置列宽
for col, value in dims.items():
ws.column_dimensions[get_column_letter(int(col))].width = value + 5
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
# 母岩母质表
def write_to_excel_table4(df:pd.DataFrame, output_path, prop_config):
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "母岩母质土壤属性统计"
# 获取属性单位
special_prop = ['耕作层厚度','阳离子','有机质','pH','有效磷','速效钾','交换性钙','交换性镁','有效硫','有效铁','有效锰','有效硅','全钾','沙粒','粉粒','粘粒']
prop_name_str = prop_config.get('项目分级','')
if prop_name_str:
prop_name = prop_name_str.split('\n')[0].strip() in special_prop
else:
prop_name = False
prop_unit_str = prop_config.get('分级标准', '')
if prop_unit_str:
prop_unit = prop_unit_str.split('\n')[1].strip()
else:
prop_unit = ''
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
cell_font = Font(name='等线', size=11)
center_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
def apply_style(cell_range, font, alignment=None, border=None):
for row in ws[cell_range]:
for cell in row:
cell.font = font
if alignment: cell.alignment = alignment
if border: cell.border = border
# 写入表头
headers = ['母岩母质','', '土种类型', '样点统计', '']
ws.append(headers)
ws.append(['', '', '', f'均值/{prop_unit}', '数量/个'])
# 合并表头单元格
ws.merge_cells('A1:B2') # 母岩母质
ws.merge_cells('C1:C2') # 土种类型
ws.merge_cells('D1:E1') # 样点统计
current_row = 3
# 按母质和母岩进行分组
grouped = df.groupby(['母质', '母岩']).agg({
'TZ': lambda x: ''.join(x), # 将土种名称用逗号连接
'mean': 'mean', # 计算均值
'count': 'sum' # 计算总数
}).reset_index()
parent_materials = grouped['母质'].unique()
for parent_material in parent_materials:
parent_material_row = current_row
if parent_material == '未知':
continue
material_group = grouped[grouped['母质'] == parent_material]
# 写入母岩母质分组(只在第一行显示)
first_row_in_group = True
for _, row_data in material_group.iterrows():
if first_row_in_group:
# 第一行显示母岩母质名称
ws.cell(row=current_row, column=1, value=parent_material)
first_row_in_group = False
else:
# 后续行留空
ws.cell(row=current_row, column=1, value='')
# 写入母岩类型
ws.cell(row=current_row, column=2, value=row_data['母岩'])
# 写入土种类型(所有土种用逗号连接)
ws.cell(row=current_row, column=3, value=row_data['TZ'])
# 写入统计数据
ws.cell(row=current_row, column=4, value=round(row_data['mean'], 1))
ws.cell(row=current_row, column=5, value=row_data['count'])
current_row += 1
# 合并母岩母质分组
if parent_material_row < current_row:
ws.merge_cells(start_row=parent_material_row, start_column=1, end_row=current_row - 1, end_column=1)
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=3)
ws.cell(row=current_row, column=1, value='全区')
# --- d. 应用样式和调整列宽 ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
apply_style(f'A1:{max_col_letter}{current_row}', cell_font, center_align, thin_border)
apply_style(f'A1:{max_col_letter}2', header_font)
# 设置列宽
ws.column_dimensions["A"].width = 20
ws.column_dimensions["B"].width = 20
ws.column_dimensions["C"].width = 30
ws.column_dimensions["D"].width = 20
ws.column_dimensions["E"].width = 20
# 保存文件
wb.save(output_path)
print(f"数据已成功写入到 {output_path}")
def main(gdb_path, soil_prop_name, trlx_features, reclassed_feature, output_path,target_areas_df, prop_config):
try:
# --- 1. 用户配置 ---
# 输出配置
temp_files = []
output_excel_path = os.path.join(output_path,f"{soil_prop_name}不同土壤类型土壤.xlsx") # 生成的Excel报告文件路径
# output_excel4_path = os.path.join(output_path,f"{soil_prop_name}不同母岩母质土壤属性.xlsx")
soil_prop_features = os.path.join(gdb_path,soil_prop_name)
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
print("开始处理数据...")
temp_out_features = r"in_memory/temp_out_type_features"
out_table_mean = r"in_memory/out_table_type_mean"
temp_files.append(temp_out_features)
temp_files.append(out_table_mean)
# 2. 用样点进行空间连接到土壤类型图斑
fields_to_keep = {
soil_prop_features: [soil_prop_name],
trlx_features: ["YL", "TS", "TZ"],
}
field_mappings = arcpy.FieldMappings()
for join_features in fields_to_keep.keys():
for field_name in fields_to_keep[join_features]:
try:
field_map = arcpy.FieldMap()
field_map.addInputField(join_features, field_name)
field_map.mergeRule = "First" # 对所有连接字段使用 "First" 规则
field_mappings.addFieldMap(field_map)
except Exception as e:
print(f"警告: 添加字段 '{field_name}' (来自 '{join_features}') 时出错,将跳过。错误信息: {e}")
# 空间连接
arcpy.analysis.SpatialJoin(soil_prop_features, trlx_features, temp_out_features, "JOIN_ONE_TO_ONE", "KEEP_ALL",field_mappings, "INTERSECT")
# 3. 交集制表计算每个TRZD的面积
arcpy.analysis.TabulateIntersection(trlx_features, "YL_TS", reclassed_feature, out_table_mean, "gridcode", out_units="SQUARE_METERS")
trlx_zhitu_df = pd.DataFrame(arcpy.da.TableToNumPyArray(out_table_mean, ["YL_TS", "gridcode", "AREA"]))
trlx_sample_df = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(temp_out_features, ["YL", "TS", "TZ", soil_prop_name]))
# 获取土壤类型图斑面积
trlx_area_df = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(trlx_features, ["YL", "TS", "Shape@Area"]))
# 处理表3数据
final_dataframe = process_data_for_table3(soil_prop_name,trlx_sample_df, trlx_zhitu_df, trlx_area_df, target_areas_df)
# print(final_dataframe)
# 生成表3
write_to_excel_table3(final_dataframe, output_excel_path, prop_config)
# 母岩母质表
# write_to_excel_table4(df_mymz, output_excel4_path, prop_config)
# return df_with_factors
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
temp_files_processor.clean_up_temp_files(temp_files)
import gc
gc.collect()
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# main()

View File

@@ -0,0 +1,512 @@
# -*- coding: utf-8 -*-
import os
import arcpy
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
from tools.config.pandas_field_cal_func import calculate_muyan, calculate_muzhi
from tools.config.custom_sort import yl_order, ts_order
from tools.core.utils.os_utils import temp_files_processor
from tools.core.utils.excel_utils import ExcelStyleUtils
# --- 3. 数据处理与分析 均值---
def process_data_for_table3(soil_prop_name, df_trlx_sample, df_trlx_zhitu, df_trlx, target_areas_df):
"""
【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。
"""
print("开始处理数据...")
def clean_df(df, columns) -> pd.DataFrame:
for col in columns:
df[col] = df[col].astype(str).str.strip()
df.replace(['<Null>', 'None', '', '<空>'], np.nan, inplace=True)
df.dropna(subset=columns, inplace=True)
return df
# ==a. 处理样点数据,计算“样点均值” ---
print("--> 步骤1: 计算样点均值...")
field_name = soil_prop_name
sample_fields = ['YL', 'TS', field_name]
df_samples = clean_df(df_trlx_sample, sample_fields)
df_samples[field_name] = df_samples[field_name].astype(float)
# 通过土属计算母岩母质
df_samples['母岩'] = df_samples['TS'].apply(calculate_muyan)
df_samples['母质'] = df_samples['母岩'].apply(calculate_muzhi)
# 按 YJDL, EJDL 分组,计算 dPH 的均值
df_sample_means = df_samples.groupby(['YL', 'TS'])[field_name].agg(['count', 'max', 'min', 'mean', 'median']).reset_index()
df_sample_mymz = df_samples.groupby(['母质', '母岩', 'TZ'])[field_name].agg(['count', 'mean', 'median']).reset_index()
# print(df_sample_mymz)
# ==b. 处理制图数据,获各等级制图面积
df_trlx_zhitu["YL"] = df_trlx_zhitu['YL_TS'].apply(lambda x: x.split('_')[0])
df_trlx_zhitu["TS"] = df_trlx_zhitu["YL_TS"].apply(lambda x: x.split('_')[1])
df_trlx_zhitu = clean_df(df_trlx_zhitu, ['YL', 'TS'])
df_trlx_zhitu.rename(columns={'MEAN': '制图均值', 'COUNT': '制图样点数'}, inplace=True)
# ==c. 处理制图数据,获各等级制图面积
df_trlx = clean_df(df_trlx, ['YL', 'TS'])
df_trlx["面积_亩"] = df_trlx["Shape@Area"] * 0.0015
filtered_props = ['ECA', 'EMG', 'ACU', 'AZN', 'AFE', 'AMN', 'AMO', 'AB', 'AS1', 'TSE']
# 拿到目标df总面积计算比例进行平差
print(target_areas_df)
if soil_prop_name == "GZCHD":
target_areas = target_areas_df[target_areas_df['EJDL'] == '耕地']['面积'].values[0]
elif soil_prop_name in filtered_props:
target_areas = target_areas_df[target_areas_df['EJDL'].isin(['耕地', '园地'])]['面积'].sum()
else:
target_areas = target_areas_df['面积'].sum()
original_area = df_trlx['面积_亩'].sum()
adjusted_area_yz = target_areas / original_area
df_trlx["面积_亩"] = df_trlx["面积_亩"] * adjusted_area_yz
df_trlx_area = df_trlx.groupby(['YL', 'TS'])['面积_亩'].sum().reset_index()
# --- c. 合并数据 ---
print("--> 步骤3: 合并数据...")
df_skeleton = pd.concat([
df_sample_means[['YL', 'TS']],
df_trlx_zhitu[['YL', 'TS']]
]).drop_duplicates().reset_index(drop=True)
df_final = pd.merge(df_skeleton, df_sample_means, on=['YL', 'TS'], how='left')
df_final = pd.merge(df_final, df_trlx_zhitu, on=['YL', 'TS'], how='left')
df_final = pd.merge(df_final, df_trlx_area, on=['YL', 'TS'], how='left')
# (可选) 按“亚类”和“土属”排序
in_yl_order = yl_order + [x for x in df_final['YL'].unique() if x not in yl_order]
in_ts_order = ts_order + [x for x in df_final['TS'].unique() if x not in ts_order]
df_final["YL"] = pd.Categorical(df_final['YL'], categories=in_yl_order, ordered=True)
df_final["TS"] = pd.Categorical(df_final['TS'], categories=in_ts_order, ordered=True)
df_final.sort_values(['YL', 'TS'], inplace=True)
print("数据处理流程完成!")
return df_final, df_sample_mymz
# 写入EXCEL 表2
def write_to_excel_table3(df, output_path, prop_config:dict, stats):
"""
将处理好的数据写入格式化的 Excel 文件。
"""
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
print(f"开始生成 Excel 报告到 '{output_path}'...")
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "不同土壤类型属性变化统计"
# 获取属性单位
special_prop = ['耕作层厚度','阳离子','有机质','pH','有效磷','速效钾','交换性钙','交换性镁','有效硫','有效铁','有效锰','有效硅','全钾']
fsn_props = ['砂粒含量','粉粒含量','黏粒含量']
prop_name_str = prop_config.get('项目分级','')
if prop_name_str:
split_name = prop_name_str.split('\n')[0].strip()
if split_name in special_prop:
prop_name = '1f'
elif split_name in fsn_props:
prop_name = '0f'
else:
prop_name = '2f'
else:
prop_name = '1f'
prop_unit_str = prop_config.get('分级标准', '')
if prop_unit_str:
prop_unit = prop_unit_str.split('\n')[1].strip()
else:
prop_unit = ''
# --- b. 绘制表头 ---
ws.merge_cells('A1:B1'); ws['A1'] = '土壤类型'
ws.merge_cells('C1:F1'); ws['C1'] = '样点统计'
ws.merge_cells('G1:H1'); ws['G1'] = '制图统计'
ws['A2'] = '亚类'
ws['B2'] = '土属'
ws['C2'] = '均值/' + prop_unit
ws['D2'] = '中位值/' + prop_unit
ws['E2'] = '范围/' + prop_unit
ws['F2'] = '数量/个'
ws['G2'] = '均值/' + prop_unit
ws['H2'] = '面积/亩'
# --- c. 填充数据 ---
current_row = 3
df_to_write = df.copy() # 使用 .copy() 避免 SettingWithCopyWarning
for yl, group_yl_df in df_to_write.groupby('YL', sort=False, observed=True):
print(f"正在写入亚类: {yl}...")
yl_start_row = current_row
# 遍历该亚类下下的所有“土属”
for _, row_data in group_yl_df.iterrows():
ws.cell(row=current_row, column=2).value = row_data['TS']
# 填充样点数据
sample_mean = row_data.get('mean')
if pd.notna(sample_mean):
ws.cell(row=current_row, column=3).value = f"{sample_mean:.{prop_name}}"
ws.cell(row=current_row, column=4).value = f"{row_data.get('median', '-'):.{prop_name}}"
ws.cell(row=current_row, column=5).value = f"{row_data.get('min', '-'):.{prop_name}}{row_data.get('max', '-'):.{prop_name}}"
ws.cell(row=current_row, column=6).value = row_data.get('count', '-')
else:
ws.cell(row=current_row, column=3).value = "-"
ws.cell(row=current_row, column=4).value = "-"
ws.cell(row=current_row, column=5).value = "-"
ws.cell(row=current_row, column=6).value = "-"
# 填充制图数据
map_mean = row_data.get('制图均值')
if pd.notna(map_mean):
ws.cell(row=current_row, column=7).value = f"{map_mean:.{prop_name}}"
ws.cell(row=current_row, column=8).value = f"{row_data.get('面积_亩', '-'):.0f}"
else:
ws.cell(row=current_row, column=7).value = "-"
ws.cell(row=current_row, column=8).value = "-"
current_row += 1
# 计算并写入“合计”行
if ws.cell(row=current_row-1, column=2).value in ["林地", "草地", "其他"]:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=yl_start_row, end_column=2)
ws.cell(row=yl_start_row, column=1).value = yl
continue
ws.cell(row=current_row, column=2).value = '合计'
# 计算合计行的均值 (均值的均值)
total_count = group_yl_df['count'].sum()
weighted_sum = group_yl_df['mean'] * group_yl_df['count']
if not weighted_sum.empty and total_count != 0:
total_sample_mean = weighted_sum.sum() / total_count
else:
total_sample_mean = None
total_median = group_yl_df['median'].mean()
min_min, max_max = group_yl_df['min'].min(), group_yl_df['max'].max()
if pd.notna(total_sample_mean):
ws.cell(row=current_row, column=3).value = f"{total_sample_mean:.{prop_name}}"
ws.cell(row=current_row, column=4).value = f"{total_median:.{prop_name}}"
ws.cell(row=current_row, column=5).value = f"{min_min:.{prop_name}}{max_max:.{prop_name}}"
ws.cell(row=current_row, column=6).value = f"{total_count:.0f}"
else:
ws.cell(row=current_row, column=3).value = "-"
ws.cell(row=current_row, column=4).value = "-"
ws.cell(row=current_row, column=5).value = "-"
ws.cell(row=current_row, column=6).value = "-"
# b. **【核心修正】: 计算合计行的“制图均值”(加权平均)**
# 准备加权平均的分子和分母
weighted_sum = 0
total_count = 0
# 遍历当前一级地类分组中的每一行
for _, row in group_yl_df.iterrows():
mean_val = row.get('制图均值')
count_val = row.get('制图样点数')
# 只有当均值和样点数都存在且有效时,才参与计算
if pd.notna(mean_val) and pd.notna(count_val) and count_val > 0:
weighted_sum += mean_val * count_val # Σ (mean * count)
total_count += count_val # Σ (count)
# 计算加权平均值
weighted_avg = (weighted_sum / total_count) if total_count > 0 else 0
total_area = group_yl_df['面积_亩'].sum()
if weighted_avg > 0:
ws.cell(row=current_row, column=7).value = f"{weighted_avg:.{prop_name}}"
ws.cell(row=current_row, column=8).value = f"{total_area:.0f}"
else:
ws.cell(row=current_row, column=7).value = "-"
ws.cell(row=current_row, column=8).value = "-"
# 合并“一级地类”单元格
if yl_start_row <= current_row:
ws.merge_cells(start_row=yl_start_row, start_column=1, end_row=current_row, end_column=1)
ws.cell(row=yl_start_row, column=1).value = yl
current_row += 1
# 计算全区的均值、范围、数量
# total_counts = df_to_write['count'].sum()
# total_weighted_sum = df_to_write['mean'] * df_to_write['count']
# total_mean = total_weighted_sum.sum() / total_counts
# total_median = df_to_write['median'].mean()
total_range = f"{df_to_write['min'].min():.{prop_name}}{df_to_write['max'].max():.{prop_name}}"
total_zhitu_weighted_sum = df_to_write['制图均值'] * df_to_write['面积_亩']
total_areas = df_to_write['面积_亩'].sum()
total_zhitu_mean = total_zhitu_weighted_sum.sum() / total_areas
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=2)
ws.cell(row=current_row, column=1).value = '全区'
ws.cell(row=current_row, column=3).value = f"{stats['mean']:.{prop_name}}"
ws.cell(row=current_row, column=4).value = f"{stats['median']:.{prop_name}}"
ws.cell(row=current_row, column=5).value = total_range
ws.cell(row=current_row, column=6).value = f"{stats['count']:.0f}"
ws.cell(row=current_row, column=7).value = f"{total_zhitu_mean:.{prop_name}}"
ws.cell(row=current_row, column=8).value = f"{total_areas:.0f}"
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
# --- d. 应用样式和调整列宽 ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}{current_row}')
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}2', header_font)
print("正在自动调整列宽...")
# 设置列宽
ExcelStyleUtils.auto_adjust_column_width(ws)
# --- e. 保存文件 ---
wb.save(output_path)
print("Excel 报告生成成功!")
# 母岩母质表
def write_to_excel_table4(df:pd.DataFrame, output_path, prop_config, stats):
if df.empty:
print("警告: 没有数据可以写入 Excel。")
return
wb = Workbook()
ws = wb.create_sheet("Mysheet", 0)
ws.title = "母岩母质土壤属性统计"
# 获取属性单位
special_prop = ['耕作层厚度','阳离子','有机质','pH','有效磷','速效钾','交换性钙','交换性镁','有效硫','有效铁','有效锰','有效硅','全钾']
fsn_props = ['砂粒含量','粉粒含量','黏粒含量','有效土层厚度']
prop_name_str = prop_config.get('项目分级','')
if prop_name_str:
split_name = prop_name_str.split('\n')[0].strip()
if split_name in special_prop:
prop_name = '1f'
elif split_name in fsn_props:
prop_name = '0f'
else:
prop_name = '2f'
else:
prop_name = '1f'
prop_unit_str = prop_config.get('分级标准', '')
if prop_unit_str:
prop_unit = prop_unit_str.split('\n')[1].strip()
else:
prop_unit = ''
# 写入表头
headers = ['母岩母质','', '土种类型', '样点统计', '']
ws.append(headers)
ws.append(['', '', '', f'均值/{prop_unit}', '数量/个'])
# 合并表头单元格
ws.merge_cells('A1:B2') # 母岩母质
ws.merge_cells('C1:C2') # 土种类型
ws.merge_cells('D1:E1') # 样点统计
current_row = 3
# 按母质和母岩进行分组
grouped = df.groupby(['母质', '母岩']).agg({
'TZ': lambda x: ''.join(x), # 将土种名称用逗号连接
'mean': 'mean', # 计算均值
'count': 'sum' # 计算总数
}).reset_index()
parent_materials = grouped['母质'].unique()
for parent_material in parent_materials:
parent_material_row = current_row
if parent_material == '未知':
continue
material_group = grouped[grouped['母质'] == parent_material]
# 写入母岩母质分组(只在第一行显示)
first_row_in_group = True
for _, row_data in material_group.iterrows():
if first_row_in_group:
# 第一行显示母岩母质名称
ws.cell(row=current_row, column=1, value=parent_material)
first_row_in_group = False
else:
# 后续行留空
ws.cell(row=current_row, column=1, value='')
# 写入母岩类型
ws.cell(row=current_row, column=2, value=row_data['母岩'])
# 写入土种类型(所有土种用逗号连接)
ws.cell(row=current_row, column=3, value=row_data['TZ'])
# 写入统计数据
ws.cell(row=current_row, column=4, value=round(row_data['mean'], 1))
ws.cell(row=current_row, column=5, value=row_data['count'])
current_row += 1
# 合并母岩母质分组
if parent_material_row < current_row:
ws.merge_cells(start_row=parent_material_row, start_column=1, end_row=current_row - 1, end_column=1)
# 计算合计值并写入
# total_mean = 0
# total_count = df['count'].sum()
# total_sum = df['mean'] * df['count']
# if total_count and total_count!=0:
# total_mean = total_sum.sum() / total_count
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=3)
ws.cell(row=current_row, column=1, value='全区')
ws.cell(row=current_row, column=4, value=f"{stats['mean']:.{prop_name}}")
ws.cell(row=current_row, column=5, value=f"{stats['count']:.0f}")
# --- a. 定义样式 ---
header_font = Font(name='等线', size=11, bold=True)
# --- d. 应用样式和调整列宽 ---
max_col_letter = get_column_letter(ws.max_column)
if current_row > 1: # 确保有数据才应用样式
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}{current_row}')
ExcelStyleUtils.set_style(ws, f'A1:{max_col_letter}2', header_font)
# 设置列宽
ws.column_dimensions["A"].width = 20
ws.column_dimensions["B"].width = 20
ws.column_dimensions["C"].width = 30
ws.column_dimensions["D"].width = 20
ws.column_dimensions["E"].width = 20
# 保存文件
wb.save(output_path)
print(f"数据已成功写入到 {output_path}")
def main(gdb_path, soil_prop_name, trlx_features, soil_prop_tif, output_path,target_areas_df, prop_config, dltb_features):
try:
# --- 1. 用户配置 ---
# 输出配置
temp_files = []
output_excel_path = os.path.join(output_path,f"{soil_prop_name}不同土壤类型土壤.xlsx") # 生成的Excel报告文件路径
output_excel4_path = os.path.join(output_path,f"{soil_prop_name}不同母岩母质土壤属性.xlsx")
soil_prop_features = os.path.join(gdb_path,soil_prop_name)
# 设置工作空间和变量
arcpy.env.workspace = gdb_path
arcpy.env.overwriteOutput = True
print("开始处理数据...")
if soil_prop_name == "GZCHD":
temp_gdtb_trlx_out = r"in_memory/temp_gdtb_trlx_out"
temp_gdtb_trlx = r"in_memory/temp_gdtb_trlx"
temp_files.append(temp_gdtb_trlx)
temp_out_features = r"in_memory/temp_out_type_features"
out_table_mean = r"in_memory/out_table_type_mean"
temp_files.append(temp_out_features)
temp_files.append(out_table_mean)
# 2. 用样点进行空间连接到土壤类型图斑
fields_to_keep = {
soil_prop_features: [soil_prop_name],
trlx_features: ["YL", "TS", "TZ"],
}
field_mappings = arcpy.FieldMappings()
for join_features in fields_to_keep.keys():
for field_name in fields_to_keep[join_features]:
try:
field_map = arcpy.FieldMap()
field_map.addInputField(join_features, field_name)
field_map.mergeRule = "First" # 对所有连接字段使用 "First" 规则
field_mappings.addFieldMap(field_map)
except Exception as e:
print(f"警告: 添加字段 '{field_name}' (来自 '{join_features}') 时出错,将跳过。错误信息: {e}")
# 定义需要过滤地类的属性列表
filtered_props = ['ECA', 'EMG', 'ACU', 'AZN', 'AFE', 'AMN', 'AMO', 'AB', 'AS1', 'TSE']
# 空间连接
arcpy.analysis.SpatialJoin(soil_prop_features, trlx_features, temp_out_features, "JOIN_ONE_TO_ONE", "KEEP_ALL",field_mappings, "INTERSECT")
if soil_prop_name == "GZCHD":
arcpy.analysis.Intersect([trlx_features, dltb_features], temp_gdtb_trlx, 'NO_FID')
arcpy.conversion.ExportFeatures(temp_gdtb_trlx,temp_gdtb_trlx_out,"DLBM LIKE '01%'")
# 3. 以表格显示分区统计 计算均值
arcpy.sa.ZonalStatisticsAsTable(temp_gdtb_trlx_out, "YL_TS", soil_prop_tif, out_table_mean, "DATA", "MEAN")
trlx_area_df = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(temp_gdtb_trlx_out, ["YL", "TS", "Shape@Area"]))
# 如果当前属性在列表中,则只统计耕地和园地
elif soil_prop_name in filtered_props:
temp_gdtb_trlx_filtered = r"in_memory/temp_gdtb_trlx_filtered"
temp_gdtb_trlx_out_filtered = r"in_memory/temp_gdtb_trlx_out_filtered"
temp_files.append(temp_gdtb_trlx_filtered)
temp_files.append(temp_gdtb_trlx_out_filtered)
# 交集土壤类型与土地利用图斑
arcpy.analysis.Intersect([trlx_features, dltb_features], temp_gdtb_trlx_filtered, 'NO_FID')
# 导出耕地和园地DLBM LIKE '01%' OR DLBM LIKE '02%'
arcpy.conversion.ExportFeatures(temp_gdtb_trlx_filtered, temp_gdtb_trlx_out_filtered, "DLBM LIKE '01%' OR DLBM LIKE '02%'")
# 使用过滤后的图斑进行分区统计(制图均值)
arcpy.sa.ZonalStatisticsAsTable(temp_gdtb_trlx_out_filtered, "YL_TS", soil_prop_tif, out_table_mean, "DATA", "MEAN")
# 获取过滤后的面积
trlx_area_df = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(temp_gdtb_trlx_out_filtered, ["YL", "TS", "Shape@Area"]))
print(f"过滤制图数据仅统计耕地和园地DLBM LIKE '01%' OR '02%'")
else:
# 3. 以表格显示分区统计 计算均值
arcpy.sa.ZonalStatisticsAsTable(trlx_features, "YL_TS", soil_prop_tif, out_table_mean, "DATA", "MEAN")
# 获取土壤类型图斑面积
trlx_area_df = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(trlx_features, ["YL", "TS", "Shape@Area"]))
trlx_zhitu_df = pd.DataFrame(arcpy.da.TableToNumPyArray(out_table_mean, ["YL_TS", "MEAN", "COUNT"]))
trlx_sample_df = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(temp_out_features, ["YL", "TS", "TZ", soil_prop_name]))
stat_sample = {
'min': trlx_sample_df[soil_prop_name].min(),
'max': trlx_sample_df[soil_prop_name].max(),
'mean':trlx_sample_df[soil_prop_name].mean(),
'median': trlx_sample_df[soil_prop_name].median(),
'count': trlx_sample_df[soil_prop_name].count()
}
# 处理表3数据
final_dataframe, df_mymz = process_data_for_table3(soil_prop_name,trlx_sample_df, trlx_zhitu_df, trlx_area_df, target_areas_df)
# print(final_dataframe)
# 生成表3
write_to_excel_table3(final_dataframe, output_excel_path, prop_config, stat_sample)
# 母岩母质表
write_to_excel_table4(df_mymz, output_excel4_path, prop_config,stat_sample)
# return df_with_factors
except Exception as e:
print(f"\n处理过程中发生严重错误: {e}")
import traceback
traceback.print_exc()
finally:
temp_files_processor.clean_up_temp_files(temp_files)
import gc
gc.collect()
# --- 4. 主程序入口 ---
# if __name__ == "__main__":
# main()

File diff suppressed because it is too large Load Diff

View File

View File

@@ -0,0 +1,660 @@
# -*- coding: utf-8 -*-
"""
输入重分类后栅格转面要素类、乡镇边界面要素类、地类图斑要素类;
按一级地类统计土壤属性面积 和 按乡镇统计土壤属性面积;
将统计结果导出为Excel表格
将Excel表格转换为jpg图片
"""
import json
import os
from pathlib import Path
import sys
import traceback
import uuid
import arcpy
import argparse
import numpy as np
import pandas as pd
from openpyxl.styles import Font, Alignment, Border, Side, numbers
from openpyxl.utils import get_column_letter
sys.path.append(str(Path(__file__).parent))
from tools.core.utils.os_utils import temp_files_processor
from tools.config.common_config import guangxi_region, yunnan_region
from utils import common_utils, 平差工具
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="将ArcGIS表格转换为Excel")
parser.add_argument("--reclassed_polygon", required=True, help="重分类栅格的面要素")
parser.add_argument("--settings_path", required=True, help="配置文件路径")
args = parser.parse_args()
if args.settings_path:
with open(args.settings_path, 'r', encoding="utf-8") as settings_file:
settings = json.load(settings_file)
area_stat_settings = settings.get("area_stat_settings", {})
if area_stat_settings:
standards_dict_path = area_stat_settings.get("config_file_path", "")
with open(standards_dict_path, 'r', encoding="utf-8") as standards_file:
standards_dict = json.load(standards_file)
config_key = common_utils.get_config_key(Path(args.reclassed_polygon).stem)
output_settings = standards_dict['export_config'][config_key]
area_stat_settings["output_settings"] = output_settings
area_stat_settings["reclassed_polygon"] = args.reclassed_polygon
area_stat_settings["soil_property"] = config_key
else:
print_status("错误: 未找到有效配置文件")
sys.exit(1)
return area_stat_settings
def print_status(message):
"""
输出状态信息到标准输出,用于 GUI 实时显示
格式: STATUS: <message>
"""
print(f"STATUS:{message}")
sys.stdout.flush() # 确保立即输出
def print_result(success, output_path="", error_message=""):
"""
输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果
格式: RESULT:True|<output_path>|
格式: RESULT:False||<error_message>
"""
if success:
print(f"RESULT:True|{output_path}|")
else:
# 在错误信息中替换换行符,避免干扰解析
cleaned_error_message = error_message.replace('\n', ' ').replace('\r', '')
print(f"RESULT:False||{cleaned_error_message}")
sys.stdout.flush() # 确保立即输出
def log_arcpy_message(message):
"""输出 ArcPy 产生的 geoprocessing 消息"""
# 可以在这里进一步处理或过滤 ArcPy 消息
if message.type == 'Message':
print_status(f"ArcPy消息: {message.message}")
elif message.type == 'Warning':
print_status(f"ArcPy警告: {message.message}")
elif message.type == 'Error':
# 对于错误,也可以记录到标准错误
print_status(f"ArcPy错误: {message.message}")
sys.stderr.write(f"ArcPyError:{message.message}\n")
sys.stderr.flush()
def get_specail_map(original_dict):
grade_map = {}
dict_len = len(original_dict)
order = 1
for grade_key, range_str in original_dict.items():
ranges = [r.strip() for r in range_str.replace('\n', ',').split(',') if r.strip()]
s_order = 0
for r in ranges:
grade_map[str(order + s_order*dict_len)] = r
s_order += 1
order += 1
return grade_map
class SoilQualityReporter:
def __init__(self, output_path, grade_map: dict, landuse_map, output_settings):
"""
初始化土壤质量报告生成器
参数:
output_path: 输出Excel文件路径
"""
self.output_path = output_path
self.grade_map = grade_map
self.landuse_map = landuse_map
self.all_grades = [int(key) for key in self.grade_map.keys()]
self.xiangmu_name = output_settings['项目分级'].split('\n')[0]
self.xiangmu_jibie =self.xiangmu_name + "分级" + output_settings['分级标准'].split('\n')[1]
def prepare_data(self, stats):
"""
准备乡镇统计和地类统计两个表格
参数:
stats: 包含原始统计数据的DataFrame
需要包含列: XZQMC(乡镇名称), YJDLBM(地类编码), GRIDCODE(土壤等级), temp_area(面积)
返回:
df_town: 乡镇统计表
df_landuse: 地类统计表
"""
# 表格1各乡镇耕地土壤有机质分级面积统计
df_town = self._create_town_table(stats)
# 表格2各地类土壤有机质分级面积统计
df_landuse = self._create_landuse_table(stats)
return df_town, df_landuse
# 先创建GRIDCODE到分组的映射
# def _map_grade(self, code):
# code = int(code)
# if code in (1,6):
# return 1
# elif code in (2, 7):
# return 2
# elif code in (3, 8):
# return 3
# elif code in (4, 9):
# return 4
# elif code in (5,10):
# return 5
# return code
def _create_town_table(self, stats:pd.DataFrame):
"""生成乡镇统计表"""
# 复制数据并添加分组列
df_stats = stats.copy()
# 如果存在YJDLBM列确保其值为字符串
if "YJDLBM" not in df_stats.columns:
# 取YNDLBM列的前两位作为YJDLBM
df_stats["YJDLBM"] = df_stats["YNDLBM"].str[:2]
# df_stats["GRID_GROUP"] = df_stats["GRIDCODE"].apply(self._map_grade)
# 使用分组列进行透视
df = df_stats[df_stats["YJDLBM"] == "01"].pivot_table(
index="XZQMC",
columns="GRIDCODE",
values="adjusted_area",
aggfunc="sum",
fill_value=0,
observed=False
)
# 确保所有等级列都存在
for grade in self.all_grades:
if grade not in df.columns:
df[grade] = 0
# 按等级排序并添加总计
df = df[self.all_grades]
df["总计"] = df.sum(axis=1)
df.loc["总计"] = df.sum(axis=0)
# 重命名列
df.columns = [self.grade_map.get(str(col), str(col)) for col in df.columns]
return df
def _create_landuse_table(self, stats):
"""生成地类统计表"""
# 复制数据并添加分组列
df_stats = stats.copy()
# df_stats["GRID_GROUP"] = df_stats["GRIDCODE"].apply(self._map_grade)
if "YJDLBM" not in df_stats.columns:
df = df_stats.pivot_table(
index="YNDLBM",
columns="GRIDCODE",
values="adjusted_area",
aggfunc="sum",
fill_value=0,
observed=False
)
else:
df = df_stats.pivot_table(
index="YJDLBM",
columns="GRIDCODE",
values="adjusted_area",
aggfunc="sum",
fill_value=0,
observed=False
)
# 确保所有等级列都存在
for grade in self.all_grades:
if grade not in df.columns:
df[grade] = 0
# 按等级排序并添加总计
df = df[self.all_grades]
df["总计"] = df.sum(axis=1)
df.loc["总计"] = df.sum(axis=0)
# 重命名索引和列
df = df.rename(index=self.landuse_map)
df.columns = [self.grade_map.get(str(col), str(col)) for col in df.columns]
return df
def generate_report(self, stats):
"""
生成完整报告
参数:
stats: 包含原始统计数据的DataFrame
"""
self.is_yunnan = True if 'YNDLBM' in stats.columns else False
# 准备数据
df_town, df_landuse = self.prepare_data(stats)
# 导出Excel
self._export_to_excel(df_town, df_landuse)
def _export_to_excel(self, df_town, df_landuse):
"""导出数据到Excel"""
with pd.ExcelWriter(self.output_path, engine='openpyxl') as writer:
workbook = writer.book
sheet = workbook.create_sheet("综合统计表")
# 写入乡镇统计表
self._write_town_table(sheet, df_town)
# 写入地类统计表
start_row_landuse = len(df_town) + 5
self._write_landuse_table(sheet, df_landuse, start_row_landuse)
# 应用通用格式
self._apply_common_format(sheet, start_row_landuse)
# 删除默认空工作表
if 'Sheet' in workbook.sheetnames:
workbook.remove(workbook['Sheet'])
def _write_town_table(self, sheet, df):
"""写入乡镇统计表"""
# 动态计算列数
last_col = len(df.columns) + 2 # 最后一列的索引
last_col_letter = get_column_letter(last_col) # 转为字母
second_last_col_letter = get_column_letter(last_col - 1)
# 表头
sheet.merge_cells(f"A1:{last_col_letter}1")
sheet["A1"] = f"各乡镇耕地土壤{self.xiangmu_name}分级面积统计表"
sheet["A1"].font = Font(size=18)
sheet["A1"].alignment = Alignment(horizontal='center')
sheet.row_dimensions[1].height = 34
# 单位行
sheet[f"{last_col_letter}2"] = "单位:亩"
sheet[f"{last_col_letter}2"].font = Font(size=14)
sheet[f"{last_col_letter}2"].alignment = Alignment(horizontal='center')
# 列标题
sheet.merge_cells("A3:B4")
sheet["A3"] = "乡镇"
# 总计
sheet.merge_cells(f"{last_col_letter}3:{last_col_letter}4")
sheet[f"{last_col_letter}3"] = "总计"
sheet.merge_cells(f"C3:{second_last_col_letter}3")
sheet["C3"] = self.xiangmu_jibie
sheet.row_dimensions[3].height = 25
# 写入分级列名
for col_num, col_name in enumerate(df.columns[:-1], start=2):
# print(col_num, col_name)
sheet.cell(row=4, column=col_num+1, value=col_name)
# 写入数据
for r_idx, (index, row) in enumerate(df.iterrows(), start=5):
sheet.merge_cells(f"A{r_idx}:B{r_idx}")
sheet.cell(row=r_idx, column=1, value=index)
for c_idx, value in enumerate(row, start=2):
sheet.cell(row=r_idx, column=c_idx+1, value=value)
def _write_landuse_table(self, sheet, df, start_row):
"""写入地类统计表"""
# 动态计算列数
last_col = len(df.columns) + 2 # 最后一列的索引
last_col_letter = get_column_letter(last_col) # 转为字母
second_last_col_letter = get_column_letter(last_col - 1)
# 表头
sheet.merge_cells(f"A{start_row}:{last_col_letter}{start_row}")
sheet[f"A{start_row}"] = f"各地类土壤{self.xiangmu_name}分级面积统计表"
sheet.row_dimensions[start_row].height = 34
# 单位行
sheet[f"{last_col_letter}{start_row+1}"] = "单位:亩"
# 列标题
if self.is_yunnan:
sheet.merge_cells(f"A{start_row+2}:B{start_row+2}")
sheet[f"A{start_row+2}"] = "土地利用类型"
sheet[f"A{start_row+3}"] = "一级"
sheet[f"B{start_row+3}"] = "二级"
else:
sheet.merge_cells(f"A{start_row+2}:B{start_row+3}")
sheet[f"A{start_row+2}"] = "土地利用\n类型"
sheet.merge_cells(f"{last_col_letter}{start_row+2}:{last_col_letter}{start_row+3}")
sheet[f"{last_col_letter}{start_row+2}"] = "总计"
sheet.merge_cells(f"C{start_row+2}:{second_last_col_letter}{start_row+2}")
sheet[f"C{start_row+2}"] = self.xiangmu_jibie
sheet.row_dimensions[start_row+2].height = 25
# 写入分级列名
for col_num, col_name in enumerate(df.columns[:-1], start=2):
sheet.cell(row=start_row+3, column=col_num+1, value=col_name)
# 写入数据
for r_idx, (index, row) in enumerate(df.iterrows(), start=start_row+4):
sheet.merge_cells(f"A{r_idx}:B{r_idx}")
sheet.cell(row=r_idx, column=1, value=index)
for c_idx, value in enumerate(row, start=2):
sheet.cell(row=r_idx, column=c_idx+1, value=value)
def _apply_common_format(self, sheet, landuse_start_row):
"""应用通用格式"""
# 设置列宽
for col in range(1, sheet.max_column + 1):
col_letter = chr(64 + col)
sheet.column_dimensions[col_letter].width = 14
for row in range(5, sheet.max_row+1):
if row not in [landuse_start_row,landuse_start_row+1, landuse_start_row+2,landuse_start_row+3]:
sheet.row_dimensions[row].height = 23
# 定义边框样式
thin_border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
# 应用样式到所有单元格
for row in sheet.iter_rows(min_row=3, max_row=sheet.max_row, min_col=1, max_col=sheet.max_column):
for cell in row:
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
cell.font = Font(bold=True, size=14)
# 特殊格式
if cell.column == 1 and cell.row > 3 and cell.row not in (landuse_start_row, landuse_start_row+2): # 列A
cell.font = Font(bold=False, size=14)
if cell.column == sheet.max_column and cell.row == landuse_start_row+1:
cell.font = Font(bold=False, size=14)
#地类统计表头
if cell.row == landuse_start_row:
cell.font = Font(bold=False, size=18)
cell.alignment = Alignment(vertical='bottom', horizontal='center')
# 地类统计表列标题
if (cell.row == 3 and cell.column == 2) or (cell.row == landuse_start_row+2 and cell.column == 2):
cell.font = Font(bold=False, size=14)
# 数字格式
if isinstance(cell.value, (int, float)):
cell.number_format = numbers.FORMAT_NUMBER
if round(cell.value,0) == 0.0:
cell.value = "-"
# 边框
if cell.row >1 and cell.row not in (landuse_start_row, landuse_start_row+1):
cell.border = thin_border
def read_arcgis_table(table_path):
"""将ArcGIS表格转换为Pandas DataFrame"""
array = arcpy.da.TableToNumPyArray(table_path, "*")
df = pd.DataFrame(array)
# df.to_csv(r"D:\工作\三普成果编制\出图数据\广西海城区\过程数据\酸化面积统计表\temp.csv")
df.columns = df.columns.str.upper()
df["temp_area"] = df["AREA"] * 0.0015
df["temp_area"] = df["temp_area"].round(4)
# 删除可能存在的OID字段如果不需要
if 'OID@' in df.columns:
df = df.drop('OID@', axis=1)
return df
# 获取每个一级地类面积主要是12类
def get_area_by_group(dltb_class_feature, excel_target_path, xzqmc, is_by_xzq=False):
try:
# 读取目标面积Excel文件
if xzqmc in yunnan_region:
target_df = pd.read_excel(excel_target_path, sheet_name="Sheet2")
landuse_types = {'0101':'水田', '0102':'水浇地', '0103':'旱地', '02':'园地', '03':'林地', '04':'草地', '12':'其他'}
elif xzqmc in guangxi_region:
target_df = pd.read_excel(excel_target_path, sheet_name="Sheet1")
landuse_types = {'01':'耕地', '02':'园地', '03':'林地', '04':'草地', '12':'其他'}
else:
target_df = pd.read_excel(excel_target_path, sheet_name="Sheet1")
landuse_types = {'01':'耕地', '02':'园地', '03':'林地', '04':'草地', '12':'其他'}
# 确保列名匹配
target_df.columns = target_df.columns.str.strip()
if is_by_xzq:
# 地类编码映射字典
land_type_mapping = {
'耕地': '01',
'园地': '02',
'林地': '03',
'草地': '04',
'其他': '12'
}
# 方法1重命名列后转换为字典
df_encoded = target_df.rename(columns=land_type_mapping)
result_dict = df_encoded.set_index('行政单位').to_dict('index')
return result_dict
# 检查要素类是否存在
if not arcpy.Exists(dltb_class_feature):
print(f"警告:输入要素类不存在: {dltb_class_feature}")
else:
if xzqmc in yunnan_region:
dlbm = 'YNDLBM'
elif xzqmc in guangxi_region:
dlbm = 'YJDLBM'
else:
dlbm = 'YJDLBM'
# 转为numpy数组供pandas统计使用
df = pd.DataFrame(arcpy.da.TableToNumPyArray(dltb_class_feature, [dlbm, "TBDLMJ"], skip_nulls=False, null_value=np.nan))
qtdl_df = df[df[dlbm] == '12']
if qtdl_df['TBDLMJ'].isnull().any() or qtdl_df['TBDLMJ'].eq(0).any():
print("警告其他地类TBDLMJ字段 存在空值或无效的记录,将不平差其他地类")
target_areas = {}
else:
area_by_group = df.groupby(dlbm)["TBDLMJ"].sum()
for key in area_by_group.keys():
area_by_group[key] = area_by_group[key] * 0.0015
target_areas = area_by_group.to_dict()
# 获取目标面积
gangnan_target = target_df[target_df['行政单位'] == xzqmc]
if gangnan_target.empty:
print(f"警告:未找到{xzqmc}的目标面积数据,将使用TBDLMJ数据进行平差")
return target_areas
for dlbm, dlmc in landuse_types.items():
if dlmc in gangnan_target.columns:
if gangnan_target[dlmc].values[0]:
target_areas[dlbm] = gangnan_target[dlmc].values[0]
return target_areas
except Exception as e:
print(f"计算面积时出错: {str(e)}")
return None
def main():
params = None
temp_files_to_clean = []
original_workspace = None
try:
# 1. 解析参数
params = parse_arguments()
reclassed_polygon = params["reclassed_polygon"]
soil_property = params["soil_property"]
xzq_polygon = params["xzq_features"]
dltb_polygon = params["dltb_features"]
output_path = params["batch_output_folder"]
input_path = params["input_folder"]
output_settings = params["output_settings"]
xzqmc = params["xzqmc"]
is_by_xzq = params["is_by_xzq"]
original_workspace = arcpy.env.workspace
arcpy.env.workspace = input_path
arcpy.env.overwriteOutput = True
if not arcpy.Exists(reclassed_polygon):
raise FileNotFoundError(f"输入文件不存在: {reclassed_polygon}")
if not arcpy.Exists(xzq_polygon):
raise FileNotFoundError(f"输入文件不存在: {xzq_polygon}")
if not arcpy.Exists(dltb_polygon):
raise FileNotFoundError(f"输入文件不存在: {dltb_polygon}")
if not os.path.exists(output_path):
os.makedirs(output_path)
output_origin_path = os.path.join(output_path, "原始结果")
if not os.path.exists(output_origin_path):
os.makedirs(output_origin_path)
output_xlsx_path = os.path.join(output_origin_path, f"{soil_property}_原始面积统计表.xlsx")
output_adjust_xlsx_path = os.path.join(output_path, f"{soil_property}_面积统计表.xlsx")
output_jpg_path = os.path.join(output_path, f"{soil_property}_area_stats.jpg")
out_feature_class = fr"in_memory\out_feature_class_{uuid.uuid4().hex[:8]}"
out_dbf_table = fr"in_memory\out_dbf_table_{uuid.uuid4().hex[:8]}"
temp_files_to_clean.append([out_feature_class, out_dbf_table])
# 求地类图斑和重分类栅格面的交集
print_status(f"求地类图斑和重分类栅格面的交集...")
in_features = [dltb_polygon, reclassed_polygon]
arcpy.analysis.Intersect(
in_features=in_features,
out_feature_class=out_feature_class,
join_attributes="ALL",
output_type="INPUT"
)
if xzqmc in yunnan_region:
print_status(f"开始执行交集制表...")
arcpy.analysis.TabulateIntersection(
in_zone_features=xzq_polygon, # 乡镇边界
zone_fields="XZQMC",
in_class_features=out_feature_class,
out_table=out_dbf_table,
class_fields="gridcode;YNDLBM",
out_units="SQUARE_METERS"
)
elif xzqmc in guangxi_region:
# 交集制表
print_status(f"开始执行交集制表...")
arcpy.analysis.TabulateIntersection(
in_zone_features=xzq_polygon, # 乡镇边界
zone_fields="XZQMC",
in_class_features=out_feature_class,
out_table=out_dbf_table,
class_fields="gridcode;YJDLBM",
out_units="SQUARE_METERS"
)
else:
print_status(f"未找到{xzqmc}的区域配置")
raise ValueError(f"未找到{xzqmc}的区域配置")
# 读取DBF表格到Pandas DataFrame
clipped_gdf = read_arcgis_table(out_dbf_table)
# 准备参数
try:
if xzqmc in yunnan_region:
stats = (
clipped_gdf.groupby(["XZQMC", "YNDLBM", "GRIDCODE"])
.agg({"temp_area": "sum"})
.reset_index()
)
elif xzqmc in guangxi_region:
stats = (
clipped_gdf.groupby(["XZQMC", "YJDLBM", "GRIDCODE"])
.agg({"temp_area": "sum"})
.reset_index()
)
else:
print_status(f"未找到{xzqmc}的区域配置")
raise ValueError(f"未找到{xzqmc}的区域配置")
except Exception as e:
stats = (
clipped_gdf.groupby(["XZQMC", "YJDLBM", "GRIDCODE"])
.agg({"temp_area": "sum"})
.reset_index()
)
stats["adjusted_area"] = stats["temp_area"]
# stats.to_csv("area_stats.csv", index=True)
# 重命名列(按实际土壤分级字段调整)
if soil_property == "PH" or soil_property == "TRRZ":
grade_map = get_specail_map(output_settings["标准等级"])
elif "1" in output_settings["标准等级"].values(): # 土壤质地
grade_map = {str(i+1): str(val) for i,val in enumerate(output_settings["标准等级"].keys())}
elif "-10-0.3" in output_settings["标准等级"].values(): # 酸化pH
grade_map = {str(i+1): str(val) for i,val in enumerate(output_settings["标准等级"].keys())}
elif "-100.1" in output_settings["标准等级"].values(): # 二普-三普变化pH
grade_map = {str(i + 1): str(val) for i, val in enumerate(output_settings["标准等级"].keys())}
else:
grade_map = {str(i+1): str(val) for i,val in enumerate(output_settings["标准等级"].values())}
if xzqmc in guangxi_region:
landuse_map = {"01": "耕地", "02": "园地", "03": "林地", "04": "草地", "12": "其他"}
else:
landuse_map = {"0101": "水田", "0102": "水浇地", "0103": "旱地", "02": "园地", "03": "林地", "04": "草地", "12": "其他"}
# 平差处理
excel_target_path = Path("tools/config_json/公布的变更调查平差面积.xlsx") # 您的目标面积Excel文件路径
each_dl_target = get_area_by_group(dltb_polygon, excel_target_path, xzqmc, is_by_xzq) # 获取每个地类目标面积
# if soil_property == "GZCHD":
# print(each_dl_target)
# each_dl_target = {"01":each_dl_target["01"]}
if is_by_xzq:
adjusted_stats = 平差工具.adjust_by_district_landuse(stats, each_dl_target)
else:
adjusted_stats = 平差工具.adjust_area_statistics(stats, each_dl_target)
# print(adjusted_stats)
# 2. 生成XLSX报告
reporter = SoilQualityReporter(output_xlsx_path, grade_map, landuse_map, output_settings)
reporter.generate_report(stats)
reporter_adjust = SoilQualityReporter(output_adjust_xlsx_path, grade_map, landuse_map, output_settings)
reporter_adjust.generate_report(adjusted_stats)
print_result(True, output_jpg_path, "")
except Exception as e:
error_msg = f"主函数错误: {str(e)}\n{traceback.format_exc()}"
print_status(error_msg)
print_result(False, error_message=error_msg)
finally:
temp_files_processor.clean_up_temp_files(temp_files_to_clean, workspace=original_workspace)
sys.exit(0)
if __name__ == '__main__':
print_status("开始执行")
main()

View File

@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
"""
输入重分类后栅格转面要素类、乡镇边界面要素类、地类图斑要素类;
按一级地类统计土壤属性面积 和 按乡镇统计土壤属性面积;
酸化情况统计表生成,第一按样点数量统计,第二按制图面积统计(分土壤类型、乡镇、土地利用类型等进行统计)
"""
import json
from pathlib import Path
import sys
import traceback
import time
import arcpy
import argparse
from tools.core.utils import 平差工具
from tools.core.utils.os_utils import temp_files_processor
sys.path.append(str(Path(__file__).parent))
from acid_stats import 空间连接, 行政区划酸化统计表, 土地利用类型酸化统计表, 土壤类型图酸化统计表
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="将ArcGIS表格转换为Excel")
parser.add_argument("--settings_path", required=True, help="配置文件路径")
args = parser.parse_args()
if args.settings_path:
with open(args.settings_path, 'r', encoding="utf-8") as settings_file:
settings = json.load(settings_file)
area_stat_settings = settings.get("acid_stat_settings", {})
else:
print_status("错误: 未找到有效配置文件")
sys.exit(1)
return area_stat_settings
def print_status(message):
"""
输出状态信息到标准输出,用于 GUI 实时显示
格式: STATUS: <message>
"""
print(f"STATUS:{message}")
sys.stdout.flush() # 确保立即输出
def print_result(success, output_path="", error_message=""):
"""
输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果
格式: RESULT:True|<output_path>|
格式: RESULT:False||<error_message>
"""
if success:
print(f"RESULT:True|{output_path}|")
else:
# 在错误信息中替换换行符,避免干扰解析
cleaned_error_message = error_message.replace('\n', ' ').replace('\r', '')
print(f"RESULT:False||{cleaned_error_message}")
sys.stdout.flush() # 确保立即输出
def log_arcpy_message(message):
"""输出 ArcPy 产生的 geoprocessing 消息"""
# 可以在这里进一步处理或过滤 ArcPy 消息
if message.type == 'Message':
print_status(f"ArcPy消息: {message.message}")
elif message.type == 'Warning':
print_status(f"ArcPy警告: {message.message}")
elif message.type == 'Error':
# 对于错误,也可以记录到标准错误
print_status(f"ArcPy错误: {message.message}")
sys.stderr.write(f"ArcPyError:{message.message}\n")
sys.stderr.flush()
def main():
params = None
temp_files_to_clean = []
original_workspace = None
try:
# 1. 解析参数
params = parse_arguments()
xzq_polygon = params["xzq_features"]
dltb_polygon = params["dltb_features"]
output_path = params["batch_output_folder"]
workspace_path = params["workspace_path"]
trlx_features = params["soil_type_features"] # 土壤类型图
assign_raster = params["assign_raster"] # 三普或者二普栅格
ph_sample_feature = params["ph_samples"] # PH样点
sh_ph_tif_temp = params["acid_raster"] # 酸化PH栅格
ph_classed_polygon = params["acid_ph_features"] # 酸化PH重分类后要素
xzqmc = params["xzqmc"]
ph_sample_table = "历史样点PH信息_Table"
original_workspace = arcpy.env.workspace
arcpy.env.workspace = workspace_path
arcpy.env.overwriteOutput = True
# sh_ph_tif = f"in_memory/temp_ph_raster"
# temp_files_to_clean.append(sh_ph_tif)
input_ph_raster = arcpy.Raster(sh_ph_tif_temp)
filtered_raster = arcpy.sa.Con(input_ph_raster > 0.3, input_ph_raster)
# filtered_raster.save(sh_ph_tif)
sh_ph_tif = arcpy.Raster(filtered_raster)
temp_files_to_clean.append(sh_ph_tif)
# 1. 进行空间连接及赋值PH样点
if not arcpy.Exists(ph_sample_table):
print_status("样点空间连接...")
空间连接.export_to_points(ph_sample_feature, dltb_polygon, trlx_features, xzq_polygon, assign_raster, workspace_path)
time.sleep(4)
excel_target_path = Path("tools/config_json/公布的变更调查平差面积.xlsx") # 您的目标面积Excel文件路径
target_area_dict = 平差工具.get_area_by_group(dltb_polygon, excel_target_path, xzqmc) # 获取每个地类目标面积
# 2. 制作统计表
print_status("生成行政区划表...")
df_with_factor = 行政区划酸化统计表.main(workspace_path, xzq_polygon, ph_classed_polygon, dltb_polygon, sh_ph_tif, output_path, target_area_dict)
time.sleep(4)
print_status("生成土地利用类型表...")
土地利用类型酸化统计表.main(workspace_path, ph_classed_polygon,dltb_polygon, sh_ph_tif, output_path,target_area_dict)
time.sleep(4)
print_status("生成土壤类型酸化表...")
土壤类型图酸化统计表.main(workspace_path, trlx_features, ph_classed_polygon, sh_ph_tif, output_path, df_with_factor)
time.sleep(4)
print_result(True, output_path, "")
except Exception as e:
error_msg = f"主函数错误: {str(e)}\n{traceback.format_exc()}"
print_status(error_msg)
print_result(False, error_message=error_msg)
finally:
temp_files_processor.clean_up_temp_files(temp_files_to_clean, workspace=original_workspace)
sys.exit(0)
if __name__ == '__main__':
print_status("开始执行")
main()

View File

@@ -0,0 +1,235 @@
# -*- coding: utf-8 -*-
"""
输入重分类后栅格转面要素类、乡镇边界面要素类、地类图斑要素类;
按一级地类统计土壤属性面积 和 按乡镇统计土壤属性面积;
土壤属性统计表生成,第一按样点数量统计,第二按制图面积统计(分土壤类型、乡镇、土地利用类型等进行统计)
"""
import json
import multiprocessing
import os
from pathlib import Path
import sys
import traceback
import time
import arcpy
import argparse
from tools.core.utils import 平差工具, common_utils
sys.path.append(str(Path(__file__).parent))
from soil_prop_stats import B1土壤属性分级分布, B2土地利用类型土壤属性, B3不同土壤类型土壤属性, E1土壤属性历史变化, B3_TRZD不同土壤类型土壤属性, B1_TRZD土壤属性分级分布, B2_TRZD土地利用类型土壤属性
from soil_prop_stats import B3_TRZD12不同土壤类型土壤属性, B1_TRZD12土壤属性分级分布, B2_TRZD12土地利用类型土壤属性
from tools.config.arcgis_field_cal_code import codeblock_dltb_yjdl, codeblock_dltb_ejdl
from tools.core.utils.os_utils import temp_files_processor
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="将ArcGIS表格转换为Excel")
parser.add_argument("--settings_path", required=True, help="配置文件路径")
args = parser.parse_args()
if args.settings_path:
with open(args.settings_path, 'r', encoding="utf-8") as settings_file:
settings = json.load(settings_file)
area_stat_settings = settings.get("soil_prop_stat_settings", {})
else:
print_status("错误: 未找到有效配置文件")
sys.exit(1)
return area_stat_settings
def print_status(message):
"""
输出状态信息到标准输出,用于 GUI 实时显示
格式: STATUS: <message>
"""
print(f"STATUS:{message}")
sys.stdout.flush() # 确保立即输出
def print_result(success, output_path="", error_message=""):
"""
输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果
格式: RESULT:True|<output_path>|
格式: RESULT:False||<error_message>
"""
if success:
print(f"RESULT:True|{output_path}|")
else:
# 在错误信息中替换换行符,避免干扰解析
cleaned_error_message = error_message.replace('\n', ' ').replace('\r', '')
print(f"RESULT:False||{cleaned_error_message}")
sys.stdout.flush() # 确保立即输出
def log_arcpy_message(message):
"""输出 ArcPy 产生的 geoprocessing 消息"""
# 可以在这里进一步处理或过滤 ArcPy 消息
if message.type == 'Message':
print_status(f"ArcPy消息: {message.message}")
elif message.type == 'Warning':
print_status(f"ArcPy警告: {message.message}")
elif message.type == 'Error':
# 对于错误,也可以记录到标准错误
print_status(f"ArcPy错误: {message.message}")
sys.stderr.write(f"ArcPyError:{message.message}\n")
sys.stderr.flush()
def process_soil_property(args):
"""处理单个土壤属性的函数,用于多进程"""
soil_prop_name, config, data_source_path, history_samples_path, reclassed_features_path, history_reclassed_features_path, sanpu_prop_tif_path, history_raster_path, dltb_features, trlx_features, output_path, target_area_df1, target_area_df2, target_area_all, xzqmc = args
try:
prop_config = config['export_config'][soil_prop_name]
reclassed_features = os.path.join(reclassed_features_path, f"{soil_prop_name}_reclassed_polygon.shp")
history_reclassed_features = os.path.join(history_reclassed_features_path, f"{soil_prop_name}_reclassed_polygon.shp")
soil_prop_tif = os.path.join(sanpu_prop_tif_path, f"{soil_prop_name}.tif")
history_raster = os.path.join(history_raster_path, f"{soil_prop_name}.tif")
if not arcpy.Exists(reclassed_features) or not arcpy.Exists(soil_prop_tif):
print(f"缺少{soil_prop_name}的栅格或重分类要素,请检查输入文件路径是否正确!")
return False
print(f"生成{soil_prop_name}的表1...")
if soil_prop_name == "TRZD12":
B1_TRZD12土壤属性分级分布.main(data_source_path, soil_prop_name, reclassed_features, dltb_features, output_path, target_area_all,xzqmc, prop_config)
elif soil_prop_name == "TRZD":
B1_TRZD土壤属性分级分布.main(data_source_path, soil_prop_name, reclassed_features, dltb_features, output_path, target_area_all,xzqmc, prop_config)
else:
B1土壤属性分级分布.main(data_source_path, soil_prop_name, reclassed_features, dltb_features, soil_prop_tif, output_path, target_area_all,xzqmc, prop_config)
time.sleep(2)
# print(f"生成{soil_prop_name}的表2...")
# if soil_prop_name == "TRZD12":
# B2_TRZD12土地利用类型土壤属性.main(data_source_path, soil_prop_name, dltb_features, reclassed_features, output_path, target_area_df2, prop_config)
# elif soil_prop_name == "TRZD":
# B2_TRZD土地利用类型土壤属性.main(data_source_path, soil_prop_name, dltb_features, reclassed_features, output_path, target_area_df2, prop_config)
# else:
# B2土地利用类型土壤属性.main(data_source_path, soil_prop_name, dltb_features, soil_prop_tif, output_path, target_area_df2, prop_config)
# time.sleep(2)
# print(f"生成{soil_prop_name}的表3...")
# if soil_prop_name == "TRZD12":
# B3_TRZD12不同土壤类型土壤属性.main(data_source_path,soil_prop_name,trlx_features,reclassed_features,output_path,target_area_df1,prop_config)
# elif soil_prop_name == "TRZD":
# B3_TRZD不同土壤类型土壤属性.main(data_source_path, soil_prop_name, trlx_features, reclassed_features, output_path, target_area_df1, prop_config)
# else:
# B3不同土壤类型土壤属性.main(data_source_path, soil_prop_name, trlx_features, soil_prop_tif, output_path, target_area_df1, prop_config, dltb_features)
# time.sleep(2)
print(f"生成{soil_prop_name}的历史对比...")
# if arcpy.Exists(history_reclassed_features) and arcpy.Exists(history_raster) and arcpy.Exists(history_samples_path):
# E1土壤属性历史变化.main(xzqmc,data_source_path,history_samples_path,reclassed_features,history_reclassed_features, soil_prop_name, dltb_features, soil_prop_tif,history_raster, output_path, target_area_all, prop_config)
# time.sleep(2)
# else:
# print_status(f"警告:缺少{soil_prop_name}的历史栅格或重分类要素,请检查输入文件路径是否正确!")
return True
except Exception as e:
print(f"处理{soil_prop_name}时出错: {str(e)}")
return False
def main():
params = None
temp_files_to_clean = []
original_workspace = None
try:
# 1. 解析参数
params = parse_arguments()
xzqmc = params["xzqmc"]
config_file = params["config_file"]
output_path = params["output_folder"]
data_source_path = params["data_source_path"]
sanpu_prop_tif_path = params["sanpu_prop_tif_folder"]
reclassed_features_path = params["reclassed_feature_folder"]
# history_reclassed_features_path = params["history_reclassed_feature_folder"]
soil_prop_name_list = params["sample_list"]
# history_samples_path = params["history_samples_folder"]
# history_raster_path = params["history_raster_folder"]
history_samples_path = r"E:\@三普属性图出图\广西武鸣区\@基础数据\测土配方样点数据\测土配方样点.gdb"
history_raster_path = r"E:\@三普属性图出图\广西武鸣区\@基础数据\测土配方栅格\投影后"
history_reclassed_features_path = r"E:\@三普属性图出图\广西武鸣区\过程数据\测土配方重分类\面积统计用栅格面"
dltb_features = os.path.join(data_source_path, "地类图斑")
trlx_features = os.path.join(data_source_path, "土壤类型图")
original_workspace = arcpy.env.workspace
arcpy.env.workspace = data_source_path
arcpy.env.overwriteOutput = True
excel_target = Path("tools/config_json/公布的变更调查平差面积.xlsx") # 您的目标面积Excel文件路径
excel_target_path = str(excel_target.resolve())
target_area_df2 = 平差工具.get_target_areas(excel_target_path,"Sheet2", xzqmc) # 获取每个二级地类目标面积
target_area_df1 = 平差工具.get_target_areas(excel_target_path,"Sheet1", xzqmc) # 获取每个一级地类目标面积
target_area_all = 平差工具.get_target_areas_by_group(excel_target_path)
# 2. 制作统计表
# 计算土地利用类型图斑的地类
if arcpy.Exists(dltb_features):
try:
check_fields = ["YJDL", "EJDL", "YJDLBM", "YJDL_EJDL"]
if not common_utils.check_fields_exist_describe(dltb_features, check_fields):
arcpy.management.CalculateField(dltb_features, "EJDL", "calculate_ejdl(!DLBM!,!DLMC!)", "PYTHON3", codeblock_dltb_ejdl)
arcpy.management.CalculateField(dltb_features, "YJDL", "calculate_yjdl(!DLBM!)", "PYTHON3", codeblock_dltb_yjdl)
arcpy.management.CalculateField(dltb_features, "YJDLBM", "!DLBM![:2]", "PYTHON3")
arcpy.management.CalculateField(dltb_features,"YJDL_EJDL","!YJDL! + '_' + !EJDL!","PYTHON3")
except Exception as e:
print(f'报什么错:{e}')
# 计算土壤类型图斑的字段用于后续交集制表
if arcpy.Exists(trlx_features):
try:
check_fields = ["YL_TS"]
if not common_utils.check_fields_exist_describe(trlx_features, check_fields):
# 计算YL_TS字段的值
arcpy.management.CalculateField(trlx_features, "YL_TS", "!YL! + '_' + !TS!", "PYTHON3")
except Exception as e:
print(f'报什么错:{e}')
# 获取土壤属性配置文件
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
# 准备多进程参数
process_args = []
for soil_prop_name in soil_prop_name_list:
args = (soil_prop_name, config, data_source_path, history_samples_path, reclassed_features_path, history_reclassed_features_path,
sanpu_prop_tif_path, history_raster_path, dltb_features, trlx_features, output_path,
target_area_df1, target_area_df2, target_area_all,xzqmc)
process_args.append(args)
# 使用多进程处理
cpu_count = multiprocessing.cpu_count()
# num_processes = min(int(cpu_count if cpu_count<2 else cpu_count/2), len(soil_prop_name_list))
num_processes = 3
print(f"使用 {num_processes} 个进程并行处理 {len(soil_prop_name_list)} 个土壤属性...")
with multiprocessing.Pool(processes=num_processes) as pool:
results = pool.map(process_soil_property, process_args)
# 检查所有任务是否成功完成
if all(results):
print_result(True, output_path, "")
else:
failed_count = results.count(False)
error_msg = f"{failed_count} 个土壤属性处理失败"
print_result(False, error_message=error_msg)
print_result(True, output_path, "")
except Exception as e:
error_msg = f"主函数错误: {str(e)}\n{traceback.format_exc()}"
print_status(error_msg)
print_result(False, error_message=error_msg)
finally:
temp_files_processor.clean_up_temp_files(temp_files_to_clean, workspace=original_workspace)
sys.exit(0)
if __name__ == '__main__':
print_status("开始执行")
multiprocessing.freeze_support()
main()

249
tools/core/test_script.py Normal file
View File

@@ -0,0 +1,249 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
测试脚本
用于测试ArcGIS Pro图层属性和标注功能
"""
import os
import sys
import argparse
import traceback
import arcpy
def log(message):
"""日志输出函数"""
print(message)
def test_arcgis_environment():
"""测试ArcGIS Pro环境"""
log("=== ArcGIS Pro环境信息 ===")
log(f"Python版本: {sys.version}")
log(f"Python路径: {sys.executable}")
log(f"当前工作目录: {os.getcwd()}")
log(f"ArcPy版本: {arcpy.GetInstallInfo()['Version']}")
log(f"ArcPy产品: {arcpy.GetInstallInfo()['ProductName']}")
# 当前环境设置
log("\n=== ArcGIS环境设置 ===")
log(f"工作空间: {arcpy.env.workspace}")
log(f"输出坐标系统: {arcpy.env.outputCoordinateSystem}")
log(f"覆盖输出: {arcpy.env.overwriteOutput}")
def test_layer_properties(layer_path):
"""测试图层属性"""
log(f"\n=== 图层属性测试: {layer_path} ===")
if not arcpy.Exists(layer_path):
log(f"错误: 图层不存在 - {layer_path}")
return
try:
# 创建图层对象
log("尝试创建图层对象...")
layer = arcpy.mp.Layer(layer_path)
log(f"成功创建图层: {layer.name}")
# 图层基本属性
log("\n图层基本属性:")
log(f"名称: {layer.name}")
log(f"数据源类型: {type(layer.dataSource).__name__ if hasattr(layer, 'dataSource') else 'N/A'}")
log(f"长名称: {layer.longName if hasattr(layer, 'longName') else 'N/A'}")
# 图层类型
log("\n图层类型判断:")
log(f"是要素图层: {layer.isFeatureLayer if hasattr(layer, 'isFeatureLayer') else 'N/A'}")
log(f"是栅格图层: {layer.isRasterLayer if hasattr(layer, 'isRasterLayer') else 'N/A'}")
log(f"是图形图层: {layer.isGroupLayer if hasattr(layer, 'isGroupLayer') else 'N/A'}")
# 支持的属性
log("\n支持的属性:")
properties = [
'LABELCLASSES', 'SHOWLABELS', 'NAME', 'DATASOURCE', 'DEFINITIONQUERY',
'VISIBLE', 'TRANSPARENCY', 'BRIGHTNESS', 'CONTRAST', 'SYMBOLOGY'
]
for prop in properties:
try:
support = hasattr(layer, prop.lower()) or (hasattr(layer, 'supports') and layer.supports(prop))
log(f"{prop}: {'支持' if support else '不支持'}")
except Exception as e:
log(f"{prop}: 检查失败 - {str(e)}")
# 尝试获取标注类
log("\n标注类测试:")
try:
if hasattr(layer, 'listLabelClasses'):
label_classes = layer.listLabelClasses()
log(f"找到 {len(label_classes)} 个标注类")
# 显示每个标注类的信息
for i, lc in enumerate(label_classes):
log(f"标注类 #{i+1}: {lc.name if hasattr(lc, 'name') else 'N/A'}")
log(f" 表达式: {lc.expression if hasattr(lc, 'expression') else 'N/A'}")
log(f" SQL查询: {lc.SQLQuery if hasattr(lc, 'SQLQuery') else 'N/A'}")
log(f" 可见: {lc.showClassLabels if hasattr(lc, 'showClassLabels') else 'N/A'}")
else:
log("图层不支持listLabelClasses方法")
# 检查标注状态
if hasattr(layer, 'showLabels'):
log(f"标注显示状态: {layer.showLabels}")
else:
log("图层没有showLabels属性")
except Exception as e:
log(f"获取标注类时出错: {str(e)}")
# 使用arcpy.da.Describe获取详细信息
log("\narcpy.da.Describe描述信息:")
try:
desc = arcpy.da.Describe(layer)
for key in desc:
# 跳过复杂对象
if isinstance(desc[key], (dict, list, tuple)):
log(f"{key}: [复杂类型]")
else:
log(f"{key}: {desc[key]}")
except Exception as e:
log(f"获取Describe信息时出错: {str(e)}")
except Exception as e:
log(f"测试图层属性时出错: {str(e)}")
log(traceback.format_exc())
def test_annotation_conversion(map_path, layer_name, output_folder):
"""测试标注转注记功能"""
log(f"\n=== 标注转注记测试: {map_path}, 图层: {layer_name} ===")
if not arcpy.Exists(map_path):
log(f"错误: 地图文档不存在 - {map_path}")
return
# 确保输出文件夹存在
if not os.path.exists(output_folder):
os.makedirs(output_folder)
try:
# 打开地图文档
log(f"打开地图文档...")
aprx = arcpy.mp.ArcGISProject(map_path)
# 获取活动地图
log(f"获取地图...")
maps = aprx.listMaps()
if not maps:
log("错误: 地图文档中没有地图")
return
target_map = maps[0] # 默认使用第一个地图
log(f"使用地图: {target_map.name}")
# 查找指定图层
log(f"查找图层: {layer_name}")
layers = target_map.listLayers(layer_name)
if not layers:
log(f"错误: 找不到图层 {layer_name}")
return
label_layer = layers[0]
log(f"找到图层: {label_layer.name}")
# 测试图层属性
test_layer_properties(label_layer)
# 尝试开启标注
try:
if hasattr(label_layer, 'showLabels'):
log(f"当前标注状态: {label_layer.showLabels}")
label_layer.showLabels = True
log(f"已开启标注,新状态: {label_layer.showLabels}")
except Exception as e:
log(f"设置标注状态时出错: {str(e)}")
# 尝试执行标注转注记
log("\n执行标注转注记...")
try:
anno_name = f"{label_layer.name}_Anno"
output_anno = os.path.join(output_folder, anno_name)
# 检查是否已存在
if arcpy.Exists(output_anno):
log(f"注记已存在: {output_anno},将尝试删除")
arcpy.management.Delete(output_anno)
# 执行转换
log("使用ConvertLabelsToAnnotation...")
arcpy.cartography.ConvertLabelsToAnnotation(
target_map,
[label_layer],
output_folder,
"FEATURE_LINKED",
"STANDARD",
None,
None,
None
)
log("ConvertLabelsToAnnotation执行成功")
except Exception as e:
log(f"执行标注转注记时出错: {str(e)}")
log(traceback.format_exc())
# 尝试替代方案
try:
log("\n尝试使用LabelFeatures替代方案...")
arcpy.cartography.LabelFeatures(
in_features=label_layer,
out_geodatabase=output_folder
)
log("LabelFeatures执行成功")
except Exception as e2:
log(f"LabelFeatures也失败: {str(e2)}")
except Exception as e:
log(f"测试标注转注记时出错: {str(e)}")
log(traceback.format_exc())
finally:
if 'aprx' in locals():
del aprx
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='ArcGIS Pro图层和标注功能测试')
parser.add_argument('--message', default='测试消息', help='测试消息')
parser.add_argument('--count', type=int, default=1, help='重复次数')
parser.add_argument('--map', help='地图文档路径(.aprx)')
parser.add_argument('--layer', help='图层名称')
parser.add_argument('--layer_path', help='图层文件路径(.lyrx)')
parser.add_argument('--output', help='输出文件夹')
args = parser.parse_args()
# 输出系统信息
test_arcgis_environment()
# 输出测试消息
for i in range(args.count):
log(f"\n{i+1}. {args.message}")
# 如果提供了图层文件路径,测试图层属性
if args.layer_path:
test_layer_properties(args.layer_path)
# 如果提供了地图文档、图层名称和输出文件夹,测试标注转注记
if args.map and args.layer and args.output:
test_annotation_conversion(args.map, args.layer, args.output)
# 退出前打印完成消息
log("\n测试脚本执行完成")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

View File

@@ -0,0 +1,26 @@
import arcpy
import pandas as pd
def read_arcgis_table(table_path):
"""
将ArcGIS表格转换为Pandas DataFrame
:param table_path: ArcGIS表格路径
:return: Pandas DataFrame
表格字段全部转换为大写
面积字段AREA转换为亩保留4位小数存储在temp_area字段中
"""
array = arcpy.da.TableToNumPyArray(table_path, "*")
df = pd.DataFrame(array)
# df.to_csv(r"D:\工作\三普成果编制\出图数据\广西海城区\过程数据\酸化面积统计表\temp.csv")
df.columns = df.columns.str.upper()
df["temp_area"] = df["AREA"] * 0.0015
df["temp_area"] = df["temp_area"].round(4)
# 删除可能存在的OID字段如果不需要
if 'OID@' in df.columns:
df = df.drop('OID@', axis=1)
return df

View File

@@ -0,0 +1,291 @@
import arcpy
import numpy as np
def get_data_type(data_path):
"""获取数据类型
:param data_path: 数据路径
:return: 数据类型
"""
if arcpy.Exists(data_path):
try:
desc = arcpy.Describe(data_path)
return desc.dataType
except:
return False
else:
return False
def get_config_key(every_string: str) -> str:
config_dict = {
"AB": "有效硼","ACU": "有效铜","AMN": "有效锰","AMO": "有效钼","AS1": "有效硫","AZN": "有效锌","CEC": "阳离子交换量","ECA": "交换性钙",
"EMG": "交换性镁","TSE": "全硒","TN": "全氮","TP": "全磷","TK": "全钾","AFE": "有效铁","AK": "速效钾","AP": "有效磷", "TRRZ": "土壤容重","LSFD":"砾石丰度",
"OM": "有机质","FL": "粉粒含量","NL": "黏粒含量","SL": "砂粒含量","PH": "土壤 pH","YXTCHD": "有效土层厚度","GZCHD": "耕作层厚度","TRZD": "土壤质地","TRZD12": "土壤质地",
"三普PH": "三普PH","二普PH": "二普PH","测土PH": "测土PH","二普-三普": "二普-三普","测土-三普": "测土-三普","二普-测土": "二普-测土"
}
try:
for key in config_dict.keys():
in_key = every_string.split("_")[0]
if key == in_key:
return key
return ""
except Exception as e:
return ""
def parse_raster_standard(standard_str):
"""解析重分类标准字符串,返回数值范围
例如:
"2.00" -> (2.0, float('inf'))
"1.002.00" -> (1.0, 2.0)
"≤0.20" -> (0, 0.2)
"""
if "," in standard_str:
temp = []
parts = standard_str.split(",\n")
for part in parts:
temp_part = parse_raster_standard(part)
temp.append(temp_part)
return temp
if "" in standard_str:
value = float(standard_str.replace("", ""))
return (value, float('inf'))
elif "" in standard_str:
parts = standard_str.split("")
return (float(parts[0]), float(parts[1]))
elif "" in standard_str:
value = float(standard_str.replace("", ""))
return (0, value)
else:
# 尝试直接解析为数值
try:
value = float(standard_str)
return (value, value)
except ValueError:
return None
def create_remap_table(standards_dict):
"""根据标准配置创建重分类映射表
参数:
standards_config -- 标准配置,格式为:
{"标准1":5-6, "标准2":7-8, ...}
remap_values -- 重分类值数组默认为从1开始的整数序列
返回:
重分类映射表,格式为 [[old_min, old_max, new_value], ...]
"""
# 确保我们有一个有效的标准列表
if not standards_dict or not isinstance(standards_dict, dict):
print("警告: 没有有效的标准数据")
return []
# 设置重分类值
standards_length = len(standards_dict)
remap_values = list(range(1, 2*standards_length + 1))
remap_table = []
for i, (key, value) in enumerate(standards_dict.items()):
range_tuple = parse_raster_standard(value)
if range_tuple:
if type(range_tuple) is list:
m = 0
for range_tuple_item in range_tuple:
j = m * standards_length + i
remap_table.append([range_tuple_item[0], range_tuple_item[1], remap_values[j]])
m = m + 1
else:
remap_table.append([range_tuple[0], range_tuple[1], remap_values[i]])
return remap_table
def check_fields_exist_describe(feature_class, field_names):
"""
使用Describe函数检查要素类中字段是否存在
"""
try:
desc = arcpy.Describe(feature_class)
existing_fields = [field.name for field in desc.fields]
for field_name in field_names:
if field_name not in existing_fields:
return False
return True
except Exception as e:
print(f"检查字段时出错: {e}")
return None
def get_grade_by_standard(value, grade_standards):
"""
通用的等级判断函数
value: 数值
grade_standards: 分级标准字典,如 {"等级一": "2.00", "等级二": "1.002.00"}
"""
if value is None:
return "无数据"
# 按等级顺序检查(从高到低)
sorted_grades = sorted(grade_standards.items(),
key=lambda x: list(grade_standards.keys()).index(x[0]))
for grade_name, grade_standard in sorted_grades:
if is_value_in_grade(value, grade_standard):
return grade_name
return "超出范围"
def is_value_in_grade(value, grade_standard):
"""
判断数值是否在分级标准范围内
"""
# 处理特殊字符
grade_standard = grade_standard.replace('', '>').replace('', '<=').replace('', '~')
# 处理多范围情况如pH值
if ',' in grade_standard:
ranges = grade_standard.split(',')
for range_str in ranges:
if is_value_in_single_range(value, range_str.strip()):
return True
return False
else:
return is_value_in_single_range(value, grade_standard)
def is_value_in_single_range(value, range_str):
"""
判断数值是否在单个范围内
"""
import re
# 提取数值
numbers = re.findall(r'[-+]?\d*\.\d+|\d+', range_str)
numbers = [float(num) for num in numbers]
if '>' in range_str and '~' in range_str:
# 格式:>下限~上限
return numbers[0] < value <= numbers[1]
elif '>' in range_str:
# 格式:>数值
return value > numbers[0]
elif '<=' in range_str:
# 格式:<=数值
return value <= numbers[0]
elif '~' in range_str:
# 格式:下限~上限
return numbers[0] < value <= numbers[1]
else:
# 无法解析,使用字符串匹配
return str(value) == range_str
def vectorized_grade_assignment(values, grade_standards):
"""
向量化的等级分配(性能更好)
"""
# 确保输入值是数值类型,如果是字符串则转换为浮点数
if isinstance(values, np.ndarray) and values.dtype.kind in 'OUS': # 字符串类型
values = values.astype(float)
elif hasattr(values, 'dtype') and values.dtype == object: # 对象类型,可能包含字符串
values = values.astype(float)
conditions = []
choices = []
# 按等级顺序构建条件
# 创建两个列表来分别存储上段和下段范围
upper_ranges = []
lower_ranges = []
# 遍历排序后的等级
for i, (level, ranges) in enumerate(sorted(grade_standards.items(), key=lambda x: list(grade_standards.keys()).index(x[0])), 1):
# 分割范围字符串
range_list = [r.strip() for r in ranges.split(',')]
if len(range_list) >= 1:
upper_ranges.append((i, range_list[0]))
if len(range_list) >= 2:
# 计算下段范围的索引(原始索引 + 等级总数)
lower_index = i + len(grade_standards)
lower_ranges.append((lower_index, range_list[1]))
# 合并结果
sorted_grades = upper_ranges + lower_ranges
# sorted_grades = sorted(grade_standards.items(), key=lambda x: list(grade_standards.keys()).index(x[0]))
for grade_name, grade_standard in sorted_grades:
condition = create_condition(values, grade_standard)
conditions.append(condition)
choices.append(grade_name)
# 使用np.select进行向量化操作
result = np.select(conditions, choices, default="超出范围")
return result
def create_condition(values, grade_standard):
"""
创建numpy条件
"""
# 清理字符串:替换特殊字符并移除换行符和空格
grade_standard = (grade_standard.replace('', '>')
.replace('', '<=')
.replace('', '~')
.replace('\n', '') # 移除换行符
.replace(' ', '')) # 移除空格
if ',' in grade_standard:
# 多范围处理
ranges = grade_standard.split(',')
condition = None
for range_str in ranges:
if range_str: # 确保不是空字符串
range_condition = create_single_condition(values, range_str.strip())
if condition is None:
condition = range_condition
else:
condition = condition | range_condition
return condition
else:
return create_single_condition(values, grade_standard)
def create_single_condition(values, range_str):
"""
创建单个范围的条件
"""
import re
# 调试输出,帮助排查问题
# print(f"处理范围字符串: '{range_str}'")
# 提取数字
numbers = re.findall(r'[-+]?\d*\.\d+|\d+', range_str)
numbers = [float(num) for num in numbers]
if not numbers:
raise ValueError(f"无法从字符串 '{range_str}' 中提取数字")
# 根据范围符号创建条件
if '>' in range_str and '<=' in range_str:
# 处理 >x<=y 的情况(虽然不常见)
return (values > numbers[0]) & (values <= numbers[1])
elif '>' in range_str and '~' in range_str:
return (values > numbers[0]) & (values <= numbers[1])
elif '>' in range_str:
return values > numbers[0]
elif '<=' in range_str:
return values <= numbers[0]
elif '~' in range_str:
return (values > numbers[0]) & (values <= numbers[1])
else:
# 如果是单个数字
try:
return values == float(range_str)
except ValueError:
raise ValueError(f"无法解析的范围字符串: '{range_str}'")

View File

@@ -0,0 +1,59 @@
# utils/excel_utils.py
import re
from openpyxl.styles import Font, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.worksheet import Worksheet
class ExcelStyleUtils:
"""Excel样式工具类"""
@staticmethod
def set_style(
ws: Worksheet,
cell_range: str,
font: Font=Font(name='宋体', size=11),
align: Alignment=Alignment(horizontal='center', vertical='center', wrap_text=True),
border: Border=Border(left=Side(style='thin'), right=Side(style='thin'),top=Side(style='thin'), bottom=Side(style='thin'))):
"""设置单元格样式"""
if cell_range:
for row in ws[cell_range]:
for cell in row:
cell.font = font
cell.alignment = align
cell.border = border
@staticmethod
def auto_adjust_column_width(ws: Worksheet):
"""自动调整列宽"""
dims = {}
for row in ws.rows:
for cell in row:
if cell.value:
merged_range = next((range for range in ws.merged_cells.ranges if cell.coordinate in range), None)
if get_merge_type(merged_range) == 'column':
continue
cell_len = 0.7 * len(re.findall('([\u4e00-\u9fa5])', str(cell.value))) + len(str(cell.value))
dims[cell.column] = max(dims.get(cell.column, 0), cell_len)
# 设置列宽
for col, value in dims.items():
ws.column_dimensions[get_column_letter(int(col))].width = value + 5
# 判断单元格类型
def get_merge_type(merged_range):
"""
判断合并类型
返回: 'row'(行合并), 'column'(列合并), 'both'(行列合并)或 None不是合并单元格
"""
if not merged_range:
return None
min_row, max_row = merged_range.min_row, merged_range.max_row
min_col, max_col = merged_range.min_col, merged_range.max_col
if max_row > min_row and max_col > min_col:
return 'both' # 同时跨行和跨列
elif max_row > min_row:
return 'row' # 行合并(垂直合并)
elif max_col > min_col:
return 'column' # 列合并(水平合并)
else:
return None # 实际上不是合并单元格

View File

@@ -0,0 +1,147 @@
from typing import List, Union
import numpy as np
# 解决百分比相加不为100%
def fix_percentages(values: List[float], total: float) -> List[float]:
"""
修正百分比相加不为100%的问题。
Args:
values (list[float]): 百分比列表,元素个数与总和相同
total (float): 总和
Returns:
list[float]: 修正后的百分比列表
Examples:
>>> values = [0.2, 0.3, 0.5]
>>> total = 1
>>> fix_percentages(values, total)
[20.0, 30.0, 50.0]
>>> values = [0.2, 0.3, 0.5]
>>> total = 0.8
>>> fix_percentages(values, total)
[25.0, 37.5, 62.5]
"""
exact = [v / total * 100 for v in values]
floor = [np.floor(p * 100) / 100 for p in exact] # 向下取整到2位小数
remainders = [exact[i] - floor[i] for i in range(len(exact))]
# 需要分配的百分点数以0.01%为单位)
to_distribute = int(round(10000 - sum(floor) * 100))
# 按余数大小分配
indices = sorted(range(len(remainders)), key=lambda i: remainders[i], reverse=True)
fixed = floor.copy()
for i in range(to_distribute):
fixed[indices[i]] += 0.01
return [round(p, 2) for p in fixed]
# === 误差矫正 ===
def correct_rounding_error(target_total:Union[int,float], adjusted_areas:List[float], original_areas:List[float]) -> List[int]:
"""
健壮的数值舍入误差矫正函数:将浮点型面积值四舍五入后,调整至目标总和。
核心逻辑基于原始数值的小数部分优先级逐次增减1来抵消舍入误差确保最终总和匹配目标值
同时避免调整后数值出现负数,防止无限循环。
Args:
target_total (int/float): 目标总和(最终舍入后数值的合计值),函数内部会转为整型
adjusted_areas (list[float]): 经过比例调整后的浮点型面积列表(待舍入的原始数据)
original_areas (list[float]): 调整前的原始浮点型面积列表(用于计算小数部分优先级)
Returns:
list[int]: 矫正后的整型面积列表,总和尽可能接近/等于target_total
若无法完全矫正,返回尽可能接近的结果并打印警告
Raises:
无显式抛出异常,所有异常会被捕获并打印错误信息,返回保底的四舍五入结果
Notes:
1. 误差矫正规则:
- 误差>0当前总和 < 目标总和):优先给小数部分大的数值+1
- 误差<0当前总和 > 目标总和):优先给小数部分小的数值-1
2. 边界限制调整时确保数值≥0避免出现负数面积
3. 防无限循环:最大迭代次数为 len(adjusted_areas) * 10超出则终止并提示剩余误差
Examples:
>>> target = 10
>>> adjusted = [3.2, 2.8, 4.1] # 四舍五入后总和=3+3+4=10无误差
>>> original = [3.2, 2.8, 4.1]
>>> correct_rounding_error(target, adjusted, original)
[3, 3, 4]
>>> target = 10
>>> adjusted = [3.1, 2.1, 4.1] # 四舍五入后总和=3+2+4=9误差+1
>>> original = [3.1, 2.1, 4.1]
>>> correct_rounding_error(target, adjusted, original)
[3, 2, 5] # 优先给小数部分最大的4.1+1
>>> target = 8
>>> adjusted = [3.9, 2.9, 1.9] # 四舍五入后总和=4+3+2=9误差-1
>>> original = [3.9, 2.9, 1.9]
>>> correct_rounding_error(target, adjusted, original)
[3, 3, 2] # 优先给小数部分最小的1.9-1实际小数1.9>2.9>3.9故调整3.9
"""
try:
target_total = int(target_total)
rounded_areas = [int(round(area)) for area in adjusted_areas]
current_total = sum(rounded_areas)
error = target_total - current_total
if error == 0 or len(adjusted_areas) == 0:
return rounded_areas
# 使用循环分配直到误差为0或无法再分配
remaining_error = error
max_iterations = len(adjusted_areas) * 10 # 防止无限循环
for _ in range(max_iterations):
if remaining_error == 0:
break
# 每次迭代重新计算小数部分和排序
decimal_parts = [float(area - int(area)) for area in original_areas]
indices = list(range(len(adjusted_areas)))
if remaining_error > 0:
indices.sort(key=lambda i: decimal_parts[i], reverse=True)
adjustment = 1
else:
indices.sort(key=lambda i: decimal_parts[i])
adjustment = -1
# 尝试分配一次调整
adjusted = False
for idx in indices:
if (adjustment == 1 and rounded_areas[idx] >= 0) or (adjustment == -1 and rounded_areas[idx] > 0):
rounded_areas[idx] += adjustment
remaining_error -= adjustment
adjusted = True
break
if not adjusted: # 无法再调整
break
if remaining_error != 0:
print(f"警告:无法完全矫正误差,剩余: {remaining_error}")
return rounded_areas
except Exception as e:
print(f"误差矫正出错: {e}")
# 返回原始四舍五入结果作为保底
return [int(round(area)) for area in adjusted_areas]
if __name__ == '__main__':
target = 10
adjusted = [3.3, 3.9, 4.2] # 四舍五入后总和=3+3+4=10无误差
original = [3.25, 2.85, 4.15]
print(correct_rounding_error(target, adjusted, original))

View File

@@ -0,0 +1,47 @@
import gc
import sys
import arcpy
# 临时文件清理
def clean_up_temp_files(temp_files, workspace=None):
"""安全清理临时文件和内存工作空间"""
try:
if temp_files:
for temp_file in temp_files:
if arcpy.Exists(temp_file):
try:
arcpy.management.Delete(temp_file)
# print_status(f"已删除临时文件: {temp_file}")
except Exception as delete_err:
sys.stderr.write(f"CleanupError:无法删除临时文件 {temp_file}: {str(delete_err)}\n")
# 清理内存工作空间 (确保在 in_memory 工作空间中操作,而不是删除其他地方的同名项)
try:
# 切换到内存工作空间进行清理
if arcpy.Exists("in_memory"):
arcpy.env.workspace = "in_memory"
# 删除内存工作空间中的所有内容
for item in arcpy.ListDatasets() + arcpy.ListFeatureClasses() + arcpy.ListRasters():
try:
arcpy.management.Delete(item)
# print_status(f"已清理内存项: in_memory/{item}")
except Exception as delete_mem_item_err:
sys.stderr.write(f"CleanupError:无法清理内存项 in_memory/{item}: {str(delete_mem_item_err)}\n")
except Exception as delete_in_memory_err:
sys.stderr.write(f"CleanupError:清理 in_memory 工作空间时发生错误: {str(delete_in_memory_err)}\n")
# 恢复原始工作空间
if workspace and arcpy.Exists(workspace):
try:
arcpy.env.workspace = workspace
arcpy.management.ClearWorkspaceCache()
except Exception as restore_ws_err:
sys.stderr.write(f"CleanupError:无法恢复原始工作空间 {workspace}: {str(restore_ws_err)}\n")
except Exception as cleanup_err:
# 外层异常捕获
sys.stderr.write(f"CleanupError:清理临时文件过程中发生未预料的错误: {str(cleanup_err)}\n")
# 强制垃圾回收
gc.collect()

View File

@@ -0,0 +1,201 @@
# 获取每个一级地类面积主要是12类
import arcpy
import numpy as np
import pandas as pd
from .math_utils import correct_rounding_error
# 获取目标面积
def get_area_by_group(dltb_class_feature, excel_target_path, xzqmc, is_by_xzq=False):
try:
# 读取目标面积Excel文件
target_df = pd.read_excel(excel_target_path)
# 确保列名匹配
target_df.columns = target_df.columns.str.strip()
if is_by_xzq:
# 地类编码映射字典
land_type_mapping = {
'耕地': '01',
'园地': '02',
'林地': '03',
'草地': '04',
'其他地类': '12'
}
# 方法1重命名列后转换为字典
df_encoded = target_df.rename(columns=land_type_mapping)
result_dict = df_encoded.set_index('行政单位').to_dict('index')
return result_dict
# 检查要素类是否存在
if not arcpy.Exists(dltb_class_feature):
print(f"警告:输入要素类不存在: {dltb_class_feature}")
else:
# 转为numpy数组供pandas统计使用
df = pd.DataFrame(arcpy.da.TableToNumPyArray(dltb_class_feature, ["YJDLBM", "TBDLMJ"], skip_nulls=False, null_value=np.nan))
qtdl_df = df[df['YJDLBM'] == '12']
if qtdl_df['TBDLMJ'].isnull().any() or qtdl_df['TBDLMJ'].eq(0).any():
print("警告其他地类TBDLMJ字段 存在空值或无效的记录,将不平差其他地类")
target_areas = {}
else:
area_by_group = df.groupby("YJDLBM")["TBDLMJ"].sum()
for key in area_by_group.keys():
area_by_group[key] = area_by_group[key] * 0.0015
target_areas = area_by_group.to_dict()
# 获取铁山港区的目标面积
gangnan_target = target_df[target_df['行政单位'] == xzqmc]
if gangnan_target.empty:
print(f"警告:未找到{xzqmc}的目标面积数据,将使用TBDLMJ数据进行平差")
return target_areas
# 提取各土地利用类型的目标面积
landuse_types = {'01':'耕地', '02':'园地', '03':'林地', '04':'草地', '12':'其他地类'}
for dlbm, dlmc in landuse_types.items():
if dlmc in gangnan_target.columns:
if gangnan_target[dlmc].values[0] and not np.isnan(gangnan_target[dlmc].values[0]):
target_areas[dlbm] = gangnan_target[dlmc].values[0]
return target_areas
except Exception as e:
print(f"计算面积时出错: {str(e)}")
return {}
# 按地类平差(全区统一平差)
def adjust_area_statistics(stats_df, target_areas):
"""
根据Excel中的目标面积对统计数据进行平差处理
Parameters:
stats_df: 原始统计数据DataFrame
excel_target_path: 包含目标面积的Excel文件路径
Returns:
adjusted_df: 平差后的DataFrame
"""
try:
if target_areas is None:
print("警告:目标面积数据为空,不进行平差")
return stats_df
# 准备平差数据
adjusted_df = stats_df.copy()
if "YJDLBM" not in adjusted_df.columns:
dlbm = "YNDLBM"
else:
dlbm = "YJDLBM"
adjusted_df['adjusted_area'] = adjusted_df['temp_area']
adjusted_df['adjustment_factor'] = 1.0
# 计算每个地类的原始总面积
original_totals = stats_df.groupby(dlbm)['temp_area'].sum().to_dict()
# 对每个地类进行平差
for yjdl, target_area in target_areas.items():
if (yjdl in original_totals and original_totals[yjdl] > 0) or target_area > 0:
adjustment_factor = target_area / original_totals[yjdl]
# 应用平差系数
mask = adjusted_df[dlbm] == yjdl
adjusted_df.loc[mask, 'adjusted_area'] = adjusted_df.loc[mask, 'temp_area'] * adjustment_factor
adjusted_df.loc[mask, 'adjustment_factor'] = adjustment_factor
# 应用误差矫正,确保总和等于目标值
adjusted_areas = adjusted_df.loc[mask, 'adjusted_area'].tolist()
original_areas = stats_df.loc[mask, 'temp_area'].tolist()
corrected_areas = correct_rounding_error(target_area, adjusted_areas, original_areas)
adjusted_df.loc[mask, 'adjusted_area'] = corrected_areas
print(f"地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}, 目标面积 = {target_area}, 矫正后总面积 = {sum(corrected_areas)}")
return adjusted_df
except Exception as e:
print(f"平差处理失败: {e}")
return stats_df
# 按行政区+地类进行平差
def adjust_by_district_landuse(stats_df:pd.DataFrame, target_areas_dict:dict):
"""
按行政区+地类进行平差
Parameters:
stats_df: 原始统计数据DataFrame
target_areas_dict: 目标面积字典,格式:{'行政区': {'地类': 目标面积}}
Returns:
adjusted_df: 平差后的DataFrame
"""
# 复制原始数据
adjusted_df = stats_df.copy()
adjusted_df['adjusted_area'] = adjusted_df['temp_area']
adjusted_df['adjustment_factor'] = 1.0
# 获取所有存在的行政区和地类
existing_districts = adjusted_df['XZQMC'].unique()
# 检查目标字典中的行政区是否存在
missing_districts = []
tt = [td for td in target_areas_dict.keys()]
for ed in existing_districts:
if ed not in tt:
missing_districts.append(ed)
# 如果有行政区不存在,返回原始数据并提示
if missing_districts:
print(f"警告:平差数据中不存在行政区: {missing_districts},未进行平差")
return stats_df
# 计算每个行政区每个地类的原始总面积
original_totals = stats_df.groupby(['XZQMC', 'YJDLBM'])['temp_area'].sum()
# 对每个行政区的每个地类进行平差
for xzqmc, landuse_targets in target_areas_dict.items():
for yjdl, target_area in landuse_targets.items():
# 检查该行政区是否有此地类数据
if (xzqmc, yjdl) in original_totals.index and original_totals.at[(xzqmc, yjdl)] > 0:
adjustment_factor = target_area / original_totals[(xzqmc, yjdl)]
# 应用平差系数
mask = (adjusted_df['XZQMC'] == xzqmc) & (adjusted_df['YJDLBM'] == yjdl)
adjusted_df.loc[mask, 'adjusted_area'] = adjusted_df.loc[mask, 'temp_area'] * adjustment_factor
adjusted_df.loc[mask, 'adjustment_factor'] = adjustment_factor
# 应用误差矫正,确保总和等于目标值
adjusted_areas = adjusted_df.loc[mask, 'adjusted_area'].tolist()
original_areas = stats_df.loc[mask, 'temp_area'].tolist()
corrected_areas = correct_rounding_error(target_area, adjusted_areas, original_areas)
adjusted_df.loc[mask, 'adjusted_area'] = corrected_areas
print(f"{xzqmc} - 地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}, 目标面积 = {target_area}, 矫正后总面积 = {sum(corrected_areas)}")
return adjusted_df
def get_target_areas(excel_path:str, sheet_name:str, xzqmc:str) -> pd.DataFrame:
df_excel = pd.read_excel(excel_path, sheet_name)
target_df = df_excel[df_excel['行政单位'] == xzqmc]
df_area_for_merge = target_df.set_index('行政单位').iloc[0].reset_index(name='面积').rename(columns={'index': 'EJDL'})
return df_area_for_merge
def get_target_areas_by_group(excel_target_path):
# 读取目标面积Excel文件
target_df = pd.read_excel(excel_target_path,"Sheet1")
# 确保列名匹配
target_df.columns = target_df.columns.str.strip()
result_dict = target_df.set_index('行政单位').to_dict('index')
return result_dict

1
tools/ui/__init__.py Normal file
View File

@@ -0,0 +1 @@
# UI界面模块

View File

@@ -0,0 +1 @@
# UI界面模块

View File

@@ -0,0 +1,67 @@
'''
pyqt6 文件列表分组
'''
from PyQt6.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QPushButton, QListWidget, QGroupBox
from PyQt6.QtCore import pyqtSignal
class FileListGroup(QGroupBox):
load_files = pyqtSignal()
def __init__(self, parent=None, title="文件列表"):
super().__init__(title, parent)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout(self)
# 文件列表
self.file_list = QListWidget()
self.file_list.setSelectionMode(QListWidget.SelectionMode.MultiSelection)
layout.addWidget(self.file_list)
# 文件列表操作按钮
file_list_btn_layout = QHBoxLayout()
# 加载文件按钮
self.load_file_btn = QPushButton("加载文件")
self.load_file_btn.clicked.connect(self.on_load_files)
# 全选/全不选按钮
select_all_btn = QPushButton("全选")
select_all_btn.clicked.connect(self.on_select_all_files)
select_none_btn = QPushButton("全不选")
select_none_btn.clicked.connect(self.on_select_none_files)
# 添加按钮到布局
file_list_btn_layout.addWidget(self.load_file_btn)
file_list_btn_layout.addWidget(select_all_btn)
file_list_btn_layout.addWidget(select_none_btn)
layout.addLayout(file_list_btn_layout)
def on_load_files(self):
# 通过信号槽机制,添加文件列表
self.load_files.emit()
def on_select_all_files(self):
# 全选文件
for i in range(self.file_list.count()):
item = self.file_list.item(i)
if item:
item.setSelected(True)
def on_select_none_files(self):
# 全不选文件
for i in range(self.file_list.count()):
item = self.file_list.item(i)
if item:
item.setSelected(False)
if __name__ == '__main__':
app = QApplication([])
app.setStyle("Fusion")
window = FileListGroup()
window.show()
app.exec()

View File

@@ -0,0 +1,64 @@
from PyQt6.QtWidgets import (QGroupBox, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QFileDialog)
class InputGroup(QGroupBox):
"""输入设置组件"""
def __init__(self, title="输入设置", parent=None):
super().__init__(title, parent)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout(self)
# 模板文件选择
template_layout = QHBoxLayout()
template_layout.addWidget(QLabel("配置文件:"))
self.template_path = QLineEdit()
template_layout.addWidget(self.template_path)
template_btn = QPushButton("浏览...")
template_btn.clicked.connect(self.browse_template)
template_layout.addWidget(template_btn)
layout.addLayout(template_layout)
# 数据源选择
data_layout = QHBoxLayout()
data_layout.addWidget(QLabel("数据源:"))
self.data_source = QLineEdit()
data_layout.addWidget(self.data_source)
data_btn = QPushButton("浏览...")
data_btn.clicked.connect(self.browse_data_source)
data_layout.addWidget(data_btn)
layout.addLayout(data_layout)
def browse_template(self):
"""浏览选择模板文件"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择模板文件",
"",
"ArcGIS Pro Project (*.aprx);;All Files (*.*)"
)
if file_path:
self.template_path.setText(file_path)
def browse_data_source(self):
"""浏览选择数据源"""
dir_path = QFileDialog.getExistingDirectory(
self,
"选择数据源目录"
)
if dir_path:
self.data_source.setText(dir_path)
def get_template_path(self):
"""获取模板文件路径"""
return self.template_path.text()
def get_data_source(self):
"""获取数据源路径"""
return self.data_source.text()
def clear(self):
"""清除输入"""
self.template_path.clear()
self.data_source.clear()

View File

View File

@@ -0,0 +1,60 @@
{
"export_map_settings": {
"config_file": "D:/ProgramData/ArcGis_Py/tools/config_json/广西_华南_地块_config.json",
"county_name": "武鸣区",
"template_aprx_file": "E:/@三普属性图出图/广西武鸣区/武鸣区模板/广西武鸣区中微量元素(地块).aprx",
"output_path": "E:/@三普属性图出图/广西武鸣区/成果图/20260411/中微量元素",
"data_source_path": "E:/@三普属性图出图/广西武鸣区/过程数据/三普重分类/按地块出图数据.gdb",
"symbol_path": "E:/@三普属性图出图/@通用数据/符号系统/广西_华南配色",
"pic_path": "E:/@三普属性图出图/广西武鸣区/过程数据/三普重分类/新建文件夹"
},
"export_image_settings": {
"input_aprx_path": "E:\\@三普属性图出图\\广西武鸣区\\成果图\\20260411\\中微量元素\\武鸣区_工作空间",
"output_image_path": "E:\\@三普属性图出图\\广西武鸣区\\成果图\\20260411\\中微量元素",
"default_format": "JPG",
"resolution": 300,
"force_regenerate": false,
"use_multiprocessing": true,
"process_count": 7
},
"raster_settings": {
"input_folder": "E:/@三普属性图出图/广西武鸣区/@基础数据/三普栅格/投影后",
"batch_output_folder": "E:/@三普属性图出图/广西武鸣区/过程数据/三普重分类/新建文件夹",
"config_file_path": "D:/ProgramData/ArcGis_Py/tools/config_json/广西_华南_地块_config.json",
"simplify": false,
"min_area": 10000.0,
"area_unit": "SQUARE_METERS",
"clip_features": "",
"clip_enabled": false
},
"area_stat_settings": {
"input_folder": "E:/@三普属性图出图/广西银海区/过程数据/面积统计用栅格面",
"batch_output_folder": "E:/@三普属性图出图/测试",
"config_file_path": "D:/ProgramData/ArcGis_Py/tools/config_json/广西_华南_地块_config.json",
"xzq_features": "E:\\@三普属性图出图\\广西银海区\\银海区土壤属性制表\\土壤属性制表数据.gdb\\行政区划",
"dltb_features": "E:\\@三普属性图出图\\广西银海区\\银海区土壤属性制表\\土壤属性制表数据.gdb\\地类图斑",
"xzqmc": "西畴县",
"is_by_xzq": false
},
"acid_stat_settings": {
"workspace_path": "E:/@三普属性图出图/广西银海区/酸化专题图/酸化专题数据库.gdb",
"batch_output_folder": "E:/@三普属性图出图/广西银海区/酸化专题图",
"xzq_features": "行政区划",
"dltb_features": "地类图斑",
"ph_samples": "三普PH",
"acid_ph_features": "二普至三普PH",
"assign_raster": "E:/@三普属性图出图/广西北海市/@基础数据/二普栅格/投影后/PH.tif",
"acid_raster": "E:/@三普属性图出图/广西北海市/北海市土壤属性制表/历史变化栅格/新建文件夹/二普-三普_PH.tif",
"soil_type_features": "土壤类型图",
"xzqmc": ""
},
"soil_prop_stat_settings": {
"xzqmc": "",
"config_file": "D:/ProgramData/ArcGis_Py/tools/config_json/广西_华南_地块_config.json",
"data_source_path": "E:/@三普属性图出图/广西武鸣区/武鸣区报告图表/制表数据.gdb",
"sanpu_prop_tif_folder": "E:/@三普属性图出图/广西武鸣区/@基础数据/三普栅格/投影后",
"reclassed_feature_folder": "E:/@三普属性图出图/广西武鸣区/过程数据/三普重分类/面积统计用栅格面",
"output_folder": "E:/@三普属性图出图/广西武鸣区/武鸣区报告图表",
"sample_list": []
}
}

291
tools/ui/main_window.py Normal file
View File

@@ -0,0 +1,291 @@
import traceback
import json
import os
import sys
from PyQt6.QtWidgets import (
QMainWindow,
QWidget,
QVBoxLayout,
QTabWidget,
QTextEdit,
QLabel,
QPushButton,
QHBoxLayout,
)
from PyQt6.QtCore import pyqtSignal, QThreadPool
from PyQt6.QtGui import QTextCursor
from .tabs.export_layout_tab import ExportImageTab
from .tabs.raster_tab import RasterTab
from .tabs.export_map_tab import ExportMapTab
from .tabs.area_stat_tab import XlsxToJpgTab
from .tabs.acid_stat_tab import AcidStatsTab # 酸化专题图表格统计
from .tabs.soil_prop_stat_tab import SoilPropStatsTab
class MainWindow(QMainWindow):
log_signal = pyqtSignal(str) # 定义日志信号
"""主窗口"""
def __init__(self):
super().__init__()
self.setWindowTitle("ArcGIS工具集")
self.resize(600, 800)
self.settings_file = os.path.join(
os.path.dirname(__file__), "config", "settings.json"
)
print(self.settings_file)
self.ensure_settings_file()
self.settings = self.load_settings()
self.init_ui()
# 在创建UI后加载设置
self.load_ui_settings()
def init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 添加选项卡
tabs = QTabWidget()
self.export_map_tab = ExportMapTab(self)
self.export_map_tab.log_signal.connect(self.log_signal) # 连接日志信号
self.export_image_tab = ExportImageTab(self)
self.raster_tab = RasterTab(self)
self.area_stat_tab = XlsxToJpgTab(self)
self.acid_stat_tab = AcidStatsTab(self)
self.soil_prop_stat_tab = SoilPropStatsTab(self)
tabs.addTab(self.raster_tab, "栅格重分类")
tabs.addTab(self.area_stat_tab, "面积统计表制作")
tabs.addTab(self.export_map_tab, "导出工程文件")
tabs.addTab(self.export_image_tab, "导出成果图")
tabs.addTab(self.acid_stat_tab, "酸化专题图表格统计")
tabs.addTab(self.soil_prop_stat_tab, "土壤属性表制作")
main_layout.addWidget(tabs)
# 添加日志区域
self.log_area = QTextEdit()
self.log_area.setReadOnly(True)
self.log_area.setFixedHeight(120)
self.log_signal.connect(self.append_log) # 日志信号连接
main_layout.addWidget(QLabel("日志输出"))
main_layout.addWidget(self.log_area)
# 添加清除日志按钮
log_control_layout = QHBoxLayout()
clear_log_btn = QPushButton("清空日志")
clear_log_btn.clicked.connect(self.clear_log)
log_control_layout.addStretch()
log_control_layout.addWidget(clear_log_btn)
main_layout.addLayout(log_control_layout)
# 连接信号与槽
self.raster_tab.value_changed.connect(self.export_map_tab.update_config_file)
self.raster_tab.value_changed.connect(self.area_stat_tab.update_config_file)
self.raster_tab.value_changed.connect(self.soil_prop_stat_tab.update_config_file)
def ensure_settings_file(self):
"""确保配置文件存在,如不存在则创建"""
if not os.path.exists(self.settings_file):
default_settings = self.get_default_settings()
try:
os.makedirs(os.path.dirname(self.settings_file), exist_ok=True)
with open(self.settings_file, "w", encoding="utf-8") as f:
json.dump(default_settings, f, indent=4, ensure_ascii=False)
except Exception as e:
print(f"创建配置文件失败: {str(e)}")
def load_settings(self):
"""加载设置"""
try:
if os.path.exists(self.settings_file):
with open(self.settings_file, "r", encoding="utf-8") as f:
return json.load(f)
return self.get_default_settings()
except Exception as e:
print(f"加载设置失败: {str(e)}")
return self.get_default_settings()
def save_settings(self):
"""保存设置"""
try:
config_dir = os.path.dirname(self.settings_file)
if not os.path.exists(config_dir):
os.makedirs(config_dir, exist_ok=True)
with open(self.settings_file, "w", encoding="utf-8") as f:
json.dump(self.settings, f, indent=4, ensure_ascii=False)
print("配置已更新")
except Exception as e:
print(f"保存设置失败: {str(e)}")
def get_default_settings(self):
"""获取默认设置"""
return {
"export_map_settings": {
"template_aprx_file": "D:/工作/ArcGisPro/模板/工程模板.aprx",
"output_path": "D:/工作/ArcGisPro/输出文件夹",
"data_source_path": "D:/工作/ArcGisPro/数据源/土壤属性图矢量数据.gdb",
"config_file": "D:/arcpystudy/ArcGisPro/tools/ui/raster_test_config.json",
"county_name": "港南区",
"symbol_path": "D:/工作/ArcGisPro/模板/符号系统",
},
"export_image_settings": {
"input_aprx_path": "D:/工作/ArcGisPro/输出文件夹/港南区_工作空间",
"output_image_path": "D:/工作/ArcGisPro/输出文件夹",
"default_format": "PDF",
"resolution": 300,
'force_regenerate': True,
'use_multiprocessing': True,
'process_count': 3
},
"raster_settings": {
"input_folder": "D:/工作/三普成果编制/港南区土壤属性图成果最终",
"batch_output_folder": "D:/工作/ArcGisPro/输出文件夹/港南区_工作空间",
"config_file_path": "D:/arcpystudy/ArcGisPro/tools/ui/raster_test_config.json",
"simplify": True,
"min_area": 10000,
"area_unit": "平方米",
},
"area_stat_settings": {
"input_folder": "D:/工作/三普成果编制/港南区土壤属性图成果最终",
"batch_output_folder": "D:/工作/ArcGisPro/输出文件夹/港南区_工作空间",
"config_file_path": "D:/arcpystudy/ArcGisPro/tools/ui/raster_test_config.json",
"dltb_polygon": "D:/工作/三普成果编制/港南区土壤属性图成果最终/港南区_地类图_2021-09-01.shp",
"xzq_polygon": "D:/工作/三普成果编制/港南区土壤属性图成果最终/港南区_区县图_2021-09-01.shp",
},
}
def closeEvent(self, event):
"""窗口关闭处理"""
thread_pool = QThreadPool.globalInstance()
if thread_pool is not None:
thread_pool.waitForDone(2000)
self.update_settings()
self.save_settings()
super().closeEvent(event)
def update_settings(self):
"""更新设置"""
try:
# 更新导出工程设置
if hasattr(self.export_map_tab, "get_settings"):
export_map_settings = self.export_map_tab.get_settings()
if export_map_settings:
self.settings["export_map_settings"] = export_map_settings
# 更新导出成果图设置
if hasattr(self.export_image_tab, "get_layout_settings"):
raster_settings = self.export_image_tab.get_layout_settings()
if raster_settings:
self.settings["export_image_settings"]= raster_settings
# 更新栅格处理设置
if hasattr(self.raster_tab, "get_raster_settings"):
raster_settings = self.raster_tab.get_raster_settings()
if raster_settings:
self.settings["raster_settings"]= raster_settings
# 更新面积统计设置
if hasattr(self.area_stat_tab, "get_area_stat_settings"):
area_stat_settings = self.area_stat_tab.get_area_stat_settings()
if area_stat_settings:
self.settings["area_stat_settings"]= area_stat_settings
# 更新酸化统计设置
if hasattr(self.acid_stat_tab, "get_acid_stat_settings"):
acid_stat_settings = self.acid_stat_tab.get_acid_stat_settings()
if acid_stat_settings:
self.settings["acid_stat_settings"]= acid_stat_settings
# 更新土壤属性统计设置
if hasattr(self.soil_prop_stat_tab, "get_soil_prop_stat_settings"):
soil_prop_stat_settings = self.soil_prop_stat_tab.get_soil_prop_stat_settings()
if soil_prop_stat_settings:
self.settings["soil_prop_stat_settings"]= soil_prop_stat_settings
except Exception as e:
print(f"更新设置失败: {str(e)}")
# 回溯
traceback.print_exc()
def load_ui_settings(self):
"""加载UI设置到各个组件"""
try:
# 加载成果图导出标签页设置
if hasattr(self.export_image_tab, "load_settings"):
self.export_image_tab.load_settings()
# 加载栅格处理标签页设置
if hasattr(self.raster_tab, "load_settings"):
self.raster_tab.load_settings(self.settings["raster_settings"])
# 加载面积统计标签页设置
if hasattr(self.area_stat_tab, "load_settings"):
self.area_stat_tab.load_settings(self.settings["area_stat_settings"])
# 加载酸化专题统计标签页设置
if hasattr(self.acid_stat_tab, "load_settings"):
self.acid_stat_tab.load_settings(self.settings["acid_stat_settings"])
# 加载土壤属性统计标签页设置
if hasattr(self.soil_prop_stat_tab, "load_settings"):
self.soil_prop_stat_tab.load_settings(self.settings["soil_prop_stat_settings"])
except Exception as e:
print(f"加载UI设置失败: {str(e)}")
def append_log(self, message):
"""在日志区域追加信息"""
try:
from datetime import datetime
timestamp = datetime.now().strftime("%H:%M:%S")
# 如果传入字符串中已存在时间戳,则不添加时间戳
if "[" in message and "]" in message:
formatted_message = message
else:
formatted_message = f"[{timestamp}] {message}"
# 根据消息类型设置不同颜色
if "错误" in message or "失败" in message or "出错" in message:
formatted_message = f'<span style="color: #e74c3c;">{formatted_message}</span>'
elif "警告" in message:
formatted_message = f'<span style="color: #f39c12;">{formatted_message}</span>'
elif "成功" in message or "完成" in message:
formatted_message = f'<span style="color: #27ae60;">{formatted_message}</span>'
else:
formatted_message = f'<span style="color: #34495e;">{formatted_message}</span>'
self.log_area.append(formatted_message)
self.log_area.verticalScrollBar().setValue(
self.log_area.verticalScrollBar().maximum()
)
# 如果消息太多,清除旧消息
if self.log_area.document().lineCount() > 500:
cursor = self.log_area.textCursor()
cursor.movePosition(QTextCursor.MoveOperation.Start)
cursor.movePosition(
QTextCursor.MoveOperation.Down, QTextCursor.MoveMode.KeepAnchor, 100
)
cursor.removeSelectedText()
except Exception as e:
print(f"追加日志失败: {str(e)}")
def clear_log(self):
"""清空日志区域"""
self.log_area.clear()
# 如果直接运行该模块
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

View File

View File

@@ -0,0 +1,475 @@
# -*- coding: utf-8 -*-
"""
脚本运行器 (支持多进程)
用于从PyQt6界面调用独立脚本并管理并行执行
整合了多进程功能。
"""
import os
import sys
import json
import subprocess
import traceback
import uuid
from PyQt6.QtCore import (QObject, pyqtSignal, QProcess, QProcessEnvironment)
from pathlib import Path # 导入Path
import functools # 导入 functools 用于偏函数
class ScriptRunner(QObject):
"""脚本运行器类,用于调用独立脚本处理任务并管理并行执行"""
# 定义信号 (包含任务ID)
# 将原始的 started, finished, error, log 信号修改为包含任务ID
task_started = pyqtSignal(str, str) # 任务ID, 任务描述/消息
task_finished = pyqtSignal(str, bool, str) # 任务ID, 是否成功, 消息 (包含结果或错误)
task_error = pyqtSignal(str, str) # 任务ID, 错误信息
task_log = pyqtSignal(str, str) # 任务ID, 日志信息
manager_log = pyqtSignal(str) # Runner 管理器自身的日志
def __init__(self, parent=None, max_concurrent=3):
super().__init__(parent)
self.max_concurrent = max_concurrent # 控制最大并行进程数
self.running_processes = {} # {task_id: QProcess实例}
self.pending_tasks = ([]) # [(task_id, script_path, args_dict, task_description, working_dir), ...]
self._next_task_id_counter = 0 # 用于生成简单的任务ID (uuid 更推荐)
# 获取项目根目录
self.base_dir = self._get_base_dir()
self.manager_log.emit(f"项目根目录: {self.base_dir}")
self.manager_log.emit(f"最大并行任务数: {self.max_concurrent}")
def _get_base_dir(self):
"""获取项目根目录"""
# 根据你提供的项目结构图调整这里的逻辑
# 假设 script_runner.py 位于 ui/runners 目录下
current_dir = os.path.dirname(os.path.abspath(__file__))
# 向上两级找到项目根目录 (runners <- ui <- project_root)
# 调整为向上两级,因为 ui 和 tools 在同一级
return os.path.dirname(os.path.dirname(current_dir)) # 假设 runners 在 ui 目录下
def _find_script(self, script_name):
"""查找脚本文件"""
# 根据你提供的项目结构图调整可能的脚本路径
base_dir = Path(self.base_dir)
possible_paths = [
base_dir / "tools" / "core" / script_name,
Path(__file__).resolve().parents[2] / "core" / script_name,
Path.cwd() / "tools" / "core" / script_name
]
for path in possible_paths:
path = os.path.normpath(path)
if os.path.exists(path):
self.manager_log.emit(f"找到脚本: {path}")
return path
self.manager_log.emit(f"警告: 无法找到脚本 '{script_name}', 尝试过以下路径:")
for path in possible_paths:
self.manager_log.emit(f" - {path} (存在: {os.path.exists(path)})")
# 返回None表示找不到
return None
def _get_arcgis_python(self):
"""获取ArcGIS Pro的Python解释器路径"""
# 这里的逻辑与之前相同
try:
arcgis_python_paths = [
r"C:\Program Files\ArcGIS\Pro\bin\Python\envs\arcgispro-py3\python.exe",
r"D:\ProgramData\ArcGis_Py\.env\arcgispro-py3-clone\python.exe",
]
# 检查环境变量 ARCGISPRO_PYTHON 或 CONDA_PREFIX (如果使用Conda环境)
if "ARCGISPRO_PYTHON" in os.environ:
arcgis_python_paths.insert(0, os.environ["ARCGISPRO_PYTHON"])
for path in arcgis_python_paths:
path = os.path.normpath(path)
if os.path.exists(path):
self.manager_log.emit(f"使用ArcGIS Python解释器: {path}")
return path
self.manager_log.emit(
"警告: 无法自动找到ArcGIS Pro Python解释器尝试使用当前Python解释器."
)
return sys.executable
except Exception as e:
self.manager_log.emit(f"获取ArcGIS Python路径失败: {str(e)}")
return sys.executable
# 修正:将添加任务到队列并尝试启动的逻辑命名为 run_script
def run_script(self, script_name, args_dict, task_description=None, working_dir=None):
"""
将一个脚本任务添加到队列中进行管理和并行执行。
Args:
script_name (str): 要执行的脚本文件名。
args_dict (dict): 传递给脚本的参数字典。
task_description (str, optional): 任务的描述。
working_dir (str, optional): 脚本执行的工作目录。
返回:
str: 分配给该任务的唯一任务ID 或 None 如果脚本不存在。
"""
script_path = self._find_script(script_name)
if not script_path or not os.path.exists(script_path):
self.manager_log.emit(f"错误: 无法添加任务,脚本 '{script_name}' 不存在.")
return None
# 生成唯一的任务ID
task_id = str(uuid.uuid4().hex[:8])
if task_description is None:
task_description = os.path.basename(script_name)
# 将任务信息添加到待处理队列
self.pending_tasks.append((task_id, script_path, args_dict, task_description, working_dir))
self.manager_log.emit(f"已添加任务{task_id} - {task_description}到队列...")
# 尝试启动下一个任务
self._start_next_task()
return task_id
# 新增方法:启动下一个可用的任务
def _start_next_task(self):
"""检查队列和正在运行的进程数量,启动下一个可用的任务"""
while len(self.running_processes) < self.max_concurrent and self.pending_tasks:
task_id, script_path, args_dict, task_description, working_dir = (self.pending_tasks.pop(0))
self.manager_log.emit(f"正在启动任务: {task_id} - {task_description}")
self.task_started.emit(task_id, task_description)
# 调用内部方法实际启动单个进程
self._start_single_process(task_id, script_path, args_dict, task_description, working_dir)
# 新增方法:实际启动单个 QProcess 进程
def _start_single_process(self, task_id, script_path, args_dict, task_description, working_dir):
"""
启动一个 QProcess 进程来执行指定的脚本任务。
这是从原始 run_script 中提取的启动逻辑。
"""
python_executable = self._get_arcgis_python()
if not python_executable or not os.path.exists(python_executable):
error_msg = f"无法找到有效的 ArcGIS Pro Python 解释器: {python_executable or '未找到'}"
self.task_error.emit(task_id, error_msg)
self._on_process_finished(task_id, None, -1, QProcess.ExitStatus.NormalExit) # 模拟失败完成
return
# 构建环境变量
env = QProcessEnvironment.systemEnvironment()
env.insert("PATH", os.environ["PATH"]) # 继承系统PATH
env.insert("PYTHONPATH", ";".join(sys.path)) # 继承当前Python路径
cmd = [python_executable, "-u", script_path] # 使用 -u 参数禁用缓冲
# 将参数字典转换为命令行参数
for key, value in args_dict.items():
if not isinstance(key, str) or not key:
self.task_error.emit(task_id, f"警告: 跳过无效的参数键: {key}")
continue
param_key = f"--{key}"
if isinstance(value, bool):
if value:
cmd.append(param_key)
elif value is not None and value != "NONE":
cmd.append(param_key)
if isinstance(value, (list, dict)):
try:
cmd.append(json.dumps(value))
except Exception as e:
self.task_error.emit(task_id, f"警告: 无法将参数 '{key}' 序列化为 JSON: {str(e)}")
cmd.append(str(value))
else:
cmd.append(str(value))
self.task_log.emit(task_id, f"即将执行命令: {subprocess.list2cmdline(cmd)}")
proc = QProcess()
proc.setProcessEnvironment(env) # 关键设置
# 连接信号到处理函数,使用 functools.partial 传递 task_id 和 process 对象
# 注意_handle_stdout/_handle_stderr 信号本身不传递 process需要通过 task_id 从 running_processes 获取
# _on_process_finished 信号传递 exit_code, exit_status通过 partial 传递 task_id 和 proc
proc.readyReadStandardOutput.connect(
functools.partial(self._handle_stdout, task_id=task_id)
)
proc.readyReadStandardError.connect(
functools.partial(self._handle_stderr, task_id=task_id)
)
proc.finished.connect(
functools.partial(self._on_process_finished, task_id=task_id, proc=proc)
)
# 设置工作目录
if working_dir:
if os.path.exists(working_dir):
proc.setWorkingDirectory(working_dir)
self.task_log.emit(task_id, f"工作目录设置为: {working_dir}")
else:
self.task_error.emit(task_id, f"警告: 工作目录不存在: {working_dir}")
# 启动进程
try:
proc.start(cmd[0], cmd[1:])
self.running_processes[task_id] = proc # 将进程实例添加到正在运行列表
self.task_log.emit(task_id, f"进程已启动PID: {proc.processId()}")
# 可以将任务描述等信息附加到 QProcess 对象,方便在槽函数中使用
proc.setProperty("task_description", task_description)
except Exception as e:
error_msg = f"无法启动进程: {str(e)}\n命令: {subprocess.list2cmdline(cmd)}"
self.task_error.emit(task_id, error_msg)
# 在启动失败时,也需要调用 finished 槽来清理并触发下一个任务
self._on_process_finished(task_id, proc, -1, QProcess.ExitStatus.NormalExit) # 模拟失败完成
# 修改处理槽函数以接受 task_id 并从 running_processes 获取进程
def _handle_stdout(self, task_id):
"""处理指定任务的标准输出"""
proc = self.running_processes.get(task_id)
if not proc:
return # 如果进程已不在运行列表中,忽略信号
try:
data = proc.readAllStandardOutput().data()
# 尝试多种解码方式,确保能处理中文
try:
text = data.decode("utf-8")
except UnicodeDecodeError:
try:
text = data.decode("gbk") # Windows 常用
except UnicodeDecodeError:
text = data.decode("utf-8", errors="replace") # 保底
if text:
for line in text.splitlines():
if line.strip():
# 根据行的前缀进一步解析,例如 STATUS: 或 RESULT:
if line.startswith("STATUS:"):
status_message = line[len("STATUS:") :].strip()
self.task_log.emit(task_id, f"状态: {status_message}")
elif line.startswith("RESULT:"):
self.task_log.emit(task_id, f"原始结果行: {line.strip()}")
proc.setProperty("last_result_line", line.strip()) # 将结果行附加到 QProcess 对象
else:
self.task_log.emit(task_id, line.strip()) # 其他普通输出
except Exception as e:
self.task_error.emit(task_id, f"处理标准输出时出错: {str(e)}")
self.task_log.emit(task_id, traceback.format_exc()) # 记录详细错误
def _handle_stderr(self, task_id):
"""处理指定任务的错误输出"""
proc = self.running_processes.get(task_id)
if not proc:
return # 如果进程已不在运行列表中,忽略信号
try:
data = proc.readAllStandardError().data()
# 尝试多种解码方式
try:
text = data.decode("utf-8")
except UnicodeDecodeError:
try:
text = data.decode("gbk") # Windows 常用
except UnicodeDecodeError:
text = data.decode("utf-8", errors="replace") # 保底
if text:
for line in text.splitlines():
if line.strip():
# 错误信息直接作为错误日志发送
self.task_error.emit(task_id, f"脚本错误: {line.strip()}")
# 可以在这里将错误输出附加到 QProcess 对象,以便在 finished 中汇总
current_stderr = proc.property("accumulated_stderr") or ""
proc.setProperty("accumulated_stderr", current_stderr + line.strip() + "\n")
except Exception as e:
self.task_error.emit(task_id, f"处理错误输出时出错: {str(e)}")
self.task_log.emit(task_id, traceback.format_exc()) # 记录详细错误
# 修改处理槽函数以接受 task_id 和 proc 并进行清理和调度
def _on_process_finished(self, exit_code, exit_status, task_id, proc):
"""处理指定任务的进程完成事件"""
self.task_log.emit(task_id, f"进程完成. 退出码: {exit_code}, 退出状态: {exit_status}")
# 在处理完成信号时,立即断开输出信号连接
if proc:
try:
proc.readyReadStandardOutput.disconnect()
except TypeError: # 有时信号可能已经断开,捕获 TypeError
pass
try:
proc.readyReadStandardError.disconnect()
except TypeError:
pass
# 也可以断开 finished 信号本身,但通常不需要,因为它只触发一次
# try:
# proc.finished.disconnect()
# except TypeError:
# pass
success = False
message = "未知错误或脚本未返回明确结果" # 默认消息
# 从运行列表中移除进程
if task_id in self.running_processes:
# 确保删除的是正确的进程实例
if self.running_processes[task_id] is proc:
del self.running_processes[task_id]
else:
self.manager_log.emit(f"警告: 任务ID {task_id} 在 running_processes 中对应进程不匹配完成信号的进程实例.")
# 这种情况比较异常,可能需要更深入的调试
else:
# 如果任务ID不在运行列表中可能是重复的 finished 信号或逻辑错误
# 或者是在 _start_single_process 中模拟失败完成的情况
if (proc and proc.processId() != 0): # 检查 proc 是否是有效的 QProcess 实例且已启动
self.manager_log.emit(f"警告: 收到未知任务ID {task_id} 的完成信号,但 PID {proc.processId()} 已知。")
else:
self.manager_log.emit(f"警告: 收到未知任务ID {task_id} 的完成信号.")
# 尝试从 QProcess 对象属性中获取 RESULT 行
# 在 _start_single_process 中模拟失败完成时proc 可能为 None
result_line = proc.property("last_result_line") if proc else None
accumulated_stderr = proc.property("accumulated_stderr") or "" if proc else ""
if result_line:
try:
# 解析 RESULT 行
parts = result_line[len("RESULT:") :].split("|", 2) # 分割最多两次
if len(parts) >= 3:
success_from_script = parts[0] == "True"
output_path = parts[1]
error_msg_from_script = parts[2]
if success_from_script:
success = True
message = f"成功: {output_path}"
else:
# 如果脚本返回失败,使用脚本提供的错误信息
success = False
message = f"脚本返回失败: {error_msg_from_script}"
else:
# 如果 RESULT 行格式不正确
success = False
message = f"脚本返回结果格式错误: {result_line}"
except Exception as e:
success = False
message = f"解析脚本返回结果时出错: {str(e)}\n原始结果行: {result_line}"
else:
# 如果没有找到 RESULT 行,根据退出码判断
if exit_status == QProcess.ExitStatus.NormalExit and exit_code == 0:
success = True
message = "脚本正常退出,但未找到 RESULT 行。"
self.task_log.emit(task_id, message) # 记录为日志而不是错误
else:
success = False
message = f"脚本异常退出 (退出码: {exit_code})."
# 如果有累积的错误输出,也添加到消息中
if accumulated_stderr:
message += f"\n错误输出:\n{accumulated_stderr.strip()}"
self.task_error.emit(task_id, message) # 记录为错误
# 清理 QProcess 实例 (仅当 proc 对象有效时)
if proc:
proc.close()
proc.deleteLater() # 延迟删除对象,确保槽函数执行完毕
# 发射任务完成信号
if len(self.running_processes) == 0:
self.task_finished.emit(task_id, success, message)
# 尝试启动下一个任务
self._start_next_task()
def stop_all_tasks(self):
"""尝试停止所有正在运行的任务"""
self.manager_log.emit("正在尝试停止所有任务...")
# 复制字典的键,避免在迭代时修改
tasks_to_stop = list(self.running_processes.keys())
for task_id in tasks_to_stop:
proc = self.running_processes.get(task_id)
if proc and proc.state() == QProcess.ProcessState.Running:
self.task_log.emit(task_id, "尝试终止进程...")
proc.terminate() # 尝试优雅终止 (发送SIGTERM)
# proc.kill() # 强制杀死进程 (发送SIGKILL) - 更可靠,但可能导致数据损坏
# --- 保留原有 run_... 方法,它们现在会调用 run_script 来添加任务 ---
def run_export_map(self, params):
script_name = "export_map_v1.py"
task_desc = f"导出地图: {params.get('county_name', '未知区县')}"
return self.run_script(script_name, params, task_description=task_desc)
def run_export_layout(self, params):
script_name = "export_layout.py"
task_desc = f"导出布局: {os.path.basename(params.get('input_aprx_folder', '未知文件夹'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_batch_export_layout(self, params):
script_name = "batch_export_layout.py"
task_desc = f"批量导出布局: {os.path.basename(params.get('input_aprx_folder', '未知文件夹'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_process_raster(self, params):
script_name = ("raster_to_polygon.py")
task_desc = (f"处理栅格: {os.path.basename(params.get('input_raster', '未知栅格'))}")
return self.run_script(script_name, params, task_description=task_desc)
def run_area_stat(self, params):
script_name = "stats_area_to_excel.py"
task_desc = f"统计面积: {os.path.basename(params.get('reclassed_polygon', '未知面要素'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_suanhua_stat(self, params):
script_name = "stats_sh_to_excel.py"
task_desc = f"统计酸化: {os.path.basename(params.get('reclassed_polygon', '未知面要素'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_soil_prop_stat(self, params):
script_name = "stats_soil_prop_to_excel.py"
task_desc = f"统计酸化: {os.path.basename(params.get('reclassed_polygon', '未知面要素'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_excel_to_jpg(self, params):
script_name = "export_excel_to_jpg_v1.py"
task_desc = f"导出Excel: {os.path.basename(params.get('excel_path', '未知文件'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_test_script(self, params):
script_name = "test_script.py"
task_desc = f"测试脚本: {params.get('message', '无消息')}"
return self.run_script(script_name, params, task_description=task_desc)
# 可以添加其他通用的任务管理方法,例如:
def get_pending_tasks(self):
"""获取当前待处理任务列表"""
# 返回任务ID和描述的列表
return [(tid, desc) for tid, sp, args, desc, wd in self.pending_tasks]
def get_running_tasks(self):
"""获取当前正在运行任务的 task_id 和描述的列表"""
# 需要从 QProcess 对象中获取任务描述 (如果在启动时设置了属性)
running_list = []
for tid, proc in self.running_processes.items():
desc = proc.property("task_description") or "未知任务"
running_list.append((tid, desc))
return running_list
def get_max_concurrent(self):
"""获取最大并行任务数设置"""
return self.max_concurrent
def set_max_concurrent(self, count):
"""设置最大并行任务数,并尝试启动更多任务"""
if count > 0:
self.max_concurrent = count
self.manager_log.emit(f"最大并行任务数已更改为: {self.max_concurrent}")
self._start_next_task() # 尝试启动更多任务
else:
self.manager_log.emit("警告: 最大并行任务数必须大于 0。")

View File

@@ -0,0 +1,655 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
脚本运行器
用于从PyQt6界面调用独立脚本
"""
import os
import sys
import json
import subprocess
import traceback
from PyQt6.QtCore import QObject, pyqtSignal, QProcess, QThread, QRunnable, QThreadPool
class LambdaTask(QRunnable):
"""
用于在QThreadPool中执行任意函数的任务类
"""
def __init__(self, fn, *args, **kwargs):
super().__init__()
self.fn = fn
self.args = args
self.kwargs = kwargs
def run(self):
"""执行函数"""
try:
self.fn(*self.args, **self.kwargs)
except Exception as e:
print(f"任务执行错误: {str(e)}")
traceback.print_exc()
class ScriptRunner(QObject):
"""脚本运行器类,用于调用独立脚本处理任务"""
# 定义信号
started = pyqtSignal(str)
finished = pyqtSignal(object)
error = pyqtSignal(str)
log = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
# self.parent_ref = weakref.ref(parent) if parent else None
# 获取项目根目录
self.base_dir = self._get_base_dir()
self.log.emit(f"项目根目录: {self.base_dir}")
def _get_base_dir(self):
"""获取项目根目录"""
# 当前文件的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 向上两级找到项目根目录 (ui -> tools -> project_root)
return os.path.dirname(os.path.dirname(current_dir))
def _find_script(self, script_name):
"""查找脚本文件"""
# 可能的脚本路径
possible_paths = [
os.path.join(self.base_dir, 'tools', 'core', script_name), # 从项目根目录查找
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'core', script_name), # 从tools目录查找
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'core', script_name)), # 相对于ui目录
os.path.join(os.getcwd(), 'tools', 'core', script_name) # 从当前工作目录查找
]
# 尝试每个可能的路径
for path in possible_paths:
path = os.path.normpath(path) # 规范化路径
if os.path.exists(path):
self.log.emit(f"找到脚本: {path}")
return path
# 如果找不到脚本,记录所有尝试的路径
self.log.emit("无法找到脚本,尝试过以下路径:")
for path in possible_paths:
path = os.path.normpath(path)
self.log.emit(f" - {path} (存在: {os.path.exists(path)})")
# 返回第一个路径(即使它不存在)
return possible_paths[0]
def run_script_external(self, script_path, args, working_dir=None):
"""
使用外部Python窗口运行脚本显示实时输出
Args:
script_path: 脚本路径
args: 命令行参数字典
working_dir: 工作目录
Returns:
返回启动是否成功
"""
try:
# 更详细的日志
self.log.emit(f"尝试在外部窗口运行脚本: {script_path}")
# 检查脚本是否存在
if not os.path.exists(script_path):
self.error.emit(f"脚本不存在: {script_path}")
self.log.emit(f"当前工作目录: {os.getcwd()}")
self.log.emit(f"尝试查找脚本...")
# 从脚本名尝试查找脚本
script_name = os.path.basename(script_path)
actual_path = self._find_script(script_name)
if not os.path.exists(actual_path):
self.error.emit(f"无法找到脚本: {script_name}")
return False
script_path = actual_path
self.log.emit(f"使用脚本路径: {script_path}")
# 获取ArcGIS Pro的Python解释器路径
python_exe = self._get_arcgis_python() or sys.executable
self.log.emit(f"使用Python解释器: {python_exe}")
# 构建命令行参数
cmd = [python_exe, script_path]
# 添加参数
for key, value in args.items():
if key.startswith('--'):
param_key = key
else:
param_key = f"--{key}"
if isinstance(value, bool):
# 布尔参数
if value:
cmd.append(param_key)
elif isinstance(value, list):
# 列表参数通过JSON序列化传递
cmd.append(param_key)
cmd.append(json.dumps(value))
elif value is not None:
# 其他参数
cmd.append(param_key)
cmd.append(str(value))
# 通知开始
self.started.emit(f"开始执行脚本: {os.path.basename(script_path)}")
# 创建临时批处理文件,为了能自动关闭窗口
batch_file_path = os.path.join(os.environ.get('TEMP', '.'), f"run_script_{os.getpid()}.bat")
# 构建批处理文件内容
batch_content = f"""@echo off
chcp 65001 > nul
echo 正在执行脚本: {os.path.basename(script_path)}...
echo 命令: {' '.join(cmd)}
echo.
"""
# 设置工作目录
if working_dir:
batch_content += f'cd /d "{working_dir}"\n'
# 添加执行命令
batch_content += f'"{python_exe}" "{script_path}"'
# 添加命令行参数
for i in range(2, len(cmd)):
# 检查参数是否需要引号
param = cmd[i]
if ' ' in param or ',' in param or ';' in param or '&' in param or '[' in param or ']' in param:
batch_content += f' "{param}"'
else:
batch_content += f' {param}'
# 添加执行完成后的提示和暂停
batch_content += "\necho.\necho 脚本执行完成窗口将在3秒后自动关闭...\ntimeout /t 3 >nul\n"
# 写入批处理文件
with open(batch_file_path, 'w', encoding='utf-8') as f:
f.write(batch_content)
self.log.emit(f"创建批处理文件: {batch_file_path}")
# 使用subprocess.Popen启动外部窗口
# 使用start命令在新窗口中运行批处理文件
start_cmd = f'start "执行脚本 - {os.path.basename(script_path)}" "{batch_file_path}"'
subprocess.Popen(start_cmd, shell=True)
# 模拟成功启动
self.log.emit("已在外部窗口启动脚本")
# 注意:由于是在外部窗口运行,我们无法获知脚本的实际结果
# 假设脚本已成功启动,但实际执行结果需要用户观察外部窗口
QThread.msleep(1000) # 等待1秒
self.finished.emit(True)
# 预定批处理文件的删除
def delete_batch_file():
try:
if os.path.exists(batch_file_path):
os.remove(batch_file_path)
self.log.emit(f"已删除临时批处理文件: {batch_file_path}")
except Exception as e:
self.log.emit(f"删除临时批处理文件失败: {str(e)}")
# 创建延迟删除任务
QThread.sleep(10) # 确保批处理文件有足够时间执行
delete_task = LambdaTask(delete_batch_file)
thread_pool = QThreadPool.globalInstance()
if thread_pool is not None:
thread_pool.start(delete_task)
return True
except Exception as e:
self.error.emit(f"启动外部脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_script(self, script_path, args, working_dir=None):
"""使用非阻塞方式运行脚本"""
try:
# 检查脚本是否存在
if not os.path.exists(script_path):
self.error.emit(f"脚本不存在: {script_path}")
self.log.emit(f"当前工作目录: {os.getcwd()}")
self.log.emit(f"尝试查找脚本...")
# 从脚本名尝试查找脚本
script_name = os.path.basename(script_path)
actual_path = self._find_script(script_name)
if not os.path.exists(actual_path):
self.error.emit(f"无法找到脚本: {script_name}")
return False
script_path = actual_path
# 获取ArcGIS Pro的Python解释器路径
python_exe = self._get_arcgis_python() or sys.executable
self.log.emit(f"使用Python解释器: {python_exe}")
# 构建命令行参数
cmd = [python_exe, "-u", script_path]
# 添加参数
for key, value in args.items():
if key.startswith('--'):
param_key = key
else:
param_key = f"--{key}"
if isinstance(value, bool):
# 布尔参数
if value:
cmd.append(param_key)
elif value is not None:
# 其他参数
cmd.append(param_key)
cmd.append(str(value))
# 创建QProcess而不是subprocess.Popen
process = QProcess()
# 连接QProcess的信号
process.readyReadStandardOutput.connect(
lambda: self._handle_stdout(process)
)
process.readyReadStandardError.connect(
lambda: self._handle_stderr(process)
)
process.finished.connect(
lambda exit_code, exit_status: self._handle_finished(
exit_code, exit_status, process
)
)
# 设置工作目录
if working_dir:
process.setWorkingDirectory(working_dir)
# 通知开始
self.started.emit(f"开始执行脚本: {os.path.basename(script_path)}")
# 非阻塞启动进程
process.start(cmd[0], cmd[1:])
# 将进程对象保存在类变量中,防止被垃圾回收
self.current_process = process
# 返回True表示进程已启动结果将通过信号异步通知
return True
except Exception as e:
self.error.emit(f"执行脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def _handle_stdout(self, process):
"""处理标准输出"""
try:
# 首先尝试使用utf-8解码
data = process.readAllStandardOutput().data()
try:
text = data.decode('utf-8')
except UnicodeDecodeError:
# utf-8解码失败尝试使用系统默认编码或GBK中文Windows常用
try:
text = data.decode('gbk')
except UnicodeDecodeError:
# 如果还是失败使用errors='replace'选项替换无法解码的字符
text = data.decode('utf-8', errors='replace')
if text:
for line in text.splitlines():
if line.strip():
self.log.emit(line.strip())
except Exception as e:
self.log.emit(f"处理标准输出时出错: {str(e)}")
def _handle_stderr(self, process):
"""处理错误输出"""
try:
# 首先尝试使用utf-8解码
data = process.readAllStandardError().data()
try:
text = data.decode('utf-8')
except UnicodeDecodeError:
# utf-8解码失败尝试使用系统默认编码或GBK中文Windows常用
try:
text = data.decode('gbk')
except UnicodeDecodeError:
# 如果还是失败使用errors='replace'选项替换无法解码的字符
text = data.decode('utf-8', errors='replace')
if text:
for line in text.splitlines():
if line.strip():
self.log.emit(f"错误: {line.strip()}")
except Exception as e:
self.log.emit(f"处理错误输出时出错: {str(e)}")
def _handle_finished(self, exit_code, exit_status, process):
"""处理进程结束"""
if exit_code == 0:
self.log.emit(f"脚本执行成功,返回代码: {exit_code}")
self.finished.emit(True)
else:
self.error.emit(f"脚本执行失败,返回代码: {exit_code}")
self.finished.emit(False)
# 清理进程
process.close()
if hasattr(self, 'current_process') and self.current_process == process:
self.current_process = None
def _get_arcgis_python(self):
"""获取ArcGIS Pro的Python解释器路径"""
try:
# 常见的ArcGIS Pro Python路径
arcgis_python_paths = [
r"C:\Program Files\ArcGIS\Pro\bin\Python\envs\arcgispro-py3\python.exe",
r"C:\Program Files\ArcGIS\Pro\bin\Python\python.exe"
]
# 检查环境变量
if 'ARCGISPRO_PYTHON' in os.environ:
arcgis_python_paths.insert(0, os.environ['ARCGISPRO_PYTHON'])
# 尝试每个可能的路径
for path in arcgis_python_paths:
if os.path.exists(path):
return path
return None # 如果找不到返回None
except Exception as e:
self.log.emit(f"获取ArcGIS Python路径失败: {str(e)}")
return None
def run_export_map(self, params):
"""
运行导出地图脚本
Args:
params: 参数字典,包含:
- config_file: 配置文件路径
- county_name: 区县名称
- polygon_list: 要导出的图层列表
- template_aprx_file: 模板文件路径
- output_path: 输出路径
- data_source_path: 数据源路径
- symbol_path: 符号文件路径
- force_regenerate: 是否强制重新生成
Returns:
返回脚本执行结果
"""
try:
# 检查必要参数是否存在
required_params = ['config_file', 'county_name', 'polygon_list',
'template_aprx_file', 'output_path',
'data_source_path', 'symbol_path']
for param in required_params:
if param not in params:
self.error.emit(f"缺少必要参数: {param}")
return False
# 查找脚本路径
script_path = self._find_script('export_map.py')
# 构建命令行参数
args = {
'config_file': params['config_file'],
'county_name': params['county_name'],
'polygon_list': json.dumps(params['polygon_list']), # 将图层列表序列化为JSON字符串
'template_aprx_file': params['template_aprx_file'],
'output_path': params['output_path'],
'data_source_path': params['data_source_path'],
'symbol_path': params['symbol_path']
}
# 添加可选参数
if 'force_regenerate' in params and params['force_regenerate']:
args['force_regenerate'] = True
# 使用外部窗口执行脚本
return self.run_script(script_path, args)
except Exception as e:
self.error.emit(f"执行导出地图脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_batch_export_map(self, params):
"""运行批量导出地图脚本 """
try:
# 获取参数
if 'export_config' not in params:
self.error.emit(f"缺少必要参数: export_config")
return False
export_config = params['export_config']
county_name = params['county_name']
template_aprx_file = params['template_aprx_file']
output_folder = params['output_path']
data_source_path = params['data_source_path']
symbol_path = params['symbol_path']
polygon_list = params.get('polygon_list', []) # 如果指定了图层列表,则只导出这些图层
force_regenerate = params.get('force_regenerate', False) # 是否强制重新生成工程文件
# 如果没有指定图层列表,则使用配置文件中的所有图层
if not polygon_list:
polygon_list = list(export_config.keys())
self.log.emit(f"开始批量导出 {len(polygon_list)} 个图层")
# 准备脚本路径
script_path = self._find_script('export_map.py')
# 记录成功和失败的图层
success_count = 0
failed_count = 0
# 循环处理每个图层
for layer_name in polygon_list:
try:
self.log.emit(f"\n===== 开始处理图层: {layer_name} =====")
# 构建命令行参数
args = {
'config': params['config_file'],
'county': county_name,
'layer': layer_name,
'template': template_aprx_file,
'output': output_folder,
'data_source': data_source_path,
'symbol': symbol_path,
'force': force_regenerate
}
# 执行脚本
if self.run_script(script_path, args):
success_count += 1
else:
failed_count += 1
except Exception as e:
self.log.emit(f"处理图层 {layer_name} 时出错: {str(e)}")
failed_count += 1
# 通知完成
self.log.emit(f"\n===== 批量处理完成 =====")
self.log.emit(f"总计图层: {len(polygon_list)}")
self.log.emit(f"成功: {success_count} 个")
self.log.emit(f"失败: {failed_count} 个")
result = {
'total': len(polygon_list),
'success': success_count,
'failed': failed_count
}
self.finished.emit(result)
return result
except Exception as e:
self.error.emit(f"执行批量导出地图脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_export_layout(self, params):
"""
运行导出布局脚本
Args:
params: 参数字典
Returns:
返回脚本执行结果
"""
try:
# 准备参数
script_path = self._find_script('export_layout.py')
# 构建命令行参数
args = {
'mode': 'single',
'aprx': params['aprx_path'],
'output': params['output_path'],
'format': params.get('export_format', 'PDF'),
'resolution': params.get('resolution', 300),
'name': params.get('output_name', '')
}
# 执行脚本
# return self.run_script(script_path, args)
except Exception as e:
self.error.emit(f"执行导出布局脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_batch_export_layout(self, params):
"""
运行批量导出布局脚本
Args:
params: 参数字典
Returns:
返回脚本执行结果
"""
try:
# 准备参数
script_path = self._find_script('export_layout.py')
# 构建命令行参数
args = {
'aprx_file_list':json.dumps(params['aprx_file_list']),
'input_aprx_folder': params['input_aprx_folder'],
'output': params['output_image_path'],
'format': params.get('export_format', 'PDF'),
'resolution': params.get('resolution', 300),
'use_multiprocessing': params.get('use_multiprocessing', False),
'process_count': params.get('process_count', 2)
}
# 执行脚本
return self.run_script(script_path, args)
except Exception as e:
self.error.emit(f"执行批量导出布局脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_process_raster(self, params):
"""
运行栅格处理脚本
Args:
params: 参数字典
Returns:
返回脚本执行结果
"""
try:
# 准备参数
script_path = self._find_script('raster_to_vector.py')
# 构建命令行参数
args = {
'command': 'process',
'input': params['input_raster'],
'output': params['output_raster'],
'format': params.get('output_format', 'TIFF'),
'compression': params.get('compression', 'LZW')
}
# 执行脚本
return self.run_script(script_path, args)
except Exception as e:
self.error.emit(f"执行栅格处理脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_raster_batch_process(self, params):
"""
运行批量栅格处理脚本
Args:
params: 参数字典
Returns:
返回脚本执行结果
"""
try:
# 准备参数
script_path = self._find_script('raster_handler.py')
params["selected_files"] = json.dumps(params.get("selected_files"))
# 执行脚本
return self.run_script(script_path, params)
except Exception as e:
self.error.emit(f"执行批量栅格处理脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_test_script(self, params):
try:
# 检查必要参数是否存在
required_params = ['message', 'count']
for param in required_params:
if param not in params:
self.error.emit(f"缺少必要参数: {param}")
return False
# 查找脚本路径
script_path = self._find_script('test_script.py')
# 构建命令行参数
args = {
'message': params['message'],
'count': params['count']
}
# 执行脚本
return self.run_script(script_path, args)
except Exception as e:
self.error.emit(f"执行导出地图脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# UI界面模块
from .export_layout_tab import ExportImageTab
from .raster_tab import RasterTab
from .export_map_tab import ExportMapTab
__all__ = ['ExportImageTab', 'RasterTab', 'ExportMapTab']

View File

@@ -0,0 +1,373 @@
# -*- coding: utf-8 -*-
"""
栅格处理界面: 提供栅格重分类、栅格转矢量和小面积图斑消除的界面操作
"""
import os
import json
import arcpy
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QGridLayout,
QGroupBox, QMessageBox)
from tools.ui.runners.script_runner import ScriptRunner
class AcidStatsTab(QWidget):
"""栅格处理窗口部件类"""
def __init__(self, parent=None):
super(AcidStatsTab, self).__init__(parent)
self.main_window = parent
# 创建的导出线程
self.script_runner = ScriptRunner()
self.connect_signals()
self.init_ui()
self.setWindowTitle("酸化状况统计")
def connect_signals(self):
# 连接ScriptRunner的信号
self.script_runner.task_started.connect(self.on_script_started)
self.script_runner.task_finished.connect(self.on_script_finished)
self.script_runner.task_error.connect(self.on_script_error)
self.script_runner.task_log.connect(self.on_script_log)
self.script_runner.manager_log.connect(self.log_message)
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout(self)
# 批量处理区域
self.batch_mode_group = QGroupBox("批量处理设置")
batch_layout = QGridLayout()
# 输入行政区名称
batch_layout.addWidget(QLabel("行政区名称:"), 0, 0)
self.input_xzqmc_edit = QLineEdit()
batch_layout.addWidget(self.input_xzqmc_edit, 0, 1)
# 设置工作空间
batch_layout.addWidget(QLabel("工作空间:"), 1, 0)
self.input_workspace_edit = QLineEdit()
batch_layout.addWidget(self.input_workspace_edit, 1, 1)
self.browse_input_workspace_btn = QPushButton("选择GDB")
self.browse_input_workspace_btn.clicked.connect(self.browse_input_workspace)
batch_layout.addWidget(self.browse_input_workspace_btn, 1, 2)
# 批量输出文件夹
batch_layout.addWidget(QLabel("输出文件夹:"), 2, 0)
self.batch_output_folder_edit = QLineEdit()
batch_layout.addWidget(self.batch_output_folder_edit, 2, 1)
self.browse_batch_output_btn = QPushButton("选择文件夹")
self.browse_batch_output_btn.clicked.connect(self.browse_batch_output_folder)
batch_layout.addWidget(self.browse_batch_output_btn, 2, 2)
# 选择乡镇界线
batch_layout.addWidget(QLabel("乡镇界线:"), 3, 0)
self.xzq_features_edit = QLineEdit()
batch_layout.addWidget(self.xzq_features_edit, 3, 1)
batch_layout.addWidget(QLabel("GDB中的要素类名"), 3, 2)
# 选择地类图斑
batch_layout.addWidget(QLabel("地类图斑:"), 4, 0)
self.dltb_features_edit = QLineEdit()
batch_layout.addWidget(self.dltb_features_edit, 4, 1)
batch_layout.addWidget(QLabel("GDB中的要素类名"), 4, 2)
# 选择历史PH样点图斑
batch_layout.addWidget(QLabel("历史PH样点:"), 5, 0)
self.ph_samples_edit = QLineEdit()
batch_layout.addWidget(self.ph_samples_edit, 5, 1)
batch_layout.addWidget(QLabel("GDB中的要素类名"), 5, 2)
# 选择重分类后的PH面要素
batch_layout.addWidget(QLabel("分类后酸化PH面:"), 6, 0)
self.acid_ph_edit = QLineEdit()
batch_layout.addWidget(self.acid_ph_edit, 6, 1)
batch_layout.addWidget(QLabel("GDB中的要素类名"), 6, 2)
# 选择土壤类型图斑
batch_layout.addWidget(QLabel("土壤类型图斑:"), 7, 0)
self.soil_type_features_edit = QLineEdit()
batch_layout.addWidget(self.soil_type_features_edit, 7, 1)
batch_layout.addWidget(QLabel("GDB中的要素类名"), 7, 2)
# 选择三普PH赋值栅格
batch_layout.addWidget(QLabel("三普PH栅格:"), 8, 0)
self.assign_raster_edit = QLineEdit()
batch_layout.addWidget(self.assign_raster_edit, 8, 1)
self.assign_raster_btn = QPushButton("选择TIF")
self.assign_raster_btn.clicked.connect(self.browse_assign_raster)
batch_layout.addWidget(self.assign_raster_btn, 8, 2)
# 选择酸化PH赋值栅格
batch_layout.addWidget(QLabel("酸化PH栅格:"), 9, 0)
self.acid_raster_edit = QLineEdit()
batch_layout.addWidget(self.acid_raster_edit, 9, 1)
self.acid_raster_btn = QPushButton("选择TIF")
self.acid_raster_btn.clicked.connect(self.browse_acid_raster)
batch_layout.addWidget(self.acid_raster_btn, 9, 2)
self.batch_mode_group.setLayout(batch_layout)
# 操作按钮
btn_layout = QHBoxLayout()
# 导出酸化统计表
self.generate_sh_stat_btn = QPushButton("生成酸化统计表")
self.generate_sh_stat_btn.clicked.connect(self.on_generate_area_stat)
self.cancel_btn = QPushButton("取消")
# self.cancel_btn.clicked.connect(self.close)
btn_layout.addWidget(self.generate_sh_stat_btn)
btn_layout.addWidget(self.cancel_btn)
# 添加所有组件到主布局
main_layout.addWidget(self.batch_mode_group)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
def browse_input_workspace(self):
"""浏览选择输入GDB"""
folder_path = QFileDialog.getExistingDirectory(self, "选择GDB数据库")
if folder_path:
self.input_workspace_edit.setText(folder_path)
def browse_batch_output_folder(self):
"""浏览选择表格输出文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择表格输出文件夹")
if folder_path:
self.batch_output_folder_edit.setText(folder_path)
def browse_assign_raster(self):
"""浏览选择赋值栅格 (支持栅格数据)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择赋值栅格", "",
"栅格文件 (*.tif *.img *.jpg);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(('.tif', '.img', '.jpg')):
self.assign_raster_edit.setText(file_path)
self.log_message(f"已选择栅格文件: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择有效的栅格文件。")
self.assign_raster_edit.clear()
def browse_acid_raster(self):
"""浏览选择赋值栅格 (支持栅格数据)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择赋值栅格", "",
"栅格文件 (*.tif *.img *.jpg);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(('.tif', '.img', '.jpg')):
self.acid_raster_edit.setText(file_path)
self.log_message(f"已选择栅格文件: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择有效的栅格文件。")
self.acid_raster_edit.clear()
def validate_inputs(self):
"""验证输入参数"""
# 验证工作空间路径
if not self.input_workspace_edit.text() or not arcpy.Exists(self.input_workspace_edit.text()):
QMessageBox.warning(self, "输入错误", "请选择有效的工作空间")
return False
if not self.batch_output_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择批量处理输出文件夹")
return False
# 行政界线验证
xzq_path = os.path.join(self.input_workspace_edit.text(),self.xzq_features_edit.text())
if not xzq_path or not arcpy.Exists(xzq_path):
QMessageBox.warning(self, "输入错误", "行政界线要素路径无效或不存在")
return False
try:
desc = arcpy.Describe(xzq_path)
if desc.dataType != 'FeatureClass':
QMessageBox.warning(self, "输入错误", f"行政界线不是一个要素类:\n{xzq_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"未知要素,请检查是否为有效要素类:\n{xzq_path}\n错误: {e}")
return False
# 地类图斑验证
dltb_path = os.path.join(self.input_workspace_edit.text(),self.dltb_features_edit.text())
if not dltb_path or not arcpy.Exists(dltb_path):
QMessageBox.warning(self, "输入错误", "地类图斑要素路径无效或不存在")
return False
try:
desc = arcpy.Describe(dltb_path)
if desc.dataType != 'FeatureClass':
QMessageBox.warning(self, "输入错误", f"地类图斑不是一个要素类:\n{dltb_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"未知要素,请检查是否为有效要素类:\n{dltb_path}\n错误: {e}")
return False
# 验证PH样点
samples_path = os.path.join(self.input_workspace_edit.text(), self.ph_samples_edit.text())
if not samples_path or not arcpy.Exists(samples_path):
QMessageBox.warning(self, "输入错误", "PH样点要素路径无效或不存在")
return False
try:
desc = arcpy.Describe(samples_path)
if desc.dataType != 'FeatureClass':
QMessageBox.warning(self, "输入错误", f"PH样点不是一个要素类:\n{samples_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"未知要素,请检查是否为有效要素类:\n{samples_path}\n错误: {e}")
return False
# 重分类后酸化面要素验证
acid_ph_path = os.path.join(self.input_workspace_edit.text(), self.acid_ph_edit.text())
if not acid_ph_path or not arcpy.Exists(acid_ph_path):
QMessageBox.warning(self, "输入错误", "酸化PH要素路径无效或不存在")
return False
try:
desc = arcpy.Describe(acid_ph_path)
if desc.dataType != 'FeatureClass':
QMessageBox.warning(self, "输入错误", f"酸化PH不是一个要素类:\n{acid_ph_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"未知要素,请检查是否为有效要素类:\n{acid_ph_path}\n错误: {e}")
return False
# 土壤类型验证
trlx_path = os.path.join(self.input_workspace_edit.text(), self.soil_type_features_edit.text())
if not self.soil_type_features_edit.text() or not arcpy.Exists(trlx_path):
QMessageBox.warning(self, "输入错误", "土壤类型要素路径无效或不存在")
return False
try:
desc = arcpy.Describe(trlx_path)
if desc.dataType != 'FeatureClass':
QMessageBox.warning(self, "输入错误", f"土壤类型不是一个要素类:\n{trlx_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"未知要素,请检查是否为有效要素类:\n{trlx_path}\n错误: {e}")
return False
# 验证输入是否为栅格
if not self.acid_raster_edit.text() or not self.assign_raster_edit.text():
QMessageBox.warning(self, "输入错误", "请选择输入栅格")
return False
elif not arcpy.Exists(self.acid_raster_edit.text()) or not arcpy.Exists(self.assign_raster_edit.text()):
QMessageBox.warning(self, "输入错误", "输入栅格不存在")
return False
return True
# 执行生成酸化统计表
def on_generate_area_stat(self):
"""执行生成面积统计表"""
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
params = {
"settings_path": settings_file,
}
task_id = self.script_runner.run_suanhua_stat(params)
if task_id:
self.log_message(f"[GUI] 面积统计任务 {task_id} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def get_acid_stat_settings(self):
"""保存当前配置到JSON文件"""
config = {
"workspace_path": self.input_workspace_edit.text(),
"batch_output_folder": self.batch_output_folder_edit.text(),
"xzq_features": self.xzq_features_edit.text(),
"dltb_features": self.dltb_features_edit.text(),
"ph_samples": self.ph_samples_edit.text(),
"acid_ph_features": self.acid_ph_edit.text(),
"assign_raster": self.assign_raster_edit.text(),
"acid_raster": self.acid_raster_edit.text(),
"soil_type_features": self.soil_type_features_edit.text(),
"xzqmc": self.input_xzqmc_edit.text()
}
return config
def load_settings(self, settings):
"""从字典加载配置"""
try:
# 批处理参数
self.input_workspace_edit.setText(settings.get("workspace_path", ""))
self.batch_output_folder_edit.setText(settings.get("batch_output_folder", ""))
self.xzq_features_edit.setText(settings.get("xzq_features", ""))
self.dltb_features_edit.setText(settings.get("dltb_features", ""))
self.ph_samples_edit.setText(settings.get("ph_samples", ""))
self.acid_ph_edit.setText(settings.get("acid_ph_features", ""))
self.assign_raster_edit.setText(settings.get("assign_raster", ""))
self.acid_raster_edit.setText(settings.get("acid_raster", ""))
self.soil_type_features_edit.setText(settings.get("soil_type_features", ""))
return True
except Exception as e:
QMessageBox.critical(self, "加载配置错误", f"加载配置时出错: {str(e)}")
return False
def set_buttons_enabled(self, enabled):
"""设置按钮是否可用"""
self.generate_sh_stat_btn.setEnabled(enabled)
def on_script_started(self, task_id, task_description):
"""脚本开始执行"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成"""
self.set_buttons_enabled(True)
status = "完成" if success else "失败"
self.log_message(f"[{task_id}] 脚本执行{status}: {message}")
def on_script_error(self,task_id, error_msg):
"""脚本执行出错"""
self.set_buttons_enabled(True)
self.log_message(f"错误:{task_id}-{error_msg}")
# QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = AcidStatsTab()
window.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,564 @@
# -*- coding: utf-8 -*-
"""
栅格处理界面: 提供栅格重分类、栅格转矢量和小面积图斑消除的界面操作
"""
import os
import json
import arcpy
import traceback
import multiprocessing
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QGridLayout, QCheckBox,
QGroupBox, QMessageBox, QSpinBox)
from tools.core.utils import common_utils
from tools.ui.runners.script_runner import ScriptRunner
from .config_editor_dialog import ConfigEditorDialogVisual
from ..components.file_list_group import FileListGroup
yunnan_dlbm = """
def yunnan_dlbm(dlbm):
if dlbm.startswith("0101"):
return "0101"
elif dlbm.startswith("0102"):
return "0102"
elif dlbm.startswith("0103"):
return "0103"
else:
return dlbm[:2]
"""
class XlsxToJpgTab(QWidget):
"""栅格处理窗口部件类"""
def __init__(self, parent=None):
super(XlsxToJpgTab, self).__init__(parent)
self.main_window = parent
# 创建的导出线程
self.script_runner = ScriptRunner()
self.connect_signals()
self.init_ui()
self.setWindowTitle("面积统计")
def connect_signals(self):
# 连接ScriptRunner的信号
self.script_runner.task_started.connect(self.on_script_started)
self.script_runner.task_finished.connect(self.on_script_finished)
self.script_runner.task_error.connect(self.on_script_error)
self.script_runner.task_log.connect(self.on_script_log)
self.script_runner.manager_log.connect(self.log_message)
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout(self)
# 批量处理区域
self.batch_mode_group = QGroupBox("批量处理设置")
batch_layout = QGridLayout()
# 配置文件选择 (公用)
batch_layout.addWidget(QLabel("配置文件:"), 0, 0)
self.config_file_edit = QLineEdit()
self.config_file_edit.setEnabled(False)
batch_layout.addWidget(self.config_file_edit, 0, 1,1,2)
# self.browse_config_btn = QPushButton("浏览...")
# self.browse_config_btn.clicked.connect(self.browse_config_file)
# self.browse_config_btn.setEnabled(False)
# batch_layout.addWidget(self.browse_config_btn, 0, 2)
# # 添加编辑配置文件的按钮 <--- Add this button
# self.edit_config_btn = QPushButton("编辑配置内容...")
# self.edit_config_btn.setEnabled(False)
# self.edit_config_btn.clicked.connect(self.open_config_editor)
# batch_layout.addWidget(self.edit_config_btn, 0, 3)
# 输入文件夹
batch_layout.addWidget(QLabel("重分类面要素:"), 1, 0)
self.input_folder_edit = QLineEdit()
batch_layout.addWidget(self.input_folder_edit, 1, 1)
self.browse_input_folder_btn = QPushButton("浏览...")
self.browse_input_folder_btn.clicked.connect(self.browse_input_folder)
batch_layout.addWidget(self.browse_input_folder_btn, 1, 2)
# 批量输出文件夹
batch_layout.addWidget(QLabel("输出文件夹:"), 2, 0)
self.batch_output_folder_edit = QLineEdit()
batch_layout.addWidget(self.batch_output_folder_edit, 2, 1)
self.browse_batch_output_btn = QPushButton("浏览...")
self.browse_batch_output_btn.clicked.connect(self.browse_batch_output_folder)
batch_layout.addWidget(self.browse_batch_output_btn, 2, 2)
# 选择乡镇界线
batch_layout.addWidget(QLabel("乡镇界线:"), 3, 0)
self.xzq_features_edit = QLineEdit()
batch_layout.addWidget(self.xzq_features_edit, 3, 1)
self.xzq_features_btn = QPushButton("浏览")
self.xzq_features_btn.clicked.connect(self.browse_xzq_features)
batch_layout.addWidget(self.xzq_features_btn, 3, 2)
# 选择地类图斑
batch_layout.addWidget(QLabel("地类图斑:"), 4, 0)
self.dltb_features_edit = QLineEdit()
batch_layout.addWidget(self.dltb_features_edit, 4, 1)
self.dltb_features_btn = QPushButton("浏览")
self.dltb_features_btn.clicked.connect(self.browse_dltb_features)
batch_layout.addWidget(self.dltb_features_btn, 4, 2)
# 输入行政区名称
batch_layout.addWidget(QLabel("行政区名称:"), 5, 0)
self.xzqmc_edit = QLineEdit()
batch_layout.addWidget(self.xzqmc_edit, 5, 1, 1, 2)
# 单选框
ext_layout = QHBoxLayout()
ext_layout.addWidget(QLabel("是否同时对行政区和地类进行平差(需要xlsx文件有其他地类的平差面积):"))
self.is_adjust_xzq_and_landuse_checkbox = QCheckBox()
ext_layout.addWidget(self.is_adjust_xzq_and_landuse_checkbox)
ext_layout.addStretch()
batch_layout.addLayout(ext_layout, 6, 0, 1, 2)
self.batch_mode_group.setLayout(batch_layout)
# 文件列表显示组
self.file_list_group = FileListGroup(self, "选择重分类后面要素")
self.file_list_group.load_files.connect(self.on_load_raster)
# --- 处理参数设置 ---
param_group = QGroupBox("处理参数")
param_layout = QVBoxLayout(param_group)
# 多进程设置
export_layout = QHBoxLayout()
mutiprocess_layout = QHBoxLayout()
mutiprocess_layout.addWidget(QLabel("使用多进程导出:"))
self.mutiprocess_check = QCheckBox()
self.mutiprocess_check.setChecked(True)
mutiprocess_layout.addWidget(self.mutiprocess_check)
mutiprocess_layout.addStretch()
export_layout.addLayout(mutiprocess_layout)
# 进程数量设置
process_count_layout = QHBoxLayout()
process_count_layout.addWidget(QLabel("进程数量:"))
self.process_count = QSpinBox()
self.process_count.setRange(1, multiprocessing.cpu_count())
self.process_count.setValue(max(1, multiprocessing.cpu_count() - 1)) # 默认使用CPU核心数-1
self.process_count.setFixedWidth(180)
process_count_layout.addWidget(self.process_count)
export_layout.addLayout(process_count_layout)
param_layout.addLayout(export_layout)
# 操作按钮
btn_layout = QHBoxLayout()
self.process_btn = QPushButton("导出到Excel")
self.process_btn.clicked.connect(self.on_start_processing)
self.excel_to_jpg_btn = QPushButton("Eexcel导出到JPG")
self.excel_to_jpg_btn.clicked.connect(self.on_export_excel_to_jpg)
self.cancel_btn = QPushButton("取消")
# self.cancel_btn.clicked.connect(self.close)
btn_layout.addWidget(self.process_btn)
btn_layout.addWidget(self.excel_to_jpg_btn)
btn_layout.addWidget(self.cancel_btn)
# 添加所有组件到主布局
main_layout.addWidget(self.batch_mode_group)
main_layout.addWidget(self.file_list_group)
main_layout.addWidget(param_group)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
def browse_input_folder(self):
"""浏览选择输入文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择重分类后面要素文件夹")
if folder_path:
self.input_folder_edit.setText(folder_path)
self.on_load_raster()
def browse_batch_output_folder(self):
"""浏览选理输出文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择输出文件夹")
if folder_path:
self.batch_output_folder_edit.setText(folder_path)
def browse_config_file(self):
"""浏览选择配置文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择配置文件", "",
"JSON 文件 (*.json);;所有文件 (*)"
)
if file_path:
self.config_file_edit.setText(file_path)
# self.validate_config_file(file_path)
# self.edit_config_btn.setEnabled(True)
def browse_xzq_features(self):
"""浏览选择乡镇界线要素 (支持 shapefile)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择乡镇界线要素 (Shapefile)", "",
"Shapefile 文件 (*.shp);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(".shp"):
self.xzq_features_edit.setText(file_path)
self.log_message(f"已选择 Shapefile: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择 .shp 文件。")
self.xzq_features_edit.clear()
def browse_dltb_features(self):
"""浏览选择地类图斑要素 (支持 shapefile)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择地类图斑要素 (Shapefile)", "",
"Shapefile 文件 (*.shp);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(".shp"):
self.dltb_features_edit.setText(file_path)
self.log_message(f"已选择 Shapefile: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择 .shp 文件。")
self.dltb_features_edit.clear()
def validate_config_file(self, config_file_path):
"""对文件进行快速检查,以查看它是否类似于预期的配置结构。"""
if not os.path.exists(config_file_path):
return False
try:
with open(config_file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
if "export_config" in config and isinstance(config["export_config"], dict):
if config["export_config"]:
first_item_value = next(iter(config["export_config"].values()))
if isinstance(first_item_value, dict) and all(k in first_item_value for k in ["项目名称", "标准等级"]):
self.log_message(f"配置文件 '{os.path.basename(config_file_path)}' 加载成功。")
return True
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 内容结构可能不匹配预期。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 中的 'export_config' 部分为空。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 缺少 'export_config' 键或格式不正确。")
return False
except json.JSONDecodeError:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 不是有效的 JSON 格式。")
return False
except Exception as e:
self.log_message(f"警告: 检查配置文件 '{os.path.basename(config_file_path)}' 时出错: {str(e)}")
return False
def open_config_editor(self):
"""打开配置文件编辑器对话框"""
config_path = self.config_file_edit.text()
if not config_path:
reply = QMessageBox.question(self, "编辑配置", "没有选择配置文件,是否创建一个新配置文件?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.No:
return
config_path = ""
# 使用配置编辑对话框
dialog = ConfigEditorDialogVisual(config_path, self)
if dialog.exec() == ConfigEditorDialogVisual.DialogCode.Accepted:
new_path = dialog.get_new_config_path()
if new_path and new_path != self.config_file_edit.text():
self.config_file_edit.setText(new_path)
self.validate_config_file(new_path)
def on_load_raster(self):
"""加载图层列表"""
try:
data_source_paths = self.input_folder_edit.text()
self.file_list_group.load_file_btn.setEnabled(False)
# 清空列表
self.file_list_group.file_list.clear()
if not data_source_paths or not arcpy.Exists(data_source_paths):
self.log_message(f"输入路径不存在或不是有效的ArcGIS工作空间/文件夹: {data_source_paths}")
return
arcpy.env.workspace = data_source_paths
# 获取所有SHP 文件
shp_files = arcpy.ListFeatureClasses("*.shp")
if shp_files:
for raster in shp_files:
self.file_list_group.file_list.addItem(raster)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个图层")
else:
self.log_message(f"文件夹 '{os.path.basename(data_source_paths)}' 中没有找到重分类后面要素文件。")
except Exception as e:
self.log_message(f"加载图层列表失败: {str(e)}")
traceback.print_exc()
finally:
self.file_list_group.load_file_btn.setEnabled(True)
def validate_inputs(self):
"""验证输入参数"""
# 验证配置文件路径
config_path = self.config_file_edit.text()
if not config_path or not os.path.exists(config_path):
QMessageBox.warning(self, "输入错误", "请选择有效的配置文件")
return False
try:
with open(config_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法加载或解析配置文件: {e}")
return False
if "export_config" not in config_data or not isinstance(config_data["export_config"], dict):
QMessageBox.warning(self, "输入错误", "配置文件的内容无效或缺少 'export_config' 部分。")
return False
standards_dict = config_data["export_config"]
if not standards_dict:
QMessageBox.warning(self, "输入错误", "配置文件中的 'export_config' 部分为空,没有定义任何处理参数。")
return False
# 验证'标准等级'用于配置重分类表
error_keys_remap = []
for key, value in standards_dict.items():
try:
levels_data = value.get("标准等级", {})
if not isinstance(levels_data, dict):
error_keys_remap.append(f"{key} ('标准等级' 格式不正确)")
continue
if not common_utils.create_remap_table(levels_data):
error_keys_remap.append(f"{key} ('标准等级' 内容无效)")
except Exception:
error_keys_remap.append(f"{key} (验证出错)")
if error_keys_remap:
QMessageBox.warning(self, "输入错误", "配置文件中部分参数的 '标准等级' 数据无效:\n" + "\n".join(error_keys_remap))
return False
# 验证输入、输出文件夹
if not self.input_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择输入重分类后面要素文件夹")
return False
if not self.batch_output_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择批量处理输出文件夹")
return False
if not self.xzqmc_edit.text():
QMessageBox.warning(self, "输入错误", "请输入县区名称")
return False
# 验证地类图斑文件
dltb_path = self.dltb_features_edit.text()
if not dltb_path and not arcpy.Exists(dltb_path):
QMessageBox.warning(self, "输入错误", "请选择有效的地类图斑文件")
return False
# 3. 验证乡镇界线文件
xzjx_path = self.xzq_features_edit.text()
if not xzjx_path or not arcpy.Exists(xzjx_path):
QMessageBox.warning(self, "输入错误", "请选择有效的乡镇界线文件。")
return False
# Optional: Check if the path points to a Feature Class (SHP or inside GDB)
try:
desc = arcpy.Describe(xzjx_path)
if desc.dataType != 'FeatureClass' and desc.dataType != 'ShapeFile':
QMessageBox.warning(self, "输入错误", f"选择的乡镇界线文件不是一个要素类:\n{xzjx_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法描述乡镇界线文件,请检查是否为有效要素类:\n{xzjx_path}\n错误: {e}")
return False
# 验证是否选择文件
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if not selected_files:
QMessageBox.warning(self, "输入错误", "请在栅格列表中勾选要处理的文件。")
return False
return True
def on_start_processing(self):
"""开始处理栅格数据"""
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
# 获取公共参数 (从 VectorParamsWidget 获取)
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if self.mutiprocess_check.isChecked():
self.script_runner.set_max_concurrent(self.process_count.value())
else:
self.script_runner.set_max_concurrent(1)
# 计算DLTB字段
try:
dltb_polygon = self.dltb_features_edit.text()
if arcpy.Exists(dltb_polygon) and not arcpy.ListFields(dltb_polygon,"YJDLBM"):
arcpy.management.CalculateField(dltb_polygon, "YJDLBM", "!DLBM![:2]", "PYTHON3")
if arcpy.Exists(dltb_polygon) and not arcpy.ListFields(dltb_polygon,"YNDLBM"):
arcpy.management.CalculateField(dltb_polygon, "YNDLBM", "yunnan_dlbm(!DLBM!)", "PYTHON3", yunnan_dlbm)
# else:
# QMessageBox.critical(self, "处理错误", "地类图斑文件不存在")
# return
except Exception as e:
print(f"计算SHFJ字段时发生错误: {e}")
return
# 调用批量处理脚本
for raster_file in selected_files:
params = {
"settings_path": settings_file,
"reclassed_polygon": raster_file,
}
# 统计面积
task_id = self.script_runner.run_area_stat(params)
# if task_id:
# self.log_message(f"[GUI] 任务 {task_id} 处理 {raster_file} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def on_export_excel_to_jpg(self):
self.log_message("[GUI] 开始处理")
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
params = {
"settings_path":settings_file
}
task_id = self.script_runner.run_excel_to_jpg(params)
if task_id:
self.log_message(f"[GUI] 批量处理任务 {task_id} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def get_area_stat_settings(self):
"""保存当前配置到JSON文件"""
config = {
"input_folder": self.input_folder_edit.text(),
"batch_output_folder": self.batch_output_folder_edit.text(),
"config_file_path": self.config_file_edit.text(),
"xzq_features": self.xzq_features_edit.text(),
"dltb_features": self.dltb_features_edit.text(),
"xzqmc": self.xzqmc_edit.text(),
"is_by_xzq": self.is_adjust_xzq_and_landuse_checkbox.isChecked(),
}
return config
def load_settings(self, settings):
"""从字典加载配置"""
try:
# 批处理参数
self.input_folder_edit.setText(settings.get("input_folder", ""))
self.batch_output_folder_edit.setText(settings.get("batch_output_folder", ""))
# 配置文件路径和加载
config_file_path = settings.get("config_file_path", "")
self.config_file_edit.setText(config_file_path)
# self.edit_config_btn.setEnabled(bool(config_file_path) and os.path.exists(config_file_path))
if config_file_path:
self.validate_config_file(config_file_path)
self.xzq_features_edit.setText(settings.get("xzq_features", ""))
self.dltb_features_edit.setText(settings.get("dltb_features", ""))
if self.input_folder_edit.text():
self.on_load_raster()
return True
except Exception as e:
QMessageBox.critical(self, "加载配置错误", f"加载配置时出错: {str(e)}")
return False
def set_buttons_enabled(self, enabled):
"""设置按钮是否可用"""
self.process_btn.setEnabled(enabled)
self.excel_to_jpg_btn.setEnabled(enabled)
def update_config_file(self, config_file_path):
"""更新配置文件路径"""
self.config_file_edit.setText(config_file_path)
def on_script_started(self, task_id, task_description):
"""脚本开始执行"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成"""
self.set_buttons_enabled(True)
status = "完成" if success else "失败"
self.log_message(f"[{task_id}] 脚本执行{status}: {message}")
def on_script_error(self,task_id, error_msg):
"""脚本执行出错"""
self.set_buttons_enabled(True)
self.log_message(f"错误:{task_id}-{error_msg}")
# QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = XlsxToJpgTab()
window.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,597 @@
# --- START OF FILE config_editor_dialog_visual.py ---
import json
import os
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTextEdit,
QPushButton, QFileDialog, QMessageBox, QLabel,
QListWidget, QListWidgetItem, QStackedWidget,
QWidget, QTableWidget, QTableWidgetItem,
QHeaderView, QSizePolicy, QSplitter, QGroupBox, QGridLayout,
QLineEdit, QInputDialog) # Added QInputDialog
from PyQt6.QtCore import Qt
class ConfigEditorDialogVisual(QDialog):
"""
用于可视化编辑JSON配置文件的模态对话框
"""
def __init__(self, config_file_path, parent=None):
super().__init__(parent)
self.setWindowTitle("编辑配置文件 (可视化)")
self.resize(800, 600)
self.config_file_path = config_file_path
self._config_data = {} # Internal dictionary to hold the parsed JSON
self.original_content_hash = None # To track changes
self.init_ui()
self.load_config_data()
# Connect signals after UI is initialized and data is loaded
self.parameter_list_widget.currentItemChanged.connect(self._load_parameter_details)
self._load_parameter_details(self.parameter_list_widget.currentItem(), None) # Load initial item details if any
def init_ui(self):
"""初始化界面"""
main_layout = QVBoxLayout(self)
# Splitter for parameter list on the left and details on the right
splitter = QSplitter(Qt.Orientation.Horizontal)
main_layout.addWidget(splitter)
# Left Panel: Parameter List
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
left_layout.addWidget(QLabel("参数列表:"))
self.parameter_list_widget = QListWidget()
left_layout.addWidget(self.parameter_list_widget)
# Buttons for adding/removing parameters
param_btn_layout = QHBoxLayout()
self.add_param_btn = QPushButton("添加参数")
self.remove_param_btn = QPushButton("移除参数")
param_btn_layout.addWidget(self.add_param_btn)
param_btn_layout.addWidget(self.remove_param_btn)
left_layout.addLayout(param_btn_layout)
self.add_param_btn.clicked.connect(self._add_parameter)
self.remove_param_btn.clicked.connect(self._remove_parameter)
splitter.addWidget(left_widget)
# Right Panel: Parameter Details (StackedWidget to potentially show empty state)
self.details_stack = QStackedWidget()
self.empty_details_widget = QLabel("请从左侧选择一个参数或添加新参数")
self.empty_details_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.details_stack.addWidget(self.empty_details_widget) # Index 0
self.parameter_details_widget = QWidget()
self._setup_parameter_details_ui(self.parameter_details_widget)
self.details_stack.addWidget(self.parameter_details_widget)
splitter.addWidget(self.details_stack)
# Set initial sizes for splitter panes
splitter.setSizes([200, 600])
# Bottom Buttons
button_layout = QHBoxLayout()
self.save_btn = QPushButton("保存")
self.save_as_btn = QPushButton("另存为...")
self.cancel_btn = QPushButton("取消")
self.save_btn.clicked.connect(self.save_config)
self.save_as_btn.clicked.connect(self.save_config_as)
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.save_btn)
button_layout.addWidget(self.save_as_btn)
button_layout.addStretch()
button_layout.addWidget(self.cancel_btn)
main_layout.addLayout(button_layout)
# Initial state: Disable save buttons until something is loaded/changed
self.save_btn.setEnabled(False)
self.save_as_btn.setEnabled(False)
self.remove_param_btn.setEnabled(False)
def _setup_parameter_details_ui(self, parent_widget):
"""Setup the UI for the parameter details panel"""
layout = QVBoxLayout(parent_widget)
# Fixed fields
fixed_fields_group = QGroupBox("基本信息")
fixed_fields_layout = QGridLayout(fixed_fields_group)
self.name_edit = QTextEdit()
self.name_edit.setFixedHeight(40) # Allow multi-line but not too tall
self.method_edit = QTextEdit()
self.method_edit.setFixedHeight(40)
self.param_level_edit = QTextEdit()
self.param_level_edit.setFixedHeight(40)
self.standard_edit = QTextEdit()
self.standard_edit.setFixedHeight(40)
fixed_fields_layout.addWidget(QLabel("项目名称:"), 0, 0)
fixed_fields_layout.addWidget(self.name_edit, 0, 1)
fixed_fields_layout.addWidget(QLabel("分析方法:"), 1, 0)
fixed_fields_layout.addWidget(self.method_edit, 1, 1)
fixed_fields_layout.addWidget(QLabel("项目分级:"), 2, 0)
fixed_fields_layout.addWidget(self.param_level_edit, 2, 1)
fixed_fields_layout.addWidget(QLabel("分级标准名称:"), 3, 0) # Renamed for clarity
fixed_fields_layout.addWidget(self.standard_edit, 3, 1)
layout.addWidget(fixed_fields_group)
# Standard Levels Table
levels_group = QGroupBox("标准等级")
levels_layout = QVBoxLayout(levels_group)
self.levels_table = QTableWidget(0, 2)
self.levels_table.setHorizontalHeaderLabels(["等级名称 (Key)", "标准值 (Value)"])
self.levels_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
self.levels_table.verticalHeader().setVisible(False)
levels_layout.addWidget(self.levels_table)
# Buttons for adding/removing levels
level_btn_layout = QHBoxLayout()
self.add_level_btn = QPushButton("添加等级")
self.remove_level_btn = QPushButton("移除等级")
level_btn_layout.addWidget(self.add_level_btn)
level_btn_layout.addWidget(self.remove_level_btn)
levels_layout.addLayout(level_btn_layout)
self.add_level_btn.clicked.connect(self._add_standard_level)
self.remove_level_btn.clicked.connect(self._remove_standard_level)
layout.addWidget(levels_group)
layout.addStretch() # Push everything to the top
# Connect signals for tracking changes (set dirty flag)
self.name_edit.textChanged.connect(self._set_dirty)
self.method_edit.textChanged.connect(self._set_dirty)
self.param_level_edit.textChanged.connect(self._set_dirty)
self.standard_edit.textChanged.connect(self._set_dirty)
self.levels_table.cellChanged.connect(self._set_dirty)
# Adding/removing items/levels also makes it dirty
self.add_param_btn.clicked.connect(self._set_dirty)
self.remove_param_btn.clicked.connect(self._set_dirty)
self.add_level_btn.clicked.connect(self._set_dirty)
self.remove_level_btn.clicked.connect(self._set_dirty)
def load_config_data(self):
"""加载文件内容到内部字典并填充UI"""
self._config_data = {} # Clear previous data
self.parameter_list_widget.clear()
if not self.config_file_path or not os.path.exists(self.config_file_path):
QMessageBox.warning(self, "加载错误", f"配置文件不存在或路径无效:\n{self.config_file_path}")
self.details_stack.setCurrentIndex(0) # Show empty message
return
try:
with open(self.config_file_path, 'r', encoding='utf-8') as f:
self._config_data = json.load(f)
# Store a hash or string representation for change detection
# Use json.dumps to handle ordering consistency
self.original_content_hash = hash(json.dumps(self._config_data, sort_keys=True, ensure_ascii=False))
# Populate the list widget from the loaded data
if "export_config" in self._config_data and isinstance(self._config_data["export_config"], dict):
for key in sorted(self._config_data["export_config"].keys()):
self.parameter_list_widget.addItem(key)
if self.parameter_list_widget.count() > 0:
self.parameter_list_widget.setCurrentRow(0)
self.details_stack.setCurrentIndex(1)
else:
self.details_stack.setCurrentIndex(0)
else:
QMessageBox.warning(self, "加载错误", "配置文件缺少 'export_config' 键或格式不正确。")
self.details_stack.setCurrentIndex(0)
except json.JSONDecodeError as e:
QMessageBox.critical(self, "加载错误", f"配置文件JSON格式无效:\n{e}")
self._config_data = {}
self.details_stack.setCurrentIndex(0)
except Exception as e:
QMessageBox.critical(self, "加载错误", f"无法读取或解析配置文件:\n{str(e)}")
self._config_data = {}
self.details_stack.setCurrentIndex(0)
def _load_parameter_details(self, current_item, previous_item):
"""Load details of the newly selected parameter"""
# Before loading new details, save changes from the previous item
if previous_item:
self._save_parameter_details(previous_item.text())
# Clear the details UI
self.name_edit.clear()
self.method_edit.clear()
self.param_level_edit.clear()
self.standard_edit.clear()
self.levels_table.setRowCount(0) # Clear table
if current_item:
key = current_item.text()
param_data = self._config_data.get("export_config", {}).get(key, {})
self.name_edit.setPlainText(param_data.get("项目名称", ""))
self.method_edit.setPlainText(param_data.get("分析方法", ""))
self.param_level_edit.setPlainText(param_data.get("项目分级", ""))
self.standard_edit.setPlainText(param_data.get("分级标准", ""))
# Load standard levels into the table
levels = param_data.get("标准等级", {})
if isinstance(levels, dict):
self.levels_table.setRowCount(len(levels))
# Sort levels by key (等级一, 等级二, etc.) for consistency
# for i, (level_key, level_value) in enumerate(sort(levels.items())):
for i, (level_key, level_value) in enumerate(levels.items()):
self.levels_table.setItem(i, 0, QTableWidgetItem(str(level_key)))
self.levels_table.setItem(i, 1, QTableWidgetItem(str(level_value).replace("\n", "\\n")))
else:
print(f"警告: 参数 '{key}''标准等级' 不是一个有效的字典。")
self.details_stack.setCurrentIndex(1)
self.remove_param_btn.setEnabled(True)
self._set_dirty(False)
else:
self.details_stack.setCurrentIndex(0)
self.remove_param_btn.setEnabled(False)
self._set_dirty(False)
def _save_parameter_details(self, key):
"""Save details from the UI widgets back to the internal dictionary for the given key"""
if not key or "export_config" not in self._config_data or key not in self._config_data["export_config"]:
return # Nothing to save if key is invalid or not in data
param_data = self._config_data["export_config"][key]
param_data["项目名称"] = self.name_edit.toPlainText()
param_data["分析方法"] = self.method_edit.toPlainText()
param_data["项目分级"] = self.param_level_edit.toPlainText()
param_data["分级标准"] = self.standard_edit.toPlainText()
# Save standard levels from the table
levels = {}
for row in range(self.levels_table.rowCount()):
key_item = self.levels_table.item(row, 0)
value_item = self.levels_table.item(row, 1)
if key_item and value_item:
level_key = key_item.text().strip()
level_value = value_item.text().strip().replace("\\n", "\n")
if level_key: # Only save if level key is not empty
levels[level_key] = level_value
else:
print(f"警告: 参数 '{key}' 中存在空的等级名称,该行将被忽略。")
param_data["标准等级"] = levels
# print(f"Saved details for parameter: {key}") # Debugging
def _add_parameter(self):
"""Add a new parameter item"""
key, ok = QInputDialog.getText(self, "添加新参数", "输入新的参数键 (例如: NEW):")
if ok and key:
key = key.strip()
if not key:
QMessageBox.warning(self, "输入错误", "参数键不能为空。")
return
if "export_config" not in self._config_data:
self._config_data["export_config"] = {}
if key in self._config_data["export_config"]:
QMessageBox.warning(self, "输入错误", f"参数键 '{key}' 已存在。")
return
# Add a default structure for the new parameter
self._config_data["export_config"][key] = {
"项目名称": "新项目名称",
"分析方法": "新分析方法",
"项目分级": "新项目分级",
"分级标准": "新分级标准",
"标准等级": {}
}
# Add to list widget and select it
self.parameter_list_widget.addItem(key)
# Re-sort the list
self.parameter_list_widget.sortItems()
# Find and select the new item
items = self.parameter_list_widget.findItems(key, Qt.MatchFlag.MatchExactly)
if items:
self.parameter_list_widget.setCurrentItem(items[0])
self._set_dirty()
def _remove_parameter(self):
"""Remove the currently selected parameter item"""
current_item = self.parameter_list_widget.currentItem()
if not current_item:
return # Nothing selected
key_to_remove = current_item.text()
reply = QMessageBox.question(
self, "移除参数", f"确定要移除参数 '{key_to_remove}' 吗?\n此操作不可撤销。",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
# Save details of the item being removed *before* removing it from data
# (This handles case where user edits an item then immediately removes it)
self._save_parameter_details(key_to_remove)
# Remove from internal data
if "export_config" in self._config_data and key_to_remove in self._config_data["export_config"]:
del self._config_data["export_config"][key_to_remove]
# Remove from list widget
row = self.parameter_list_widget.row(current_item)
self.parameter_list_widget.takeItem(row)
# Clear details panel and maybe select next item or show empty
self._load_parameter_details(self.parameter_list_widget.currentItem(), current_item)
self._set_dirty()
def _add_standard_level(self):
"""Add a new row to the standard levels table"""
current_item = self.parameter_list_widget.currentItem()
if not current_item:
QMessageBox.warning(self, "添加等级", "请先选择一个参数。")
return
row_count = self.levels_table.rowCount()
self.levels_table.insertRow(row_count)
# Add default items (editable)
self.levels_table.setItem(row_count, 0, QTableWidgetItem("新等级"))
self.levels_table.setItem(row_count, 1, QTableWidgetItem(""))
self._set_dirty()
def _remove_standard_level(self):
"""Remove selected rows from the standard levels table"""
# selected_rows = sorted(list(set(index.row() for index in self.levels_table.selectedIndexes())), reverse=True)
selected_rows = list(set(index.row() for index in self.levels_table.selectedIndexes()))
if not selected_rows:
QMessageBox.warning(self, "移除等级", "请选择要移除的等级行。")
return
for row in selected_rows:
self.levels_table.removeRow(row)
self._set_dirty()
def _set_dirty(self, dirty=True):
"""Set or clear the dirty flag and update button states"""
# This slot might receive a bool from cellChanged, or be called with True/False
# Ensure it's treated as a boolean
self._is_dirty = bool(dirty)
# Enable save buttons if dirty
self.save_btn.setEnabled(self._is_dirty)
self.save_as_btn.setEnabled(True)
# print(f"Dirty state: {self._is_dirty}") # Debugging
def is_dirty(self):
"""Check if the configuration has been modified"""
if not self._is_dirty:
return False
# Perform a more thorough check by saving current state to a temp dict
# and comparing its hash with the original hash
try:
# Save details of the currently selected item first
current_key = self.parameter_list_widget.currentItem()
if current_key:
self._save_parameter_details(current_key.text())
current_hash = hash(json.dumps(self._config_data, sort_keys=True, ensure_ascii=False))
return current_hash != self.original_content_hash
except Exception as e:
print(f"Error checking dirty state: {e}")
# If hashing fails, assume it's dirty to be safe
return True
def validate_config(self):
"""Validate the structure of the internal config data before saving"""
if "export_config" not in self._config_data or not isinstance(self._config_data["export_config"], dict):
QMessageBox.warning(self, "验证失败", "配置缺少主键 'export_config' 或其格式不正确。")
return False
standards_dict = self._config_data["export_config"]
error_keys = []
for key, value in standards_dict.items():
# Check for required fields and types
if not isinstance(value, dict):
error_keys.append(f"{key}: 结构不是字典")
continue
required_keys = ["项目名称", "分析方法", "项目分级", "分级标准", "标准等级"]
if not all(k in value for k in required_keys):
error_keys.append(f"{key}: 缺少必要字段")
continue
if not isinstance(value.get("标准等级"), dict):
error_keys.append(f"{key}: '标准等级' 不是字典")
continue
if error_keys:
QMessageBox.warning(self, "验证失败", "配置文件结构或内容存在问题:\n" + "\n".join(error_keys))
return False
return True
def save_config(self):
"""保存配置到当前文件路径"""
# Save details of the currently selected item before saving the whole config
current_item = self.parameter_list_widget.currentItem()
if current_item:
self._save_parameter_details(current_item.text())
if not self.validate_config():
return False
if not self.config_file_path:
# If no path is set (e.g., started with no file and added items), force Save As
return self.save_config_as()
return self._perform_save(self.config_file_path)
def save_config_as(self):
"""另存配置"""
file_path, _ = QFileDialog.getSaveFileName(
self, "另存配置文件", self.config_file_path if self.config_file_path else "",
"JSON 文件 (*.json);;所有文件 (*)"
)
if file_path:
# Save details of the currently selected item before saving the whole config
current_item = self.parameter_list_widget.currentItem()
if current_item:
self._save_parameter_details(current_item.text())
if not self.validate_config():
return False
if self._perform_save(file_path):
# Update the current file path if Save As was successful
self.config_file_path = file_path
# Reload from the new path to reset original_content_hash and dirty state
self.load_config_data()
QMessageBox.information(self, "另存成功", f"配置文件已另存为:\n{self.config_file_path}")
self.accept()
return True
return False
return False
def _perform_save(self, path):
"""Execute the saving of the config data to a file"""
try:
# Ensure directory exists
dir_name = os.path.dirname(path)
if dir_name and not os.path.exists(dir_name):
os.makedirs(dir_name, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
# Use indent=4 for readability and ensure_ascii=False for Chinese chars
json.dump(self._config_data, f, indent=4, ensure_ascii=False)
# Update the original content hash after saving
self.original_content_hash = hash(json.dumps(self._config_data, sort_keys=True, ensure_ascii=False))
self._set_dirty(False) # Clear dirty flag
# For standard save, don't close the dialog, just inform
# QMessageBox.information(self, "保存成功", f"配置文件已保存:\n{path}") # Removed msgbox for standard save
return True
except Exception as e:
QMessageBox.critical(self, "保存失败", f"保存文件时出错:\n{str(e)}")
return False
def closeEvent(self, event):
"""Handle close event, ask to save if dirty"""
if self.is_dirty():
reply = QMessageBox.question(
self, "保存更改",
"配置文件已修改,是否保存?",
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel,
QMessageBox.StandardButton.Save
)
if reply == QMessageBox.StandardButton.Save:
if self.save_config():
event.accept()
else:
event.ignore()
elif reply == QMessageBox.StandardButton.Discard:
event.accept()
else:
event.ignore()
else:
event.accept()
# Method to retrieve the potentially new file path after saving (e.g., Save As)
def get_new_config_path(self):
return self.config_file_path
# Example running (for testing this dialog itself)
if __name__ == '__main__':
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
# Create a dummy config file for testing if it doesn't exist
test_dir = "temp_config_editor_test"
os.makedirs(test_dir, exist_ok=True)
test_config_path = os.path.join(test_dir, "test_config.json")
dummy_data = {
"export_config": {
"AB": {
"项目名称": "测试硼",
"分析方法": "测试方法",
"项目分级": "硼级别",
"分级标准": "硼标准",
"标准等级": {
"等级1": ">2.00",
"等级2": "1.00~2.00"
}
},
"ZN": {
"项目名称": "测试锌",
"分析方法": "锌方法",
"项目分级": "锌级别",
"分级标准": "锌标准",
"标准等级": {
"A级": ">3.00",
"B级": "2.00~3.00",
"C级": "<2.00"
}
}
}
}
if not os.path.exists(test_config_path):
try:
with open(test_config_path, 'w', encoding='utf-8') as f:
json.dump(dummy_data, f, indent=4, ensure_ascii=False)
print(f"Created dummy config file: {test_config_path}")
except Exception as e:
print(f"Error creating dummy file: {e}")
test_config_path = "" # Clear path if creation fails
dialog = ConfigEditorDialogVisual(test_config_path)
result = dialog.exec()
if result == QDialog.DialogCode.Accepted:
final_path = dialog.get_new_config_path()
print(f"Dialog accepted. Final config path: {final_path}")
else:
print("Dialog rejected or cancelled.")
# Optional: Clean up test files/directory
# import shutil
# if os.path.exists(test_dir):
# shutil.rmtree(test_dir)
sys.exit(app.exec())

View File

@@ -0,0 +1,291 @@
import multiprocessing
import os
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLineEdit, QFileDialog, QMessageBox, QListWidget,
QComboBox, QSpinBox, QGroupBox, QFormLayout, QCheckBox)
from ..runners.script_runner import ScriptRunner
from ..components.file_list_group import FileListGroup
class ExportImageTab(QWidget):
"""导出成果图标签页"""
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent
self.runner = ScriptRunner(self)
self.connect_signals()
self.init_ui()
self.load_settings()
self.on_load_files()
def connect_signals(self):
"""连接脚本运行器信号"""
# 连接ScriptRunner的信号
self.runner.task_started.connect(self.on_script_started)
self.runner.task_finished.connect(self.on_script_finished)
self.runner.task_error.connect(self.on_script_error)
self.runner.task_log.connect(self.on_script_log)
def init_ui(self):
main_layout = QVBoxLayout(self)
# 输入输出设置组
io_group = QGroupBox("输入输出设置")
io_layout = QFormLayout()
# 地图文档文件夹选择
data_layout = QHBoxLayout()
self.input_aprx_path_edit = QLineEdit()
data_layout.addWidget(self.input_aprx_path_edit)
browse_data_btn = QPushButton("浏览...")
browse_data_btn.clicked.connect(self.browse_data_source)
data_layout.addWidget(browse_data_btn)
io_layout.addRow("工程文件夹:", data_layout)
# 输出路径选择
output_layout = QHBoxLayout()
self.output_image_path_edit = QLineEdit()
output_layout.addWidget(self.output_image_path_edit)
browse_output_btn = QPushButton("浏览...")
browse_output_btn.clicked.connect(self.browse_output)
output_layout.addWidget(browse_output_btn)
io_layout.addRow("输出文件夹:", output_layout)
io_group.setLayout(io_layout)
main_layout.addWidget(io_group)
# 添加文件列表布局
self.file_list_group = FileListGroup(self, "选择要导出的文件")
self.file_list_group.load_files.connect(self.on_load_files)
main_layout.addWidget(self.file_list_group)
# 导出设置组
export_group = QGroupBox("导出设置")
export_layout = QFormLayout()
# 格式选择
self.format_combo = QComboBox()
self.format_combo.addItems(["PDF", "PNG", "JPG", "TIFF", "EPS", "SVG", "AI"])
export_layout.addRow("导出格式:", self.format_combo)
# 分辨率设置
self.resolution_spinbox = QSpinBox()
self.resolution_spinbox.setRange(72, 1200)
self.resolution_spinbox.setSingleStep(12)
self.resolution_spinbox.setValue(300)
self.resolution_spinbox.setSuffix(" DPI")
export_layout.addRow("分辨率:", self.resolution_spinbox)
# 强制重新生成选项
self.image_force_regenerate_check = QCheckBox("强制重新生成工程文件")
self.image_force_regenerate_check.setChecked(False)
self.image_force_regenerate_check.setToolTip("勾选后将忽略已存在的工程文件,重新生成")
export_layout.addRow("处理选项:", self.image_force_regenerate_check)
# 多进程设置
self.use_multiprocessing = QCheckBox("使用多进程导出")
self.use_multiprocessing.setChecked(True)
export_layout.addRow("批量模式:", self.use_multiprocessing)
# 进程数量设置
self.process_count = QSpinBox()
self.process_count.setRange(1, multiprocessing.cpu_count())
self.process_count.setValue(max(1, multiprocessing.cpu_count() - 1)) # 默认使用CPU核心数-1
export_layout.addRow("进程数量:", self.process_count)
export_group.setLayout(export_layout)
main_layout.addWidget(export_group)
# 操作按钮
self.export_existing_btn = QPushButton("导出成果图")
self.export_existing_btn.setToolTip("仅导出输出文件夹中已存在的工程文件")
self.export_existing_btn.clicked.connect(self.on_export_existing)
main_layout.addWidget(self.export_existing_btn)
# main_layout.addStretch()
def browse_data_source(self):
"""浏览工程目录"""
initial_dir = ""
if os.path.exists(self.input_aprx_path_edit.text()):
initial_dir = os.path.dirname(self.input_aprx_path_edit.text())
dir_path = QFileDialog.getExistingDirectory(self, "选择工程目录", initial_dir)
if dir_path:
self.input_aprx_path_edit.setText(dir_path)
self.on_load_files()
def browse_output(self):
"""选择输出路径"""
initial_dir = ""
if os.path.exists(self.output_image_path_edit.text()):
initial_dir = os.path.dirname(self.output_image_path_edit.text())
dir_path = QFileDialog.getExistingDirectory(self, "选择输出路径", initial_dir)
if dir_path:
self.output_image_path_edit.setText(dir_path)
def on_load_files(self):
"""加载图层列表"""
try:
input_aprx_path = self.input_aprx_path_edit.text()
if not os.path.exists(input_aprx_path):
QMessageBox.warning(self, "错误", "请先选择输入文件夹")
return
# 清空列表
self.file_list_group.file_list.clear()
# 添加文件
for file_name in os.listdir(input_aprx_path):
file_path = os.path.join(input_aprx_path, file_name)
if file_name.endswith(".aprx") and os.path.isfile(file_path):
self.file_list_group.file_list.addItem(file_path)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个文件")
except Exception as e:
self.log_message(f"加载文件失败: {str(e)}")
def on_script_started(self, task_id, task_description):
"""脚本开始执行回调"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成回调"""
self.set_buttons_enabled(True)
if success:
QMessageBox.information(self, "成功", "成果图导出完成!")
else:
QMessageBox.warning(self, "失败", "导出过程中出现错,请查看日志详情")
def on_script_error(self, error_msg):
"""脚本执行错误回调"""
self.set_buttons_enabled(True)
self.log_message(f"错误: {error_msg}")
QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
def set_buttons_enabled(self, enabled):
"""设置按钮启用状态"""
self.export_existing_btn.setEnabled(enabled)
def validate_and_params(self):
"""验证输入参数并返回参数"""
# 验证工程文件
aprx_files = self.file_list_group.file_list.selectedItems()
if not aprx_files or len(aprx_files) == 0:
QMessageBox.warning(self, "警告", "请选择要导出的文件")
return None
# 验证工程目录
input_aprx_folder = self.input_aprx_path_edit.text()
if not os.path.exists(input_aprx_folder):
QMessageBox.warning(self, "警告", "请选择有效的工程文件目录")
return None
# 验证输出路径
output_image_path = self.output_image_path_edit.text()
if not os.path.exists(output_image_path):
QMessageBox.warning(self, "警告", "请选择输出路径")
return None
aprx_file_list = [file.text() for file in aprx_files]
output_image_path = os.path.join(output_image_path, self.format_combo.currentText().lower())
return {
'input_aprx_folder': input_aprx_folder,
'aprx_file_list': aprx_file_list,
'output_image_path': output_image_path
}
def get_layout_settings(self):
""" 获取布局设置 """
config = {
'input_aprx_path': self.input_aprx_path_edit.text(),
'output_image_path': self.output_image_path_edit.text(),
'default_format': self.format_combo.currentText(),
'resolution': self.resolution_spinbox.value(),
'force_regenerate': self.image_force_regenerate_check.isChecked(),
'use_multiprocessing': self.use_multiprocessing.isChecked(),
'process_count': self.process_count.value()
}
return config
def load_settings(self):
"""加载设置"""
if not self.main_window:
return
try:
settings = self.main_window.settings
export_image_settings = settings.get('export_image_settings', {})
self.input_aprx_path_edit.setText(export_image_settings.get('input_aprx_path', ''))
self.output_image_path_edit.setText(export_image_settings.get('output_image_path', ''))
# 设置导出格式
format_index = self.format_combo.findText(export_image_settings.get('default_format', 'PDF'))
if format_index >= 0:
self.format_combo.setCurrentIndex(format_index)
# 设置分辨率
self.resolution_spinbox.setValue(export_image_settings.get('resolution', 300))
except Exception as e:
self.log_message(f"加载设置失败: {str(e)}")
def on_export_layout(self):
"""仅导出布局按钮点击事件"""
# 验证并获取参数
layout_params = self.validate_and_params()
if not layout_params:
return
# 准备导出布局参数
layout_params['export_format'] = self.format_combo.currentText()
layout_params['resolution'] = self.resolution_spinbox.value()
layout_params['image_force_regenerate'] = self.image_force_regenerate_check.isChecked()
print(layout_params)
# 调用导出布局脚本
# self.runner.run_export_layout(layout_params)
def on_export_existing(self):
"""导出已有工程按钮点击事件"""
# 验证并获取参数
layout_params = self.validate_and_params()
if not layout_params:
return
# 确认是否导出所有工程文件
result = QMessageBox.question(
self,
"确认",
f"将导出 {len(layout_params['aprx_file_list'])} 个工程文件的布局,是否继续?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if result == QMessageBox.StandardButton.No:
return
# 准备批量导出布局参数
layout_params['export_format'] = self.format_combo.currentText()
layout_params['resolution'] = self.resolution_spinbox.value()
layout_params['image_force_regenerate'] = self.image_force_regenerate_check.isChecked()
layout_params['use_multiprocessing'] = self.use_multiprocessing.isChecked()
layout_params['process_count'] = self.process_count.value()
# 调用批量导出布局脚本
# print(layout_params)
self.runner.run_export_layout(layout_params)

View File

@@ -0,0 +1,385 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
导出地图选项卡模块
"""
import os
import arcpy
import traceback
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel,
QLineEdit, QPushButton, QCheckBox, QFileDialog, QMessageBox)
from PyQt6.QtCore import pyqtSignal
from ..components.file_list_group import FileListGroup
from ..runners.script_runner import ScriptRunner
class ExportMapTab(QWidget):
"""导出地图选项卡"""
# 定义日志信号,用于将日志消息传递给主窗口
log_signal = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent
self.setupUi()
self.load_settings()
def setupUi(self):
"""设置界面"""
# 主布局
self.main_layout = QVBoxLayout(self)
# 参数区域
self.form_layout = QFormLayout()
# 区县名称
self.county_name_edit = QLineEdit()
self.form_layout.addRow("区县名称:", self.county_name_edit)
# 配置文件
config_layout = QHBoxLayout()
self.config_file_edit = QLineEdit()
self.config_file_edit.setEnabled(False)
config_layout.addWidget(self.config_file_edit)
# self.config_file_browse = QPushButton("浏览...")
# self.config_file_browse.clicked.connect(self.on_browse_config_file)
# config_layout.addWidget(self.config_file_browse)
self.form_layout.addRow("配置文件:", config_layout)
# 模板文件
self.template_aprx_file_edit = QLineEdit()
self.template_path_browse = QPushButton("浏览...")
self.template_path_browse.clicked.connect(self.on_browse_template_path)
template_layout = QHBoxLayout()
template_layout.addWidget(self.template_aprx_file_edit)
template_layout.addWidget(self.template_path_browse)
self.form_layout.addRow("模板文件:", template_layout)
# 数据源路径
self.data_source_path_edit = QLineEdit()
self.data_source_path_browse = QPushButton("浏览...")
self.data_source_path_browse.clicked.connect(self.on_browse_data_source_path)
data_source_layout = QHBoxLayout()
data_source_layout.addWidget(self.data_source_path_edit)
data_source_layout.addWidget(self.data_source_path_browse)
self.form_layout.addRow("数据源路径:", data_source_layout)
# 符号系统路径
self.symbol_path_edit = QLineEdit()
self.symbol_path_browse = QPushButton("浏览...")
self.symbol_path_browse.clicked.connect(self.on_browse_symbol_path)
symbol_layout = QHBoxLayout()
symbol_layout.addWidget(self.symbol_path_edit)
symbol_layout.addWidget(self.symbol_path_browse)
self.form_layout.addRow("符号系统路径:", symbol_layout)
# 统计表格图片路径
self.pic_path_edit = QLineEdit()
self.pic_path_browse = QPushButton("浏览...")
self.pic_path_browse.clicked.connect(self.on_browse_pic_path)
pic_layout = QHBoxLayout()
pic_layout.addWidget(self.pic_path_edit)
pic_layout.addWidget(self.pic_path_browse)
self.form_layout.addRow("面积统计图片路径:", pic_layout)
# 输出路径
self.output_path_edit = QLineEdit()
self.output_path_browse = QPushButton("浏览...")
self.output_path_browse.clicked.connect(self.on_browse_output_path)
output_layout = QHBoxLayout()
output_layout.addWidget(self.output_path_edit)
output_layout.addWidget(self.output_path_browse)
self.form_layout.addRow("输出路径:", output_layout)
# 文件列表布局
self.file_list_group = FileListGroup(self, "选择要导出的要素类:")
self.file_list_group.load_files.connect(self.on_load_polygon)
# 选项区域
options_layout = QVBoxLayout()
# 强制重新生成选项
self.force_regenerate_check = QCheckBox("强制重新生成工程文件")
options_layout.addWidget(self.force_regenerate_check)
# 添加到主布局
self.main_layout.addLayout(self.form_layout)
self.main_layout.addWidget(self.file_list_group)
self.main_layout.addLayout(options_layout)
# 导出按钮
self.export_button = QPushButton("导出地图")
self.export_button.clicked.connect(self.on_export_map)
self.main_layout.addWidget(self.export_button)
# 状态标签
self.status_label = QLabel("状态: 就绪")
self.main_layout.addWidget(self.status_label)
def on_browse_config_file(self):
"""浏览配置文件"""
initial_dir = ""
if os.path.exists(self.config_file_edit.text()):
initial_dir = self.config_file_edit.text()
file_path, _ = QFileDialog.getOpenFileName(self, "选择配置文件", initial_dir, "JSON文件 (*.json);;所有文件 (*.*)")
if file_path:
self.config_file_edit.setText(file_path)
def on_browse_template_path(self):
"""浏览模板文件"""
initial_dir = ""
if os.path.exists(self.template_aprx_file_edit.text()):
initial_dir = self.template_aprx_file_edit.text()
file_path, _ = QFileDialog.getOpenFileName(self, "选择模板文件", initial_dir, "ArcGIS Pro工程文件 (*.aprx)")
if file_path:
self.template_aprx_file_edit.setText(file_path)
def on_browse_data_source_path(self):
"""浏览数据源路径"""
initial_dir = ""
if os.path.exists(self.data_source_path_edit.text()):
initial_dir = os.path.dirname(self.data_source_path_edit.text())
folder_path = QFileDialog.getExistingDirectory(self, "选择数据源文件夹", initial_dir)
if folder_path and folder_path.endswith(".gdb"):
self.data_source_path_edit.setText(folder_path)
# 自动加载图层列表
self.on_load_polygon()
else:
QMessageBox.warning(self, "错误", "请选择GDB的数据源。")
def on_browse_symbol_path(self):
"""浏览符号系统路径"""
initial_dir = ""
if os.path.exists(self.symbol_path_edit.text()):
initial_dir = os.path.dirname(self.symbol_path_edit.text())
folder_path = QFileDialog.getExistingDirectory(self, "选择符号系统文件夹", initial_dir)
if folder_path:
self.symbol_path_edit.setText(folder_path)
def on_browse_pic_path(self):
"""浏览符号系统路径"""
initial_dir = ""
if os.path.exists(self.pic_path_edit.text()):
initial_dir = os.path.dirname(self.pic_path_edit.text())
folder_path = QFileDialog.getExistingDirectory(self, "选择符号系统文件夹", initial_dir)
if folder_path:
self.pic_path_edit.setText(folder_path)
def on_browse_output_path(self):
"""浏览输出路径"""
initial_dir = ""
if os.path.exists(self.output_path_edit.text()):
initial_dir = os.path.dirname(self.output_path_edit.text())
folder_path = QFileDialog.getExistingDirectory(self, "选择输出文件夹", initial_dir)
if folder_path.endswith(".gdb"):
QMessageBox.warning(self, "警告", "请选择有效的输出路径")
return
self.output_path_edit.setText(folder_path)
def on_load_polygon(self):
"""加载图层列表"""
try:
data_source_paths = self.data_source_path_edit.text()
# 清空列表
self.file_list_group.file_list.clear()
# 添加图层
if arcpy.Exists(data_source_paths):
arcpy.env.workspace = data_source_paths
for polygon in arcpy.ListFeatureClasses(feature_type="Polygon"):
self.file_list_group.file_list.addItem(polygon)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个图层")
except Exception as e:
self.log_error(f"加载图层列表失败: {str(e)}")
traceback.print_exc()
def get_selected_polygon(self):
"""获取选中的图层列表"""
selected_items = self.file_list_group.file_list.selectedItems()
return [item.text() for item in selected_items]
def on_export_map(self):
"""导出地图按钮点击事件"""
try:
# 验证并获取参数
params = self.validate_and_params()
if not params:
return
polygon_list = params.get('polygon_list')
# 清空日志
self.log_message("开始导出工程文件...")
self.log_message(f"选中的图层: {polygon_list}")
# 创建导出线程
self.runner = ScriptRunner()
self.runner.task_log.connect(self.on_script_log)
self.runner.task_error.connect(self.log_error)
self.runner.task_finished.connect(self.on_script_finished)
# 禁用导出按钮
self.export_button.setEnabled(False)
self.status_label.setText("状态: 正在导出...")
# 运行导出脚本(使用外部窗口)
# print(params)
self.runner.run_export_map(params)
except Exception as e:
self.log_error(f"导出准备过程出错: {str(e)}")
traceback.print_exc()
def log_message(self, message):
"""添加日志消息"""
# 如果父窗口存在,使用父窗口的日志功能
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
# 否则发射自己的信号
self.log_signal.emit(message)
def log_error(self, message):
"""添加错误日志"""
self.log_message(f"错误: {message}")
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""导出完成事件处理"""
# 恢复导出按钮
self.export_button.setEnabled(True)
if success:
self.status_label.setText("状态: 导出完成")
QMessageBox.information(self, "成功", "地图导出完成!")
else:
self.status_label.setText("状态: 导出失败")
QMessageBox.warning(self, "失败", "地图导出过程中出现错误,请查看日志详情")
def validate_and_params(self):
# 获取输入参数
county_name = self.county_name_edit.text().strip()
if not county_name:
QMessageBox.warning(self, "错误", "请输入区县名称")
return
# 获取面要素列表
polygon_list = self.get_selected_polygon()
if not polygon_list:
QMessageBox.warning(self, "错误", "请选择至少一个要素")
return
# 获取配置文件
config_file = self.config_file_edit.text()
if not os.path.exists(config_file):
QMessageBox.warning(self, "错误", "配置文件不存在")
return
# 获取模板文件
template_aprx_file = self.template_aprx_file_edit.text()
if not os.path.exists(template_aprx_file):
QMessageBox.warning(self, "错误", "模板文件不存在")
return
# 获取输出路径
output_path = self.output_path_edit.text()
if not output_path:
QMessageBox.warning(self, "错误", "请选择输出路径")
return
# 获取数据源路径
data_source_path = self.data_source_path_edit.text()
if not os.path.exists(data_source_path) and not data_source_path.endswith(".gdb"):
QMessageBox.warning(self, "错误", "请确认是否是有效数据源")
return
# 获取符号系统路径
symbol_path = self.symbol_path_edit.text()
if not os.path.exists(symbol_path):
QMessageBox.warning(self, "错误", "符号系统路径不存在")
return
# 获取图片源路径
pic_path = self.pic_path_edit.text()
if not os.path.exists(pic_path):
QMessageBox.warning(self, "错误", "符号系统路径不存在")
return
# 是否强制重新生成
force_regenerate = self.force_regenerate_check.isChecked()
# 准备参数
return {
'config_file': config_file,
'county_name': county_name,
'polygon_list': polygon_list,
'template_aprx_file': template_aprx_file,
'output_path': output_path,
'data_source_path': data_source_path,
'symbol_path': symbol_path,
'force_regenerate': force_regenerate,
'pic_path': pic_path
}
def load_settings(self):
"""从主窗口加载设置"""
try:
if not self.main_window or not hasattr(self.main_window, 'settings'):
return
settings = self.main_window.settings
if 'export_map_settings' in settings:
paths = settings['export_map_settings']
# 直接使用get方法设置默认值
self.config_file_edit.setText(paths.get("config_file", ""))
self.county_name_edit.setText(paths.get("county_name", ""))
self.template_aprx_file_edit.setText(paths.get("template_aprx_file", ""))
self.output_path_edit.setText(paths.get("output_path", ""))
self.data_source_path_edit.setText(paths.get("data_source_path", ""))
self.symbol_path_edit.setText(paths.get("symbol_path", ""))
self.pic_path_edit.setText(paths.get("pic_path", ""))
except Exception as e:
self.log_error(f"加载设置失败: {str(e)}")
def get_settings(self):
"""获取当前设置,供主窗口保存使用"""
return {
'config_file': self.config_file_edit.text(),
'county_name': self.county_name_edit.text(),
'template_aprx_file': self.template_aprx_file_edit.text(),
'output_path': self.output_path_edit.text(),
'data_source_path': self.data_source_path_edit.text(),
'symbol_path': self.symbol_path_edit.text(),
'pic_path': self.pic_path_edit.text()
}
def update_config_file(self, config_file):
self.config_file_edit.setText(config_file)

View File

@@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
"""
栅格处理共享组件: 提供栅格重分类、栅格转矢量和小面积图斑消除的共享UI组件
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox,
QGroupBox, QTableWidget, QTableWidgetItem, QHeaderView,
QPushButton, QSpinBox, QDoubleSpinBox, QComboBox
)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QDoubleValidator
class ReclassificationWidget(QWidget):
"""重分类参数组件"""
paramsChanged = pyqtSignal() # 参数变化信号
def __init__(self, parent=None):
super(ReclassificationWidget, self).__init__(parent)
self.init_ui()
def init_ui(self):
"""初始化UI"""
layout = QVBoxLayout(self)
group_box = QGroupBox("重分类映射表")
group_layout = QVBoxLayout(group_box)
self.reclass_table = QTableWidget()
self.reclass_table.setColumnCount(3)
self.reclass_table.setHorizontalHeaderLabels(["", "", "新值"])
# PyQt6 中horizontalHeader().setSectionResizeMode 接受 QHeaderView.ResizeMode 枚举
self.reclass_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) # type: ignore
# PyQt6 中setSelectionBehavior 接受 QAbstractItemView.SelectionBehavior 枚举
from PyQt6.QtWidgets import QAbstractItemView # 需要导入 QAbstractItemView
self.reclass_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) # 整行选择
# 连接单元格变化信号
self.reclass_table.itemChanged.connect(self.on_item_changed)
# 添加默认行
self.add_row()
group_layout.addWidget(self.reclass_table)
# 添加/删除行按钮
btn_layout = QHBoxLayout()
add_row_btn = QPushButton("添加行")
del_row_btn = QPushButton("删除行")
# PyQt6 信号槽连接
add_row_btn.clicked.connect(self.add_row)
del_row_btn.clicked.connect(self.del_row)
btn_layout.addWidget(add_row_btn)
btn_layout.addWidget(del_row_btn)
group_layout.addLayout(btn_layout)
layout.addWidget(group_box)
def add_row(self, from_value=None, to_value=None, new_value=None):
"""添加一行重分类规则"""
row_count = self.reclass_table.rowCount()
self.reclass_table.insertRow(row_count)
# 设置默认值或传入的值
from_item = QTableWidgetItem(str(from_value) if from_value is not None else "")
to_item = QTableWidgetItem(str(to_value) if to_value is not None else "")
new_value_item = QTableWidgetItem(str(new_value) if new_value is not None else "")
# 设置数值验证器 (QTableWidgetItem 本身没有 setValidator 方法)
# 如果需要验证,可以考虑使用代理 (Delegate)
self.reclass_table.setItem(row_count, 0, from_item)
self.reclass_table.setItem(row_count, 1, to_item)
self.reclass_table.setItem(row_count, 2, new_value_item)
def del_row(self):
"""删除选中的行"""
selected_rows = self.reclass_table.selectedIndexes()
if not selected_rows:
return
# 从最后一个开始删除,以避免索引变化问题
rows_to_delete = sorted(list(set([index.row() for index in selected_rows])), reverse=True)
for row in rows_to_delete:
self.reclass_table.removeRow(row)
# 确保至少有一行
if self.reclass_table.rowCount() == 0:
self.add_row()
self.paramsChanged.emit() # 参数变化
def on_item_changed(self, item):
"""当表格单元格内容变化时触发"""
# 在这里可以添加一些简单的验证,例如检查是否为数字
try:
text = item.text()
if text:
if item.column() in [0, 1]: # "从" 和 "到" 列
float(text) # 尝试转换为浮点数
elif item.column() == 2: # "新值" 列
int(text) # 尝试转换为整数
except ValueError:
# 如果转换失败,可以给用户提示或清空单元格
# item.setText("") # 或者其他处理方式
pass # 暂时不做处理依赖后期的validate_inputs
self.paramsChanged.emit() # 参数变化
def get_remap_table(self):
"""从表格获取重分类映射表"""
remap_table = []
for row in range(self.reclass_table.rowCount()):
try:
from_item = self.reclass_table.item(row, 0)
to_item = self.reclass_table.item(row, 1)
new_value_item = self.reclass_table.item(row, 2)
# 检查是否为空或无效
if not from_item or not from_item.text() or \
not to_item or not to_item.text() or \
not new_value_item or not new_value_item.text():
continue # 跳过空行
from_value_str = from_item.text()
to_value_str = to_item.text()
new_value_str = new_value_item.text()
# 处理特殊值,例如 "inf" 表示无穷大
from_value = float(from_value_str) if from_value_str.lower() not in ('inf', '-inf') else (float('-inf') if from_value_str.lower() == '-inf' else float('inf'))
to_value = float(to_value_str) if to_value_str.lower() not in ('inf', '-inf') else (float('-inf') if to_value_str.lower() == '-inf' else float('inf'))
new_value = int(new_value_str)
# 验证范围的有效性
if from_value > to_value:
print(f"警告: 无效范围 {from_value} > {to_value},跳过此行")
continue # 跳过无效范围的行
remap_table.append([from_value, to_value, new_value])
except (ValueError, TypeError) as e:
print(f"警告: 读取重分类表格时遇到无效数值或类型,跳过此行. 错误: {e}")
continue
return remap_table
def set_table_data(self, data):
"""设置重分类表格数据"""
self.reclass_table.clearContents()
self.reclass_table.setRowCount(0)
if not data:
self.add_row() # 添加一行默认行
return
for row_data in data:
if len(row_data) == 3:
from_value, to_value, new_value = row_data
self.add_row(from_value, to_value, new_value)
class VectorParamsWidget(QWidget):
"""矢量化参数组件"""
paramsChanged = pyqtSignal() # 参数变化信号
def __init__(self, parent=None):
super(VectorParamsWidget, self).__init__(parent)
self.init_ui()
def init_ui(self):
"""初始化UI"""
layout = QHBoxLayout(self)
# 矢量化参数
# 简化多边形边界
simplify_layout = QHBoxLayout()
simplify_layout.addWidget(QLabel("简化多边形边界:"))
self.simplify_check = QCheckBox()
self.simplify_check.setChecked(False) # 默认不选中
# PyQt6 信号槽连接
self.simplify_check.stateChanged.connect(self.on_simplify_changed)
simplify_layout.addWidget(self.simplify_check)
simplify_layout.addStretch()
layout.addLayout(simplify_layout)
# 最小面积阈值
min_area_layout = QHBoxLayout()
min_area_layout.addWidget(QLabel("最小面积:"))
self.min_area_spin = QDoubleSpinBox()
self.min_area_spin.setRange(0.1, 1000000)
self.min_area_spin.setValue(100)
self.min_area_spin.setSingleStep(10)
# PyQt6 信号槽连接
self.min_area_spin.valueChanged.connect(lambda: self.paramsChanged.emit())
min_area_layout.addWidget(self.min_area_spin)
# 面积单位
self.area_unit_combo = QComboBox()
self.area_unit_combo.addItems(["平方米", "公顷", "平方公里"])
self.area_unit_map = {
"平方米": "SQUARE_METERS",
"公顷": "HECTARES",
"平方公里": "SQUARE_KILOMETERS"
}
# PyQt6 信号槽连接
self.area_unit_combo.currentIndexChanged.connect(lambda: self.paramsChanged.emit())
min_area_layout.addWidget(self.area_unit_combo)
layout.addLayout(min_area_layout)
def on_simplify_changed(self, state):
"""当简化选项状态变化时触发"""
# PyQt6 中 Qt.Checked 仍然可用
is_checked = state == Qt.CheckState.Checked
self.paramsChanged.emit()
def get_params(self):
"""获取矢量化参数"""
is_simplify = self.simplify_check.isChecked()
return {
"simplify": is_simplify,
"min_area": self.min_area_spin.value(),
"area_unit": self.area_unit_map[self.area_unit_combo.currentText()]
}
def set_params(self, params):
"""设置矢量化参数"""
if "simplify" in params:
simplify_value = params["simplify"]
self.simplify_check.setChecked(simplify_value)
if "min_area" in params:
self.min_area_spin.setValue(params["min_area"])
if "area_unit" in params:
for text, value in self.area_unit_map.items():
if value == params["area_unit"]:
index = self.area_unit_combo.findText(text)
if index >= 0:
self.area_unit_combo.setCurrentIndex(index)
break

519
tools/ui/tabs/raster_tab.py Normal file
View File

@@ -0,0 +1,519 @@
# -*- coding: utf-8 -*-
"""
栅格处理界面: 提供栅格重分类、栅格转矢量和小面积图斑消除的界面操作
"""
import os
import json
import arcpy
import traceback
import multiprocessing
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QGridLayout, QCheckBox,
QGroupBox, QMessageBox, QSpinBox)
from PyQt6.QtCore import pyqtSignal
from tools.core.utils import common_utils
from tools.ui.runners.script_runner import ScriptRunner
from .config_editor_dialog import ConfigEditorDialogVisual
from .raster_processing_common import VectorParamsWidget
from ..components.file_list_group import FileListGroup
class RasterTab(QWidget):
value_changed = pyqtSignal(str)
"""栅格处理窗口部件类"""
def __init__(self, parent=None):
super(RasterTab, self).__init__(parent)
self.main_window = parent
self.init_ui()
self.setWindowTitle("栅格处理")
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout(self)
# 批量处理区域
self.batch_mode_group = QGroupBox("重分类并消除小面积图斑")
batch_layout = QGridLayout()
# 配置文件选择 (公用)
batch_layout.addWidget(QLabel("配置文件:"), 0, 0)
self.config_file_edit = QLineEdit()
self.config_file_edit.textChanged.connect(self.on_config_file_changed)
batch_layout.addWidget(self.config_file_edit, 0, 1)
self.browse_config_btn = QPushButton("浏览...")
self.browse_config_btn.clicked.connect(self.browse_config_file)
batch_layout.addWidget(self.browse_config_btn, 0, 2)
# 添加编辑配置文件的按钮 <--- Add this button
self.edit_config_btn = QPushButton("编辑配置内容...")
self.edit_config_btn.setEnabled(False)
self.edit_config_btn.clicked.connect(self.open_config_editor)
batch_layout.addWidget(self.edit_config_btn, 0, 3)
# 输入文件夹
batch_layout.addWidget(QLabel("栅格文件夹:"), 1, 0)
self.input_folder_edit = QLineEdit()
batch_layout.addWidget(self.input_folder_edit, 1, 1, 1, 2)
self.browse_input_folder_btn = QPushButton("浏览...")
self.browse_input_folder_btn.clicked.connect(self.browse_input_folder)
batch_layout.addWidget(self.browse_input_folder_btn, 1, 3)
# 批量输出文件夹
batch_layout.addWidget(QLabel("输出文件夹:"), 2, 0)
self.batch_output_folder_edit = QLineEdit()
batch_layout.addWidget(self.batch_output_folder_edit, 2, 1, 1, 2)
self.browse_batch_output_btn = QPushButton("浏览...")
self.browse_batch_output_btn.clicked.connect(self.browse_batch_output_folder)
batch_layout.addWidget(self.browse_batch_output_btn, 2, 3)
# 选择裁剪面checkbox
self.clip_checkbox = QCheckBox("启用裁剪")
self.clip_checkbox.setChecked(False)
self.clip_checkbox.stateChanged.connect(self.on_clip_checkbox_changed)
batch_layout.addWidget(self.clip_checkbox, 3, 0)
self.clip_features_edit = QLineEdit()
self.clip_features_edit.setVisible(False)
batch_layout.addWidget(self.clip_features_edit, 3, 1, 1, 2)
self.clip_features_btn = QPushButton("选择裁剪面")
self.clip_features_btn.clicked.connect(self.browse_clip_features)
self.clip_features_btn.setVisible(False)
batch_layout.addWidget(self.clip_features_btn, 3, 3)
self.batch_mode_group.setLayout(batch_layout)
# 文件列表显示组
self.file_list_group = FileListGroup(self, "选择要处理的栅格")
self.file_list_group.load_files.connect(self.on_load_raster)
# --- 处理参数设置 ---
param_group = QGroupBox("处理参数")
param_layout = QVBoxLayout(param_group)
# 矢量化参数组件 (包含简化和最小面积阈值)
self.vector_params = VectorParamsWidget()
param_layout.addWidget(self.vector_params)
# 多进程设置
export_layout = QHBoxLayout()
mutiprocess_layout = QHBoxLayout()
mutiprocess_layout.addWidget(QLabel("使用多进程导出:"))
self.mutiprocess_check = QCheckBox()
self.mutiprocess_check.setChecked(True)
mutiprocess_layout.addWidget(self.mutiprocess_check)
mutiprocess_layout.addStretch()
export_layout.addLayout(mutiprocess_layout)
# 进程数量设置
process_count_layout = QHBoxLayout()
process_count_layout.addWidget(QLabel("进程数量:"))
self.process_count = QSpinBox()
self.process_count.setRange(1, multiprocessing.cpu_count())
self.process_count.setValue(max(1, multiprocessing.cpu_count() - 1)) # 默认使用CPU核心数-1
self.process_count.setFixedWidth(180)
process_count_layout.addWidget(self.process_count)
export_layout.addLayout(process_count_layout)
param_layout.addLayout(export_layout)
# 操作按钮
btn_layout = QHBoxLayout()
self.process_btn = QPushButton("开始处理")
self.process_btn.clicked.connect(self.on_start_processing)
self.cancel_btn = QPushButton("取消")
# self.cancel_btn.clicked.connect(self.close)
btn_layout.addWidget(self.process_btn)
btn_layout.addWidget(self.cancel_btn)
# 添加所有组件到主布局
main_layout.addWidget(self.batch_mode_group)
main_layout.addWidget(self.file_list_group)
main_layout.addWidget(param_group)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
def browse_input_folder(self):
"""浏览选择输入文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择栅格文件夹")
if folder_path:
self.input_folder_edit.setText(folder_path)
self.on_load_raster()
def browse_batch_output_folder(self):
"""浏览选择批量处理输出文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择批量处理输出文件夹")
if folder_path:
if folder_path.endswith(".gdb"):
QMessageBox.warning(self, "错误", "请选择一个文件夹,而不是一个数据库文件")
return
self.batch_output_folder_edit.setText(folder_path)
def browse_config_file(self):
"""浏览选择配置文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择配置文件", "",
"JSON 文件 (*.json);;所有文件 (*)"
)
if file_path:
self.config_file_edit.setText(file_path)
self.validate_config_file(file_path)
self.edit_config_btn.setEnabled(True)
def browse_clip_features(self):
"""浏览选择裁剪面 (支持 shapefile)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择裁剪要素 (Shapefile)", "",
"Shapefile 文件 (*.shp);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(".shp"):
self.clip_features_edit.setText(file_path)
self.log_message(f"已选择 Shapefile: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择 .shp 文件。")
self.clip_features_edit.clear()
def validate_config_file(self, config_file_path):
"""对文件进行快速检查,以查看它是否类似于预期的配置结构。"""
if not os.path.exists(config_file_path):
return False
try:
with open(config_file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
if "export_config" in config and isinstance(config["export_config"], dict):
if config["export_config"]:
first_item_value = next(iter(config["export_config"].values()))
if isinstance(first_item_value, dict) and all(k in first_item_value for k in ["项目名称", "标准等级"]):
self.log_message(f"配置文件 '{os.path.basename(config_file_path)}' 加载成功。")
return True
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 内容结构可能不匹配预期。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 中的 'export_config' 部分为空。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 缺少 'export_config' 键或格式不正确。")
return False
except json.JSONDecodeError:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 不是有效的 JSON 格式。")
return False
except Exception as e:
self.log_message(f"警告: 检查配置文件 '{os.path.basename(config_file_path)}' 时出错: {str(e)}")
return False
def open_config_editor(self):
"""打开配置文件编辑器对话框"""
config_path = self.config_file_edit.text()
if not config_path:
reply = QMessageBox.question(self, "编辑配置", "没有选择配置文件,是否创建一个新配置文件?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.No:
return
config_path = ""
# 使用配置编辑对话框
dialog = ConfigEditorDialogVisual(config_path, self)
if dialog.exec() == ConfigEditorDialogVisual.DialogCode.Accepted:
new_path = dialog.get_new_config_path()
if new_path and new_path != self.config_file_edit.text():
self.config_file_edit.setText(new_path)
self.validate_config_file(new_path)
def on_config_file_changed(self):
text = self.config_file_edit.text()
self.value_changed.emit(text)
def on_clip_checkbox_changed(self):
clip_checkbox_state = self.clip_checkbox.isChecked()
self.clip_features_edit.setVisible(clip_checkbox_state)
self.clip_features_btn.setVisible(clip_checkbox_state)
def on_load_raster(self):
"""加载图层列表"""
try:
data_source_paths = self.input_folder_edit.text()
self.file_list_group.load_file_btn.setEnabled(False)
# 清空列表
self.file_list_group.file_list.clear()
if not data_source_paths or not arcpy.Exists(data_source_paths):
self.log_message(f"输入路径不存在或不是有效的ArcGIS工作空间/文件夹: {data_source_paths}")
return
arcpy.env.workspace = data_source_paths
rasters = arcpy.ListRasters()
if rasters:
for raster in rasters:
self.file_list_group.file_list.addItem(raster)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个图层")
else:
self.log_message(f"文件夹 '{os.path.basename(data_source_paths)}' 中没有找到栅格图层。")
except Exception as e:
self.log_message(f"加载图层列表失败: {str(e)}")
traceback.print_exc()
finally:
self.file_list_group.load_file_btn.setEnabled(True)
def validate_inputs(self):
"""验证输入参数"""
# 验证配置文件路径
config_path = self.config_file_edit.text()
if not config_path or not os.path.exists(config_path):
QMessageBox.warning(self, "输入错误", "请选择有效的配置文件")
return False
try:
with open(config_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法加载或解析配置文件: {e}")
return False
if "export_config" not in config_data or not isinstance(config_data["export_config"], dict):
QMessageBox.warning(self, "输入错误", "配置文件的内容无效或缺少 'export_config' 部分。")
return False
standards_dict = config_data["export_config"]
if not standards_dict:
QMessageBox.warning(self, "输入错误", "配置文件中的 'export_config' 部分为空,没有定义任何处理参数。")
return False
# 验证'标准等级'用于配置重分类表
error_keys_remap = []
for key, value in standards_dict.items():
try:
levels_data = value.get("标准等级", {})
if not isinstance(levels_data, dict):
error_keys_remap.append(f"{key} ('标准等级' 格式不正确)")
continue
if not common_utils.create_remap_table(levels_data):
error_keys_remap.append(f"{key} ('标准等级' 内容无效)")
except Exception:
error_keys_remap.append(f"{key} (验证出错)")
if error_keys_remap:
QMessageBox.warning(self, "输入错误", "配置文件中部分参数的 '标准等级' 数据无效:\n" + "\n".join(error_keys_remap))
return False
# 验证输入、输出文件夹
if not self.input_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择输入栅格文件夹")
return False
if not self.batch_output_folder_edit.text() or self.batch_output_folder_edit.text().endswith('.gdb'):
QMessageBox.warning(self, "输入错误", "请选择批量处理输出文件夹")
return False
if self.clip_checkbox.isChecked():
clip_path = self.clip_features_edit.text()
if not clip_path or not arcpy.Exists(clip_path):
QMessageBox.warning(self, "输入错误", "已启用裁剪,但裁剪要素路径无效或不存在")
return False
# 3. 验证裁剪要素是否可用
if self.clip_checkbox.isChecked():
clip_path = self.clip_features_edit.text()
if not clip_path:
QMessageBox.warning(self, "输入错误", "已启用裁剪,但裁剪要素路径为空。")
return False
# Use arcpy.Exists and Describe to validate the path regardless of whether it's SHP or GDB/FeatureClass
if not arcpy.Exists(clip_path):
QMessageBox.warning(self, "输入错误", f"已启用裁剪,但裁剪要素路径不存在:\n{clip_path}")
return False
# Optional: Check if the path points to a Feature Class (SHP or inside GDB)
try:
desc = arcpy.Describe(clip_path)
if desc.dataType != 'ShapeFile':
QMessageBox.warning(self, "输入错误", f"已启用裁剪,但选择的路径不是一个要素类:\n{clip_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法描述裁剪要素路径,请检查是否为有效要素类:\n{clip_path}\n错误: {e}")
return False
# 验证是否选择文件
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if not selected_files:
QMessageBox.warning(self, "输入错误", "请在栅格列表中勾选要处理的文件。")
return False
return True
def on_start_processing(self):
"""开始处理栅格数据"""
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
# 创建的导出线程
self.script_runner = ScriptRunner()
# 连接ScriptRunner的信号
self.script_runner.task_started.connect(self.on_script_started)
self.script_runner.task_finished.connect(self.on_script_finished)
self.script_runner.task_error.connect(self.on_script_error)
self.script_runner.task_log.connect(self.on_script_log)
self.script_runner.manager_log.connect(self.log_message)
# 获取公共参数 (从 VectorParamsWidget 获取)
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if self.mutiprocess_check.isChecked():
self.script_runner.set_max_concurrent(self.process_count.value())
else:
self.script_runner.set_max_concurrent(1)
# 调用批量处理脚本
for raster_file in selected_files:
params = {
"settings_path": settings_file,
"input_raster": raster_file,
}
task_id = self.script_runner.run_process_raster(params)
# if task_id:
# self.log_message(f"[GUI] 任务 {task_id} 处理 {raster_file} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def get_raster_settings(self):
"""保存当前配置到JSON文件"""
# 从 VectorParamsWidget 获取参数
vector_params = self.vector_params.get_params()
config = {
"input_folder": self.input_folder_edit.text(),
"batch_output_folder": self.batch_output_folder_edit.text(),
"config_file_path": self.config_file_edit.text(),
"simplify": vector_params["simplify"], # 从 VectorParamsWidget 获取
"min_area": vector_params["min_area"], # 从 VectorParamsWidget 获取
"area_unit": vector_params["area_unit"], # 从 VectorParamsWidget 获取
"clip_features": self.clip_features_edit.text() if self.clip_checkbox.isChecked() else "",
"clip_enabled": self.clip_checkbox.isChecked()
}
return config
def load_settings(self, settings):
"""从字典加载配置"""
try:
# 批处理参数
self.input_folder_edit.setText(settings.get("input_folder", ""))
self.batch_output_folder_edit.setText(settings.get("batch_output_folder", ""))
# 配置文件路径和加载
config_file_path = settings.get("config_file_path", "")
self.config_file_edit.setText(config_file_path)
self.edit_config_btn.setEnabled(bool(config_file_path) and os.path.exists(config_file_path))
if config_file_path:
self.validate_config_file(config_file_path)
clip_enabled = settings.get("clip_enabled", False)
self.clip_checkbox.setChecked(clip_enabled)
self.clip_features_edit.setText(settings.get("clip_features", ""))
self.on_clip_checkbox_changed()
# 矢量化参数 (使用 VectorParamsWidget 的 set_params 方法)
vector_params_config = {
"simplify": settings.get("simplify", False), # 注意默认值需要与 VectorParamsWidget 匹配
"min_area": settings.get("min_area", 1000),
"area_unit": settings.get("area_unit", "平方米")
}
self.vector_params.set_params(vector_params_config)
if self.input_folder_edit.text():
self.on_load_raster()
return True
except Exception as e:
QMessageBox.critical(self, "加载配置错误", f"加载配置时出错: {str(e)}")
return False
def set_buttons_enabled(self, enabled):
"""设置按钮是否可用"""
self.process_btn.setEnabled(enabled)
def on_script_started(self, task_id, task_description):
"""脚本开始执行"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成"""
try:
# 将shp文件导入到GDB数据库
if self.script_runner.get_running_tasks() == []:
output_path = self.batch_output_folder_edit.text()
shape_files = []
for file in os.listdir(output_path):
if file.endswith("eliminate.shp"):
shape_files.append(os.path.join(output_path, file))
if shape_files:
try:
path_result = arcpy.management.CreateFileGDB(output_path, "过程数据(消除小图斑).gdb", "CURRENT")
# 批量导入数据
arcpy.conversion.FeatureClassToGeodatabase(shape_files, path_result)
arcpy.management.Delete(shape_files)
QMessageBox.information(self, "成功", f"数据已保存到{path_result}")
self.set_buttons_enabled(True)
except Exception as e:
self.log_message(f"批量导入数据出错: {str(e)}")
except Exception as e:
self.set_buttons_enabled(True)
self.log_message(f"批量导入数据出错: {str(e)}")
def on_script_error(self, error_msg):
"""脚本执行出错"""
self.set_buttons_enabled(True)
self.log_message(f"错误: {error_msg}")
QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = RasterTab()
window.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,353 @@
# -*- coding: utf-8 -*-
"""
栅格处理界面: 提供栅格重分类、栅格转矢量和小面积图斑消除的界面操作
"""
import os
import traceback
import arcpy
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QGridLayout,
QGroupBox, QMessageBox)
from tools.ui.components.file_list_group import FileListGroup
from tools.ui.runners.script_runner import ScriptRunner
from tools.ui.tabs.config_editor_dialog import ConfigEditorDialogVisual
class SoilPropStatsTab(QWidget):
"""栅格处理窗口部件类"""
def __init__(self, parent=None):
super(SoilPropStatsTab, self).__init__(parent)
self.main_window = parent
# 创建的导出线程
self.script_runner = ScriptRunner()
self.connect_signals()
self.init_ui()
self.setWindowTitle("土壤属性统计")
def connect_signals(self):
# 连接ScriptRunner的信号
self.script_runner.task_started.connect(self.on_script_started)
self.script_runner.task_finished.connect(self.on_script_finished)
self.script_runner.task_error.connect(self.on_script_error)
self.script_runner.task_log.connect(self.on_script_log)
self.script_runner.manager_log.connect(self.log_message)
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout(self)
# 批量处理区域
self.batch_mode_group = QGroupBox("批量处理设置")
batch_layout = QGridLayout()
# 配置文件选择 (公用)
batch_layout.addWidget(QLabel("配置文件:"), 0, 0)
self.config_file_edit = QLineEdit()
self.config_file_edit.setEnabled(False)
batch_layout.addWidget(self.config_file_edit, 0, 1,1,2)
# self.browse_config_btn = QPushButton("浏览...")
# self.browse_config_btn.clicked.connect(self.browse_config_file)
# self.browse_config_btn.setEnabled(False)
# batch_layout.addWidget(self.browse_config_btn, 0, 2)
# # 添加编辑配置文件的按钮 <--- Add this button
# self.edit_config_btn = QPushButton("编辑配置内容...")
# self.edit_config_btn.setEnabled(False)
# self.edit_config_btn.clicked.connect(self.open_config_editor)
# batch_layout.addWidget(self.edit_config_btn, 0, 3)
# 输入行政区名称
batch_layout.addWidget(QLabel("行政区名称:"), 1, 0)
self.input_xzqmc_edit = QLineEdit()
batch_layout.addWidget(self.input_xzqmc_edit, 1, 1)
# 工作空间路径 - 使用水平布局容器
label_container = QWidget()
label_layout = QHBoxLayout(label_container)
label_layout.setContentsMargins(0, 0, 0, 0) # 去掉边距
label = QLabel("")
label.setToolTip("""<font color='blue'>各属性样点<br>地类图斑<br>土壤类型图<br>母岩母质</font>""")
text_label = QLabel("数据源路径:")
label_layout.addWidget(label)
label_layout.addWidget(text_label)
batch_layout.addWidget(label_container, 2, 0) # 将整个容器放在第2行第0列
self.data_source_path_edit = QLineEdit()
batch_layout.addWidget(self.data_source_path_edit, 2, 1)
self.browse_input_workspace_btn = QPushButton("选择GDB")
self.browse_input_workspace_btn.clicked.connect(self.browse_input_workspace)
batch_layout.addWidget(self.browse_input_workspace_btn, 2, 2)
# 选择三普属性栅格文件夹
batch_layout.addWidget(QLabel("三普属性栅格:"), 3, 0)
self.input_sanpu_prop_tif_edit = QLineEdit()
batch_layout.addWidget(self.input_sanpu_prop_tif_edit, 3, 1)
self.browse_input_sanpu_prop_tif_btn = QPushButton("选择文件夹")
self.browse_input_sanpu_prop_tif_btn.clicked.connect(self.browse_input_sanpu_prop_tif)
batch_layout.addWidget(self.browse_input_sanpu_prop_tif_btn, 3, 2)
# 选择三普土壤属性重分类后的面要素
batch_layout.addWidget(QLabel("属性重分类面:"), 4, 0)
self.input_reclassed_feature_folder_edit = QLineEdit()
batch_layout.addWidget(self.input_reclassed_feature_folder_edit, 4, 1)
self.browse_input_reclassed_feature_folder_btn = QPushButton("选择文件夹")
self.browse_input_reclassed_feature_folder_btn.clicked.connect(self.browse_input_reclassed_feature_folder)
batch_layout.addWidget(self.browse_input_reclassed_feature_folder_btn, 4, 2)
# 批量输出文件夹
batch_layout.addWidget(QLabel("输出文件夹:"), 5, 0)
self.batch_output_folder_edit = QLineEdit()
batch_layout.addWidget(self.batch_output_folder_edit, 5, 1)
self.browse_batch_output_btn = QPushButton("选择文件夹")
self.browse_batch_output_btn.clicked.connect(self.browse_batch_output_folder)
batch_layout.addWidget(self.browse_batch_output_btn, 5, 2)
self.batch_mode_group.setLayout(batch_layout)
# 文件列表布局
self.file_list_group = FileListGroup(self, "选择要导出的三普属性样点:")
self.file_list_group.load_files.connect(self.on_load_polygon)
# 操作按钮
btn_layout = QHBoxLayout()
# 导出酸化统计表
self.generate_sh_stat_btn = QPushButton("生成土壤属性统计表")
self.generate_sh_stat_btn.clicked.connect(self.on_generate_area_stat)
self.cancel_btn = QPushButton("取消")
# self.cancel_btn.clicked.connect(self.close)
btn_layout.addWidget(self.generate_sh_stat_btn)
btn_layout.addWidget(self.cancel_btn)
# 添加所有组件到主布局
main_layout.addWidget(self.batch_mode_group)
main_layout.addWidget(self.file_list_group)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
def browse_config_file(self):
"""浏览选择配置文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择配置文件", "",
"JSON 文件 (*.json);;所有文件 (*)"
)
if file_path:
self.config_file_edit.setText(file_path)
# self.validate_config_file(file_path)
# self.edit_config_btn.setEnabled(True)
def on_load_polygon(self):
"""加载图层列表"""
try:
data_source_paths = self.data_source_path_edit.text()
# 清空列表
self.file_list_group.file_list.clear()
# 添加图层
if arcpy.Exists(data_source_paths):
arcpy.env.workspace = data_source_paths
for polygon in arcpy.ListFeatureClasses(feature_type="Point"):
self.file_list_group.file_list.addItem(polygon)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个图层")
except Exception as e:
self.log_message(f"加载图层列表失败: {str(e)}")
traceback.print_exc()
def browse_input_workspace(self):
"""浏览选择输入GDB"""
folder_path = QFileDialog.getExistingDirectory(self, "选择GDB数据库")
if folder_path:
self.data_source_path_edit.setText(folder_path)
def browse_input_sanpu_prop_tif(self):
"""浏览选择输入GDB"""
folder_path = QFileDialog.getExistingDirectory(self, "选择文件夹啊")
if folder_path:
self.input_sanpu_prop_tif_edit.setText(folder_path)
def browse_input_reclassed_feature_folder(self):
"""浏览选择输入GDB"""
folder_path = QFileDialog.getExistingDirectory(self, "选择GDB数据库")
if folder_path:
self.input_reclassed_feature_folder_edit.setText(folder_path)
def browse_batch_output_folder(self):
"""浏览选择表格输出文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择表格输出文件夹")
if folder_path:
self.batch_output_folder_edit.setText(folder_path)
def validate_inputs(self):
"""验证输入参数"""
# 验证行政区名称
if not self.input_xzqmc_edit.text():
QMessageBox.warning(self, "输入错误", "请输入行政区名称")
return False
# 验证工作空间路径
if not self.data_source_path_edit.text() or not arcpy.Exists(self.data_source_path_edit.text()):
QMessageBox.warning(self, "输入错误", "请选择有效的工作空间")
return False
if not self.batch_output_folder_edit.text() or not arcpy.Exists(self.batch_output_folder_edit.text()):
QMessageBox.warning(self, "输入错误", "请选择批量处理输出文件夹")
return False
if not self.input_sanpu_prop_tif_edit.text() or not arcpy.Exists(self.input_sanpu_prop_tif_edit.text()):
QMessageBox.warning(self, "输入错误", "请选择有效的三普属性栅格文件夹")
return False
# 验证选择的要素类是否有对应的属性栅格
missing_items = []
for item in self.file_list_group.file_list.selectedItems():
if not arcpy.Exists(os.path.join(self.input_sanpu_prop_tif_edit.text(), f"{item.text()}.tif")):
missing_items.append(item.text())
if missing_items:
QMessageBox.warning(self, "输入错误", f"选择的样点中无相应的栅格文件: {missing_items}")
return False
# 验证数据源中是否存在 地类图斑、土壤类型图斑、母岩母质图斑 等要素
if not arcpy.Exists(os.path.join(self.data_source_path_edit.text(), "地类图斑")):
QMessageBox.warning(self, "输入错误", "工作空间中不存在 地类图斑.shp")
return False
if not arcpy.Exists(os.path.join(self.data_source_path_edit.text(), "土壤类型图")):
QMessageBox.warning(self, "输入错误", "工作空间中不存在土壤类型图.shp")
return False
# if not arcpy.Exists(os.path.join(self.data_source_path_edit.text(), "母岩母质图斑")):
# QMessageBox.warning(self, "输入错误", "工作空间中不存在母岩母质图斑.shp")
# return False
# 验证文件列表中是否已选中项目
if not self.file_list_group.file_list.selectedItems():
QMessageBox.warning(self, "输入错误", "请至少选择一个属性")
return False
return True
def open_config_editor(self):
"""打开配置文件编辑器对话框"""
config_path = self.config_file_edit.text()
if not config_path:
reply = QMessageBox.question(self, "编辑配置", "没有选择配置文件,是否创建一个新配置文件?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.No:
return
config_path = ""
# 使用配置编辑对话框
dialog = ConfigEditorDialogVisual(config_path, self)
if dialog.exec() == ConfigEditorDialogVisual.DialogCode.Accepted:
new_path = dialog.get_new_config_path()
if new_path and new_path != self.config_file_edit.text():
self.config_file_edit.setText(new_path)
# self.validate_config_file(new_path)
# 执行生成酸化统计表
def on_generate_area_stat(self):
"""执行生成面积统计表"""
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
params = {
"settings_path": settings_file,
}
task_id = self.script_runner.run_soil_prop_stat(params)
if task_id:
self.log_message(f"[GUI] 属性表格生成任务 {task_id} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def get_soil_prop_stat_settings(self):
"""保存当前配置到JSON文件"""
config = {
"xzqmc": self.input_xzqmc_edit.text(),
"config_file": self.config_file_edit.text(),
"data_source_path": self.data_source_path_edit.text(),
"sanpu_prop_tif_folder": self.input_sanpu_prop_tif_edit.text(),
"reclassed_feature_folder": self.input_reclassed_feature_folder_edit.text(),
"output_folder": self.batch_output_folder_edit.text(),
"sample_list": [item.text() for item in self.file_list_group.file_list.selectedItems()]
}
return config
def load_settings(self, settings):
"""从字典加载配置"""
try:
# 批处理参数
self.data_source_path_edit.setText(settings.get("data_source_path", ""))
self.input_sanpu_prop_tif_edit.setText(settings.get("sanpu_prop_tif_folder", ""))
self.batch_output_folder_edit.setText(settings.get("output_folder", ""))
self.input_reclassed_feature_folder_edit.setText(settings.get("reclassed_feature_folder", ""))
return True
except Exception as e:
QMessageBox.critical(self, "加载配置错误", f"加载配置时出错: {str(e)}")
return False
def set_buttons_enabled(self, enabled):
"""设置按钮是否可用"""
self.generate_sh_stat_btn.setEnabled(enabled)
def update_config_file(self, config_file_path):
"""更新配置文件路径"""
self.config_file_edit.setText(config_file_path)
def on_script_started(self, task_id, task_description):
"""脚本开始执行"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成"""
self.set_buttons_enabled(True)
self.log_message("脚本执行完成")
def on_script_error(self,task_id, error_msg):
"""脚本执行出错"""
self.set_buttons_enabled(True)
self.log_message(f"错误:{task_id}-{error_msg}")
# QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = SoilPropStatsTab()
window.show()
sys.exit(app.exec())

569
tools/ui/tabs/test_tab.py Normal file
View File

@@ -0,0 +1,569 @@
import json
import os
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QLineEdit, QFileDialog, QMessageBox,
QComboBox, QSpinBox, QGroupBox, QFormLayout,
QTableWidget, QHeaderView, QSizePolicy, QTableWidgetItem,
QCheckBox)
import arcpy
from ..runners.script_runner import ScriptRunner
class TestTab(QWidget):
"""导出成果图标签页"""
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent
self.script_runner = ScriptRunner(self)
self.connect_signals()
self.init_ui()
self.load_settings()
def connect_signals(self):
"""连接脚本运行器信号"""
# 连接ScriptRunner的信号
self.script_runner.started.connect(self.on_script_started)
self.script_runner.finished.connect(self.on_script_finished)
self.script_runner.error.connect(self.on_script_error)
self.script_runner.log.connect(self.log_message)
def init_ui(self):
layout = QVBoxLayout(self)
# 配置文件设置组
config_group = QGroupBox("配置设置")
config_layout = QVBoxLayout(config_group)
# 配置文件选择
config_file_layout = QHBoxLayout()
config_file_layout.addWidget(QLabel("配置文件:"))
self.config_file_path = QLineEdit()
config_file_layout.addWidget(self.config_file_path)
config_file_btn = QPushButton("浏览...")
config_file_btn.clicked.connect(self.browse_config_file)
config_file_layout.addWidget(config_file_btn)
config_layout.addLayout(config_file_layout)
# 区县名字设置
county_name_layout = QHBoxLayout()
county_name_layout.addWidget(QLabel("区县名称:"))
self.county_name = QLineEdit()
self.county_name.setPlaceholderText("请输入区县名称")
county_name_layout.addWidget(self.county_name)
config_layout.addLayout(county_name_layout)
# 图层名称选择
layer_select_layout = QHBoxLayout()
layer_select_layout.addWidget(QLabel("选择图层:"))
self.layer_name_combo = QComboBox()
self.layer_name_combo.setMinimumWidth(200)
self.layer_name_combo.currentIndexChanged.connect(self.on_layer_name_changed)
layer_select_layout.addWidget(self.layer_name_combo)
layer_select_layout.addStretch()
config_layout.addLayout(layer_select_layout)
# 配置文件设置显示区域
export_layout = QVBoxLayout()
export_layout.addWidget(QLabel("配置文件设置:"))
export_layout.minimumHeightForWidth(200)
self.export_config_display = QTableWidget(0, 2)
self.export_config_display.setHorizontalHeaderLabels(["元素名称", "元素内容"])
header = self.export_config_display.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
header.setDefaultSectionSize(150)
self.export_config_display.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
export_layout.addWidget(self.export_config_display)
config_layout.addLayout(export_layout)
layout.addWidget(config_group)
# 输入输出设置组
io_group = QGroupBox("输入输出设置")
io_layout = QVBoxLayout()
# 地图文档选择
doc_layout = QHBoxLayout()
doc_layout.addWidget(QLabel("模板文件:"))
self.template_aprx_file = QLineEdit()
doc_layout.addWidget(self.template_aprx_file)
browse_doc_btn = QPushButton("浏览...")
browse_doc_btn.clicked.connect(self.browse_doc)
doc_layout.addWidget(browse_doc_btn)
io_layout.addLayout(doc_layout)
# 数据源选择
data_layout = QHBoxLayout()
data_layout.addWidget(QLabel("数据源:"))
self.data_source_path = QLineEdit()
data_layout.addWidget(self.data_source_path)
browse_data_btn = QPushButton("浏览...")
browse_data_btn.clicked.connect(self.browse_data_source)
data_layout.addWidget(browse_data_btn)
io_layout.addLayout(data_layout)
# 符号系统文件夹选择
symbol_layout = QHBoxLayout()
symbol_layout.addWidget(QLabel("符号系统:"))
self.symbol_path = QLineEdit()
symbol_layout.addWidget(self.symbol_path)
browse_symbol_btn = QPushButton("浏览...")
browse_symbol_btn.clicked.connect(self.browse_symbol_file)
symbol_layout.addWidget(browse_symbol_btn)
io_layout.addLayout(symbol_layout)
# 输出路径选择
output_layout = QHBoxLayout()
output_layout.addWidget(QLabel("输出路径:"))
self.output_path = QLineEdit()
output_layout.addWidget(self.output_path)
browse_output_btn = QPushButton("浏览...")
browse_output_btn.clicked.connect(self.browse_output)
output_layout.addWidget(browse_output_btn)
io_layout.addLayout(output_layout)
io_group.setLayout(io_layout)
layout.addWidget(io_group)
# 导出设置组
export_group = QGroupBox("导出设置")
export_layout = QFormLayout()
# 格式选择
self.format_combo = QComboBox()
self.format_combo.addItems(["PDF", "PNG", "JPG", "TIFF", "EPS", "SVG", "AI"])
export_layout.addRow("导出格式:", self.format_combo)
# 分辨率设置
self.resolution_spinbox = QSpinBox()
self.resolution_spinbox.setRange(72, 1200)
self.resolution_spinbox.setSingleStep(12)
self.resolution_spinbox.setValue(300)
self.resolution_spinbox.setSuffix(" DPI")
export_layout.addRow("分辨率:", self.resolution_spinbox)
# 强制重新生成选项
self.force_regenerate = QCheckBox("强制重新生成工程文件")
self.force_regenerate.setChecked(False)
self.force_regenerate.setToolTip("勾选后将忽略已存在的工程文件,重新生成")
export_layout.addRow("处理选项:", self.force_regenerate)
export_group.setLayout(export_layout)
layout.addWidget(export_group)
# 操作按钮
button_layout = QHBoxLayout()
self.export_layout_btn = QPushButton("仅导出布局")
self.export_layout_btn.clicked.connect(self.on_export_layout)
self.export_btn = QPushButton("导出")
self.export_btn.clicked.connect(self.on_export)
self.batch_export_btn = QPushButton("批量导出")
self.batch_export_btn.clicked.connect(self.on_batch_export)
self.export_existing_btn = QPushButton("导出已有工程")
self.export_existing_btn.setToolTip("仅导出输出文件夹中已存在的工程文件")
self.export_existing_btn.clicked.connect(self.on_export_existing)
self.test_btn = QPushButton("测试功能")
self.test_btn.setToolTip("测试功能")
self.test_btn.clicked.connect(self.on_test_functions)
button_layout.addStretch()
button_layout.addWidget(self.export_layout_btn)
button_layout.addWidget(self.export_btn)
button_layout.addWidget(self.batch_export_btn)
button_layout.addWidget(self.export_existing_btn)
button_layout.addWidget(self.test_btn)
layout.addLayout(button_layout)
layout.addStretch()
def browse_config_file(self):
"""浏览配置文件"""
initial_path = os.getcwd()
file_path, _ = QFileDialog.getOpenFileName(self, "选择配置文件", initial_path, "JSON Files (*.json);;All Files (*.*)")
if file_path:
self.config_file_path.setText(file_path)
self.load_export_config(file_path)
def browse_doc(self):
"""浏览地图文档"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择地图文档",
"",
"ArcGIS Pro Project (*.aprx);;All Files (*.*)"
)
if file_path:
self.template_aprx_file.setText(file_path)
def browse_data_source(self):
"""浏览数据源"""
dir_path = QFileDialog.getExistingDirectory(
self,
"选择数据源目录"
)
if dir_path:
self.data_source_path.setText(dir_path)
def browse_symbol_file(self):
"""浏览符号系统文件夹"""
dir_path = QFileDialog.getExistingDirectory(
self,
"选择符号系统文件夹"
)
if dir_path:
self.symbol_path.setText(dir_path)
def browse_output(self):
"""选择输出路径"""
dir_path = QFileDialog.getExistingDirectory(
self,
"选择输出路径"
)
if dir_path:
self.output_path.setText(dir_path)
def load_export_config(self, file_path):
"""加载导出配置文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
export_config = config.get('export_config', {})
if not export_config:
QMessageBox.warning(self, "警告", "配置文件中未找到导出设置")
return
# 保存配置到实例变量
self._config = config
self._export_config = export_config
# 清空并添加图层名称到下拉框
self.layer_name_combo.clear()
self.layer_name_combo.addItems(export_config.keys())
# 如果有图层,自动选择第一个
if self.layer_name_combo.count() > 0:
self.layer_name_combo.setCurrentIndex(0)
self.log_message(f"已加载配置文件: {file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载配置文件失败: {str(e)}")
def on_layer_name_changed(self, index):
"""图层名称变更时更新配置显示"""
if index < 0 or not hasattr(self, '_export_config'):
return
layer_name = self.layer_name_combo.currentText()
layer_config = self._export_config.get(layer_name, {})
# 清空并重新添加行
self.export_config_display.setRowCount(0)
for i, (key, value) in enumerate(layer_config.items()):
self.export_config_display.insertRow(i)
self.export_config_display.setItem(i, 0, QTableWidgetItem(key))
self.export_config_display.setItem(i, 1, QTableWidgetItem(str(value)))
def on_script_started(self, message):
"""脚本开始执行回调"""
self.set_buttons_enabled(False)
self.log_message(message)
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成回调"""
self.set_buttons_enabled(True)
self.log_message("脚本执行完成")
def on_script_error(self, error_msg):
"""脚本执行错误回调"""
self.set_buttons_enabled(True)
self.log_message(f"错误: {error_msg}")
QMessageBox.critical(self, "错误", error_msg)
def set_buttons_enabled(self, enabled):
"""设置按钮启用状态"""
self.export_btn.setEnabled(enabled)
self.batch_export_btn.setEnabled(enabled)
self.export_layout_btn.setEnabled(enabled)
self.export_existing_btn.setEnabled(enabled)
self.test_btn.setEnabled(enabled)
def on_export(self):
"""导出按钮点击事件"""
# 验证输入
if not self.validate_inputs():
return
# 获取参数
params = self.get_export_params()
if not params:
return
# 添加配置文件参数
params['config_file'] = self.config_file_path.text()
# 调用导出地图脚本
self.script_runner.run_export_map(params)
def on_batch_export(self):
"""批量导出按钮点击事件"""
# 验证输入
if not self.validate_inputs(check_layer=False):
return
# 获取参数
params = self.get_export_params(include_layer=False)
if not params:
return
# 检查配置是否包含图层
if not hasattr(self, '_export_config') or not self._export_config:
QMessageBox.warning(self, "警告", "请先加载配置文件")
return
# 添加配置文件路径参数
params['config_file'] = self.config_file_path.text()
params['export_config'] = self._export_config
# 检查可用图层
polygon_list = list(self._export_config.keys())
available_layers = []
for layer_name in polygon_list:
layer_path = os.path.join(params.get("data_source_path"), layer_name)
if arcpy.Exists(layer_path):
available_layers.append(layer_name)
params['polygon_list'] = available_layers
# 确认是否批量导出所有图层
layer_count = len(available_layers)
result = QMessageBox.question(
self,
"确认",
f"数据源可用图层 {layer_count} 个,是否继续?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if result == QMessageBox.No:
return
# 调用批量导出脚本
self.script_runner.run_batch_export_map(params)
def validate_inputs(self, check_layer=True):
"""验证输入参数"""
if check_layer and self.layer_name_combo.currentIndex() < 0:
QMessageBox.warning(self, "警告", "请选择一个图层")
return False
if not self.county_name.text():
QMessageBox.warning(self, "警告", "请输入区县名称")
return False
if not self.template_aprx_file.text() or not os.path.exists(self.template_aprx_file.text()):
QMessageBox.warning(self, "警告", "请选择有效的模板文件")
return False
if not self.data_source_path.text() or not os.path.exists(self.data_source_path.text()):
QMessageBox.warning(self, "警告", "请选择有效的数据源")
return False
if not self.output_path.text():
QMessageBox.warning(self, "警告", "请选择输出路径")
return False
return True
def get_export_params(self, include_layer=True):
"""获取导出参数"""
try:
params = {
'county_name': self.county_name.text(),
'template_aprx_file': self.template_aprx_file.text(),
'data_source_path': self.data_source_path.text(),
'symbol_path': self.symbol_path.text(),
'output_path': self.output_path.text(),
'force_regenerate': self.force_regenerate.isChecked()
}
if include_layer:
params['layer_name'] = self.layer_name_combo.currentText()
return params
except Exception as e:
QMessageBox.critical(self, "错误", f"获取参数失败: {str(e)}")
return None
def load_settings(self):
"""加载设置"""
if not self.main_window:
return
try:
settings = self.main_window.settings
last_paths = settings.get('last_paths', {})
if last_paths.get('config_file'):
self.config_file_path.setText(last_paths.get('config_file', ''))
if os.path.exists(last_paths.get('config_file', '')):
self.load_export_config(last_paths.get('config_file', ''))
self.county_name.setText(last_paths.get('county_name', ''))
self.template_aprx_file.setText(last_paths.get('template_aprx_file', ''))
self.data_source_path.setText(last_paths.get('data_source_path', ''))
self.symbol_path.setText(last_paths.get('symbol_path', ''))
self.output_path.setText(last_paths.get('output_path', ''))
export_settings = settings.get('export_settings', {})
# 设置导出格式
format_index = self.format_combo.findText(export_settings.get('default_format', 'PDF'))
if format_index >= 0:
self.format_combo.setCurrentIndex(format_index)
# 设置分辨率
self.resolution_spinbox.setValue(export_settings.get('resolution', 300))
except Exception as e:
self.log_message(f"加载设置失败: {str(e)}")
def on_export_layout(self):
"""仅导出布局按钮点击事件"""
# 验证输入
if not self.validate_inputs():
return
# 获取参数
export_params = self.get_export_params()
if not export_params:
return
# 检查导出配置和图层名称
layer_name = self.layer_name_combo.currentText()
if not hasattr(self, '_export_config') or layer_name not in self._export_config:
QMessageBox.warning(self, "警告", "请先加载配置文件并选择图层")
return
# 准备工程文件路径
single_export_config = self._export_config.get(layer_name, {})
temp_file_name = single_export_config['项目名称'].split('\n')[1]
file_name = temp_file_name.replace('{区县占位符}', export_params['county_name'])
aprx_path = os.path.join(export_params['output_path'], f"{file_name}.aprx")
# 检查工程文件是否存在
if not os.path.exists(aprx_path):
result = QMessageBox.question(
self,
"确认",
f"工程文件不存在: {aprx_path}\n是否先生成工程文件?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if result == QMessageBox.Yes:
# 先生成工程文件
# 添加配置文件路径参数
export_params['config_file'] = self.config_file_path.text()
# 执行生成工程文件
self.script_runner.run_export_map(export_params)
return
else:
return
# 准备导出布局参数
layout_params = {
'aprx_path': aprx_path,
'output_path': export_params['output_path'],
'export_format': self.format_combo.currentText(),
'resolution': self.resolution_spinbox.value(),
'output_name': file_name
}
# 调用导出布局脚本
self.script_runner.run_export_layout(layout_params)
def on_export_existing(self):
"""导出已有工程按钮点击事件"""
# 验证输出路径
if not self.output_path.text() or not os.path.exists(self.output_path.text()):
QMessageBox.warning(self, "警告", "请选择有效的输出路径")
return
output_path = self.output_path.text()
# 查找所有aprx文件
aprx_files = []
for file in os.listdir(output_path):
if file.lower().endswith('.aprx'):
aprx_files.append(os.path.join(output_path, file))
if not aprx_files:
QMessageBox.warning(self, "警告", f"在指定目录中未找到aprx文件: {output_path}")
return
# 确认是否导出所有工程文件
result = QMessageBox.question(
self,
"确认",
f"将导出 {len(aprx_files)} 个工程文件的布局,是否继续?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if result == QMessageBox.No:
return
# 准备批量导出布局参数
batch_params = {
'aprx_folder': output_path,
'output_path': os.path.join(output_path, self.format_combo.currentText().lower()),
'export_format': self.format_combo.currentText(),
'resolution': self.resolution_spinbox.value()
}
# 调用批量导出布局脚本
self.script_runner.run_batch_export_layout(batch_params)
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
def on_test_functions(self):
"""测试功能按钮点击事件"""
self.log_message("正在执行测试功能...")
try:
# 调用测试脚本
test_params = {
'message': "测试成功!这是来自脚本的消息。",
'count': 1
}
# 使用script_runner的run_test_script方法
if hasattr(self.script_runner, 'run_test_script'):
self.script_runner.run_test_script(test_params)
self.log_message("测试脚本调用已启动,请查看日志")
else:
# 如果没有run_test_script方法尝试调用test_script.py
script_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'core', 'test_script.py'))
if os.path.exists(script_path):
self.log_message(f"找到测试脚本: {script_path}")
args = {
'message': test_params['message'],
'count': test_params['count']
}
self.script_runner.run_script(script_path, args)
else:
QMessageBox.information(self, "测试", "找不到测试脚本: test_script.py")
self.log_message(f"找不到测试脚本: {script_path}")
except Exception as e:
self.log_message(f"测试功能调用失败: {str(e)}")
QMessageBox.critical(self, "错误", f"测试功能调用失败: {str(e)}")

View File

@@ -0,0 +1,496 @@
# -*- coding: utf-8 -*-
"""
栅格处理界面: 提供栅格重分类、栅格转矢量和小面积图斑消除的界面操作
"""
import os
import json
import arcpy
import traceback
import multiprocessing
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QGridLayout, QCheckBox,
QGroupBox, QMessageBox, QSpinBox)
from tools.core.utils import common_utils
from tools.ui.runners.script_runner import ScriptRunner
from .config_editor_dialog import ConfigEditorDialogVisual
from ..components.file_list_group import FileListGroup
class XlsxToJpgTab(QWidget):
"""栅格处理窗口部件类"""
def __init__(self, parent=None):
super(XlsxToJpgTab, self).__init__(parent)
self.main_window = parent
self.init_ui()
self.setWindowTitle("面积统计")
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout(self)
# 批量处理区域
self.batch_mode_group = QGroupBox("批量处理设置")
batch_layout = QGridLayout()
# 配置文件选择 (公用)
batch_layout.addWidget(QLabel("配置文件:"), 0, 0)
self.config_file_edit = QLineEdit()
batch_layout.addWidget(self.config_file_edit, 0, 1)
self.browse_config_btn = QPushButton("浏览...")
self.browse_config_btn.clicked.connect(self.browse_config_file)
batch_layout.addWidget(self.browse_config_btn, 0, 2)
# 添加编辑配置文件的按钮 <--- Add this button
self.edit_config_btn = QPushButton("编辑配置内容...")
self.edit_config_btn.setEnabled(False)
self.edit_config_btn.clicked.connect(self.open_config_editor)
batch_layout.addWidget(self.edit_config_btn, 0, 3)
# 输入文件夹
batch_layout.addWidget(QLabel("重分类面要素:"), 1, 0)
self.input_folder_edit = QLineEdit()
batch_layout.addWidget(self.input_folder_edit, 1, 1, 1, 2)
self.browse_input_folder_btn = QPushButton("浏览...")
self.browse_input_folder_btn.clicked.connect(self.browse_input_folder)
batch_layout.addWidget(self.browse_input_folder_btn, 1, 3)
# 批量输出文件夹
batch_layout.addWidget(QLabel("输出文件夹:"), 2, 0)
self.batch_output_folder_edit = QLineEdit()
batch_layout.addWidget(self.batch_output_folder_edit, 2, 1, 1, 2)
self.browse_batch_output_btn = QPushButton("浏览...")
self.browse_batch_output_btn.clicked.connect(self.browse_batch_output_folder)
batch_layout.addWidget(self.browse_batch_output_btn, 2, 3)
# 选择乡镇界线
batch_layout.addWidget(QLabel("乡镇界线:"), 3, 0)
self.xzq_features_edit = QLineEdit()
batch_layout.addWidget(self.xzq_features_edit, 3, 1, 1, 2)
self.xzq_features_btn = QPushButton("浏览")
self.xzq_features_btn.clicked.connect(self.browse_xzq_features)
batch_layout.addWidget(self.xzq_features_btn, 3, 3)
# 选择地类图斑
batch_layout.addWidget(QLabel("地类图斑:"), 4, 0)
self.dltb_features_edit = QLineEdit()
batch_layout.addWidget(self.dltb_features_edit, 4, 1, 1, 2)
self.dltb_features_btn = QPushButton("浏览")
self.dltb_features_btn.clicked.connect(self.browse_dltb_features)
batch_layout.addWidget(self.dltb_features_btn, 4, 3)
# 输入行政区名称
batch_layout.addWidget(QLabel("行政区名称:"), 5, 0)
self.xzqmc_edit = QLineEdit()
batch_layout.addWidget(self.xzqmc_edit, 5, 1, 1, 2)
# 单选框
batch_layout.addWidget(QLabel("是否同时平差行政区和地类:"), 6, 0)
self.is_adjust_xzq_and_landuse_checkbox = QCheckBox()
batch_layout.addWidget(self.is_adjust_xzq_and_landuse_checkbox, 6, 1)
self.batch_mode_group.setLayout(batch_layout)
# 文件列表显示组
self.file_list_group = FileListGroup(self, "选择要处理的栅格")
self.file_list_group.load_files.connect(self.on_load_raster)
# --- 处理参数设置 ---
param_group = QGroupBox("处理参数")
param_layout = QVBoxLayout(param_group)
# 多进程设置
export_layout = QHBoxLayout()
mutiprocess_layout = QHBoxLayout()
mutiprocess_layout.addWidget(QLabel("使用多进程导出:"))
self.mutiprocess_check = QCheckBox()
self.mutiprocess_check.setChecked(True)
mutiprocess_layout.addWidget(self.mutiprocess_check)
mutiprocess_layout.addStretch()
export_layout.addLayout(mutiprocess_layout)
# 进程数量设置
process_count_layout = QHBoxLayout()
process_count_layout.addWidget(QLabel("进程数量:"))
self.process_count = QSpinBox()
self.process_count.setRange(1, multiprocessing.cpu_count())
self.process_count.setValue(max(1, multiprocessing.cpu_count() - 1)) # 默认使用CPU核心数-1
self.process_count.setFixedWidth(180)
process_count_layout.addWidget(self.process_count)
export_layout.addLayout(process_count_layout)
param_layout.addLayout(export_layout)
# 操作按钮
btn_layout = QHBoxLayout()
self.process_btn = QPushButton("开始处理")
self.process_btn.clicked.connect(self.on_start_processing)
self.cancel_btn = QPushButton("取消")
# self.cancel_btn.clicked.connect(self.close)
btn_layout.addWidget(self.process_btn)
btn_layout.addWidget(self.cancel_btn)
# 添加所有组件到主布局
main_layout.addWidget(self.batch_mode_group)
main_layout.addWidget(self.file_list_group)
main_layout.addWidget(param_group)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
def browse_input_folder(self):
"""浏览选择输入文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择栅格文件夹")
if folder_path:
self.input_folder_edit.setText(folder_path)
self.on_load_raster()
def browse_batch_output_folder(self):
"""浏览选择批量处理输出文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择批量处理输出文件夹")
if folder_path:
self.batch_output_folder_edit.setText(folder_path)
def browse_config_file(self):
"""浏览选择配置文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择配置文件", "",
"JSON 文件 (*.json);;所有文件 (*)"
)
if file_path:
self.config_file_edit.setText(file_path)
self.validate_config_file(file_path)
self.edit_config_btn.setEnabled(True)
def browse_xzq_features(self):
"""浏览选择裁剪面 (支持 shapefile)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择裁剪要素 (Shapefile)", "",
"Shapefile 文件 (*.shp);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(".shp"):
self.xzq_features_edit.setText(file_path)
self.log_message(f"已选择 Shapefile: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择 .shp 文件。")
self.xzq_features_edit.clear()
def browse_dltb_features(self):
"""浏览选择裁剪面 (支持 shapefile)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择裁剪要素 (Shapefile)", "",
"Shapefile 文件 (*.shp);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(".shp"):
self.dltb_features_edit.setText(file_path)
self.log_message(f"已选择 Shapefile: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择 .shp 文件。")
self.dltb_features_edit.clear()
def validate_config_file(self, config_file_path):
"""对文件进行快速检查,以查看它是否类似于预期的配置结构。"""
if not os.path.exists(config_file_path):
return False
try:
with open(config_file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
if "export_config" in config and isinstance(config["export_config"], dict):
if config["export_config"]:
first_item_value = next(iter(config["export_config"].values()))
if isinstance(first_item_value, dict) and all(k in first_item_value for k in ["项目名称", "标准等级"]):
self.log_message(f"配置文件 '{os.path.basename(config_file_path)}' 加载成功。")
return True
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 内容结构可能不匹配预期。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 中的 'export_config' 部分为空。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 缺少 'export_config' 键或格式不正确。")
return False
except json.JSONDecodeError:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 不是有效的 JSON 格式。")
return False
except Exception as e:
self.log_message(f"警告: 检查配置文件 '{os.path.basename(config_file_path)}' 时出错: {str(e)}")
return False
def open_config_editor(self):
"""打开配置文件编辑器对话框"""
config_path = self.config_file_edit.text()
if not config_path:
reply = QMessageBox.question(self, "编辑配置", "没有选择配置文件,是否创建一个新配置文件?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.No:
return
config_path = ""
# 使用配置编辑对话框
dialog = ConfigEditorDialogVisual(config_path, self)
if dialog.exec() == ConfigEditorDialogVisual.DialogCode.Accepted:
new_path = dialog.get_new_config_path()
if new_path and new_path != self.config_file_edit.text():
self.config_file_edit.setText(new_path)
self.validate_config_file(new_path)
def on_load_raster(self):
"""加载图层列表"""
try:
data_source_paths = self.input_folder_edit.text()
self.file_list_group.load_file_btn.setEnabled(False)
# 清空列表
self.file_list_group.file_list.clear()
if not data_source_paths or not arcpy.Exists(data_source_paths):
self.log_message(f"输入路径不存在或不是有效的ArcGIS工作空间/文件夹: {data_source_paths}")
return
arcpy.env.workspace = data_source_paths
# 获取所有SHP 文件
shp_files = arcpy.ListFeatureClasses("*.shp")
if shp_files:
for raster in shp_files:
self.file_list_group.file_list.addItem(raster)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个图层")
else:
self.log_message(f"文件夹 '{os.path.basename(data_source_paths)}' 中没有找到栅格图层。")
except Exception as e:
self.log_message(f"加载图层列表失败: {str(e)}")
traceback.print_exc()
finally:
self.file_list_group.load_file_btn.setEnabled(True)
def validate_inputs(self):
"""验证输入参数"""
# 验证配置文件路径
config_path = self.config_file_edit.text()
if not config_path or not os.path.exists(config_path):
QMessageBox.warning(self, "输入错误", "请选择有效的配置文件")
return False
try:
with open(config_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法加载或解析配置文件: {e}")
return False
if "export_config" not in config_data or not isinstance(config_data["export_config"], dict):
QMessageBox.warning(self, "输入错误", "配置文件的内容无效或缺少 'export_config' 部分。")
return False
standards_dict = config_data["export_config"]
if not standards_dict:
QMessageBox.warning(self, "输入错误", "配置文件中的 'export_config' 部分为空,没有定义任何处理参数。")
return False
# 验证'标准等级'用于配置重分类表
error_keys_remap = []
for key, value in standards_dict.items():
try:
levels_data = value.get("标准等级", {})
if not isinstance(levels_data, dict):
error_keys_remap.append(f"{key} ('标准等级' 格式不正确)")
continue
if not common_utils.create_remap_table(levels_data):
error_keys_remap.append(f"{key} ('标准等级' 内容无效)")
except Exception:
error_keys_remap.append(f"{key} (验证出错)")
if error_keys_remap:
QMessageBox.warning(self, "输入错误", "配置文件中部分参数的 '标准等级' 数据无效:\n" + "\n".join(error_keys_remap))
return False
# 验证输入、输出文件夹
if not self.input_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择输入栅格文件夹")
return False
if not self.batch_output_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择批量处理输出文件夹")
return False
clip_path = self.xzq_features_edit.text()
if not clip_path or not arcpy.Exists(clip_path):
QMessageBox.warning(self, "输入错误", "已启用裁剪,但裁剪要素路径无效或不存在")
return False
# 3. 验证乡镇界限
clip_path = self.xzq_features_edit.text()
if not clip_path:
QMessageBox.warning(self, "输入错误", "已启用裁剪,但裁剪要素路径为空。")
return False
# Use arcpy.Exists and Describe to validate the path regardless of whether it's SHP or GDB/FeatureClass
if not arcpy.Exists(clip_path):
QMessageBox.warning(self, "输入错误", f"已启用裁剪,但裁剪要素路径不存在:\n{clip_path}")
return False
# Optional: Check if the path points to a Feature Class (SHP or inside GDB)
try:
desc = arcpy.Describe(clip_path)
if desc.dataType != 'FeatureClass' and desc.dataType != 'ShapeFile':
QMessageBox.warning(self, "输入错误", f"已启用裁剪,但选择的路径不是一个要素类:\n{clip_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法描述裁剪要素路径,请检查是否为有效要素类:\n{clip_path}\n错误: {e}")
return False
# 验证是否选择文件
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if not selected_files:
QMessageBox.warning(self, "输入错误", "请在栅格列表中勾选要处理的文件。")
return False
return True
def on_start_processing(self):
"""开始处理栅格数据"""
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
# 创建的导出线程
self.script_runner = ScriptRunner()
# 连接ScriptRunner的信号
self.script_runner.task_started.connect(self.on_script_started)
self.script_runner.task_finished.connect(self.on_script_finished)
self.script_runner.task_error.connect(self.on_script_error)
self.script_runner.task_log.connect(self.on_script_log)
self.script_runner.manager_log.connect(self.log_message)
# 获取公共参数 (从 VectorParamsWidget 获取)
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if self.mutiprocess_check.isChecked():
self.script_runner.set_max_concurrent(self.process_count.value())
else:
self.script_runner.set_max_concurrent(1)
# 调用批量处理脚本
for raster_file in selected_files:
params = {
"settings_path": settings_file,
"reclassed_polygon": raster_file,
}
# 统计面积
task_id = self.script_runner.run_area_stat(params)
# if task_id:
# self.log_message(f"[GUI] 任务 {task_id} 处理 {raster_file} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def get_area_stat_settings(self):
"""保存当前配置到JSON文件"""
config = {
"input_folder": self.input_folder_edit.text(),
"batch_output_folder": self.batch_output_folder_edit.text(),
"config_file_path": self.config_file_edit.text(),
"xzq_features": self.xzq_features_edit.text(),
"dltb_features": self.dltb_features_edit.text(),
}
return config
def load_settings(self, settings):
"""从字典加载配置"""
try:
# 批处理参数
self.input_folder_edit.setText(settings.get("input_folder", ""))
self.batch_output_folder_edit.setText(settings.get("batch_output_folder", ""))
# 配置文件路径和加载
config_file_path = settings.get("config_file_path", "")
self.config_file_edit.setText(config_file_path)
self.edit_config_btn.setEnabled(bool(config_file_path) and os.path.exists(config_file_path))
if config_file_path:
self.validate_config_file(config_file_path)
self.xzq_features_edit.setText(settings.get("xzq_features", ""))
self.dltb_features_edit.setText(settings.get("dltb_features", ""))
if self.input_folder_edit.text():
self.on_load_raster()
return True
except Exception as e:
QMessageBox.critical(self, "加载配置错误", f"加载配置时出错: {str(e)}")
return False
def set_buttons_enabled(self, enabled):
"""设置按钮是否可用"""
self.process_btn.setEnabled(enabled)
def on_script_started(self, task_id, task_description):
"""脚本开始执行"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成"""
self.set_buttons_enabled(True)
self.log_message("脚本执行完成")
def on_script_error(self,task_id, error_msg):
"""脚本执行出错"""
self.set_buttons_enabled(True)
self.log_message(f"错误:{task_id}-{error_msg}")
# QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = XlsxToJpgTab()
window.show()
sys.exit(app.exec())