feat: 添加 Linux/Windows 双平台测试套件(云南省界叠加分析)
- test-linux: platform=linux, docker + python3 - test-windows: platform=windows, arcpy + python3 - 附示例 GeoJSON: 云南/昆明/大理/百色 Ref: SuiteHub/agent-profiles#22
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": { "name": "百色市", "code": "451000", "population": 3600000 },
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[105.5, 24.5], [105.8, 24.3], [106.2, 24.0], [106.8, 23.8],
|
||||||
|
[107.3, 23.5], [107.5, 23.2], [107.3, 23.0], [107.0, 23.2],
|
||||||
|
[106.5, 23.5], [106.0, 23.8], [105.7, 24.0], [105.5, 24.5]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": { "name": "大理州", "code": "532900", "population": 3300000 },
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[99.5, 26.5], [99.8, 26.3], [100.0, 26.2], [100.3, 26.0],
|
||||||
|
[100.5, 25.8], [100.4, 25.5], [100.3, 25.3], [100.1, 25.1],
|
||||||
|
[99.9, 25.0], [99.7, 25.1], [99.5, 25.3], [99.5, 26.5]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": { "name": "昆明市", "code": "530100", "population": 8500000 },
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[102.4, 25.7], [102.6, 25.6], [102.9, 25.5], [103.1, 25.4],
|
||||||
|
[103.4, 25.2], [103.3, 24.9], [103.2, 24.7], [103.0, 24.5],
|
||||||
|
[102.8, 24.4], [102.6, 24.5], [102.5, 24.7], [102.4, 24.9],
|
||||||
|
[102.4, 25.7]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": { "name": "云南省", "code": "530000" },
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[97.5, 27.5], [98.5, 28.8], [100.0, 29.3], [101.5, 28.5],
|
||||||
|
[103.0, 28.0], [104.5, 27.5], [105.5, 26.5], [106.2, 25.5],
|
||||||
|
[106.0, 24.0], [105.5, 23.0], [104.5, 22.2], [103.5, 22.0],
|
||||||
|
[102.5, 21.5], [101.5, 21.1], [100.5, 21.8], [99.5, 22.5],
|
||||||
|
[98.5, 23.5], [97.8, 24.5], [97.5, 25.5], [97.5, 27.5]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Step 1: GeoJSON → SHP
|
||||||
|
将省界和城市 GeoJSON 文件转换为 Shapefile
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import geopandas as gpd
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
province_path = os.environ.get("PARAM_PROVINCE", "")
|
||||||
|
cities_param = os.environ.get("PARAM_CITIES", "")
|
||||||
|
output_dir = os.environ.get("PARAM_OUTPUT_DIR", "/tmp/output/shp")
|
||||||
|
|
||||||
|
if not province_path:
|
||||||
|
print(json.dumps({"error": "缺少 province 参数", "success": False}))
|
||||||
|
sys.exit(1)
|
||||||
|
if not cities_param:
|
||||||
|
print(json.dumps({"error": "缺少 cities 参数", "success": False}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 读取并转换省界
|
||||||
|
print(json.dumps({"msg": f"读取省界: {province_path}"}))
|
||||||
|
province_gdf = gpd.read_file(province_path)
|
||||||
|
province_shp = os.path.join(output_dir, "province.shp")
|
||||||
|
province_gdf.to_file(province_shp, driver="ESRI Shapefile")
|
||||||
|
print(json.dumps({"msg": f"省界已转为 SHP: {province_shp}", "count": len(province_gdf)}))
|
||||||
|
|
||||||
|
# 读取并转换每个城市 GeoJSON
|
||||||
|
city_files = [c.strip() for c in cities_param.split(",") if c.strip()]
|
||||||
|
converted = []
|
||||||
|
for cf in city_files:
|
||||||
|
if not os.path.isfile(cf):
|
||||||
|
print(json.dumps({"warn": f"文件不存在,跳过: {cf}"}))
|
||||||
|
continue
|
||||||
|
print(json.dumps({"msg": f"读取城市: {cf}"}))
|
||||||
|
gdf = gpd.read_file(cf)
|
||||||
|
base = os.path.splitext(os.path.basename(cf))[0]
|
||||||
|
shp_path = os.path.join(output_dir, f"{base}.shp")
|
||||||
|
gdf.to_file(shp_path, driver="ESRI Shapefile")
|
||||||
|
converted.append(base)
|
||||||
|
print(json.dumps({"msg": f"已转换: {cf} → {shp_path}", "count": len(gdf)}))
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"province_shp": province_shp,
|
||||||
|
"city_count": len(converted),
|
||||||
|
"cities": converted
|
||||||
|
}
|
||||||
|
print(json.dumps(result))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Step 3: 输出 Excel
|
||||||
|
将叠加分析结果写入 Excel 文件
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, Alignment, PatternFill
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
result_dir = os.environ.get("PARAM_RESULT_DIR", "/tmp/output/result")
|
||||||
|
output_path = os.environ.get("PARAM_OUTPUT_PATH", "")
|
||||||
|
|
||||||
|
if not output_path:
|
||||||
|
print(json.dumps({"error": "缺少 output_path 参数", "success": False}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 读取分析结果
|
||||||
|
result_json = os.path.join(result_dir, "overlay_results.json")
|
||||||
|
if not os.path.isfile(result_json):
|
||||||
|
print(json.dumps({"error": f"结果文件不存在: {result_json}", "success": False}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(result_json, "r", encoding="utf-8") as f:
|
||||||
|
results = json.load(f)
|
||||||
|
|
||||||
|
# 创建工作簿
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "叠加分析结果"
|
||||||
|
|
||||||
|
# 标题样式
|
||||||
|
header_font = Font(bold=True, size=12, color="FFFFFF")
|
||||||
|
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||||
|
header_align = Alignment(horizontal="center", vertical="center")
|
||||||
|
|
||||||
|
# 表头
|
||||||
|
headers = ["城市", "城市代码", "城市名称", "人口", "在省内", "相交", "状态"]
|
||||||
|
for col, h in enumerate(headers, 1):
|
||||||
|
cell = ws.cell(row=1, column=col, value=h)
|
||||||
|
cell.font = header_font
|
||||||
|
cell.fill = header_fill
|
||||||
|
cell.alignment = header_align
|
||||||
|
|
||||||
|
# 数据行
|
||||||
|
pass_fill = PatternFill(start_color="E2EFDA", end_color="E2EFDA", fill_type="solid")
|
||||||
|
fail_fill = PatternFill(start_color="FCE4EC", end_color="FCE4EC", fill_type="solid")
|
||||||
|
|
||||||
|
for row_idx, r in enumerate(results, 2):
|
||||||
|
ws.cell(row=row_idx, column=1, value=r.get("city", ""))
|
||||||
|
ws.cell(row=row_idx, column=2, value=r.get("city_code", ""))
|
||||||
|
ws.cell(row=row_idx, column=3, value=r.get("city_name", ""))
|
||||||
|
ws.cell(row=row_idx, column=4, value=r.get("population", 0))
|
||||||
|
ws.cell(row=row_idx, column=5, value="是" if r.get("in_province") else "否")
|
||||||
|
ws.cell(row=row_idx, column=6, value="是" if r.get("intersects") else "否")
|
||||||
|
ws.cell(row=row_idx, column=7, value=r.get("status", ""))
|
||||||
|
|
||||||
|
# 条件高亮
|
||||||
|
fill = pass_fill if r.get("in_province") else fail_fill
|
||||||
|
for col in range(1, 8):
|
||||||
|
ws.cell(row=row_idx, column=col).fill = fill
|
||||||
|
|
||||||
|
# 调整列宽
|
||||||
|
ws.column_dimensions["A"].width = 15
|
||||||
|
ws.column_dimensions["B"].width = 12
|
||||||
|
ws.column_dimensions["C"].width = 15
|
||||||
|
ws.column_dimensions["D"].width = 12
|
||||||
|
ws.column_dimensions["E"].width = 10
|
||||||
|
ws.column_dimensions["F"].width = 10
|
||||||
|
ws.column_dimensions["G"].width = 15
|
||||||
|
|
||||||
|
# 确保输出目录存在
|
||||||
|
out_dir = os.path.dirname(output_path)
|
||||||
|
if out_dir:
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
wb.save(output_path)
|
||||||
|
print(json.dumps({
|
||||||
|
"success": True,
|
||||||
|
"output_path": output_path,
|
||||||
|
"total": len(results),
|
||||||
|
"msg": f"Excel 已输出到: {output_path}"
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Step 2: 叠加分析
|
||||||
|
判断各城市是否在省界内(spatial join),输出分析结果
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import geopandas as gpd
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
province_shp = os.environ.get("PARAM_PROVINCE_SHP", "")
|
||||||
|
cities_dir = os.environ.get("PARAM_CITIES_DIR", "/tmp/output/shp")
|
||||||
|
output_dir = os.environ.get("PARAM_OUTPUT_DIR", "/tmp/output/result")
|
||||||
|
|
||||||
|
if not province_shp or not os.path.isfile(province_shp):
|
||||||
|
print(json.dumps({"error": f"省界 SHP 文件不存在: {province_shp}", "success": False}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 读取省界
|
||||||
|
print(json.dumps({"msg": f"读取省界 SHP: {province_shp}"}))
|
||||||
|
province = gpd.read_file(province_shp)
|
||||||
|
|
||||||
|
if province.empty:
|
||||||
|
print(json.dumps({"error": "省界数据为空", "success": False}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 确保坐标系一致
|
||||||
|
target_crs = province.crs or "EPSG:4326"
|
||||||
|
province = province.to_crs(target_crs)
|
||||||
|
|
||||||
|
# 查找所有城市 SHP
|
||||||
|
city_shp_files = glob.glob(os.path.join(cities_dir, "*.shp"))
|
||||||
|
city_shp_files = [f for f in city_shp_files
|
||||||
|
if os.path.basename(f) != "province.shp"]
|
||||||
|
|
||||||
|
if not city_shp_files:
|
||||||
|
print(json.dumps({"error": "未找到城市 SHP 文件", "success": False}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for shp in city_shp_files:
|
||||||
|
city_name = os.path.splitext(os.path.basename(shp))[0]
|
||||||
|
print(json.dumps({"msg": f"分析城市: {city_name}"}))
|
||||||
|
city = gpd.read_file(shp)
|
||||||
|
|
||||||
|
if city.empty:
|
||||||
|
results.append({"city": city_name, "in_province": False,
|
||||||
|
"reason": "数据为空", "status": "跳过"})
|
||||||
|
continue
|
||||||
|
|
||||||
|
city = city.to_crs(target_crs)
|
||||||
|
|
||||||
|
# 空间判断:城市中心点是否在省界内
|
||||||
|
city_centroid = city.geometry.centroid.iloc[0]
|
||||||
|
in_province = province.geometry.contains(city_centroid).any()
|
||||||
|
|
||||||
|
# 也检查 intersect 作为辅助判断
|
||||||
|
intersects = province.geometry.intersects(city.geometry).any()
|
||||||
|
|
||||||
|
status = "通过 ✅" if in_province else "不通过 ❌"
|
||||||
|
|
||||||
|
city_name_prop = city.iloc[0].get("name", city_name)
|
||||||
|
city_code = city.iloc[0].get("code", "")
|
||||||
|
city_pop = city.iloc[0].get("population", 0)
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"city": city_name,
|
||||||
|
"city_code": str(city_code),
|
||||||
|
"city_name": str(city_name_prop),
|
||||||
|
"population": int(city_pop) if city_pop else 0,
|
||||||
|
"in_province": bool(in_province),
|
||||||
|
"intersects": bool(intersects),
|
||||||
|
"status": status
|
||||||
|
})
|
||||||
|
print(json.dumps({"msg": f"{city_name}: {status}"}))
|
||||||
|
|
||||||
|
# 输出分析结果 JSON
|
||||||
|
result_path = os.path.join(output_dir, "overlay_results.json")
|
||||||
|
with open(result_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"success": True,
|
||||||
|
"total_cities": len(results),
|
||||||
|
"in_province": sum(1 for r in results if r["in_province"]),
|
||||||
|
"not_in_province": sum(1 for r in results if not r["in_province"]),
|
||||||
|
"results": results,
|
||||||
|
"result_json": result_path
|
||||||
|
}
|
||||||
|
print(json.dumps(summary))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: 云南省界叠加分析(Linux 测试)
|
||||||
|
description: 验证 GIS Actions 4.0 Linux 双平台执行链路的测试套件。将示例 GeoJSON 转为 SHP → 叠加分析(判断城市是否在省内)→ 输出 Excel
|
||||||
|
version: 1.0.0
|
||||||
|
author: SuiteForge
|
||||||
|
platform: linux
|
||||||
|
slug: test-yunnan-overlay-linux
|
||||||
|
tags: [测试, 叠加分析, Linux, 云南省]
|
||||||
|
category: 测试验证
|
||||||
|
|
||||||
|
params:
|
||||||
|
province:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
desc: 省界 GeoJSON 文件路径
|
||||||
|
cities:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
desc: 待分析城市 GeoJSON 文件路径,多个文件用逗号分隔
|
||||||
|
output_path:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
desc: 输出 Excel 文件路径(.xlsx)
|
||||||
|
|
||||||
|
base_image: gis-base:latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- id: geojson_to_shp
|
||||||
|
name: 转为 SHP
|
||||||
|
runtime: docker
|
||||||
|
script_id: convert
|
||||||
|
params:
|
||||||
|
province: $params.province
|
||||||
|
cities: $params.cities
|
||||||
|
output_dir: /tmp/output/shp
|
||||||
|
|
||||||
|
- id: overlay
|
||||||
|
name: 叠加分析
|
||||||
|
runtime: docker
|
||||||
|
script_id: overlay
|
||||||
|
depends_on: [geojson_to_shp]
|
||||||
|
params:
|
||||||
|
province_shp: /tmp/output/shp/province.shp
|
||||||
|
cities_dir: /tmp/output/shp
|
||||||
|
output_dir: /tmp/output/result
|
||||||
|
|
||||||
|
- id: export_excel
|
||||||
|
name: 输出 Excel
|
||||||
|
runtime: python3
|
||||||
|
script_id: excel
|
||||||
|
depends_on: [overlay]
|
||||||
|
params:
|
||||||
|
result_dir: /tmp/output/result
|
||||||
|
output_path: $params.output_path
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": { "name": "百色市", "code": "451000", "population": 3600000 },
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[105.5, 24.5], [105.8, 24.3], [106.2, 24.0], [106.8, 23.8],
|
||||||
|
[107.3, 23.5], [107.5, 23.2], [107.3, 23.0], [107.0, 23.2],
|
||||||
|
[106.5, 23.5], [106.0, 23.8], [105.7, 24.0], [105.5, 24.5]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": { "name": "大理州", "code": "532900", "population": 3300000 },
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[99.5, 26.5], [99.8, 26.3], [100.0, 26.2], [100.3, 26.0],
|
||||||
|
[100.5, 25.8], [100.4, 25.5], [100.3, 25.3], [100.1, 25.1],
|
||||||
|
[99.9, 25.0], [99.7, 25.1], [99.5, 25.3], [99.5, 26.5]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": { "name": "昆明市", "code": "530100", "population": 8500000 },
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[102.4, 25.7], [102.6, 25.6], [102.9, 25.5], [103.1, 25.4],
|
||||||
|
[103.4, 25.2], [103.3, 24.9], [103.2, 24.7], [103.0, 24.5],
|
||||||
|
[102.8, 24.4], [102.6, 24.5], [102.5, 24.7], [102.4, 24.9],
|
||||||
|
[102.4, 25.7]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": { "name": "云南省", "code": "530000" },
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[97.5, 27.5], [98.5, 28.8], [100.0, 29.3], [101.5, 28.5],
|
||||||
|
[103.0, 28.0], [104.5, 27.5], [105.5, 26.5], [106.2, 25.5],
|
||||||
|
[106.0, 24.0], [105.5, 23.0], [104.5, 22.2], [103.5, 22.0],
|
||||||
|
[102.5, 21.5], [101.5, 21.1], [100.5, 21.8], [99.5, 22.5],
|
||||||
|
[98.5, 23.5], [97.8, 24.5], [97.5, 25.5], [97.5, 27.5]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Step 1: GeoJSON → SHP (arcpy)
|
||||||
|
将省界和城市 GeoJSON 文件转换为 Shapefile
|
||||||
|
使用 arcpy 的 JSON To Feature Class 工具
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import arcpy
|
||||||
|
|
||||||
|
# 允许覆写输出
|
||||||
|
arcpy.env.overwriteOutput = True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
province_path = os.environ.get("PARAM_PROVINCE", "")
|
||||||
|
cities_param = os.environ.get("PARAM_CITIES", "")
|
||||||
|
output_dir = os.environ.get("PARAM_OUTPUT_DIR", r"C:\temp\output\shp")
|
||||||
|
|
||||||
|
if not province_path:
|
||||||
|
print json.dumps({"error": "缺少 province 参数", "success": False})
|
||||||
|
sys.exit(1)
|
||||||
|
if not cities_param:
|
||||||
|
print json.dumps({"error": "缺少 cities 参数", "success": False})
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not os.path.isdir(output_dir):
|
||||||
|
os.makedirs(output_dir)
|
||||||
|
|
||||||
|
# 转换省界
|
||||||
|
print json.dumps({"msg": "转换省界: " + province_path})
|
||||||
|
province_name = os.path.splitext(os.path.basename(province_path))[0]
|
||||||
|
province_shp = os.path.join(output_dir, "province.shp")
|
||||||
|
# GeoJSON → Feature Class → Shapefile
|
||||||
|
arcpy.JSONToFeatures_conversion(province_path, province_shp)
|
||||||
|
print json.dumps({"msg": "省界已转为 SHP: " + province_shp})
|
||||||
|
|
||||||
|
# 转换每个城市
|
||||||
|
city_files = [c.strip() for c in cities_param.split(",") if c.strip()]
|
||||||
|
converted = []
|
||||||
|
for cf in city_files:
|
||||||
|
if not os.path.isfile(cf):
|
||||||
|
print json.dumps({"warn": "文件不存在,跳过: " + cf})
|
||||||
|
continue
|
||||||
|
print json.dumps({"msg": "转换城市: " + cf})
|
||||||
|
base = os.path.splitext(os.path.basename(cf))[0]
|
||||||
|
shp_path = os.path.join(output_dir, base + ".shp")
|
||||||
|
arcpy.JSONToFeatures_conversion(cf, shp_path)
|
||||||
|
converted.append(base)
|
||||||
|
print json.dumps({"msg": "已转换: " + cf + " -> " + shp_path})
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"province_shp": province_shp,
|
||||||
|
"city_count": len(converted),
|
||||||
|
"cities": converted
|
||||||
|
}
|
||||||
|
print json.dumps(result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Step 2: 叠加分析 (arcpy)
|
||||||
|
判断各城市是否在省界内,输出分析结果 JSON
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import arcpy
|
||||||
|
|
||||||
|
arcpy.env.overwriteOutput = True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
province_shp = os.environ.get("PARAM_PROVINCE_SHP", "")
|
||||||
|
cities_dir = os.environ.get("PARAM_CITIES_DIR", r"C:\temp\output\shp")
|
||||||
|
output_dir = os.environ.get("PARAM_OUTPUT_DIR", r"C:\temp\output\result")
|
||||||
|
|
||||||
|
if not province_shp or not os.path.isfile(province_shp):
|
||||||
|
print json.dumps({"error": "省界 SHP 文件不存在: " + str(province_shp), "success": False})
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not os.path.isdir(output_dir):
|
||||||
|
os.makedirs(output_dir)
|
||||||
|
|
||||||
|
# 查找所有城市 SHP
|
||||||
|
city_shp_files = glob.glob(os.path.join(cities_dir, "*.shp"))
|
||||||
|
city_shp_files = [f for f in city_shp_files
|
||||||
|
if os.path.basename(f) != "province.shp"]
|
||||||
|
|
||||||
|
if not city_shp_files:
|
||||||
|
print json.dumps({"error": "未找到城市 SHP 文件", "success": False})
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 创建省界图层
|
||||||
|
province_lyr = "province_layer"
|
||||||
|
arcpy.MakeFeatureLayer_management(province_shp, province_lyr)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
desc = arcpy.Describe(province_shp)
|
||||||
|
sr = desc.spatialReference
|
||||||
|
|
||||||
|
for shp in city_shp_files:
|
||||||
|
city_name = os.path.splitext(os.path.basename(shp))[0]
|
||||||
|
print json.dumps({"msg": "分析城市: " + city_name})
|
||||||
|
|
||||||
|
# 创建城市图层
|
||||||
|
city_lyr = "city_layer_" + city_name
|
||||||
|
arcpy.MakeFeatureLayer_management(shp, city_lyr)
|
||||||
|
|
||||||
|
# 空间选择:城市中心点在省界内
|
||||||
|
# 先获取城市几何中心
|
||||||
|
city_centroid_shp = os.path.join(output_dir, city_name + "_centroid.shp")
|
||||||
|
arcpy.FeatureToPoint_management(shp, city_centroid_shp, "INSIDE")
|
||||||
|
|
||||||
|
centroid_lyr = "centroid_layer_" + city_name
|
||||||
|
arcpy.MakeFeatureLayer_management(city_centroid_shp, centroid_lyr)
|
||||||
|
|
||||||
|
# Select by location: 中心点在省界内
|
||||||
|
arcpy.SelectLayerByLocation_management(centroid_lyr, "WITHIN", province_lyr)
|
||||||
|
count = int(arcpy.GetCount_management(centroid_lyr).getOutput(0))
|
||||||
|
in_province = count > 0
|
||||||
|
|
||||||
|
# 获取属性
|
||||||
|
city_name_prop = city_name
|
||||||
|
city_code = ""
|
||||||
|
population = 0
|
||||||
|
try:
|
||||||
|
with arcpy.da.SearchCursor(shp, ["name", "code", "population"]) as cursor:
|
||||||
|
for row in cursor:
|
||||||
|
if row[0]:
|
||||||
|
city_name_prop = str(row[0])
|
||||||
|
if row[1]:
|
||||||
|
city_code = str(row[1])
|
||||||
|
if row[2]:
|
||||||
|
population = int(row[2])
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
status = u"通过 \u2705" if in_province else u"不通过 \u274c"
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"city": city_name,
|
||||||
|
"city_code": city_code,
|
||||||
|
"city_name": city_name_prop,
|
||||||
|
"population": population,
|
||||||
|
"in_province": in_province,
|
||||||
|
"intersects": in_province,
|
||||||
|
"status": status
|
||||||
|
})
|
||||||
|
print json.dumps({"msg": city_name + ": " + status})
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
arcpy.Delete_management(city_lyr)
|
||||||
|
arcpy.Delete_management(centroid_lyr)
|
||||||
|
arcpy.Delete_management(city_centroid_shp)
|
||||||
|
|
||||||
|
arcpy.Delete_management(province_lyr)
|
||||||
|
|
||||||
|
# 输出 JSON
|
||||||
|
result_json = os.path.join(output_dir, "overlay_results.json")
|
||||||
|
with open(result_json, "w") as f:
|
||||||
|
json.dump(results, f, indent=2)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"success": True,
|
||||||
|
"total_cities": len(results),
|
||||||
|
"in_province": sum(1 for r in results if r["in_province"]),
|
||||||
|
"not_in_province": sum(1 for r in results if not r["in_province"]),
|
||||||
|
"results": results,
|
||||||
|
"result_json": result_json
|
||||||
|
}
|
||||||
|
print json.dumps(summary)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Step 3: 输出 Excel
|
||||||
|
将叠加分析结果写入 Excel 文件
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, Alignment, PatternFill
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
result_dir = os.environ.get("PARAM_RESULT_DIR", "/tmp/output/result")
|
||||||
|
output_path = os.environ.get("PARAM_OUTPUT_PATH", "")
|
||||||
|
|
||||||
|
if not output_path:
|
||||||
|
print(json.dumps({"error": "缺少 output_path 参数", "success": False}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 读取分析结果
|
||||||
|
result_json = os.path.join(result_dir, "overlay_results.json")
|
||||||
|
if not os.path.isfile(result_json):
|
||||||
|
print(json.dumps({"error": f"结果文件不存在: {result_json}", "success": False}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(result_json, "r", encoding="utf-8") as f:
|
||||||
|
results = json.load(f)
|
||||||
|
|
||||||
|
# 创建工作簿
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "叠加分析结果"
|
||||||
|
|
||||||
|
# 标题样式
|
||||||
|
header_font = Font(bold=True, size=12, color="FFFFFF")
|
||||||
|
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||||
|
header_align = Alignment(horizontal="center", vertical="center")
|
||||||
|
|
||||||
|
# 表头
|
||||||
|
headers = ["城市", "城市代码", "城市名称", "人口", "在省内", "相交", "状态"]
|
||||||
|
for col, h in enumerate(headers, 1):
|
||||||
|
cell = ws.cell(row=1, column=col, value=h)
|
||||||
|
cell.font = header_font
|
||||||
|
cell.fill = header_fill
|
||||||
|
cell.alignment = header_align
|
||||||
|
|
||||||
|
# 数据行
|
||||||
|
pass_fill = PatternFill(start_color="E2EFDA", end_color="E2EFDA", fill_type="solid")
|
||||||
|
fail_fill = PatternFill(start_color="FCE4EC", end_color="FCE4EC", fill_type="solid")
|
||||||
|
|
||||||
|
for row_idx, r in enumerate(results, 2):
|
||||||
|
ws.cell(row=row_idx, column=1, value=r.get("city", ""))
|
||||||
|
ws.cell(row=row_idx, column=2, value=r.get("city_code", ""))
|
||||||
|
ws.cell(row=row_idx, column=3, value=r.get("city_name", ""))
|
||||||
|
ws.cell(row=row_idx, column=4, value=r.get("population", 0))
|
||||||
|
ws.cell(row=row_idx, column=5, value="是" if r.get("in_province") else "否")
|
||||||
|
ws.cell(row=row_idx, column=6, value="是" if r.get("intersects") else "否")
|
||||||
|
ws.cell(row=row_idx, column=7, value=r.get("status", ""))
|
||||||
|
|
||||||
|
# 条件高亮
|
||||||
|
fill = pass_fill if r.get("in_province") else fail_fill
|
||||||
|
for col in range(1, 8):
|
||||||
|
ws.cell(row=row_idx, column=col).fill = fill
|
||||||
|
|
||||||
|
# 调整列宽
|
||||||
|
ws.column_dimensions["A"].width = 15
|
||||||
|
ws.column_dimensions["B"].width = 12
|
||||||
|
ws.column_dimensions["C"].width = 15
|
||||||
|
ws.column_dimensions["D"].width = 12
|
||||||
|
ws.column_dimensions["E"].width = 10
|
||||||
|
ws.column_dimensions["F"].width = 10
|
||||||
|
ws.column_dimensions["G"].width = 15
|
||||||
|
|
||||||
|
# 确保输出目录存在
|
||||||
|
out_dir = os.path.dirname(output_path)
|
||||||
|
if out_dir:
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
wb.save(output_path)
|
||||||
|
print(json.dumps({
|
||||||
|
"success": True,
|
||||||
|
"output_path": output_path,
|
||||||
|
"total": len(results),
|
||||||
|
"msg": f"Excel 已输出到: {output_path}"
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: 云南省界叠加分析(Windows 测试)
|
||||||
|
description: 验证 GIS Actions 4.0 Windows 执行链路的测试套件。将示例 GeoJSON 转为 SHP → 叠加分析(arcpy)→ 输出 Excel
|
||||||
|
version: 1.0.0
|
||||||
|
author: SuiteForge
|
||||||
|
platform: windows
|
||||||
|
slug: test-yunnan-overlay-windows
|
||||||
|
tags: [测试, 叠加分析, Windows, 云南省]
|
||||||
|
category: 测试验证
|
||||||
|
|
||||||
|
params:
|
||||||
|
province:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
desc: 省界 GeoJSON 文件路径
|
||||||
|
cities:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
desc: 待分析城市 GeoJSON 文件路径,多个文件用逗号分隔
|
||||||
|
output_path:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
desc: 输出 Excel 文件路径(.xlsx)
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- id: geojson_to_shp
|
||||||
|
name: 转为 SHP
|
||||||
|
runtime: arcpy
|
||||||
|
script_id: arcpy_convert
|
||||||
|
params:
|
||||||
|
province: $params.province
|
||||||
|
cities: $params.cities
|
||||||
|
output_dir: /tmp/output/shp
|
||||||
|
|
||||||
|
- id: overlay
|
||||||
|
name: 叠加分析
|
||||||
|
runtime: arcpy
|
||||||
|
script_id: arcpy_overlay
|
||||||
|
depends_on: [geojson_to_shp]
|
||||||
|
params:
|
||||||
|
province_shp: /tmp/output/shp/province.shp
|
||||||
|
cities_dir: /tmp/output/shp
|
||||||
|
output_dir: /tmp/output/result
|
||||||
|
|
||||||
|
- id: export_excel
|
||||||
|
name: 输出 Excel
|
||||||
|
runtime: python3
|
||||||
|
script_id: excel
|
||||||
|
depends_on: [overlay]
|
||||||
|
params:
|
||||||
|
result_dir: /tmp/output/result
|
||||||
|
output_path: $params.output_path
|
||||||
Reference in New Issue
Block a user