274 lines
7.2 KiB
TypeScript
274 lines
7.2 KiB
TypeScript
import { GeoFence, GeoFenceAlert } from '../types/equipment';
|
||
|
||
/**
|
||
* 生成测试用的电子围栏数据
|
||
*/
|
||
export function generateTestGeoFences(machineryIds: string[]): GeoFence[] {
|
||
const now = new Date().toISOString();
|
||
|
||
return [
|
||
// 矩形作业区
|
||
{
|
||
id: 'fence-test-1',
|
||
name: '1号作业区',
|
||
type: 'polygon',
|
||
points: [
|
||
{ latitude: 36.6512, longitude: 117.1201 },
|
||
{ latitude: 36.6532, longitude: 117.1201 },
|
||
{ latitude: 36.6532, longitude: 117.1251 },
|
||
{ latitude: 36.6512, longitude: 117.1251 },
|
||
],
|
||
machineryIds: machineryIds.slice(0, 2),
|
||
alertOnExit: true,
|
||
alertOnEnter: false,
|
||
countWorkHours: true,
|
||
enabled: true,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
createdBy: '系统管理员',
|
||
},
|
||
|
||
// 圆形禁入区
|
||
{
|
||
id: 'fence-test-2',
|
||
name: '东侧水塘禁入区',
|
||
type: 'circle',
|
||
center: { latitude: 36.6500, longitude: 117.1200 },
|
||
radius: 200,
|
||
machineryIds: machineryIds,
|
||
alertOnExit: false,
|
||
alertOnEnter: true,
|
||
countWorkHours: false,
|
||
enabled: true,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
createdBy: '系统管理员',
|
||
},
|
||
|
||
// 大型多边形作业区
|
||
{
|
||
id: 'fence-test-3',
|
||
name: '2号地块-西区',
|
||
type: 'polygon',
|
||
points: [
|
||
{ latitude: 36.6480, longitude: 117.1150 },
|
||
{ latitude: 36.6500, longitude: 117.1150 },
|
||
{ latitude: 36.6510, longitude: 117.1180 },
|
||
{ latitude: 36.6500, longitude: 117.1200 },
|
||
{ latitude: 36.6480, longitude: 117.1190 },
|
||
],
|
||
machineryIds: machineryIds.slice(0, 1),
|
||
alertOnExit: true,
|
||
alertOnEnter: false,
|
||
countWorkHours: true,
|
||
enabled: true,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
createdBy: '系统管理员',
|
||
},
|
||
|
||
// 休息区(圆形)
|
||
{
|
||
id: 'fence-test-4',
|
||
name: '农机停放区',
|
||
type: 'circle',
|
||
center: { latitude: 36.6550, longitude: 117.1100 },
|
||
radius: 100,
|
||
machineryIds: machineryIds,
|
||
alertOnExit: false,
|
||
alertOnEnter: false,
|
||
countWorkHours: false,
|
||
enabled: true,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
createdBy: '系统管理员',
|
||
},
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 生成测试用的围栏报警数据
|
||
*/
|
||
export function generateTestGeoFenceAlerts(
|
||
fences: GeoFence[],
|
||
machineryData: { id: string; name: string }[]
|
||
): GeoFenceAlert[] {
|
||
const alerts: GeoFenceAlert[] = [];
|
||
|
||
// 生成一些历史报警
|
||
if (fences.length > 0 && machineryData.length > 0) {
|
||
const fence1 = fences[0];
|
||
const machinery1 = machineryData[0];
|
||
|
||
alerts.push({
|
||
id: 'alert-test-1',
|
||
fenceId: fence1.id,
|
||
fenceName: fence1.name,
|
||
machineryId: machinery1.id,
|
||
machineryName: machinery1.name,
|
||
alertType: 'exit',
|
||
location: { latitude: 36.6510, longitude: 117.1252 },
|
||
alertedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), // 2小时前
|
||
acknowledged: false,
|
||
});
|
||
}
|
||
|
||
if (fences.length > 1 && machineryData.length > 1) {
|
||
const fence2 = fences[1];
|
||
const machinery2 = machineryData[1];
|
||
|
||
alerts.push({
|
||
id: 'alert-test-2',
|
||
fenceId: fence2.id,
|
||
fenceName: fence2.name,
|
||
machineryId: machinery2.id,
|
||
machineryName: machinery2.name,
|
||
alertType: 'enter',
|
||
location: { latitude: 36.6501, longitude: 117.1201 },
|
||
alertedAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(), // 30分钟前
|
||
acknowledged: true,
|
||
acknowledgedBy: '值班员张三',
|
||
acknowledgedAt: new Date(Date.now() - 25 * 60 * 1000).toISOString(),
|
||
});
|
||
}
|
||
|
||
return alerts;
|
||
}
|
||
|
||
/**
|
||
* 验证围栏坐标是否有效
|
||
*/
|
||
export function validateGeoFence(fence: Partial<GeoFence>): { valid: boolean; errors: string[] } {
|
||
const errors: string[] = [];
|
||
|
||
if (!fence.name || fence.name.trim() === '') {
|
||
errors.push('围栏名称不能为空');
|
||
}
|
||
|
||
if (!fence.type) {
|
||
errors.push('必须选择围栏类型');
|
||
}
|
||
|
||
if (fence.type === 'circle') {
|
||
if (!fence.center) {
|
||
errors.push('圆形围栏必须设置中心点');
|
||
} else {
|
||
if (!isValidLatitude(fence.center.latitude)) {
|
||
errors.push('中心点纬度无效(应在-90到90之间)');
|
||
}
|
||
if (!isValidLongitude(fence.center.longitude)) {
|
||
errors.push('中心点经度无效(应在-180到180之间)');
|
||
}
|
||
}
|
||
|
||
if (!fence.radius || fence.radius <= 0) {
|
||
errors.push('圆形围栏半径必须大于0');
|
||
}
|
||
}
|
||
|
||
if (fence.type === 'polygon') {
|
||
if (!fence.points || fence.points.length < 3) {
|
||
errors.push('多边形围栏至少需要3个顶点');
|
||
} else {
|
||
fence.points.forEach((point, index) => {
|
||
if (!isValidLatitude(point.latitude)) {
|
||
errors.push(`顶点${index + 1}的纬度无效`);
|
||
}
|
||
if (!isValidLongitude(point.longitude)) {
|
||
errors.push(`顶点${index + 1}的经度无效`);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
if (!fence.machineryIds || fence.machineryIds.length === 0) {
|
||
errors.push('必须至少关联一台农机');
|
||
}
|
||
|
||
if (!fence.alertOnEnter && !fence.alertOnExit) {
|
||
errors.push('建议至少启用一种报警方式(进入或离开)');
|
||
}
|
||
|
||
return {
|
||
valid: errors.length === 0,
|
||
errors,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 验证纬度是否有效
|
||
*/
|
||
function isValidLatitude(lat: number): boolean {
|
||
return !isNaN(lat) && lat >= -90 && lat <= 90;
|
||
}
|
||
|
||
/**
|
||
* 验证经度是否有效
|
||
*/
|
||
function isValidLongitude(lng: number): boolean {
|
||
return !isNaN(lng) && lng >= -180 && lng <= 180;
|
||
}
|
||
|
||
/**
|
||
* 计算两点之间的距离(米)
|
||
* 使用 Haversine 公式
|
||
*/
|
||
export function calculateDistance(
|
||
point1: { latitude: number; longitude: number },
|
||
point2: { latitude: number; longitude: number }
|
||
): number {
|
||
const R = 6371000; // 地球半径,米
|
||
const lat1 = (point1.latitude * Math.PI) / 180;
|
||
const lat2 = (point2.latitude * Math.PI) / 180;
|
||
const deltaLat = ((point2.latitude - point1.latitude) * Math.PI) / 180;
|
||
const deltaLng = ((point2.longitude - point1.longitude) * Math.PI) / 180;
|
||
|
||
const a =
|
||
Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
|
||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2);
|
||
|
||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||
|
||
return R * c;
|
||
}
|
||
|
||
/**
|
||
* 判断点是否在圆形围栏内
|
||
*/
|
||
export function isPointInCircle(
|
||
point: { latitude: number; longitude: number },
|
||
center: { latitude: number; longitude: number },
|
||
radius: number
|
||
): boolean {
|
||
const distance = calculateDistance(point, center);
|
||
return distance <= radius;
|
||
}
|
||
|
||
/**
|
||
* 判断点是否在多边形围栏内
|
||
* 使用射线法(Ray Casting Algorithm)
|
||
*/
|
||
export function isPointInPolygon(
|
||
point: { latitude: number; longitude: number },
|
||
polygon: { latitude: number; longitude: number }[]
|
||
): boolean {
|
||
let inside = false;
|
||
const x = point.longitude;
|
||
const y = point.latitude;
|
||
|
||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||
const xi = polygon[i].longitude;
|
||
const yi = polygon[i].latitude;
|
||
const xj = polygon[j].longitude;
|
||
const yj = polygon[j].latitude;
|
||
|
||
const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
||
|
||
if (intersect) {
|
||
inside = !inside;
|
||
}
|
||
}
|
||
|
||
return inside;
|
||
}
|