@fleet-frontend/mower-maps 0.0.9-beta.10 → 0.0.9-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/constants.d.ts +1 -0
- package/dist/config/constants.d.ts.map +1 -1
- package/dist/config/styles.d.ts.map +1 -1
- package/dist/index.esm.js +162 -53
- package/dist/index.js +162 -53
- package/dist/render/MowerMapRenderer.d.ts.map +1 -1
- package/dist/render/layers/PathLayer.d.ts +10 -2
- package/dist/render/layers/PathLayer.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/config/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;CAUtB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,iBAAiB
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/config/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;CAUtB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;CAOpB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;CAMhB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;CAUf,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;CAWrB,CAAC;AAGX,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEjF,eAAO,MAAM,qBAAqB,sqUAqB3B,CAAC;AAER;;GAEG;AACH,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B;;GAEG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC;;GAEG;AACH,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC;;GAEG;AACH,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAC9C;;GAEG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC;;GAEG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC;;GAEG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AACvC;;GAEG;AACH,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAChD;;GAEG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC;;GAEG;AACH,eAAO,MAAM,UAAU,KAAK,CAAC;AAE7B,eAAO,MAAM,eAAe,4BAA4B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/config/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/config/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEpG;;GAEG;AACH,eAAO,MAAM,eAAe,EAMvB,aAAa,CAAC;AAEnB,eAAO,MAAM,sBAAsB,EAK9B,aAAa,CAAC;AAEnB,eAAO,MAAM,eAAe,EAKvB,aAAa,CAAC;AAEnB,eAAO,MAAM,oBAAoB,EAM5B,iBAAiB,CAAC;AAEvB,eAAO,MAAM,aAAa,EAKrB,eAAe,CAAC;AAErB,eAAO,MAAM,gBAAgB;;;;;;;CAO5B,CAAC;AAEF,eAAO,MAAM,cAAc;;;;;;CAM1B,CAAC;AAEF,eAAO,MAAM,cAAc,EAKtB,YAAY,CAAC;AAElB,eAAO,MAAM,cAAc,EAAE,SAS5B,CAAC"}
|
package/dist/index.esm.js
CHANGED
|
@@ -67,7 +67,8 @@ const DEFAULT_LINE_WIDTHS = {
|
|
|
67
67
|
const DEFAULT_OPACITIES = {
|
|
68
68
|
FULL: 1.0,
|
|
69
69
|
HIGH: 0.7,
|
|
70
|
-
MEDIUM: 0.6
|
|
70
|
+
MEDIUM: 0.6,
|
|
71
|
+
DOODLE: 0.8};
|
|
71
72
|
/**
|
|
72
73
|
* 默认半径设置
|
|
73
74
|
*/
|
|
@@ -1044,15 +1045,37 @@ class PathLayer extends BaseLayer {
|
|
|
1044
1045
|
d += ' Z ';
|
|
1045
1046
|
}
|
|
1046
1047
|
});
|
|
1047
|
-
// 3. svgElements
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1048
|
+
// 3. svgElements(解析 SVG 字符串并提取 path 数据)
|
|
1049
|
+
Object.values(svgElements).forEach((svgPath) => {
|
|
1050
|
+
const svgPathString = svgPath?.metadata?.svg;
|
|
1051
|
+
if (svgPathString && typeof svgPathString === 'string' && svgPathString.trim()) {
|
|
1052
|
+
// 处理转义字符
|
|
1053
|
+
const processedSvgString = svgPathString.replace(/\\n/g, '\n').replace(/\\"/g, '"');
|
|
1054
|
+
// 解析 SVG 字符串
|
|
1055
|
+
const parser = new DOMParser();
|
|
1056
|
+
const svgDoc = parser.parseFromString(processedSvgString, 'image/svg+xml');
|
|
1057
|
+
const svgElement = svgDoc.documentElement;
|
|
1058
|
+
if (svgElement.tagName === 'svg') {
|
|
1059
|
+
// 查找 path 元素
|
|
1060
|
+
const pathElement = svgElement.querySelector('path');
|
|
1061
|
+
if (pathElement) {
|
|
1062
|
+
const pathData = pathElement.getAttribute('d');
|
|
1063
|
+
if (pathData) {
|
|
1064
|
+
// 获取 SVG 元素的变换参数
|
|
1065
|
+
const centerCoords = svgPath.coordinates?.[0] || [0, 0];
|
|
1066
|
+
const center = [centerCoords[0], centerCoords[1]];
|
|
1067
|
+
const userScale = svgPath.metadata.scale || 1;
|
|
1068
|
+
const direction = svgPath.metadata?.direction || 0;
|
|
1069
|
+
const originalWidth = parseFloat(svgElement.getAttribute('width') || '76');
|
|
1070
|
+
const originalHeight = parseFloat(svgElement.getAttribute('height') || '68');
|
|
1071
|
+
// 应用变换到路径数据
|
|
1072
|
+
const transformedPathData = this.transformSvgPath(pathData, center, userScale, direction, originalWidth, originalHeight);
|
|
1073
|
+
d += transformedPathData + ' ';
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1053
1076
|
}
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1056
1079
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1057
1080
|
path.setAttribute('d', d);
|
|
1058
1081
|
const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
|
|
@@ -1077,47 +1100,132 @@ class PathLayer extends BaseLayer {
|
|
|
1077
1100
|
// 2. 创建一个组,应用 clipPath
|
|
1078
1101
|
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
1079
1102
|
group.setAttribute('clip-path', `url(#${clipPathId})`);
|
|
1080
|
-
group.setAttribute('opacity', '0.
|
|
1081
|
-
// 3.
|
|
1103
|
+
group.setAttribute('opacity', '0.5'); // 统一透明度,防止叠加脏乱
|
|
1104
|
+
// 3. 优化渲染:按样式分组并合并路径
|
|
1105
|
+
this.renderOptimizedPaths(group);
|
|
1106
|
+
svgGroup.appendChild(group);
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* 优化渲染:按样式分组并合并路径,减少 DOM 节点数量
|
|
1110
|
+
*/
|
|
1111
|
+
renderOptimizedPaths(group) {
|
|
1112
|
+
// 按样式分组存储路径数据
|
|
1113
|
+
const styleGroups = new Map();
|
|
1114
|
+
// 收集所有路径数据并按样式分组
|
|
1082
1115
|
for (const element of this.elements) {
|
|
1083
|
-
|
|
1116
|
+
// 类型断言:PathLayer 中的 elements 实际上是 PathElements 结构
|
|
1117
|
+
const pathElement = element;
|
|
1118
|
+
const { id, elements } = pathElement;
|
|
1084
1119
|
this.boundaryPaths[id] = [];
|
|
1120
|
+
elements.forEach((pathElement) => {
|
|
1121
|
+
const { coordinates, style } = pathElement;
|
|
1122
|
+
if (coordinates.length < 2)
|
|
1123
|
+
return;
|
|
1124
|
+
// 生成样式键(用于分组)
|
|
1125
|
+
const styleKey = this.generateStyleKey(style);
|
|
1126
|
+
// 构建路径数据
|
|
1127
|
+
let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
|
|
1128
|
+
for (let i = 1; i < coordinates.length; i++) {
|
|
1129
|
+
pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
|
|
1130
|
+
}
|
|
1131
|
+
// 按样式分组存储
|
|
1132
|
+
if (!styleGroups.has(styleKey)) {
|
|
1133
|
+
styleGroups.set(styleKey, { pathData: [], elements: [] });
|
|
1134
|
+
}
|
|
1135
|
+
styleGroups.get(styleKey).pathData.push(pathData);
|
|
1136
|
+
styleGroups.get(styleKey).elements.push(pathElement);
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
// 为每种样式创建一个合并的 path 元素
|
|
1140
|
+
styleGroups.forEach((groupData) => {
|
|
1141
|
+
const { pathData, elements } = groupData;
|
|
1142
|
+
if (pathData.length === 0)
|
|
1143
|
+
return;
|
|
1144
|
+
// 使用第一个元素的样式作为该组的样式
|
|
1145
|
+
const firstElement = elements[0];
|
|
1146
|
+
const style = firstElement.style;
|
|
1147
|
+
// 创建合并的 path 元素
|
|
1148
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1149
|
+
// 合并所有路径数据
|
|
1150
|
+
const mergedPathData = pathData.join(' ');
|
|
1151
|
+
path.setAttribute('d', mergedPathData);
|
|
1152
|
+
// 设置样式属性
|
|
1153
|
+
path.setAttribute('fill', 'none');
|
|
1154
|
+
path.setAttribute('stroke', style.lineColor || '#000000');
|
|
1155
|
+
path.setAttribute('mix-blend-mode', 'normal');
|
|
1156
|
+
const lineWidth = Math.max(style.lineWidth || 1, 0.5);
|
|
1157
|
+
path.setAttribute('stroke-width', lineWidth.toString());
|
|
1158
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
1159
|
+
path.setAttribute('stroke-linejoin', 'round');
|
|
1160
|
+
path.classList.add('vector-path');
|
|
1161
|
+
// 将合并的 path 添加到组中
|
|
1162
|
+
group.appendChild(path);
|
|
1163
|
+
// 保存引用到 boundaryPaths 中(保持兼容性)
|
|
1085
1164
|
elements.forEach((element) => {
|
|
1086
|
-
|
|
1165
|
+
const { id } = element;
|
|
1166
|
+
if (!this.boundaryPaths[id]) {
|
|
1167
|
+
this.boundaryPaths[id] = [];
|
|
1168
|
+
}
|
|
1169
|
+
this.boundaryPaths[id].push(path);
|
|
1087
1170
|
});
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* 变换 SVG 路径数据
|
|
1175
|
+
*/
|
|
1176
|
+
transformSvgPath(pathData, center, scale, direction, originalWidth, originalHeight) {
|
|
1177
|
+
// 解析路径数据并应用变换
|
|
1178
|
+
const commands = pathData.match(/[MmLlHhVvCcSsQqTtAaZz][^MmLlHhVvCcSsQqTtAaZz]*/g) || [];
|
|
1179
|
+
let transformedCommands = [];
|
|
1180
|
+
for (const command of commands) {
|
|
1181
|
+
const type = command[0];
|
|
1182
|
+
const params = command
|
|
1183
|
+
.slice(1)
|
|
1184
|
+
.trim()
|
|
1185
|
+
.split(/[\s,]+/)
|
|
1186
|
+
.filter(Boolean)
|
|
1187
|
+
.map(Number);
|
|
1188
|
+
if (type === 'Z' || type === 'z') {
|
|
1189
|
+
// 闭合路径,不需要变换
|
|
1190
|
+
transformedCommands.push(command);
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
// 处理坐标参数
|
|
1194
|
+
let transformedParams = [];
|
|
1195
|
+
for (let i = 0; i < params.length; i += 2) {
|
|
1196
|
+
if (i + 1 < params.length) {
|
|
1197
|
+
let x = params[i];
|
|
1198
|
+
let y = params[i + 1];
|
|
1199
|
+
// 应用变换:先平移到中心,然后缩放、旋转,最后平移到目标位置
|
|
1200
|
+
// 1. 平移到原点(相对于原始尺寸的中心)
|
|
1201
|
+
x -= originalWidth / 2;
|
|
1202
|
+
y -= originalHeight / 2;
|
|
1203
|
+
// 2. 应用缩放
|
|
1204
|
+
x *= scale;
|
|
1205
|
+
y *= scale;
|
|
1206
|
+
// 3. 应用旋转
|
|
1207
|
+
const cos = Math.cos(-direction);
|
|
1208
|
+
const sin = Math.sin(-direction);
|
|
1209
|
+
const newX = x * cos - y * sin;
|
|
1210
|
+
const newY = x * sin + y * cos;
|
|
1211
|
+
// 4. 平移到目标位置
|
|
1212
|
+
x = newX + center[0];
|
|
1213
|
+
y = newY + center[1];
|
|
1214
|
+
transformedParams.push(x, y);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
// 重建命令
|
|
1218
|
+
if (transformedParams.length > 0) {
|
|
1219
|
+
transformedCommands.push(type + transformedParams.join(' '));
|
|
1220
|
+
}
|
|
1088
1221
|
}
|
|
1089
|
-
|
|
1222
|
+
return transformedCommands.join(' ');
|
|
1090
1223
|
}
|
|
1091
1224
|
/**
|
|
1092
|
-
*
|
|
1225
|
+
* 生成样式键,用于路径分组
|
|
1093
1226
|
*/
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
if (coordinates.length < 2)
|
|
1097
|
-
return;
|
|
1098
|
-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1099
|
-
// 构建路径数据
|
|
1100
|
-
let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
|
|
1101
|
-
for (let i = 1; i < coordinates.length; i++) {
|
|
1102
|
-
pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
|
|
1103
|
-
}
|
|
1104
|
-
path.style.mixBlendMode = 'normal';
|
|
1105
|
-
// 设置路径属性
|
|
1106
|
-
path.setAttribute('d', pathData);
|
|
1107
|
-
// 直接给fill的颜色设置透明度会导致path重叠的部分颜色叠加,所以使用fill填充实色,通过fill-opacity设置透明度
|
|
1108
|
-
path.setAttribute('fill', 'none');
|
|
1109
|
-
// path.setAttribute('fill-opacity', '0.4');
|
|
1110
|
-
path.setAttribute('stroke', style.lineColor || '#000000');
|
|
1111
|
-
path.setAttribute('mix-blend-mode', 'normal');
|
|
1112
|
-
const lineWidth = Math.max(style.lineWidth || 1, 0.5);
|
|
1113
|
-
path.setAttribute('stroke-width', lineWidth.toString());
|
|
1114
|
-
path.setAttribute('stroke-linecap', 'round');
|
|
1115
|
-
path.setAttribute('stroke-linejoin', 'round');
|
|
1116
|
-
// 注意:这里不设置 opacity,因为透明度由父组控制
|
|
1117
|
-
// path.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
1118
|
-
path.classList.add('vector-path');
|
|
1119
|
-
this.boundaryPaths[id].push(path);
|
|
1120
|
-
group.appendChild(path);
|
|
1227
|
+
generateStyleKey(style) {
|
|
1228
|
+
return `${style.lineColor || '#000000'}-${style.lineWidth || 1}-${style.opacity || 1}`;
|
|
1121
1229
|
}
|
|
1122
1230
|
}
|
|
1123
1231
|
|
|
@@ -1541,7 +1649,7 @@ const DOODLE_STYLES = {
|
|
|
1541
1649
|
lineColor: '#ff5722',
|
|
1542
1650
|
fillColor: '#ff9800', // 粉色半透明填充
|
|
1543
1651
|
lineWidth: DEFAULT_LINE_WIDTHS.TIME_LIMIT_OBSTACLE,
|
|
1544
|
-
opacity: DEFAULT_OPACITIES.
|
|
1652
|
+
opacity: DEFAULT_OPACITIES.DOODLE,
|
|
1545
1653
|
};
|
|
1546
1654
|
const PATH_EDGE_STYLES = {
|
|
1547
1655
|
lineWidth: DEFAULT_LINE_WIDTHS.PATH,
|
|
@@ -8902,7 +9010,7 @@ const MowerMapRenderer = forwardRef(({ unitType = UnitsType.Imperial, language =
|
|
|
8902
9010
|
// const mapRef = useMap();
|
|
8903
9011
|
const [isGoogleMapsReady, setIsGoogleMapsReady] = useState(false);
|
|
8904
9012
|
const [hasInitializedBounds, setHasInitializedBounds] = useState(false);
|
|
8905
|
-
const { clearSubBoundaryBorder, clearObstacles } = useSubBoundaryBorderStore();
|
|
9013
|
+
const { clearSubBoundaryBorder, clearObstacles, clearSvgElements } = useSubBoundaryBorderStore();
|
|
8906
9014
|
const currentProcessMowingStatusRef = useRef(false);
|
|
8907
9015
|
const { updateProcessStateIsMowing, processStateIsMowing } = useProcessMowingState();
|
|
8908
9016
|
const [mowPartitionData, setMowPartitionData] = useState(null);
|
|
@@ -9071,6 +9179,7 @@ const MowerMapRenderer = forwardRef(({ unitType = UnitsType.Imperial, language =
|
|
|
9071
9179
|
return () => {
|
|
9072
9180
|
clearSubBoundaryBorder();
|
|
9073
9181
|
clearObstacles();
|
|
9182
|
+
clearSvgElements();
|
|
9074
9183
|
updateProcessStateIsMowing(false);
|
|
9075
9184
|
currentProcessMowingStatusRef.current = false;
|
|
9076
9185
|
if (overlayRef.current) {
|
|
@@ -9261,8 +9370,8 @@ const MowerMapRenderer = forwardRef(({ unitType = UnitsType.Imperial, language =
|
|
|
9261
9370
|
setMowPartitionData(mowingPartition);
|
|
9262
9371
|
curMowPartitionData = mowingPartition;
|
|
9263
9372
|
}
|
|
9264
|
-
const positionData = realTimeData?.find(item => item?.type === RealTimeDataType.LOCATION);
|
|
9265
|
-
const statusData = realTimeData?.find(item => item?.type === RealTimeDataType.STATUS);
|
|
9373
|
+
const positionData = realTimeData?.find((item) => item?.type === RealTimeDataType.LOCATION);
|
|
9374
|
+
const statusData = realTimeData?.find((item) => item?.type === RealTimeDataType.STATUS);
|
|
9266
9375
|
if (statusData || positionData) {
|
|
9267
9376
|
const currentStatus = statusData?.vehicleState || positionData?.vehicleState;
|
|
9268
9377
|
// 车辆回桩不会回传最后的park的位置,所以根据实时数据的状态数据判断车辆回到桩上
|
|
@@ -9275,20 +9384,20 @@ const MowerMapRenderer = forwardRef(({ unitType = UnitsType.Imperial, language =
|
|
|
9275
9384
|
setMowPartitionData(null);
|
|
9276
9385
|
curMowPartitionData = null;
|
|
9277
9386
|
}
|
|
9278
|
-
else if (currentStatus === RobotStatus.MOWING &&
|
|
9387
|
+
else if (currentStatus === RobotStatus.MOWING &&
|
|
9388
|
+
curMowPartitionData &&
|
|
9389
|
+
!curMowPartitionData?.partitionIds) {
|
|
9279
9390
|
// 如果当前是割草状态,但是地块数据初始化过且不存在则认为是全局割草,则把所有地块都高亮
|
|
9280
|
-
const allPartitionIds = mapJson?.sub_maps?.map(item => item?.id);
|
|
9391
|
+
const allPartitionIds = mapJson?.sub_maps?.map((item) => item?.id);
|
|
9281
9392
|
setMowPartitionData({
|
|
9282
|
-
partitionIds: allPartitionIds
|
|
9393
|
+
partitionIds: allPartitionIds,
|
|
9283
9394
|
});
|
|
9284
9395
|
curMowPartitionData = {
|
|
9285
|
-
partitionIds: allPartitionIds
|
|
9396
|
+
partitionIds: allPartitionIds,
|
|
9286
9397
|
};
|
|
9287
9398
|
}
|
|
9288
9399
|
}
|
|
9289
|
-
if (!mapJson ||
|
|
9290
|
-
!pathJson ||
|
|
9291
|
-
!overlayRef.current)
|
|
9400
|
+
if (!mapJson || !pathJson || !overlayRef.current)
|
|
9292
9401
|
return;
|
|
9293
9402
|
// 根据后端推送的实时数据,进行不同处理
|
|
9294
9403
|
if (curMowPartitionData) {
|
package/dist/index.js
CHANGED
|
@@ -69,7 +69,8 @@ const DEFAULT_LINE_WIDTHS = {
|
|
|
69
69
|
const DEFAULT_OPACITIES = {
|
|
70
70
|
FULL: 1.0,
|
|
71
71
|
HIGH: 0.7,
|
|
72
|
-
MEDIUM: 0.6
|
|
72
|
+
MEDIUM: 0.6,
|
|
73
|
+
DOODLE: 0.8};
|
|
73
74
|
/**
|
|
74
75
|
* 默认半径设置
|
|
75
76
|
*/
|
|
@@ -1046,15 +1047,37 @@ class PathLayer extends BaseLayer {
|
|
|
1046
1047
|
d += ' Z ';
|
|
1047
1048
|
}
|
|
1048
1049
|
});
|
|
1049
|
-
// 3. svgElements
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1050
|
+
// 3. svgElements(解析 SVG 字符串并提取 path 数据)
|
|
1051
|
+
Object.values(svgElements).forEach((svgPath) => {
|
|
1052
|
+
const svgPathString = svgPath?.metadata?.svg;
|
|
1053
|
+
if (svgPathString && typeof svgPathString === 'string' && svgPathString.trim()) {
|
|
1054
|
+
// 处理转义字符
|
|
1055
|
+
const processedSvgString = svgPathString.replace(/\\n/g, '\n').replace(/\\"/g, '"');
|
|
1056
|
+
// 解析 SVG 字符串
|
|
1057
|
+
const parser = new DOMParser();
|
|
1058
|
+
const svgDoc = parser.parseFromString(processedSvgString, 'image/svg+xml');
|
|
1059
|
+
const svgElement = svgDoc.documentElement;
|
|
1060
|
+
if (svgElement.tagName === 'svg') {
|
|
1061
|
+
// 查找 path 元素
|
|
1062
|
+
const pathElement = svgElement.querySelector('path');
|
|
1063
|
+
if (pathElement) {
|
|
1064
|
+
const pathData = pathElement.getAttribute('d');
|
|
1065
|
+
if (pathData) {
|
|
1066
|
+
// 获取 SVG 元素的变换参数
|
|
1067
|
+
const centerCoords = svgPath.coordinates?.[0] || [0, 0];
|
|
1068
|
+
const center = [centerCoords[0], centerCoords[1]];
|
|
1069
|
+
const userScale = svgPath.metadata.scale || 1;
|
|
1070
|
+
const direction = svgPath.metadata?.direction || 0;
|
|
1071
|
+
const originalWidth = parseFloat(svgElement.getAttribute('width') || '76');
|
|
1072
|
+
const originalHeight = parseFloat(svgElement.getAttribute('height') || '68');
|
|
1073
|
+
// 应用变换到路径数据
|
|
1074
|
+
const transformedPathData = this.transformSvgPath(pathData, center, userScale, direction, originalWidth, originalHeight);
|
|
1075
|
+
d += transformedPathData + ' ';
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1055
1078
|
}
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1058
1081
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1059
1082
|
path.setAttribute('d', d);
|
|
1060
1083
|
const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
|
|
@@ -1079,47 +1102,132 @@ class PathLayer extends BaseLayer {
|
|
|
1079
1102
|
// 2. 创建一个组,应用 clipPath
|
|
1080
1103
|
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
1081
1104
|
group.setAttribute('clip-path', `url(#${clipPathId})`);
|
|
1082
|
-
group.setAttribute('opacity', '0.
|
|
1083
|
-
// 3.
|
|
1105
|
+
group.setAttribute('opacity', '0.5'); // 统一透明度,防止叠加脏乱
|
|
1106
|
+
// 3. 优化渲染:按样式分组并合并路径
|
|
1107
|
+
this.renderOptimizedPaths(group);
|
|
1108
|
+
svgGroup.appendChild(group);
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* 优化渲染:按样式分组并合并路径,减少 DOM 节点数量
|
|
1112
|
+
*/
|
|
1113
|
+
renderOptimizedPaths(group) {
|
|
1114
|
+
// 按样式分组存储路径数据
|
|
1115
|
+
const styleGroups = new Map();
|
|
1116
|
+
// 收集所有路径数据并按样式分组
|
|
1084
1117
|
for (const element of this.elements) {
|
|
1085
|
-
|
|
1118
|
+
// 类型断言:PathLayer 中的 elements 实际上是 PathElements 结构
|
|
1119
|
+
const pathElement = element;
|
|
1120
|
+
const { id, elements } = pathElement;
|
|
1086
1121
|
this.boundaryPaths[id] = [];
|
|
1122
|
+
elements.forEach((pathElement) => {
|
|
1123
|
+
const { coordinates, style } = pathElement;
|
|
1124
|
+
if (coordinates.length < 2)
|
|
1125
|
+
return;
|
|
1126
|
+
// 生成样式键(用于分组)
|
|
1127
|
+
const styleKey = this.generateStyleKey(style);
|
|
1128
|
+
// 构建路径数据
|
|
1129
|
+
let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
|
|
1130
|
+
for (let i = 1; i < coordinates.length; i++) {
|
|
1131
|
+
pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
|
|
1132
|
+
}
|
|
1133
|
+
// 按样式分组存储
|
|
1134
|
+
if (!styleGroups.has(styleKey)) {
|
|
1135
|
+
styleGroups.set(styleKey, { pathData: [], elements: [] });
|
|
1136
|
+
}
|
|
1137
|
+
styleGroups.get(styleKey).pathData.push(pathData);
|
|
1138
|
+
styleGroups.get(styleKey).elements.push(pathElement);
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
// 为每种样式创建一个合并的 path 元素
|
|
1142
|
+
styleGroups.forEach((groupData) => {
|
|
1143
|
+
const { pathData, elements } = groupData;
|
|
1144
|
+
if (pathData.length === 0)
|
|
1145
|
+
return;
|
|
1146
|
+
// 使用第一个元素的样式作为该组的样式
|
|
1147
|
+
const firstElement = elements[0];
|
|
1148
|
+
const style = firstElement.style;
|
|
1149
|
+
// 创建合并的 path 元素
|
|
1150
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1151
|
+
// 合并所有路径数据
|
|
1152
|
+
const mergedPathData = pathData.join(' ');
|
|
1153
|
+
path.setAttribute('d', mergedPathData);
|
|
1154
|
+
// 设置样式属性
|
|
1155
|
+
path.setAttribute('fill', 'none');
|
|
1156
|
+
path.setAttribute('stroke', style.lineColor || '#000000');
|
|
1157
|
+
path.setAttribute('mix-blend-mode', 'normal');
|
|
1158
|
+
const lineWidth = Math.max(style.lineWidth || 1, 0.5);
|
|
1159
|
+
path.setAttribute('stroke-width', lineWidth.toString());
|
|
1160
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
1161
|
+
path.setAttribute('stroke-linejoin', 'round');
|
|
1162
|
+
path.classList.add('vector-path');
|
|
1163
|
+
// 将合并的 path 添加到组中
|
|
1164
|
+
group.appendChild(path);
|
|
1165
|
+
// 保存引用到 boundaryPaths 中(保持兼容性)
|
|
1087
1166
|
elements.forEach((element) => {
|
|
1088
|
-
|
|
1167
|
+
const { id } = element;
|
|
1168
|
+
if (!this.boundaryPaths[id]) {
|
|
1169
|
+
this.boundaryPaths[id] = [];
|
|
1170
|
+
}
|
|
1171
|
+
this.boundaryPaths[id].push(path);
|
|
1089
1172
|
});
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* 变换 SVG 路径数据
|
|
1177
|
+
*/
|
|
1178
|
+
transformSvgPath(pathData, center, scale, direction, originalWidth, originalHeight) {
|
|
1179
|
+
// 解析路径数据并应用变换
|
|
1180
|
+
const commands = pathData.match(/[MmLlHhVvCcSsQqTtAaZz][^MmLlHhVvCcSsQqTtAaZz]*/g) || [];
|
|
1181
|
+
let transformedCommands = [];
|
|
1182
|
+
for (const command of commands) {
|
|
1183
|
+
const type = command[0];
|
|
1184
|
+
const params = command
|
|
1185
|
+
.slice(1)
|
|
1186
|
+
.trim()
|
|
1187
|
+
.split(/[\s,]+/)
|
|
1188
|
+
.filter(Boolean)
|
|
1189
|
+
.map(Number);
|
|
1190
|
+
if (type === 'Z' || type === 'z') {
|
|
1191
|
+
// 闭合路径,不需要变换
|
|
1192
|
+
transformedCommands.push(command);
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
// 处理坐标参数
|
|
1196
|
+
let transformedParams = [];
|
|
1197
|
+
for (let i = 0; i < params.length; i += 2) {
|
|
1198
|
+
if (i + 1 < params.length) {
|
|
1199
|
+
let x = params[i];
|
|
1200
|
+
let y = params[i + 1];
|
|
1201
|
+
// 应用变换:先平移到中心,然后缩放、旋转,最后平移到目标位置
|
|
1202
|
+
// 1. 平移到原点(相对于原始尺寸的中心)
|
|
1203
|
+
x -= originalWidth / 2;
|
|
1204
|
+
y -= originalHeight / 2;
|
|
1205
|
+
// 2. 应用缩放
|
|
1206
|
+
x *= scale;
|
|
1207
|
+
y *= scale;
|
|
1208
|
+
// 3. 应用旋转
|
|
1209
|
+
const cos = Math.cos(-direction);
|
|
1210
|
+
const sin = Math.sin(-direction);
|
|
1211
|
+
const newX = x * cos - y * sin;
|
|
1212
|
+
const newY = x * sin + y * cos;
|
|
1213
|
+
// 4. 平移到目标位置
|
|
1214
|
+
x = newX + center[0];
|
|
1215
|
+
y = newY + center[1];
|
|
1216
|
+
transformedParams.push(x, y);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
// 重建命令
|
|
1220
|
+
if (transformedParams.length > 0) {
|
|
1221
|
+
transformedCommands.push(type + transformedParams.join(' '));
|
|
1222
|
+
}
|
|
1090
1223
|
}
|
|
1091
|
-
|
|
1224
|
+
return transformedCommands.join(' ');
|
|
1092
1225
|
}
|
|
1093
1226
|
/**
|
|
1094
|
-
*
|
|
1227
|
+
* 生成样式键,用于路径分组
|
|
1095
1228
|
*/
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
if (coordinates.length < 2)
|
|
1099
|
-
return;
|
|
1100
|
-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1101
|
-
// 构建路径数据
|
|
1102
|
-
let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
|
|
1103
|
-
for (let i = 1; i < coordinates.length; i++) {
|
|
1104
|
-
pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
|
|
1105
|
-
}
|
|
1106
|
-
path.style.mixBlendMode = 'normal';
|
|
1107
|
-
// 设置路径属性
|
|
1108
|
-
path.setAttribute('d', pathData);
|
|
1109
|
-
// 直接给fill的颜色设置透明度会导致path重叠的部分颜色叠加,所以使用fill填充实色,通过fill-opacity设置透明度
|
|
1110
|
-
path.setAttribute('fill', 'none');
|
|
1111
|
-
// path.setAttribute('fill-opacity', '0.4');
|
|
1112
|
-
path.setAttribute('stroke', style.lineColor || '#000000');
|
|
1113
|
-
path.setAttribute('mix-blend-mode', 'normal');
|
|
1114
|
-
const lineWidth = Math.max(style.lineWidth || 1, 0.5);
|
|
1115
|
-
path.setAttribute('stroke-width', lineWidth.toString());
|
|
1116
|
-
path.setAttribute('stroke-linecap', 'round');
|
|
1117
|
-
path.setAttribute('stroke-linejoin', 'round');
|
|
1118
|
-
// 注意:这里不设置 opacity,因为透明度由父组控制
|
|
1119
|
-
// path.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
1120
|
-
path.classList.add('vector-path');
|
|
1121
|
-
this.boundaryPaths[id].push(path);
|
|
1122
|
-
group.appendChild(path);
|
|
1229
|
+
generateStyleKey(style) {
|
|
1230
|
+
return `${style.lineColor || '#000000'}-${style.lineWidth || 1}-${style.opacity || 1}`;
|
|
1123
1231
|
}
|
|
1124
1232
|
}
|
|
1125
1233
|
|
|
@@ -1543,7 +1651,7 @@ const DOODLE_STYLES = {
|
|
|
1543
1651
|
lineColor: '#ff5722',
|
|
1544
1652
|
fillColor: '#ff9800', // 粉色半透明填充
|
|
1545
1653
|
lineWidth: DEFAULT_LINE_WIDTHS.TIME_LIMIT_OBSTACLE,
|
|
1546
|
-
opacity: DEFAULT_OPACITIES.
|
|
1654
|
+
opacity: DEFAULT_OPACITIES.DOODLE,
|
|
1547
1655
|
};
|
|
1548
1656
|
const PATH_EDGE_STYLES = {
|
|
1549
1657
|
lineWidth: DEFAULT_LINE_WIDTHS.PATH,
|
|
@@ -8904,7 +9012,7 @@ const MowerMapRenderer = React.forwardRef(({ unitType = UnitsType.Imperial, lang
|
|
|
8904
9012
|
// const mapRef = useMap();
|
|
8905
9013
|
const [isGoogleMapsReady, setIsGoogleMapsReady] = React.useState(false);
|
|
8906
9014
|
const [hasInitializedBounds, setHasInitializedBounds] = React.useState(false);
|
|
8907
|
-
const { clearSubBoundaryBorder, clearObstacles } = useSubBoundaryBorderStore();
|
|
9015
|
+
const { clearSubBoundaryBorder, clearObstacles, clearSvgElements } = useSubBoundaryBorderStore();
|
|
8908
9016
|
const currentProcessMowingStatusRef = React.useRef(false);
|
|
8909
9017
|
const { updateProcessStateIsMowing, processStateIsMowing } = useProcessMowingState();
|
|
8910
9018
|
const [mowPartitionData, setMowPartitionData] = React.useState(null);
|
|
@@ -9073,6 +9181,7 @@ const MowerMapRenderer = React.forwardRef(({ unitType = UnitsType.Imperial, lang
|
|
|
9073
9181
|
return () => {
|
|
9074
9182
|
clearSubBoundaryBorder();
|
|
9075
9183
|
clearObstacles();
|
|
9184
|
+
clearSvgElements();
|
|
9076
9185
|
updateProcessStateIsMowing(false);
|
|
9077
9186
|
currentProcessMowingStatusRef.current = false;
|
|
9078
9187
|
if (overlayRef.current) {
|
|
@@ -9263,8 +9372,8 @@ const MowerMapRenderer = React.forwardRef(({ unitType = UnitsType.Imperial, lang
|
|
|
9263
9372
|
setMowPartitionData(mowingPartition);
|
|
9264
9373
|
curMowPartitionData = mowingPartition;
|
|
9265
9374
|
}
|
|
9266
|
-
const positionData = realTimeData?.find(item => item?.type === RealTimeDataType.LOCATION);
|
|
9267
|
-
const statusData = realTimeData?.find(item => item?.type === RealTimeDataType.STATUS);
|
|
9375
|
+
const positionData = realTimeData?.find((item) => item?.type === RealTimeDataType.LOCATION);
|
|
9376
|
+
const statusData = realTimeData?.find((item) => item?.type === RealTimeDataType.STATUS);
|
|
9268
9377
|
if (statusData || positionData) {
|
|
9269
9378
|
const currentStatus = statusData?.vehicleState || positionData?.vehicleState;
|
|
9270
9379
|
// 车辆回桩不会回传最后的park的位置,所以根据实时数据的状态数据判断车辆回到桩上
|
|
@@ -9277,20 +9386,20 @@ const MowerMapRenderer = React.forwardRef(({ unitType = UnitsType.Imperial, lang
|
|
|
9277
9386
|
setMowPartitionData(null);
|
|
9278
9387
|
curMowPartitionData = null;
|
|
9279
9388
|
}
|
|
9280
|
-
else if (currentStatus === RobotStatus.MOWING &&
|
|
9389
|
+
else if (currentStatus === RobotStatus.MOWING &&
|
|
9390
|
+
curMowPartitionData &&
|
|
9391
|
+
!curMowPartitionData?.partitionIds) {
|
|
9281
9392
|
// 如果当前是割草状态,但是地块数据初始化过且不存在则认为是全局割草,则把所有地块都高亮
|
|
9282
|
-
const allPartitionIds = mapJson?.sub_maps?.map(item => item?.id);
|
|
9393
|
+
const allPartitionIds = mapJson?.sub_maps?.map((item) => item?.id);
|
|
9283
9394
|
setMowPartitionData({
|
|
9284
|
-
partitionIds: allPartitionIds
|
|
9395
|
+
partitionIds: allPartitionIds,
|
|
9285
9396
|
});
|
|
9286
9397
|
curMowPartitionData = {
|
|
9287
|
-
partitionIds: allPartitionIds
|
|
9398
|
+
partitionIds: allPartitionIds,
|
|
9288
9399
|
};
|
|
9289
9400
|
}
|
|
9290
9401
|
}
|
|
9291
|
-
if (!mapJson ||
|
|
9292
|
-
!pathJson ||
|
|
9293
|
-
!overlayRef.current)
|
|
9402
|
+
if (!mapJson || !pathJson || !overlayRef.current)
|
|
9294
9403
|
return;
|
|
9295
9404
|
// 根据后端推送的实时数据,进行不同处理
|
|
9296
9405
|
if (curMowPartitionData) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MowerMapRenderer.d.ts","sourceRoot":"","sources":["../../src/render/MowerMapRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AA6Cf,OAAO,EAAa,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAO1F,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,MAAM,EAAE,GAAG,CAAC;KACb;CACF;AAgGD,eAAO,MAAM,gBAAgB,
|
|
1
|
+
{"version":3,"file":"MowerMapRenderer.d.ts","sourceRoot":"","sources":["../../src/render/MowerMapRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AA6Cf,OAAO,EAAa,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAO1F,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,MAAM,EAAE,GAAG,CAAC;KACb;CACF;AAgGD,eAAO,MAAM,gBAAgB,mGA+sB5B,CAAC;AAIF,eAAe,gBAAgB,CAAC;AAChC,YAAY,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,CAAC"}
|
|
@@ -18,8 +18,16 @@ export declare class PathLayer extends BaseLayer {
|
|
|
18
18
|
*/
|
|
19
19
|
drawSVG(svgGroup: SVGGElement, scale: number, lineScale: number): void;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* 优化渲染:按样式分组并合并路径,减少 DOM 节点数量
|
|
22
22
|
*/
|
|
23
|
-
private
|
|
23
|
+
private renderOptimizedPaths;
|
|
24
|
+
/**
|
|
25
|
+
* 变换 SVG 路径数据
|
|
26
|
+
*/
|
|
27
|
+
private transformSvgPath;
|
|
28
|
+
/**
|
|
29
|
+
* 生成样式键,用于路径分组
|
|
30
|
+
*/
|
|
31
|
+
private generateStyleKey;
|
|
24
32
|
}
|
|
25
33
|
//# sourceMappingURL=PathLayer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PathLayer.d.ts","sourceRoot":"","sources":["../../../src/render/layers/PathLayer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PathLayer.d.ts","sourceRoot":"","sources":["../../../src/render/layers/PathLayer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;GAGG;AACH,qBAAa,SAAU,SAAQ,SAAS;IACtC,KAAK,EAAE,MAAM,CAAK;IAClB,KAAK,EAAE,MAAM,CAAK;IAClB,SAAS,EAAE,MAAM,CAAK;IACtB,aAAa,EAAE,GAAG,CAAM;;IAOxB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAmG3B;;OAEG;IACI,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAuB7E;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA2E5B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAkExB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAGzB"}
|