feat: 引入面积计算器和坐标转换器组件,并添加新的依赖项和实用函数。
This commit is contained in:
@@ -13,16 +13,45 @@
|
||||
<div class="grid grid-2 gap-xl">
|
||||
<!-- Points Input -->
|
||||
<div class="card">
|
||||
<h3 class="card-title flex justify-between">
|
||||
<span><LayoutList class="icon-md" /> 顶点坐标 (经度, 纬度)</span>
|
||||
<button @click="addPoint" class="btn btn-secondary btn-sm">➕ 添加顶点</button>
|
||||
</h3>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 class="card-title" style="margin: 0; display: flex; align-items: center; gap: 8px;">
|
||||
<LayoutList class="icon-md" /> 顶点坐标 (经度, 纬度)
|
||||
</h3>
|
||||
<button @click="addPoint" class="btn btn-secondary btn-sm" style="display: flex; align-items: center; gap: 4px; padding: 4px 12px; font-size: 0.875rem;">
|
||||
➕ 添加顶点
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-lg mb-md grid grid-2 gap-sm">
|
||||
<div class="input-group">
|
||||
<label class="input-label">坐标类型</label>
|
||||
<select v-model="coordType" class="input" @change="handleModeChange">
|
||||
<option value="lnglat">经纬度 (Lng/Lat)</option>
|
||||
<option value="planar">平面坐标 (X/Y)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">计算方式</label>
|
||||
<select v-model="calcMethod" class="input" :disabled="coordType === 'planar'">
|
||||
<option value="geodetic">测地线 (严密球面)</option>
|
||||
<option value="planar">简单平面 (直角公式)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="points-list">
|
||||
<div v-for="(point, index) in points" :key="index" class="point-input">
|
||||
<span class="point-index">{{ index + 1 }}</span>
|
||||
<input type="number" v-model.number="point.lng" class="input" placeholder="经度 (Lon)" step="0.000001">
|
||||
<input type="number" v-model.number="point.lat" class="input" placeholder="纬度 (Lat)" step="0.000001">
|
||||
|
||||
<template v-if="coordType === 'lnglat'">
|
||||
<input type="number" v-model.number="point.lng" class="input" placeholder="经度 (Lon)" step="0.000001">
|
||||
<input type="number" v-model.number="point.lat" class="input" placeholder="纬度 (Lat)" step="0.000001">
|
||||
</template>
|
||||
<template v-else>
|
||||
<input type="number" v-model.number="point.x" class="input" placeholder="X坐标 (北移)">
|
||||
<input type="number" v-model.number="point.y" class="input" placeholder="Y坐标 (东移)">
|
||||
</template>
|
||||
|
||||
<button v-if="points.length > 3" @click="removePoint(index)" class="btn-remove" title="删除">
|
||||
<Trash2 class="icon-sm" />
|
||||
</button>
|
||||
@@ -30,13 +59,6 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-lg">
|
||||
<div class="input-group">
|
||||
<label class="input-label">计算模式</label>
|
||||
<select v-model="calcMode" class="input">
|
||||
<option value="geodetic">球面计算 (WGS84 测地线)</option>
|
||||
<option value="planar">平面计算 (简单几何)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button @click="calculate" class="btn btn-primary btn-full mt-md">开始计算</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,44 +109,109 @@ import * as turf from '@turf/turf'
|
||||
import { ChevronLeft, SquareStack, LayoutList, Trash2, Activity, Info } from 'lucide-vue-next'
|
||||
|
||||
const points = ref([
|
||||
{ lng: 116.391, lat: 39.907 },
|
||||
{ lng: 116.401, lat: 39.907 },
|
||||
{ lng: 116.401, lat: 39.917 },
|
||||
{ lng: 116.391, lat: 39.917 }
|
||||
{ lng: 108.5597255, lat: 22.8589543, x: 36557444.57, y: 2529026.75 },
|
||||
{ lng: 108.5607409, lat: 22.8589507, x: 36557548.78, y: 2529026.75 },
|
||||
{ lng: 108.5607373, lat: 22.8580860, x: 36557548.78, y: 2528930.99 },
|
||||
{ lng: 108.5597220, lat: 22.8580896, x: 36557444.57, y: 2528930.99 }
|
||||
])
|
||||
|
||||
const calcMode = ref('geodetic')
|
||||
const coordType = ref('lnglat')
|
||||
const calcMethod = ref('geodetic')
|
||||
const area = ref(null)
|
||||
const perimeter = ref(0)
|
||||
|
||||
const handleModeChange = () => {
|
||||
area.value = null
|
||||
perimeter.value = 0
|
||||
if (coordType.value === 'planar') {
|
||||
calcMethod.value = 'planar'
|
||||
} else {
|
||||
calcMethod.value = 'geodetic'
|
||||
}
|
||||
}
|
||||
|
||||
const addPoint = () => {
|
||||
const last = points.value[points.value.length - 1]
|
||||
points.value.push({ lng: last.lng + 0.01, lat: last.lat })
|
||||
points.value.push({
|
||||
lng: last.lng !== undefined ? Number(last.lng) + 0.001 : 116.391,
|
||||
lat: last.lat !== undefined ? Number(last.lat) : 39.907,
|
||||
x: last.x !== undefined ? Number(last.x) + 100 : 4419500,
|
||||
y: last.y !== undefined ? Number(last.y) : 39533900
|
||||
})
|
||||
}
|
||||
|
||||
const removePoint = (index) => {
|
||||
points.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const calculate = () => {
|
||||
// Turf needs the first and last point to be the same to close the polygon
|
||||
const coords = points.value.map(p => [p.lng, p.lat])
|
||||
coords.push(coords[0])
|
||||
|
||||
try {
|
||||
const polygon = turf.polygon([coords])
|
||||
const calculatePlanar = (pts) => {
|
||||
let a = 0;
|
||||
let p = 0;
|
||||
const n = pts.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const j = (i + 1) % n;
|
||||
// Shoelace: sum(x_i * y_{i+1} - x_{i+1} * y_i)
|
||||
a += pts[i].x * pts[j].y;
|
||||
a -= pts[j].x * pts[i].y;
|
||||
|
||||
if (calcMode.value === 'geodetic') {
|
||||
// Perimeter
|
||||
const dx = pts[j].x - pts[i].x;
|
||||
const dy = pts[j].y - pts[i].y;
|
||||
p += Math.sqrt(dx*dx + dy*dy);
|
||||
}
|
||||
return { area: Math.abs(a / 2), perimeter: p };
|
||||
}
|
||||
|
||||
const calculate = () => {
|
||||
try {
|
||||
// Ensure all inputs are numbers
|
||||
const rawPoints = points.value.map(p => ({
|
||||
lng: Number(p.lng || 0),
|
||||
lat: Number(p.lat || 0),
|
||||
x: Number(p.x || 0),
|
||||
y: Number(p.y || 0)
|
||||
}));
|
||||
|
||||
if (coordType.value === 'lnglat' && calcMethod.value === 'geodetic') {
|
||||
const coords = rawPoints.map(p => [p.lng, p.lat])
|
||||
coords.push(coords[0])
|
||||
const polygon = turf.polygon([coords])
|
||||
area.value = turf.area(polygon)
|
||||
perimeter.value = turf.length(polygon, { units: 'meters' })
|
||||
} else {
|
||||
// Simple planar area as fallback/comparison
|
||||
area.value = Math.abs(turf.area(polygon)) // Turf area is always geodetic, for true planar you'd need a custom formula
|
||||
// Note: Turf.js doesn't have a dedicated "planar" area for user-defined units,
|
||||
// but we can simulate it or just use geodetic as it's more accurate for surveying.
|
||||
let pts = [];
|
||||
if (coordType.value === 'planar') {
|
||||
// *** 关键改进:使用相对坐标偏移,防止大数计算丢失精度 ***
|
||||
const x0 = rawPoints[0].x;
|
||||
const y0 = rawPoints[0].y;
|
||||
pts = rawPoints.map(p => ({ x: p.x - x0, y: p.y - y0 }));
|
||||
} else {
|
||||
// Approximate LngLat to meters using Equirectangular projection at local latitude
|
||||
const latAvg = rawPoints.reduce((acc, cur) => acc + cur.lat, 0) / rawPoints.length;
|
||||
const radLat = latAvg * Math.PI / 180;
|
||||
const kx = 111319.49 * Math.cos(radLat);
|
||||
const ky = 111319.49;
|
||||
|
||||
// Use relative coordinates to avoid large number precision issues with degree calculation
|
||||
const lng0 = rawPoints[0].lng;
|
||||
const lat0 = rawPoints[0].lat;
|
||||
|
||||
pts = rawPoints.map(p => ({
|
||||
x: (p.lng - lng0) * kx,
|
||||
y: (p.lat - lat0) * ky
|
||||
}));
|
||||
}
|
||||
|
||||
if (pts.length < 3) {
|
||||
alert('顶点数量不足')
|
||||
return
|
||||
}
|
||||
const res = calculatePlanar(pts)
|
||||
area.value = res.area
|
||||
perimeter.value = res.perimeter
|
||||
}
|
||||
} catch (e) {
|
||||
alert('多边形无效,请检查坐标点顺序(需按顺时针或逆时针排列且不能自相交)')
|
||||
alert('计算失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user