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即将到来,新的一年继续努力,解锁新的人生技能,提升人生高度,加油!