@fleet-frontend/mower-maps
Version:
a mower maps in google maps
1,556 lines (1,547 loc) • 908 kB
JavaScript
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import * as React from 'react';
import React__default, { memo, useMemo, useEffect, useState, useRef, useCallback, createContext, useContext, forwardRef, useImperativeHandle, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom';
/**
* 常量和枚举类型定义
*/
/**
* 机器人状态枚举
*/
var RobotStatus;
(function (RobotStatus) {
RobotStatus[RobotStatus["PARKED"] = 1] = "PARKED";
RobotStatus[RobotStatus["CHARGING"] = 2] = "CHARGING";
RobotStatus[RobotStatus["STANDBY"] = 3] = "STANDBY";
RobotStatus[RobotStatus["MOWING"] = 4] = "MOWING";
RobotStatus[RobotStatus["WORKING"] = 5] = "WORKING";
RobotStatus[RobotStatus["MAPPING"] = 6] = "MAPPING";
RobotStatus[RobotStatus["ERROR"] = 7] = "ERROR";
RobotStatus[RobotStatus["UPGRADING"] = 8] = "UPGRADING";
RobotStatus[RobotStatus["DISCONNECTED"] = 9] = "DISCONNECTED";
RobotStatus[RobotStatus["UNKNOWN"] = -1] = "UNKNOWN";
RobotStatus[RobotStatus["TASK_DELAY"] = 10] = "TASK_DELAY";
})(RobotStatus || (RobotStatus = {}));
/**
* RTK状态枚举
*/
var RTK_STATE;
(function (RTK_STATE) {
RTK_STATE[RTK_STATE["LOW_RTK"] = 1] = "LOW_RTK";
RTK_STATE[RTK_STATE["MIDDLE_RTK"] = 2] = "MIDDLE_RTK";
RTK_STATE[RTK_STATE["HIGH_RTK"] = 3] = "HIGH_RTK";
RTK_STATE[RTK_STATE["NO_POSTURE"] = 10] = "NO_POSTURE";
RTK_STATE[RTK_STATE["OUT_OF_RANGE"] = 11] = "OUT_OF_RANGE";
RTK_STATE[RTK_STATE["OFF_LINE"] = 19] = "OFF_LINE";
})(RTK_STATE || (RTK_STATE = {}));
/**
* 实时数据类型枚举
*/
var REAL_TIME_DATA_TYPE;
(function (REAL_TIME_DATA_TYPE) {
REAL_TIME_DATA_TYPE[REAL_TIME_DATA_TYPE["LOCATION"] = 1] = "LOCATION";
REAL_TIME_DATA_TYPE[REAL_TIME_DATA_TYPE["PROCESS"] = 2] = "PROCESS";
})(REAL_TIME_DATA_TYPE || (REAL_TIME_DATA_TYPE = {}));
/**
* 地图渲染相关常量配置
*/
// 默认坐标
const DEFAULT_COORDINATES = {
sw: [116.35497283935547, 40.0374755859375],
ne: [116.35584259033203, 40.038658142089844],
};
/**
* 缩放因子 - 将米转换为像素
*/
const SCALE_FACTOR = 50; // 50像素/米
/**
* 默认线宽设置
*/
const DEFAULT_LINE_WIDTHS = {
BOUNDARY: 3,
OBSTACLE: 3,
CHARGING_PILE: 3,
CHANNEL: 3,
PATH: 20,
VISION_OFF_AREA: 3,
TIME_LIMIT_OBSTACLE: 1,
};
/**
* 默认透明度设置
*/
const DEFAULT_OPACITIES = {
FULL: 1.0,
HIGH: 0.7,
MEDIUM: 0.6,
DOODLE: 0.8};
/**
* 默认半径设置
*/
const DEFAULT_RADII = {
CHARGING_PILE: 12};
/**
* 图层等级
*/
const LAYER_LEVELS = {
BOUNDARY: 2,
BOUNDARY_BORDER: 4};
/**
* 图层默认id
*/
const LAYER_DEFAULT_TYPE = {
CHANNEL: 'channel',
BOUNDARY: 'boundary',
PATH: 'path',
BOUNDARY_BORDER: 'boundary_border',
OBSTACLE: 'obstacle',
CHARGING_PILE: 'charging_pile',
POINT: 'point',
SVG: 'svg',
VISION_OFF_AREA: 'vision_off_area',
ANTENNA: 'antenna',
MOW_GATE: 'mow_gate',
};
/**
* 遍历割草任务,下述四个字段可以在路径中唯一确定遍历的位置(当前区域、当前块、当前行、在当前行上的路程)
*/
const ACTION_BLOCK_COVER = 5;
/**
* 遍历割草块转移任务(在同一区域中的一个块转移到下一个块),下述四个字段可以在路径中唯一确定转移的位置(当前区域、前置块、当前转移路径线序号、在当前线上的路程)
*/
const ACTION_BLOCK_TRANSFER = 6;
/**
* 边界割草任务(割草任务内部的巡边任务)
*/
const ACTION_BOUNDARY_TASK = 8;
const SVG_MAP_VIEW_ID = 'fleet-maps-svg-map-view';
const ALL_DIRECTION_SELECTED = 63;
const MIN_DIRECTION_ANGLE = -14;
const MAX_DIRECTION_ANGLE = 15;
/**
* 数据类型枚举
*/
var DataType;
(function (DataType) {
DataType["BOUNDARY"] = "boundary";
DataType["CHANNEL"] = "channel";
DataType["OBSTACLE"] = "obstacle";
DataType["PATH"] = "path";
DataType["DOODLE"] = "doodle";
DataType["VISION_OFF"] = "visionOff";
DataType["ANTENNA"] = "antenna";
DataType["CHARGING_PILE"] = "chargingPile";
})(DataType || (DataType = {}));
/**
* 渲染类型枚举
*/
var RenderType;
(function (RenderType) {
RenderType["POINT"] = "point";
RenderType["POLYGON"] = "polygon";
RenderType["LINE"] = "line";
RenderType["SVG"] = "svg";
})(RenderType || (RenderType = {}));
/**
* 基础数据类
*/
class BaseData {
constructor(id, type, level, renderType, points, originalData) {
this.id = id;
this.type = type;
this.level = level;
this.renderType = renderType;
this.points = points;
this.originalData = originalData;
}
}
/**
* 边界数据类
*/
class BoundaryData extends BaseData {
constructor(originalData, level, channels, style) {
const convertedPoints = convertPointsFormat(originalData?.points) || [];
super(originalData?.id, DataType.BOUNDARY, level, RenderType.POLYGON, convertedPoints, originalData);
const { isFlowGlobal, cuttingHeight } = convertHeightsetToParams(originalData?.height_set || 0);
this.area = originalData?.area;
this.name = originalData?.name;
this.cuttingHeight = cuttingHeight || 0;
this.isFlowGlobal = isFlowGlobal || 0;
this.obstacleMowEdge = originalData?.obstacle_mow_edge || 0;
this.edgeMowing = originalData?.mow_edge || 0;
this.edgeVision = originalData?.edge_vf || 0;
this.direction = originalData?.avai_segs || 0;
this.angle = originalData?.base_angle || -14;
this.recBaseAngle = originalData?.rec_base_angle;
this.channels = channels;
this.style = style;
}
}
/**
* 通道数据类
*/
class ChannelData extends BaseData {
constructor(originalData, level, connectedBoundaries, isChargingPileTunnel, style) {
const convertedPoints = convertPointsFormat(originalData?.points) || [];
super(originalData?.id, DataType.CHANNEL, level, RenderType.LINE, convertedPoints, originalData);
this.connectedBoundaries = connectedBoundaries;
this.isChargingPileTunnel = isChargingPileTunnel;
this.style = style;
}
}
/**
* 障碍物数据类
*/
class ObstacleData extends BaseData {
constructor(originalData, level, style) {
const convertedPoints = convertPointsFormat(originalData?.points) || [];
super(originalData?.id, DataType.OBSTACLE, level, RenderType.POLYGON, convertedPoints, originalData);
this.area = originalData?.area;
this.status = originalData?.status ?? 1;
this.start_timestamp = originalData?.start_timestamp;
this.end_timestamp = originalData?.end_timestamp;
this.name = originalData?.name || 'Noname';
this.style = style;
}
}
/**
* 路径数据类
*/
class PathData extends BaseData {
constructor(originalData, level, pathType, style) {
const convertedPoints = convertPointsFormat(originalData?.points) || [];
super(originalData?.id, DataType.PATH, level, RenderType.LINE, convertedPoints, originalData);
this.pathType = pathType;
this.style = style;
}
}
/**
* 涂鸦数据类
*/
class DoodleData extends BaseData {
constructor(originalData, level, style) {
const center = originalData?.center || [0, 0];
super(originalData?.id, DataType.DOODLE, level, RenderType.SVG, [center], originalData);
this.svg = originalData?.svg || '';
this.scale = originalData?.scale || 1;
this.direction = originalData?.direction || 0;
this.center = center;
this.style = style;
this.create_ts = originalData?.create_ts || Date.now();
this.expiration_ts = originalData?.expiration_ts || Date.now() + 3600000;
}
}
/**
* 视觉盲区数据类
*/
class VisionOffData extends BaseData {
constructor(originalData, level, style) {
const convertedPoints = convertPointsFormat(originalData?.points) || [];
super(originalData?.id, DataType.VISION_OFF, level, RenderType.POLYGON, convertedPoints, originalData);
this.style = style;
}
}
/**
* 天线数据类
*/
class AntennaData extends BaseData {
constructor(originalData, level, size, style) {
const position = originalData?.position || [0, 0];
super(originalData?.id, DataType.ANTENNA, level, RenderType.POINT, [position], originalData);
this.antennaId = originalData?.antennaId || 0;
this.online = originalData?.online || false;
this.size = size;
this.style = style;
}
}
/**
* 充电桩数据类
*/
class ChargingPileData extends BaseData {
constructor(originalData, level, tunnel, style) {
const position = originalData?.position || [0, 0];
super(originalData?.id, DataType.CHARGING_PILE, level, RenderType.POINT, [position], originalData);
this.position = position;
this.direction = originalData?.direction || 0;
this.width = originalData?.width || 0;
this.length = originalData?.length || 0;
this.navPos = originalData?.nav_pos;
this.tunnel = tunnel;
this.style = style;
}
}
/**
* 初始化boundary
*/
function initBoundary() {
const boundary = {
id: null,
area: 0,
name: 'Zone',
avai_segs: 0,
base_angle: 0,
edge_vf: 0,
height_set: 0,
mow_edge: 0,
obstacle_mow_edge: 0,
points: [],
rec_base_angle: 0,
type: 'BOUNDARY',
};
return new BoundaryData(boundary, 100);
}
function initObstacle() {
const obstacle = {
id: null,
area: 0,
name: 'Noname',
status: 1,
end_timestamp: 0,
start_timestamp: 0,
points: [],
type: 'OBSTACLE',
};
return new ObstacleData(obstacle, 100);
}
function initChannel() {
const channel = {
id: null,
name: '',
connection: [0, 0],
points: [],
type: 'TUNNEL',
};
return new ChannelData(channel, 100);
}
function initVisionOff(points) {
const visionOff = {
id: null,
name: '',
points: points || [],
type: 'VISION_OFF_AREA',
};
return new VisionOffData(visionOff, 100);
}
function initDoodle() {
const doodle = {
id: null,
name: '',
center: [0, 0],
svg: '',
scale: 1,
direction: 0,
create_ts: Math.floor(Date.now() / 1000),
expiration_ts: Math.floor(Date.now() / 1000) + 3600 * 24 * 7, // 1小时后过期
type: 'TIME_LIMIT_OBSTACLE',
};
return new DoodleData(doodle, 100);
}
/**
* 统一地图数据处理器
* 负责将原始的地图数据转换为统一的数据结构
*/
class UnifiedMapDataProcessor {
/**
* 处理地图数据
* @param mapData 地图数据
* @param mapConfig 地图配置
*/
static processMapData(mapData, mapConfig) {
this.mapConfig = mapConfig;
// 收集所有地图元素
const allElements = this.collectAllElements(mapData);
return this.processElements(allElements, mapConfig);
}
/**
* 处理非结构地图数据
* @param mapData 地图数据
* @param mapConfig 地图配置
*/
static processUnstructMapData(mapData, mapConfig) {
this.mapConfig = mapConfig;
// 收集所有地图元素
const allElements = mapData.elements;
return this.processElements(allElements, mapConfig);
}
static processElements(allElements, mapConfig) {
this.mapConfig = mapConfig;
// 处理所有元素
const processedData = {};
// 按类型分组元素
const elementsByType = {
TIME_LIMIT_OBSTACLE: [],
VISION_OFF_AREA: [],
CHARGING_PILE: [],
OBSTACLE: [],
BOUNDARY: [],
TUNNEL: [],
TUNNEL_TO_CHARGING_PILE: [],
};
// 分组所有元素
for (const element of allElements) {
if (elementsByType[element.type]) {
elementsByType[element.type].push(element);
}
}
// 处理通道
if (elementsByType.TUNNEL.length > 0) {
processedData[DataType.CHANNEL] = processedData[DataType.CHANNEL] || [];
processedData[DataType.CHANNEL].push(...this.processChannelElements(elementsByType.TUNNEL));
}
// 处理边界
if (elementsByType.BOUNDARY.length > 0) {
processedData[DataType.BOUNDARY] = this.processBoundaryElements(elementsByType.BOUNDARY);
}
// 处理障碍物
if (elementsByType.OBSTACLE.length > 0) {
processedData[DataType.OBSTACLE] = this.processObstacleElements(elementsByType.OBSTACLE);
}
// 处理充电桩
if (elementsByType.CHARGING_PILE.length > 0) {
processedData[DataType.CHARGING_PILE] = [];
processedData[DataType.CHANNEL] = processedData[DataType.CHANNEL] || [];
const { chargingPileData, channelData } = this.processChargingPileElements(elementsByType.CHARGING_PILE);
processedData[DataType.CHARGING_PILE].push(...chargingPileData);
processedData[DataType.CHANNEL].push(...channelData);
}
// 处理时间限制障碍物
if (elementsByType.TIME_LIMIT_OBSTACLE.length > 0) {
const { obstacleData, doodleData } = this.processTimeLimitObstacleElements(elementsByType.TIME_LIMIT_OBSTACLE);
if (obstacleData.length > 0) {
processedData[DataType.OBSTACLE] = processedData[DataType.OBSTACLE] || [];
processedData[DataType.OBSTACLE].push(...obstacleData);
}
if (doodleData.length > 0) {
processedData[DataType.DOODLE] = doodleData;
}
}
// 处理视觉盲区
if (elementsByType.VISION_OFF_AREA.length > 0) {
processedData[DataType.VISION_OFF] = this.processVisionOffElements(elementsByType.VISION_OFF_AREA);
}
// 建立数据之间的关联关系
this.establishRelationships(processedData);
return processedData;
}
/**
* 处理路径数据
* @param pathData 路径数据
* @param mapConfig 地图配置
*/
static processPathData(pathData, mapConfig) {
if (!pathData || !mapConfig) {
return {};
}
const processedData = {};
const pathDataList = [];
Object.keys(pathData).forEach((key) => {
const id = Number(key);
const points = pathData[key].points || [];
// 使用Python相同的逻辑:按线段分组而不是按点分组
const pathSegments = this.createPathSegmentsByType(points);
// 处理边缘路径段
for (const segment of pathSegments.edge) {
// 转换 Point[] 为 number[][]
const segmentPoints = segment.points.map((point) => [point.x, point.y]);
pathDataList.push(new PathData({
id: Number(id),
name: `Edge Path ${pathDataList.length}`,
type: 'PATH',
points: segmentPoints,
}, 100, // 中等层级
'EDGE', {
lineColor: mapConfig.path.edgeLineColor,
lineWidth: mapConfig.path.lineWidth,
opacity: mapConfig.path.opacity,
}));
}
// 处理割草路径段
for (const segment of pathSegments.mowing) {
// 转换 Point[] 为 number[][]
const segmentPoints = segment.points.map((point) => [point.x, point.y]);
pathDataList.push(new PathData({
id: Number(id),
name: `Mowing Path ${pathDataList.length}`,
type: 'PATH',
points: segmentPoints,
}, 100, // 中等层级
'MOWING', {
lineColor: mapConfig.path.mowedLineColor,
lineWidth: mapConfig.path.lineWidth,
opacity: mapConfig.path.opacity,
}));
}
// 处理传输路径段
for (const segment of pathSegments.trans) {
// 转换 Point[] 为 number[][]
const segmentPoints = segment.points.map((point) => [point.x, point.y]);
pathDataList.push(new PathData({
id: Number(id),
name: `Trans Path ${pathDataList.length}`,
type: 'PATH',
points: segmentPoints,
}, 100, // 中等层级
'TRANS', {
lineColor: mapConfig.path.transLineColor,
lineWidth: mapConfig.path.lineWidth,
opacity: mapConfig.path.opacity,
}));
}
});
if (pathDataList.length > 0) {
processedData[DataType.PATH] = pathDataList;
}
return processedData;
}
/**
* 处理天线数据
* @param antennas 天线数据
* @param antennaConfig 天线配置
*/
static processAntennaData(antennas, antennaConfig) {
if (!antennas || antennas.length === 0) {
return {};
}
const processedData = {};
const antennaDataList = [];
for (const antenna of antennas) {
if (!antenna || !antenna.position || !Array.isArray(antenna.position)) {
continue;
}
// 天线1
if (antennaConfig.antennaOneStatus !== false) {
antennaDataList.push(new AntennaData({
id: Number(antenna.id),
name: `Antenna ${antenna.id}_1`,
type: 'ANTENNA',
position: antenna.position,
antennaId: 1,
online: antenna.antenna_one_status === 1,
}, 200, // 最高层级
antennaConfig.size, {
fillColor: antennaConfig.fillColor,
strokeColor: antennaConfig.strokeColor,
opacity: antennaConfig.opacity,
}));
}
// 天线2
if (antennaConfig.antennaTwoStatus !== false) {
antennaDataList.push(new AntennaData({
id: Number(antenna.id),
name: `Antenna ${antenna.id}_2`,
type: 'ANTENNA',
position: antenna.position,
antennaId: 2,
online: antenna.antenna_two_status === 1,
}, 200, // 最高层级
antennaConfig.size, {
fillColor: antennaConfig.fillColor,
strokeColor: antennaConfig.strokeColor,
opacity: antennaConfig.opacity,
}));
}
}
if (antennaDataList.length > 0) {
processedData[DataType.ANTENNA] = antennaDataList;
}
return processedData;
}
/**
* 将处理后的数据转换为绘制元素
* @param processedData 处理后的数据
*/
static toDrawElements(processedData) {
const drawElements = [];
// 定义层级顺序(从低到高)
const typeOrder = [
DataType.CHANNEL,
DataType.BOUNDARY,
DataType.PATH,
DataType.OBSTACLE,
DataType.CHARGING_PILE,
DataType.VISION_OFF,
DataType.DOODLE,
DataType.ANTENNA,
];
// 按照层级顺序处理各种类型的数据
for (const type of typeOrder) {
const dataList = processedData[type];
if (!dataList || dataList.length === 0) {
continue;
}
// 按照层级排序
dataList.sort((a, b) => a.level - b.level);
// 根据不同类型转换为绘制元素
for (const data of dataList) {
const drawElement = this.convertToDrawElement(data);
if (drawElement) {
drawElements.push(drawElement);
}
}
}
return drawElements;
}
/**
* 处理边界元素
* @param elements 边界元素
*/
static processBoundaryElements(elements) {
const boundaryDataList = [];
for (const element of elements) {
try {
const convertedPoints = convertPointsFormat(element.points);
if (!convertedPoints || convertedPoints.length < 3) {
continue;
}
const boundaryData = new BoundaryData(element, 100, [], // 通道数据后续建立关联
this.mapConfig.boundary);
boundaryDataList.push(boundaryData);
}
catch (error) {
console.warn(`Error processing BOUNDARY element:`, element, error);
}
}
return boundaryDataList;
}
/**
* 处理障碍物元素
* @param elements 障碍物元素
*/
static processObstacleElements(elements) {
const obstacleDataList = [];
for (const element of elements) {
try {
const convertedPoints = convertPointsFormat(element.points);
if (!convertedPoints || convertedPoints.length < 3) {
continue;
}
const obstacleData = new ObstacleData(element, 150, // 高层级
this.mapConfig.obstacle || {});
obstacleDataList.push(obstacleData);
}
catch (error) {
console.warn(`Error processing OBSTACLE element:`, element, error);
}
}
return obstacleDataList;
}
/**
* 处理通道元素
* @param elements 通道元素
*/
static processChannelElements(elements) {
const channelDataList = [];
for (const element of elements) {
try {
const convertedPoints = convertPointsFormat(element.points);
if (!convertedPoints || convertedPoints.length < 2) {
continue;
}
const channelData = new ChannelData(element, 50, // 最低层级
[], // 连接的边界后续建立关联
false, this.mapConfig.channel || {});
channelDataList.push(channelData);
}
catch (error) {
console.warn(`Error processing TUNNEL element:`, element, error);
}
}
return channelDataList;
}
/**
* 处理充电桩元素
* @param elements 充电桩元素
*/
static processChargingPileElements(elements) {
const chargingPileDataList = [];
const channelDataList = [];
for (const element of elements) {
try {
// 处理充电桩
const chargingPileData = new ChargingPileData(element, 150, // 高层级
null, // 通道数据后续建立关联
this.mapConfig.chargingPile || {});
chargingPileDataList.push(chargingPileData);
// 处理充电桩通道
if (element.tunnel &&
element.tunnel.points &&
Array.isArray(element.tunnel.points) &&
element.tunnel.points.length > 0) {
// 回桩通道需要将充电桩的position点添加到通道的终点
element.tunnel.points.push([...element.position, 0]);
if (element.tunnel.points && element.tunnel.points.length >= 2) {
const channelData = new ChannelData({
...element.tunnel,
id: `${element.id}_tunnel`,
}, 50, // 最低层级
[], // 连接的边界后续建立关联
true, this.mapConfig.channel || {});
channelDataList.push(channelData);
// 建立充电桩和通道的关联
chargingPileData.tunnel = channelData;
}
}
}
catch (error) {
console.warn(`Error processing CHARGING_PILE element:`, element, error);
}
}
return { chargingPileData: chargingPileDataList, channelData: channelDataList };
}
/**
* 处理视觉盲区元素
* @param elements 视觉盲区元素
*/
static processVisionOffElements(elements) {
const visionOffDataList = [];
for (const element of elements) {
try {
const convertedPoints = convertPointsFormat(element.points);
if (!convertedPoints || convertedPoints.length < 3) {
continue;
}
const visionOffData = new VisionOffData(element, 150, // 高层级
this.mapConfig.visionOff || {});
visionOffDataList.push(visionOffData);
}
catch (error) {
console.warn(`Error processing VISION_OFF_AREA element:`, element, error);
}
}
return visionOffDataList;
}
/**
* 处理时间限制障碍物元素
* @param elements 时间限制障碍物元素
*/
static processTimeLimitObstacleElements(elements) {
const obstacleDataList = [];
const doodleDataList = [];
for (const element of elements) {
try {
// 如果有SVG数据,创建涂鸦数据
// 如果有过期时间,且过期时间大于当前时间(秒级别),才需要渲染
if ('svg' in element &&
element.svg &&
'center' in element &&
element.center &&
'scale' in element &&
element.scale !== undefined &&
'direction' in element &&
element.direction !== undefined &&
(element.expiration_ts === undefined ||
element.expiration_ts > Math.floor(Date.now() / 1000))) {
const convertedPoints = convertPoint(element.center);
if (!convertedPoints) {
continue;
}
const doodleData = new DoodleData(element, 150, // 高层级
this.mapConfig.doodle || {});
doodleDataList.push(doodleData);
}
// 如果有points数据,创建障碍物数据
else if ('points' in element &&
element.points &&
Array.isArray(element.points) &&
element.points.length >= 3 &&
(element.expiration_ts === undefined ||
element.expiration_ts > Math.floor(Date.now() / 1000))) {
const convertedPoints = convertPointsFormat(element.points);
if (!convertedPoints || convertedPoints.length < 3) {
continue;
}
const obstacleData = new ObstacleData(element, 150, // 高层级
this.mapConfig.obstacle || {});
obstacleDataList.push(obstacleData);
}
}
catch (error) {
console.warn(`Error processing TIME_LIMIT_OBSTACLE element:`, element, error);
}
}
return { obstacleData: obstacleDataList, doodleData: doodleDataList };
}
/**
* 建立数据之间的关联关系
* @param processedData 处理后的数据
*/
static establishRelationships(processedData) {
// 建立通道和边界的关联关系
if (processedData[DataType.CHANNEL] && processedData[DataType.BOUNDARY]) {
const channels = processedData[DataType.CHANNEL];
const boundaries = processedData[DataType.BOUNDARY];
for (const channel of channels) {
if (channel.originalData && channel.originalData.connection) {
const connectionIds = Array.isArray(channel.originalData.connection)
? channel.originalData.connection
: [channel.originalData.connection];
for (const connectionId of connectionIds) {
const connectedBoundary = boundaries.find((b) => b.id === connectionId);
if (connectedBoundary) {
// 建立通道到边界的关联
if (!channel.connectedBoundaries) {
channel.connectedBoundaries = [];
}
channel.connectedBoundaries.push(connectedBoundary);
// 建立边界到通道的关联
if (!connectedBoundary.channels) {
connectedBoundary.channels = [];
}
connectedBoundary.channels.push(channel);
}
}
}
}
}
// 建立充电桩和通道的关联关系
if (processedData[DataType.CHARGING_PILE] && processedData[DataType.CHANNEL]) {
const chargingPiles = processedData[DataType.CHARGING_PILE];
const channels = processedData[DataType.CHANNEL];
for (const chargingPile of chargingPiles) {
if (!chargingPile.tunnel) {
const tunnel = channels.find((c) => c.isChargingPileTunnel &&
c.originalData &&
c.originalData.id === `${chargingPile.id}_tunnel`);
if (tunnel) {
chargingPile.tunnel = tunnel;
}
}
}
}
}
/**
* 将处理后的数据转换为绘制元素
* @param data 处理后的数据
*/
static convertToDrawElement(data) {
switch (data.type) {
case DataType.BOUNDARY:
return this.convertBoundaryToDrawElement(data);
case DataType.OBSTACLE:
return this.convertObstacleToDrawElement(data);
case DataType.CHANNEL:
return this.convertChannelToDrawElement(data);
case DataType.VISION_OFF:
return this.convertVisionOffToDrawElement(data);
case DataType.DOODLE:
return this.convertDoodleToDrawElement(data);
case DataType.PATH:
return this.convertPathToDrawElement(data);
case DataType.ANTENNA:
return this.convertAntennaToDrawElement(data);
case DataType.CHARGING_PILE:
return this.convertChargingPileToDrawElement(data);
default:
console.warn(`Unknown data type: ${data.type}`);
return null;
}
}
/**
* 将边界数据转换为绘制元素
* @param data 边界数据
*/
static convertBoundaryToDrawElement(data) {
const len = data.points?.length || 0;
const firstPoint = data.points?.[0];
const lastPoint = data.points?.[len - 1];
const isClosed = firstPoint?.[0] === lastPoint?.[0] && firstPoint?.[1] === lastPoint?.[1];
// 如果地图没有闭合,则手动新增闭合点,避免border最后一部分没有闭合的情况
const coordinates = [...data.points];
if (!isClosed && coordinates.length > 0) {
coordinates.push([firstPoint?.[0], firstPoint?.[1], lastPoint?.[2]]);
}
return {
type: 'boundary',
coordinates,
style: data.style,
originalData: data.originalData,
};
}
/**
* 将障碍物数据转换为绘制元素
* @param data 障碍物数据
*/
static convertObstacleToDrawElement(data) {
return {
type: 'obstacle',
coordinates: data.points,
style: data.style,
originalData: data.originalData,
};
}
/**
* 将通道数据转换为绘制元素
* @param data 通道数据
*/
static convertChannelToDrawElement(data) {
return {
type: 'channel',
coordinates: data.points,
style: data.style,
originalData: data.originalData,
};
}
/**
* 将视觉盲区数据转换为绘制元素
* @param data 视觉盲区数据
*/
static convertVisionOffToDrawElement(data) {
return {
type: 'vision_off_area',
coordinates: data.points,
style: data.style,
originalData: data.originalData,
};
}
/**
* 将涂鸦数据转换为绘制元素
* @param data 涂鸦数据
*/
static convertDoodleToDrawElement(data) {
return {
type: 'svg',
coordinates: [data.center],
style: data.style,
metadata: {
svg: data.svg,
scale: data.scale,
direction: data.direction,
},
originalData: data.originalData,
};
}
/**
* 将路径数据转换为绘制元素
* @param data 路径数据
*/
static convertPathToDrawElement(data) {
return {
type: 'path',
coordinates: data.points,
style: data.style,
originalData: data.originalData,
pathType: data.pathType,
};
}
/**
* 将天线数据转换为绘制元素
* @param data 天线数据
*/
static convertAntennaToDrawElement(data) {
return {
type: 'antenna',
coordinates: data.points,
style: data.style,
metadata: {
antennaId: data.antennaId,
online: data.online,
size: data.size,
},
originalData: data.originalData,
};
}
/**
* 将充电桩数据转换为绘制元素
* @param data 充电桩数据
*/
static convertChargingPileToDrawElement(data) {
return {
type: 'charging_pile',
coordinates: [data.position],
style: data.style,
metadata: {
direction: data.direction,
width: data.width,
length: data.length,
navPos: data.navPos,
},
originalData: data.originalData,
};
}
/**
* 收集所有地图元素
* @param mapData 地图数据
*/
static collectAllElements(mapData) {
const allElements = [];
// 处理子地图中的元素
if (mapData.sub_maps && mapData.sub_maps.length > 0) {
for (const subMap of mapData.sub_maps) {
if (subMap.elements && subMap.elements.length > 0) {
allElements.push(...subMap.elements);
}
}
}
// 处理障碍物
if (mapData.obstacles && mapData.obstacles.length > 0) {
allElements.push(...mapData.obstacles);
}
// 处理隧道
if (mapData.tunnels && mapData.tunnels.length > 0) {
allElements.push(...mapData.tunnels);
}
// 处理时间限制障碍物
if (mapData.time_limit_obstacles && mapData.time_limit_obstacles.length > 0) {
allElements.push(...mapData.time_limit_obstacles);
}
// 处理视觉盲区
if (mapData.vision_off_areas && mapData.vision_off_areas.length > 0) {
allElements.push(...mapData.vision_off_areas);
}
return allElements;
}
/**
* 按线段类型创建路径段
* @param points 路径点
*/
static createPathSegmentsByType(points) {
// 这里简化处理,实际应该调用原有的createPathSegmentsByType函数
// 由于没有完整的实现,这里只是一个占位符
return {
edge: [],
mowing: [],
trans: [],
};
}
}
/**
* 边界相关样式配置
*/
const BOUNDARY_STYLES = {
lineColor: '#ffffff',
fillColor: 'rgba(239, 255, 237, 0.1)', // 更鲜艳的绿色半透明填充,增强可见性
lineWidth: DEFAULT_LINE_WIDTHS.BOUNDARY,
opacity: DEFAULT_OPACITIES.FULL,
mowingLineColor: 'rgba(99, 216, 174, 1)',
};
const VISION_OFF_AREA_STYLES = {
lineColor: 'rgba(108, 167, 255, 1)',
fillColor: 'rgba(43, 128, 255, 0.45)', // 浅蓝色半透明填充
lineWidth: DEFAULT_LINE_WIDTHS.VISION_OFF_AREA,
opacity: DEFAULT_OPACITIES.HIGH,
};
const OBSTACLE_STYLES = {
lineColor: 'rgba(255, 122, 51, 1)',
fillColor: 'rgba(255, 113, 51, 0.2)', // 红色半透明填充
lineWidth: DEFAULT_LINE_WIDTHS.OBSTACLE,
opacity: DEFAULT_OPACITIES.FULL,
};
const CHARGING_PILE_STYLES = {
lineColor: 'blue',
fillColor: 'blue',
lineWidth: DEFAULT_LINE_WIDTHS.CHARGING_PILE,
radius: DEFAULT_RADII.CHARGING_PILE,
opacity: DEFAULT_OPACITIES.FULL,
};
const DOODLE_STYLES = {
lineColor: '#ff5722',
fillColor: '#ff9800', // 粉色半透明填充
lineWidth: DEFAULT_LINE_WIDTHS.TIME_LIMIT_OBSTACLE,
opacity: DEFAULT_OPACITIES.DOODLE,
};
const PATH_EDGE_STYLES = {
lineWidth: DEFAULT_LINE_WIDTHS.PATH,
opacity: DEFAULT_OPACITIES.MEDIUM,
transLineColor: 'transparent',
edgeLineColor: 'rgba(231, 238, 246)',
mowedLineColor: 'rgba(231, 238, 246)',
mowingLineColor: 'rgba(123, 200, 187)',
// edgeLineColor: 'red',
// mowedLineColor: 'red',
// mowingLineColor: 'red',
};
const CHANNEL_STYLES = {
lineColor: 'purple',
lineWidth: DEFAULT_LINE_WIDTHS.CHANNEL,
bottomLineWidth: 20,
bottomLineColor: '#8498A9',
opacity: DEFAULT_OPACITIES.FULL,
};
const ANTENNA_STYLES = {
lineColor: 'transparent',
fillColor: 'transparent',
radius: 12, // 默认24px大小
opacity: DEFAULT_OPACITIES.FULL,
};
const DEFAULT_STYLES = {
[DataType.BOUNDARY]: BOUNDARY_STYLES,
[DataType.VISION_OFF]: VISION_OFF_AREA_STYLES,
[DataType.OBSTACLE]: OBSTACLE_STYLES,
[DataType.CHARGING_PILE]: CHARGING_PILE_STYLES,
[DataType.DOODLE]: DOODLE_STYLES,
[DataType.PATH]: PATH_EDGE_STYLES,
[DataType.CHANNEL]: CHANNEL_STYLES,
[DataType.ANTENNA]: ANTENNA_STYLES,
};
function restorePoint(point) {
return [
point[0] / SCALE_FACTOR,
-point[1] / SCALE_FACTOR,
];
}
function restorePointsFormat(points) {
if (!points || points.length === 0)
return [];
return points.map((point) => {
if (point.length >= 2) {
// 对前两个元素应用缩放因子,保留其他元素
return [
...restorePoint(point.slice(0, 2)),
...point.slice(2), // 保留第三个及以后的元素
];
}
return point;
});
}
function convertPoint(point) {
return [
point[0] * SCALE_FACTOR,
-point[1] * SCALE_FACTOR, // Y轴翻转,与Python代码一致
];
}
/**
* 转换点格式,为坐标点添加缩放因子
* 保持原数组格式,只对前两个元素(x, y 坐标)应用缩放因子
* 支持二维或三维数组 [x, y] 或 [x, y, other]
*/
function convertPointsFormat(points) {
if (!points || points.length === 0)
return [];
return points.map((point) => {
if (point.length >= 2) {
// 对前两个元素应用缩放因子,保留其他元素
return [
...convertPoint(point.slice(0, 2)),
...point.slice(2), // 保留第三个及以后的元素
];
}
return point;
});
}
/**
* 转换位置格式从 [number, number] 到 {x: number, y: number}
* 添加缩放因子将米转换为像素
*/
function convertPositionFormat(position) {
if (!position || position.length !== 2)
return null;
return {
x: position[0] * SCALE_FACTOR,
y: -position[1] * SCALE_FACTOR, // Y轴翻转
};
}
/**
* 转换单个坐标点
*/
function convertCoordinate(x, y) {
return {
x: x * SCALE_FACTOR,
y: -y * SCALE_FACTOR, // Y轴翻转
};
}
/**
* 用于判断三个点的方向的辅助方法
*/
function orientation(p, q, r) {
const val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
if (val == 0)
return 0; // colinear
return val > 0 ? 1 : 2; // clock or counterclock wise
}
/**
* 检查点q是否在线段pr上的辅助方法
*/
function onSegment$1(p, q, r) {
if (q.x <= Math.max(p.x, r.x) &&
q.x >= Math.min(p.x, r.x) &&
q.y <= Math.max(p.y, r.y) &&
q.y >= Math.min(p.y, r.y)) {
return true;
}
return false;
}
/**
* 判断两条线段是否相交的方法
*/
function doTwoLinesIntersect(p1, q1, p2, q2) {
//处理p1和q1两个点相同的情况
if (p1.x - q1.x == 0 && p1.y - q1.y == 0) {
return false;
}
if (p2.x - q2.x == 0 && p2.y - q2.y == 0) {
return false;
}
// 计算四个点的方向
const o1 = orientation(p1, q1, p2);
const o2 = orientation(p1, q1, q2);
const o3 = orientation(p2, q2, p1);
const o4 = orientation(p2, q2, q1);
// 一般情况,如果四个方向两两不同,则线段相交
if (o1 != o2 && o3 != o4) {
return true;
}
// 特殊情况,当线段的端点在另一条线段上时
if (o1 == 0 && onSegment$1(p1, q1, p2))
return true;
if (o2 == 0 && onSegment$1(p1, q1, q2))
return true;
if (o3 == 0 && onSegment$1(p2, q2, p1))
return true;
if (o4 == 0 && onSegment$1(p2, q2, q1))
return true;
// 如果以上情况都不满足,则线段不相交
return false;
}
/**
* 判断多点折线是否相交
*/
function doIntersect(points1, points2) {
if (points1 == null || points2 == null || points1.length < 3 || points2.length < 3) {
return false;
}
for (let i = 0; i < points1.length - 1; i++) {
for (let j = 0; j < points2.length - 1; j++) {
if (doTwoLinesIntersect(points1[i], points1[i + 1], points2[j], points2[j + 1])) {
return true;
}
}
}
return false;
}
/**
* 判断点是否在多边形内
*/
function isPointInPolygon$1(point, polygon) {
if (polygon == null || polygon.length < 3) {
return false;
}
let count = 0;
let size = polygon.length;
let p1, p2, p3;
for (let i = 0; i < size; i++) {
p1 = polygon[i];
p2 = polygon[(i + 1) % size];
if (p1.y == null || p2.y == null || p1.x == null || p2.x == null) {
continue;
}
if (p1.y === p2.y) {
continue;
}
if (point.y > Math.min(p1.y, p2.y) && point.y < Math.max(p1.y, p2.y)) {
const interX = ((point.y - p1.y) * (p2.x - p1.x)) / (p2.y - p1.y) + p1.x;
if (interX >= point.x) {
count++;
}
else if (interX == point.x) {
return true;
}
}
else {
if (point.y == p2.y && point.x <= p2.x) {
p3 = polygon[(i + 2) % size];
if (point.y >= Math.min(p1.y, p3.y) && point.y <= Math.max(p1.y, p3.y)) {
// 若当前点的y坐标位于 p1和p3组成的线段关于y轴的投影中,则记为该点的射线只穿过端点一次。
++count;
}
else {
// 若当前点的y坐标不能包含在p1和p3组成的线段关于y轴的投影中,则点射线通过的两条线段组成了一个弯折的部分,
// 此时我们记射线穿过该端点两次
count += 2;
}
}
}
}
return count % 2 == 1;
}
/**
* 两个图形是否完全分离,互相不包含
*/
function isOutsideToEachOther(points1, points2) {
// 相交关系
if (doIntersect(points1, points2)) {
return false;
}
// 点关系,判断每个图形的点都在另一个图形外部
for (let point of points1) {
if (isPointInPolygon$1(point, points2)) {
// Log.i("ycf", "isOutsideToEachOther: mapPoint1=" + mapPoint);
return false;
}
}
for (let point of points2) {
if (isPointInPolygon$1(point, points1)) {
// Log.i("ycf", "isOutsideToEachOther: mapPoint2=" + mapPoint);
return false;
}
}
return true;
}
/**
* 计算两点间的欧几里得距离
* @param point1 第一个点
* @param point2 第二个点
* @returns 两点间的距离
*/
function calculateDistance(point1, point2) {
const dx = point1.x - point2.x;
const dy = point1.y - point2.y;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* 计算点到线段的垂足坐标
* @param point 目标点
* @param lineStart 线段起点
* @param lineEnd 线段终点
* @returns 垂足坐标和参数信息
*/
function calculatePerpendicularFoot(point, lineStart, lineEnd) {
const A = lineEnd.x - lineStart.x;
const B = lineEnd.y - lineStart.y;
const C = point.x - lineStart.x;
const D = point.y - lineStart.y;
const dot = A * C + B * D;
const lenSq = A * A + B * B;
if (lenSq === 0) {
return { ...lineStart, param: 0 };
}
const param = dot / lenSq;
return {
x: lineStart.x + param * A,
y: lineStart.y + param * B,
param: param,
};
}
/**
* 判断垂足是否在线段上(而不是延长线上)
* @param foot 垂足计算结果
* @returns 是否在线段上
*/
function isFootOnSegment(foot) {
return foot.param >= 0 && foot.param <= 1;
}
/**
* 按Python逻辑创建路径段:根据连续的两点之间的关系确定线段类型
*/
function createPathSegmentsByType(list) {
const segments = {
edge: [],
mowing: [],
trans: [],
};
if (list.length < 2)
return segments;
let lastPoint = null;
let currentSegment = [];
let currentSegmentType = null;
for (const currentPoint of list) {
const currentCoord = {
x: currentPoint.postureX,
y: currentPoint.postureY,
};
if (lastPoint !== null) {
// 判断上一个点和当前点是否需要绘制 (iso端逻辑)
const lastShouldDraw = lastPoint.pathType === '00' ||
lastPoint.pathType === '01' ||
lastPoint.knifeRotation === '01';
const currentShouldDraw = currentPoint.pathType === '00' ||
currentPoint.pathType === '01' ||
currentPoint.knifeRotation === '01';
let segmentType;
if (lastShouldDraw && currentShouldDraw) {
// 需要绘制的两点之间用实线连接
segmentType = currentPoint.pathType === '00' ? 'edge' : 'mowing';
}
else {
// 不需要绘制的两点之间用虚线连接
segmentType = 'trans';
}
// 如果是新的段类型,结束当前段并开始新段
if (currentSegmentType !== segmentType) {
if (currentSegment.length >= 2) {
segments[currentSegmentType].push({ points: [...currentSegment] });
}
// 开始新段
currentSegment = [
{
x: lastPoint.postureX,
y: lastPoint.postureY,
},
currentCoord,
];
currentSegmentType = segmentType;
}
else {
// 继续当前段
currentSegment.push(currentCoord);
}
}
lastPoint = currentPoint;
}
// 添加最后一段
if (currentSegment.length >= 2 && currentSegmentType) {
segments[currentSegmentType].push({ points: currentSegment });
}
return segments;
}
/**
* 计算地图边界
*/
function calculateMapBounds(mapData) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
// 更新边界的辅助函数
const updateBounds = (points) => {
for (const point of points) {
if (point.length >= 2) {
minX = Math.min(minX, point[0]);
minY = Math.min(minY, point[1]);
maxX = Math.max(maxX, point[0]);
maxY = Math.max(maxY, point[1]);
}
}
};
// 处理所有地图元素
const allElements = [];
// 收集所有元素
if (mapData.sub_maps) {
for (const subMap of mapData.sub_maps) {
allElements.push(...(subMap.elements || []));
}
}
if (mapData.obstacles)
allElements.push(...mapData.obstacles);
if (mapData.tunnels)
allElements.push(...mapData.tunnels);
if (mapData.time_limit_obstacles)
allElements.push(...mapData.time_limit_obstacles);
if (mapData.vision_off_areas)
allElements.push(...mapData.vision_off_areas);
// 计算元素边界
for (const element of allElements) {
if (element.points) {
const convertedPoints = convertPointsFormat(element.points);
if (convertedPoints) {
updateBounds(convertedPoints);
}
}
if (element.position) {
const point = convertPositionFormat(element.position);
if (point)
updateBounds([[point.x, point.y]]);
}
}
// 如果没有找到边界,返回默认值
if (minX === Infinity) {
return { minX: 0, minY: 0, maxX: 100, maxY: 100 };
}
return { minX, minY, maxX, maxY };
}
/**
* 计算所有地块围成的边界
* @param bounds
* @param margin
* @returns
*/
function calculateBoundaryBounds(mapData) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
// 更新边界的辅助函数
const updateBounds = (points) => {
for (const point of points) {
if (point.length >= 2) {
minX = Math.min(minX, point[0]);
minY = Math.min(minY, point[1]);
maxX = Math.max(maxX, point[0]);
maxY = Math.max(maxY, point[1]);
}
}
};
// 处理所有地图元素
const allElements = [];
// 收集所有元素
if (mapData.sub_maps) {
for (const subMap of mapData.sub_maps) {
allElements.push(...subMap.elements);
}
}
// 计算元素边界
for (const element of allElements) {
if (element.type === 'BOUNDARY' && element.points) {
const convertedPoints = convertPointsFormat(element.points);
if (convertedPoints) {
updateBounds(convertedPoints);
}
}
if (element.type === 'BOUNDARY' && element.position) {
const point = convertPositionFormat(element.position);
if (point)
updateBounds([[point.x, point.y]]);
}
}
// 如果没有找到边界,返回默认值
if (minX === Infinity) {
return { minX: 0, minY: 0, maxX: 100, maxY: 100 };
}
return { minX, minY, maxX, maxY };
}
function calculateBoundaryBoundsCenter(mapData) {
const boundaryBounds = calculateBoundaryBounds(mapData);
if (boundaryBounds.minX === Infinity) {
return [50, 50];
}
return [
boundaryBounds.minX + (boundaryBounds.maxX - boundaryBounds.minX) / 2,
boundaryBounds.minY + (boundaryBounds.maxY - boundaryBounds.minY) / 2,
];
}
/**
* 检查GPS坐标是否有效
*/
function isValidGpsCoordinate$1(lat, lng) {
// 检查纬度范围 (-90 到 90) 和经度范围 (-180 到 180)
// 同时排除明显无效的坐标(如0,0或者很接近0的值)
return (lat >= -90 &&
lat <= 90 &&
lng >= -180 &&
lng <= 180 &&
!(Math.abs(lat) < 0.001 && Math.abs(lng) < 0.001) // 排除接近(0,0)的坐标
);
}
/**
* 从地图几何数据估算GPS边界坐标
* 当GPS坐标无效时,基于地图的几何边界来估算SW和NE的GPS坐标
* calculateMapBounds返回的数据除以50后代表准确的物理单位(米)
*/
function estimateGpsFromMapBounds(mapData) {
try {
const bounds = calculateMapBounds(mapData);
if (!bounds || bounds.minX === Infinity) {
return {
sw: [0, 0],
ne: [0, 0],
};
}
// 将边界数据转换为物理单位(米)
const minXMeters = bounds.minX / SCALE_FACTOR; // 西边界
const minYMeters = bounds.minY / SCALE_FACTOR; // 南边界
const maxXMeters = bounds.maxX / SCALE_FACTOR; // 东边界
const maxYMeters = bounds.maxY / SCALE_FACTOR; // 北边界
const mapWidthMeters = maxXMeters - minXMeters;
const mapHeightMeters = maxYMeters - minYMeters;
// 凤凰岭的GPS坐标作为地图中心点
const centerLat = 40.103;
const centerLng = 116.072222;
// const centerLat = 40.03806686401367;
// const centerLng = 116.35540771484375;
// 精确的坐标转换常数
// 1度纬度 = 约111,320米(在地球上任何地方都基本相同)
// 1度经度 = 约111,320 * cos(纬度) 米(随纬度变化)
const METERS_PER_DEGREE_LAT = 111320;
const METERS_PER_DEGREE_LNG = 111320 * Math.cos((centerLat * Math.PI) / 180);
// 计算SW(西南角)GPS坐标
// 从中心点减去半个地图的宽度和高度
const swLat = centerLat - mapHeightMeters / 2 / METERS_PER_DEGREE_LAT;
cons