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 ") 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 }