From 4857cb6e45fb46f155587f0088914177e9926d2a Mon Sep 17 00:00:00 2001 From: missum Date: Wed, 22 Apr 2026 12:27:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 15 + aprx_batch_gui.py | 79 + tools/__init__.py | 6 + tools/config/arcgis_field_cal_code.py | 54 + tools/config/common_config.py | 49 + tools/config/custom_sort.py | 125 ++ tools/config/pandas_field_cal_func.py | 141 ++ tools/config_json/raster_test_config.json | 315 ++++ tools/config_json/云南_华南_config.json | 328 +++++ tools/config_json/云南_华南_地块_config.json | 328 +++++ tools/config_json/云南_西南_config.json | 328 +++++ tools/config_json/华南分级.json | 30 + tools/config_json/广西_华南_config.json | 468 ++++++ tools/config_json/广西_华南_地块_config.json | 404 ++++++ tools/config_json/广西_西南_config.json | 418 ++++++ tools/config_json/广西_西南_地块_config.json | 405 ++++++ tools/config_json/广西_长江中下游_config.json | 365 +++++ .../广西_长江中下游_地块_config.json | 365 +++++ tools/config_json/西南分级.json | 30 + tools/config_json/酸化专题配置_config.json | 91 ++ tools/core/__init__.py | 4 + tools/core/acid_stats/__init__.py | 0 .../core/acid_stats/土地利用类型酸化统计表.py | 610 ++++++++ tools/core/acid_stats/土壤类型图酸化统计表.py | 629 ++++++++ tools/core/acid_stats/空间连接.py | 167 +++ tools/core/acid_stats/行政区划酸化统计表.py | 641 +++++++++ tools/core/export_excel_to_jpg_v1.py | 167 +++ tools/core/export_layout.py | 310 ++++ tools/core/export_map_v1.py | 572 ++++++++ tools/core/raster_to_polygon.py | 475 ++++++ .../B1_TRZD12土壤属性分级分布.py | 392 +++++ .../B1_TRZD土壤属性分级分布.py | 360 +++++ .../soil_prop_stats/B1土壤属性分级分布.py | 513 +++++++ .../B2_TRZD12土地利用类型土壤属性.py | 315 ++++ .../B2_TRZD土地利用类型土壤属性.py | 336 +++++ .../soil_prop_stats/B2土地利用类型土壤属性.py | 328 +++++ .../B3_TRZD12不同土壤类型土壤属性.py | 446 ++++++ .../B3_TRZD不同土壤类型土壤属性.py | 465 ++++++ .../soil_prop_stats/B3不同土壤类型土壤属性.py | 512 +++++++ .../soil_prop_stats/E1土壤属性历史变化.py | 1269 +++++++++++++++++ tools/core/soil_prop_stats/__init__.py | 0 tools/core/stats_area_to_excel.py | 660 +++++++++ tools/core/stats_sh_to_excel.py | 143 ++ tools/core/stats_soil_prop_to_excel.py | 235 +++ tools/core/test_script.py | 249 ++++ tools/core/utils/__init__.py | 0 tools/core/utils/arcgis_utils.py | 26 + tools/core/utils/common_utils.py | 291 ++++ tools/core/utils/excel_utils.py | 59 + tools/core/utils/math_utils.py | 147 ++ .../utils/os_utils/temp_files_processor.py | 47 + tools/core/utils/平差工具.py | 201 +++ tools/ui/__init__.py | 1 + tools/ui/components/__init__.py | 1 + tools/ui/components/file_list_group.py | 67 + tools/ui/components/input_group.py | 64 + tools/ui/config/__init__.py | 0 tools/ui/config/settings.json | 60 + tools/ui/main_window.py | 291 ++++ tools/ui/runners/__init__.py | 0 tools/ui/runners/script_runner.py | 475 ++++++ tools/ui/runners/script_runner.txt | 655 +++++++++ tools/ui/tabs/__init__.py | 9 + tools/ui/tabs/acid_stat_tab.py | 373 +++++ tools/ui/tabs/area_stat_tab.py | 564 ++++++++ tools/ui/tabs/config_editor_dialog.py | 597 ++++++++ tools/ui/tabs/export_layout_tab.py | 291 ++++ tools/ui/tabs/export_map_tab.py | 385 +++++ tools/ui/tabs/raster_processing_common.py | 244 ++++ tools/ui/tabs/raster_tab.py | 519 +++++++ tools/ui/tabs/soil_prop_stat_tab.py | 353 +++++ tools/ui/tabs/test_tab.py | 569 ++++++++ tools/ui/tabs/xlsx_jpg_tab.py | 496 +++++++ 73 files changed, 20927 insertions(+) create mode 100644 .gitignore create mode 100644 aprx_batch_gui.py create mode 100644 tools/__init__.py create mode 100644 tools/config/arcgis_field_cal_code.py create mode 100644 tools/config/common_config.py create mode 100644 tools/config/custom_sort.py create mode 100644 tools/config/pandas_field_cal_func.py create mode 100644 tools/config_json/raster_test_config.json create mode 100644 tools/config_json/云南_华南_config.json create mode 100644 tools/config_json/云南_华南_地块_config.json create mode 100644 tools/config_json/云南_西南_config.json create mode 100644 tools/config_json/华南分级.json create mode 100644 tools/config_json/广西_华南_config.json create mode 100644 tools/config_json/广西_华南_地块_config.json create mode 100644 tools/config_json/广西_西南_config.json create mode 100644 tools/config_json/广西_西南_地块_config.json create mode 100644 tools/config_json/广西_长江中下游_config.json create mode 100644 tools/config_json/广西_长江中下游_地块_config.json create mode 100644 tools/config_json/西南分级.json create mode 100644 tools/config_json/酸化专题配置_config.json create mode 100644 tools/core/__init__.py create mode 100644 tools/core/acid_stats/__init__.py create mode 100644 tools/core/acid_stats/土地利用类型酸化统计表.py create mode 100644 tools/core/acid_stats/土壤类型图酸化统计表.py create mode 100644 tools/core/acid_stats/空间连接.py create mode 100644 tools/core/acid_stats/行政区划酸化统计表.py create mode 100644 tools/core/export_excel_to_jpg_v1.py create mode 100644 tools/core/export_layout.py create mode 100644 tools/core/export_map_v1.py create mode 100644 tools/core/raster_to_polygon.py create mode 100644 tools/core/soil_prop_stats/B1_TRZD12土壤属性分级分布.py create mode 100644 tools/core/soil_prop_stats/B1_TRZD土壤属性分级分布.py create mode 100644 tools/core/soil_prop_stats/B1土壤属性分级分布.py create mode 100644 tools/core/soil_prop_stats/B2_TRZD12土地利用类型土壤属性.py create mode 100644 tools/core/soil_prop_stats/B2_TRZD土地利用类型土壤属性.py create mode 100644 tools/core/soil_prop_stats/B2土地利用类型土壤属性.py create mode 100644 tools/core/soil_prop_stats/B3_TRZD12不同土壤类型土壤属性.py create mode 100644 tools/core/soil_prop_stats/B3_TRZD不同土壤类型土壤属性.py create mode 100644 tools/core/soil_prop_stats/B3不同土壤类型土壤属性.py create mode 100644 tools/core/soil_prop_stats/E1土壤属性历史变化.py create mode 100644 tools/core/soil_prop_stats/__init__.py create mode 100644 tools/core/stats_area_to_excel.py create mode 100644 tools/core/stats_sh_to_excel.py create mode 100644 tools/core/stats_soil_prop_to_excel.py create mode 100644 tools/core/test_script.py create mode 100644 tools/core/utils/__init__.py create mode 100644 tools/core/utils/arcgis_utils.py create mode 100644 tools/core/utils/common_utils.py create mode 100644 tools/core/utils/excel_utils.py create mode 100644 tools/core/utils/math_utils.py create mode 100644 tools/core/utils/os_utils/temp_files_processor.py create mode 100644 tools/core/utils/平差工具.py create mode 100644 tools/ui/__init__.py create mode 100644 tools/ui/components/__init__.py create mode 100644 tools/ui/components/file_list_group.py create mode 100644 tools/ui/components/input_group.py create mode 100644 tools/ui/config/__init__.py create mode 100644 tools/ui/config/settings.json create mode 100644 tools/ui/main_window.py create mode 100644 tools/ui/runners/__init__.py create mode 100644 tools/ui/runners/script_runner.py create mode 100644 tools/ui/runners/script_runner.txt create mode 100644 tools/ui/tabs/__init__.py create mode 100644 tools/ui/tabs/acid_stat_tab.py create mode 100644 tools/ui/tabs/area_stat_tab.py create mode 100644 tools/ui/tabs/config_editor_dialog.py create mode 100644 tools/ui/tabs/export_layout_tab.py create mode 100644 tools/ui/tabs/export_map_tab.py create mode 100644 tools/ui/tabs/raster_processing_common.py create mode 100644 tools/ui/tabs/raster_tab.py create mode 100644 tools/ui/tabs/soil_prop_stat_tab.py create mode 100644 tools/ui/tabs/test_tab.py create mode 100644 tools/ui/tabs/xlsx_jpg_tab.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9567ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# 忽略所有 .pyc 文件 +*.pyc +*.zip +*.csv + +# 排除test目录、__pycache__目录、其他工具目录以及常规目录和文件 +test/ +__pycache__/ +其他工具/ +.idea/ +.qoder/ +.vscode/ + +# 忽略根目录下的xlsx文件 +*.xlsx diff --git a/aprx_batch_gui.py b/aprx_batch_gui.py new file mode 100644 index 0000000..12874d2 --- /dev/null +++ b/aprx_batch_gui.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +ArcGIS Pro批量处理工具 - GUI应用程序入口点 +""" + +import sys +import os +import traceback + +def main(): + """主函数,启动GUI应用程序""" + try: + from PyQt6.QtWidgets import QApplication + print("PyQt6导入成功") + + # 添加项目路径到sys.path + project_root = os.path.dirname(os.path.abspath(__file__)) + if project_root not in sys.path: + sys.path.insert(0, project_root) + print(f"添加项目路径到sys.path: {project_root}") + + # 使用重构版的MainWindow + from tools.ui.main_window import MainWindow + print("MainWindow导入成功") + + # 查找并设置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): + os.environ['ARCGISPRO_PYTHON'] = path + print(f"设置ArcGIS Pro Python环境: {path}") + break + except Exception as e: + print(f"设置Python环境失败: {str(e)}") + + # 验证脚本目录 + core_script_dir = os.path.join(project_root, 'tools', 'core') + if os.path.exists(core_script_dir): + print(f"核心脚本目录存在: {core_script_dir}") + script_files = [f for f in os.listdir(core_script_dir) if f.endswith('.py')] + print(f"发现脚本文件: {', '.join(script_files)}") + else: + print(f"警告: 核心脚本目录不存在: {core_script_dir}") + # 尝试创建核心脚本目录 + os.makedirs(core_script_dir, exist_ok=True) + print(f"已创建核心脚本目录: {core_script_dir}") + + # 启动GUI + app = QApplication(sys.argv) + # 设置应用程序样式 + # app.setStyle("Fusion") + window = MainWindow() + print("MainWindow实例创建成功") + + window.show() + print("窗口显示成功") + + sys.exit(app.exec()) + except Exception as e: + print(f"错误: {e}") + print("详细错误信息:") + traceback.print_exc() + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..432466b --- /dev/null +++ b/tools/__init__.py @@ -0,0 +1,6 @@ +""" +ArcGIS Pro工具集 +包含各种地图处理和栅格处理工具 +""" + +__version__ = '1.1.0' \ No newline at end of file diff --git a/tools/config/arcgis_field_cal_code.py b/tools/config/arcgis_field_cal_code.py new file mode 100644 index 0000000..153a10f --- /dev/null +++ b/tools/config/arcgis_field_cal_code.py @@ -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 "其他" + """ \ No newline at end of file diff --git a/tools/config/common_config.py b/tools/config/common_config.py new file mode 100644 index 0000000..8d055fb --- /dev/null +++ b/tools/config/common_config.py @@ -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", + "二普-三普": "二普-三普", + "测土-三普": "测土-三普", + "二普-测土": "二普-测土" +} \ No newline at end of file diff --git a/tools/config/custom_sort.py b/tools/config/custom_sort.py new file mode 100644 index 0000000..18361d4 --- /dev/null +++ b/tools/config/custom_sort.py @@ -0,0 +1,125 @@ +''' +自定义排序:yl_order-亚类排序,ts_order-土属排序 +''' + +yl_order = [ + "典型砖红壤", + "典型赤红壤", + "典型红壤", + "黄红壤", + "红壤性土", + "典型黄壤", + "漂洗黄壤", + "表潜黄壤", + "黄壤性土", + "暗黄棕壤", + "典型新积土", + "滨海风沙土", + "红色石灰土", + "黑色石灰土", + "棕色石灰土", + "黄色石灰土", + "酸性紫色土", + "中性紫色土", + "石灰性紫色土", + "硅质岩粗骨土", + "典型潮土", + "灰潮土", + "山地灌丛草甸土", + "泥炭沼泽土", + "盐化沼泽土", + "滨海潮滩盐土", + "含盐酸性硫酸盐土", + "潴育水稻土", + "淹育水稻土", + "渗育水稻土", + "潜育水稻土", + "漂洗水稻土", + "咸酸水稻土", +] +ts_order = [ + "涂砂质砖红壤", + "暗泥质砖红壤", + "麻砂质砖红壤", + "砂泥质砖红壤", + "红泥质赤红壤", + "暗泥质赤红壤", + "麻砂质赤红壤", + "硅质赤红壤", + "砂泥质赤红壤", + "泥质赤红壤", + "灰泥质赤红壤", + "红泥质红壤", + "麻砂质红壤", + "砂泥质红壤", + "泥质红壤", + "麻砂质黄红壤", + "硅质黄红壤", + "砂泥质黄红壤", + "灰泥质黄红壤", + "泥砂质红壤性土", + "麻砂质黄壤", + "砂泥质黄壤", + "麻砂质漂洗黄壤", + "砂泥质漂洗黄壤", + "砂泥质表潜黄壤", + "砂泥质黄壤性土", + "灰泥质黄壤性土", + "麻砂质暗黄棕壤", + "砂泥质暗黄棕壤", + "山洪土", + "滨海固定风沙土", + "红色石灰土", + "黑色石灰土", + "棕色石灰土", + "黄色石灰土", + "酸紫砂土", + "酸紫壤土", + "酸紫黏土", + "紫泥土", + "灰紫壤土", + "灰紫泥土", + "白粉土", + "潮壤土", + "石灰性灰潮壤土", + "灰潮砂土", + "灰潮壤土", + "山地灌丛草甸砂土", + "山地灌丛草甸壤土", + "泥炭沼泽土", + "盐化沼泽土", + "涂砂盐土", + "涂泥盐土", + "含盐酸性硫酸盐土", + "潮泥田", + "潮泥砂田", + "涂泥田", + "麻砂泥田", + "砂泥田", + "鳝泥田", + "灰泥田", + "紫泥田", + "红泥田", + "白粉泥田", + "暗泥田", + "浅潮泥田", + "浅潮泥砂田", + "浅暗泥田", + "浅麻砂泥田", + "浅砂泥田", + "浅鳝泥田", + "浅灰泥田", + "浅紫泥田", + "浅白粉泥田", + "浅红泥田", + "渗潮泥田", + "渗砂泥田", + "渗紫泥田", + "青潮泥田", + "青灰泥田", + "青红泥田", + "烂泥田", + "泥炭土田", + "漂红泥田", + "咸酸田", +] diff --git a/tools/config/pandas_field_cal_func.py b/tools/config/pandas_field_cal_func.py new file mode 100644 index 0000000..933c832 --- /dev/null +++ b/tools/config/pandas_field_cal_func.py @@ -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 diff --git a/tools/config_json/raster_test_config.json b/tools/config_json/raster_test_config.json new file mode 100644 index 0000000..0aedad1 --- /dev/null +++ b/tools/config_json/raster_test_config.json @@ -0,0 +1,315 @@ +{ + "export_config": { + "AB": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图", + "分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法", + "项目分级": "有效硼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.00~2.00", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "ACU": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铜\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1.80", + "等级二": "1.00~1.80", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "AMN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锰\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "20.0~30.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AMO": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图", + "分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法", + "项目分级": "有效钼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">0.20", + "等级二": "0.15~0.20", + "等级三": "0.10~0.15", + "等级四": "0.05~0.10", + "等级五": "≤0.05" + } + }, + "AS1": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "有效硫\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "30.0~40.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "AZN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锌\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "2.00~3.00", + "等级三": "1.00~2.00", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "CEC": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图", + "分析方法": "分析方法:EDTA-乙酸铵交换法", + "项目分级": "阳离子\n交换量级别", + "分级标准": "分级标准/\n(cmol/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "ECA": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "交换性钙\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1800", + "等级二": "1300~1800", + "等级三": "800~1300", + "等级四": "300~800", + "等级五": "≤300" + } + }, + "EMG": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "交换性镁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">250", + "等级二": "150~250", + "等级三": "70~150", + "等级四": "30~70", + "等级五": "≤30" + } + }, + "TSE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图", + "分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法", + "项目分级": "全硒\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "0.40~3.00", + "等级三": "0.17~0.40", + "等级四": "≤0.17" + } + }, + "TN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图", + "分析方法": "分析方法:自动定氮仪法", + "项目分级": "全氮\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.50~2.00", + "等级三": "1.00~1.50", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "TP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全磷\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">1.50", + "等级二": "1.00~1.50", + "等级三": "0.60~1.00", + "等级四": "0.40~0.60", + "等级五": "≤0.40" + } + }, + "TK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全钾\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AFE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "10.0~20.0", + "等级三": "8.0~10.0", + "等级四": "3.0~8.0", + "等级五": "≤3.0" + } + }, + "AK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图", + "分析方法": "分析方法:乙酸钠浸提-火焰光度法", + "项目分级": "速效钾\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">150", + "等级二": "100~150", + "等级三": "75~100", + "等级四": "50~75", + "等级五": "≤50" + } + }, + "AP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图", + "分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法", + "项目分级": "有效磷\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "20.0~40.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "30.0~35.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "FL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "粉粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤20", + "等级二": "20~40", + "等级三": "40~60", + "等级四": "60~80", + "等级五": ">80" + } + }, + "NL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "黏粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "SL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "砂粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图", + "分析方法": "分析方法:电位法", + "项目分级": "土壤pH\n级别", + "分级标准": "分级标准/\npH值", + "标准等级": { + "等级一": "6.0~7.0", + "等级二": "7.0~7.5,\n5.5~6.0", + "等级三": "7.5~8.0,\n5.0~5.5", + "等级四": "8.0~8.5,\n4.5~5.0", + "等级五": ">8.5,\n≤4.5" + } + }, + "YXTCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "有效土层\n厚度级别", + "分级标准": "分级标准/\ncm", + "标准等级": { + "等级一": ">80", + "等级二": "70~80", + "等级三": "60~70", + "等级四": "50~60", + "等级五": "≤50" + } + }, + "GZCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "耕作层\n厚度级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">25.0", + "等级二": "20.0~25.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "TRRZ": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图", + "分析方法": "分析方法:热鼓风加热环刀法", + "项目分级": "土壤容重\n级别", + "分级标准": "分级标准/\n(g/cm³)", + "标准等级": { + "等级一": "1.00~1.20", + "等级二": "1.20~1.30", + "等级三": "1.30~1.40,\n0.90~1.0", + "等级四": "1.40~1.50", + "等级五": ">1.50,\n≤0.90" + } + } + } +} \ No newline at end of file diff --git a/tools/config_json/云南_华南_config.json b/tools/config_json/云南_华南_config.json new file mode 100644 index 0000000..f6f63a7 --- /dev/null +++ b/tools/config_json/云南_华南_config.json @@ -0,0 +1,328 @@ +{ + "export_config": { + "AB": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图", + "分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法", + "项目分级": "有效硼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.00~2.00", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "ACU": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铜\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1.80", + "等级二": "1.00~1.80", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "AMN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锰\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "20.0~30.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AMO": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图", + "分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法", + "项目分级": "有效钼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">0.20", + "等级二": "0.15~0.20", + "等级三": "0.10~0.15", + "等级四": "0.05~0.10", + "等级五": "≤0.05" + } + }, + "AS1": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "有效硫\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "30.0~40.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "AZN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锌\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "2.00~3.00", + "等级三": "1.00~2.00", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "CEC": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图", + "分析方法": "分析方法:EDTA-乙酸铵交换法", + "项目分级": "阳离子\n交换量级别", + "分级标准": "分级标准/\n(cmol/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "ECA": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图(地块)", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性钙\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1000", + "等级二": "500~1000", + "等级三": "200~500", + "等级四": "50~200", + "等级五": "≤50" + } + }, + "EMG": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图(地块)", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性镁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">200", + "等级二": "100~200", + "等级三": "50~100", + "等级四": "25~50", + "等级五": "≤25" + } + }, + "TSE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图", + "分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法", + "项目分级": "全硒\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "0.40~3.00", + "等级三": "0.17~0.40", + "等级四": "≤0.17" + } + }, + "TN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图", + "分析方法": "分析方法:自动定氮仪法", + "项目分级": "全氮\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.50~2.00", + "等级三": "1.00~1.50", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "TP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全磷\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">1.50", + "等级二": "1.00~1.50", + "等级三": "0.60~1.00", + "等级四": "0.40~0.60", + "等级五": "≤0.40" + } + }, + "TK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全钾\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AFE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "10.0~20.0", + "等级三": "8.0~10.0", + "等级四": "3.0~8.0", + "等级五": "≤3.0" + } + }, + "AK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图", + "分析方法": "分析方法:乙酸钠浸提-火焰光度法", + "项目分级": "速效钾\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">150", + "等级二": "100~150", + "等级三": "75~100", + "等级四": "50~75", + "等级五": "≤50" + } + }, + "AP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图", + "分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法", + "项目分级": "有效磷\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "20.0~40.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "30.0~35.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "FL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "粉粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤20", + "等级二": "20~40", + "等级三": "40~60", + "等级四": "60~80", + "等级五": ">80" + } + }, + "NL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "黏粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "SL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "砂粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "6.0~7.0", + "等级二": "7.0~7.5,\n5.5~6.0", + "等级三": "7.5~8.0,\n5.0~5.5", + "等级四": "8.0~8.5,\n4.5~5.0", + "等级五": ">8.5,\n≤4.5" + } + }, + "YXTCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "有效土层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">80", + "等级二": "70~80", + "等级三": "60~70", + "等级四": "50~60", + "等级五": "≤50" + } + }, + "GZCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "耕作层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">25.0", + "等级二": "20.0~25.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "TRRZ": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图", + "分析方法": "分析方法:热鼓风加热环刀法", + "项目分级": "土壤容重\n级别", + "分级标准": "分级标准/\n(g/cm³)", + "标准等级": { + "等级一": "1.00~1.20", + "等级二": "1.20~1.30", + "等级三": "1.30~1.40,\n0.90~1.0", + "等级四": "1.40~1.50", + "等级五": ">1.50,\n≤0.90" + } + }, + "TRZD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "土壤质地\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "黏壤质": "1", + "黏质": "2", + "壤质": "3", + "砂壤质": "4", + "砂质": "5" + } + } + } +} \ No newline at end of file diff --git a/tools/config_json/云南_华南_地块_config.json b/tools/config_json/云南_华南_地块_config.json new file mode 100644 index 0000000..900e584 --- /dev/null +++ b/tools/config_json/云南_华南_地块_config.json @@ -0,0 +1,328 @@ +{ + "export_config": { + "AB": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图(地块)", + "分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法", + "项目分级": "有效硼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.00~2.00", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "ACU": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铜\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1.80", + "等级二": "1.00~1.80", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "AMN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锰\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "20.0~30.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AMO": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图(地块)", + "分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法", + "项目分级": "有效钼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">0.20", + "等级二": "0.15~0.20", + "等级三": "0.10~0.15", + "等级四": "0.05~0.10", + "等级五": "≤0.05" + } + }, + "AS1": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图(地块)", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "有效硫\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "30.0~40.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "AZN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锌\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "2.00~3.00", + "等级三": "1.00~2.00", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "CEC": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图(地块)", + "分析方法": "分析方法:EDTA-乙酸铵交换法", + "项目分级": "阳离子\n交换量级别", + "分级标准": "分级标准/\n(cmol/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "ECA": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图(地块)", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性钙\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1000", + "等级二": "500~1000", + "等级三": "200~500", + "等级四": "50~200", + "等级五": "≤50" + } + }, + "EMG": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图(地块)", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性镁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">200", + "等级二": "100~200", + "等级三": "50~100", + "等级四": "25~50", + "等级五": "≤25" + } + }, + "TSE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图(地块)", + "分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法", + "项目分级": "全硒\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "0.40~3.00", + "等级三": "0.17~0.40", + "等级四": "≤0.17" + } + }, + "TN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图(地块)", + "分析方法": "分析方法:自动定氮仪法", + "项目分级": "全氮\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.50~2.00", + "等级三": "1.00~1.50", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "TP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图(地块)", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全磷\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">1.50", + "等级二": "1.00~1.50", + "等级三": "0.60~1.00", + "等级四": "0.40~0.60", + "等级五": "≤0.40" + } + }, + "TK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图(地块)", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全钾\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AFE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "10.0~20.0", + "等级三": "8.0~10.0", + "等级四": "3.0~8.0", + "等级五": "≤3.0" + } + }, + "AK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图(地块)", + "分析方法": "分析方法:乙酸钠浸提-火焰光度法", + "项目分级": "速效钾\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">150", + "等级二": "100~150", + "等级三": "75~100", + "等级四": "50~75", + "等级五": "≤50" + } + }, + "AP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图(地块)", + "分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法", + "项目分级": "有效磷\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "20.0~40.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图(地块)", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "30.0~35.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "FL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "粉粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤20", + "等级二": "20~40", + "等级三": "40~60", + "等级四": "60~80", + "等级五": ">80" + } + }, + "NL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "黏粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "SL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "砂粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图(地块)", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "6.0~7.0", + "等级二": "7.0~7.5,\n5.5~6.0", + "等级三": "7.5~8.0,\n5.0~5.5", + "等级四": "8.0~8.5,\n4.5~5.0", + "等级五": ">8.5,\n≤4.5" + } + }, + "YXTCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图(地块)", + "分析方法": "分析方法:剖面法", + "项目分级": "有效土层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">80", + "等级二": "70~80", + "等级三": "60~70", + "等级四": "50~60", + "等级五": "≤50" + } + }, + "GZCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图(地块)", + "分析方法": "分析方法:剖面法", + "项目分级": "耕作层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">25.0", + "等级二": "20.0~25.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "TRRZ": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图(地块)", + "分析方法": "分析方法:热鼓风加热环刀法", + "项目分级": "土壤容重\n级别", + "分级标准": "分级标准/\n(g/cm³)", + "标准等级": { + "等级一": "1.00~1.20", + "等级二": "1.20~1.30", + "等级三": "1.30~1.40,\n0.90~1.0", + "等级四": "1.40~1.50", + "等级五": ">1.50,\n≤0.90" + } + }, + "TRZD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "土壤质地\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "1", + "等级二": "2", + "等级三": "3", + "等级四": "4", + "等级五": "5" + } + } + } +} \ No newline at end of file diff --git a/tools/config_json/云南_西南_config.json b/tools/config_json/云南_西南_config.json new file mode 100644 index 0000000..ad97aa6 --- /dev/null +++ b/tools/config_json/云南_西南_config.json @@ -0,0 +1,328 @@ +{ + "export_config": { + "AB": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图", + "分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法", + "项目分级": "有效硼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1.00", + "等级二": "0.80~1.00", + "等级三": "0.50~0.80", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "ACU": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铜\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.00~2.00", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "AMN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锰\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "15.0~30.0", + "等级三": "5.0~15.0", + "等级四": "1.0~5.0", + "等级五": "≤1.0" + } + }, + "AMO": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图", + "分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法", + "项目分级": "有效钼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">0.20", + "等级二": "0.15~0.20", + "等级三": "0.10~0.15", + "等级四": "0.05~0.10", + "等级五": "≤0.05" + } + }, + "AS1": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "有效硫\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "30.0~40.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "AZN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锌\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "1.00~3.00", + "等级三": "0.50~1.00", + "等级四": "0.30~0.50", + "等级五": "≤0.30" + } + }, + "CEC": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图", + "分析方法": "分析方法:EDTA-乙酸铵交换法", + "项目分级": "阳离子\n交换量级别", + "分级标准": "分级标准/\n(cmol/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "20.0~30.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "ECA": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性钙\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1500", + "等级二": "1000~1500", + "等级三": "500~1000", + "等级四": "200~500", + "等级五": "≤200" + } + }, + "EMG": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性镁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">200", + "等级二": "150~200", + "等级三": "100~150", + "等级四": "50~100", + "等级五": "≤50" + } + }, + "TSE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图", + "分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法", + "项目分级": "全硒\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "0.40~3.00", + "等级三": "0.17~0.40", + "等级四": "≤0.17" + } + }, + "TN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图", + "分析方法": "分析方法:自动定氮仪法", + "项目分级": "全氮\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.50~2.00", + "等级三": "1.00~1.50", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "TP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全磷\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">1.00", + "等级二": "0.80~1.00", + "等级三": "0.60~0.80", + "等级四": "0.40~0.60", + "等级五": "≤0.40" + } + }, + "TK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全钾\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AFE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "10.0~20.0", + "等级三": "5.0~10.0", + "等级四": "3.0~5.0", + "等级五": "≤3.0" + } + }, + "AK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图", + "分析方法": "分析方法:乙酸钠浸提-火焰光度法", + "项目分级": "速效钾\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">150", + "等级二": "100~150", + "等级三": "75~100", + "等级四": "50~75", + "等级五": "≤50" + } + }, + "AP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图", + "分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法", + "项目分级": "有效磷\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "25.0~40.0", + "等级三": "15.0~25.0", + "等级四": "5.0~15.0", + "等级五": "≤5.0" + } + }, + "OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "25.0~35.0", + "等级三": "15.0~25.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "FL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "粉粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤20", + "等级二": "20~40", + "等级三": "40~60", + "等级四": "60~80", + "等级五": ">80" + } + }, + "NL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "黏粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "SL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "砂粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "6.0~7.0", + "等级二": "7.0~7.5,\n5.5~6.0", + "等级三": "7.5~8.0,\n5.0~5.5", + "等级四": "8.0~8.5,\n4.5~5.0", + "等级五": ">8.5,\n≤4.5" + } + }, + "YXTCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "有效土层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">80", + "等级二": "70~80", + "等级三": "60~70", + "等级四": "50~60", + "等级五": "≤50" + } + }, + "GZCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "耕作层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">25.0", + "等级二": "20.0~25.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "TRRZ": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图", + "分析方法": "分析方法:热鼓风加热环刀法", + "项目分级": "土壤容重\n级别", + "分级标准": "分级标准/\n(g/cm³)", + "标准等级": { + "等级一": "1.10~1.25", + "等级二": "1.25~1.35,\n1.00~1.10", + "等级三": "1.35~1.45", + "等级四": "1.45~1.55,\n0.90~1.00", + "等级五": ">1.55,\n≤0.90" + } + }, + "TRZD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "土壤质地\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "黏壤质": "1", + "黏质": "2", + "壤质": "3", + "砂壤质": "4", + "砂质": "5" + } + } + } +} \ No newline at end of file diff --git a/tools/config_json/华南分级.json b/tools/config_json/华南分级.json new file mode 100644 index 0000000..6a44cdf --- /dev/null +++ b/tools/config_json/华南分级.json @@ -0,0 +1,30 @@ +{ + "AB": ">2.00;1.00~2.00;0.50~1.00;0.20~0.50;≤0.20", + "ACU": ">1.80;1.00~1.80;0.50~1.00;0.20~0.50;≤0.20", + "AMN": ">30.0;20.0~30.0;10.0~20.0;5.0~10.0;≤5.0", + "AMO": ">0.20;0.15~0.20;0.10~0.15;0.05~0.10;≤0.05", + "AS1": ">40.0;30.0~40.0;20.0~30.0;10.0~20.0;≤10.0", + "AZN": ">3.00;2.00~3.00;1.00~2.00;0.50~1.00;≤0.50", + "CEC": ">20.0;15.0~20.0;10.0~15.0;5.0~10.0;≤5.0", + "ECA": ">4.99;2.50~4.99;1.00~2.50;0.25~1.00;≤0.25", + "EMG": ">1.64;0.82~1.64;0.41~0.82;0.21~0.41;≤0.21", + "TSE": ">3.00;0.40~3.00;0.17~0.40;≤0.17", + "TN": ">2.00;1.50~2.00;1.00~1.50;0.50~1.00;≤0.50", + "TP": ">1.50;1.00~1.50;0.60~1.00;0.40~0.60;≤0.40", + "TK": ">20.0;15.0~20.0;10.0~15.0;5.0~10.0;≤5.0", + "AFE": ">20.0;10.0~20.0;8.0~10.0;3.0~8.0;≤3.0", + "AK": ">150;100~150;75~100;50~75;≤50", + "AP": ">40.0;20.0~40.0;10.0~20.0;5.0~10.0;≤5.0", + "OM": ">35.0;30.0~35.0;20.0~30.0;10.0~20.0;≤10.0", + "FL": "≤15;15~30;30~45;45~75;>75", + "NL": "≤15;15~25;25~45;45~65;>65", + "SL": "≤30;30~40;40~55;55~85;>85", + "PH": "6.0~7.0;7.0~7.5,5.5~6.0;7.5~8.0,5.0~5.5;8.0~8.5,4.5~5.0;>8.5,≤4.5", + "YXTCHD": ">100;80~100;60~80;40~60;≤40", + "GZCHD": ">25.0;20.0~25.0;15.0~20.0;10.0~15.0;≤10.0", + "TRRZ": "1.00~1.20;1.20~1.30;1.30~1.40,0.90~1.0;1.40~1.50;>1.50,≤0.90", + "TRZD": "1;2;3;4;5", + "二普PH": ">6.5;5.5~6.5;4.5~5.5;≤4.5", + "三普PH": ">6.5;5.5~6.5;4.5~5.5;≤4.5", + "酸化PH": ">1.0;0.5~1.0;0.3~0.5;-0.3~0.3;-10~-0.3" +} \ No newline at end of file diff --git a/tools/config_json/广西_华南_config.json b/tools/config_json/广西_华南_config.json new file mode 100644 index 0000000..4b7f8fa --- /dev/null +++ b/tools/config_json/广西_华南_config.json @@ -0,0 +1,468 @@ +{ + "export_config": { + "AB": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图", + "分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法", + "项目分级": "有效硼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.00~2.00", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "ACU": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铜\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1.80", + "等级二": "1.00~1.80", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "AMN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锰\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "20.0~30.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AMO": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图", + "分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法", + "项目分级": "有效钼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">0.20", + "等级二": "0.15~0.20", + "等级三": "0.10~0.15", + "等级四": "0.05~0.10", + "等级五": "≤0.05" + } + }, + "AS1": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "有效硫\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "30.0~40.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "AZN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锌\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "2.00~3.00", + "等级三": "1.00~2.00", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "CEC": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图", + "分析方法": "分析方法:EDTA-乙酸铵交换法", + "项目分级": "阳离子\n交换量级别", + "分级标准": "分级标准/\n(cmol/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "ECA": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性钙\n含量级别", + "分级标准": "分级标准/\ncmol(½Ca²⁺)/kg", + "标准等级": { + "等级一": ">4.99", + "等级二": "2.50~4.99", + "等级三": "1.00~2.50", + "等级四": "0.25~1.00", + "等级五": "≤0.25" + } + }, + "EMG": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性镁\n含量级别", + "分级标准": "分级标准/\ncmol(½Mg²⁺)/kg", + "标准等级": { + "等级一": ">1.64", + "等级二": "0.82~1.64", + "等级三": "0.41~0.82", + "等级四": "0.21~0.41", + "等级五": "≤0.21" + } + }, + "TSE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图", + "分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法", + "项目分级": "全硒\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "0.40~3.00", + "等级三": "0.17~0.40", + "等级四": "≤0.17" + } + }, + "TN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图", + "分析方法": "分析方法:自动定氮仪法", + "项目分级": "全氮\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.50~2.00", + "等级三": "1.00~1.50", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "TP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全磷\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">1.50", + "等级二": "1.00~1.50", + "等级三": "0.60~1.00", + "等级四": "0.40~0.60", + "等级五": "≤0.40" + } + }, + "TK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全钾\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AFE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "10.0~20.0", + "等级三": "8.0~10.0", + "等级四": "3.0~8.0", + "等级五": "≤3.0" + } + }, + "AK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图", + "分析方法": "分析方法:乙酸钠浸提-火焰光度法", + "项目分级": "速效钾\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">150", + "等级二": "100~150", + "等级三": "75~100", + "等级四": "50~75", + "等级五": "≤50" + } + }, + "AP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图", + "分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法", + "项目分级": "有效磷\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "20.0~40.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "30.0~35.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "FL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "粉粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~30", + "等级三": "30~45", + "等级四": "45~75", + "等级五": ">75" + } + }, + "NL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "黏粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "SL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "砂粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤30", + "等级二": "30~40", + "等级三": "40~55", + "等级四": "55~85", + "等级五": ">85" + } + }, + "PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "≤4.5", + "等级二": "4.5~5.0", + "等级三": "5.0~5.5", + "等级四": "5.5~6.0", + "等级五": "6.0~7.0", + "等级六": "7.0~7.5", + "等级七": "7.5~8.0", + "等级八": "8.0~8.5", + "等级九": ">8.5" + } + }, + "YXTCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "有效土层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">100", + "等级二": "80~100", + "等级三": "60~80", + "等级四": "40~60", + "等级五": "≤40" + } + }, + "GZCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "耕作层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">25.0", + "等级二": "20.0~25.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "TRRZ": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图", + "分析方法": "分析方法:热鼓风加热环刀法", + "项目分级": "土壤容重\n级别", + "分级标准": "分级标准/\n(g/cm³)", + "标准等级": { + "等级一": "≤0.90", + "等级二": "0.90~1.0", + "等级三": "1.00~1.20", + "等级四": "1.20~1.30", + "等级五": "1.30~1.40", + "等级六": "1.40~1.50", + "等级七": ">1.50" + } + }, + "LSFD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砾石丰度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "砾石丰度\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤0", + "等级二": "0~5", + "等级三": "5~15", + "等级四": "15~50", + "等级五": ">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.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "测土PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤pH分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": ">6.5", + "等级二": "5.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "三普PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": ">6.5", + "等级二": "5.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "酸化PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "重度酸化": ">1.0", + "中度酸化": "0.5~1.0", + "轻度酸化": "0.3~0.5", + "弱酸化": "0.1~0.3", + "碱化": "-10~0.1" + } + }, + "二普-三普_PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "重度酸化": ">1.0", + "中度酸化": "0.5~1.0", + "轻度酸化": "0.3~0.5", + "弱酸化": "0.1~0.3", + "碱化": "-10~0.1" + } + }, + "测土-三普_PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "重度酸化": ">1.0", + "中度酸化": "0.5~1.0", + "轻度酸化": "0.3~0.5", + "弱酸化": "0.1~0.3", + "碱化": "-10~0.1" + } + }, + "二普-测土_PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "重度酸化": ">1.0", + "中度酸化": "0.5~1.0", + "轻度酸化": "0.3~0.5", + "弱酸化": "0.1~0.3", + "碱化": "-10~0.1" + } + }, + "专题图OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": "≤10.0", + "等级二": "10.0~20.0", + "等级三": "20.0~30.0", + "等级四": "30.0~40.0", + "等级五": ">40.0" + } + } + } +} \ No newline at end of file diff --git a/tools/config_json/广西_华南_地块_config.json b/tools/config_json/广西_华南_地块_config.json new file mode 100644 index 0000000..93ab8ce --- /dev/null +++ b/tools/config_json/广西_华南_地块_config.json @@ -0,0 +1,404 @@ +{ + "export_config": { + "AB": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图(地块)", + "分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法", + "项目分级": "有效硼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.00~2.00", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "ACU": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铜\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1.80", + "等级二": "1.00~1.80", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "AMN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锰\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "20.0~30.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AMO": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图(地块)", + "分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法", + "项目分级": "有效钼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">0.20", + "等级二": "0.15~0.20", + "等级三": "0.10~0.15", + "等级四": "0.05~0.10", + "等级五": "≤0.05" + } + }, + "AS1": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图(地块)", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "有效硫\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "30.0~40.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "AZN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锌\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "2.00~3.00", + "等级三": "1.00~2.00", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "CEC": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图(地块)", + "分析方法": "分析方法:EDTA-乙酸铵交换法", + "项目分级": "阳离子\n交换量级别", + "分级标准": "分级标准/\n(cmol/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "ECA": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图(地块)", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性钙\n含量级别", + "分级标准": "分级标准/\ncmol(½Ca²⁺)/kg", + "标准等级": { + "等级一": ">4.99", + "等级二": "2.50~4.99", + "等级三": "1.00~2.50", + "等级四": "0.25~1.00", + "等级五": "≤0.25" + } + }, + "EMG": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图(地块)", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性镁\n含量级别", + "分级标准": "分级标准/\ncmol(½Mg²⁺)/kg", + "标准等级": { + "等级一": ">1.64", + "等级二": "0.82~1.64", + "等级三": "0.41~0.82", + "等级四": "0.21~0.41", + "等级五": "≤0.21" + } + }, + "TSE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图(地块)", + "分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法", + "项目分级": "全硒\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "0.40~3.00", + "等级三": "0.17~0.40", + "等级四": "≤0.17" + } + }, + "TN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图(地块)", + "分析方法": "分析方法:自动定氮仪法", + "项目分级": "全氮\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.50~2.00", + "等级三": "1.00~1.50", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "TP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图(地块)", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全磷\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">1.50", + "等级二": "1.00~1.50", + "等级三": "0.60~1.00", + "等级四": "0.40~0.60", + "等级五": "≤0.40" + } + }, + "TK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图(地块)", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全钾\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AFE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "10.0~20.0", + "等级三": "8.0~10.0", + "等级四": "3.0~8.0", + "等级五": "≤3.0" + } + }, + "AK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图(地块)", + "分析方法": "分析方法:乙酸钠浸提-火焰光度法", + "项目分级": "速效钾\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">150", + "等级二": "100~150", + "等级三": "75~100", + "等级四": "50~75", + "等级五": "≤50" + } + }, + "AP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图(地块)", + "分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法", + "项目分级": "有效磷\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "20.0~40.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图(地块)", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "30.0~35.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "FL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "粉粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~30", + "等级三": "30~45", + "等级四": "45~75", + "等级五": ">75" + } + }, + "NL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "黏粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "SL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "砂粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤30", + "等级二": "30~40", + "等级三": "40~55", + "等级四": "55~85", + "等级五": ">85" + } + }, + "PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图(地块)", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "≤4.5", + "等级二": "4.5~5.0", + "等级三": "5.0~5.5", + "等级四": "5.5~6.0", + "等级五": "6.0~7.0", + "等级六": "7.0~7.5", + "等级七": "7.5~8.0", + "等级八": "8.0~8.5", + "等级九": ">8.5" + } + }, + "YXTCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图(地块)", + "分析方法": "分析方法:剖面法", + "项目分级": "有效土层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">100", + "等级二": "80~100", + "等级三": "60~80", + "等级四": "40~60", + "等级五": "≤40" + } + }, + "GZCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图(地块)", + "分析方法": "分析方法:剖面法", + "项目分级": "耕作层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">25.0", + "等级二": "20.0~25.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "TRRZ": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图(地块)", + "分析方法": "分析方法:热鼓风加热环刀法", + "项目分级": "土壤容重\n级别", + "分级标准": "分级标准/\n(g/cm³)", + "标准等级": { + "等级一": "≤0.90", + "等级二": "0.90~1.0", + "等级三": "1.00~1.20", + "等级四": "1.20~1.30", + "等级五": "1.30~1.40", + "等级六": "1.40~1.50", + "等级七": ">1.50" + } + }, + "LSFD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砾石丰度分布图(地块)", + "分析方法": "分析方法:剖面法", + "项目分级": "砾石丰度\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤0", + "等级二": "0~5", + "等级三": "5~15", + "等级四": "15~50", + "等级五": ">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.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "三普PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图(地块)", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": ">6.5", + "等级二": "5.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "酸化PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图(地块)", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "重度酸化": ">1.0", + "中度酸化": "0.5~1.0", + "轻度酸化": "0.3~0.5", + "未酸化": "-0.3~0.3", + "碱化": "-10~-0.3" + } + } + } +} \ No newline at end of file diff --git a/tools/config_json/广西_西南_config.json b/tools/config_json/广西_西南_config.json new file mode 100644 index 0000000..a2c7411 --- /dev/null +++ b/tools/config_json/广西_西南_config.json @@ -0,0 +1,418 @@ +{ + "export_config": { + "AB": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图", + "分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法", + "项目分级": "有效硼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1.00", + "等级二": "0.8~1.00", + "等级三": "0.50~0.80", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "ACU": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铜\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.00~2.00", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "AMN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锰\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "15.0~30.0", + "等级三": "5.0~15.0", + "等级四": "1.0~5.0", + "等级五": "≤1.0" + } + }, + "AMO": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图", + "分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法", + "项目分级": "有效钼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">0.20", + "等级二": "0.15~0.20", + "等级三": "0.10~0.15", + "等级四": "0.05~0.10", + "等级五": "≤0.05" + } + }, + "AS1": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "有效硫\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "30.0~40.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "AZN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锌\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "1.00~3.00", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "CEC": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图", + "分析方法": "分析方法:EDTA-乙酸铵交换法", + "项目分级": "阳离子\n交换量级别", + "分级标准": "分级标准/\n(cmol/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "20.0~30.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "ECA": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性钙\n含量级别", + "分级标准": "分级标准/\ncmol(½Ca²⁺)/kg", + "标准等级": { + "等级一": ">7.49", + "等级二": "4.99~7.49", + "等级三": "2.50~4.99", + "等级四": "1.00~2.50", + "等级五": "≤1.00" + } + }, + "EMG": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性镁\n含量级别", + "分级标准": "分级标准/\ncmol(½Mg²⁺)/kg", + "标准等级": { + "等级一": ">1.64", + "等级二": "1.23~1.64", + "等级三": "0.82~1.23", + "等级四": "0.41~0.82", + "等级五": "≤0.41" + } + }, + "TSE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图", + "分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法", + "项目分级": "全硒\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "0.40~3.00", + "等级三": "0.17~0.40", + "等级四": "≤0.17" + } + }, + "TN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图", + "分析方法": "分析方法:自动定氮仪法", + "项目分级": "全氮\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.50~2.00", + "等级三": "1.00~1.50", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "TP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全磷\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">1.00", + "等级二": "0.80~1.00", + "等级三": "0.60~0.80", + "等级四": "0.40~0.60", + "等级五": "≤0.40" + } + }, + "TK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全钾\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AFE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "10.0~20.0", + "等级三": "5.0~10.0", + "等级四": "3.0~5.0", + "等级五": "≤3.0" + } + }, + "AK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图", + "分析方法": "分析方法:乙酸钠浸提-火焰光度法", + "项目分级": "速效钾\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">150", + "等级二": "100~150", + "等级三": "75~100", + "等级四": "50~75", + "等级五": "≤50" + } + }, + "AP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图", + "分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法", + "项目分级": "有效磷\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "25.0~40.0", + "等级三": "15.0~25.0", + "等级四": "5.0~15.0", + "等级五": "≤5.0" + } + }, + "OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "25.0~35.0", + "等级三": "15.0~25.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "FL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "粉粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~30", + "等级三": "30~45", + "等级四": "45~75", + "等级五": ">75" + } + }, + "NL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "黏粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "SL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "砂粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤30", + "等级二": "30~40", + "等级三": "40~55", + "等级四": "55~85", + "等级五": ">85" + } + }, + "PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "≤4.5", + "等级二": "4.5~5.0", + "等级三": "5.0~5.5", + "等级四": "5.5~6.0", + "等级五": "6.0~7.0", + "等级六": "7.0~7.5", + "等级七": "7.5~8.0", + "等级八": "8.0~8.5", + "等级九": ">8.5" + } + }, + "YXTCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "有效土层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">100", + "等级二": "80~100", + "等级三": "60~80", + "等级四": "40~60", + "等级五": "≤40" + } + }, + "GZCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "耕作层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">25.0", + "等级二": "20.0~25.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "TRRZ": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图", + "分析方法": "分析方法:热鼓风加热环刀法", + "项目分级": "土壤容重\n级别", + "分级标准": "分级标准/\n(g/cm³)", + "标准等级": { + "等级一": "≤0.90", + "等级二": "0.90~1.00", + "等级三": "1.00~1.10", + "等级四": "1.10~1.25", + "等级五": "1.25~1.35", + "等级六": "1.35~1.45", + "等级七": "1.45~1.55", + "等级八": ">1.55" + } + }, + "LSFD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砾石丰度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "砾石丰度\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤0", + "等级二": "0~5", + "等级三": "5~15", + "等级四": "15~50", + "等级五": ">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.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "三普PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": ">6.5", + "等级二": "5.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "酸化PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "重度酸化": ">1.0", + "中度酸化": "0.5~1.0", + "轻度酸化": "0.3~0.5", + "未酸化": "-0.3~0.3", + "碱化": "-10~-0.3" + } + }, + "专题图OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": "≤10.0", + "等级二": "10.0~20.0", + "等级三": "20.0~30.0", + "等级四": "30.0~40.0", + "等级五": ">40.0" + } + } + } +} \ No newline at end of file diff --git a/tools/config_json/广西_西南_地块_config.json b/tools/config_json/广西_西南_地块_config.json new file mode 100644 index 0000000..b725ae0 --- /dev/null +++ b/tools/config_json/广西_西南_地块_config.json @@ -0,0 +1,405 @@ +{ + "export_config": { + "AB": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图(地块)", + "分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法", + "项目分级": "有效硼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1.00", + "等级二": "0.8~1.00", + "等级三": "0.50~0.80", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "ACU": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铜\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.00~2.00", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "AMN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锰\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "15.0~30.0", + "等级三": "5.0~15.0", + "等级四": "1.0~5.0", + "等级五": "≤1.0" + } + }, + "AMO": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图(地块)", + "分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法", + "项目分级": "有效钼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">0.20", + "等级二": "0.15~0.20", + "等级三": "0.10~0.15", + "等级四": "0.05~0.10", + "等级五": "≤0.05" + } + }, + "AS1": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图(地块)", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "有效硫\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "30.0~40.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "AZN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锌\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "1.00~3.00", + "等级三": "0.50~1.00", + "等级四": "0.20~0.50", + "等级五": "≤0.20" + } + }, + "CEC": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图(地块)", + "分析方法": "分析方法:EDTA-乙酸铵交换法", + "项目分级": "阳离子\n交换量级别", + "分级标准": "分级标准/\n(cmol/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "20.0~30.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "ECA": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图(地块)", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性钙\n含量级别", + "分级标准": "分级标准/\ncmol(½Ca²⁺)/kg", + "标准等级": { + "等级一": ">7.49", + "等级二": "4.99~7.49", + "等级三": "2.50~4.99", + "等级四": "1.00~2.50", + "等级五": "≤1.00" + } + }, + "EMG": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图(地块)", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性镁\n含量级别", + "分级标准": "分级标准/\ncmol(½Mg²⁺)/kg", + "标准等级": { + "等级一": ">1.64", + "等级二": "1.23~1.64", + "等级三": "0.82~1.23", + "等级四": "0.41~0.82", + "等级五": "≤0.41" + } + }, + "TSE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图(地块)", + "分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法", + "项目分级": "全硒\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "0.40~3.00", + "等级三": "0.17~0.40", + "等级四": "≤0.17" + } + }, + "TN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图(地块)", + "分析方法": "分析方法:自动定氮仪法", + "项目分级": "全氮\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.50~2.00", + "等级三": "1.00~1.50", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "TP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图(地块)", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全磷\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">1.00", + "等级二": "0.80~1.00", + "等级三": "0.60~0.80", + "等级四": "0.40~0.60", + "等级五": "≤0.40" + } + }, + "TK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图(地块)", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全钾\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "15.0~20.0", + "等级三": "10.0~15.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "AFE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "10.0~20.0", + "等级三": "5.0~10.0", + "等级四": "3.0~5.0", + "等级五": "≤3.0" + } + }, + "AK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图(地块)", + "分析方法": "分析方法:乙酸钠浸提-火焰光度法", + "项目分级": "速效钾\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">150", + "等级二": "100~150", + "等级三": "75~100", + "等级四": "50~75", + "等级五": "≤50" + } + }, + "AP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图(地块)", + "分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法", + "项目分级": "有效磷\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "25.0~40.0", + "等级三": "15.0~25.0", + "等级四": "5.0~15.0", + "等级五": "≤5.0" + } + }, + "OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图(地块)", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "25.0~35.0", + "等级三": "15.0~25.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "FL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "粉粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~30", + "等级三": "30~45", + "等级四": "45~75", + "等级五": ">75" + } + }, + "NL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "黏粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "SL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "砂粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤30", + "等级二": "30~40", + "等级三": "40~55", + "等级四": "55~85", + "等级五": ">85" + } + }, + "PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图(地块)", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "≤4.5", + "等级二": "4.5~5.0", + "等级三": "5.0~5.5", + "等级四": "5.5~6.0", + "等级五": "6.0~7.0", + "等级六": "7.0~7.5", + "等级七": "7.5~8.0", + "等级八": "8.0~8.5", + "等级九": ">8.5" + } + }, + "YXTCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图(地块)", + "分析方法": "分析方法:剖面法", + "项目分级": "有效土层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">100", + "等级二": "80~100", + "等级三": "60~80", + "等级四": "40~60", + "等级五": "≤40" + } + }, + "GZCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图(地块)", + "分析方法": "分析方法:剖面法", + "项目分级": "耕作层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">25.0", + "等级二": "20.0~25.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "TRRZ": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图(地块)", + "分析方法": "分析方法:热鼓风加热环刀法", + "项目分级": "土壤容重\n级别", + "分级标准": "分级标准/\n(g/cm³)", + "标准等级": { + "等级一": "≤0.90", + "等级二": "0.90~1.00", + "等级三": "1.00~1.10", + "等级四": "1.10~1.25", + "等级五": "1.25~1.35", + "等级六": "1.35~1.45", + "等级七": "1.45~1.55", + "等级八": ">1.55" + } + }, + "LSFD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砾石丰度分布图(地块)", + "分析方法": "分析方法:剖面法", + "项目分级": "砾石丰度\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤0", + "等级二": "0~5", + "等级三": "5~15", + "等级四": "15~50", + "等级五": ">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.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "三普PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图(地块)", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": ">6.5", + "等级二": "5.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "酸化PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图(地块)", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "重度酸化": ">1.0", + "中度酸化": "0.5~1.0", + "轻度酸化": "0.3~0.5", + "未酸化": "-0.3~0.3", + "碱化": "-10~-0.3" + } + } + } +} \ No newline at end of file diff --git a/tools/config_json/广西_长江中下游_config.json b/tools/config_json/广西_长江中下游_config.json new file mode 100644 index 0000000..ba7607a --- /dev/null +++ b/tools/config_json/广西_长江中下游_config.json @@ -0,0 +1,365 @@ +{ + "export_config": { + "AB": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图", + "分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法", + "项目分级": "有效硼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.00~2.00", + "等级三": "0.50~1.00", + "等级四": "0.25~0.50", + "等级五": "≤0.25" + } + }, + "ACU": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铜\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1.80", + "等级二": "1.00~1.80", + "等级三": "0.20~1.00", + "等级四": "0.10~0.20", + "等级五": "≤0.10" + } + }, + "AMN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锰\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">50.0", + "等级二": "15.0~50.0", + "等级三": "7.0~15.0", + "等级四": "3.0~7.0", + "等级五": "≤3.0" + } + }, + "AMO": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图", + "分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法", + "项目分级": "有效钼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">0.20", + "等级二": "0.15~0.20", + "等级三": "0.10~0.15", + "等级四": "0.05~0.10", + "等级五": "≤0.05" + } + }, + "AS1": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "有效硫\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "30.0~40.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "AZN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锌\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "2.00~3.00", + "等级三": "1.00~2.00", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "CEC": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图", + "分析方法": "分析方法:EDTA-乙酸铵交换法", + "项目分级": "阳离子\n交换量级别", + "分级标准": "分级标准/\n(cmol/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "20.0~30.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "ECA": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性钙\n含量级别", + "分级标准": "分级标准/\ncmol(½Ca²⁺)/kg", + "标准等级": { + "等级一": ">5.99", + "等级二": "3.99~5.99", + "等级三": "2.50~3.99", + "等级四": "1.00~2.50", + "等级五": "≤1.00" + } + }, + "EMG": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性镁\n含量级别", + "分级标准": "分级标准/\ncmol(½Mg²⁺)/kg", + "标准等级": { + "等级一": ">2.47", + "等级二": "1.64~2.47", + "等级三": "0.82~1.64", + "等级四": "0.41~0.82", + "等级五": "≤0.41" + } + }, + "TSE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图", + "分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法", + "项目分级": "全硒\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "0.40~3.00", + "等级三": "0.17~0.40", + "等级四": "≤0.17" + } + }, + "TN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图", + "分析方法": "分析方法:自动定氮仪法", + "项目分级": "全氮\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.50~2.00", + "等级三": "1.00~1.50", + "等级四": "0.75~1.00", + "等级五": "≤0.75" + } + }, + "TP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全磷\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">1.00", + "等级二": "0.80~1.00", + "等级三": "0.60~0.80", + "等级四": "0.40~0.60", + "等级五": "≤0.40" + } + }, + "TK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全钾\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">25.0", + "等级二": "20.0~25.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "AFE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "10.0~20.0", + "等级三": "4.5~10.0", + "等级四": "2.5~4.5", + "等级五": "≤2.5" + } + }, + "AK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图", + "分析方法": "分析方法:乙酸钠浸提-火焰光度法", + "项目分级": "速效钾\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">150", + "等级二": "125~150", + "等级三": "100~125", + "等级四": "75~100", + "等级五": "≤75" + } + }, + "AP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图", + "分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法", + "项目分级": "有效磷\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "25.0~35.0", + "等级三": "15.0~25.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "25.0~35.0", + "等级三": "15.0~25.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "FL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "粉粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~30", + "等级三": "30~45", + "等级四": "45~75", + "等级五": ">75" + } + }, + "NL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "黏粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "SL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "砂粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤30", + "等级二": "30~40", + "等级三": "40~55", + "等级四": "55~85", + "等级五": ">85" + } + }, + "PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "6.5~7.5", + "等级二": "5.5~6.5", + "等级三": "7.5~8.5", + "等级四": "4.5~5.5", + "等级五": ">8.5,\n≤4.5" + } + }, + "YXTCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "有效土层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">100", + "等级二": "80~100", + "等级三": "60~80", + "等级四": "40~60", + "等级五": "≤40" + } + }, + "GZCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图", + "分析方法": "分析方法:剖面法", + "项目分级": "耕作层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">20.0", + "等级二": "16.0~20.0", + "等级三": "12.0~16.0", + "等级四": "8.0~12.0", + "等级五": "≤8.0" + } + }, + "TRRZ": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图", + "分析方法": "分析方法:热鼓风加热环刀法", + "项目分级": "土壤容重\n级别", + "分级标准": "分级标准/\n(g/cm³)", + "标准等级": { + "等级一": "1.00~1.20", + "等级二": "1.20~1.30,\n0.90~1.00", + "等级三": "1.30~1.40", + "等级四": "1.40~1.50", + "等级五": ">1.50,\n≤0.90" + } + }, + "TRZD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图", + "分析方法": "分析方法:吸管法", + "项目分级": "土壤质地\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "黏壤质": "1", + "黏质": "2", + "壤质": "3", + "砂壤质": "4", + "砂质": "5" + } + }, + "二普PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤pH分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": ">6.5", + "等级二": "5.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "三普PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": ">6.5", + "等级二": "5.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "酸化PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "重度酸化": ">1.0", + "中度酸化": "0.5~1.0", + "轻度酸化": "0.3~0.5", + "未酸化": "-0.3~0.3", + "碱化": "-10~-0.3" + } + } + } +} \ No newline at end of file diff --git a/tools/config_json/广西_长江中下游_地块_config.json b/tools/config_json/广西_长江中下游_地块_config.json new file mode 100644 index 0000000..ad901ff --- /dev/null +++ b/tools/config_json/广西_长江中下游_地块_config.json @@ -0,0 +1,365 @@ +{ + "export_config": { + "AB": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硼含量分布图(地块)", + "分析方法": "分析方法:沸水提取-电感耦合等离子体发射光谱法", + "项目分级": "有效硼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.00~2.00", + "等级三": "0.50~1.00", + "等级四": "0.25~0.50", + "等级五": "≤0.25" + } + }, + "ACU": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铜含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铜\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">1.80", + "等级二": "1.00~1.80", + "等级三": "0.20~1.00", + "等级四": "0.10~0.20", + "等级五": "≤0.10" + } + }, + "AMN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锰含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锰\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">50.0", + "等级二": "15.0~50.0", + "等级三": "7.0~15.0", + "等级四": "3.0~7.0", + "等级五": "≤3.0" + } + }, + "AMO": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效钼含量分布图(地块)", + "分析方法": "分析方法:草酸-草酸铵浸提-电感耦合等离子体质谱法", + "项目分级": "有效钼\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">0.20", + "等级二": "0.15~0.20", + "等级三": "0.10~0.15", + "等级四": "0.05~0.10", + "等级五": "≤0.05" + } + }, + "AS1": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效硫含量分布图(地块)", + "分析方法": "分析方法:电感耦合等离子体发射光谱法", + "项目分级": "有效硫\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">40.0", + "等级二": "30.0~40.0", + "等级三": "20.0~30.0", + "等级四": "10.0~20.0", + "等级五": "≤10.0" + } + }, + "AZN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效锌含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效锌\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "2.00~3.00", + "等级三": "1.00~2.00", + "等级四": "0.50~1.00", + "等级五": "≤0.50" + } + }, + "CEC": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤阳离子交换量分布图(地块)", + "分析方法": "分析方法:EDTA-乙酸铵交换法", + "项目分级": "阳离子\n交换量级别", + "分级标准": "分级标准/\n(cmol/kg)", + "标准等级": { + "等级一": ">30.0", + "等级二": "20.0~30.0", + "等级三": "10.0~20.0", + "等级四": "5.0~10.0", + "等级五": "≤5.0" + } + }, + "ECA": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性钙含量分布图(地块)", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性钙\n含量级别", + "分级标准": "分级标准/\ncmol(½Ca²⁺)/kg", + "标准等级": { + "等级一": ">5.99", + "等级二": "3.99~5.99", + "等级三": "2.50~3.99", + "等级四": "1.00~2.50", + "等级五": "≤1.00" + } + }, + "EMG": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤交换性镁含量分布图(地块)", + "分析方法": "分析方法:原子吸收分光光度法", + "项目分级": "交换性镁\n含量级别", + "分级标准": "分级标准/\ncmol(½Mg²⁺)/kg", + "标准等级": { + "等级一": ">2.47", + "等级二": "1.64~2.47", + "等级三": "0.82~1.64", + "等级四": "0.41~0.82", + "等级五": "≤0.41" + } + }, + "TSE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全硒含量分布图(地块)", + "分析方法": "分析方法:酸溶-氢化物发生-原子荧光光谱法", + "项目分级": "全硒\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">3.00", + "等级二": "0.40~3.00", + "等级三": "0.17~0.40", + "等级四": "≤0.17" + } + }, + "TN": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全氮含量分布图(地块)", + "分析方法": "分析方法:自动定氮仪法", + "项目分级": "全氮\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">2.00", + "等级二": "1.50~2.00", + "等级三": "1.00~1.50", + "等级四": "0.75~1.00", + "等级五": "≤0.75" + } + }, + "TP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全磷含量分布图(地块)", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全磷\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">1.00", + "等级二": "0.80~1.00", + "等级三": "0.60~0.80", + "等级四": "0.40~0.60", + "等级五": "≤0.40" + } + }, + "TK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤全钾含量分布图(地块)", + "分析方法": "分析方法:酸消解-电感耦合等离子体发射光谱法", + "项目分级": "全钾\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">25.0", + "等级二": "20.0~25.0", + "等级三": "15.0~20.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "AFE": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效铁含量分布图(地块)", + "分析方法": "分析方法:二乙三胺五乙酸(DTPA)浸提法", + "项目分级": "有效铁\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">20.0", + "等级二": "10.0~20.0", + "等级三": "4.5~10.0", + "等级四": "2.5~4.5", + "等级五": "≤2.5" + } + }, + "AK": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤速效钾含量分布图(地块)", + "分析方法": "分析方法:乙酸钠浸提-火焰光度法", + "项目分级": "速效钾\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">150", + "等级二": "125~150", + "等级三": "100~125", + "等级四": "75~100", + "等级五": "≤75" + } + }, + "AP": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效磷含量分布图(地块)", + "分析方法": "分析方法:氟化铵-盐酸溶液浸提-钼锑抗比色法", + "项目分级": "有效磷\n含量级别", + "分级标准": "分级标准/\n(mg/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "25.0~35.0", + "等级三": "15.0~25.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "OM": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有机质含量分布图(地块)", + "分析方法": "分析方法:重铬酸钾氧化-容量法", + "项目分级": "有机质\n含量级别", + "分级标准": "分级标准/\n(g/kg)", + "标准等级": { + "等级一": ">35.0", + "等级二": "25.0~35.0", + "等级三": "15.0~25.0", + "等级四": "10.0~15.0", + "等级五": "≤10.0" + } + }, + "FL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤粉粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "粉粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~30", + "等级三": "30~45", + "等级四": "45~75", + "等级五": ">75" + } + }, + "NL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤黏粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "黏粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤15", + "等级二": "15~25", + "等级三": "25~45", + "等级四": "45~65", + "等级五": ">65" + } + }, + "SL": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤砂粒含量分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "砂粒含量\n级别", + "分级标准": "分级标准/\n(%)", + "标准等级": { + "等级一": "≤30", + "等级二": "30~40", + "等级三": "40~55", + "等级四": "55~85", + "等级五": ">85" + } + }, + "PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤pH分布图(地块)", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "6.5~7.5", + "等级二": "5.5~6.5", + "等级三": "7.5~8.5", + "等级四": "4.5~5.5", + "等级五": ">8.5,\n≤4.5" + } + }, + "YXTCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤有效土层厚度分布图(地块)", + "分析方法": "分析方法:剖面法", + "项目分级": "有效土层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">100", + "等级二": "80~100", + "等级三": "60~80", + "等级四": "40~60", + "等级五": "≤40" + } + }, + "GZCHD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤耕作层厚度分布图(地块)", + "分析方法": "分析方法:剖面法", + "项目分级": "耕作层厚度\n级别", + "分级标准": "分级标准/\n(cm)", + "标准等级": { + "等级一": ">20.0", + "等级二": "16.0~20.0", + "等级三": "12.0~16.0", + "等级四": "8.0~12.0", + "等级五": "≤8.0" + } + }, + "TRRZ": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤容重分布图(地块)", + "分析方法": "分析方法:热鼓风加热环刀法", + "项目分级": "土壤容重\n级别", + "分级标准": "分级标准/\n(g/cm³)", + "标准等级": { + "等级一": "1.00~1.20", + "等级二": "1.20~1.30,\n0.90~1.00", + "等级三": "1.30~1.40", + "等级四": "1.40~1.50", + "等级五": ">1.50,\n≤0.90" + } + }, + "TRZD": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤质地分布图(地块)", + "分析方法": "分析方法:吸管法", + "项目分级": "土壤质地\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "黏壤质": "1", + "黏质": "2", + "壤质": "3", + "砂壤质": "4", + "砂质": "5" + } + }, + "二普PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤pH分布图(地块)", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": ">6.5", + "等级二": "5.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "三普PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}三普土壤pH分布图(地块)", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": ">6.5", + "等级二": "5.5~6.5", + "等级三": "4.5~5.5", + "等级四": "≤4.5" + } + }, + "酸化PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}二普土壤酸化分布图(地块)", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "重度酸化": ">1.0", + "中度酸化": "0.5~1.0", + "轻度酸化": "0.3~0.5", + "未酸化": "-0.3~0.3", + "碱化": "-10~-0.3" + } + } + } +} \ No newline at end of file diff --git a/tools/config_json/西南分级.json b/tools/config_json/西南分级.json new file mode 100644 index 0000000..73b3409 --- /dev/null +++ b/tools/config_json/西南分级.json @@ -0,0 +1,30 @@ +{ + "AB": ">1.00;0.8~1.00;0.50~0.80;0.20~0.50;≤0.20", + "ACU": ">2.00;1.00~2.00;0.50~1.00;0.20~0.50;≤0.20", + "AMN": ">30.0;15.0~30.0;5.0~15.0;1.0~5.0;≤1.0", + "AMO": ">0.20;0.15~0.20;0.10~0.15;0.05~0.10;≤0.05", + "AS1": ">40.0;30.0~40.0;20.0~30.0;10.0~20.0;≤10.0", + "AZN": ">3.00;1.00~3.00;0.50~1.00;0.20~0.50;≤0.20", + "CEC": ">30.0;20.0~30.0;15.0~20.0;10.0~15.0;≤10.0", + "ECA": ">7.49;4.99~7.49;2.50~4.99;1.00~2.50;≤1.00", + "EMG": ">1.64;1.23~1.64;0.82~1.23;0.41~0.82;≤0.41", + "TSE": ">3.00;0.40~3.00;0.17~0.40;≤0.17", + "TN": ">2.00;1.50~2.00;1.00~1.50;0.50~1.00;≤0.50", + "TP": ">1.00;0.80~1.00;0.60~0.80;0.40~0.60;≤0.40", + "TK": ">20.0;15.0~20.0;10.0~15.0;5.0~10.0;≤5.0", + "AFE": ">20.0;10.0~20.0;5.0~10.0;3.0~5.0;≤3.0", + "AK": ">150;100~150;75~100;50~75;≤50", + "AP": ">40.0;25.0~40.0;15.0~25.0;5.0~15.0;≤5.0", + "OM": ">35.0;25.0~35.0;15.0~25.0;10.0~15.0;≤10.0", + "FL": "≤15;15~30;30~45;45~75;>75", + "NL": "≤15;15~25;25~45;45~65;>65", + "SL": "≤30;30~40;40~55;55~85;>85", + "PH": "6.0~7.0;7.0~7.5,5.5~6.0;7.5~8.0,5.0~5.5;8.0~8.5,4.5~5.0;>8.5,≤4.5", + "YXTCHD": ">100;80~100;60~80;40~60;≤40", + "GZCHD": ">25.0;20.0~25.0;15.0~20.0;10.0~15.0;≤10.0", + "TRRZ": "1.10~1.25;1.25~1.35,1.00~1.10;1.35~1.45;1.45~1.55,0.90~1.00;>1.55,≤0.90", + "TRZD": "1;2;3;4;5", + "二普 PH": ">6.5;5.5~6.5;4.5~5.5;≤4.5", + "三普 PH": ">6.5;5.5~6.5;4.5~5.5;≤4.5", + "酸化 PH": ">1.0;0.5~1.0;0.3~0.5;-0.3~0.3;-10~-0.3" +} \ No newline at end of file diff --git a/tools/config_json/酸化专题配置_config.json b/tools/config_json/酸化专题配置_config.json new file mode 100644 index 0000000..d5a69a6 --- /dev/null +++ b/tools/config_json/酸化专题配置_config.json @@ -0,0 +1,91 @@ +{ + "export_config": { + "三普PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤三普pH值分级分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "≤4.5", + "等级二": "4.5~5.0", + "等级三": "5.0~5.5", + "等级四": "5.5~6.5", + "等级五": "6.5~7.5", + "等级六": "7.5~8.5", + "等级七": "8.5~9.0", + "等级八": ">9.0" + } + }, + "二普PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤二普pH值分级分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "≤4.5", + "等级二": "4.5~5.0", + "等级三": "5.0~5.5", + "等级四": "5.5~6.5", + "等级五": "6.5~7.5", + "等级六": "7.5~8.5", + "等级七": "8.5~9.0", + "等级八": ">9.0" + } + }, + "测土PH": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}测土配方施肥pH值分级分布图", + "分析方法": "分析方法:电位法", + "项目分级": "pH\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "等级一": "≤4.5", + "等级二": "4.5~5.0", + "等级三": "5.0~5.5", + "等级四": "5.5~6.5", + "等级五": "6.5~7.5", + "等级六": "7.5~8.5", + "等级七": "8.5~9.0", + "等级八": ">9.0" + } + }, + "二普-三普": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤酸化等级分布图(土壤二普-土壤三普)", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "弱酸化": "0.1~0.3", + "轻度酸化": "0.3~0.5", + "中度酸化": "0.5~1.0", + "重度酸化": ">1.0", + "其它": "-10~0.1" + } + }, + "二普-测土": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤酸化等级分布图(土壤二普-测土配方施肥)", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "弱酸化": "0.1~0.3", + "轻度酸化": "0.3~0.5", + "中度酸化": "0.5~1.0", + "重度酸化": ">1.0", + "其它": "-10~0.1" + } + }, + "测土-三普": { + "项目名称": "第三次全国土壤普查成果\n{区县占位符}土壤酸化等级分布图(测土配方施肥-土壤三普)", + "分析方法": "分析方法:电位法", + "项目分级": "酸化\n级别", + "分级标准": "分级标准/\n ", + "标准等级": { + "弱酸化": "0.1~0.3", + "轻度酸化": "0.3~0.5", + "中度酸化": "0.5~1.0", + "重度酸化": ">1.0", + "其它": "-10~0.1" + } + } + } +} \ No newline at end of file diff --git a/tools/core/__init__.py b/tools/core/__init__.py new file mode 100644 index 0000000..f97164e --- /dev/null +++ b/tools/core/__init__.py @@ -0,0 +1,4 @@ +""" +核心功能模块 +包含独立的功能脚本,可以在独立进程中运行 +""" \ No newline at end of file diff --git a/tools/core/acid_stats/__init__.py b/tools/core/acid_stats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/core/acid_stats/土地利用类型酸化统计表.py b/tools/core/acid_stats/土地利用类型酸化统计表.py new file mode 100644 index 0000000..b7f8699 --- /dev/null +++ b/tools/core/acid_stats/土地利用类型酸化统计表.py @@ -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(['', '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(['', '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() \ No newline at end of file diff --git a/tools/core/acid_stats/土壤类型图酸化统计表.py b/tools/core/acid_stats/土壤类型图酸化统计表.py new file mode 100644 index 0000000..ada2a80 --- /dev/null +++ b/tools/core/acid_stats/土壤类型图酸化统计表.py @@ -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(['', '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(['', '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() diff --git a/tools/core/acid_stats/空间连接.py b/tools/core/acid_stats/空间连接.py new file mode 100644 index 0000000..c514bd8 --- /dev/null +++ b/tools/core/acid_stats/空间连接.py @@ -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("清理完成。") \ No newline at end of file diff --git a/tools/core/acid_stats/行政区划酸化统计表.py b/tools/core/acid_stats/行政区划酸化统计表.py new file mode 100644 index 0000000..987015b --- /dev/null +++ b/tools/core/acid_stats/行政区划酸化统计表.py @@ -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(['', '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(['', '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(['', '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() diff --git a/tools/core/export_excel_to_jpg_v1.py b/tools/core/export_excel_to_jpg_v1.py new file mode 100644 index 0000000..b82796d --- /dev/null +++ b/tools/core/export_excel_to_jpg_v1.py @@ -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: + """ + print(f"STATUS:{message}") + sys.stdout.flush() # 确保立即输出 + +def print_result(success, output_path="", error_message=""): + """ + 输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果 + 格式: RESULT:True|| + 格式: RESULT:False|| + """ + 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() diff --git a/tools/core/export_layout.py b/tools/core/export_layout.py new file mode 100644 index 0000000..60ee607 --- /dev/null +++ b/tools/core/export_layout.py @@ -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()) \ No newline at end of file diff --git a/tools/core/export_map_v1.py b/tools/core/export_map_v1.py new file mode 100644 index 0000000..39942cf --- /dev/null +++ b/tools/core/export_map_v1.py @@ -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: + """ + print(f"STATUS:{message}") + sys.stdout.flush() # 确保立即输出 + +def print_result(success, output_path="", error_message=""): + """ + 输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果 + 格式: RESULT:True|| + 格式: RESULT:False|| + """ + 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()) diff --git a/tools/core/raster_to_polygon.py b/tools/core/raster_to_polygon.py new file mode 100644 index 0000000..af352fe --- /dev/null +++ b/tools/core/raster_to_polygon.py @@ -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: + """ + print(f"STATUS:{message}") + sys.stdout.flush() # 确保立即输出 + +def print_result(success, output_path="", error_message=""): + """ + 输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果 + 格式: RESULT:True|| + 格式: RESULT:False|| + """ + 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() \ No newline at end of file diff --git a/tools/core/soil_prop_stats/B1_TRZD12土壤属性分级分布.py b/tools/core/soil_prop_stats/B1_TRZD12土壤属性分级分布.py new file mode 100644 index 0000000..61fed4b --- /dev/null +++ b/tools/core/soil_prop_stats/B1_TRZD12土壤属性分级分布.py @@ -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(['', '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() diff --git a/tools/core/soil_prop_stats/B1_TRZD土壤属性分级分布.py b/tools/core/soil_prop_stats/B1_TRZD土壤属性分级分布.py new file mode 100644 index 0000000..84780a2 --- /dev/null +++ b/tools/core/soil_prop_stats/B1_TRZD土壤属性分级分布.py @@ -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(['', '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() diff --git a/tools/core/soil_prop_stats/B1土壤属性分级分布.py b/tools/core/soil_prop_stats/B1土壤属性分级分布.py new file mode 100644 index 0000000..ae0abd9 --- /dev/null +++ b/tools/core/soil_prop_stats/B1土壤属性分级分布.py @@ -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(['', '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() diff --git a/tools/core/soil_prop_stats/B2_TRZD12土地利用类型土壤属性.py b/tools/core/soil_prop_stats/B2_TRZD12土地利用类型土壤属性.py new file mode 100644 index 0000000..b0b23f7 --- /dev/null +++ b/tools/core/soil_prop_stats/B2_TRZD12土地利用类型土壤属性.py @@ -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(['', '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) diff --git a/tools/core/soil_prop_stats/B2_TRZD土地利用类型土壤属性.py b/tools/core/soil_prop_stats/B2_TRZD土地利用类型土壤属性.py new file mode 100644 index 0000000..a388403 --- /dev/null +++ b/tools/core/soil_prop_stats/B2_TRZD土地利用类型土壤属性.py @@ -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(['', '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) diff --git a/tools/core/soil_prop_stats/B2土地利用类型土壤属性.py b/tools/core/soil_prop_stats/B2土地利用类型土壤属性.py new file mode 100644 index 0000000..4a73f87 --- /dev/null +++ b/tools/core/soil_prop_stats/B2土地利用类型土壤属性.py @@ -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(['', '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() diff --git a/tools/core/soil_prop_stats/B3_TRZD12不同土壤类型土壤属性.py b/tools/core/soil_prop_stats/B3_TRZD12不同土壤类型土壤属性.py new file mode 100644 index 0000000..1c7a472 --- /dev/null +++ b/tools/core/soil_prop_stats/B3_TRZD12不同土壤类型土壤属性.py @@ -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(['', '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() diff --git a/tools/core/soil_prop_stats/B3_TRZD不同土壤类型土壤属性.py b/tools/core/soil_prop_stats/B3_TRZD不同土壤类型土壤属性.py new file mode 100644 index 0000000..6e97934 --- /dev/null +++ b/tools/core/soil_prop_stats/B3_TRZD不同土壤类型土壤属性.py @@ -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(['', '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() diff --git a/tools/core/soil_prop_stats/B3不同土壤类型土壤属性.py b/tools/core/soil_prop_stats/B3不同土壤类型土壤属性.py new file mode 100644 index 0000000..c502a19 --- /dev/null +++ b/tools/core/soil_prop_stats/B3不同土壤类型土壤属性.py @@ -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(['', '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() diff --git a/tools/core/soil_prop_stats/E1土壤属性历史变化.py b/tools/core/soil_prop_stats/E1土壤属性历史变化.py new file mode 100644 index 0000000..1a08dcc --- /dev/null +++ b/tools/core/soil_prop_stats/E1土壤属性历史变化.py @@ -0,0 +1,1269 @@ +# -*- 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.core.utils import common_utils +from tools.core.utils.math_utils import * +from tools.config.pandas_field_cal_func import * +from tools.core.utils.excel_utils import ExcelStyleUtils +from tools.core.utils.os_utils import temp_files_processor + + +# --- 2. 辅助函数 --- +yjdl_order = ["耕地", "园地", "林地", "草地", "其他"] +ejdl_order = ["水田", "旱地", "水浇地", "果园", "茶园", "橡胶园", "其他园地"] + +# 计算属性等级 +def get_prop_level(prop_level): + """根据输入值判断 返回等级""" + if pd.isna(prop_level) or prop_level == 0: + return "-" + # 请根据您的实际分级标准调整这里的阈值 + if prop_level == 1 or prop_level == 6 or prop_level == '等级一': + return "Ⅰ级" + elif prop_level == 2 or prop_level == 7 or prop_level == '等级二': + return "Ⅱ级" + elif prop_level == 3 or prop_level == 8 or prop_level == '等级三': + return "Ⅲ级" + elif prop_level == 4 or prop_level == 9 or prop_level == '等级四': + return "Ⅳ级" + elif prop_level == 5 or 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 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 = "样点属性分级" + + # 使用向量化方法(性能更好) + 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(sanpu_gdb_path, history_gdb_path, soil_prop_feature_name, prop_config=None): + """ + 【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。 + """ + print("开始处理数据...") + + def clean_df(df, columns): + for col in columns: + df[col] = df[col].astype(str).str.strip() + df.replace(['', 'None', '', '<空>'], np.nan, inplace=True) + df.dropna(subset=columns, inplace=True) + return df + + # ==a. 处理三普样点数据,计算“样点均值” --- + print("--> 步骤1: 计算样点均值...") + field_name = soil_prop_feature_name + sanpu_sample = os.path.join(sanpu_gdb_path, soil_prop_feature_name) + df_samples = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(sanpu_sample, ['TDLYLX',field_name])) + df_samples = clean_df(df_samples, ['TDLYLX', field_name]) + df_samples[field_name] = df_samples[field_name].astype(float) + # 只要土地利用为耕地的 + df_samples = df_samples[df_samples['TDLYLX'].str.startswith('01')] + + processed_df = process_soil_dataframe(df_samples, prop_config, field_name) # 返回具有属性分级的列 + processed_df['样点属性分级'] = processed_df['样点属性分级'].astype('int') + if field_name == 'PH': + processed_df['属性分级'] = processed_df['样点属性分级'].apply(get_prop_level_for_pH) + else: + processed_df['属性分级'] = processed_df['样点属性分级'].apply(get_prop_level) + + # ===处理样点数据,计算 各分级样点数 + df_sample_means = processed_df.groupby(['属性分级','样点属性分级']).size().reset_index(name='样点数') + df_sample_means['样点数占比'] = fix_percentages(df_sample_means['样点数'].values , df_sample_means['样点数'].sum()) + + + # ==处理历史样点数据,计算“样点均值” + history_sample = os.path.join(history_gdb_path, soil_prop_feature_name) + history_df_samples = pd.DataFrame(arcpy.da.FeatureClassToNumPyArray(history_sample, ['DLBM',field_name])) + history_df_samples['TDLYLX'] = history_df_samples['DLBM'] + history_df_samples = clean_df(history_df_samples, ['TDLYLX',field_name]) + history_df_samples[field_name] = history_df_samples[field_name].astype(float) + # 只要土地利用为耕地的 + history_df_samples = history_df_samples[history_df_samples['TDLYLX'].str.startswith('01')] + + history_processed_df = process_soil_dataframe(history_df_samples, prop_config, field_name) # 返回具有属性分级的列 + history_processed_df['样点属性分级'] = history_processed_df['样点属性分级'].astype('int') + if field_name == 'PH': + history_processed_df['属性分级'] = history_processed_df['样点属性分级'].apply(get_prop_level_for_pH) + else: + history_processed_df['属性分级'] = history_processed_df['样点属性分级'].apply(get_prop_level) + + # ===处理样点数据,计算 各分级样点数 + history_df_sample_means = history_processed_df.groupby(['属性分级','样点属性分级']).size().reset_index(name='历史样点数') + history_df_sample_means['历史样点数占比'] = fix_percentages(history_df_sample_means['历史样点数'].values, history_df_sample_means['历史样点数'].sum()) + + # --- c. 合并数据 --- + print("--> 步骤3: 合并数据...") + df_skeleton = pd.concat([ + df_sample_means[['属性分级','样点属性分级']], + history_df_sample_means[['属性分级','样点属性分级']] + ]).drop_duplicates().reset_index(drop=True) + + df_final = pd.merge(df_skeleton, df_sample_means, on=['属性分级','样点属性分级'], how='left') + df_final = pd.merge(df_final, history_df_sample_means, on=['属性分级','样点属性分级'], how='left') + df_final.sort_values(['属性分级'], inplace=True) + + # ----d. 按土地利用类型统计均值--- + # 计算三普 + df_samples["YJDL"] = df_samples['TDLYLX'].apply(calculate_yjdl) + df_samples["EJDL"] = df_samples['TDLYLX'].apply(calculate_ejdl) + + dltb_df_samples = df_samples.groupby(['YJDL', 'EJDL'])[field_name].agg(['mean','median','min','max','count']).reset_index() + dltb_df_samples.rename(columns={'mean':'样点均值', 'median':'样点中位值', 'min':'样点最小值', 'max':'样点最大值', 'count':'样点数'}, inplace=True) + + # 计算历史样点 + history_df_samples["YJDL"] = history_df_samples['TDLYLX'].apply(calculate_yjdl) + history_df_samples["EJDL"] = history_df_samples['TDLYLX'].apply(calculate_ejdl) + + hist_dltb_df_samples = history_df_samples.groupby(['YJDL', 'EJDL'])[field_name].agg(['mean','median','min','max','count']).reset_index() + hist_dltb_df_samples.rename(columns={'mean':'历史样点均值', 'median':'历史样点中位值', 'min':'历史样点最小值', 'max':'历史样点最大值', 'count':'历史样点数'}, inplace=True) + + # --- e. 合并地类数据 --- + dltb_skeleton = pd.concat([ + dltb_df_samples[['YJDL', 'EJDL']], + hist_dltb_df_samples[['YJDL', 'EJDL']] + ]).drop_duplicates().reset_index(drop=True) + + dltb_final_df = pd.merge(dltb_skeleton, dltb_df_samples, on=['YJDL', 'EJDL'], how='left') + dltb_final_df = pd.merge(dltb_final_df, hist_dltb_df_samples, on=['YJDL', 'EJDL'], how='left') + + # 按一级、二级地类排序 + in_ejdl_order = ejdl_order + [x for x in dltb_final_df['EJDL'].unique() if x not in ejdl_order] + dltb_final_df['YJDL'] = pd.Categorical(dltb_final_df['YJDL'], categories=yjdl_order, ordered=True) + dltb_final_df['EJDL'] = pd.Categorical(dltb_final_df['EJDL'], categories=in_ejdl_order, ordered=True) + dltb_final_df.sort_values(['YJDL', 'EJDL'], inplace=True) + + print("数据处理流程完成!") + return df_final, processed_df, history_processed_df, dltb_final_df + +# 土壤制图历史变化表格 +def process_data_for_table2(gdb_path,xzqmc,reclassed_features, history_reclassed_features, soil_prop_feature_name,dltb_features, target_areas_dict, sanpu_tif, history_tif): + """ + 【最终版 v2】: 增加对制图样点数的处理,以支持加权平均计算。 + """ + print("开始处理数据...") + + def clean_df(df, columns): + for col in columns: + df[col] = df[col].astype(str).str.strip() + df.replace(['', 'None', '', '<空>'], np.nan, inplace=True) + df.dropna(subset=columns, inplace=True) + return df + + # arcpy.management.CreateFileGDB(os.path.dirname(sanpu_tif), "temp") + # workspace = os.path.join(os.path.dirname(sanpu_tif), "temp.gdb") + # arcpy.env.workspace = workspace + # arcpy.env.overwriteOutput = True + + # target_total_area = df_target_area['面积'].sum().round(0) + # print(f"目标总面积: {target_total_area} 亩") + is_by_xzq = True if xzqmc in ["北海市","来宾市","楚雄自治州"] else False + xzq = os.path.join(gdb_path,"行政区划") + print("k开始制图") + # ==a. 三普重分类数据和地类图斑进行交集制表 --- + temp_feature = f"in_memory/temp_poly_{soil_prop_feature_name}" + out_table = f"in_memory/temp_tabulate_{soil_prop_feature_name}" + arcpy.analysis.Intersect( + in_features=[dltb_features,reclassed_features], + out_feature_class=temp_feature, + join_attributes="ALL", + output_type="INPUT" + ) + # 行政区划和相交结果进行交集制表 + arcpy.analysis.TabulateIntersection( + in_zone_features=xzq, # 乡镇边界 + zone_fields="XZQMC", + in_class_features=temp_feature, + out_table=out_table, + class_fields="gridcode;YJDL;EJDL", + out_units="SQUARE_METERS" + ) + df_sample = pd.DataFrame(arcpy.da.TableToNumPyArray(out_table, ['GRIDCODE','XZQMC', 'YJDL', 'EJDL','AREA'])) + df_sample['temp_area'] = df_sample['AREA']*0.0015 + # 只需要YJDL是耕地的 + df_sample = df_sample[df_sample['YJDL'] == '耕地'] + df_samples = df_sample.groupby(["XZQMC","YJDL", "GRIDCODE"]).agg({"temp_area": "sum"}).reset_index() + # print(df_map_data) + + try: + if is_by_xzq: + print("目标为北海市,同时对区县和地类平差") + df_samples['adjusted_area'] = df_samples['temp_area'] + df_samples['adjustment_factor'] = 1.0 + + # 获取所有存在的行政区和地类 + existing_districts = df_samples['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_samples.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: # type: ignore + adjustment_factor = target_area / original_totals[(xzqmc, yjdl)] + + # 应用平差系数 + mask = (df_samples['XZQMC'] == xzqmc) & (df_samples['YJDL'] == yjdl) + df_samples.loc[mask, 'temp_area'] = df_samples.loc[mask, 'temp_area'] * adjustment_factor + df_samples.loc[mask, 'adjustment_factor'] = adjustment_factor + + # print(f"{xzqmc} - 地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}") + else: + # 用df_target_area按YJDL进行平差计算 + original_totals = df_samples.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_samples['YJDL'] == yjdl + df_samples.loc[mask, 'temp_area'] = df_samples.loc[mask, 'temp_area'] * adjustment_factor + df_samples.loc[mask, 'adjustment_factor'] = adjustment_factor + + # print(f"地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}") + except Exception as e: + print(f"平差处理失败: {e}") + + df_samples['面积_亩'] = df_samples['temp_area'] + if soil_prop_feature_name == 'PH': + df_samples['属性分级'] = df_samples['GRIDCODE'].apply(get_prop_level_for_pH) + else: + df_samples['属性分级'] = df_samples['GRIDCODE'].apply(get_prop_level) + df_sample_means = df_samples.groupby(['属性分级','GRIDCODE'])['面积_亩'].sum().reset_index(name='三普面积') + + df_sample_means['三普平差后面积'] = df_sample_means['三普面积'] + # target_total_area = round(sum(target_areas_dict.get(xzqmc,'').values()),0) + + #corrected_areas = correct_rounding_error(target_total_area,df_sample_means['三普平差后面积'].tolist(),df_sample_means['三普面积'].tolist()) + # df_sample_means['三普最终面积'] = corrected_areas + df_sample_means['三普最终面积'] = df_sample_means['三普平差后面积'] + df_sample_means['三普面积占比'] = fix_percentages(df_sample_means['三普最终面积'].values , df_sample_means['三普最终面积'].sum()) + print("样点数计算完成。") + + + # ==b. 处理历史制图数据,计算“样点均值” + temp_his_feature = f"in_memory/temp_hist_{soil_prop_feature_name}" + hist_out_table = f"in_memory/temp_hist_tabulate_{soil_prop_feature_name}" + arcpy.analysis.Intersect( + in_features=[dltb_features,history_reclassed_features], + out_feature_class=temp_his_feature, + join_attributes="ALL", + output_type="INPUT" + ) + # 行政区划和相交结果进行交集制表 + arcpy.analysis.TabulateIntersection( + in_zone_features=xzq, # 乡镇边界 + zone_fields="XZQMC", + in_class_features=temp_his_feature, + out_table=hist_out_table, + class_fields="gridcode;YJDL;EJDL", + out_units="SQUARE_METERS" + ) + history_df_sample = pd.DataFrame(arcpy.da.TableToNumPyArray(hist_out_table, ['GRIDCODE','XZQMC', 'YJDL', 'EJDL','AREA'])) + history_df_sample['temp_area'] = history_df_sample['AREA']*0.0015 + # 只需要YJDL是耕地的 + history_df_sample = history_df_sample[history_df_sample['YJDL'] == '耕地'] + history_df_samples = history_df_sample.groupby(["XZQMC","YJDL", "GRIDCODE"]).agg({"temp_area": "sum"}).reset_index() + + # print(df_map_data) + try: + if is_by_xzq: + history_df_samples['adjusted_area'] = history_df_samples['temp_area'] + history_df_samples['adjustment_factor'] = 1.0 + + # 获取所有存在的行政区和地类 + existing_districts = history_df_samples['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 = history_df_samples.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: # type: ignore + adjustment_factor = target_area / original_totals[(xzqmc, yjdl)] + + # 应用平差系数 + mask = (history_df_samples['XZQMC'] == xzqmc) & (history_df_samples['YJDL'] == yjdl) + history_df_samples.loc[mask, 'temp_area'] = history_df_samples.loc[mask, 'temp_area'] * adjustment_factor + history_df_samples.loc[mask, 'adjustment_factor'] = adjustment_factor + + # print(f"{xzqmc} - 地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}") + else: + # 用df_target_area按YJDL进行平差计算 + original_totals = history_df_samples.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 = history_df_samples['YJDL'] == yjdl + history_df_samples.loc[mask, 'temp_area'] = history_df_samples.loc[mask, 'temp_area'] * adjustment_factor + history_df_samples.loc[mask, 'adjustment_factor'] = adjustment_factor + + # print(f"地类 {yjdl}: 平差系数 = {adjustment_factor:.6f}") + except Exception as e: + print(f"平差处理失败: {e}") + + history_df_samples['面积_亩'] = history_df_samples['temp_area'] + if soil_prop_feature_name == 'PH': + history_df_samples['属性分级'] = history_df_samples['GRIDCODE'].apply(get_prop_level_for_pH) + else: + history_df_samples['属性分级'] = history_df_samples['GRIDCODE'].apply(get_prop_level) + history_df_sample_means = history_df_samples.groupby(['属性分级','GRIDCODE'])['面积_亩'].sum().reset_index(name='历史面积') + history_df_sample_means['历史平差后面积'] = history_df_sample_means['历史面积'] + + #hist_corrected_areas = correct_rounding_error(target_total_area,history_df_sample_means['历史平差后面积'].tolist(),history_df_sample_means['历史面积'].tolist()) + history_df_sample_means['历史最终面积'] = history_df_sample_means['历史平差后面积'] + history_df_sample_means['历史面积占比'] = fix_percentages(history_df_sample_means['历史最终面积'].values, history_df_sample_means['历史最终面积'].sum()) + + # --- c. 合并数据 --- + print("--> 步骤3: 合并数据...") + df_skeleton = pd.concat([ + df_sample_means[['属性分级','GRIDCODE']], + history_df_sample_means[['属性分级','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, history_df_sample_means, on=['属性分级','GRIDCODE'], how='left') + # print(df_final) + df_final.sort_values(['属性分级'], inplace=True) + print("制图面积计算完成。") + + # # 全域均值、中位值、范围计算 + # out_table_stats = f"in_memory/temp_zonal_stats_{soil_prop_feature_name}" + # out_table_stats_hist = f"in_memory/temp_hist_zonal_stats_{soil_prop_feature_name}" + # print("--> 步骤4: 计算全域统计值...") + # arcpy.management.CalculateField(dltb_features, "ZONE_NAME", "'全域'", "PYTHON3") + # print("--> 计算地类图斑字段完成...") + # arcpy.sa.ZonalStatisticsAsTable(dltb_features, "ZONE_NAME", sanpu_tif, out_table_stats,"DATA", "ALL") + # arcpy.sa.ZonalStatisticsAsTable(dltb_features, "ZONE_NAME", history_tif, out_table_stats_hist,"DATA", "ALL") + # print("全域统计计算完成。") + # df_zonal_stats = pd.DataFrame(arcpy.da.TableToNumPyArray(out_table_stats, ["ZONE_NAME", "MEAN", "MEDIAN", "MIN", "MAX"])) + # df_zonal_stats_hist = pd.DataFrame(arcpy.da.TableToNumPyArray(out_table_stats_hist, ["ZONE_NAME", "MEAN", "MEDIAN", "MIN", "MAX"])) + + # 按一级地类、二级地类进行以表格显示分区统计ZonalStatisticsAsTable + dltb_out_table_stats = f"in_memory/temp_dltb_zonal_stats_{soil_prop_feature_name}" + dltb_out_table_stats_hist = f"in_memory/temp_hist_dltb_zonal_stats_{soil_prop_feature_name}" + arcpy.sa.ZonalStatisticsAsTable(dltb_features, "YJDL_EJDL", sanpu_tif, dltb_out_table_stats,"DATA", "ALL") + arcpy.sa.ZonalStatisticsAsTable(dltb_features, "YJDL_EJDL", history_tif, dltb_out_table_stats_hist,"DATA", "ALL") + print("地类分区统计计算完成。") + # 读取分区统计并包含 COUNT 字段(用于加权计算) + df_dltb_zonal_stats = pd.DataFrame(arcpy.da.TableToNumPyArray(dltb_out_table_stats, ["YJDL_EJDL", "MEAN", "MEDIAN", "MIN", "MAX", "COUNT"])) + df_dltb_zonal_stats_hist = pd.DataFrame(arcpy.da.TableToNumPyArray(dltb_out_table_stats_hist, ["YJDL_EJDL", "MEAN", "MEDIAN", "MIN", "MAX", "COUNT"])) + # 分离 YJDL 和 EJDL 列 + df_dltb_zonal_stats[['YJDL', 'EJDL']] = df_dltb_zonal_stats['YJDL_EJDL'].str.split('_', expand=True) + df_dltb_zonal_stats_hist[['YJDL', 'EJDL']] = df_dltb_zonal_stats_hist['YJDL_EJDL'].str.split('_', expand=True) + df_dltb_zonal_stats.drop(columns=['YJDL_EJDL'], inplace=True) + df_dltb_zonal_stats_hist.drop(columns=['YJDL_EJDL'], inplace=True) + # 重命名列以便后续表格生成使用(制图前缀与历史制图前缀) + df_dltb_zonal_stats.rename(columns={ + 'MEAN':'制图均值', 'MEDIAN':'制图中位值', 'MIN':'制图最小值', 'MAX':'制图最大值', 'COUNT':'制图像元数' + }, inplace=True) + df_dltb_zonal_stats_hist.rename(columns={ + 'MEAN':'历史制图均值', 'MEDIAN':'历史制图中位值', 'MIN':'历史制图最小值', 'MAX':'历史制图最大值', 'COUNT':'历史制图像元数' + }, inplace=True) + + # 合并骨架(保证存在于任一表中的 YJDL/EJDL 组合都会出现在最终表中) + dltb_skeleton = pd.concat([ + df_dltb_zonal_stats[['YJDL', 'EJDL']], + df_dltb_zonal_stats_hist[['YJDL', 'EJDL']] + ]).drop_duplicates().reset_index(drop=True) + + dltb_final_df = pd.merge(dltb_skeleton, df_dltb_zonal_stats, on=['YJDL', 'EJDL'], how='left') + dltb_final_df = pd.merge(dltb_final_df, df_dltb_zonal_stats_hist, on=['YJDL', 'EJDL'], how='left') + + # 按一级、二级地类排序(保持与其他函数一致的顺序) + in_ejdl_order = ejdl_order + [x for x in dltb_final_df['EJDL'].unique() if x not in ejdl_order] + dltb_final_df['YJDL'] = pd.Categorical(dltb_final_df['YJDL'], categories=yjdl_order, ordered=True) + dltb_final_df['EJDL'] = pd.Categorical(dltb_final_df['EJDL'], categories=in_ejdl_order, ordered=True) + dltb_final_df.sort_values(['YJDL', 'EJDL'], inplace=True) + + # --- 基于分区统计计算全域统计(加权均值、加权中位数近似、全域最小/最大) --- + def weighted_mean(values, weights): + vals = np.array(values, dtype=float) + w = np.array(weights, dtype=float) + mask = ~np.isnan(vals) & ~np.isnan(w) + if mask.sum() == 0 or w[mask].sum() == 0: + return np.nan + return (vals[mask] * w[mask]).sum() / w[mask].sum() + + def weighted_median(values, weights): + vals = np.array(values, dtype=float) + w = np.array(weights, dtype=float) + mask = ~np.isnan(vals) & ~np.isnan(w) & (w > 0) + if mask.sum() == 0: + return np.nan + vals = vals[mask] + w = w[mask] + order = np.argsort(vals) + vals = vals[order] + w = w[order] + cumw = np.cumsum(w) + half = w.sum() / 2.0 + idx = np.searchsorted(cumw, half) + idx = min(max(idx, 0), len(vals)-1) + return float(vals[idx]) + + # 当前制图全域统计(使用制图像元数进行加权) + if '制图像元数' in dltb_final_df.columns: + curr_mean = weighted_mean(dltb_final_df['制图均值'], dltb_final_df['制图像元数']) + curr_median = weighted_median(dltb_final_df['制图中位值'], dltb_final_df['制图像元数']) + else: + curr_mean = dltb_final_df['制图均值'].mean() + curr_median = dltb_final_df['制图中位值'].median() + curr_min = dltb_final_df['制图最小值'].min() + curr_max = dltb_final_df['制图最大值'].max() + + dltb_agg = pd.DataFrame([{ + 'ZONE_NAME':'全域', + 'MEAN': curr_mean, + 'MEDIAN': curr_median, + 'MIN': curr_min, + 'MAX': curr_max + }]) + + # 历史制图全域统计(使用历史制图像元数进行加权) + if '历史制图像元数' in dltb_final_df.columns: + hist_mean = weighted_mean(dltb_final_df['历史制图均值'], dltb_final_df['历史制图像元数']) + hist_median = weighted_median(dltb_final_df['历史制图中位值'], dltb_final_df['历史制图像元数']) + else: + hist_mean = dltb_final_df['历史制图均值'].mean() + hist_median = dltb_final_df['历史制图中位值'].median() + hist_min = dltb_final_df['历史制图最小值'].min() + hist_max = dltb_final_df['历史制图最大值'].max() + + dltb_agg_hist = pd.DataFrame([{ + 'ZONE_NAME':'全域', + 'MEAN': hist_mean, + 'MEDIAN': hist_median, + 'MIN': hist_min, + 'MAX': hist_max + }]) + + print("数据处理流程完成。") + # 返回顺序:制图分区分级面积表、全域栅格统计(三普)、全域栅格统计(历史)、制图分区均值表 + return df_final, dltb_agg, dltb_agg_hist, dltb_final_df + + +# --- 3. Excel 制表 样点--- +def write_to_excel_table1(wb, df:pd.DataFrame, sanpu_df, history_df, output_path, prop_config, soil_prop_name): + """ + 【最终修正版】: 将处理好的数据写入格式化的 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}'...") + ws = wb.create_sheet("历史变化(样点)", 0) + + # 获取属性单位 + 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:F1'); ws['A1'] = f'{split_name}分级历史变化(样点统计)' + ws.merge_cells('A2:B2'); ws['A2'] = '土壤三普分级' + ws.merge_cells('C2:D2'); ws['C2'] = '测土配方施肥' + ws.merge_cells('E2:F2'); ws['E2'] = '土壤三普' + + ws['A3'] = '分级'; ws['B3'] = f'值域/{prop_unit}' if prop_unit.strip() else '值域' + ws['C3'] = '数量/个'; ws['D3'] = '占比%' + ws['E3'] = '数量/个'; ws['F3'] = '占比%' + + 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 = 4 + + 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['样点属性分级'], '-') + + # --- 填充单元格的逻辑开始 --- + 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. 填充总计行 + # 3. 合计单元格填充 + ws.merge_cells(f'A{current_row}:B{current_row}') + ws.merge_cells(f'C{current_row}:D{current_row}') + ws.merge_cells(f'E{current_row}:F{current_row}') + ws.cell(row=current_row, column=1).value = f'全县均值/{prop_unit}' if prop_unit.strip() else '全县均值' + ws.cell(row=current_row, column=3).value = f'{history_df[soil_prop_name].mean():.{prop_name}}' + ws.cell(row=current_row, column=5).value = f'{sanpu_df[soil_prop_name].mean():.{prop_name}}' + + ws.merge_cells(f'A{current_row + 1}:B{current_row + 1}') + ws.merge_cells(f'C{current_row+1}:D{current_row+1}') + ws.merge_cells(f'E{current_row+1}:F{current_row+1}') + ws.cell(row=current_row + 1, column=1).value = f'全县中位值/{prop_unit}' if prop_unit.strip() else '全县中位值' + ws.cell(row=current_row + 1, column=3).value = f'{history_df[soil_prop_name].median():.{prop_name}}' + ws.cell(row=current_row + 1, column=5).value = f'{sanpu_df[soil_prop_name].median():.{prop_name}}' + + ws.merge_cells(f'A{current_row + 2}:B{current_row + 2}') + ws.merge_cells(f'C{current_row+2}:D{current_row+2}') + ws.merge_cells(f'E{current_row+2}:F{current_row+2}') + ws.cell(row=current_row + 2, column=1).value = f'全县范围/{prop_unit}' if prop_unit.strip() else '全县范围' + ws.cell(row=current_row + 2, column=3).value = f'{history_df[soil_prop_name].min():.{prop_name}} ~ {history_df[soil_prop_name].max():.{prop_name}}' + ws.cell(row=current_row + 2, column=5).value = f'{sanpu_df[soil_prop_name].min():.{prop_name}} ~ {sanpu_df[soil_prop_name].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+2}') + ExcelStyleUtils.set_style(ws, f'A1:F3', header_font) + + print("正在自动调整列宽...") + + # 自动调整列宽 + ExcelStyleUtils.auto_adjust_column_width(ws) + + # --- e. 保存文件 --- + # wb.save(output_path) + print("Excel 报告生成成功!") + +# --- 4. Excel 制表 制图--- +def write_to_excel_table2(wb,zt_df, sp_qy_df, hist_qy_df, output_path, prop_config, soil_prop_name): + """ + 【最终修正版】: 将处理好的数据写入格式化的 Excel 文件。 + """ + if zt_df.empty or sp_qy_df.empty or hist_qy_df.empty: + print("警告: 没有数据可以写入 Excel,将创建一个空的报告。") + return + + print(f"开始生成 Excel 报告到 '{output_path}'...") + ws = wb.create_sheet("历史变化(制图)", 1) + + # 获取属性单位 + 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:F1'); ws['A1'] = f'{split_name}分级历史变化(制图统计)' + ws.merge_cells('A2:B2'); ws['A2'] = '土壤三普分级' + ws.merge_cells('C2:D2'); ws['C2'] = '测土配方施肥' + ws.merge_cells('E2:F2'); ws['E2'] = '土壤三普' + + ws['A3'] = '分级'; ws['B3'] = f'值域/{prop_unit}' if prop_unit.strip() else '值域' + ws['C3'] = '面积/亩'; ws['D3'] = '占比%' + ws['E3'] = '面积/亩'; ws['F3'] = '占比%' + + 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 = 4 + + df_to_write = zt_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. 填充总计行 + # 3. 合计单元格填充 + ws.merge_cells(f'A{current_row}:B{current_row}') + ws.merge_cells(f'C{current_row}:D{current_row}') + ws.merge_cells(f'E{current_row}:F{current_row}') + ws.cell(row=current_row, column=1).value = f'全县均值/{prop_unit}' if prop_unit.strip() else '全县均值' + ws.cell(row=current_row, column=3).value = f'{hist_qy_df["MEAN"].values[0]:.{prop_name}}' + ws.cell(row=current_row, column=5).value = f'{sp_qy_df["MEAN"].values[0]:.{prop_name}}' + + ws.merge_cells(f'A{current_row + 1}:B{current_row + 1}') + ws.merge_cells(f'C{current_row+1}:D{current_row+1}') + ws.merge_cells(f'E{current_row+1}:F{current_row+1}') + ws.cell(row=current_row + 1, column=1).value = f'全县中位值/{prop_unit}' if prop_unit.strip() else '全县中位值' + ws.cell(row=current_row + 1, column=3).value = f'{hist_qy_df["MEDIAN"].values[0]:.{prop_name}}' + ws.cell(row=current_row + 1, column=5).value = f'{sp_qy_df["MEDIAN"].values[0]:.{prop_name}}' + + ws.merge_cells(f'A{current_row + 2}:B{current_row + 2}') + ws.merge_cells(f'C{current_row+2}:D{current_row+2}') + ws.merge_cells(f'E{current_row+2}:F{current_row+2}') + ws.cell(row=current_row + 2, column=1).value = f'全县范围/{prop_unit}' if prop_unit.strip() else '全县范围' + ws.cell(row=current_row + 2, column=3).value = f'{hist_qy_df["MIN"].values[0]:.{prop_name}} ~ {hist_qy_df["MAX"].values[0]:.{prop_name}}' + ws.cell(row=current_row + 2, column=5).value = f'{sp_qy_df["MIN"].values[0]:.{prop_name}} ~ {sp_qy_df["MAX"].values[0]:.{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+2}') + ExcelStyleUtils.set_style(ws, f'A1:F3', header_font) + + print("正在自动调整列宽...") + + # 自动调整列宽 + ExcelStyleUtils.auto_adjust_column_width(ws) + + # --- e. 保存文件 --- + # wb.save(output_path) + print("Excel 报告生成成功!") + +# --- 4. Excel 制表 样点均值--- +def write_to_excel_table3(wb, df, output_path, prop_config, soil_prop_name): + """ + 将处理好的数据写入格式化的 Excel 文件。 + """ + if df.empty: + print("警告: 没有数据可以写入 Excel。") + return + + print(f"开始生成 Excel 报告到 '{output_path}'...") + ws = wb.create_sheet("不同土地利用类型(样点统计)", 2) + + # 获取属性名 + 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 = prop_config.get('分级标准', '').split('\n')[1].strip() if prop_config.get('分级标准', '') else '' + + # --- b. 绘制表头 --- + ws.merge_cells('A1:I1'); ws['A1'] = f"不同土地利用类型土壤{split_name}历史变化(样点统计)" + ws.merge_cells('A2:B2'); ws['A2'] = '土地利用类型' + ws.merge_cells('C2:E2'); ws['C2'] = '测土配方施肥' + ws.merge_cells('F2:H2'); ws['F2'] = '土壤三普' + ws.merge_cells('I2:I3'); ws['I2'] = '变幅/%' + + jz = f'均值/{prop_unit}' if prop_unit else '均值' + fw = f'范围/{prop_unit}' if prop_unit else '范围' + + ws['A3'] = '一级' + ws['B3'] = '二级' + ws['C3'] = jz + ws['D3'] = fw + ws['E3'] = '数量/个' + ws['F3'] = jz + ws['G3'] = fw + ws['H3'] = '数量/个' + + # --- c. 填充数据 --- + current_row = 4 + + df_to_write = df.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 + + + if group_yl_df.empty: + 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 + 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 = '-' + ws.cell(row=current_row, column=7).value = '-' + ws.cell(row=current_row, column=8).value = '-' + ws.cell(row=current_row, column=9).value = '-' + + current_row += 1 + continue + + # 遍历该一级地类下的所有“二级地类” + for _, row_data in group_yl_df.iterrows(): + ws.cell(row=current_row, column=2).value = row_data['EJDL'] + print(f" 写入二级地类: {row_data['EJDL']}...") + + # 填充历史样点数据 + sample_mean = row_data.get('历史样点均值') + sample_min = row_data.get('历史样点最小值') + sample_max = row_data.get('历史样点最大值') + sample_count = row_data.get('历史样点数') + if pd.notna(sample_mean) and sample_count > 0: + ws.cell(row=current_row, column=3).value = f"{sample_mean:.{prop_name}}" + ws.cell(row=current_row, column=4).value = f"{sample_min:.{prop_name}}~{sample_max:.{prop_name}}" + ws.cell(row=current_row, column=5).value = f"{sample_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 = "-" + + # 填充三普样点数据 + map_mean = row_data.get('样点均值') + map_min = row_data.get('样点最小值') + map_max = row_data.get('样点最大值') + map_count = row_data.get('样点数') + if pd.notna(map_mean) and map_count > 0: + ws.cell(row=current_row, column=6).value = f"{map_mean:.{prop_name}}" + ws.cell(row=current_row, column=7).value = f"{map_min:.{prop_name}}~{map_max:.{prop_name}}" + ws.cell(row=current_row, column=8).value = f"{map_count:.0f}" + else: + ws.cell(row=current_row, column=6).value = "-" + ws.cell(row=current_row, column=7).value = "-" + ws.cell(row=current_row, column=8).value = "-" + + # 填充变幅 + if pd.notna(sample_mean) and pd.notna(map_mean) and sample_mean != 0: + change_range = ((map_mean - sample_mean) / sample_mean) * 100 + ws.cell(row=current_row, column=9).value = f"{change_range:.1f}" + else: + ws.cell(row=current_row, column=9).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 = '合计' + + # 计算合计行的历史样点均值 (加权平均) + # a. 加权平均计算“样点均值” + weighted_sum = (group_yl_df['历史样点均值'] * group_yl_df['历史样点数']).sum() + total_count = group_yl_df['历史样点数'].sum() + total_sample_mean = weighted_sum / total_count if total_count > 0 else 0 + total_min = group_yl_df['历史样点最小值'].min() + total_max = group_yl_df['历史样点最大值'].max() + + if pd.notna(total_sample_mean) and total_count > 0: + ws.cell(row=current_row, column=3).value = f"{total_sample_mean:.{prop_name}}" + ws.cell(row=current_row, column=4).value = f"{total_min:.{prop_name}}~{total_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 = "-" + + # 计算合计行的三普样点均值 (加权平均) + weighted_sum = (group_yl_df['样点均值'] * group_yl_df['样点数']).sum() + total_count = group_yl_df['样点数'].sum() + weighted_avg = weighted_sum / total_count if total_count > 0 else 0 + total_min = group_yl_df['样点最小值'].min() + total_max = group_yl_df['样点最大值'].max() + if pd.notna(weighted_avg) and weighted_avg != 0 and total_sample_mean>0: + change_range = ((weighted_avg - total_sample_mean) / total_sample_mean) * 100 + ws.cell(row=current_row, column=9).value = f"{change_range:.1f}" + else: + ws.cell(row=current_row, column=9).value = f"-" + + if total_count > 0: + ws.cell(row=current_row, column=6).value = f"{weighted_avg:.{prop_name}}" + ws.cell(row=current_row, column=7).value = f"{total_min:.{prop_name}}~{total_max:.{prop_name}}" + ws.cell(row=current_row, column=8).value = f"{total_count:.0f}" + else: + ws.cell(row=current_row, column=6).value = "-" + 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 + + # 计算并写入“全县合计”行 + ws.merge_cells(f'A{current_row}:B{current_row}') + ws.cell(row=current_row, column=1).value = '全县' + + # 历史样点均值 (加权平均) + weighted_sum = (df_to_write['历史样点均值'] * df_to_write['历史样点数']).sum() + total_count = df_to_write['历史样点数'].sum() + total_sample_mean = weighted_sum / total_count if total_count > 0 else 0 + total_min = df_to_write['历史样点最小值'].min() + total_max = df_to_write['历史样点最大值'].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_min:.{prop_name}}~{total_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 = "-" + # 三普样点均值 (加权平均) + weighted_sum = (df_to_write['样点均值'] * df_to_write['样点数']).sum() + total_count = df_to_write['样点数'].sum() + weighted_avg = weighted_sum / total_count if total_count > 0 else 0 + total_min = df_to_write['样点最小值'].min() + total_max = df_to_write['样点最大值'].max() + if total_count > 0: + ws.cell(row=current_row, column=6).value = f"{weighted_avg:.{prop_name}}" + ws.cell(row=current_row, column=7).value = f"{total_min:.{prop_name}}~{total_max:.{prop_name}}" + ws.cell(row=current_row, column=8).value = f"{total_count:.0f}" + else: + ws.cell(row=current_row, column=6).value = "-" + ws.cell(row=current_row, column=7).value = "-" + ws.cell(row=current_row, column=8).value = "-" + # 变幅计算 + if pd.notna(total_sample_mean) and total_sample_mean != 0: + change_range = ((weighted_avg - total_sample_mean) / total_sample_mean) * 100 + ws.cell(row=current_row, column=9).value = f"{change_range:.1f}" + + # --- 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 报告生成成功!") + +# --- 4. Excel 制表 制图均值 --- +def write_to_excel_table4(wb, df, df_all, df_all_hist, output_path, prop_config, soil_prop_name): + """ + 将处理好的数据写入格式化的 Excel 文件。 + """ + if df.empty: + print("警告: 没有数据可以写入 Excel。") + return + + print(f"开始生成 Excel 报告到 '{output_path}'...") + ws = wb.create_sheet("不同土地利用类型(制图统计)", 3) + + # 获取属性名 + 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 = prop_config.get('分级标准', '').split('\n')[1].strip() if prop_config.get('分级标准', '') else '' + + jz = f'均值/{prop_unit}' if prop_unit else '均值' + fw = f'范围/{prop_unit}' if prop_unit else '范围' + + # --- b. 绘制表头 --- + ws.merge_cells('A1:E1'); ws['A1'] = f"不同土地利用类型土壤{split_name}历史变化(制图统计)" + ws.merge_cells('A2:B2'); ws['A2'] = '土地利用类型' + ws.merge_cells('C2:D2'); ws['C2'] = jz + ws.merge_cells('E2:E3'); ws['E2'] = '变幅/%' + + ws['A3'] = '一级' + ws['B3'] = '二级' + ws['C3'] = '测土配方施肥' + ws['D3'] = '土壤三普' + + # --- c. 填充数据 --- + current_row = 4 + + df_to_write = df.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 + + if group_yl_df.empty: + 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 + ws.cell(row=current_row, column=3).value = '-' + ws.cell(row=current_row, column=4).value = '-' + ws.cell(row=current_row, column=5).value = '-' + + current_row += 1 + continue + + # 遍历该一级地类下的所有“二级地类” + for _, row_data in group_yl_df.iterrows(): + ws.cell(row=current_row, column=2).value = row_data['EJDL'] + print(f" 写入二级地类: {row_data['EJDL']}...") + + # 填充历史样点数据 + sample_mean = row_data.get('历史制图均值') + if pd.notna(sample_mean): + ws.cell(row=current_row, column=3).value = f"{sample_mean:.{prop_name}}" + else: + 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:.{prop_name}}" + else: + ws.cell(row=current_row, column=4).value = "-" + + # 填充变幅 + if pd.notna(sample_mean) and pd.notna(map_mean) and sample_mean != 0: + change_range = ((map_mean - sample_mean) / sample_mean) * 100 + ws.cell(row=current_row, column=5).value = f"{change_range:.1f}" + else: + ws.cell(row=current_row, column=5).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 = '合计' + + # 计算合计行的历史制图均值 (加权平均) + # a. 加权平均计算“制图均值” + weighted_sum = (group_yl_df['历史制图均值'] * group_yl_df['历史制图像元数']).sum() + total_count = group_yl_df['历史制图像元数'].sum() + total_sample_mean = weighted_sum / total_count if total_count > 0 else 0 + + if pd.notna(total_sample_mean) and total_count > 0: + ws.cell(row=current_row, column=3).value = f"{total_sample_mean:.{prop_name}}" + else: + ws.cell(row=current_row, column=3).value = "-" + + # 计算合计行的三普样点均值 (加权平均) + weighted_sum = (group_yl_df['制图均值'] * group_yl_df['制图像元数']).sum() + total_count = group_yl_df['制图像元数'].sum() + weighted_avg = weighted_sum / total_count if total_count > 0 else 0 + + if total_count > 0: + ws.cell(row=current_row, column=4).value = f"{weighted_avg:.{prop_name}}" + else: + ws.cell(row=current_row, column=4).value = "-" + + # 写入变幅 + if pd.notna(weighted_avg) and weighted_avg != 0: + change_range = ((weighted_avg - total_sample_mean) / total_sample_mean) * 100 + ws.cell(row=current_row, column=5).value = f"{change_range:.1f}" + + # 合并“一级地类”单元格 + 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 + + # 计算并写入“全县合计”行 + ws.merge_cells(f'A{current_row}:B{current_row}') + ws.cell(row=current_row, column=1).value = '全县' + + # 历史均值 (加权平均) + total_sample_mean = df_all_hist['MEAN'].values[0] + if pd.notna(total_sample_mean): + ws.cell(row=current_row, column=3).value = f"{total_sample_mean:.{prop_name}}" + else: + ws.cell(row=current_row, column=3).value = "-" + # 三普样点均值 (加权平均) + weighted_avg = df_all["MEAN"].values[0] + if total_count > 0: + ws.cell(row=current_row, column=4).value = f"{weighted_avg:.{prop_name}}" + else: + ws.cell(row=current_row, column=4).value = "-" + # 变幅计算 + if pd.notna(total_sample_mean) and total_sample_mean != 0: + change_range = ((weighted_avg - total_sample_mean) / total_sample_mean) * 100 + ws.cell(row=current_row, column=5).value = f"{change_range:.1f}" + + # --- 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(xzqmc,gdb_path,history_samples_path,reclassed_features,history_reclassed_features, soil_prop_name, dltb_features, sanpu_raster, history_raster, output_path, target_area_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("开始处理数据...") + wb= Workbook() + # 生成表1 土壤属性分级分布 的统计Excel报告 + final_dataframe,sanpu_df,history_df, dltb_df = process_data_for_table1(gdb_path,history_samples_path, soil_prop_name, prop_config) + write_to_excel_table1(wb,final_dataframe,sanpu_df,history_df, output_excel_path, prop_config, soil_prop_name) + write_to_excel_table3(wb,dltb_df, output_excel_path, prop_config, soil_prop_name) + print(final_dataframe) + zhitu_df, sanpu_all_df, history_all_df, dltb_zhitu_df = process_data_for_table2(gdb_path,xzqmc,reclassed_features, history_reclassed_features, soil_prop_name,dltb_features, target_area_df, sanpu_raster,history_raster) + write_to_excel_table2(wb,zhitu_df,sanpu_all_df,history_all_df,output_excel_path, prop_config, soil_prop_name) + write_to_excel_table4(wb, dltb_zhitu_df, sanpu_all_df, history_all_df, output_excel_path, prop_config, soil_prop_name) + + + # 保存工作簿 + wb.save(output_excel_path) + + # 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() diff --git a/tools/core/soil_prop_stats/__init__.py b/tools/core/soil_prop_stats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/core/stats_area_to_excel.py b/tools/core/stats_area_to_excel.py new file mode 100644 index 0000000..972ec28 --- /dev/null +++ b/tools/core/stats_area_to_excel.py @@ -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: + """ + print(f"STATUS:{message}") + sys.stdout.flush() # 确保立即输出 + + +def print_result(success, output_path="", error_message=""): + """ + 输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果 + 格式: RESULT:True|| + 格式: RESULT:False|| + """ + 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 "-10~0.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() diff --git a/tools/core/stats_sh_to_excel.py b/tools/core/stats_sh_to_excel.py new file mode 100644 index 0000000..8b1bcdc --- /dev/null +++ b/tools/core/stats_sh_to_excel.py @@ -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: + """ + print(f"STATUS:{message}") + sys.stdout.flush() # 确保立即输出 + +def print_result(success, output_path="", error_message=""): + """ + 输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果 + 格式: RESULT:True|| + 格式: RESULT:False|| + """ + 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() diff --git a/tools/core/stats_soil_prop_to_excel.py b/tools/core/stats_soil_prop_to_excel.py new file mode 100644 index 0000000..438891c --- /dev/null +++ b/tools/core/stats_soil_prop_to_excel.py @@ -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: + """ + print(f"STATUS:{message}") + sys.stdout.flush() # 确保立即输出 + +def print_result(success, output_path="", error_message=""): + """ + 输出最终结果到标准输出,用于 GUI 判断任务状态和获取结果 + 格式: RESULT:True|| + 格式: RESULT:False|| + """ + 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() diff --git a/tools/core/test_script.py b/tools/core/test_script.py new file mode 100644 index 0000000..22eb688 --- /dev/null +++ b/tools/core/test_script.py @@ -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()) \ No newline at end of file diff --git a/tools/core/utils/__init__.py b/tools/core/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/core/utils/arcgis_utils.py b/tools/core/utils/arcgis_utils.py new file mode 100644 index 0000000..685ac76 --- /dev/null +++ b/tools/core/utils/arcgis_utils.py @@ -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 \ No newline at end of file diff --git a/tools/core/utils/common_utils.py b/tools/core/utils/common_utils.py new file mode 100644 index 0000000..c941503 --- /dev/null +++ b/tools/core/utils/common_utils.py @@ -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.00~2.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.00~2.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}'") \ No newline at end of file diff --git a/tools/core/utils/excel_utils.py b/tools/core/utils/excel_utils.py new file mode 100644 index 0000000..953166f --- /dev/null +++ b/tools/core/utils/excel_utils.py @@ -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 # 实际上不是合并单元格 \ No newline at end of file diff --git a/tools/core/utils/math_utils.py b/tools/core/utils/math_utils.py new file mode 100644 index 0000000..026bda6 --- /dev/null +++ b/tools/core/utils/math_utils.py @@ -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)) + + \ No newline at end of file diff --git a/tools/core/utils/os_utils/temp_files_processor.py b/tools/core/utils/os_utils/temp_files_processor.py new file mode 100644 index 0000000..3c806e5 --- /dev/null +++ b/tools/core/utils/os_utils/temp_files_processor.py @@ -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() \ No newline at end of file diff --git a/tools/core/utils/平差工具.py b/tools/core/utils/平差工具.py new file mode 100644 index 0000000..e500297 --- /dev/null +++ b/tools/core/utils/平差工具.py @@ -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 \ No newline at end of file diff --git a/tools/ui/__init__.py b/tools/ui/__init__.py new file mode 100644 index 0000000..b73015a --- /dev/null +++ b/tools/ui/__init__.py @@ -0,0 +1 @@ +# UI界面模块 \ No newline at end of file diff --git a/tools/ui/components/__init__.py b/tools/ui/components/__init__.py new file mode 100644 index 0000000..b73015a --- /dev/null +++ b/tools/ui/components/__init__.py @@ -0,0 +1 @@ +# UI界面模块 \ No newline at end of file diff --git a/tools/ui/components/file_list_group.py b/tools/ui/components/file_list_group.py new file mode 100644 index 0000000..cf65d40 --- /dev/null +++ b/tools/ui/components/file_list_group.py @@ -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() \ No newline at end of file diff --git a/tools/ui/components/input_group.py b/tools/ui/components/input_group.py new file mode 100644 index 0000000..5b69c21 --- /dev/null +++ b/tools/ui/components/input_group.py @@ -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() \ No newline at end of file diff --git a/tools/ui/config/__init__.py b/tools/ui/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/ui/config/settings.json b/tools/ui/config/settings.json new file mode 100644 index 0000000..4ae5feb --- /dev/null +++ b/tools/ui/config/settings.json @@ -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": [] + } +} \ No newline at end of file diff --git a/tools/ui/main_window.py b/tools/ui/main_window.py new file mode 100644 index 0000000..e5ae7be --- /dev/null +++ b/tools/ui/main_window.py @@ -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'{formatted_message}' + elif "警告" in message: + formatted_message = f'{formatted_message}' + elif "成功" in message or "完成" in message: + formatted_message = f'{formatted_message}' + else: + formatted_message = f'{formatted_message}' + + 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()) diff --git a/tools/ui/runners/__init__.py b/tools/ui/runners/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/ui/runners/script_runner.py b/tools/ui/runners/script_runner.py new file mode 100644 index 0000000..4ac73ec --- /dev/null +++ b/tools/ui/runners/script_runner.py @@ -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。") diff --git a/tools/ui/runners/script_runner.txt b/tools/ui/runners/script_runner.txt new file mode 100644 index 0000000..0f964ab --- /dev/null +++ b/tools/ui/runners/script_runner.txt @@ -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 + \ No newline at end of file diff --git a/tools/ui/tabs/__init__.py b/tools/ui/tabs/__init__.py new file mode 100644 index 0000000..71a9b0b --- /dev/null +++ b/tools/ui/tabs/__init__.py @@ -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'] \ No newline at end of file diff --git a/tools/ui/tabs/acid_stat_tab.py b/tools/ui/tabs/acid_stat_tab.py new file mode 100644 index 0000000..5f1ae95 --- /dev/null +++ b/tools/ui/tabs/acid_stat_tab.py @@ -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()) \ No newline at end of file diff --git a/tools/ui/tabs/area_stat_tab.py b/tools/ui/tabs/area_stat_tab.py new file mode 100644 index 0000000..7da5c28 --- /dev/null +++ b/tools/ui/tabs/area_stat_tab.py @@ -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()) \ No newline at end of file diff --git a/tools/ui/tabs/config_editor_dialog.py b/tools/ui/tabs/config_editor_dialog.py new file mode 100644 index 0000000..48e29d1 --- /dev/null +++ b/tools/ui/tabs/config_editor_dialog.py @@ -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()) \ No newline at end of file diff --git a/tools/ui/tabs/export_layout_tab.py b/tools/ui/tabs/export_layout_tab.py new file mode 100644 index 0000000..32613c9 --- /dev/null +++ b/tools/ui/tabs/export_layout_tab.py @@ -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) diff --git a/tools/ui/tabs/export_map_tab.py b/tools/ui/tabs/export_map_tab.py new file mode 100644 index 0000000..9b125aa --- /dev/null +++ b/tools/ui/tabs/export_map_tab.py @@ -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) \ No newline at end of file diff --git a/tools/ui/tabs/raster_processing_common.py b/tools/ui/tabs/raster_processing_common.py new file mode 100644 index 0000000..db2cf8f --- /dev/null +++ b/tools/ui/tabs/raster_processing_common.py @@ -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 \ No newline at end of file diff --git a/tools/ui/tabs/raster_tab.py b/tools/ui/tabs/raster_tab.py new file mode 100644 index 0000000..629e4e8 --- /dev/null +++ b/tools/ui/tabs/raster_tab.py @@ -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()) \ No newline at end of file diff --git a/tools/ui/tabs/soil_prop_stat_tab.py b/tools/ui/tabs/soil_prop_stat_tab.py new file mode 100644 index 0000000..b32b13c --- /dev/null +++ b/tools/ui/tabs/soil_prop_stat_tab.py @@ -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("""各属性样点
地类图斑
土壤类型图
母岩母质
""") + 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()) \ No newline at end of file diff --git a/tools/ui/tabs/test_tab.py b/tools/ui/tabs/test_tab.py new file mode 100644 index 0000000..bf48025 --- /dev/null +++ b/tools/ui/tabs/test_tab.py @@ -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)}") diff --git a/tools/ui/tabs/xlsx_jpg_tab.py b/tools/ui/tabs/xlsx_jpg_tab.py new file mode 100644 index 0000000..c45e6a7 --- /dev/null +++ b/tools/ui/tabs/xlsx_jpg_tab.py @@ -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()) \ No newline at end of file