python和arcgispro的实践(AI辅助编程)

1、多方案融入方案

融合到一个代码中,设置可选项的三种方式

Qwen3-Max

21:27:25

非常好的想法!我们可以将 三种判断内外环的方法 融合到一个 ArcGIS Python Toolbox(.pyt)工具中,并通过 下拉菜单参数 让用户选择使用哪种方法。


✅ 最终目标

  • 一个 .pyt 工具;
  • 用户可选三种内外环识别方式:
    • 按部件顺序(Part Order) ------ 默认,最快;
    • 按有向面积符号(Signed Area)
    • 按空间包含关系(Point-in-Polygon) ------ 最鲁棒;
  • 其他功能不变:简化、编号(J/K)、最西北起始、打断线、合并 BDCDYH。

2、全面添加进度条和关键步骤结果反馈

3、不同的方式可以采用不同的处理方式

全数界址点的方式生成界址线太慢了,能不能考虑两种不同的方式采用不同的界址线生成方式

Qwen3-Max

02:12:13

非常好的问题!确实,在 "全数输出" 模式下,界址点数量极大(每个原始折点都保留),如果仍用 SplitLineAtPoint + 点匹配的方式生成界址线,会非常慢,因为:

  • SplitLineAtPoint 在大量点上分割线效率低;
  • 后续还要逐点匹配 POINT_ID,计算量大。

✅ 优化思路:两种模式,两种界址线生成策略

输出模式 界址点来源 推荐界址线生成方式
全数输出 原始宗地边界的所有折点 直接从宗地多边形提取边界线段
抽稀输出 抽稀后保留的关键点 用 SplitLineAtPoint + 点匹配(必要)

理由

  • 全数模式下,界址点 = 原始边界折点,因此界址线天然就是原始边界的每一段线段;
  • 无需再"分割+匹配",直接遍历多边形环的相邻点对即可高效构建界址线;
  • 抽稀模式下,界址点是简化后的子集,必须通过空间分割才能准确对应线段。

🔧 修改方案(仅修改界址线生成部分)

我们将根据 output_mode 分支处理:

✅ 全数输出:直接从原始多边形提取线段

  • 遍历每个宗地的外环/内环;
  • 对环中每两个相邻点(如 P0→P1, P1→P2, ..., Pn-1→P0)创建线段;
  • 自动分配 START_ID / END_ID(按 J1, J2... 或 K1, K2... 规则);
  • 速度极快,O(n) 线性复杂度。

🌀 抽稀输出:保留原逻辑(SplitLineAtPoint)

4、保留1版阶段性的代码

在新的优化代码测试出来之前,留一个比较好的基础,代码都是迭代出来的,没有谁是一下子做好的。

5、好的分段式代码要保留

最后再融合一下

6、优化代码

即使代码可以运行,也要试着优化一下代码,提高速度和稳定性,这是AI最擅长的

7、代码命名

以固定的名称加1.0、2.0、3.0以此类推,至少保留3代源代码,方便更改,因为你很容易陷入推导重来的困境中,你和AI之间已经陷入了死的循环,这个时候你就需要在阶段性的基础上重新来过,这个时候你没有任何的基础,你就会很绝望。

8、既要又要还要怎么办

非常好的思路!✅

你提出的方案完全可行,且符合地籍数据的双重编号需求

  • 宗地内编号(如 J1, J2...):用于宗地图、草图、界址说明表;
  • 全局流水号(如 P0001, P0002...):用于界址线的 START_ID / END_ID,保证拓扑唯一性。

✅ 实现目标

字段 用途 示例
LOCAL_ID 宗地内编号 J1, J2, K1...
GLOBAL_ID 全局唯一流水号 P0001, P0002...
界址线 START_ID / END_ID 引用 GLOBAL_ID P0001 → P0003

这样:

  • 宗地图按 LOCAL_ID 标注;
  • 界址线拓扑用 GLOBAL_ID 连接,无歧义;
  • 同一坐标点在不同宗地中可有不同 LOCAL_ID,但共享同一个 GLOBAL_ID(物理点唯一)。

