生产管理系统前端 - 提交空间数据管理开发页面
This commit is contained in:
146
crop-x/src/lib/mapLoader.ts
Normal file
146
crop-x/src/lib/mapLoader.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 高德地图SDK动态加载器
|
||||
* 用于在不修改index.html的情况下加载高德地图SDK
|
||||
*/
|
||||
|
||||
// 高德地图配置
|
||||
const AMAP_CONFIG = {
|
||||
// 替换为你的高德地图API Key
|
||||
// 申请地址: https://console.amap.com/
|
||||
key: 'YOUR_AMAP_KEY',
|
||||
|
||||
// 替换为你的安全密钥(可选,用于提高安全性)
|
||||
securityJsCode: '',
|
||||
|
||||
// SDK版本
|
||||
version: '2.0',
|
||||
|
||||
// 可选插件
|
||||
plugins: ['AMap.Scale', 'AMap.ToolBar', 'AMap.Geocoder'] as string[],
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载高德地图SDK
|
||||
* @returns Promise<any> 返回AMap对象或null(占位模式)
|
||||
*/
|
||||
export const loadAMapScript = (): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 如果已经加载,直接返回
|
||||
if (window.AMap) {
|
||||
console.log('✅ 高德地图SDK已加载');
|
||||
resolve(window.AMap);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查Key是否配置
|
||||
if (AMAP_CONFIG.key === 'YOUR_AMAP_KEY' || !AMAP_CONFIG.key) {
|
||||
// 使用占位地图(功能完整)
|
||||
console.log('💡 使用占位地图模式(功能完整)');
|
||||
console.log('💡 如需真实地图,请在 /lib/mapLoader.ts 中配置高德地图Key');
|
||||
console.log('💡 申请地址: https://console.amap.com/');
|
||||
resolve(null); // 返回null表示使用占位地图
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 设置安全密钥(如果提供)
|
||||
if (AMAP_CONFIG.securityJsCode) {
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode: AMAP_CONFIG.securityJsCode,
|
||||
};
|
||||
}
|
||||
|
||||
// 创建script标签
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
|
||||
// 构建SDK URL
|
||||
let url = `https://webapi.amap.com/maps?v=${AMAP_CONFIG.version}&key=${AMAP_CONFIG.key}`;
|
||||
|
||||
// 添加插件
|
||||
if (AMAP_CONFIG.plugins.length > 0) {
|
||||
url += `&plugin=${AMAP_CONFIG.plugins.join(',')}`;
|
||||
}
|
||||
|
||||
script.src = url;
|
||||
|
||||
// 加载成功
|
||||
script.onload = () => {
|
||||
console.log('✅ 高德地图SDK加载成功');
|
||||
console.log('📍 版本:', window.AMap?.version);
|
||||
resolve(window.AMap);
|
||||
};
|
||||
|
||||
// 加载失败
|
||||
script.onerror = () => {
|
||||
console.error('❌ 高德地图SDK加载失败');
|
||||
reject(new Error('高德地图SDK加载失败'));
|
||||
};
|
||||
|
||||
// 添加到页面
|
||||
document.head.appendChild(script);
|
||||
|
||||
console.log('🔄 正在加载高德地图SDK...');
|
||||
} catch (error) {
|
||||
console.error('❌ 加载高德地图SDK时发生错误:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查高德地图SDK是否已加载
|
||||
* @returns boolean
|
||||
*/
|
||||
export const isAMapLoaded = (): boolean => {
|
||||
return typeof window !== 'undefined' && !!window.AMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取高德地图版本
|
||||
* @returns string | null
|
||||
*/
|
||||
export const getAMapVersion = (): string | null => {
|
||||
if (isAMapLoaded()) {
|
||||
return window.AMap.version || null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// TypeScript 类型声明
|
||||
declare global {
|
||||
interface Window {
|
||||
AMap: any;
|
||||
_AMapSecurityConfig: {
|
||||
securityJsCode: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用示例:
|
||||
*
|
||||
* import { loadAMapScript, isAMapLoaded } from './lib/mapLoader';
|
||||
*
|
||||
* // 在组件中使用
|
||||
* useEffect(() => {
|
||||
* if (!isAMapLoaded()) {
|
||||
* loadAMapScript()
|
||||
* .then((AMap) => {
|
||||
* if (AMap) {
|
||||
* console.log('地图SDK加载成功,可以初始化地图');
|
||||
* initMap();
|
||||
* } else {
|
||||
* console.log('使用占位地图模式');
|
||||
* }
|
||||
* })
|
||||
* .catch((error) => {
|
||||
* console.error('地图SDK加载失败,使用占位地图', error);
|
||||
* });
|
||||
* } else {
|
||||
* initMap();
|
||||
* }
|
||||
* }, []);
|
||||
*/
|
||||
|
||||
export {};
|
||||
937
crop-x/src/lib/spatialDataService.ts
Normal file
937
crop-x/src/lib/spatialDataService.ts
Normal file
@@ -0,0 +1,937 @@
|
||||
/**
|
||||
* 空间数据服务API
|
||||
* 提供PostGIS风格的空间查询、几何计算和数据导出功能
|
||||
*/
|
||||
|
||||
// ===== 类型定义 =====
|
||||
|
||||
export interface Point {
|
||||
lat: number;
|
||||
lng: number;
|
||||
alt?: number; // 海拔高度
|
||||
}
|
||||
|
||||
export interface Polygon {
|
||||
points: Point[];
|
||||
holes?: Point[][]; // 多边形的孔洞
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
geometry: Polygon;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface SpatialQueryResult<T = any> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
timestamp: string;
|
||||
executionTime: number; // 毫秒
|
||||
}
|
||||
|
||||
// ===== 常量定义 =====
|
||||
|
||||
// WGS-84椭球参数
|
||||
const WGS84_A = 6378137.0; // 长半轴(米)
|
||||
const WGS84_B = 6356752.314245; // 短半轴(米)
|
||||
const WGS84_F = 1 / 298.257223563; // 扁率
|
||||
|
||||
// 1亩 = 666.67平方米
|
||||
const MU_TO_SQUARE_METERS = 666.67;
|
||||
|
||||
// ===== 1. 空间查询API =====
|
||||
|
||||
/**
|
||||
* 点面查询:判断点是否在多边形内
|
||||
* 使用射线法(Ray Casting Algorithm)
|
||||
*/
|
||||
export class SpatialQuery {
|
||||
/**
|
||||
* 点在多边形内查询
|
||||
* @param point 查询点
|
||||
* @param fields 地块列表
|
||||
* @returns 包含该点的地块列表
|
||||
*/
|
||||
static pointInPolygon(point: Point, fields: Field[]): SpatialQueryResult<{
|
||||
matched: boolean;
|
||||
fields: Array<{
|
||||
field: Field;
|
||||
distanceToBorder: number; // 到边界的最短距离(米)
|
||||
}>;
|
||||
}> {
|
||||
const startTime = performance.now();
|
||||
const results: Array<{ field: Field; distanceToBorder: number }> = [];
|
||||
|
||||
for (const field of fields) {
|
||||
if (this._isPointInPolygon(point, field.geometry.points)) {
|
||||
const distance = this._pointToPolygonDistance(point, field.geometry.points);
|
||||
results.push({ field, distanceToBorder: distance });
|
||||
}
|
||||
}
|
||||
|
||||
const executionTime = performance.now() - startTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
matched: results.length > 0,
|
||||
fields: results,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
executionTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 多边形相交查询
|
||||
* @param sourceField 源地块
|
||||
* @param targetFields 目标地块列表
|
||||
* @returns 与源地块相交的地块列表
|
||||
*/
|
||||
static polygonIntersect(
|
||||
sourceField: Field,
|
||||
targetFields: Field[]
|
||||
): SpatialQueryResult<{
|
||||
intersections: Array<{
|
||||
field: Field;
|
||||
intersectArea: number; // 相交面积(亩)
|
||||
intersectRatio: number; // 相交比例(%)
|
||||
intersectGeometry: Polygon; // 相交区域几何
|
||||
}>;
|
||||
}> {
|
||||
const startTime = performance.now();
|
||||
const intersections: Array<{
|
||||
field: Field;
|
||||
intersectArea: number;
|
||||
intersectRatio: number;
|
||||
intersectGeometry: Polygon;
|
||||
}> = [];
|
||||
|
||||
const sourceArea = GeometryCalculator.calculateArea(sourceField.geometry);
|
||||
|
||||
for (const targetField of targetFields) {
|
||||
if (targetField.id === sourceField.id) continue;
|
||||
|
||||
if (this._polygonsIntersect(sourceField.geometry.points, targetField.geometry.points)) {
|
||||
const intersectGeometry = this._calculateIntersection(
|
||||
sourceField.geometry,
|
||||
targetField.geometry
|
||||
);
|
||||
const intersectArea = GeometryCalculator.calculateArea(intersectGeometry);
|
||||
const intersectRatio = (intersectArea / sourceArea) * 100;
|
||||
|
||||
intersections.push({
|
||||
field: targetField,
|
||||
intersectArea,
|
||||
intersectRatio,
|
||||
intersectGeometry,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const executionTime = performance.now() - startTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { intersections },
|
||||
timestamp: new Date().toISOString(),
|
||||
executionTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 相邻地块查询
|
||||
* @param sourceField 源地块
|
||||
* @param targetFields 目标地块列表
|
||||
* @returns 与源地块相邻的地块列表
|
||||
*/
|
||||
static adjacentPolygons(
|
||||
sourceField: Field,
|
||||
targetFields: Field[]
|
||||
): SpatialQueryResult<{
|
||||
adjacentFields: Array<{
|
||||
field: Field;
|
||||
sharedBorderLength: number; // 共享边界长度(米)
|
||||
sharedBorderPoints: Point[]; // 共享边界点
|
||||
}>;
|
||||
}> {
|
||||
const startTime = performance.now();
|
||||
const adjacentFields: Array<{
|
||||
field: Field;
|
||||
sharedBorderLength: number;
|
||||
sharedBorderPoints: Point[];
|
||||
}> = [];
|
||||
|
||||
for (const targetField of targetFields) {
|
||||
if (targetField.id === sourceField.id) continue;
|
||||
|
||||
const { isAdjacent, sharedBorder } = this._checkAdjacency(
|
||||
sourceField.geometry.points,
|
||||
targetField.geometry.points
|
||||
);
|
||||
|
||||
if (isAdjacent && sharedBorder.length > 0) {
|
||||
const sharedBorderLength = GeometryCalculator.calculatePerimeter({
|
||||
points: sharedBorder,
|
||||
});
|
||||
|
||||
adjacentFields.push({
|
||||
field: targetField,
|
||||
sharedBorderLength,
|
||||
sharedBorderPoints: sharedBorder,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const executionTime = performance.now() - startTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { adjacentFields },
|
||||
timestamp: new Date().toISOString(),
|
||||
executionTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓冲区分析
|
||||
* @param sourceField 源地块
|
||||
* @param bufferDistance 缓冲区距离(米)
|
||||
* @param targetFields 目标地块列表
|
||||
* @returns 缓冲区内的地块列表
|
||||
*/
|
||||
static bufferAnalysis(
|
||||
sourceField: Field,
|
||||
bufferDistance: number,
|
||||
targetFields: Field[]
|
||||
): SpatialQueryResult<{
|
||||
bufferGeometry: Polygon;
|
||||
bufferArea: number; // 缓冲区面积(亩)
|
||||
fieldsInBuffer: Array<{
|
||||
field: Field;
|
||||
distance: number; // 最短距离(米)
|
||||
overlap: boolean; // 是否重叠
|
||||
}>;
|
||||
}> {
|
||||
const startTime = performance.now();
|
||||
|
||||
// 生成缓冲区几何
|
||||
const bufferGeometry = this._createBuffer(sourceField.geometry, bufferDistance);
|
||||
const bufferArea = GeometryCalculator.calculateArea(bufferGeometry);
|
||||
|
||||
const fieldsInBuffer: Array<{
|
||||
field: Field;
|
||||
distance: number;
|
||||
overlap: boolean;
|
||||
}> = [];
|
||||
|
||||
for (const targetField of targetFields) {
|
||||
if (targetField.id === sourceField.id) continue;
|
||||
|
||||
const distance = this._polygonToPolygonDistance(
|
||||
sourceField.geometry.points,
|
||||
targetField.geometry.points
|
||||
);
|
||||
|
||||
if (distance <= bufferDistance) {
|
||||
const overlap = this._polygonsIntersect(
|
||||
bufferGeometry.points,
|
||||
targetField.geometry.points
|
||||
);
|
||||
fieldsInBuffer.push({
|
||||
field: targetField,
|
||||
distance,
|
||||
overlap,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const executionTime = performance.now() - startTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
bufferGeometry,
|
||||
bufferArea,
|
||||
fieldsInBuffer,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
executionTime,
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 私有辅助方法 =====
|
||||
|
||||
/**
|
||||
* 射线法判断点是否在多边形内
|
||||
*/
|
||||
private static _isPointInPolygon(point: Point, polygon: Point[]): boolean {
|
||||
let inside = false;
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
const xi = polygon[i].lng,
|
||||
yi = polygon[i].lat;
|
||||
const xj = polygon[j].lng,
|
||||
yj = polygon[j].lat;
|
||||
|
||||
const intersect =
|
||||
yi > point.lat !== yj > point.lat &&
|
||||
point.lng < ((xj - xi) * (point.lat - yi)) / (yj - yi) + xi;
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算点到多边形边界的最短距离
|
||||
*/
|
||||
private static _pointToPolygonDistance(point: Point, polygon: Point[]): number {
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
const p1 = polygon[i];
|
||||
const p2 = polygon[(i + 1) % polygon.length];
|
||||
const distance = this._pointToSegmentDistance(point, p1, p2);
|
||||
minDistance = Math.min(minDistance, distance);
|
||||
}
|
||||
|
||||
return minDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算点到线段的距离
|
||||
*/
|
||||
private static _pointToSegmentDistance(point: Point, p1: Point, p2: Point): number {
|
||||
const dx = p2.lng - p1.lng;
|
||||
const dy = p2.lat - p1.lat;
|
||||
|
||||
if (dx === 0 && dy === 0) {
|
||||
return GeometryCalculator.haversineDistance(point, p1);
|
||||
}
|
||||
|
||||
const t = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
1,
|
||||
((point.lng - p1.lng) * dx + (point.lat - p1.lat) * dy) / (dx * dx + dy * dy)
|
||||
)
|
||||
);
|
||||
|
||||
const nearestPoint: Point = {
|
||||
lat: p1.lat + t * dy,
|
||||
lng: p1.lng + t * dx,
|
||||
};
|
||||
|
||||
return GeometryCalculator.haversineDistance(point, nearestPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个多边形是否相交
|
||||
*/
|
||||
private static _polygonsIntersect(poly1: Point[], poly2: Point[]): boolean {
|
||||
// 检查是否有顶点在另一个多边形内
|
||||
for (const point of poly1) {
|
||||
if (this._isPointInPolygon(point, poly2)) return true;
|
||||
}
|
||||
for (const point of poly2) {
|
||||
if (this._isPointInPolygon(point, poly1)) return true;
|
||||
}
|
||||
|
||||
// 检查边是否相交
|
||||
for (let i = 0; i < poly1.length; i++) {
|
||||
const p1 = poly1[i];
|
||||
const p2 = poly1[(i + 1) % poly1.length];
|
||||
|
||||
for (let j = 0; j < poly2.length; j++) {
|
||||
const p3 = poly2[j];
|
||||
const p4 = poly2[(j + 1) % poly2.length];
|
||||
|
||||
if (this._segmentsIntersect(p1, p2, p3, p4)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两条线段是否相交
|
||||
*/
|
||||
private static _segmentsIntersect(p1: Point, p2: Point, p3: Point, p4: Point): boolean {
|
||||
const ccw = (A: Point, B: Point, C: Point) => {
|
||||
return (C.lat - A.lat) * (B.lng - A.lng) > (B.lat - A.lat) * (C.lng - A.lng);
|
||||
};
|
||||
|
||||
return ccw(p1, p3, p4) !== ccw(p2, p3, p4) && ccw(p1, p2, p3) !== ccw(p1, p2, p4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个多边形的相交区域(简化实现)
|
||||
*/
|
||||
private static _calculateIntersection(poly1: Polygon, poly2: Polygon): Polygon {
|
||||
// 这里使用简化算法,实际应用中应使用Sutherland-Hodgman算法
|
||||
const intersectPoints: Point[] = [];
|
||||
|
||||
// 收集在两个多边形内的点
|
||||
for (const point of poly1.points) {
|
||||
if (this._isPointInPolygon(point, poly2.points)) {
|
||||
intersectPoints.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
for (const point of poly2.points) {
|
||||
if (this._isPointInPolygon(point, poly1.points)) {
|
||||
intersectPoints.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有交点,返回空多边形
|
||||
if (intersectPoints.length === 0) {
|
||||
return { points: [] };
|
||||
}
|
||||
|
||||
// 计算凸包作为相交区域的近似
|
||||
return { points: this._convexHull(intersectPoints) };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算凸包(Graham扫描算法)
|
||||
*/
|
||||
private static _convexHull(points: Point[]): Point[] {
|
||||
if (points.length < 3) return points;
|
||||
|
||||
// 找到最下最左的点
|
||||
let start = points[0];
|
||||
points.forEach((p) => {
|
||||
if (p.lat < start.lat || (p.lat === start.lat && p.lng < start.lng)) {
|
||||
start = p;
|
||||
}
|
||||
});
|
||||
|
||||
// 按极角排序
|
||||
const sorted = points
|
||||
.filter((p) => p !== start)
|
||||
.sort((a, b) => {
|
||||
const angleA = Math.atan2(a.lat - start.lat, a.lng - start.lng);
|
||||
const angleB = Math.atan2(b.lat - start.lat, b.lng - start.lng);
|
||||
return angleA - angleB;
|
||||
});
|
||||
|
||||
const hull: Point[] = [start];
|
||||
|
||||
for (const point of sorted) {
|
||||
while (hull.length >= 2) {
|
||||
const p2 = hull[hull.length - 1];
|
||||
const p1 = hull[hull.length - 2];
|
||||
const cross =
|
||||
(p2.lng - p1.lng) * (point.lat - p1.lat) -
|
||||
(p2.lat - p1.lat) * (point.lng - p1.lng);
|
||||
if (cross <= 0) {
|
||||
hull.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
hull.push(point);
|
||||
}
|
||||
|
||||
return hull;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查两个多边形是否相邻
|
||||
*/
|
||||
private static _checkAdjacency(
|
||||
poly1: Point[],
|
||||
poly2: Point[]
|
||||
): { isAdjacent: boolean; sharedBorder: Point[] } {
|
||||
const sharedBorder: Point[] = [];
|
||||
const tolerance = 0.00001; // 约1米的容差
|
||||
|
||||
for (let i = 0; i < poly1.length; i++) {
|
||||
const p1 = poly1[i];
|
||||
const p2 = poly1[(i + 1) % poly1.length];
|
||||
|
||||
for (let j = 0; j < poly2.length; j++) {
|
||||
const p3 = poly2[j];
|
||||
const p4 = poly2[(j + 1) % poly2.length];
|
||||
|
||||
// 检查边是否重合
|
||||
if (this._edgesOverlap(p1, p2, p3, p4, tolerance)) {
|
||||
if (sharedBorder.length === 0 || !this._pointsEqual(sharedBorder[sharedBorder.length - 1], p1, tolerance)) {
|
||||
sharedBorder.push(p1);
|
||||
}
|
||||
sharedBorder.push(p2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isAdjacent: sharedBorder.length >= 2,
|
||||
sharedBorder,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两条边是否重合
|
||||
*/
|
||||
private static _edgesOverlap(
|
||||
p1: Point,
|
||||
p2: Point,
|
||||
p3: Point,
|
||||
p4: Point,
|
||||
tolerance: number
|
||||
): boolean {
|
||||
return (
|
||||
(this._pointsEqual(p1, p3, tolerance) && this._pointsEqual(p2, p4, tolerance)) ||
|
||||
(this._pointsEqual(p1, p4, tolerance) && this._pointsEqual(p2, p3, tolerance))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个点是否相等(在容差范围内)
|
||||
*/
|
||||
private static _pointsEqual(p1: Point, p2: Point, tolerance: number): boolean {
|
||||
return (
|
||||
Math.abs(p1.lat - p2.lat) < tolerance && Math.abs(p1.lng - p2.lng) < tolerance
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算多边形到多边形的最短距离
|
||||
*/
|
||||
private static _polygonToPolygonDistance(poly1: Point[], poly2: Point[]): number {
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const point of poly1) {
|
||||
const distance = this._pointToPolygonDistance(point, poly2);
|
||||
minDistance = Math.min(minDistance, distance);
|
||||
}
|
||||
|
||||
for (const point of poly2) {
|
||||
const distance = this._pointToPolygonDistance(point, poly1);
|
||||
minDistance = Math.min(minDistance, distance);
|
||||
}
|
||||
|
||||
return minDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建缓冲区(简化实现)
|
||||
*/
|
||||
private static _createBuffer(geometry: Polygon, distance: number): Polygon {
|
||||
const bufferPoints: Point[] = [];
|
||||
const points = geometry.points;
|
||||
|
||||
// 简化算法:对每个顶点,在法线方向上偏移distance距离
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const prev = points[i === 0 ? points.length - 1 : i - 1];
|
||||
const curr = points[i];
|
||||
const next = points[(i + 1) % points.length];
|
||||
|
||||
// 计算法向量
|
||||
const v1 = { lat: curr.lat - prev.lat, lng: curr.lng - prev.lng };
|
||||
const v2 = { lat: next.lat - curr.lat, lng: next.lng - curr.lng };
|
||||
|
||||
// 计算平均法向量
|
||||
const normal = {
|
||||
lat: -(v1.lng + v2.lng),
|
||||
lng: v1.lat + v2.lat,
|
||||
};
|
||||
|
||||
// 归一化
|
||||
const length = Math.sqrt(normal.lat * normal.lat + normal.lng * normal.lng);
|
||||
if (length > 0) {
|
||||
normal.lat /= length;
|
||||
normal.lng /= length;
|
||||
}
|
||||
|
||||
// 偏移顶点(简化:使用度数偏移,实际应转换为米)
|
||||
const offsetDegrees = distance / 111320; // 约111.32km每度
|
||||
bufferPoints.push({
|
||||
lat: curr.lat + normal.lat * offsetDegrees,
|
||||
lng: curr.lng + normal.lng * offsetDegrees,
|
||||
});
|
||||
}
|
||||
|
||||
return { points: bufferPoints };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 2. 几何计算API =====
|
||||
|
||||
export class GeometryCalculator {
|
||||
/**
|
||||
* 计算多边形精确面积(考虑地球曲率)
|
||||
* 使用球面三角形面积公式
|
||||
*/
|
||||
static calculateArea(geometry: Polygon): number {
|
||||
const points = geometry.points;
|
||||
if (points.length < 3) return 0;
|
||||
|
||||
// 将多边形分解为三角形,计算球面三角形面积之和
|
||||
let totalArea = 0;
|
||||
const origin = points[0];
|
||||
|
||||
for (let i = 1; i < points.length - 1; i++) {
|
||||
const area = this._sphericalTriangleArea(origin, points[i], points[i + 1]);
|
||||
totalArea += area;
|
||||
}
|
||||
|
||||
// 转换为亩
|
||||
return totalArea / MU_TO_SQUARE_METERS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算球面三角形面积(L'Huilier定理)
|
||||
*/
|
||||
private static _sphericalTriangleArea(p1: Point, p2: Point, p3: Point): number {
|
||||
const R = WGS84_A; // 使用WGS-84长半轴
|
||||
|
||||
// 转换为弧度
|
||||
const lat1 = this._toRadians(p1.lat);
|
||||
const lng1 = this._toRadians(p1.lng);
|
||||
const lat2 = this._toRadians(p2.lat);
|
||||
const lng2 = this._toRadians(p2.lng);
|
||||
const lat3 = this._toRadians(p3.lat);
|
||||
const lng3 = this._toRadians(p3.lng);
|
||||
|
||||
// 计算边长(球面距离)
|
||||
const a = this._sphericalDistance(lat2, lng2, lat3, lng3, R);
|
||||
const b = this._sphericalDistance(lat3, lng3, lat1, lng1, R);
|
||||
const c = this._sphericalDistance(lat1, lng1, lat2, lng2, R);
|
||||
|
||||
// 半周长
|
||||
const s = (a + b + c) / 2;
|
||||
|
||||
// L'Huilier定理
|
||||
const E = 4 * Math.atan(Math.sqrt(Math.tan(s / (2 * R)) * Math.tan((s - a) / (2 * R)) * Math.tan((s - b) / (2 * R)) * Math.tan((s - c) / (2 * R))));
|
||||
|
||||
// 面积 = R² * E
|
||||
return R * R * E;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算球面距离
|
||||
*/
|
||||
private static _sphericalDistance(
|
||||
lat1: number,
|
||||
lng1: number,
|
||||
lat2: number,
|
||||
lng2: number,
|
||||
radius: number
|
||||
): number {
|
||||
const dLat = lat2 - lat1;
|
||||
const dLng = lng2 - lng1;
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return radius * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算多边形周长(考虑地球曲率)
|
||||
*/
|
||||
static calculatePerimeter(geometry: Polygon): number {
|
||||
const points = geometry.points;
|
||||
if (points.length < 2) return 0;
|
||||
|
||||
let perimeter = 0;
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const p1 = points[i];
|
||||
const p2 = points[(i + 1) % points.length];
|
||||
perimeter += this.haversineDistance(p1, p2);
|
||||
}
|
||||
|
||||
return perimeter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Haversine公式计算两点间距离(米)
|
||||
*/
|
||||
static haversineDistance(p1: Point, p2: Point): number {
|
||||
const R = WGS84_A;
|
||||
|
||||
const lat1 = this._toRadians(p1.lat);
|
||||
const lng1 = this._toRadians(p1.lng);
|
||||
const lat2 = this._toRadians(p2.lat);
|
||||
const lng2 = this._toRadians(p2.lng);
|
||||
|
||||
return this._sphericalDistance(lat1, lng1, lat2, lng2, R);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算多边形中心点(几何中心)
|
||||
*/
|
||||
static calculateCentroid(geometry: Polygon): Point {
|
||||
const points = geometry.points;
|
||||
if (points.length === 0) return { lat: 0, lng: 0 };
|
||||
|
||||
let sumLat = 0;
|
||||
let sumLng = 0;
|
||||
let sumArea = 0;
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const p1 = points[i];
|
||||
const p2 = points[(i + 1) % points.length];
|
||||
|
||||
const cross = p1.lng * p2.lat - p2.lng * p1.lat;
|
||||
sumArea += cross;
|
||||
sumLat += (p1.lat + p2.lat) * cross;
|
||||
sumLng += (p1.lng + p2.lng) * cross;
|
||||
}
|
||||
|
||||
sumArea /= 2;
|
||||
|
||||
if (Math.abs(sumArea) < 1e-10) {
|
||||
// 如果面积接近0,使用简单平均
|
||||
const avgLat = points.reduce((sum, p) => sum + p.lat, 0) / points.length;
|
||||
const avgLng = points.reduce((sum, p) => sum + p.lng, 0) / points.length;
|
||||
return { lat: avgLat, lng: avgLng };
|
||||
}
|
||||
|
||||
const centroidLat = sumLat / (6 * sumArea);
|
||||
const centroidLng = sumLng / (6 * sumArea);
|
||||
|
||||
return { lat: centroidLat, lng: centroidLng };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算包围盒(Bounding Box)
|
||||
*/
|
||||
static calculateBoundingBox(geometry: Polygon): {
|
||||
minLat: number;
|
||||
maxLat: number;
|
||||
minLng: number;
|
||||
maxLng: number;
|
||||
center: Point;
|
||||
} {
|
||||
const points = geometry.points;
|
||||
if (points.length === 0) {
|
||||
return {
|
||||
minLat: 0,
|
||||
maxLat: 0,
|
||||
minLng: 0,
|
||||
maxLng: 0,
|
||||
center: { lat: 0, lng: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
let minLat = points[0].lat;
|
||||
let maxLat = points[0].lat;
|
||||
let minLng = points[0].lng;
|
||||
let maxLng = points[0].lng;
|
||||
|
||||
for (const point of points) {
|
||||
minLat = Math.min(minLat, point.lat);
|
||||
maxLat = Math.max(maxLat, point.lat);
|
||||
minLng = Math.min(minLng, point.lng);
|
||||
maxLng = Math.max(maxLng, point.lng);
|
||||
}
|
||||
|
||||
return {
|
||||
minLat,
|
||||
maxLat,
|
||||
minLng,
|
||||
maxLng,
|
||||
center: {
|
||||
lat: (minLat + maxLat) / 2,
|
||||
lng: (minLng + maxLng) / 2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 角度转弧度
|
||||
*/
|
||||
private static _toRadians(degrees: number): number {
|
||||
return (degrees * Math.PI) / 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* 弧度转角度
|
||||
*/
|
||||
private static _toDegrees(radians: number): number {
|
||||
return (radians * 180) / Math.PI;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 3. 数据导出API =====
|
||||
|
||||
export class DataExporter {
|
||||
/**
|
||||
* 导出为GeoJSON格式
|
||||
*/
|
||||
static exportToGeoJSON(fields: Field[]): string {
|
||||
const features = fields.map((field) => ({
|
||||
type: 'Feature',
|
||||
id: field.id,
|
||||
properties: {
|
||||
name: field.name,
|
||||
code: field.code,
|
||||
area: GeometryCalculator.calculateArea(field.geometry),
|
||||
perimeter: GeometryCalculator.calculatePerimeter(field.geometry),
|
||||
centroid: GeometryCalculator.calculateCentroid(field.geometry),
|
||||
...field.properties,
|
||||
},
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [field.geometry.points.map((p) => [p.lng, p.lat])],
|
||||
},
|
||||
}));
|
||||
|
||||
const geoJSON = {
|
||||
type: 'FeatureCollection',
|
||||
crs: {
|
||||
type: 'name',
|
||||
properties: {
|
||||
name: 'EPSG:4326', // WGS-84
|
||||
},
|
||||
},
|
||||
features,
|
||||
};
|
||||
|
||||
return JSON.stringify(geoJSON, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出为KML格式
|
||||
*/
|
||||
static exportToKML(fields: Field[]): string {
|
||||
const placemarks = fields
|
||||
.map((field) => {
|
||||
const coords = field.geometry.points.map((p) => `${p.lng},${p.lat},0`).join(' ');
|
||||
|
||||
return `
|
||||
<Placemark>
|
||||
<name>${field.name}</name>
|
||||
<description>
|
||||
编号: ${field.code}
|
||||
面积: ${GeometryCalculator.calculateArea(field.geometry).toFixed(2)} 亩
|
||||
周长: ${GeometryCalculator.calculatePerimeter(field.geometry).toFixed(0)} 米
|
||||
</description>
|
||||
<Polygon>
|
||||
<outerBoundaryIs>
|
||||
<LinearRing>
|
||||
<coordinates>${coords}</coordinates>
|
||||
</LinearRing>
|
||||
</outerBoundaryIs>
|
||||
</Polygon>
|
||||
</Placemark>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
<name>地块数据</name>
|
||||
<description>智慧农业生产管理系统地块导出</description>
|
||||
${placemarks}
|
||||
</Document>
|
||||
</kml>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出为Shapefile格式(WKT格式)
|
||||
*/
|
||||
static exportToWKT(field: Field): string {
|
||||
const coords = field.geometry.points.map((p) => `${p.lng} ${p.lat}`).join(', ');
|
||||
return `POLYGON((${coords}))`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出为CSV格式
|
||||
*/
|
||||
static exportToCSV(fields: Field[]): string {
|
||||
const headers = ['ID', '名称', '编号', '面积(亩)', '周长(米)', '中心点纬度', '中心点经度'];
|
||||
const rows = fields.map((field) => {
|
||||
const centroid = GeometryCalculator.calculateCentroid(field.geometry);
|
||||
return [
|
||||
field.id,
|
||||
field.name,
|
||||
field.code,
|
||||
GeometryCalculator.calculateArea(field.geometry).toFixed(2),
|
||||
GeometryCalculator.calculatePerimeter(field.geometry).toFixed(0),
|
||||
centroid.lat.toFixed(6),
|
||||
centroid.lng.toFixed(6),
|
||||
];
|
||||
});
|
||||
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...rows.map((row) => row.join(',')),
|
||||
].join('\n');
|
||||
|
||||
return csvContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
static downloadFile(content: string, filename: string, mimeType: string): void {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 4. 空间索引(用于性能优化) =====
|
||||
|
||||
export class SpatialIndex {
|
||||
private rtree: Map<string, { bbox: any; field: Field }>;
|
||||
|
||||
constructor() {
|
||||
this.rtree = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入地块
|
||||
*/
|
||||
insert(field: Field): void {
|
||||
const bbox = GeometryCalculator.calculateBoundingBox(field.geometry);
|
||||
this.rtree.set(field.id, { bbox, field });
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速查询可能相交的地块
|
||||
*/
|
||||
query(bbox: {
|
||||
minLat: number;
|
||||
maxLat: number;
|
||||
minLng: number;
|
||||
maxLng: number;
|
||||
}): Field[] {
|
||||
const results: Field[] = [];
|
||||
|
||||
for (const [_, item] of this.rtree) {
|
||||
if (this._bboxesIntersect(bbox, item.bbox)) {
|
||||
results.push(item.field);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个包围盒是否相交
|
||||
*/
|
||||
private _bboxesIntersect(
|
||||
bbox1: { minLat: number; maxLat: number; minLng: number; maxLng: number },
|
||||
bbox2: { minLat: number; maxLat: number; minLng: number; maxLng: number }
|
||||
): boolean {
|
||||
return !(
|
||||
bbox1.maxLat < bbox2.minLat ||
|
||||
bbox1.minLat > bbox2.maxLat ||
|
||||
bbox1.maxLng < bbox2.minLng ||
|
||||
bbox1.minLng > bbox2.maxLng
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user