You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
hy-screen-2.0/scripts/prepare-artificial-grasslan...

253 lines
8.4 KiB

import fs from "node:fs"
import path from "node:path"
import process from "node:process"
const projectRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..")
const sourcePath = process.argv[2]
const targetPath = path.join(projectRoot, "public/datas/yak-industry-chain/artificial_grassland_plots.geojson")
const summaryPath = path.join(projectRoot, "src/views/yakIndustryChain/artificialGrasslandSummary.json")
const townshipPath = path.join(projectRoot, "public/datas/geojsons/hongyuan-townships.geojson")
if (!sourcePath) {
console.error("Usage: node scripts/prepare-artificial-grassland-data.mjs <source.geojson>")
process.exit(1)
}
const source = readJson(sourcePath)
const townships = readJson(townshipPath)
const sourceLabel = "21.95万亩种草矢量数据"
const displayTownOrder = (townships.features || []).map((feature) => feature.properties?.name).filter(Boolean)
const seenTopology = new Set()
const features = []
for (const sourceFeature of source.features || []) {
const candidate = toPlotCandidate(sourceFeature)
if (!candidate) continue
const topologyKey = canonicalRing(candidate.ring)
if (!topologyKey || seenTopology.has(topologyKey)) continue
seenTopology.add(topologyKey)
const name = cleanName(sourceFeature.properties?.name)
const category = getPlotCategory(name)
const center = getRingCenter(candidate.ring)
const town = getTownByPoint(center, townships.features || [])
const id = `AG-${String(features.length + 1).padStart(3, "0")}`
features.push({
type: "Feature",
properties: {
id,
plot_code: id,
name,
plot_name: name,
stageKey: "artificial-grassland",
stage_name: "人工种草",
town,
town_name: town,
plot_category: category.name,
plot_category_key: category.key,
data_status: "待核定",
source_type: candidate.sourceType,
data_source: sourceLabel,
geometry_note: candidate.note,
},
geometry: {
type: "Polygon",
coordinates: [closeRing(candidate.ring)],
},
})
}
const summary = buildSummary(features, displayTownOrder)
writeJson(targetPath, {
type: "FeatureCollection",
name: "artificial_grassland_plots",
generatedAt: new Date().toISOString(),
description: "人工种草空间资料。图斑为待核定资料,不作为正式面积统计口径。",
features,
})
writeJson(summaryPath, summary)
console.log(`Generated ${features.length} artificial grassland plots.`)
console.log(`GeoJSON: ${targetPath}`)
console.log(`Summary: ${summaryPath}`)
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"))
}
function writeJson(filePath, value) {
fs.mkdirSync(path.dirname(filePath), { recursive: true })
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8")
}
function toPlotCandidate(feature = {}) {
const geometry = feature.geometry || {}
if (geometry.type === "Polygon") {
const ring = cleanRing(geometry.coordinates?.[0])
if (ring.length < 3) return null
return {
ring,
sourceType: "面图斑",
note: "原始面图斑已闭合后展示",
}
}
if (geometry.type !== "LineString") return null
const originalRing = Array.isArray(geometry.coordinates) ? geometry.coordinates : []
if (originalRing.length < 4 || !samePoint(originalRing[0], originalRing.at(-1))) return null
const ring = cleanRing(originalRing)
if (ring.length < 3) return null
return {
ring,
sourceType: "边界图斑",
note: "原始闭合边界转换为图斑展示",
}
}
function cleanRing(coordinates = []) {
const ring = []
coordinates.forEach((coordinate) => {
if (!Array.isArray(coordinate) || coordinate.length < 2) return
const next = [roundCoordinate(coordinate[0]), roundCoordinate(coordinate[1])]
if (!ring.length || !samePoint(ring.at(-1), next)) ring.push(next)
})
if (ring.length > 1 && samePoint(ring[0], ring.at(-1))) ring.pop()
return ring
}
function closeRing(ring = []) {
return [...ring, [...ring[0]]]
}
function canonicalRing(ring = []) {
if (ring.length < 3) return ""
const variants = [ring, [...ring].reverse()]
let result = ""
variants.forEach((variant) => {
variant.forEach((_, index) => {
const rotated = [...variant.slice(index), ...variant.slice(0, index)]
const signature = rotated.map((point) => `${point[0]},${point[1]}`).join("|")
if (!result || signature < result) result = signature
})
})
return result
}
function getRingCenter(ring = []) {
if (!ring.length) return [0, 0]
const total = ring.reduce(
(result, point) => [result[0] + Number(point[0] || 0), result[1] + Number(point[1] || 0)],
[0, 0],
)
return [total[0] / ring.length, total[1] / ring.length]
}
function getTownByPoint(point, townshipFeatures) {
for (const feature of townshipFeatures) {
const geometry = feature.geometry || {}
const polygons = geometry.type === "MultiPolygon" ? geometry.coordinates : geometry.type === "Polygon" ? [geometry.coordinates] : []
if (polygons.some((polygon) => isPointInRing(point, polygon?.[0] || []))) {
return feature.properties?.name || "未归属"
}
}
return "未归属"
}
function isPointInRing(point, ring = []) {
if (!Array.isArray(point) || ring.length < 3) return false
let inside = false
for (let index = 0, previous = ring.length - 1; index < ring.length; previous = index++) {
const [currentX, currentY] = ring[index]
const [previousX, previousY] = ring[previous]
const crosses = (currentY > point[1]) !== (previousY > point[1])
&& point[0] < ((previousX - currentX) * (point[1] - currentY)) / (previousY - currentY) + currentX
if (crosses) inside = !inside
}
return inside
}
function getPlotCategory(name) {
if (name.startsWith("撒播")) return { key: "broadcast", name: "撒播图斑" }
if (name.startsWith("总范围")) return { key: "high-yield", name: "高产稳产范围" }
if (name.startsWith("调拨")) return { key: "allocation", name: "调拨地块" }
if (name.includes("地块")) return { key: "project-plot", name: "项目地块" }
return { key: "unclassified", name: "待分类图斑" }
}
function buildSummary(plotFeatures, townOrder) {
const rows = plotFeatures.map((feature) => feature.properties || {})
const townRows = townOrder.map((name) => {
const value = rows.filter((item) => item.town === name).length
return {
key: name,
name,
value,
unit: "个",
extra: value ? "待核定图斑" : "暂无图斑",
zero: value === 0,
}
})
const categoryRows = groupRows(rows, "plot_category").map((item) => ({
...item,
unit: "个",
}))
const sourceRows = groupRows(rows, "source_type").map((item) => ({
...item,
unit: "个",
}))
const plotRows = rows
.map((item) => ({
key: item.id,
id: item.id,
name: item.name,
fullName: item.name,
town: item.town,
category: item.plot_category,
status: item.data_status,
stageKey: "artificial-grassland",
stageName: "人工种草",
canLocate: true,
locateText: "定位",
searchText: [item.name, item.town, item.plot_category, item.data_status].filter(Boolean).join(" "),
}))
.sort((left, right) => Number(right.category === "项目地块") - Number(left.category === "项目地块") || left.name.localeCompare(right.name, "zh-CN"))
return {
generatedAt: new Date().toISOString(),
sourceLabel,
statusLabel: "待核定",
totalPlots: rows.length,
coveredTowns: townRows.filter((item) => item.value > 0).length,
projectPlots: rows.filter((item) => item.plot_category === "项目地块").length,
pendingPlots: rows.length,
townRows,
categoryRows,
sourceRows,
plotRows,
}
}
function groupRows(rows, key) {
const grouped = new Map()
rows.forEach((item) => {
const name = String(item[key] || "未分类")
grouped.set(name, (grouped.get(name) || 0) + 1)
})
return [...grouped.entries()]
.map(([name, value]) => ({ key: name, name, value }))
.sort((left, right) => right.value - left.value || left.name.localeCompare(right.name, "zh-CN"))
}
function cleanName(value) {
const text = String(value || "").trim()
return text || "未命名图斑"
}
function roundCoordinate(value) {
return Number(Number(value).toFixed(8))
}
function samePoint(left = [], right = []) {
return Math.abs(Number(left[0]) - Number(right[0])) < 1e-8 && Math.abs(Number(left[1]) - Number(right[1])) < 1e-8
}