9、关于exe文件的制作(重点首便携式绿色版-把重要的功能集成到exe中,避免对外部Python、Poppler的依赖

是的,只要打包正确,生成的 .exe 文件可以拷贝到其他 Windows 电脑上直接运行,无需安装 Python、Poppler 或任何依赖 ------ 这就是 PyInstaller 的核心优势:单文件便携式分发


✅ 前提条件(必须满足)

要确保在 目标电脑(没有 Python 的电脑)上也能运行,你需要:

1. 使用 --onefile 打包

你已经用了:

bash

编辑

1--onefile

✅ 这会把 Python 解释器、所有依赖库、你的代码、Poppler 全部压缩进一个 .exe 文件。


2. Poppler 被正确内嵌

  • 你在打包时用了:

bash

编辑

1--add-data "poppler;poppler"

  • 并且项目目录下确实有完整的 poppler/Library/bin/... 结构

✅ 这样 Poppler 就会被打包进 exe,运行时自动解压到临时目录(如 C:\Users\XXX\AppData\Local\Temp\_MEIxxxxx\poppler),程序通过 resource_path() 正确访问它。


3. 目标电脑是相同架构(通常是 x64)

  • 你用的是 64 位 Windows + 64 位 Python → 生成的 exe 只能在 64 位 Windows 上运行
  • 如果目标电脑是 32 位(极少见),则无法运行

💡 现在绝大多数电脑都是 64 位,所以一般没问题。


4. 目标电脑有基本 VC++ 运行库(通常已有)

Poppler 的 DLL 可能依赖 Microsoft Visual C++ Redistributable。

好消息 :Windows 10/11 默认已包含这些运行库,99% 的新电脑无需额外安装

如果遇到"缺少 VCRUNTIME140.dll"等错误,可让用户安装 Microsoft Visual C++ Redistributable(免费)。


🧪 如何验证是否真的"便携"?

测试方法(强烈建议):

  • 打包完成后,把 dist/PDF2JPG_GUI.exe 拷贝到 另一台没装 Python 的电脑
  • 双击运行
  • 选择 PDF 文件夹 → 转换 → 成功生成 JPG

✅ 如果成功,说明你的 exe 是真正便携的!


⚠️ 常见导致"不能跨电脑运行"的原因

问题 是否影响跨电脑运行 解决方案
Poppler 没打包进去 ❌ 会失败 确保 --add-data "poppler;poppler" 且路径正确
使用了绝对路径(如 C:\mytool\poppler) ❌ 会失败 必须用 resource_path()
目标电脑是 32 位,你是 64 位打包 ❌ 会失败 确保架构一致(现在基本都是 x64)
防病毒软件误删 exe ⚠️ 可能发生 加白名单或提醒用户信任
缺少 VC++ 运行库(老旧系统) ⚠️ 极少数情况 安装 VC++ Redist

✅ 最佳实践建议

  • 在干净的虚拟机中测试 (如 Windows Sandbox)
    • Win + R → 输入 windowsandbox → 回车
    • 把你的 .exe 拖进去运行,看是否正常
  • 不要依赖外部 PATH
    所有路径必须通过 resource_path() 获取
  • 使用最新版 Poppler
    https://github.com/oschwartz10612/poppler-windows 下载 -library-release.zip

✅ 结论

只要你按照正确方式打包(含完整 Poppler + 使用 resource_path),生成的 .exe 就可以在任意 64 位 Windows 10/11 电脑上双击运行,无需安装任何东西。

这就是你想要的"绿色便携版"!🎉

如果需要,我可以提供一个 打包检查清单自动化构建脚本(.bat),帮你避免常见错误。需要吗?

代码

|

Poppler这个是什么

Qwen3-Max

Poppler 是一个开源的 PDF 渲染和处理库,主要用于将 PDF 文件转换为图像、提取文本、获取页面信息等操作。它本身不是 Python 库,而是一个用 C++ 编写的底层工具集,在 Linux、macOS 和 Windows 上广泛使用。


🧩 为什么你的 Python 程序需要 Poppler?

你在用的 Python 库是 pdf2image,它的作用是:

把 PDF 的每一页转成 PIL 图像(如 JPG/PNG)

但 pdf2image 自己不能解析 PDF ,它只是个"中间人",真正干活的是 Poppler 提供的命令行工具 pdftoppm

工作流程:

text

编辑

1你的 Python 脚本

2 ↓

3调用 pdf2image.convert_from_path()

4 ↓

5pdf2image 调用系统中的 pdftoppm.exe(来自 Poppler)

6 ↓

7pdftoppm 读取 PDF → 渲染为像素图 → 输出 PPM/PGM 格式

8 ↓

9pdf2image 用 PIL 将其转为 JPG/PNG

✅ 所以:没有 Poppler,pdf2image 就无法工作!


📦 Poppler 包含哪些关键文件?(Windows 版)

当你下载 poppler-windows 后,核心文件在:

text

编辑

1poppler/

2└── Library/

3 └── bin/

4 ├── pdftoppm.exe ← 最重要的!用于 PDF 转图像

5 ├── pdfinfo.exe ← 获取 PDF 页数、作者等元数据

6 ├── pdftocairo.exe ← 另一种渲染方式(支持 PNG、SVG 等)

7 └── *.dll ← 如 libpoppler-xxx.dll(运行时依赖)

🔸 你的程序必须能访问 pdftoppm.exe,否则报错:"Unable to get page count" 或 "Poppler not found"


🌍 Poppler vs 其他方案

方案 优点 缺点
Poppler + pdf2image 开源、免费、高质量渲染、支持复杂 PDF 需要额外安装 Poppler(但可内嵌)
PyMuPDF (fitz) 单一 Python 包,无需外部依赖,速度快 商业项目需授权(AGPL)
pdf2image + Ghostscript 可替代 Poppler 安装更复杂,Windows 支持弱

✅ 对于你这种"打包成 exe 分发"的需求,Poppler 是最成熟、兼容性最好的选择


❓ 常见问题解答

Q1: 能不能不用 Poppler?

可以,但要换库,比如用 PyMuPDF(也叫 fitz):

python

编辑

1import fitz # pip install PyMuPDF

2

3doc = fitz.open("file.pdf")

4for i, page in enumerate(doc):

5 pix = page.get_pixmap(dpi=150)

6 pix.save(f"page_{i+1}.jpg")

✅ 优点:单文件打包更容易(无外部 exe)

⚠️ 注意:PyMuPDF 是 AGPL 协议,商业闭源项目需谨慎


Q2: 为什么不能直接用 pip install poppler?

因为 Poppler 不是纯 Python 包 ,它是 C++ 编译的二进制程序。

Python 的 pip 只能装 .py 文件或带预编译 wheel 的包,而 Poppler 需要单独提供 .exe 和 .dll。

所以必须手动下载 Poppler for Windows,并告诉 pdf2image 它在哪。


Q3: 内嵌 Poppler 会让 exe 很大吗?

会,但合理:

  • Poppler(bin + dll)≈ 30~50 MB
  • Python 运行时 + 依赖 ≈ 20~30 MB
  • 最终 exe 大小:50~80 MB

✅ 换来的是 真正的便携性 ------ 用户双击即用,值得!


✅ 总结

关键点 说明
Poppler 是什么 开源 PDF 渲染引擎(C++ 编写)
为什么需要它 pdf2image 依赖它的 pdftoppm.exe 来转 PDF 为图
能否省略 不能(除非换用 PyMuPDF 等替代方案)
如何分发 打包进 exe(用 --add-data),用户无需安装
是否合法 是,Poppler 使用 GPLv2/LGPL,允许免费使用

10、ArcGIS Pro 的 Python Command Prompt 默认路径

C:\Program Files\ArcGIS\Pro\bin\Python\Scripts\proenv.bat

原始代码:

python 复制代码
# -*- coding: utf-8 -*-
import arcpy
import os
import math
import uuid


def perpendicular_distance(point, start, end):
    if start.equals(end):
        return math.sqrt((point.X - start.X) ** 2 + (point.Y - start.Y) ** 2)
    abx = end.X - start.X
    aby = end.Y - start.Y
    apx = point.X - start.X
    apy = point.Y - start.Y
    ab2 = abx * abx + aby * aby
    if ab2 == 0:
        return math.sqrt(apx * apx + apy * apy)
    t = (apx * abx + apy * aby) / ab2
    t = max(0, min(1, t))
    proj_x = start.X + t * abx
    proj_y = start.Y + t * aby
    dx = point.X - proj_x
    dy = point.Y - proj_y
    return math.sqrt(dx * dx + dy * dy)


def douglas_peucker_keep(points, tolerance, keep_set):
    if len(points) <= 2:
        return points[:]

    def point_key(pt):
        return (round(pt.X, 9), round(pt.Y, 9))

    has_keep = any(point_key(p) in keep_set for p in points[1:-1])
    if not has_keep:
        max_dist = 0
        index = -1
        for i in range(1, len(points) - 1):
            dist = perpendicular_distance(points[i], points[0], points[-1])
            if dist > max_dist:
                max_dist = dist
                index = i
        if max_dist > tolerance:
            left = douglas_peucker_keep(points[:index+1], tolerance, keep_set)
            right = douglas_peucker_keep(points[index:], tolerance, keep_set)
            return left[:-1] + right
        else:
            return [points[0], points[-1]]

    result = [points[0]]
    i = 0
    n = len(points)

    while i < n - 1:
        j = i + 1
        next_keep = None
        while j < n:
            if point_key(points[j]) in keep_set or j == n - 1:
                next_keep = j
                break
            j += 1
        if next_keep is None:
            next_keep = n - 1

        segment = points[i:next_keep+1]
        if len(segment) <= 2:
            if len(segment) == 2 and not segment[1].equals(result[-1]):
                result.append(segment[1])
        else:
            simplified_seg = douglas_peucker_keep(segment, tolerance, keep_set)
            if simplified_seg and not simplified_seg[0].equals(result[-1]):
                result.extend(simplified_seg)
            elif simplified_seg:
                result.extend(simplified_seg[1:])
        i = next_keep

    cleaned = [result[0]]
    for pt in result[1:]:
        if not pt.equals(cleaned[-1]):
            cleaned.append(pt)
    return cleaned


def extract_rings_from_part(part):
    rings = []
    current_ring = []
    for pt in part:
        if pt is None:
            if len(current_ring) >= 3:
                if not current_ring[0].equals(current_ring[-1]):
                    current_ring.append(arcpy.Point(current_ring[0].X, current_ring[0].Y))
                rings.append(current_ring)
            current_ring = []
        else:
            current_ring.append(pt)
    if len(current_ring) >= 3:
        if not current_ring[0].equals(current_ring[-1]):
            current_ring.append(arcpy.Point(current_ring[0].X, current_ring[0].Y))
        rings.append(current_ring)
    return rings


def is_clockwise(points):
    if len(points) < 3:
        return False
    area = 0.0
    n = len(points)
    for i in range(n):
        j = (i + 1) % n
        area += points[i].X * points[j].Y
        area -= points[j].X * points[i].Y
    return area < 0


def rotate_ring_to_northwest_start(ring):
    if len(ring) <= 1:
        return ring
    best_index = 0
    best_y = ring[0].Y
    best_x = ring[0].X
    for i in range(1, len(ring)):
        pt = ring[i]
        if pt.Y > best_y or (pt.Y == best_y and pt.X < best_x):
            best_y = pt.Y
            best_x = pt.X
            best_index = i
    return ring[best_index:] + ring[:best_index]


class Toolbox(object):
    def __init__(self):
        self.label = "宗地边界处理工具(支持全数/抽稀输出 + 交接点保护)"
        self.alias = "ParcelBoundaryWithJunctionPreserve"
        self.tools = [GenerateJZDandJZX]


class GenerateJZDandJZX(object):
    def __init__(self):
        self.label = "生成界址点(JZD)与界址线(JZX)"
        self.description = (
            "功能:\n"
            "- 全数输出:输出所有原始折点\n"
            "- 抽稀输出:从原始边界中提取关键折点(不重建面),但地块交接点强制保留\n"
            "- 外环为顺时针(CW),内环为逆时针(CCW)\n"
            "- 界址点从西北角(最北且最西)开始编号\n"
            "- 外环编号 J1,J2...(每宗地独立)\n"
            "- 内环编号 K1,K2...(每宗地独立)\n"
            "- 每个宗地内部自动删除空间重复界址点\n"
            "- LOCAL_ID:宗地内编号(用于宗地图)\n"
            "- GLOBAL_ID:全局唯一流水号 P0001...(用于拓扑)\n"
            "- 界址线 START_ID/END_ID 使用 GLOBAL_ID"
        )
        self.category = "地籍管理"

    def getParameterInfo(self):
        input_parcel = arcpy.Parameter(
            displayName="输入宗地面图层",
            name="input_parcel",
            datatype="GPFeatureLayer",
            parameterType="Required",
            direction="Input")
        input_parcel.filter.list = ["Polygon"]

        output_mode = arcpy.Parameter(
            displayName="界址点输出模式",
            name="output_mode",
            datatype="GPString",
            parameterType="Required",
            direction="Input")
        output_mode.filter.type = "ValueList"
        output_mode.filter.list = ["全数输出", "抽稀输出"]
        output_mode.value = "全数输出"

        tolerance = arcpy.Parameter(
            displayName="简化容差(米)",
            name="tolerance",
            datatype="GPDouble",
            parameterType="Optional",
            direction="Input")
        tolerance.value = 5.0
        tolerance.enabled = False

        use_inner_prefix = arcpy.Parameter(
            displayName="内环界址点使用独立前缀(如 K1, K2...)",
            name="use_inner_prefix",
            datatype="GPBoolean",
            parameterType="Optional",
            direction="Input")
        use_inner_prefix.value = True

        output_jzd = arcpy.Parameter(
            displayName="输出界址点",
            name="output_jzd",
            datatype="DEFeatureClass",
            parameterType="Required",
            direction="Output")

        output_jzx = arcpy.Parameter(
            displayName="输出界址线",
            name="output_jzx",
            datatype="DEFeatureClass",
            parameterType="Required",
            direction="Output")

        return [input_parcel, output_mode, tolerance, use_inner_prefix, output_jzd, output_jzx]

    def updateParameters(self, parameters):
        if parameters[1].value == "抽稀输出":
            parameters[2].enabled = True
        else:
            parameters[2].enabled = False
        return

    def execute(self, parameters, messages):
        input_parcel = parameters[0].valueAsText
        output_mode = parameters[1].valueAsText
        tolerance = float(parameters[2].value) if parameters[2].value else 5.0
        use_inner_prefix = bool(parameters[3].value)
        output_jzd = parameters[4].valueAsText
        output_jzx = parameters[5].valueAsText

        desc = arcpy.Describe(input_parcel)
        sr = desc.spatialReference

        field_names_upper = [f.name.upper() for f in arcpy.ListFields(input_parcel)]
        if "ZDDM" not in field_names_upper:
            arcpy.AddError("输入图层缺少必需字段:ZDDM")
            return

        ZDDM_field = [f.name for f in arcpy.ListFields(input_parcel) if f.name.upper() == "ZDDM"][0]

        temp_ws = arcpy.env.scratchGDB
        uniq = str(uuid.uuid4().hex)[:6]

        total_parcels = int(arcpy.GetCount_management(input_parcel)[0])
        arcpy.AddMessage(f"📊 输入宗地总数: {total_parcels} 个")

        # ==============================
        # 步骤 1: 复制并修复几何
        # ==============================
        arcpy.SetProgressor("step", "正在复制并修复宗地几何...", 0, 6, 1)
        raw_parcels = os.path.join(temp_ws, f"parcels_{uniq}")
        arcpy.management.CopyFeatures(input_parcel, raw_parcels)
        arcpy.management.RepairGeometry(raw_parcels, "DELETE_NULL")
        arcpy.SetProgressorPosition()

        # ==============================
        # 步骤 2: 生成原始边界线(用于找交接点 & 分割)
        # ==============================
        arcpy.SetProgressorLabel("正在提取宗地边界线...")
        original_boundary = os.path.join(temp_ws, f"original_boundary_{uniq}")
        arcpy.management.PolygonToLine(raw_parcels, original_boundary, "IDENTIFY_NEIGHBORS")
        arcpy.SetProgressorPosition()

        # ==============================
        # 步骤 3: 提取交接点(公共边端点)
        # ==============================
        arcpy.SetProgressorLabel("正在识别地块交接点...")
        junction_points = set()
        with arcpy.da.SearchCursor(original_boundary, ["SHAPE@", "LEFT_FID", "RIGHT_FID"]) as cur:
            for line_geom, left_fid, right_fid in cur:
                if line_geom and left_fid != -1 and right_fid != -1 and left_fid != right_fid:
                    start_key = (round(line_geom.firstPoint.X, 9), round(line_geom.firstPoint.Y, 9))
                    end_key   = (round(line_geom.lastPoint.X, 9),  round(line_geom.lastPoint.Y, 9))
                    junction_points.add(start_key)
                    junction_points.add(end_key)
        arcpy.AddMessage(f"📍 检测到 {len(junction_points)} 个地块交接点(抽稀时将强制保留)")
        arcpy.SetProgressorPosition()

        # ==============================
        # 步骤 4: 根据模式准备界址点
        # ==============================
        if output_mode == "全数输出":
            arcpy.AddMessage("📌 使用原始宗地边界(全数输出界址点)")
            parcel_key_points = {}
            all_key_points_set = set()

            with arcpy.da.SearchCursor(raw_parcels, ["SHAPE@", ZDDM_field]) as s_cur:
                for geom, ZDDM_val in s_cur:
                    if geom is None or geom.area <= 0:
                        continue
                    ZDDM_val = str(ZDDM_val).strip() if ZDDM_val else ""
                    key_pts_for_parcel = []

                    for part in geom:
                        rings = extract_rings_from_part(part)
                        if not rings:
                            continue

                        outer_ring = rings[0]
                        cleaned_outer = [p for i, p in enumerate(outer_ring[:-1]) if i == 0 or not p.equals(outer_ring[i-1])]
                        for pt in cleaned_outer:
                            key = (round(pt.X, 9), round(pt.Y, 9))
                            all_key_points_set.add(key)
                            key_pts_for_parcel.append((pt, 'outer', 0))

                        for idx, ring in enumerate(rings[1:], start=1):
                            cleaned_inner = [p for i, p in enumerate(ring[:-1]) if i == 0 or not p.equals(ring[i-1])]
                            for pt in cleaned_inner:
                                key = (round(pt.X, 9), round(pt.Y, 9))
                                all_key_points_set.add(key)
                                key_pts_for_parcel.append((pt, 'inner', idx))

                    parcel_key_points[ZDDM_val] = key_pts_for_parcel

        else:  # 抽稀输出
            arcpy.AddMessage(f"🔍 正在从原始边界中提取关键折点(容差={tolerance}米,不重建面)...")
            parcel_key_points = {}
            all_key_points_set = set()

            with arcpy.da.SearchCursor(raw_parcels, ["SHAPE@", ZDDM_field]) as s_cur:
                for geom, ZDDM_val in s_cur:
                    if geom is None or geom.area <= 0:
                        continue
                    ZDDM_val = str(ZDDM_val).strip() if ZDDM_val else ""
                    key_pts_for_parcel = []

                    for part in geom:
                        rings = extract_rings_from_part(part)
                        if not rings:
                            continue

                        outer_ring = rings[0]
                        cleaned_outer = [p for i, p in enumerate(outer_ring[:-1]) if i == 0 or not p.equals(outer_ring[i-1])]
                        if len(cleaned_outer) >= 2:
                            simplified_outer = douglas_peucker_keep(cleaned_outer, tolerance, junction_points)
                            for pt in simplified_outer:
                                key = (round(pt.X, 9), round(pt.Y, 9))
                                all_key_points_set.add(key)
                                key_pts_for_parcel.append((pt, 'outer', 0))

                        for idx, ring in enumerate(rings[1:], start=1):
                            cleaned_inner = [p for i, p in enumerate(ring[:-1]) if i == 0 or not p.equals(ring[i-1])]
                            if len(cleaned_inner) >= 2:
                                simplified_inner = douglas_peucker_keep(cleaned_inner, tolerance, junction_points)
                                for pt in simplified_inner:
                                    key = (round(pt.X, 9), round(pt.Y, 9))
                                    all_key_points_set.add(key)
                                    key_pts_for_parcel.append((pt, 'inner', idx))

                    parcel_key_points[ZDDM_val] = key_pts_for_parcel

        # ==============================
        # 步骤 5: 创建关键点要素类(用于 SplitLineAtPoint)
        # ==============================
        arcpy.SetProgressorLabel("正在创建关键界址点图层...")
        key_points_fc = os.path.join(temp_ws, f"key_points_{uniq}")
        arcpy.management.CreateFeatureclass(temp_ws, f"key_points_{uniq}", "POINT", spatial_reference=sr)
        with arcpy.da.InsertCursor(key_points_fc, ["SHAPE@"]) as cur:
            for x, y in all_key_points_set:
                cur.insertRow([arcpy.PointGeometry(arcpy.Point(x, y), sr)])
        arcpy.SetProgressorPosition()

        # ==============================
        # 步骤 6: 分配 GLOBAL_ID 和 LOCAL_ID(按新方向规则)
        # ==============================
        arcpy.SetProgressorLabel("正在分配界址点编号(LOCAL_ID + GLOBAL_ID)...")

        coord_to_global = {}
        global_counter = 1
        all_coords = set()
        for pts_info in parcel_key_points.values():
            for pt, _, _ in pts_info:
                key = (round(pt.X, 9), round(pt.Y, 9))
                all_coords.add(key)

        for key in sorted(all_coords):
            coord_to_global[key] = f"P{global_counter:04d}"
            global_counter += 1

        coord_to_local = {}
        for ZDDM_val, pts_info in parcel_key_points.items():
            outer_pts = [pt for pt, typ, _ in pts_info if typ == 'outer']
            inner_by_ring = {}
            for pt, typ, ring_idx in pts_info:
                if typ == 'inner':
                    inner_by_ring.setdefault(ring_idx, []).append(pt)

            # === 外环处理:先旋转到西北角,再确保为顺时针(CW)===
            if outer_pts:
                outer_rotated = rotate_ring_to_northwest_start(outer_pts)
                if not is_clockwise(outer_rotated):  # 如果不是 CW(即 CCW),则反转
                    outer_rotated.reverse()
                for i, pt in enumerate(outer_rotated):
                    key = (round(pt.X, 9), round(pt.Y, 9))
                    coord_to_local[(ZDDM_val, key)] = f"J{i+1}"

            # === 内环处理:先旋转到西北角,再确保为逆时针(CCW)===
            inner_counter = 0
            for ring_idx in sorted(inner_by_ring.keys()):
                ring_pts = inner_by_ring[ring_idx]
                ring_rotated = rotate_ring_to_northwest_start(ring_pts)
                if is_clockwise(ring_rotated):  # 如果是 CW,则反转成 CCW
                    ring_rotated.reverse()
                for pt in ring_rotated:
                    inner_counter += 1
                    key = (round(pt.X, 9), round(pt.Y, 9))
                    coord_to_local[(ZDDM_val, key)] = f"K{inner_counter}"

        # ==============================
        # 步骤 7: 生成界址点输出
        # ==============================
        jzd_temp = os.path.join(temp_ws, f"jzd_{uniq}")
        arcpy.management.CreateFeatureclass(temp_ws, f"jzd_{uniq}", "POINT", spatial_reference=sr)
        arcpy.management.AddField(jzd_temp, "LOCAL_ID", "TEXT", field_length=20)
        arcpy.management.AddField(jzd_temp, "GLOBAL_ID", "TEXT", field_length=20)
        arcpy.management.AddField(jzd_temp, "X", "DOUBLE")
        arcpy.management.AddField(jzd_temp, "Y", "DOUBLE")
        arcpy.management.AddField(jzd_temp, ZDDM_field, "TEXT", field_length=255)

        inserted_check = set()
        with arcpy.da.InsertCursor(jzd_temp, ["SHAPE@", "LOCAL_ID", "GLOBAL_ID", "X", "Y", ZDDM_field]) as icur:
            for ZDDM_val, pts_info in parcel_key_points.items():
                for pt, _, _ in pts_info:
                    coord_key = (round(pt.X, 9), round(pt.Y, 9))
                    local_key = (ZDDM_val, coord_key)
                    if local_key in inserted_check:
                        continue
                    if coord_key in coord_to_global and local_key in coord_to_local:
                        local_id = coord_to_local[local_key]
                        global_id = coord_to_global[coord_key]
                        icur.insertRow([
                            arcpy.PointGeometry(pt, sr),
                            local_id,
                            global_id,
                            pt.X,
                            pt.Y,
                            ZDDM_val
                        ])
                        inserted_check.add(local_key)

        jzd_total = int(arcpy.GetCount_management(jzd_temp)[0])
        arcpy.AddMessage(f"✅ 共生成界址点: {jzd_total} 个(LOCAL_ID: J/K;GLOBAL_ID: P0001...)")
        arcpy.SetProgressorPosition()

        # ==============================
        # 步骤 8: 生成界址线
        # ==============================
        arcpy.SetProgressorLabel("正在生成界址线(关联 GLOBAL_ID)...")
        jzx_split = os.path.join(temp_ws, f"jzx_split_{uniq}")
        arcpy.management.SplitLineAtPoint(original_boundary, key_points_fc, jzx_split, "0.1 Meters")

        fid_to_ZDDM = {}
        with arcpy.da.SearchCursor(raw_parcels, ["OID@", ZDDM_field]) as cur:
            for fid, val in cur:
                fid_to_ZDDM[fid] = str(val).strip() if val else ""

        jzx_final = os.path.join(temp_ws, f"jzx_final_{uniq}")
        arcpy.management.CreateFeatureclass(temp_ws, f"jzx_final_{uniq}", "POLYLINE", spatial_reference=sr)
        arcpy.management.AddField(jzx_final, "START_ID", "TEXT", field_length=20)
        arcpy.management.AddField(jzx_final, "END_ID", "TEXT", field_length=20)
        arcpy.management.AddField(jzx_final, ZDDM_field, "TEXT", field_length=255)

        def get_global_id(point):
            key = (round(point.X, 9), round(point.Y, 9))
            return coord_to_global.get(key, "")

        jzx_count = 0
        with arcpy.da.InsertCursor(jzx_final, ["SHAPE@", "START_ID", "END_ID", ZDDM_field]) as i_cur:
            with arcpy.da.SearchCursor(jzx_split, ["SHAPE@", "LEFT_FID", "RIGHT_FID"]) as l_cur:
                for line_geom, left_fid, right_fid in l_cur:
                    if not line_geom or line_geom.length <= 0:
                        continue
                    start_gid = get_global_id(line_geom.firstPoint)
                    end_gid = get_global_id(line_geom.lastPoint)
                    if not (start_gid and end_gid):
                        continue

                    zddms = []
                    if left_fid != -1 and left_fid in fid_to_ZDDM:
                        z = fid_to_ZDDM[left_fid]
                        if z: zddms.append(z)
                    if right_fid != -1 and right_fid in fid_to_ZDDM:
                        z = fid_to_ZDDM[right_fid]
                        if z and z not in zddms:
                            zddms.append(z)
                    final_zddm = "\\".join(zddms) if zddms else ""

                    i_cur.insertRow([line_geom, start_gid, end_gid, final_zddm])
                    jzx_count += 1

        arcpy.AddMessage(f"✅ 共生成界址线: {jzx_count} 条(START_ID/END_ID 使用 GLOBAL_ID)")
        arcpy.SetProgressorPosition()

        # ==============================
        # 步骤 9: 输出结果
        # ==============================
        arcpy.SetProgressorLabel("正在写入最终结果...")
        arcpy.management.CopyFeatures(jzd_temp, output_jzd)
        arcpy.management.CopyFeatures(jzx_final, output_jzx)
        arcpy.ResetProgressor()

        arcpy.AddMessage("🎉 界址点与界址线生成完成!")
        arcpy.AddMessage(f"   📍 界址点: {output_jzd} (共 {jzd_total} 个)")
        arcpy.AddMessage(f"   📏 界址线: {output_jzx} (共 {jzx_count} 条)")
        if output_mode == "抽稀输出":
            arcpy.AddMessage(f"   ⚙️ 已启用抽稀(容差={tolerance}米),{len(junction_points)} 个交接点已强制保留。")
        else:
            arcpy.AddMessage("   📌 全数输出原始界址点。")

曾经的编码是自己遥不可及的技能,现如今如果你有基本的技能,可以理性有逻辑的提出你的问题AI就会给你构建代码,然后你就可以在Arcgispro中运行,接着就是反复的运行-修改-运行,直至达到了你想要的结果,这个过程是一个反复的过程,过程很痛苦,但是当你运行成功之后,你的世界就会打开一扇窗,恭喜你进入计算机的世界,这种体验放到以前是不可想象的,但是现在有AI的加持,你完全可以实现,这个时候你就会明白为什么上学了,学习学习的能力才是最重要的,我们要的从来不是具体的鱼,而是捕鱼的技能。

2025马上就结束了,2026即将到来,新的一年继续努力,解锁新的人生技能,提升人生高度,加油!

相关推荐
Summer不秃2 小时前
使用 SnapDOM + jsPDF 生成高质量 PDF (含多页分页, 附源码)
前端·javascript·vue.js·pdf·node.js
老歌老听老掉牙2 小时前
SymPy 中矩阵乘法的顺序与元素类型分析
python·矩阵·sympy
AC赳赳老秦2 小时前
工业互联网赋能智造:DeepSeek解析产线传感器数据驱动质量管控新范式
前端·数据库·人工智能·zookeeper·json·flume·deepseek
陈葛杰2 小时前
VMware 安装 Rocky Linux 9.6(Minimal 版)超详细图文教程|轻量 · 安全 · 生产级
linux·运维·服务器
时间之里2 小时前
【linux】:目录解析
linux·运维·服务器
Student_Zhang2 小时前
一个管理项目中所有弹窗的弹窗管理器(PopupManager)
前端·ios·github
2501_945837432 小时前
云服务器运维实战:从环境搭建到安全加固全流程
服务器
网络风云2 小时前
HTML 模块化方案
前端·html
bosins2 小时前
基于Python实现PDF文件个人隐私信息检查
开发语言·python·pdf