UNPKG

@fleet-frontend/mower-maps

Version:

a mower maps in google maps

1,556 lines (1,544 loc) 916 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var React = require('react'); var ReactDOM = require('react-dom'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React); /** * 常量和枚举类型定义 */ /** * 机器人状态枚举 */ 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; /** * 数据类型枚举 */ exports.DataType = void 0; (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"; })(exports.DataType || (exports.DataType = {})); /** * 渲染类型枚举 */ exports.RenderType = void 0; (function (RenderType) { RenderType["POINT"] = "point"; RenderType["POLYGON"] = "polygon"; RenderType["LINE"] = "line"; RenderType["SVG"] = "svg"; })(exports.RenderType || (exports.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, exports.DataType.BOUNDARY, level, exports.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, exports.DataType.CHANNEL, level, exports.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, exports.DataType.OBSTACLE, level, exports.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, exports.DataType.PATH, level, exports.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, exports.DataType.DOODLE, level, exports.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, exports.DataType.VISION_OFF, level, exports.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, exports.DataType.ANTENNA, level, exports.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, exports.DataType.CHARGING_PILE, level, exports.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[exports.DataType.CHANNEL] = processedData[exports.DataType.CHANNEL] || []; processedData[exports.DataType.CHANNEL].push(...this.processChannelElements(elementsByType.TUNNEL)); } // 处理边界 if (elementsByType.BOUNDARY.length > 0) { processedData[exports.DataType.BOUNDARY] = this.processBoundaryElements(elementsByType.BOUNDARY); } // 处理障碍物 if (elementsByType.OBSTACLE.length > 0) { processedData[exports.DataType.OBSTACLE] = this.processObstacleElements(elementsByType.OBSTACLE); } // 处理充电桩 if (elementsByType.CHARGING_PILE.length > 0) { processedData[exports.DataType.CHARGING_PILE] = []; processedData[exports.DataType.CHANNEL] = processedData[exports.DataType.CHANNEL] || []; const { chargingPileData, channelData } = this.processChargingPileElements(elementsByType.CHARGING_PILE); processedData[exports.DataType.CHARGING_PILE].push(...chargingPileData); processedData[exports.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[exports.DataType.OBSTACLE] = processedData[exports.DataType.OBSTACLE] || []; processedData[exports.DataType.OBSTACLE].push(...obstacleData); } if (doodleData.length > 0) { processedData[exports.DataType.DOODLE] = doodleData; } } // 处理视觉盲区 if (elementsByType.VISION_OFF_AREA.length > 0) { processedData[exports.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[exports.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[exports.DataType.ANTENNA] = antennaDataList; } return processedData; } /** * 将处理后的数据转换为绘制元素 * @param processedData 处理后的数据 */ static toDrawElements(processedData) { const drawElements = []; // 定义层级顺序(从低到高) const typeOrder = [ exports.DataType.CHANNEL, exports.DataType.BOUNDARY, exports.DataType.PATH, exports.DataType.OBSTACLE, exports.DataType.CHARGING_PILE, exports.DataType.VISION_OFF, exports.DataType.DOODLE, exports.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[exports.DataType.CHANNEL] && processedData[exports.DataType.BOUNDARY]) { const channels = processedData[exports.DataType.CHANNEL]; const boundaries = processedData[exports.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[exports.DataType.CHARGING_PILE] && processedData[exports.DataType.CHANNEL]) { const chargingPiles = processedData[exports.DataType.CHARGING_PILE]; const channels = processedData[exports.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 exports.DataType.BOUNDARY: return this.convertBoundaryToDrawElement(data); case exports.DataType.OBSTACLE: return this.convertObstacleToDrawElement(data); case exports.DataType.CHANNEL: return this.convertChannelToDrawElement(data); case exports.DataType.VISION_OFF: return this.convertVisionOffToDrawElement(data); case exports.DataType.DOODLE: return this.convertDoodleToDrawElement(data); case exports.DataType.PATH: return this.convertPathToDrawElement(data); case exports.DataType.ANTENNA: return this.convertAntennaToDrawElement(data); case exports.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 = { [exports.DataType.BOUNDARY]: BOUNDARY_STYLES, [exports.DataType.VISION_OFF]: VISION_OFF_AREA_STYLES, [exports.DataType.OBSTACLE]: OBSTACLE_STYLES, [exports.DataType.CHARGING_PILE]: CHARGING_PILE_STYLES, [exports.DataType.DOODLE]: DOODLE_STYLES, [exports.DataType.PATH]: PATH_EDGE_STYLES, [exports.DataType.CHANNEL]: CHANNEL_STYLES, [exports.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], }; } //