@fleet-frontend/mower-maps 0.0.9-beta.9 → 0.1.0-beta.1

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.
Files changed (51) hide show
  1. package/dist/config/constants.d.ts +1 -0
  2. package/dist/config/constants.d.ts.map +1 -1
  3. package/dist/config/styles.d.ts +1 -1
  4. package/dist/config/styles.d.ts.map +1 -1
  5. package/dist/index.esm.js +3412 -398
  6. package/dist/index.js +3412 -398
  7. package/dist/processor/MapDataProcessor.d.ts.map +1 -1
  8. package/dist/render/BoundaryLabelsManager.d.ts +7 -2
  9. package/dist/render/BoundaryLabelsManager.d.ts.map +1 -1
  10. package/dist/render/MowerMapOverlay.d.ts +6 -1
  11. package/dist/render/MowerMapOverlay.d.ts.map +1 -1
  12. package/dist/render/MowerMapRenderer.d.ts.map +1 -1
  13. package/dist/render/MowerPositionManager.d.ts +2 -0
  14. package/dist/render/MowerPositionManager.d.ts.map +1 -1
  15. package/dist/render/SvgMapView.d.ts.map +1 -1
  16. package/dist/render/layers/BoundaryBorderLayer.d.ts +4 -0
  17. package/dist/render/layers/BoundaryBorderLayer.d.ts.map +1 -1
  18. package/dist/render/layers/ChannelLayer.d.ts +21 -0
  19. package/dist/render/layers/ChannelLayer.d.ts.map +1 -1
  20. package/dist/render/layers/PathLayer.d.ts +8 -4
  21. package/dist/render/layers/PathLayer.d.ts.map +1 -1
  22. package/dist/store/useCurrentMowingDataStore.d.ts +4 -0
  23. package/dist/store/useCurrentMowingDataStore.d.ts.map +1 -0
  24. package/dist/store/usePartitionDataStore.d.ts +4 -0
  25. package/dist/store/usePartitionDataStore.d.ts.map +1 -0
  26. package/dist/types/renderer.d.ts +4 -0
  27. package/dist/types/renderer.d.ts.map +1 -1
  28. package/dist/types/store.d.ts +13 -5
  29. package/dist/types/store.d.ts.map +1 -1
  30. package/dist/types/utils.d.ts +3 -3
  31. package/dist/types/utils.d.ts.map +1 -1
  32. package/dist/utils/boundaryUtils.d.ts.map +1 -1
  33. package/dist/utils/common.d.ts +10 -0
  34. package/dist/utils/common.d.ts.map +1 -1
  35. package/dist/utils/coordinates.d.ts +27 -0
  36. package/dist/utils/coordinates.d.ts.map +1 -1
  37. package/dist/utils/handleRealTime.d.ts +6 -2
  38. package/dist/utils/handleRealTime.d.ts.map +1 -1
  39. package/dist/utils/math.d.ts +1 -1
  40. package/dist/utils/math.d.ts.map +1 -1
  41. package/dist/utils/mower.d.ts +2 -2
  42. package/dist/utils/mower.d.ts.map +1 -1
  43. package/dist/utils/pathSegments.d.ts.map +1 -1
  44. package/dist/utils/pointInBoundary.d.ts.map +1 -1
  45. package/dist/utils/unionFind.d.ts +49 -0
  46. package/dist/utils/unionFind.d.ts.map +1 -0
  47. package/package.json +2 -1
  48. package/dist/store/processMowingState.d.ts +0 -4
  49. package/dist/store/processMowingState.d.ts.map +0 -1
  50. package/dist/store/useSubBoundaryBorderStore.d.ts +0 -15
  51. package/dist/store/useSubBoundaryBorderStore.d.ts.map +0 -1
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
  */
@@ -228,7 +229,6 @@ class SvgMapView {
228
229
  */
229
230
  removeLayer(layer) {
230
231
  const index = this.layers.indexOf(layer);
231
- console.log('removeLayer----->', index);
232
232
  if (index !== -1) {
233
233
  this.layers.splice(index, 1);
234
234
  this.refresh();
@@ -276,7 +276,6 @@ class SvgMapView {
276
276
  width: boundWidth + padding * 2,
277
277
  height: boundHeight + padding * 2,
278
278
  };
279
- console.log('viewbox->', this.viewBox);
280
279
  // 根据宽高比选择合适的preserveAspectRatio设置
281
280
  if (Math.abs(contentAspectRatio - containerAspectRatio) < 0.01) {
282
281
  // 宽高比接近,使用slice填满容器
@@ -286,7 +285,6 @@ class SvgMapView {
286
285
  // 宽高比差异较大,使用meet确保内容完全可见
287
286
  this.svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
288
287
  }
289
- console.log('fitToView');
290
288
  this.updateViewBox();
291
289
  }
292
290
  /**
@@ -357,7 +355,6 @@ class SvgMapView {
357
355
  * 绘制图层,不传参数则默认绘制所有图层
358
356
  */
359
357
  onDrawLayers(type) {
360
- console.log('onDrawLayers----->', type);
361
358
  if (type) {
362
359
  const layer = this.layers.find((layer) => layer.getType() === type);
363
360
  if (layer) {
@@ -434,7 +431,6 @@ class SvgMapView {
434
431
  refresh() {
435
432
  if (this.destroyed)
436
433
  return;
437
- console.log('refresh----->');
438
434
  this.render();
439
435
  }
440
436
  // ==================== 拖拽功能 ====================
@@ -780,7 +776,7 @@ const createImpl = (createState) => {
780
776
  };
781
777
  const create = (createState) => createState ? createImpl(createState) : createImpl;
782
778
 
783
- const useSubBoundaryBorderStore = create((set, get) => ({
779
+ const usePartitionDataStore = create((set, get) => ({
784
780
  subBoundaryBorder: {},
785
781
  // 追加单个数据
786
782
  addSubBoundaryBorder: (key, element) => set((state) => ({
@@ -811,6 +807,2484 @@ const useSubBoundaryBorderStore = create((set, get) => ({
811
807
  clearSvgElements: () => set({ svgElements: {} }),
812
808
  }));
813
809
 
810
+ /**
811
+ * splaytree v3.1.2
812
+ * Fast Splay tree for Node and browser
813
+ *
814
+ * @author Alexander Milevski <info@w8r.name>
815
+ * @license MIT
816
+ * @preserve
817
+ */
818
+
819
+ /*! *****************************************************************************
820
+ Copyright (c) Microsoft Corporation. All rights reserved.
821
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
822
+ this file except in compliance with the License. You may obtain a copy of the
823
+ License at http://www.apache.org/licenses/LICENSE-2.0
824
+
825
+ THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
826
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
827
+ WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
828
+ MERCHANTABLITY OR NON-INFRINGEMENT.
829
+
830
+ See the Apache Version 2.0 License for specific language governing permissions
831
+ and limitations under the License.
832
+ ***************************************************************************** */
833
+
834
+ function __generator(thisArg, body) {
835
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
836
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
837
+ function verb(n) { return function (v) { return step([n, v]); }; }
838
+ function step(op) {
839
+ if (f) throw new TypeError("Generator is already executing.");
840
+ while (_) try {
841
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
842
+ if (y = 0, t) op = [op[0] & 2, t.value];
843
+ switch (op[0]) {
844
+ case 0: case 1: t = op; break;
845
+ case 4: _.label++; return { value: op[1], done: false };
846
+ case 5: _.label++; y = op[1]; op = [0]; continue;
847
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
848
+ default:
849
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
850
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
851
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
852
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
853
+ if (t[2]) _.ops.pop();
854
+ _.trys.pop(); continue;
855
+ }
856
+ op = body.call(thisArg, _);
857
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
858
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
859
+ }
860
+ }
861
+
862
+ var Node = /** @class */ (function () {
863
+ function Node(key, data) {
864
+ this.next = null;
865
+ this.key = key;
866
+ this.data = data;
867
+ this.left = null;
868
+ this.right = null;
869
+ }
870
+ return Node;
871
+ }());
872
+
873
+ /* follows "An implementation of top-down splaying"
874
+ * by D. Sleator <sleator@cs.cmu.edu> March 1992
875
+ */
876
+ function DEFAULT_COMPARE(a, b) {
877
+ return a > b ? 1 : a < b ? -1 : 0;
878
+ }
879
+ /**
880
+ * Simple top down splay, not requiring i to be in the tree t.
881
+ */
882
+ function splay(i, t, comparator) {
883
+ var N = new Node(null, null);
884
+ var l = N;
885
+ var r = N;
886
+ while (true) {
887
+ var cmp = comparator(i, t.key);
888
+ //if (i < t.key) {
889
+ if (cmp < 0) {
890
+ if (t.left === null)
891
+ break;
892
+ //if (i < t.left.key) {
893
+ if (comparator(i, t.left.key) < 0) {
894
+ var y = t.left; /* rotate right */
895
+ t.left = y.right;
896
+ y.right = t;
897
+ t = y;
898
+ if (t.left === null)
899
+ break;
900
+ }
901
+ r.left = t; /* link right */
902
+ r = t;
903
+ t = t.left;
904
+ //} else if (i > t.key) {
905
+ }
906
+ else if (cmp > 0) {
907
+ if (t.right === null)
908
+ break;
909
+ //if (i > t.right.key) {
910
+ if (comparator(i, t.right.key) > 0) {
911
+ var y = t.right; /* rotate left */
912
+ t.right = y.left;
913
+ y.left = t;
914
+ t = y;
915
+ if (t.right === null)
916
+ break;
917
+ }
918
+ l.right = t; /* link left */
919
+ l = t;
920
+ t = t.right;
921
+ }
922
+ else
923
+ break;
924
+ }
925
+ /* assemble */
926
+ l.right = t.left;
927
+ r.left = t.right;
928
+ t.left = N.right;
929
+ t.right = N.left;
930
+ return t;
931
+ }
932
+ function insert(i, data, t, comparator) {
933
+ var node = new Node(i, data);
934
+ if (t === null) {
935
+ node.left = node.right = null;
936
+ return node;
937
+ }
938
+ t = splay(i, t, comparator);
939
+ var cmp = comparator(i, t.key);
940
+ if (cmp < 0) {
941
+ node.left = t.left;
942
+ node.right = t;
943
+ t.left = null;
944
+ }
945
+ else if (cmp >= 0) {
946
+ node.right = t.right;
947
+ node.left = t;
948
+ t.right = null;
949
+ }
950
+ return node;
951
+ }
952
+ function split(key, v, comparator) {
953
+ var left = null;
954
+ var right = null;
955
+ if (v) {
956
+ v = splay(key, v, comparator);
957
+ var cmp = comparator(v.key, key);
958
+ if (cmp === 0) {
959
+ left = v.left;
960
+ right = v.right;
961
+ }
962
+ else if (cmp < 0) {
963
+ right = v.right;
964
+ v.right = null;
965
+ left = v;
966
+ }
967
+ else {
968
+ left = v.left;
969
+ v.left = null;
970
+ right = v;
971
+ }
972
+ }
973
+ return { left: left, right: right };
974
+ }
975
+ function merge$1(left, right, comparator) {
976
+ if (right === null)
977
+ return left;
978
+ if (left === null)
979
+ return right;
980
+ right = splay(left.key, right, comparator);
981
+ right.left = left;
982
+ return right;
983
+ }
984
+ /**
985
+ * Prints level of the tree
986
+ */
987
+ function printRow(root, prefix, isTail, out, printNode) {
988
+ if (root) {
989
+ out("" + prefix + (isTail ? '└── ' : '├── ') + printNode(root) + "\n");
990
+ var indent = prefix + (isTail ? ' ' : '│ ');
991
+ if (root.left)
992
+ printRow(root.left, indent, false, out, printNode);
993
+ if (root.right)
994
+ printRow(root.right, indent, true, out, printNode);
995
+ }
996
+ }
997
+ var Tree = /** @class */ (function () {
998
+ function Tree(comparator) {
999
+ if (comparator === void 0) { comparator = DEFAULT_COMPARE; }
1000
+ this._root = null;
1001
+ this._size = 0;
1002
+ this._comparator = comparator;
1003
+ }
1004
+ /**
1005
+ * Inserts a key, allows duplicates
1006
+ */
1007
+ Tree.prototype.insert = function (key, data) {
1008
+ this._size++;
1009
+ return this._root = insert(key, data, this._root, this._comparator);
1010
+ };
1011
+ /**
1012
+ * Adds a key, if it is not present in the tree
1013
+ */
1014
+ Tree.prototype.add = function (key, data) {
1015
+ var node = new Node(key, data);
1016
+ if (this._root === null) {
1017
+ node.left = node.right = null;
1018
+ this._size++;
1019
+ this._root = node;
1020
+ }
1021
+ var comparator = this._comparator;
1022
+ var t = splay(key, this._root, comparator);
1023
+ var cmp = comparator(key, t.key);
1024
+ if (cmp === 0)
1025
+ this._root = t;
1026
+ else {
1027
+ if (cmp < 0) {
1028
+ node.left = t.left;
1029
+ node.right = t;
1030
+ t.left = null;
1031
+ }
1032
+ else if (cmp > 0) {
1033
+ node.right = t.right;
1034
+ node.left = t;
1035
+ t.right = null;
1036
+ }
1037
+ this._size++;
1038
+ this._root = node;
1039
+ }
1040
+ return this._root;
1041
+ };
1042
+ /**
1043
+ * @param {Key} key
1044
+ * @return {Node|null}
1045
+ */
1046
+ Tree.prototype.remove = function (key) {
1047
+ this._root = this._remove(key, this._root, this._comparator);
1048
+ };
1049
+ /**
1050
+ * Deletes i from the tree if it's there
1051
+ */
1052
+ Tree.prototype._remove = function (i, t, comparator) {
1053
+ var x;
1054
+ if (t === null)
1055
+ return null;
1056
+ t = splay(i, t, comparator);
1057
+ var cmp = comparator(i, t.key);
1058
+ if (cmp === 0) { /* found it */
1059
+ if (t.left === null) {
1060
+ x = t.right;
1061
+ }
1062
+ else {
1063
+ x = splay(i, t.left, comparator);
1064
+ x.right = t.right;
1065
+ }
1066
+ this._size--;
1067
+ return x;
1068
+ }
1069
+ return t; /* It wasn't there */
1070
+ };
1071
+ /**
1072
+ * Removes and returns the node with smallest key
1073
+ */
1074
+ Tree.prototype.pop = function () {
1075
+ var node = this._root;
1076
+ if (node) {
1077
+ while (node.left)
1078
+ node = node.left;
1079
+ this._root = splay(node.key, this._root, this._comparator);
1080
+ this._root = this._remove(node.key, this._root, this._comparator);
1081
+ return { key: node.key, data: node.data };
1082
+ }
1083
+ return null;
1084
+ };
1085
+ /**
1086
+ * Find without splaying
1087
+ */
1088
+ Tree.prototype.findStatic = function (key) {
1089
+ var current = this._root;
1090
+ var compare = this._comparator;
1091
+ while (current) {
1092
+ var cmp = compare(key, current.key);
1093
+ if (cmp === 0)
1094
+ return current;
1095
+ else if (cmp < 0)
1096
+ current = current.left;
1097
+ else
1098
+ current = current.right;
1099
+ }
1100
+ return null;
1101
+ };
1102
+ Tree.prototype.find = function (key) {
1103
+ if (this._root) {
1104
+ this._root = splay(key, this._root, this._comparator);
1105
+ if (this._comparator(key, this._root.key) !== 0)
1106
+ return null;
1107
+ }
1108
+ return this._root;
1109
+ };
1110
+ Tree.prototype.contains = function (key) {
1111
+ var current = this._root;
1112
+ var compare = this._comparator;
1113
+ while (current) {
1114
+ var cmp = compare(key, current.key);
1115
+ if (cmp === 0)
1116
+ return true;
1117
+ else if (cmp < 0)
1118
+ current = current.left;
1119
+ else
1120
+ current = current.right;
1121
+ }
1122
+ return false;
1123
+ };
1124
+ Tree.prototype.forEach = function (visitor, ctx) {
1125
+ var current = this._root;
1126
+ var Q = []; /* Initialize stack s */
1127
+ var done = false;
1128
+ while (!done) {
1129
+ if (current !== null) {
1130
+ Q.push(current);
1131
+ current = current.left;
1132
+ }
1133
+ else {
1134
+ if (Q.length !== 0) {
1135
+ current = Q.pop();
1136
+ visitor.call(ctx, current);
1137
+ current = current.right;
1138
+ }
1139
+ else
1140
+ done = true;
1141
+ }
1142
+ }
1143
+ return this;
1144
+ };
1145
+ /**
1146
+ * Walk key range from `low` to `high`. Stops if `fn` returns a value.
1147
+ */
1148
+ Tree.prototype.range = function (low, high, fn, ctx) {
1149
+ var Q = [];
1150
+ var compare = this._comparator;
1151
+ var node = this._root;
1152
+ var cmp;
1153
+ while (Q.length !== 0 || node) {
1154
+ if (node) {
1155
+ Q.push(node);
1156
+ node = node.left;
1157
+ }
1158
+ else {
1159
+ node = Q.pop();
1160
+ cmp = compare(node.key, high);
1161
+ if (cmp > 0) {
1162
+ break;
1163
+ }
1164
+ else if (compare(node.key, low) >= 0) {
1165
+ if (fn.call(ctx, node))
1166
+ return this; // stop if smth is returned
1167
+ }
1168
+ node = node.right;
1169
+ }
1170
+ }
1171
+ return this;
1172
+ };
1173
+ /**
1174
+ * Returns array of keys
1175
+ */
1176
+ Tree.prototype.keys = function () {
1177
+ var keys = [];
1178
+ this.forEach(function (_a) {
1179
+ var key = _a.key;
1180
+ return keys.push(key);
1181
+ });
1182
+ return keys;
1183
+ };
1184
+ /**
1185
+ * Returns array of all the data in the nodes
1186
+ */
1187
+ Tree.prototype.values = function () {
1188
+ var values = [];
1189
+ this.forEach(function (_a) {
1190
+ var data = _a.data;
1191
+ return values.push(data);
1192
+ });
1193
+ return values;
1194
+ };
1195
+ Tree.prototype.min = function () {
1196
+ if (this._root)
1197
+ return this.minNode(this._root).key;
1198
+ return null;
1199
+ };
1200
+ Tree.prototype.max = function () {
1201
+ if (this._root)
1202
+ return this.maxNode(this._root).key;
1203
+ return null;
1204
+ };
1205
+ Tree.prototype.minNode = function (t) {
1206
+ if (t === void 0) { t = this._root; }
1207
+ if (t)
1208
+ while (t.left)
1209
+ t = t.left;
1210
+ return t;
1211
+ };
1212
+ Tree.prototype.maxNode = function (t) {
1213
+ if (t === void 0) { t = this._root; }
1214
+ if (t)
1215
+ while (t.right)
1216
+ t = t.right;
1217
+ return t;
1218
+ };
1219
+ /**
1220
+ * Returns node at given index
1221
+ */
1222
+ Tree.prototype.at = function (index) {
1223
+ var current = this._root;
1224
+ var done = false;
1225
+ var i = 0;
1226
+ var Q = [];
1227
+ while (!done) {
1228
+ if (current) {
1229
+ Q.push(current);
1230
+ current = current.left;
1231
+ }
1232
+ else {
1233
+ if (Q.length > 0) {
1234
+ current = Q.pop();
1235
+ if (i === index)
1236
+ return current;
1237
+ i++;
1238
+ current = current.right;
1239
+ }
1240
+ else
1241
+ done = true;
1242
+ }
1243
+ }
1244
+ return null;
1245
+ };
1246
+ Tree.prototype.next = function (d) {
1247
+ var root = this._root;
1248
+ var successor = null;
1249
+ if (d.right) {
1250
+ successor = d.right;
1251
+ while (successor.left)
1252
+ successor = successor.left;
1253
+ return successor;
1254
+ }
1255
+ var comparator = this._comparator;
1256
+ while (root) {
1257
+ var cmp = comparator(d.key, root.key);
1258
+ if (cmp === 0)
1259
+ break;
1260
+ else if (cmp < 0) {
1261
+ successor = root;
1262
+ root = root.left;
1263
+ }
1264
+ else
1265
+ root = root.right;
1266
+ }
1267
+ return successor;
1268
+ };
1269
+ Tree.prototype.prev = function (d) {
1270
+ var root = this._root;
1271
+ var predecessor = null;
1272
+ if (d.left !== null) {
1273
+ predecessor = d.left;
1274
+ while (predecessor.right)
1275
+ predecessor = predecessor.right;
1276
+ return predecessor;
1277
+ }
1278
+ var comparator = this._comparator;
1279
+ while (root) {
1280
+ var cmp = comparator(d.key, root.key);
1281
+ if (cmp === 0)
1282
+ break;
1283
+ else if (cmp < 0)
1284
+ root = root.left;
1285
+ else {
1286
+ predecessor = root;
1287
+ root = root.right;
1288
+ }
1289
+ }
1290
+ return predecessor;
1291
+ };
1292
+ Tree.prototype.clear = function () {
1293
+ this._root = null;
1294
+ this._size = 0;
1295
+ return this;
1296
+ };
1297
+ Tree.prototype.toList = function () {
1298
+ return toList(this._root);
1299
+ };
1300
+ /**
1301
+ * Bulk-load items. Both array have to be same size
1302
+ */
1303
+ Tree.prototype.load = function (keys, values, presort) {
1304
+ if (values === void 0) { values = []; }
1305
+ if (presort === void 0) { presort = false; }
1306
+ var size = keys.length;
1307
+ var comparator = this._comparator;
1308
+ // sort if needed
1309
+ if (presort)
1310
+ sort(keys, values, 0, size - 1, comparator);
1311
+ if (this._root === null) { // empty tree
1312
+ this._root = loadRecursive(keys, values, 0, size);
1313
+ this._size = size;
1314
+ }
1315
+ else { // that re-builds the whole tree from two in-order traversals
1316
+ var mergedList = mergeLists(this.toList(), createList(keys, values), comparator);
1317
+ size = this._size + size;
1318
+ this._root = sortedListToBST({ head: mergedList }, 0, size);
1319
+ }
1320
+ return this;
1321
+ };
1322
+ Tree.prototype.isEmpty = function () { return this._root === null; };
1323
+ Object.defineProperty(Tree.prototype, "size", {
1324
+ get: function () { return this._size; },
1325
+ enumerable: true,
1326
+ configurable: true
1327
+ });
1328
+ Object.defineProperty(Tree.prototype, "root", {
1329
+ get: function () { return this._root; },
1330
+ enumerable: true,
1331
+ configurable: true
1332
+ });
1333
+ Tree.prototype.toString = function (printNode) {
1334
+ if (printNode === void 0) { printNode = function (n) { return String(n.key); }; }
1335
+ var out = [];
1336
+ printRow(this._root, '', true, function (v) { return out.push(v); }, printNode);
1337
+ return out.join('');
1338
+ };
1339
+ Tree.prototype.update = function (key, newKey, newData) {
1340
+ var comparator = this._comparator;
1341
+ var _a = split(key, this._root, comparator), left = _a.left, right = _a.right;
1342
+ if (comparator(key, newKey) < 0) {
1343
+ right = insert(newKey, newData, right, comparator);
1344
+ }
1345
+ else {
1346
+ left = insert(newKey, newData, left, comparator);
1347
+ }
1348
+ this._root = merge$1(left, right, comparator);
1349
+ };
1350
+ Tree.prototype.split = function (key) {
1351
+ return split(key, this._root, this._comparator);
1352
+ };
1353
+ Tree.prototype[Symbol.iterator] = function () {
1354
+ var current, Q, done;
1355
+ return __generator(this, function (_a) {
1356
+ switch (_a.label) {
1357
+ case 0:
1358
+ current = this._root;
1359
+ Q = [];
1360
+ done = false;
1361
+ _a.label = 1;
1362
+ case 1:
1363
+ if (!!done) return [3 /*break*/, 6];
1364
+ if (!(current !== null)) return [3 /*break*/, 2];
1365
+ Q.push(current);
1366
+ current = current.left;
1367
+ return [3 /*break*/, 5];
1368
+ case 2:
1369
+ if (!(Q.length !== 0)) return [3 /*break*/, 4];
1370
+ current = Q.pop();
1371
+ return [4 /*yield*/, current];
1372
+ case 3:
1373
+ _a.sent();
1374
+ current = current.right;
1375
+ return [3 /*break*/, 5];
1376
+ case 4:
1377
+ done = true;
1378
+ _a.label = 5;
1379
+ case 5: return [3 /*break*/, 1];
1380
+ case 6: return [2 /*return*/];
1381
+ }
1382
+ });
1383
+ };
1384
+ return Tree;
1385
+ }());
1386
+ function loadRecursive(keys, values, start, end) {
1387
+ var size = end - start;
1388
+ if (size > 0) {
1389
+ var middle = start + Math.floor(size / 2);
1390
+ var key = keys[middle];
1391
+ var data = values[middle];
1392
+ var node = new Node(key, data);
1393
+ node.left = loadRecursive(keys, values, start, middle);
1394
+ node.right = loadRecursive(keys, values, middle + 1, end);
1395
+ return node;
1396
+ }
1397
+ return null;
1398
+ }
1399
+ function createList(keys, values) {
1400
+ var head = new Node(null, null);
1401
+ var p = head;
1402
+ for (var i = 0; i < keys.length; i++) {
1403
+ p = p.next = new Node(keys[i], values[i]);
1404
+ }
1405
+ p.next = null;
1406
+ return head.next;
1407
+ }
1408
+ function toList(root) {
1409
+ var current = root;
1410
+ var Q = [];
1411
+ var done = false;
1412
+ var head = new Node(null, null);
1413
+ var p = head;
1414
+ while (!done) {
1415
+ if (current) {
1416
+ Q.push(current);
1417
+ current = current.left;
1418
+ }
1419
+ else {
1420
+ if (Q.length > 0) {
1421
+ current = p = p.next = Q.pop();
1422
+ current = current.right;
1423
+ }
1424
+ else
1425
+ done = true;
1426
+ }
1427
+ }
1428
+ p.next = null; // that'll work even if the tree was empty
1429
+ return head.next;
1430
+ }
1431
+ function sortedListToBST(list, start, end) {
1432
+ var size = end - start;
1433
+ if (size > 0) {
1434
+ var middle = start + Math.floor(size / 2);
1435
+ var left = sortedListToBST(list, start, middle);
1436
+ var root = list.head;
1437
+ root.left = left;
1438
+ list.head = list.head.next;
1439
+ root.right = sortedListToBST(list, middle + 1, end);
1440
+ return root;
1441
+ }
1442
+ return null;
1443
+ }
1444
+ function mergeLists(l1, l2, compare) {
1445
+ var head = new Node(null, null); // dummy
1446
+ var p = head;
1447
+ var p1 = l1;
1448
+ var p2 = l2;
1449
+ while (p1 !== null && p2 !== null) {
1450
+ if (compare(p1.key, p2.key) < 0) {
1451
+ p.next = p1;
1452
+ p1 = p1.next;
1453
+ }
1454
+ else {
1455
+ p.next = p2;
1456
+ p2 = p2.next;
1457
+ }
1458
+ p = p.next;
1459
+ }
1460
+ if (p1 !== null) {
1461
+ p.next = p1;
1462
+ }
1463
+ else if (p2 !== null) {
1464
+ p.next = p2;
1465
+ }
1466
+ return head.next;
1467
+ }
1468
+ function sort(keys, values, left, right, compare) {
1469
+ if (left >= right)
1470
+ return;
1471
+ var pivot = keys[(left + right) >> 1];
1472
+ var i = left - 1;
1473
+ var j = right + 1;
1474
+ while (true) {
1475
+ do
1476
+ i++;
1477
+ while (compare(keys[i], pivot) < 0);
1478
+ do
1479
+ j--;
1480
+ while (compare(keys[j], pivot) > 0);
1481
+ if (i >= j)
1482
+ break;
1483
+ var tmp = keys[i];
1484
+ keys[i] = keys[j];
1485
+ keys[j] = tmp;
1486
+ tmp = values[i];
1487
+ values[i] = values[j];
1488
+ values[j] = tmp;
1489
+ }
1490
+ sort(keys, values, left, j, compare);
1491
+ sort(keys, values, j + 1, right, compare);
1492
+ }
1493
+
1494
+ const epsilon$1 = 1.1102230246251565e-16;
1495
+ const splitter = 134217729;
1496
+ const resulterrbound = (3 + 8 * epsilon$1) * epsilon$1;
1497
+
1498
+ // fast_expansion_sum_zeroelim routine from oritinal code
1499
+ function sum(elen, e, flen, f, h) {
1500
+ let Q, Qnew, hh, bvirt;
1501
+ let enow = e[0];
1502
+ let fnow = f[0];
1503
+ let eindex = 0;
1504
+ let findex = 0;
1505
+ if ((fnow > enow) === (fnow > -enow)) {
1506
+ Q = enow;
1507
+ enow = e[++eindex];
1508
+ } else {
1509
+ Q = fnow;
1510
+ fnow = f[++findex];
1511
+ }
1512
+ let hindex = 0;
1513
+ if (eindex < elen && findex < flen) {
1514
+ if ((fnow > enow) === (fnow > -enow)) {
1515
+ Qnew = enow + Q;
1516
+ hh = Q - (Qnew - enow);
1517
+ enow = e[++eindex];
1518
+ } else {
1519
+ Qnew = fnow + Q;
1520
+ hh = Q - (Qnew - fnow);
1521
+ fnow = f[++findex];
1522
+ }
1523
+ Q = Qnew;
1524
+ if (hh !== 0) {
1525
+ h[hindex++] = hh;
1526
+ }
1527
+ while (eindex < elen && findex < flen) {
1528
+ if ((fnow > enow) === (fnow > -enow)) {
1529
+ Qnew = Q + enow;
1530
+ bvirt = Qnew - Q;
1531
+ hh = Q - (Qnew - bvirt) + (enow - bvirt);
1532
+ enow = e[++eindex];
1533
+ } else {
1534
+ Qnew = Q + fnow;
1535
+ bvirt = Qnew - Q;
1536
+ hh = Q - (Qnew - bvirt) + (fnow - bvirt);
1537
+ fnow = f[++findex];
1538
+ }
1539
+ Q = Qnew;
1540
+ if (hh !== 0) {
1541
+ h[hindex++] = hh;
1542
+ }
1543
+ }
1544
+ }
1545
+ while (eindex < elen) {
1546
+ Qnew = Q + enow;
1547
+ bvirt = Qnew - Q;
1548
+ hh = Q - (Qnew - bvirt) + (enow - bvirt);
1549
+ enow = e[++eindex];
1550
+ Q = Qnew;
1551
+ if (hh !== 0) {
1552
+ h[hindex++] = hh;
1553
+ }
1554
+ }
1555
+ while (findex < flen) {
1556
+ Qnew = Q + fnow;
1557
+ bvirt = Qnew - Q;
1558
+ hh = Q - (Qnew - bvirt) + (fnow - bvirt);
1559
+ fnow = f[++findex];
1560
+ Q = Qnew;
1561
+ if (hh !== 0) {
1562
+ h[hindex++] = hh;
1563
+ }
1564
+ }
1565
+ if (Q !== 0 || hindex === 0) {
1566
+ h[hindex++] = Q;
1567
+ }
1568
+ return hindex;
1569
+ }
1570
+
1571
+ function estimate(elen, e) {
1572
+ let Q = e[0];
1573
+ for (let i = 1; i < elen; i++) Q += e[i];
1574
+ return Q;
1575
+ }
1576
+
1577
+ function vec(n) {
1578
+ return new Float64Array(n);
1579
+ }
1580
+
1581
+ const ccwerrboundA = (3 + 16 * epsilon$1) * epsilon$1;
1582
+ const ccwerrboundB = (2 + 12 * epsilon$1) * epsilon$1;
1583
+ const ccwerrboundC = (9 + 64 * epsilon$1) * epsilon$1 * epsilon$1;
1584
+
1585
+ const B = vec(4);
1586
+ const C1 = vec(8);
1587
+ const C2 = vec(12);
1588
+ const D = vec(16);
1589
+ const u = vec(4);
1590
+
1591
+ function orient2dadapt(ax, ay, bx, by, cx, cy, detsum) {
1592
+ let acxtail, acytail, bcxtail, bcytail;
1593
+ let bvirt, c, ahi, alo, bhi, blo, _i, _j, _0, s1, s0, t1, t0, u3;
1594
+
1595
+ const acx = ax - cx;
1596
+ const bcx = bx - cx;
1597
+ const acy = ay - cy;
1598
+ const bcy = by - cy;
1599
+
1600
+ s1 = acx * bcy;
1601
+ c = splitter * acx;
1602
+ ahi = c - (c - acx);
1603
+ alo = acx - ahi;
1604
+ c = splitter * bcy;
1605
+ bhi = c - (c - bcy);
1606
+ blo = bcy - bhi;
1607
+ s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);
1608
+ t1 = acy * bcx;
1609
+ c = splitter * acy;
1610
+ ahi = c - (c - acy);
1611
+ alo = acy - ahi;
1612
+ c = splitter * bcx;
1613
+ bhi = c - (c - bcx);
1614
+ blo = bcx - bhi;
1615
+ t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);
1616
+ _i = s0 - t0;
1617
+ bvirt = s0 - _i;
1618
+ B[0] = s0 - (_i + bvirt) + (bvirt - t0);
1619
+ _j = s1 + _i;
1620
+ bvirt = _j - s1;
1621
+ _0 = s1 - (_j - bvirt) + (_i - bvirt);
1622
+ _i = _0 - t1;
1623
+ bvirt = _0 - _i;
1624
+ B[1] = _0 - (_i + bvirt) + (bvirt - t1);
1625
+ u3 = _j + _i;
1626
+ bvirt = u3 - _j;
1627
+ B[2] = _j - (u3 - bvirt) + (_i - bvirt);
1628
+ B[3] = u3;
1629
+
1630
+ let det = estimate(4, B);
1631
+ let errbound = ccwerrboundB * detsum;
1632
+ if (det >= errbound || -det >= errbound) {
1633
+ return det;
1634
+ }
1635
+
1636
+ bvirt = ax - acx;
1637
+ acxtail = ax - (acx + bvirt) + (bvirt - cx);
1638
+ bvirt = bx - bcx;
1639
+ bcxtail = bx - (bcx + bvirt) + (bvirt - cx);
1640
+ bvirt = ay - acy;
1641
+ acytail = ay - (acy + bvirt) + (bvirt - cy);
1642
+ bvirt = by - bcy;
1643
+ bcytail = by - (bcy + bvirt) + (bvirt - cy);
1644
+
1645
+ if (acxtail === 0 && acytail === 0 && bcxtail === 0 && bcytail === 0) {
1646
+ return det;
1647
+ }
1648
+
1649
+ errbound = ccwerrboundC * detsum + resulterrbound * Math.abs(det);
1650
+ det += (acx * bcytail + bcy * acxtail) - (acy * bcxtail + bcx * acytail);
1651
+ if (det >= errbound || -det >= errbound) return det;
1652
+
1653
+ s1 = acxtail * bcy;
1654
+ c = splitter * acxtail;
1655
+ ahi = c - (c - acxtail);
1656
+ alo = acxtail - ahi;
1657
+ c = splitter * bcy;
1658
+ bhi = c - (c - bcy);
1659
+ blo = bcy - bhi;
1660
+ s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);
1661
+ t1 = acytail * bcx;
1662
+ c = splitter * acytail;
1663
+ ahi = c - (c - acytail);
1664
+ alo = acytail - ahi;
1665
+ c = splitter * bcx;
1666
+ bhi = c - (c - bcx);
1667
+ blo = bcx - bhi;
1668
+ t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);
1669
+ _i = s0 - t0;
1670
+ bvirt = s0 - _i;
1671
+ u[0] = s0 - (_i + bvirt) + (bvirt - t0);
1672
+ _j = s1 + _i;
1673
+ bvirt = _j - s1;
1674
+ _0 = s1 - (_j - bvirt) + (_i - bvirt);
1675
+ _i = _0 - t1;
1676
+ bvirt = _0 - _i;
1677
+ u[1] = _0 - (_i + bvirt) + (bvirt - t1);
1678
+ u3 = _j + _i;
1679
+ bvirt = u3 - _j;
1680
+ u[2] = _j - (u3 - bvirt) + (_i - bvirt);
1681
+ u[3] = u3;
1682
+ const C1len = sum(4, B, 4, u, C1);
1683
+
1684
+ s1 = acx * bcytail;
1685
+ c = splitter * acx;
1686
+ ahi = c - (c - acx);
1687
+ alo = acx - ahi;
1688
+ c = splitter * bcytail;
1689
+ bhi = c - (c - bcytail);
1690
+ blo = bcytail - bhi;
1691
+ s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);
1692
+ t1 = acy * bcxtail;
1693
+ c = splitter * acy;
1694
+ ahi = c - (c - acy);
1695
+ alo = acy - ahi;
1696
+ c = splitter * bcxtail;
1697
+ bhi = c - (c - bcxtail);
1698
+ blo = bcxtail - bhi;
1699
+ t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);
1700
+ _i = s0 - t0;
1701
+ bvirt = s0 - _i;
1702
+ u[0] = s0 - (_i + bvirt) + (bvirt - t0);
1703
+ _j = s1 + _i;
1704
+ bvirt = _j - s1;
1705
+ _0 = s1 - (_j - bvirt) + (_i - bvirt);
1706
+ _i = _0 - t1;
1707
+ bvirt = _0 - _i;
1708
+ u[1] = _0 - (_i + bvirt) + (bvirt - t1);
1709
+ u3 = _j + _i;
1710
+ bvirt = u3 - _j;
1711
+ u[2] = _j - (u3 - bvirt) + (_i - bvirt);
1712
+ u[3] = u3;
1713
+ const C2len = sum(C1len, C1, 4, u, C2);
1714
+
1715
+ s1 = acxtail * bcytail;
1716
+ c = splitter * acxtail;
1717
+ ahi = c - (c - acxtail);
1718
+ alo = acxtail - ahi;
1719
+ c = splitter * bcytail;
1720
+ bhi = c - (c - bcytail);
1721
+ blo = bcytail - bhi;
1722
+ s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);
1723
+ t1 = acytail * bcxtail;
1724
+ c = splitter * acytail;
1725
+ ahi = c - (c - acytail);
1726
+ alo = acytail - ahi;
1727
+ c = splitter * bcxtail;
1728
+ bhi = c - (c - bcxtail);
1729
+ blo = bcxtail - bhi;
1730
+ t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);
1731
+ _i = s0 - t0;
1732
+ bvirt = s0 - _i;
1733
+ u[0] = s0 - (_i + bvirt) + (bvirt - t0);
1734
+ _j = s1 + _i;
1735
+ bvirt = _j - s1;
1736
+ _0 = s1 - (_j - bvirt) + (_i - bvirt);
1737
+ _i = _0 - t1;
1738
+ bvirt = _0 - _i;
1739
+ u[1] = _0 - (_i + bvirt) + (bvirt - t1);
1740
+ u3 = _j + _i;
1741
+ bvirt = u3 - _j;
1742
+ u[2] = _j - (u3 - bvirt) + (_i - bvirt);
1743
+ u[3] = u3;
1744
+ const Dlen = sum(C2len, C2, 4, u, D);
1745
+
1746
+ return D[Dlen - 1];
1747
+ }
1748
+
1749
+ function orient2d(ax, ay, bx, by, cx, cy) {
1750
+ const detleft = (ay - cy) * (bx - cx);
1751
+ const detright = (ax - cx) * (by - cy);
1752
+ const det = detleft - detright;
1753
+
1754
+ const detsum = Math.abs(detleft + detright);
1755
+ if (Math.abs(det) >= ccwerrboundA * detsum) return det;
1756
+
1757
+ return -orient2dadapt(ax, ay, bx, by, cx, cy, detsum);
1758
+ }
1759
+
1760
+ /**
1761
+ * A bounding box has the format:
1762
+ *
1763
+ * { ll: { x: xmin, y: ymin }, ur: { x: xmax, y: ymax } }
1764
+ *
1765
+ */
1766
+
1767
+ const isInBbox = (bbox, point) => {
1768
+ return bbox.ll.x <= point.x && point.x <= bbox.ur.x && bbox.ll.y <= point.y && point.y <= bbox.ur.y;
1769
+ };
1770
+
1771
+ /* Returns either null, or a bbox (aka an ordered pair of points)
1772
+ * If there is only one point of overlap, a bbox with identical points
1773
+ * will be returned */
1774
+ const getBboxOverlap = (b1, b2) => {
1775
+ // check if the bboxes overlap at all
1776
+ if (b2.ur.x < b1.ll.x || b1.ur.x < b2.ll.x || b2.ur.y < b1.ll.y || b1.ur.y < b2.ll.y) return null;
1777
+
1778
+ // find the middle two X values
1779
+ const lowerX = b1.ll.x < b2.ll.x ? b2.ll.x : b1.ll.x;
1780
+ const upperX = b1.ur.x < b2.ur.x ? b1.ur.x : b2.ur.x;
1781
+
1782
+ // find the middle two Y values
1783
+ const lowerY = b1.ll.y < b2.ll.y ? b2.ll.y : b1.ll.y;
1784
+ const upperY = b1.ur.y < b2.ur.y ? b1.ur.y : b2.ur.y;
1785
+
1786
+ // put those middle values together to get the overlap
1787
+ return {
1788
+ ll: {
1789
+ x: lowerX,
1790
+ y: lowerY
1791
+ },
1792
+ ur: {
1793
+ x: upperX,
1794
+ y: upperY
1795
+ }
1796
+ };
1797
+ };
1798
+
1799
+ /* Javascript doesn't do integer math. Everything is
1800
+ * floating point with percision Number.EPSILON.
1801
+ *
1802
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON
1803
+ */
1804
+
1805
+ let epsilon = Number.EPSILON;
1806
+
1807
+ // IE Polyfill
1808
+ if (epsilon === undefined) epsilon = Math.pow(2, -52);
1809
+ const EPSILON_SQ = epsilon * epsilon;
1810
+
1811
+ /* FLP comparator */
1812
+ const cmp = (a, b) => {
1813
+ // check if they're both 0
1814
+ if (-epsilon < a && a < epsilon) {
1815
+ if (-epsilon < b && b < epsilon) {
1816
+ return 0;
1817
+ }
1818
+ }
1819
+
1820
+ // check if they're flp equal
1821
+ const ab = a - b;
1822
+ if (ab * ab < EPSILON_SQ * a * b) {
1823
+ return 0;
1824
+ }
1825
+
1826
+ // normal comparison
1827
+ return a < b ? -1 : 1;
1828
+ };
1829
+
1830
+ /**
1831
+ * This class rounds incoming values sufficiently so that
1832
+ * floating points problems are, for the most part, avoided.
1833
+ *
1834
+ * Incoming points are have their x & y values tested against
1835
+ * all previously seen x & y values. If either is 'too close'
1836
+ * to a previously seen value, it's value is 'snapped' to the
1837
+ * previously seen value.
1838
+ *
1839
+ * All points should be rounded by this class before being
1840
+ * stored in any data structures in the rest of this algorithm.
1841
+ */
1842
+
1843
+ class PtRounder {
1844
+ constructor() {
1845
+ this.reset();
1846
+ }
1847
+ reset() {
1848
+ this.xRounder = new CoordRounder();
1849
+ this.yRounder = new CoordRounder();
1850
+ }
1851
+ round(x, y) {
1852
+ return {
1853
+ x: this.xRounder.round(x),
1854
+ y: this.yRounder.round(y)
1855
+ };
1856
+ }
1857
+ }
1858
+ class CoordRounder {
1859
+ constructor() {
1860
+ this.tree = new Tree();
1861
+ // preseed with 0 so we don't end up with values < Number.EPSILON
1862
+ this.round(0);
1863
+ }
1864
+
1865
+ // Note: this can rounds input values backwards or forwards.
1866
+ // You might ask, why not restrict this to just rounding
1867
+ // forwards? Wouldn't that allow left endpoints to always
1868
+ // remain left endpoints during splitting (never change to
1869
+ // right). No - it wouldn't, because we snap intersections
1870
+ // to endpoints (to establish independence from the segment
1871
+ // angle for t-intersections).
1872
+ round(coord) {
1873
+ const node = this.tree.add(coord);
1874
+ const prevNode = this.tree.prev(node);
1875
+ if (prevNode !== null && cmp(node.key, prevNode.key) === 0) {
1876
+ this.tree.remove(coord);
1877
+ return prevNode.key;
1878
+ }
1879
+ const nextNode = this.tree.next(node);
1880
+ if (nextNode !== null && cmp(node.key, nextNode.key) === 0) {
1881
+ this.tree.remove(coord);
1882
+ return nextNode.key;
1883
+ }
1884
+ return coord;
1885
+ }
1886
+ }
1887
+
1888
+ // singleton available by import
1889
+ const rounder = new PtRounder();
1890
+
1891
+ /* Cross Product of two vectors with first point at origin */
1892
+ const crossProduct = (a, b) => a.x * b.y - a.y * b.x;
1893
+
1894
+ /* Dot Product of two vectors with first point at origin */
1895
+ const dotProduct = (a, b) => a.x * b.x + a.y * b.y;
1896
+
1897
+ /* Comparator for two vectors with same starting point */
1898
+ const compareVectorAngles = (basePt, endPt1, endPt2) => {
1899
+ const res = orient2d(basePt.x, basePt.y, endPt1.x, endPt1.y, endPt2.x, endPt2.y);
1900
+ if (res > 0) return -1;
1901
+ if (res < 0) return 1;
1902
+ return 0;
1903
+ };
1904
+ const length = v => Math.sqrt(dotProduct(v, v));
1905
+
1906
+ /* Get the sine of the angle from pShared -> pAngle to pShaed -> pBase */
1907
+ const sineOfAngle = (pShared, pBase, pAngle) => {
1908
+ const vBase = {
1909
+ x: pBase.x - pShared.x,
1910
+ y: pBase.y - pShared.y
1911
+ };
1912
+ const vAngle = {
1913
+ x: pAngle.x - pShared.x,
1914
+ y: pAngle.y - pShared.y
1915
+ };
1916
+ return crossProduct(vAngle, vBase) / length(vAngle) / length(vBase);
1917
+ };
1918
+
1919
+ /* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */
1920
+ const cosineOfAngle = (pShared, pBase, pAngle) => {
1921
+ const vBase = {
1922
+ x: pBase.x - pShared.x,
1923
+ y: pBase.y - pShared.y
1924
+ };
1925
+ const vAngle = {
1926
+ x: pAngle.x - pShared.x,
1927
+ y: pAngle.y - pShared.y
1928
+ };
1929
+ return dotProduct(vAngle, vBase) / length(vAngle) / length(vBase);
1930
+ };
1931
+
1932
+ /* Get the x coordinate where the given line (defined by a point and vector)
1933
+ * crosses the horizontal line with the given y coordiante.
1934
+ * In the case of parrallel lines (including overlapping ones) returns null. */
1935
+ const horizontalIntersection = (pt, v, y) => {
1936
+ if (v.y === 0) return null;
1937
+ return {
1938
+ x: pt.x + v.x / v.y * (y - pt.y),
1939
+ y: y
1940
+ };
1941
+ };
1942
+
1943
+ /* Get the y coordinate where the given line (defined by a point and vector)
1944
+ * crosses the vertical line with the given x coordiante.
1945
+ * In the case of parrallel lines (including overlapping ones) returns null. */
1946
+ const verticalIntersection = (pt, v, x) => {
1947
+ if (v.x === 0) return null;
1948
+ return {
1949
+ x: x,
1950
+ y: pt.y + v.y / v.x * (x - pt.x)
1951
+ };
1952
+ };
1953
+
1954
+ /* Get the intersection of two lines, each defined by a base point and a vector.
1955
+ * In the case of parrallel lines (including overlapping ones) returns null. */
1956
+ const intersection$1 = (pt1, v1, pt2, v2) => {
1957
+ // take some shortcuts for vertical and horizontal lines
1958
+ // this also ensures we don't calculate an intersection and then discover
1959
+ // it's actually outside the bounding box of the line
1960
+ if (v1.x === 0) return verticalIntersection(pt2, v2, pt1.x);
1961
+ if (v2.x === 0) return verticalIntersection(pt1, v1, pt2.x);
1962
+ if (v1.y === 0) return horizontalIntersection(pt2, v2, pt1.y);
1963
+ if (v2.y === 0) return horizontalIntersection(pt1, v1, pt2.y);
1964
+
1965
+ // General case for non-overlapping segments.
1966
+ // This algorithm is based on Schneider and Eberly.
1967
+ // http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf - pg 244
1968
+
1969
+ const kross = crossProduct(v1, v2);
1970
+ if (kross == 0) return null;
1971
+ const ve = {
1972
+ x: pt2.x - pt1.x,
1973
+ y: pt2.y - pt1.y
1974
+ };
1975
+ const d1 = crossProduct(ve, v1) / kross;
1976
+ const d2 = crossProduct(ve, v2) / kross;
1977
+
1978
+ // take the average of the two calculations to minimize rounding error
1979
+ const x1 = pt1.x + d2 * v1.x,
1980
+ x2 = pt2.x + d1 * v2.x;
1981
+ const y1 = pt1.y + d2 * v1.y,
1982
+ y2 = pt2.y + d1 * v2.y;
1983
+ const x = (x1 + x2) / 2;
1984
+ const y = (y1 + y2) / 2;
1985
+ return {
1986
+ x: x,
1987
+ y: y
1988
+ };
1989
+ };
1990
+
1991
+ class SweepEvent {
1992
+ // for ordering sweep events in the sweep event queue
1993
+ static compare(a, b) {
1994
+ // favor event with a point that the sweep line hits first
1995
+ const ptCmp = SweepEvent.comparePoints(a.point, b.point);
1996
+ if (ptCmp !== 0) return ptCmp;
1997
+
1998
+ // the points are the same, so link them if needed
1999
+ if (a.point !== b.point) a.link(b);
2000
+
2001
+ // favor right events over left
2002
+ if (a.isLeft !== b.isLeft) return a.isLeft ? 1 : -1;
2003
+
2004
+ // we have two matching left or right endpoints
2005
+ // ordering of this case is the same as for their segments
2006
+ return Segment.compare(a.segment, b.segment);
2007
+ }
2008
+
2009
+ // for ordering points in sweep line order
2010
+ static comparePoints(aPt, bPt) {
2011
+ if (aPt.x < bPt.x) return -1;
2012
+ if (aPt.x > bPt.x) return 1;
2013
+ if (aPt.y < bPt.y) return -1;
2014
+ if (aPt.y > bPt.y) return 1;
2015
+ return 0;
2016
+ }
2017
+
2018
+ // Warning: 'point' input will be modified and re-used (for performance)
2019
+ constructor(point, isLeft) {
2020
+ if (point.events === undefined) point.events = [this];else point.events.push(this);
2021
+ this.point = point;
2022
+ this.isLeft = isLeft;
2023
+ // this.segment, this.otherSE set by factory
2024
+ }
2025
+ link(other) {
2026
+ if (other.point === this.point) {
2027
+ throw new Error("Tried to link already linked events");
2028
+ }
2029
+ const otherEvents = other.point.events;
2030
+ for (let i = 0, iMax = otherEvents.length; i < iMax; i++) {
2031
+ const evt = otherEvents[i];
2032
+ this.point.events.push(evt);
2033
+ evt.point = this.point;
2034
+ }
2035
+ this.checkForConsuming();
2036
+ }
2037
+
2038
+ /* Do a pass over our linked events and check to see if any pair
2039
+ * of segments match, and should be consumed. */
2040
+ checkForConsuming() {
2041
+ // FIXME: The loops in this method run O(n^2) => no good.
2042
+ // Maintain little ordered sweep event trees?
2043
+ // Can we maintaining an ordering that avoids the need
2044
+ // for the re-sorting with getLeftmostComparator in geom-out?
2045
+
2046
+ // Compare each pair of events to see if other events also match
2047
+ const numEvents = this.point.events.length;
2048
+ for (let i = 0; i < numEvents; i++) {
2049
+ const evt1 = this.point.events[i];
2050
+ if (evt1.segment.consumedBy !== undefined) continue;
2051
+ for (let j = i + 1; j < numEvents; j++) {
2052
+ const evt2 = this.point.events[j];
2053
+ if (evt2.consumedBy !== undefined) continue;
2054
+ if (evt1.otherSE.point.events !== evt2.otherSE.point.events) continue;
2055
+ evt1.segment.consume(evt2.segment);
2056
+ }
2057
+ }
2058
+ }
2059
+ getAvailableLinkedEvents() {
2060
+ // point.events is always of length 2 or greater
2061
+ const events = [];
2062
+ for (let i = 0, iMax = this.point.events.length; i < iMax; i++) {
2063
+ const evt = this.point.events[i];
2064
+ if (evt !== this && !evt.segment.ringOut && evt.segment.isInResult()) {
2065
+ events.push(evt);
2066
+ }
2067
+ }
2068
+ return events;
2069
+ }
2070
+
2071
+ /**
2072
+ * Returns a comparator function for sorting linked events that will
2073
+ * favor the event that will give us the smallest left-side angle.
2074
+ * All ring construction starts as low as possible heading to the right,
2075
+ * so by always turning left as sharp as possible we'll get polygons
2076
+ * without uncessary loops & holes.
2077
+ *
2078
+ * The comparator function has a compute cache such that it avoids
2079
+ * re-computing already-computed values.
2080
+ */
2081
+ getLeftmostComparator(baseEvent) {
2082
+ const cache = new Map();
2083
+ const fillCache = linkedEvent => {
2084
+ const nextEvent = linkedEvent.otherSE;
2085
+ cache.set(linkedEvent, {
2086
+ sine: sineOfAngle(this.point, baseEvent.point, nextEvent.point),
2087
+ cosine: cosineOfAngle(this.point, baseEvent.point, nextEvent.point)
2088
+ });
2089
+ };
2090
+ return (a, b) => {
2091
+ if (!cache.has(a)) fillCache(a);
2092
+ if (!cache.has(b)) fillCache(b);
2093
+ const {
2094
+ sine: asine,
2095
+ cosine: acosine
2096
+ } = cache.get(a);
2097
+ const {
2098
+ sine: bsine,
2099
+ cosine: bcosine
2100
+ } = cache.get(b);
2101
+
2102
+ // both on or above x-axis
2103
+ if (asine >= 0 && bsine >= 0) {
2104
+ if (acosine < bcosine) return 1;
2105
+ if (acosine > bcosine) return -1;
2106
+ return 0;
2107
+ }
2108
+
2109
+ // both below x-axis
2110
+ if (asine < 0 && bsine < 0) {
2111
+ if (acosine < bcosine) return -1;
2112
+ if (acosine > bcosine) return 1;
2113
+ return 0;
2114
+ }
2115
+
2116
+ // one above x-axis, one below
2117
+ if (bsine < asine) return -1;
2118
+ if (bsine > asine) return 1;
2119
+ return 0;
2120
+ };
2121
+ }
2122
+ }
2123
+
2124
+ // Give segments unique ID's to get consistent sorting of
2125
+ // segments and sweep events when all else is identical
2126
+ let segmentId = 0;
2127
+ class Segment {
2128
+ /* This compare() function is for ordering segments in the sweep
2129
+ * line tree, and does so according to the following criteria:
2130
+ *
2131
+ * Consider the vertical line that lies an infinestimal step to the
2132
+ * right of the right-more of the two left endpoints of the input
2133
+ * segments. Imagine slowly moving a point up from negative infinity
2134
+ * in the increasing y direction. Which of the two segments will that
2135
+ * point intersect first? That segment comes 'before' the other one.
2136
+ *
2137
+ * If neither segment would be intersected by such a line, (if one
2138
+ * or more of the segments are vertical) then the line to be considered
2139
+ * is directly on the right-more of the two left inputs.
2140
+ */
2141
+ static compare(a, b) {
2142
+ const alx = a.leftSE.point.x;
2143
+ const blx = b.leftSE.point.x;
2144
+ const arx = a.rightSE.point.x;
2145
+ const brx = b.rightSE.point.x;
2146
+
2147
+ // check if they're even in the same vertical plane
2148
+ if (brx < alx) return 1;
2149
+ if (arx < blx) return -1;
2150
+ const aly = a.leftSE.point.y;
2151
+ const bly = b.leftSE.point.y;
2152
+ const ary = a.rightSE.point.y;
2153
+ const bry = b.rightSE.point.y;
2154
+
2155
+ // is left endpoint of segment B the right-more?
2156
+ if (alx < blx) {
2157
+ // are the two segments in the same horizontal plane?
2158
+ if (bly < aly && bly < ary) return 1;
2159
+ if (bly > aly && bly > ary) return -1;
2160
+
2161
+ // is the B left endpoint colinear to segment A?
2162
+ const aCmpBLeft = a.comparePoint(b.leftSE.point);
2163
+ if (aCmpBLeft < 0) return 1;
2164
+ if (aCmpBLeft > 0) return -1;
2165
+
2166
+ // is the A right endpoint colinear to segment B ?
2167
+ const bCmpARight = b.comparePoint(a.rightSE.point);
2168
+ if (bCmpARight !== 0) return bCmpARight;
2169
+
2170
+ // colinear segments, consider the one with left-more
2171
+ // left endpoint to be first (arbitrary?)
2172
+ return -1;
2173
+ }
2174
+
2175
+ // is left endpoint of segment A the right-more?
2176
+ if (alx > blx) {
2177
+ if (aly < bly && aly < bry) return -1;
2178
+ if (aly > bly && aly > bry) return 1;
2179
+
2180
+ // is the A left endpoint colinear to segment B?
2181
+ const bCmpALeft = b.comparePoint(a.leftSE.point);
2182
+ if (bCmpALeft !== 0) return bCmpALeft;
2183
+
2184
+ // is the B right endpoint colinear to segment A?
2185
+ const aCmpBRight = a.comparePoint(b.rightSE.point);
2186
+ if (aCmpBRight < 0) return 1;
2187
+ if (aCmpBRight > 0) return -1;
2188
+
2189
+ // colinear segments, consider the one with left-more
2190
+ // left endpoint to be first (arbitrary?)
2191
+ return 1;
2192
+ }
2193
+
2194
+ // if we get here, the two left endpoints are in the same
2195
+ // vertical plane, ie alx === blx
2196
+
2197
+ // consider the lower left-endpoint to come first
2198
+ if (aly < bly) return -1;
2199
+ if (aly > bly) return 1;
2200
+
2201
+ // left endpoints are identical
2202
+ // check for colinearity by using the left-more right endpoint
2203
+
2204
+ // is the A right endpoint more left-more?
2205
+ if (arx < brx) {
2206
+ const bCmpARight = b.comparePoint(a.rightSE.point);
2207
+ if (bCmpARight !== 0) return bCmpARight;
2208
+ }
2209
+
2210
+ // is the B right endpoint more left-more?
2211
+ if (arx > brx) {
2212
+ const aCmpBRight = a.comparePoint(b.rightSE.point);
2213
+ if (aCmpBRight < 0) return 1;
2214
+ if (aCmpBRight > 0) return -1;
2215
+ }
2216
+ if (arx !== brx) {
2217
+ // are these two [almost] vertical segments with opposite orientation?
2218
+ // if so, the one with the lower right endpoint comes first
2219
+ const ay = ary - aly;
2220
+ const ax = arx - alx;
2221
+ const by = bry - bly;
2222
+ const bx = brx - blx;
2223
+ if (ay > ax && by < bx) return 1;
2224
+ if (ay < ax && by > bx) return -1;
2225
+ }
2226
+
2227
+ // we have colinear segments with matching orientation
2228
+ // consider the one with more left-more right endpoint to be first
2229
+ if (arx > brx) return 1;
2230
+ if (arx < brx) return -1;
2231
+
2232
+ // if we get here, two two right endpoints are in the same
2233
+ // vertical plane, ie arx === brx
2234
+
2235
+ // consider the lower right-endpoint to come first
2236
+ if (ary < bry) return -1;
2237
+ if (ary > bry) return 1;
2238
+
2239
+ // right endpoints identical as well, so the segments are idential
2240
+ // fall back on creation order as consistent tie-breaker
2241
+ if (a.id < b.id) return -1;
2242
+ if (a.id > b.id) return 1;
2243
+
2244
+ // identical segment, ie a === b
2245
+ return 0;
2246
+ }
2247
+
2248
+ /* Warning: a reference to ringWindings input will be stored,
2249
+ * and possibly will be later modified */
2250
+ constructor(leftSE, rightSE, rings, windings) {
2251
+ this.id = ++segmentId;
2252
+ this.leftSE = leftSE;
2253
+ leftSE.segment = this;
2254
+ leftSE.otherSE = rightSE;
2255
+ this.rightSE = rightSE;
2256
+ rightSE.segment = this;
2257
+ rightSE.otherSE = leftSE;
2258
+ this.rings = rings;
2259
+ this.windings = windings;
2260
+ // left unset for performance, set later in algorithm
2261
+ // this.ringOut, this.consumedBy, this.prev
2262
+ }
2263
+ static fromRing(pt1, pt2, ring) {
2264
+ let leftPt, rightPt, winding;
2265
+
2266
+ // ordering the two points according to sweep line ordering
2267
+ const cmpPts = SweepEvent.comparePoints(pt1, pt2);
2268
+ if (cmpPts < 0) {
2269
+ leftPt = pt1;
2270
+ rightPt = pt2;
2271
+ winding = 1;
2272
+ } else if (cmpPts > 0) {
2273
+ leftPt = pt2;
2274
+ rightPt = pt1;
2275
+ winding = -1;
2276
+ } else throw new Error(`Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]`);
2277
+ const leftSE = new SweepEvent(leftPt, true);
2278
+ const rightSE = new SweepEvent(rightPt, false);
2279
+ return new Segment(leftSE, rightSE, [ring], [winding]);
2280
+ }
2281
+
2282
+ /* When a segment is split, the rightSE is replaced with a new sweep event */
2283
+ replaceRightSE(newRightSE) {
2284
+ this.rightSE = newRightSE;
2285
+ this.rightSE.segment = this;
2286
+ this.rightSE.otherSE = this.leftSE;
2287
+ this.leftSE.otherSE = this.rightSE;
2288
+ }
2289
+ bbox() {
2290
+ const y1 = this.leftSE.point.y;
2291
+ const y2 = this.rightSE.point.y;
2292
+ return {
2293
+ ll: {
2294
+ x: this.leftSE.point.x,
2295
+ y: y1 < y2 ? y1 : y2
2296
+ },
2297
+ ur: {
2298
+ x: this.rightSE.point.x,
2299
+ y: y1 > y2 ? y1 : y2
2300
+ }
2301
+ };
2302
+ }
2303
+
2304
+ /* A vector from the left point to the right */
2305
+ vector() {
2306
+ return {
2307
+ x: this.rightSE.point.x - this.leftSE.point.x,
2308
+ y: this.rightSE.point.y - this.leftSE.point.y
2309
+ };
2310
+ }
2311
+ isAnEndpoint(pt) {
2312
+ return pt.x === this.leftSE.point.x && pt.y === this.leftSE.point.y || pt.x === this.rightSE.point.x && pt.y === this.rightSE.point.y;
2313
+ }
2314
+
2315
+ /* Compare this segment with a point.
2316
+ *
2317
+ * A point P is considered to be colinear to a segment if there
2318
+ * exists a distance D such that if we travel along the segment
2319
+ * from one * endpoint towards the other a distance D, we find
2320
+ * ourselves at point P.
2321
+ *
2322
+ * Return value indicates:
2323
+ *
2324
+ * 1: point lies above the segment (to the left of vertical)
2325
+ * 0: point is colinear to segment
2326
+ * -1: point lies below the segment (to the right of vertical)
2327
+ */
2328
+ comparePoint(point) {
2329
+ if (this.isAnEndpoint(point)) return 0;
2330
+ const lPt = this.leftSE.point;
2331
+ const rPt = this.rightSE.point;
2332
+ const v = this.vector();
2333
+
2334
+ // Exactly vertical segments.
2335
+ if (lPt.x === rPt.x) {
2336
+ if (point.x === lPt.x) return 0;
2337
+ return point.x < lPt.x ? 1 : -1;
2338
+ }
2339
+
2340
+ // Nearly vertical segments with an intersection.
2341
+ // Check to see where a point on the line with matching Y coordinate is.
2342
+ const yDist = (point.y - lPt.y) / v.y;
2343
+ const xFromYDist = lPt.x + yDist * v.x;
2344
+ if (point.x === xFromYDist) return 0;
2345
+
2346
+ // General case.
2347
+ // Check to see where a point on the line with matching X coordinate is.
2348
+ const xDist = (point.x - lPt.x) / v.x;
2349
+ const yFromXDist = lPt.y + xDist * v.y;
2350
+ if (point.y === yFromXDist) return 0;
2351
+ return point.y < yFromXDist ? -1 : 1;
2352
+ }
2353
+
2354
+ /**
2355
+ * Given another segment, returns the first non-trivial intersection
2356
+ * between the two segments (in terms of sweep line ordering), if it exists.
2357
+ *
2358
+ * A 'non-trivial' intersection is one that will cause one or both of the
2359
+ * segments to be split(). As such, 'trivial' vs. 'non-trivial' intersection:
2360
+ *
2361
+ * * endpoint of segA with endpoint of segB --> trivial
2362
+ * * endpoint of segA with point along segB --> non-trivial
2363
+ * * endpoint of segB with point along segA --> non-trivial
2364
+ * * point along segA with point along segB --> non-trivial
2365
+ *
2366
+ * If no non-trivial intersection exists, return null
2367
+ * Else, return null.
2368
+ */
2369
+ getIntersection(other) {
2370
+ // If bboxes don't overlap, there can't be any intersections
2371
+ const tBbox = this.bbox();
2372
+ const oBbox = other.bbox();
2373
+ const bboxOverlap = getBboxOverlap(tBbox, oBbox);
2374
+ if (bboxOverlap === null) return null;
2375
+
2376
+ // We first check to see if the endpoints can be considered intersections.
2377
+ // This will 'snap' intersections to endpoints if possible, and will
2378
+ // handle cases of colinearity.
2379
+
2380
+ const tlp = this.leftSE.point;
2381
+ const trp = this.rightSE.point;
2382
+ const olp = other.leftSE.point;
2383
+ const orp = other.rightSE.point;
2384
+
2385
+ // does each endpoint touch the other segment?
2386
+ // note that we restrict the 'touching' definition to only allow segments
2387
+ // to touch endpoints that lie forward from where we are in the sweep line pass
2388
+ const touchesOtherLSE = isInBbox(tBbox, olp) && this.comparePoint(olp) === 0;
2389
+ const touchesThisLSE = isInBbox(oBbox, tlp) && other.comparePoint(tlp) === 0;
2390
+ const touchesOtherRSE = isInBbox(tBbox, orp) && this.comparePoint(orp) === 0;
2391
+ const touchesThisRSE = isInBbox(oBbox, trp) && other.comparePoint(trp) === 0;
2392
+
2393
+ // do left endpoints match?
2394
+ if (touchesThisLSE && touchesOtherLSE) {
2395
+ // these two cases are for colinear segments with matching left
2396
+ // endpoints, and one segment being longer than the other
2397
+ if (touchesThisRSE && !touchesOtherRSE) return trp;
2398
+ if (!touchesThisRSE && touchesOtherRSE) return orp;
2399
+ // either the two segments match exactly (two trival intersections)
2400
+ // or just on their left endpoint (one trivial intersection
2401
+ return null;
2402
+ }
2403
+
2404
+ // does this left endpoint matches (other doesn't)
2405
+ if (touchesThisLSE) {
2406
+ // check for segments that just intersect on opposing endpoints
2407
+ if (touchesOtherRSE) {
2408
+ if (tlp.x === orp.x && tlp.y === orp.y) return null;
2409
+ }
2410
+ // t-intersection on left endpoint
2411
+ return tlp;
2412
+ }
2413
+
2414
+ // does other left endpoint matches (this doesn't)
2415
+ if (touchesOtherLSE) {
2416
+ // check for segments that just intersect on opposing endpoints
2417
+ if (touchesThisRSE) {
2418
+ if (trp.x === olp.x && trp.y === olp.y) return null;
2419
+ }
2420
+ // t-intersection on left endpoint
2421
+ return olp;
2422
+ }
2423
+
2424
+ // trivial intersection on right endpoints
2425
+ if (touchesThisRSE && touchesOtherRSE) return null;
2426
+
2427
+ // t-intersections on just one right endpoint
2428
+ if (touchesThisRSE) return trp;
2429
+ if (touchesOtherRSE) return orp;
2430
+
2431
+ // None of our endpoints intersect. Look for a general intersection between
2432
+ // infinite lines laid over the segments
2433
+ const pt = intersection$1(tlp, this.vector(), olp, other.vector());
2434
+
2435
+ // are the segments parrallel? Note that if they were colinear with overlap,
2436
+ // they would have an endpoint intersection and that case was already handled above
2437
+ if (pt === null) return null;
2438
+
2439
+ // is the intersection found between the lines not on the segments?
2440
+ if (!isInBbox(bboxOverlap, pt)) return null;
2441
+
2442
+ // round the the computed point if needed
2443
+ return rounder.round(pt.x, pt.y);
2444
+ }
2445
+
2446
+ /**
2447
+ * Split the given segment into multiple segments on the given points.
2448
+ * * Each existing segment will retain its leftSE and a new rightSE will be
2449
+ * generated for it.
2450
+ * * A new segment will be generated which will adopt the original segment's
2451
+ * rightSE, and a new leftSE will be generated for it.
2452
+ * * If there are more than two points given to split on, new segments
2453
+ * in the middle will be generated with new leftSE and rightSE's.
2454
+ * * An array of the newly generated SweepEvents will be returned.
2455
+ *
2456
+ * Warning: input array of points is modified
2457
+ */
2458
+ split(point) {
2459
+ const newEvents = [];
2460
+ const alreadyLinked = point.events !== undefined;
2461
+ const newLeftSE = new SweepEvent(point, true);
2462
+ const newRightSE = new SweepEvent(point, false);
2463
+ const oldRightSE = this.rightSE;
2464
+ this.replaceRightSE(newRightSE);
2465
+ newEvents.push(newRightSE);
2466
+ newEvents.push(newLeftSE);
2467
+ const newSeg = new Segment(newLeftSE, oldRightSE, this.rings.slice(), this.windings.slice());
2468
+
2469
+ // when splitting a nearly vertical downward-facing segment,
2470
+ // sometimes one of the resulting new segments is vertical, in which
2471
+ // case its left and right events may need to be swapped
2472
+ if (SweepEvent.comparePoints(newSeg.leftSE.point, newSeg.rightSE.point) > 0) {
2473
+ newSeg.swapEvents();
2474
+ }
2475
+ if (SweepEvent.comparePoints(this.leftSE.point, this.rightSE.point) > 0) {
2476
+ this.swapEvents();
2477
+ }
2478
+
2479
+ // in the point we just used to create new sweep events with was already
2480
+ // linked to other events, we need to check if either of the affected
2481
+ // segments should be consumed
2482
+ if (alreadyLinked) {
2483
+ newLeftSE.checkForConsuming();
2484
+ newRightSE.checkForConsuming();
2485
+ }
2486
+ return newEvents;
2487
+ }
2488
+
2489
+ /* Swap which event is left and right */
2490
+ swapEvents() {
2491
+ const tmpEvt = this.rightSE;
2492
+ this.rightSE = this.leftSE;
2493
+ this.leftSE = tmpEvt;
2494
+ this.leftSE.isLeft = true;
2495
+ this.rightSE.isLeft = false;
2496
+ for (let i = 0, iMax = this.windings.length; i < iMax; i++) {
2497
+ this.windings[i] *= -1;
2498
+ }
2499
+ }
2500
+
2501
+ /* Consume another segment. We take their rings under our wing
2502
+ * and mark them as consumed. Use for perfectly overlapping segments */
2503
+ consume(other) {
2504
+ let consumer = this;
2505
+ let consumee = other;
2506
+ while (consumer.consumedBy) consumer = consumer.consumedBy;
2507
+ while (consumee.consumedBy) consumee = consumee.consumedBy;
2508
+ const cmp = Segment.compare(consumer, consumee);
2509
+ if (cmp === 0) return; // already consumed
2510
+ // the winner of the consumption is the earlier segment
2511
+ // according to sweep line ordering
2512
+ if (cmp > 0) {
2513
+ const tmp = consumer;
2514
+ consumer = consumee;
2515
+ consumee = tmp;
2516
+ }
2517
+
2518
+ // make sure a segment doesn't consume it's prev
2519
+ if (consumer.prev === consumee) {
2520
+ const tmp = consumer;
2521
+ consumer = consumee;
2522
+ consumee = tmp;
2523
+ }
2524
+ for (let i = 0, iMax = consumee.rings.length; i < iMax; i++) {
2525
+ const ring = consumee.rings[i];
2526
+ const winding = consumee.windings[i];
2527
+ const index = consumer.rings.indexOf(ring);
2528
+ if (index === -1) {
2529
+ consumer.rings.push(ring);
2530
+ consumer.windings.push(winding);
2531
+ } else consumer.windings[index] += winding;
2532
+ }
2533
+ consumee.rings = null;
2534
+ consumee.windings = null;
2535
+ consumee.consumedBy = consumer;
2536
+
2537
+ // mark sweep events consumed as to maintain ordering in sweep event queue
2538
+ consumee.leftSE.consumedBy = consumer.leftSE;
2539
+ consumee.rightSE.consumedBy = consumer.rightSE;
2540
+ }
2541
+
2542
+ /* The first segment previous segment chain that is in the result */
2543
+ prevInResult() {
2544
+ if (this._prevInResult !== undefined) return this._prevInResult;
2545
+ if (!this.prev) this._prevInResult = null;else if (this.prev.isInResult()) this._prevInResult = this.prev;else this._prevInResult = this.prev.prevInResult();
2546
+ return this._prevInResult;
2547
+ }
2548
+ beforeState() {
2549
+ if (this._beforeState !== undefined) return this._beforeState;
2550
+ if (!this.prev) this._beforeState = {
2551
+ rings: [],
2552
+ windings: [],
2553
+ multiPolys: []
2554
+ };else {
2555
+ const seg = this.prev.consumedBy || this.prev;
2556
+ this._beforeState = seg.afterState();
2557
+ }
2558
+ return this._beforeState;
2559
+ }
2560
+ afterState() {
2561
+ if (this._afterState !== undefined) return this._afterState;
2562
+ const beforeState = this.beforeState();
2563
+ this._afterState = {
2564
+ rings: beforeState.rings.slice(0),
2565
+ windings: beforeState.windings.slice(0),
2566
+ multiPolys: []
2567
+ };
2568
+ const ringsAfter = this._afterState.rings;
2569
+ const windingsAfter = this._afterState.windings;
2570
+ const mpsAfter = this._afterState.multiPolys;
2571
+
2572
+ // calculate ringsAfter, windingsAfter
2573
+ for (let i = 0, iMax = this.rings.length; i < iMax; i++) {
2574
+ const ring = this.rings[i];
2575
+ const winding = this.windings[i];
2576
+ const index = ringsAfter.indexOf(ring);
2577
+ if (index === -1) {
2578
+ ringsAfter.push(ring);
2579
+ windingsAfter.push(winding);
2580
+ } else windingsAfter[index] += winding;
2581
+ }
2582
+
2583
+ // calcualte polysAfter
2584
+ const polysAfter = [];
2585
+ const polysExclude = [];
2586
+ for (let i = 0, iMax = ringsAfter.length; i < iMax; i++) {
2587
+ if (windingsAfter[i] === 0) continue; // non-zero rule
2588
+ const ring = ringsAfter[i];
2589
+ const poly = ring.poly;
2590
+ if (polysExclude.indexOf(poly) !== -1) continue;
2591
+ if (ring.isExterior) polysAfter.push(poly);else {
2592
+ if (polysExclude.indexOf(poly) === -1) polysExclude.push(poly);
2593
+ const index = polysAfter.indexOf(ring.poly);
2594
+ if (index !== -1) polysAfter.splice(index, 1);
2595
+ }
2596
+ }
2597
+
2598
+ // calculate multiPolysAfter
2599
+ for (let i = 0, iMax = polysAfter.length; i < iMax; i++) {
2600
+ const mp = polysAfter[i].multiPoly;
2601
+ if (mpsAfter.indexOf(mp) === -1) mpsAfter.push(mp);
2602
+ }
2603
+ return this._afterState;
2604
+ }
2605
+
2606
+ /* Is this segment part of the final result? */
2607
+ isInResult() {
2608
+ // if we've been consumed, we're not in the result
2609
+ if (this.consumedBy) return false;
2610
+ if (this._isInResult !== undefined) return this._isInResult;
2611
+ const mpsBefore = this.beforeState().multiPolys;
2612
+ const mpsAfter = this.afterState().multiPolys;
2613
+ switch (operation.type) {
2614
+ case "union":
2615
+ {
2616
+ // UNION - included iff:
2617
+ // * On one side of us there is 0 poly interiors AND
2618
+ // * On the other side there is 1 or more.
2619
+ const noBefores = mpsBefore.length === 0;
2620
+ const noAfters = mpsAfter.length === 0;
2621
+ this._isInResult = noBefores !== noAfters;
2622
+ break;
2623
+ }
2624
+ case "intersection":
2625
+ {
2626
+ // INTERSECTION - included iff:
2627
+ // * on one side of us all multipolys are rep. with poly interiors AND
2628
+ // * on the other side of us, not all multipolys are repsented
2629
+ // with poly interiors
2630
+ let least;
2631
+ let most;
2632
+ if (mpsBefore.length < mpsAfter.length) {
2633
+ least = mpsBefore.length;
2634
+ most = mpsAfter.length;
2635
+ } else {
2636
+ least = mpsAfter.length;
2637
+ most = mpsBefore.length;
2638
+ }
2639
+ this._isInResult = most === operation.numMultiPolys && least < most;
2640
+ break;
2641
+ }
2642
+ case "xor":
2643
+ {
2644
+ // XOR - included iff:
2645
+ // * the difference between the number of multipolys represented
2646
+ // with poly interiors on our two sides is an odd number
2647
+ const diff = Math.abs(mpsBefore.length - mpsAfter.length);
2648
+ this._isInResult = diff % 2 === 1;
2649
+ break;
2650
+ }
2651
+ case "difference":
2652
+ {
2653
+ // DIFFERENCE included iff:
2654
+ // * on exactly one side, we have just the subject
2655
+ const isJustSubject = mps => mps.length === 1 && mps[0].isSubject;
2656
+ this._isInResult = isJustSubject(mpsBefore) !== isJustSubject(mpsAfter);
2657
+ break;
2658
+ }
2659
+ default:
2660
+ throw new Error(`Unrecognized operation type found ${operation.type}`);
2661
+ }
2662
+ return this._isInResult;
2663
+ }
2664
+ }
2665
+
2666
+ class RingIn {
2667
+ constructor(geomRing, poly, isExterior) {
2668
+ if (!Array.isArray(geomRing) || geomRing.length === 0) {
2669
+ throw new Error("Input geometry is not a valid Polygon or MultiPolygon");
2670
+ }
2671
+ this.poly = poly;
2672
+ this.isExterior = isExterior;
2673
+ this.segments = [];
2674
+ if (typeof geomRing[0][0] !== "number" || typeof geomRing[0][1] !== "number") {
2675
+ throw new Error("Input geometry is not a valid Polygon or MultiPolygon");
2676
+ }
2677
+ const firstPoint = rounder.round(geomRing[0][0], geomRing[0][1]);
2678
+ this.bbox = {
2679
+ ll: {
2680
+ x: firstPoint.x,
2681
+ y: firstPoint.y
2682
+ },
2683
+ ur: {
2684
+ x: firstPoint.x,
2685
+ y: firstPoint.y
2686
+ }
2687
+ };
2688
+ let prevPoint = firstPoint;
2689
+ for (let i = 1, iMax = geomRing.length; i < iMax; i++) {
2690
+ if (typeof geomRing[i][0] !== "number" || typeof geomRing[i][1] !== "number") {
2691
+ throw new Error("Input geometry is not a valid Polygon or MultiPolygon");
2692
+ }
2693
+ let point = rounder.round(geomRing[i][0], geomRing[i][1]);
2694
+ // skip repeated points
2695
+ if (point.x === prevPoint.x && point.y === prevPoint.y) continue;
2696
+ this.segments.push(Segment.fromRing(prevPoint, point, this));
2697
+ if (point.x < this.bbox.ll.x) this.bbox.ll.x = point.x;
2698
+ if (point.y < this.bbox.ll.y) this.bbox.ll.y = point.y;
2699
+ if (point.x > this.bbox.ur.x) this.bbox.ur.x = point.x;
2700
+ if (point.y > this.bbox.ur.y) this.bbox.ur.y = point.y;
2701
+ prevPoint = point;
2702
+ }
2703
+ // add segment from last to first if last is not the same as first
2704
+ if (firstPoint.x !== prevPoint.x || firstPoint.y !== prevPoint.y) {
2705
+ this.segments.push(Segment.fromRing(prevPoint, firstPoint, this));
2706
+ }
2707
+ }
2708
+ getSweepEvents() {
2709
+ const sweepEvents = [];
2710
+ for (let i = 0, iMax = this.segments.length; i < iMax; i++) {
2711
+ const segment = this.segments[i];
2712
+ sweepEvents.push(segment.leftSE);
2713
+ sweepEvents.push(segment.rightSE);
2714
+ }
2715
+ return sweepEvents;
2716
+ }
2717
+ }
2718
+ class PolyIn {
2719
+ constructor(geomPoly, multiPoly) {
2720
+ if (!Array.isArray(geomPoly)) {
2721
+ throw new Error("Input geometry is not a valid Polygon or MultiPolygon");
2722
+ }
2723
+ this.exteriorRing = new RingIn(geomPoly[0], this, true);
2724
+ // copy by value
2725
+ this.bbox = {
2726
+ ll: {
2727
+ x: this.exteriorRing.bbox.ll.x,
2728
+ y: this.exteriorRing.bbox.ll.y
2729
+ },
2730
+ ur: {
2731
+ x: this.exteriorRing.bbox.ur.x,
2732
+ y: this.exteriorRing.bbox.ur.y
2733
+ }
2734
+ };
2735
+ this.interiorRings = [];
2736
+ for (let i = 1, iMax = geomPoly.length; i < iMax; i++) {
2737
+ const ring = new RingIn(geomPoly[i], this, false);
2738
+ if (ring.bbox.ll.x < this.bbox.ll.x) this.bbox.ll.x = ring.bbox.ll.x;
2739
+ if (ring.bbox.ll.y < this.bbox.ll.y) this.bbox.ll.y = ring.bbox.ll.y;
2740
+ if (ring.bbox.ur.x > this.bbox.ur.x) this.bbox.ur.x = ring.bbox.ur.x;
2741
+ if (ring.bbox.ur.y > this.bbox.ur.y) this.bbox.ur.y = ring.bbox.ur.y;
2742
+ this.interiorRings.push(ring);
2743
+ }
2744
+ this.multiPoly = multiPoly;
2745
+ }
2746
+ getSweepEvents() {
2747
+ const sweepEvents = this.exteriorRing.getSweepEvents();
2748
+ for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {
2749
+ const ringSweepEvents = this.interiorRings[i].getSweepEvents();
2750
+ for (let j = 0, jMax = ringSweepEvents.length; j < jMax; j++) {
2751
+ sweepEvents.push(ringSweepEvents[j]);
2752
+ }
2753
+ }
2754
+ return sweepEvents;
2755
+ }
2756
+ }
2757
+ class MultiPolyIn {
2758
+ constructor(geom, isSubject) {
2759
+ if (!Array.isArray(geom)) {
2760
+ throw new Error("Input geometry is not a valid Polygon or MultiPolygon");
2761
+ }
2762
+ try {
2763
+ // if the input looks like a polygon, convert it to a multipolygon
2764
+ if (typeof geom[0][0][0] === "number") geom = [geom];
2765
+ } catch (ex) {
2766
+ // The input is either malformed or has empty arrays.
2767
+ // In either case, it will be handled later on.
2768
+ }
2769
+ this.polys = [];
2770
+ this.bbox = {
2771
+ ll: {
2772
+ x: Number.POSITIVE_INFINITY,
2773
+ y: Number.POSITIVE_INFINITY
2774
+ },
2775
+ ur: {
2776
+ x: Number.NEGATIVE_INFINITY,
2777
+ y: Number.NEGATIVE_INFINITY
2778
+ }
2779
+ };
2780
+ for (let i = 0, iMax = geom.length; i < iMax; i++) {
2781
+ const poly = new PolyIn(geom[i], this);
2782
+ if (poly.bbox.ll.x < this.bbox.ll.x) this.bbox.ll.x = poly.bbox.ll.x;
2783
+ if (poly.bbox.ll.y < this.bbox.ll.y) this.bbox.ll.y = poly.bbox.ll.y;
2784
+ if (poly.bbox.ur.x > this.bbox.ur.x) this.bbox.ur.x = poly.bbox.ur.x;
2785
+ if (poly.bbox.ur.y > this.bbox.ur.y) this.bbox.ur.y = poly.bbox.ur.y;
2786
+ this.polys.push(poly);
2787
+ }
2788
+ this.isSubject = isSubject;
2789
+ }
2790
+ getSweepEvents() {
2791
+ const sweepEvents = [];
2792
+ for (let i = 0, iMax = this.polys.length; i < iMax; i++) {
2793
+ const polySweepEvents = this.polys[i].getSweepEvents();
2794
+ for (let j = 0, jMax = polySweepEvents.length; j < jMax; j++) {
2795
+ sweepEvents.push(polySweepEvents[j]);
2796
+ }
2797
+ }
2798
+ return sweepEvents;
2799
+ }
2800
+ }
2801
+
2802
+ class RingOut {
2803
+ /* Given the segments from the sweep line pass, compute & return a series
2804
+ * of closed rings from all the segments marked to be part of the result */
2805
+ static factory(allSegments) {
2806
+ const ringsOut = [];
2807
+ for (let i = 0, iMax = allSegments.length; i < iMax; i++) {
2808
+ const segment = allSegments[i];
2809
+ if (!segment.isInResult() || segment.ringOut) continue;
2810
+ let prevEvent = null;
2811
+ let event = segment.leftSE;
2812
+ let nextEvent = segment.rightSE;
2813
+ const events = [event];
2814
+ const startingPoint = event.point;
2815
+ const intersectionLEs = [];
2816
+
2817
+ /* Walk the chain of linked events to form a closed ring */
2818
+ while (true) {
2819
+ prevEvent = event;
2820
+ event = nextEvent;
2821
+ events.push(event);
2822
+
2823
+ /* Is the ring complete? */
2824
+ if (event.point === startingPoint) break;
2825
+ while (true) {
2826
+ const availableLEs = event.getAvailableLinkedEvents();
2827
+
2828
+ /* Did we hit a dead end? This shouldn't happen.
2829
+ * Indicates some earlier part of the algorithm malfunctioned. */
2830
+ if (availableLEs.length === 0) {
2831
+ const firstPt = events[0].point;
2832
+ const lastPt = events[events.length - 1].point;
2833
+ throw new Error(`Unable to complete output ring starting at [${firstPt.x},` + ` ${firstPt.y}]. Last matching segment found ends at` + ` [${lastPt.x}, ${lastPt.y}].`);
2834
+ }
2835
+
2836
+ /* Only one way to go, so cotinue on the path */
2837
+ if (availableLEs.length === 1) {
2838
+ nextEvent = availableLEs[0].otherSE;
2839
+ break;
2840
+ }
2841
+
2842
+ /* We must have an intersection. Check for a completed loop */
2843
+ let indexLE = null;
2844
+ for (let j = 0, jMax = intersectionLEs.length; j < jMax; j++) {
2845
+ if (intersectionLEs[j].point === event.point) {
2846
+ indexLE = j;
2847
+ break;
2848
+ }
2849
+ }
2850
+ /* Found a completed loop. Cut that off and make a ring */
2851
+ if (indexLE !== null) {
2852
+ const intersectionLE = intersectionLEs.splice(indexLE)[0];
2853
+ const ringEvents = events.splice(intersectionLE.index);
2854
+ ringEvents.unshift(ringEvents[0].otherSE);
2855
+ ringsOut.push(new RingOut(ringEvents.reverse()));
2856
+ continue;
2857
+ }
2858
+ /* register the intersection */
2859
+ intersectionLEs.push({
2860
+ index: events.length,
2861
+ point: event.point
2862
+ });
2863
+ /* Choose the left-most option to continue the walk */
2864
+ const comparator = event.getLeftmostComparator(prevEvent);
2865
+ nextEvent = availableLEs.sort(comparator)[0].otherSE;
2866
+ break;
2867
+ }
2868
+ }
2869
+ ringsOut.push(new RingOut(events));
2870
+ }
2871
+ return ringsOut;
2872
+ }
2873
+ constructor(events) {
2874
+ this.events = events;
2875
+ for (let i = 0, iMax = events.length; i < iMax; i++) {
2876
+ events[i].segment.ringOut = this;
2877
+ }
2878
+ this.poly = null;
2879
+ }
2880
+ getGeom() {
2881
+ // Remove superfluous points (ie extra points along a straight line),
2882
+ let prevPt = this.events[0].point;
2883
+ const points = [prevPt];
2884
+ for (let i = 1, iMax = this.events.length - 1; i < iMax; i++) {
2885
+ const pt = this.events[i].point;
2886
+ const nextPt = this.events[i + 1].point;
2887
+ if (compareVectorAngles(pt, prevPt, nextPt) === 0) continue;
2888
+ points.push(pt);
2889
+ prevPt = pt;
2890
+ }
2891
+
2892
+ // ring was all (within rounding error of angle calc) colinear points
2893
+ if (points.length === 1) return null;
2894
+
2895
+ // check if the starting point is necessary
2896
+ const pt = points[0];
2897
+ const nextPt = points[1];
2898
+ if (compareVectorAngles(pt, prevPt, nextPt) === 0) points.shift();
2899
+ points.push(points[0]);
2900
+ const step = this.isExteriorRing() ? 1 : -1;
2901
+ const iStart = this.isExteriorRing() ? 0 : points.length - 1;
2902
+ const iEnd = this.isExteriorRing() ? points.length : -1;
2903
+ const orderedPoints = [];
2904
+ for (let i = iStart; i != iEnd; i += step) orderedPoints.push([points[i].x, points[i].y]);
2905
+ return orderedPoints;
2906
+ }
2907
+ isExteriorRing() {
2908
+ if (this._isExteriorRing === undefined) {
2909
+ const enclosing = this.enclosingRing();
2910
+ this._isExteriorRing = enclosing ? !enclosing.isExteriorRing() : true;
2911
+ }
2912
+ return this._isExteriorRing;
2913
+ }
2914
+ enclosingRing() {
2915
+ if (this._enclosingRing === undefined) {
2916
+ this._enclosingRing = this._calcEnclosingRing();
2917
+ }
2918
+ return this._enclosingRing;
2919
+ }
2920
+
2921
+ /* Returns the ring that encloses this one, if any */
2922
+ _calcEnclosingRing() {
2923
+ // start with the ealier sweep line event so that the prevSeg
2924
+ // chain doesn't lead us inside of a loop of ours
2925
+ let leftMostEvt = this.events[0];
2926
+ for (let i = 1, iMax = this.events.length; i < iMax; i++) {
2927
+ const evt = this.events[i];
2928
+ if (SweepEvent.compare(leftMostEvt, evt) > 0) leftMostEvt = evt;
2929
+ }
2930
+ let prevSeg = leftMostEvt.segment.prevInResult();
2931
+ let prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null;
2932
+ while (true) {
2933
+ // no segment found, thus no ring can enclose us
2934
+ if (!prevSeg) return null;
2935
+
2936
+ // no segments below prev segment found, thus the ring of the prev
2937
+ // segment must loop back around and enclose us
2938
+ if (!prevPrevSeg) return prevSeg.ringOut;
2939
+
2940
+ // if the two segments are of different rings, the ring of the prev
2941
+ // segment must either loop around us or the ring of the prev prev
2942
+ // seg, which would make us and the ring of the prev peers
2943
+ if (prevPrevSeg.ringOut !== prevSeg.ringOut) {
2944
+ if (prevPrevSeg.ringOut.enclosingRing() !== prevSeg.ringOut) {
2945
+ return prevSeg.ringOut;
2946
+ } else return prevSeg.ringOut.enclosingRing();
2947
+ }
2948
+
2949
+ // two segments are from the same ring, so this was a penisula
2950
+ // of that ring. iterate downward, keep searching
2951
+ prevSeg = prevPrevSeg.prevInResult();
2952
+ prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null;
2953
+ }
2954
+ }
2955
+ }
2956
+ class PolyOut {
2957
+ constructor(exteriorRing) {
2958
+ this.exteriorRing = exteriorRing;
2959
+ exteriorRing.poly = this;
2960
+ this.interiorRings = [];
2961
+ }
2962
+ addInterior(ring) {
2963
+ this.interiorRings.push(ring);
2964
+ ring.poly = this;
2965
+ }
2966
+ getGeom() {
2967
+ const geom = [this.exteriorRing.getGeom()];
2968
+ // exterior ring was all (within rounding error of angle calc) colinear points
2969
+ if (geom[0] === null) return null;
2970
+ for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {
2971
+ const ringGeom = this.interiorRings[i].getGeom();
2972
+ // interior ring was all (within rounding error of angle calc) colinear points
2973
+ if (ringGeom === null) continue;
2974
+ geom.push(ringGeom);
2975
+ }
2976
+ return geom;
2977
+ }
2978
+ }
2979
+ class MultiPolyOut {
2980
+ constructor(rings) {
2981
+ this.rings = rings;
2982
+ this.polys = this._composePolys(rings);
2983
+ }
2984
+ getGeom() {
2985
+ const geom = [];
2986
+ for (let i = 0, iMax = this.polys.length; i < iMax; i++) {
2987
+ const polyGeom = this.polys[i].getGeom();
2988
+ // exterior ring was all (within rounding error of angle calc) colinear points
2989
+ if (polyGeom === null) continue;
2990
+ geom.push(polyGeom);
2991
+ }
2992
+ return geom;
2993
+ }
2994
+ _composePolys(rings) {
2995
+ const polys = [];
2996
+ for (let i = 0, iMax = rings.length; i < iMax; i++) {
2997
+ const ring = rings[i];
2998
+ if (ring.poly) continue;
2999
+ if (ring.isExteriorRing()) polys.push(new PolyOut(ring));else {
3000
+ const enclosingRing = ring.enclosingRing();
3001
+ if (!enclosingRing.poly) polys.push(new PolyOut(enclosingRing));
3002
+ enclosingRing.poly.addInterior(ring);
3003
+ }
3004
+ }
3005
+ return polys;
3006
+ }
3007
+ }
3008
+
3009
+ /**
3010
+ * NOTE: We must be careful not to change any segments while
3011
+ * they are in the SplayTree. AFAIK, there's no way to tell
3012
+ * the tree to rebalance itself - thus before splitting
3013
+ * a segment that's in the tree, we remove it from the tree,
3014
+ * do the split, then re-insert it. (Even though splitting a
3015
+ * segment *shouldn't* change its correct position in the
3016
+ * sweep line tree, the reality is because of rounding errors,
3017
+ * it sometimes does.)
3018
+ */
3019
+
3020
+ class SweepLine {
3021
+ constructor(queue) {
3022
+ let comparator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Segment.compare;
3023
+ this.queue = queue;
3024
+ this.tree = new Tree(comparator);
3025
+ this.segments = [];
3026
+ }
3027
+ process(event) {
3028
+ const segment = event.segment;
3029
+ const newEvents = [];
3030
+
3031
+ // if we've already been consumed by another segment,
3032
+ // clean up our body parts and get out
3033
+ if (event.consumedBy) {
3034
+ if (event.isLeft) this.queue.remove(event.otherSE);else this.tree.remove(segment);
3035
+ return newEvents;
3036
+ }
3037
+ const node = event.isLeft ? this.tree.add(segment) : this.tree.find(segment);
3038
+ if (!node) throw new Error(`Unable to find segment #${segment.id} ` + `[${segment.leftSE.point.x}, ${segment.leftSE.point.y}] -> ` + `[${segment.rightSE.point.x}, ${segment.rightSE.point.y}] ` + "in SweepLine tree.");
3039
+ let prevNode = node;
3040
+ let nextNode = node;
3041
+ let prevSeg = undefined;
3042
+ let nextSeg = undefined;
3043
+
3044
+ // skip consumed segments still in tree
3045
+ while (prevSeg === undefined) {
3046
+ prevNode = this.tree.prev(prevNode);
3047
+ if (prevNode === null) prevSeg = null;else if (prevNode.key.consumedBy === undefined) prevSeg = prevNode.key;
3048
+ }
3049
+
3050
+ // skip consumed segments still in tree
3051
+ while (nextSeg === undefined) {
3052
+ nextNode = this.tree.next(nextNode);
3053
+ if (nextNode === null) nextSeg = null;else if (nextNode.key.consumedBy === undefined) nextSeg = nextNode.key;
3054
+ }
3055
+ if (event.isLeft) {
3056
+ // Check for intersections against the previous segment in the sweep line
3057
+ let prevMySplitter = null;
3058
+ if (prevSeg) {
3059
+ const prevInter = prevSeg.getIntersection(segment);
3060
+ if (prevInter !== null) {
3061
+ if (!segment.isAnEndpoint(prevInter)) prevMySplitter = prevInter;
3062
+ if (!prevSeg.isAnEndpoint(prevInter)) {
3063
+ const newEventsFromSplit = this._splitSafely(prevSeg, prevInter);
3064
+ for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {
3065
+ newEvents.push(newEventsFromSplit[i]);
3066
+ }
3067
+ }
3068
+ }
3069
+ }
3070
+
3071
+ // Check for intersections against the next segment in the sweep line
3072
+ let nextMySplitter = null;
3073
+ if (nextSeg) {
3074
+ const nextInter = nextSeg.getIntersection(segment);
3075
+ if (nextInter !== null) {
3076
+ if (!segment.isAnEndpoint(nextInter)) nextMySplitter = nextInter;
3077
+ if (!nextSeg.isAnEndpoint(nextInter)) {
3078
+ const newEventsFromSplit = this._splitSafely(nextSeg, nextInter);
3079
+ for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {
3080
+ newEvents.push(newEventsFromSplit[i]);
3081
+ }
3082
+ }
3083
+ }
3084
+ }
3085
+
3086
+ // For simplicity, even if we find more than one intersection we only
3087
+ // spilt on the 'earliest' (sweep-line style) of the intersections.
3088
+ // The other intersection will be handled in a future process().
3089
+ if (prevMySplitter !== null || nextMySplitter !== null) {
3090
+ let mySplitter = null;
3091
+ if (prevMySplitter === null) mySplitter = nextMySplitter;else if (nextMySplitter === null) mySplitter = prevMySplitter;else {
3092
+ const cmpSplitters = SweepEvent.comparePoints(prevMySplitter, nextMySplitter);
3093
+ mySplitter = cmpSplitters <= 0 ? prevMySplitter : nextMySplitter;
3094
+ }
3095
+
3096
+ // Rounding errors can cause changes in ordering,
3097
+ // so remove afected segments and right sweep events before splitting
3098
+ this.queue.remove(segment.rightSE);
3099
+ newEvents.push(segment.rightSE);
3100
+ const newEventsFromSplit = segment.split(mySplitter);
3101
+ for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {
3102
+ newEvents.push(newEventsFromSplit[i]);
3103
+ }
3104
+ }
3105
+ if (newEvents.length > 0) {
3106
+ // We found some intersections, so re-do the current event to
3107
+ // make sure sweep line ordering is totally consistent for later
3108
+ // use with the segment 'prev' pointers
3109
+ this.tree.remove(segment);
3110
+ newEvents.push(event);
3111
+ } else {
3112
+ // done with left event
3113
+ this.segments.push(segment);
3114
+ segment.prev = prevSeg;
3115
+ }
3116
+ } else {
3117
+ // event.isRight
3118
+
3119
+ // since we're about to be removed from the sweep line, check for
3120
+ // intersections between our previous and next segments
3121
+ if (prevSeg && nextSeg) {
3122
+ const inter = prevSeg.getIntersection(nextSeg);
3123
+ if (inter !== null) {
3124
+ if (!prevSeg.isAnEndpoint(inter)) {
3125
+ const newEventsFromSplit = this._splitSafely(prevSeg, inter);
3126
+ for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {
3127
+ newEvents.push(newEventsFromSplit[i]);
3128
+ }
3129
+ }
3130
+ if (!nextSeg.isAnEndpoint(inter)) {
3131
+ const newEventsFromSplit = this._splitSafely(nextSeg, inter);
3132
+ for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {
3133
+ newEvents.push(newEventsFromSplit[i]);
3134
+ }
3135
+ }
3136
+ }
3137
+ }
3138
+ this.tree.remove(segment);
3139
+ }
3140
+ return newEvents;
3141
+ }
3142
+
3143
+ /* Safely split a segment that is currently in the datastructures
3144
+ * IE - a segment other than the one that is currently being processed. */
3145
+ _splitSafely(seg, pt) {
3146
+ // Rounding errors can cause changes in ordering,
3147
+ // so remove afected segments and right sweep events before splitting
3148
+ // removeNode() doesn't work, so have re-find the seg
3149
+ // https://github.com/w8r/splay-tree/pull/5
3150
+ this.tree.remove(seg);
3151
+ const rightSE = seg.rightSE;
3152
+ this.queue.remove(rightSE);
3153
+ const newEvents = seg.split(pt);
3154
+ newEvents.push(rightSE);
3155
+ // splitting can trigger consumption
3156
+ if (seg.consumedBy === undefined) this.tree.add(seg);
3157
+ return newEvents;
3158
+ }
3159
+ }
3160
+
3161
+ // Limits on iterative processes to prevent infinite loops - usually caused by floating-point math round-off errors.
3162
+ const POLYGON_CLIPPING_MAX_QUEUE_SIZE = typeof process !== "undefined" && process.env.POLYGON_CLIPPING_MAX_QUEUE_SIZE || 1000000;
3163
+ const POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS = typeof process !== "undefined" && process.env.POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS || 1000000;
3164
+ class Operation {
3165
+ run(type, geom, moreGeoms) {
3166
+ operation.type = type;
3167
+ rounder.reset();
3168
+
3169
+ /* Convert inputs to MultiPoly objects */
3170
+ const multipolys = [new MultiPolyIn(geom, true)];
3171
+ for (let i = 0, iMax = moreGeoms.length; i < iMax; i++) {
3172
+ multipolys.push(new MultiPolyIn(moreGeoms[i], false));
3173
+ }
3174
+ operation.numMultiPolys = multipolys.length;
3175
+
3176
+ /* BBox optimization for difference operation
3177
+ * If the bbox of a multipolygon that's part of the clipping doesn't
3178
+ * intersect the bbox of the subject at all, we can just drop that
3179
+ * multiploygon. */
3180
+ if (operation.type === "difference") {
3181
+ // in place removal
3182
+ const subject = multipolys[0];
3183
+ let i = 1;
3184
+ while (i < multipolys.length) {
3185
+ if (getBboxOverlap(multipolys[i].bbox, subject.bbox) !== null) i++;else multipolys.splice(i, 1);
3186
+ }
3187
+ }
3188
+
3189
+ /* BBox optimization for intersection operation
3190
+ * If we can find any pair of multipolygons whose bbox does not overlap,
3191
+ * then the result will be empty. */
3192
+ if (operation.type === "intersection") {
3193
+ // TODO: this is O(n^2) in number of polygons. By sorting the bboxes,
3194
+ // it could be optimized to O(n * ln(n))
3195
+ for (let i = 0, iMax = multipolys.length; i < iMax; i++) {
3196
+ const mpA = multipolys[i];
3197
+ for (let j = i + 1, jMax = multipolys.length; j < jMax; j++) {
3198
+ if (getBboxOverlap(mpA.bbox, multipolys[j].bbox) === null) return [];
3199
+ }
3200
+ }
3201
+ }
3202
+
3203
+ /* Put segment endpoints in a priority queue */
3204
+ const queue = new Tree(SweepEvent.compare);
3205
+ for (let i = 0, iMax = multipolys.length; i < iMax; i++) {
3206
+ const sweepEvents = multipolys[i].getSweepEvents();
3207
+ for (let j = 0, jMax = sweepEvents.length; j < jMax; j++) {
3208
+ queue.insert(sweepEvents[j]);
3209
+ if (queue.size > POLYGON_CLIPPING_MAX_QUEUE_SIZE) {
3210
+ // prevents an infinite loop, an otherwise common manifestation of bugs
3211
+ throw new Error("Infinite loop when putting segment endpoints in a priority queue " + "(queue size too big).");
3212
+ }
3213
+ }
3214
+ }
3215
+
3216
+ /* Pass the sweep line over those endpoints */
3217
+ const sweepLine = new SweepLine(queue);
3218
+ let prevQueueSize = queue.size;
3219
+ let node = queue.pop();
3220
+ while (node) {
3221
+ const evt = node.key;
3222
+ if (queue.size === prevQueueSize) {
3223
+ // prevents an infinite loop, an otherwise common manifestation of bugs
3224
+ const seg = evt.segment;
3225
+ throw new Error(`Unable to pop() ${evt.isLeft ? "left" : "right"} SweepEvent ` + `[${evt.point.x}, ${evt.point.y}] from segment #${seg.id} ` + `[${seg.leftSE.point.x}, ${seg.leftSE.point.y}] -> ` + `[${seg.rightSE.point.x}, ${seg.rightSE.point.y}] from queue.`);
3226
+ }
3227
+ if (queue.size > POLYGON_CLIPPING_MAX_QUEUE_SIZE) {
3228
+ // prevents an infinite loop, an otherwise common manifestation of bugs
3229
+ throw new Error("Infinite loop when passing sweep line over endpoints " + "(queue size too big).");
3230
+ }
3231
+ if (sweepLine.segments.length > POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS) {
3232
+ // prevents an infinite loop, an otherwise common manifestation of bugs
3233
+ throw new Error("Infinite loop when passing sweep line over endpoints " + "(too many sweep line segments).");
3234
+ }
3235
+ const newEvents = sweepLine.process(evt);
3236
+ for (let i = 0, iMax = newEvents.length; i < iMax; i++) {
3237
+ const evt = newEvents[i];
3238
+ if (evt.consumedBy === undefined) queue.insert(evt);
3239
+ }
3240
+ prevQueueSize = queue.size;
3241
+ node = queue.pop();
3242
+ }
3243
+
3244
+ // free some memory we don't need anymore
3245
+ rounder.reset();
3246
+
3247
+ /* Collect and compile segments we're keeping into a multipolygon */
3248
+ const ringsOut = RingOut.factory(sweepLine.segments);
3249
+ const result = new MultiPolyOut(ringsOut);
3250
+ return result.getGeom();
3251
+ }
3252
+ }
3253
+
3254
+ // singleton available by import
3255
+ const operation = new Operation();
3256
+
3257
+ const union = function (geom) {
3258
+ for (var _len = arguments.length, moreGeoms = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
3259
+ moreGeoms[_key - 1] = arguments[_key];
3260
+ }
3261
+ return operation.run("union", geom, moreGeoms);
3262
+ };
3263
+ const intersection = function (geom) {
3264
+ for (var _len2 = arguments.length, moreGeoms = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
3265
+ moreGeoms[_key2 - 1] = arguments[_key2];
3266
+ }
3267
+ return operation.run("intersection", geom, moreGeoms);
3268
+ };
3269
+ const xor = function (geom) {
3270
+ for (var _len3 = arguments.length, moreGeoms = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
3271
+ moreGeoms[_key3 - 1] = arguments[_key3];
3272
+ }
3273
+ return operation.run("xor", geom, moreGeoms);
3274
+ };
3275
+ const difference = function (subjectGeom) {
3276
+ for (var _len4 = arguments.length, clippingGeoms = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
3277
+ clippingGeoms[_key4 - 1] = arguments[_key4];
3278
+ }
3279
+ return operation.run("difference", subjectGeom, clippingGeoms);
3280
+ };
3281
+ var index = {
3282
+ union: union,
3283
+ intersection: intersection,
3284
+ xor: xor,
3285
+ difference: difference
3286
+ };
3287
+
814
3288
  /**
815
3289
  * 路径图层
816
3290
  * 专门处理路径元素的渲染
@@ -845,10 +3319,14 @@ class ChannelLayer extends BaseLayer {
845
3319
  }
846
3320
  /**
847
3321
  * 创建排除分区内部的 clipPath 定义
3322
+ * 思路: 由于channel不能画在分区内部,所以我们根据svg大小设定了可画区域是svg的viewBox,对应的矩形大小,然后把分区进行镂空,就可以得到可画区域
3323
+ * 1. 先计算所有分区的边界,如果能拿到边界的svg的大小,就使用这个如果拿不到,就根据分区去计算
3324
+ * 2. 获取需要镂空的路径,其中,如果分区存在相交,需要把两个分区进行合并获取外轮廓路径。
3325
+ * 3. 将svg大小的矩形设置为顺时针,然后将需要镂空的路径设置为逆时针,结合fill-rule为evenodd,就可以得到可画区域
848
3326
  */
849
3327
  createExclusionClipPathDefinitions(svgGroup) {
850
3328
  // 获取所有分区边界数据
851
- const subBoundaryBorder = useSubBoundaryBorderStore.getState().subBoundaryBorder;
3329
+ const subBoundaryBorder = usePartitionDataStore.getState().subBoundaryBorder;
852
3330
  if (!subBoundaryBorder || Object.keys(subBoundaryBorder).length === 0) {
853
3331
  return {};
854
3332
  }
@@ -859,7 +3337,7 @@ class ChannelLayer extends BaseLayer {
859
3337
  defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
860
3338
  svgGroup.appendChild(defs);
861
3339
  }
862
- // �� 修改:计算包含所有分区和通道的边界框
3340
+ // 计算包含所有分区和通道的边界框
863
3341
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
864
3342
  // 1. 先计算所有分区的边界,如果能拿到边界的svg的大小,就使用这个如果拿不到,就根据分区去计算
865
3343
  const svg = document.getElementById(SVG_MAP_VIEW_ID);
@@ -883,68 +3361,34 @@ class ChannelLayer extends BaseLayer {
883
3361
  }
884
3362
  }
885
3363
  }
886
- // 2. 再计算所有通道的边界
887
- for (const element of this.elements) {
888
- // const tunnelConnection = element.originalData?.connection;
889
- // if (tunnelConnection && Array.isArray(tunnelConnection)) {
890
- // const clipPathId = `channel-exclude-${
891
- // element.originalData?.id || Math.random().toString(36).substr(2, 9)
892
- // }`;
893
- // // 检查是否已存在该 clipPath
894
- // const existingClipPath = defs.querySelector(`#${clipPathId}`);
895
- // if (existingClipPath) continue;
896
- // // 创建 clipPath
897
- // const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
898
- // clipPath.setAttribute('id', clipPathId);
899
- // clipPath.setAttribute('clipPathUnits', 'userSpaceOnUse');
900
- // // === 合成一个 path ===
901
- // let d = `M ${minX} ${minY} L ${maxX} ${minY} L ${maxX} ${maxY} L ${minX} ${maxY} Z`;
902
- // for (const partitionId of tunnelConnection) {
903
- // const boundaryData = subBoundaryBorder[partitionId];
904
- // if (boundaryData && boundaryData.coordinates.length >= 3) {
905
- // d += ` M ${boundaryData.coordinates[0][0]} ${boundaryData.coordinates[0][1]}`;
906
- // for (let i = 1; i < boundaryData.coordinates.length; i++) {
907
- // d += ` L ${boundaryData.coordinates[i][0]} ${boundaryData.coordinates[i][1]}`;
908
- // }
909
- // d += ' Z';
910
- // }
911
- // }
912
- // const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
913
- // path.setAttribute('d', d);
914
- // path.setAttribute('clip-rule', 'evenodd'); // 关键
915
- // clipPath.appendChild(path);
916
- // defs.appendChild(clipPath);
917
- // clipPathIdsMap[element.originalData?.id.toString()] = clipPathId;
918
- // } else {
919
- const clipPathId = `channel-exclude-all-${element.originalData?.id || Math.random().toString(36).substr(2, 9)}`;
920
- // 检查是否已存在该 clipPath
921
- const existingClipPath = defs.querySelector(`#${clipPathId}`);
922
- if (existingClipPath)
923
- continue;
924
- // 创建 clipPath
925
- const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
926
- clipPath.setAttribute('id', clipPathId);
927
- clipPath.setAttribute('clipPathUnits', 'userSpaceOnUse');
928
- // === 合成一个 path ===
929
- let d = `M ${minX} ${minY} L ${maxX} ${minY} L ${maxX} ${maxY} L ${minX} ${maxY} Z`;
930
- for (const partitionId in subBoundaryBorder) {
931
- const boundaryData = subBoundaryBorder[partitionId];
932
- if (boundaryData && boundaryData.coordinates.length >= 3) {
933
- d += ` M ${boundaryData.coordinates[0][0]} ${boundaryData.coordinates[0][1]}`;
934
- for (let i = 1; i < boundaryData.coordinates.length; i++) {
935
- d += ` L ${boundaryData.coordinates[i][0]} ${boundaryData.coordinates[i][1]}`;
936
- }
937
- d += ' Z';
938
- }
3364
+ // 整理出clipPath路径
3365
+ const clipPathId = 'channel-exclude-all';
3366
+ // 创建 clipPath
3367
+ const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
3368
+ clipPath.setAttribute('id', clipPathId);
3369
+ clipPath.setAttribute('fill-rule', 'evenodd');
3370
+ // === 合成一个 path ===
3371
+ // 外轮廓(顺时针)
3372
+ let d = `M ${minX} ${minY} L ${maxX} ${minY} L ${maxX} ${maxY} L ${minX} ${maxY} Z`;
3373
+ // 获取所有需要挖空的分区路径
3374
+ const partitionPaths = this.mergeOverlappingPartitions(subBoundaryBorder);
3375
+ // 将所有分区路径追加到clipPath中(逆时针,形成挖空)
3376
+ partitionPaths.forEach((partitionCoords) => {
3377
+ if (partitionCoords.length >= 3) {
3378
+ // 判断方向并构建路径
3379
+ const isCounterclockwise = this.isCounterclockwise(partitionCoords);
3380
+ const partitionPath = this.buildPathData(partitionCoords, isCounterclockwise);
3381
+ d += ` ${partitionPath}`;
939
3382
  }
940
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
941
- path.setAttribute('d', d);
942
- path.setAttribute('clip-rule', 'evenodd'); // 关键
943
- clipPath.appendChild(path);
944
- defs.appendChild(clipPath);
3383
+ });
3384
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3385
+ path.setAttribute('d', d);
3386
+ clipPath.appendChild(path);
3387
+ defs.appendChild(clipPath);
3388
+ // 为所有通道映射clipPath
3389
+ for (const element of this.elements) {
945
3390
  clipPathIdsMap[element.originalData?.id.toString()] = clipPathId;
946
3391
  }
947
- // }
948
3392
  return clipPathIdsMap;
949
3393
  }
950
3394
  /**
@@ -954,6 +3398,7 @@ class ChannelLayer extends BaseLayer {
954
3398
  const { coordinates, style } = element;
955
3399
  if (coordinates.length < 2)
956
3400
  return;
3401
+ // 这里需要判断点是否在任意一个分区内,如果在的话,就把当前路径绘制成透明的
957
3402
  // 构建路径数据,使用整数坐标以避免渲染问题
958
3403
  let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
959
3404
  for (let i = 1; i < coordinates.length; i++) {
@@ -996,6 +3441,157 @@ class ChannelLayer extends BaseLayer {
996
3441
  topPath.classList.add('vector-path-top');
997
3442
  svgGroup.appendChild(topPath);
998
3443
  }
3444
+ /**
3445
+ * 判断多边形是否为逆时针方向
3446
+ * 使用叉积法计算多边形的有向面积
3447
+ */
3448
+ isCounterclockwise(coordinates) {
3449
+ if (coordinates.length < 3)
3450
+ return false;
3451
+ let sum = 0;
3452
+ for (let i = 0; i < coordinates.length; i++) {
3453
+ const current = coordinates[i];
3454
+ const next = coordinates[(i + 1) % coordinates.length];
3455
+ // 计算叉积
3456
+ sum += (next[0] - current[0]) * (next[1] + current[1]);
3457
+ }
3458
+ // 如果sum > 0,则为逆时针;如果sum < 0,则为顺时针
3459
+ return sum > 0;
3460
+ }
3461
+ /**
3462
+ * 检查两个多边形是否相交
3463
+ */
3464
+ doPolygonsIntersect(polygon1, polygon2) {
3465
+ try {
3466
+ // 使用polygon-clipping的intersection方法检查是否相交
3467
+ const intersection = index.intersection([polygon1], [polygon2]);
3468
+ return intersection.length > 0;
3469
+ }
3470
+ catch (error) {
3471
+ console.warn('Intersection check failed:', error);
3472
+ return false;
3473
+ }
3474
+ }
3475
+ /**
3476
+ * 根据方向构建路径数据
3477
+ */
3478
+ buildPathData(coordinates, isCounterclockwise) {
3479
+ if (coordinates.length < 3)
3480
+ return '';
3481
+ let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
3482
+ if (isCounterclockwise) {
3483
+ // 逆时针方向,按原顺序构建
3484
+ for (let i = 1; i < coordinates.length; i++) {
3485
+ pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
3486
+ }
3487
+ }
3488
+ else {
3489
+ // 顺时针方向,需要反转顺序
3490
+ for (let i = coordinates.length - 1; i > 0; i--) {
3491
+ pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
3492
+ }
3493
+ }
3494
+ pathData += ' Z';
3495
+ return pathData;
3496
+ }
3497
+ /**
3498
+ * 智能合并重叠的分区,返回所有需要挖空的路径
3499
+ */
3500
+ mergeOverlappingPartitions(subBoundaryBorder) {
3501
+ try {
3502
+ // 将所有分区转换为polygon-clipping格式
3503
+ const polygons = [];
3504
+ const partitionIds = [];
3505
+ Object.entries(subBoundaryBorder).forEach(([partitionId, boundaryData]) => {
3506
+ if (boundaryData?.coordinates && boundaryData.coordinates.length >= 3) {
3507
+ // 确保坐标格式正确(去掉第三个z坐标)
3508
+ const coords = boundaryData.coordinates.map((coord) => [coord[0], coord[1]]);
3509
+ polygons.push(coords);
3510
+ partitionIds.push(partitionId);
3511
+ }
3512
+ });
3513
+ if (polygons.length === 0)
3514
+ return [];
3515
+ if (polygons.length === 1)
3516
+ return [polygons[0]];
3517
+ // console.info('原始分区数量:', polygons.length);
3518
+ // 检查哪些分区之间有相交
3519
+ const intersectingGroups = [];
3520
+ const processed = new Set();
3521
+ // console.info('polygons===', polygons);
3522
+ for (let i = 0; i < polygons.length; i++) {
3523
+ if (processed.has(i))
3524
+ continue;
3525
+ const currentGroup = [i];
3526
+ processed.add(i);
3527
+ // 查找与当前分区相交的所有分区
3528
+ for (let j = i + 1; j < polygons.length; j++) {
3529
+ if (processed.has(j))
3530
+ continue;
3531
+ if (this.doPolygonsIntersect(polygons[i], polygons[j])) {
3532
+ currentGroup.push(j);
3533
+ processed.add(j);
3534
+ // console.info(`分区 ${partitionIds[i]} 与分区 ${partitionIds[j]} 相交`);
3535
+ }
3536
+ }
3537
+ if (currentGroup.length > 1) {
3538
+ // 有相交的分区,进行合并
3539
+ intersectingGroups.push(currentGroup);
3540
+ }
3541
+ }
3542
+ // console.info('相交分组:', intersectingGroups);
3543
+ // 存储最终需要挖空的所有路径
3544
+ const finalPaths = [];
3545
+ // 1. 合并相交的分区组
3546
+ intersectingGroups.forEach((group) => {
3547
+ if (group.length > 1) {
3548
+ const groupPolygons = group.map((index) => polygons[index]);
3549
+ // console.info('groupPolygons===', groupPolygons);
3550
+ try {
3551
+ // 将坐标包装成polygon-clipping期望的格式
3552
+ const wrappedPolygons = groupPolygons.map((poly) => [poly]); // 包装成Polygon格式
3553
+ // 使用reduce方法逐个合并
3554
+ let merged = wrappedPolygons[0];
3555
+ for (let i = 1; i < wrappedPolygons.length; i++) {
3556
+ merged = index.union(merged, wrappedPolygons[i]);
3557
+ }
3558
+ if (merged.length > 0) {
3559
+ // 转换坐标格式
3560
+ const firstPolygon = merged[0];
3561
+ if (firstPolygon && firstPolygon.length > 0) {
3562
+ const coords = firstPolygon[0].map((point) => [point[0], point[1]]);
3563
+ finalPaths.push(coords);
3564
+ }
3565
+ }
3566
+ }
3567
+ catch (error) {
3568
+ console.warn('合并相交分区失败:', error);
3569
+ // 如果合并失败,添加原始分区
3570
+ group.forEach((index) => finalPaths.push(polygons[index]));
3571
+ }
3572
+ }
3573
+ });
3574
+ // 2. 添加没有相交的分区
3575
+ // console.info('polygons===', polygons);
3576
+ const newIntersectingGroups = intersectingGroups.flat();
3577
+ // console.info('newIntersectingGroups===', newIntersectingGroups);
3578
+ for (let i = 0; i < polygons.length; i++) {
3579
+ if (!newIntersectingGroups.includes(i)) {
3580
+ finalPaths.push(polygons[i]);
3581
+ }
3582
+ }
3583
+ // console.info('finalPaths===', finalPaths);
3584
+ // console.info('最终挖空路径数量:', finalPaths.length);
3585
+ return finalPaths;
3586
+ }
3587
+ catch (error) {
3588
+ console.error('polygon-clipping union failed:', error);
3589
+ // 如果合并失败,返回原始分区
3590
+ return Object.values(subBoundaryBorder)
3591
+ .filter((boundaryData) => boundaryData?.coordinates && boundaryData.coordinates.length >= 3)
3592
+ .map((boundaryData) => boundaryData.coordinates.map((coord) => [coord[0], coord[1]]));
3593
+ }
3594
+ }
999
3595
  }
1000
3596
 
1001
3597
  /**
@@ -1012,26 +3608,29 @@ class PathLayer extends BaseLayer {
1012
3608
  this.type = LAYER_DEFAULT_TYPE.PATH;
1013
3609
  }
1014
3610
  /**
1015
- * 创建所有分区并集的 clipPath
3611
+ * 为每个分区创建独立的 clipPath
1016
3612
  */
1017
- createUnionClipPath(svgGroup) {
1018
- const { subBoundaryBorder, obstacles, svgElements } = useSubBoundaryBorderStore.getState();
3613
+ createPartitionClipPaths(svgGroup) {
3614
+ const { subBoundaryBorder, obstacles, svgElements } = usePartitionDataStore.getState();
1019
3615
  // 确保 defs 元素存在
1020
3616
  let defs = svgGroup.querySelector('defs');
1021
3617
  if (!defs) {
1022
3618
  defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
1023
3619
  svgGroup.appendChild(defs);
1024
3620
  }
1025
- const clipPathId = 'clip-union-partitions';
1026
- // 如果已存在,先移除
1027
- const existing = defs.querySelector(`#${clipPathId}`);
1028
- if (existing)
1029
- defs.removeChild(existing);
1030
- // 合成所有分区的 path
1031
- let d = '';
1032
- // 1. 外圈(主边界,顺时针)
1033
- Object.values(subBoundaryBorder).forEach((item) => {
1034
- const bCoords = item.coordinates;
3621
+ const clipPathIds = {};
3622
+ // 为每个分区创建独立的 clipPath
3623
+ Object.keys(subBoundaryBorder).forEach((partitionId) => {
3624
+ const partitionData = subBoundaryBorder[partitionId];
3625
+ const clipPathId = `clip-partition-${partitionId}`;
3626
+ // 如果已存在,先移除
3627
+ const existing = defs.querySelector(`#${clipPathId}`);
3628
+ if (existing)
3629
+ defs.removeChild(existing);
3630
+ // 合成该分区的 path
3631
+ let d = '';
3632
+ // 1. 该分区的外圈边界(顺时针)
3633
+ const bCoords = partitionData.coordinates;
1035
3634
  if (bCoords.length >= 3) {
1036
3635
  d += `M ${bCoords[0][0]} ${bCoords[0][1]}`;
1037
3636
  for (let i = 1; i < bCoords.length; i++) {
@@ -1039,35 +3638,59 @@ class PathLayer extends BaseLayer {
1039
3638
  }
1040
3639
  d += ' Z ';
1041
3640
  }
1042
- });
1043
- // 2. 内圈(禁区,逆时针)
1044
- Object.values(obstacles).forEach((item) => {
1045
- const bCoords = item.coordinates;
1046
- if (bCoords.length >= 3) {
1047
- d += `M ${bCoords[bCoords.length - 1][0]} ${bCoords[bCoords.length - 1][1]}`;
1048
- for (let i = bCoords.length - 2; i >= 0; i--) {
1049
- d += ` L ${bCoords[i][0]} ${bCoords[i][1]}`;
3641
+ // 2. 所有禁区(逆时针)- 禁区影响所有分区
3642
+ Object.values(obstacles).forEach((item) => {
3643
+ const obstacleCoords = item.coordinates;
3644
+ if (obstacleCoords.length >= 3) {
3645
+ d += `M ${obstacleCoords[obstacleCoords.length - 1][0]} ${obstacleCoords[obstacleCoords.length - 1][1]}`;
3646
+ for (let i = obstacleCoords.length - 2; i >= 0; i--) {
3647
+ d += ` L ${obstacleCoords[i][0]} ${obstacleCoords[i][1]}`;
3648
+ }
3649
+ d += ' Z ';
1050
3650
  }
1051
- d += ' Z ';
1052
- }
1053
- });
1054
- // 3. svgElements(直接拼接path字符串,建议逆时针)
1055
- if (Array.isArray(svgElements)) {
1056
- svgElements.forEach((svgPath) => {
3651
+ });
3652
+ // 3. 所有 svgElements(逆时针)- SVG元素影响所有分区
3653
+ Object.values(svgElements).forEach((svgPath) => {
1057
3654
  const svgPathString = svgPath?.metadata?.svg;
1058
3655
  if (svgPathString && typeof svgPathString === 'string' && svgPathString.trim()) {
1059
- d += svgPathString + ' ';
3656
+ // 处理转义字符
3657
+ const processedSvgString = svgPathString.replace(/\\n/g, '\n').replace(/\\"/g, '"');
3658
+ // 解析 SVG 字符串
3659
+ const parser = new DOMParser();
3660
+ const svgDoc = parser.parseFromString(processedSvgString, 'image/svg+xml');
3661
+ const svgElement = svgDoc.documentElement;
3662
+ if (svgElement.tagName === 'svg') {
3663
+ // 查找 path 元素
3664
+ const pathElement = svgElement.querySelector('path');
3665
+ if (pathElement) {
3666
+ const pathData = pathElement.getAttribute('d');
3667
+ if (pathData) {
3668
+ // 获取 SVG 元素的变换参数
3669
+ const centerCoords = svgPath.coordinates?.[0] || [0, 0];
3670
+ const center = [centerCoords[0], centerCoords[1]];
3671
+ const userScale = svgPath.metadata.scale || 1;
3672
+ const direction = svgPath.metadata?.direction || 0;
3673
+ const originalWidth = parseFloat(svgElement.getAttribute('width') || '76');
3674
+ const originalHeight = parseFloat(svgElement.getAttribute('height') || '68');
3675
+ // 应用变换到路径数据
3676
+ const transformedPathData = this.transformSvgPath(pathData, center, userScale, direction, originalWidth, originalHeight);
3677
+ d += transformedPathData + ' ';
3678
+ }
3679
+ }
3680
+ }
1060
3681
  }
1061
3682
  });
1062
- }
1063
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1064
- path.setAttribute('d', d);
1065
- const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
1066
- clipPath.setAttribute('id', clipPathId);
1067
- clipPath.setAttribute('clipPathUnits', 'userSpaceOnUse');
1068
- clipPath.appendChild(path);
1069
- defs.appendChild(clipPath);
1070
- return clipPathId;
3683
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3684
+ path.setAttribute('d', d);
3685
+ const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
3686
+ clipPath.setAttribute('id', clipPathId);
3687
+ // clipPath.setAttribute('clipPathUnits', 'userSpaceOnUse');
3688
+ clipPath.setAttribute('clip-rule', 'evenodd');
3689
+ clipPath.appendChild(path);
3690
+ defs.appendChild(clipPath);
3691
+ clipPathIds[partitionId] = clipPathId;
3692
+ });
3693
+ return clipPathIds;
1071
3694
  }
1072
3695
  /**
1073
3696
  * SVG渲染方法
@@ -1079,52 +3702,158 @@ class PathLayer extends BaseLayer {
1079
3702
  this.scale = scale || 1;
1080
3703
  this.lineScale = lineScale || 1;
1081
3704
  svgGroup.style.isolation = 'isolate';
1082
- // 1. 创建分区并集 clipPath
1083
- const clipPathId = this.createUnionClipPath(svgGroup);
1084
- // 2. 创建一个组,应用 clipPath
1085
- const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
1086
- group.setAttribute('clip-path', `url(#${clipPathId})`);
1087
- group.setAttribute('opacity', '0.6'); // 统一透明度,防止叠加脏乱
1088
- // 3. 渲染所有路径
3705
+ // 1. 为每个分区创建独立的 clipPath
3706
+ const clipPathIds = this.createPartitionClipPaths(svgGroup);
3707
+ // 2. 按分区渲染路径
3708
+ this.renderPathsByPartition(svgGroup, clipPathIds);
3709
+ }
3710
+ /**
3711
+ * 按分区渲染路径
3712
+ */
3713
+ renderPathsByPartition(svgGroup, clipPathIds) {
3714
+ // 按分区+类型+样式分组路径数据
3715
+ const partitionTypeGroups = new Map();
3716
+ // 收集所有路径数据并按分区+类型+样式分组
1089
3717
  for (const element of this.elements) {
1090
- const { id, elements } = element;
3718
+ const pathElement = element;
3719
+ const { id, elements } = pathElement;
1091
3720
  this.boundaryPaths[id] = [];
3721
+ elements.forEach((pathElement) => {
3722
+ const { coordinates, style, type } = pathElement;
3723
+ if (type === 'trans' || style.lineColor === 'transparent') {
3724
+ return;
3725
+ }
3726
+ if (coordinates.length < 2)
3727
+ return;
3728
+ // 构建路径数据
3729
+ let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
3730
+ for (let i = 1; i < coordinates.length; i++) {
3731
+ pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
3732
+ }
3733
+ // 根据路径类型设置不同的颜色
3734
+ let lineColor;
3735
+ if (type === 'trans') {
3736
+ lineColor = style.transLineColor || 'transparent';
3737
+ }
3738
+ else if (type === 'mowing') {
3739
+ lineColor = style.mowingLineColor || style.lineColor || '#000000';
3740
+ }
3741
+ else if (type === 'edge') {
3742
+ lineColor = style.edgeLineColor || style.lineColor || '#000000';
3743
+ }
3744
+ else {
3745
+ lineColor = style.lineColor || '#000000';
3746
+ }
3747
+ // 按分区+类型+样式分组存储
3748
+ const groupKey = `${id}-${lineColor}-${style.lineWidth || 1}`;
3749
+ if (!partitionTypeGroups.has(groupKey)) {
3750
+ partitionTypeGroups.set(groupKey, {
3751
+ pathData: [],
3752
+ elements: [],
3753
+ style: { ...style, lineColor },
3754
+ type,
3755
+ });
3756
+ }
3757
+ partitionTypeGroups.get(groupKey).pathData.push(pathData);
3758
+ partitionTypeGroups.get(groupKey).elements.push(pathElement);
3759
+ });
3760
+ }
3761
+ // 为每个分区创建独立的组并应用对应的 clipPath
3762
+ const partitionGroups = new Map();
3763
+ partitionTypeGroups.forEach((groupData, groupKey) => {
3764
+ const { pathData, elements, style } = groupData;
3765
+ if (pathData.length === 0)
3766
+ return;
3767
+ // 从groupKey中提取分区ID
3768
+ const partitionId = groupKey.split('-')[0];
3769
+ // 获取该分区的 clipPath ID
3770
+ const clipPathId = clipPathIds[partitionId];
3771
+ if (!clipPathId)
3772
+ return;
3773
+ // 获取或创建该分区的组
3774
+ let group = partitionGroups.get(partitionId);
3775
+ if (!group) {
3776
+ group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
3777
+ group.setAttribute('clip-path', `url(#${clipPathId})`);
3778
+ group.setAttribute('opacity', '0.5');
3779
+ partitionGroups.set(partitionId, group);
3780
+ svgGroup.appendChild(group);
3781
+ }
3782
+ // 创建该类型的 path 元素
3783
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3784
+ const mergedPathData = pathData.join(' ');
3785
+ path.setAttribute('d', mergedPathData);
3786
+ // 设置样式属性
3787
+ path.setAttribute('fill', 'none');
3788
+ path.setAttribute('stroke', style.lineColor);
3789
+ path.setAttribute('mix-blend-mode', 'normal');
3790
+ const lineWidth = Math.max(style.lineWidth || 1, 0.5);
3791
+ path.setAttribute('stroke-width', lineWidth.toString());
3792
+ path.setAttribute('stroke-linecap', 'round');
3793
+ path.setAttribute('stroke-linejoin', 'round');
3794
+ path.classList.add('vector-path');
3795
+ // 将 path 添加到组中
3796
+ group.appendChild(path);
3797
+ // 保存引用到 boundaryPaths 中
1092
3798
  elements.forEach((element) => {
1093
- this.renderPathToGroup(group, id, element);
3799
+ const { id } = element;
3800
+ if (!this.boundaryPaths[id]) {
3801
+ this.boundaryPaths[id] = [];
3802
+ }
3803
+ this.boundaryPaths[id].push(path);
1094
3804
  });
1095
- }
1096
- svgGroup.appendChild(group);
3805
+ });
1097
3806
  }
1098
3807
  /**
1099
- * 渲染单个路径到指定的组中
1100
- */
1101
- renderPathToGroup(group, id, element) {
1102
- const { coordinates, style } = element;
1103
- if (coordinates.length < 2)
1104
- return;
1105
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1106
- // 构建路径数据
1107
- let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
1108
- for (let i = 1; i < coordinates.length; i++) {
1109
- pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
3808
+ * 变换 SVG 路径数据
3809
+ */
3810
+ transformSvgPath(pathData, center, scale, direction, originalWidth, originalHeight) {
3811
+ // 解析路径数据并应用变换
3812
+ const commands = pathData.match(/[MmLlHhVvCcSsQqTtAaZz][^MmLlHhVvCcSsQqTtAaZz]*/g) || [];
3813
+ let transformedCommands = [];
3814
+ for (const command of commands) {
3815
+ const type = command[0];
3816
+ const params = command
3817
+ .slice(1)
3818
+ .trim()
3819
+ .split(/[\s,]+/)
3820
+ .filter(Boolean)
3821
+ .map(Number);
3822
+ if (type === 'Z' || type === 'z') {
3823
+ // 闭合路径,不需要变换
3824
+ transformedCommands.push(command);
3825
+ continue;
3826
+ }
3827
+ // 处理坐标参数
3828
+ let transformedParams = [];
3829
+ for (let i = 0; i < params.length; i += 2) {
3830
+ if (i + 1 < params.length) {
3831
+ let x = params[i];
3832
+ let y = params[i + 1];
3833
+ // 应用变换:先平移到中心,然后缩放、旋转,最后平移到目标位置
3834
+ // 1. 平移到原点(相对于原始尺寸的中心)
3835
+ x -= originalWidth / 2;
3836
+ y -= originalHeight / 2;
3837
+ // 2. 应用缩放
3838
+ x *= scale;
3839
+ y *= scale;
3840
+ // 3. 应用旋转
3841
+ const cos = Math.cos(-direction);
3842
+ const sin = Math.sin(-direction);
3843
+ const newX = x * cos - y * sin;
3844
+ const newY = x * sin + y * cos;
3845
+ // 4. 平移到目标位置
3846
+ x = newX + center[0];
3847
+ y = newY + center[1];
3848
+ transformedParams.push(x, y);
3849
+ }
3850
+ }
3851
+ // 重建命令
3852
+ if (transformedParams.length > 0) {
3853
+ transformedCommands.push(type + transformedParams.join(' '));
3854
+ }
1110
3855
  }
1111
- path.style.mixBlendMode = 'normal';
1112
- // 设置路径属性
1113
- path.setAttribute('d', pathData);
1114
- // 直接给fill的颜色设置透明度会导致path重叠的部分颜色叠加,所以使用fill填充实色,通过fill-opacity设置透明度
1115
- path.setAttribute('fill', 'none');
1116
- // path.setAttribute('fill-opacity', '0.4');
1117
- path.setAttribute('stroke', style.lineColor || '#000000');
1118
- path.setAttribute('mix-blend-mode', 'normal');
1119
- const lineWidth = Math.max(style.lineWidth || 1, 0.5);
1120
- path.setAttribute('stroke-width', lineWidth.toString());
1121
- path.setAttribute('stroke-linecap', 'round');
1122
- path.setAttribute('stroke-linejoin', 'round');
1123
- // 注意:这里不设置 opacity,因为透明度由父组控制
1124
- // path.setAttribute('vector-effect', 'non-scaling-stroke');
1125
- path.classList.add('vector-path');
1126
- this.boundaryPaths[id].push(path);
1127
- group.appendChild(path);
3856
+ return transformedCommands.join(' ');
1128
3857
  }
1129
3858
  }
1130
3859
 
@@ -1548,15 +4277,18 @@ const DOODLE_STYLES = {
1548
4277
  lineColor: '#ff5722',
1549
4278
  fillColor: '#ff9800', // 粉色半透明填充
1550
4279
  lineWidth: DEFAULT_LINE_WIDTHS.TIME_LIMIT_OBSTACLE,
1551
- opacity: DEFAULT_OPACITIES.HIGH,
4280
+ opacity: DEFAULT_OPACITIES.DOODLE,
1552
4281
  };
1553
4282
  const PATH_EDGE_STYLES = {
1554
4283
  lineWidth: DEFAULT_LINE_WIDTHS.PATH,
1555
4284
  opacity: DEFAULT_OPACITIES.MEDIUM,
1556
- edgeLineColor: 'rgba(231, 238, 246)',
1557
4285
  transLineColor: 'transparent',
4286
+ edgeLineColor: 'rgba(231, 238, 246)',
1558
4287
  mowedLineColor: 'rgba(231, 238, 246)',
1559
4288
  mowingLineColor: 'rgba(123, 200, 187)',
4289
+ // edgeLineColor: 'red',
4290
+ // mowedLineColor: 'red',
4291
+ // mowingLineColor: 'red',
1560
4292
  };
1561
4293
  const CHANNEL_STYLES = {
1562
4294
  lineColor: 'purple',
@@ -1590,13 +4322,13 @@ const DEFAULT_STYLES = {
1590
4322
  function convertPointsFormat(points) {
1591
4323
  if (!points || points.length === 0)
1592
4324
  return null;
1593
- return points.map(point => {
4325
+ return points.map((point) => {
1594
4326
  if (point.length >= 2) {
1595
4327
  // 对前两个元素应用缩放因子,保留其他元素
1596
4328
  return [
1597
4329
  point[0] * SCALE_FACTOR,
1598
4330
  -point[1] * SCALE_FACTOR, // Y轴翻转,与Python代码一致
1599
- ...point.slice(2) // 保留第三个及以后的元素
4331
+ ...point.slice(2), // 保留第三个及以后的元素
1600
4332
  ];
1601
4333
  }
1602
4334
  return point;
@@ -1611,7 +4343,7 @@ function convertPositionFormat(position) {
1611
4343
  return null;
1612
4344
  return {
1613
4345
  x: position[0] * SCALE_FACTOR,
1614
- y: -position[1] * SCALE_FACTOR // Y轴翻转
4346
+ y: -position[1] * SCALE_FACTOR, // Y轴翻转
1615
4347
  };
1616
4348
  }
1617
4349
  /**
@@ -1620,9 +4352,146 @@ function convertPositionFormat(position) {
1620
4352
  function convertCoordinate(x, y) {
1621
4353
  return {
1622
4354
  x: x * SCALE_FACTOR,
1623
- y: -y * SCALE_FACTOR // Y轴翻转
4355
+ y: -y * SCALE_FACTOR, // Y轴翻转
1624
4356
  };
1625
4357
  }
4358
+ /**
4359
+ * @param x x坐标
4360
+ * @param y y坐标
4361
+ * @param isAllowInBoundary 是否允许点在边界上的判断
4362
+ * @return ture-点在边界上即可视为在边界内,false-严格判断点在边界内
4363
+ */
4364
+ function isPointIn(x, y, pointList, isAllowInBoundary) {
4365
+ let count = 0;
4366
+ let size = pointList.length;
4367
+ let p1, p2, p3;
4368
+ for (let i = 0; i < size; i++) {
4369
+ p1 = pointList[i];
4370
+ p2 = pointList[(i + 1) % size];
4371
+ if (p1.y == null || p2.y == null || p1.x == null || p2.x == null) {
4372
+ continue;
4373
+ }
4374
+ if (p1.y === p2.y) {
4375
+ continue;
4376
+ }
4377
+ if (y > Math.min(p1.y, p2.y) && y < Math.max(p1.y, p2.y)) {
4378
+ const interX = ((y - p1.y) * (p2.x - p1.x)) / (p2.y - p1.y) + p1.x;
4379
+ if (interX >= x) {
4380
+ count++;
4381
+ }
4382
+ else if (interX == x) {
4383
+ return isAllowInBoundary;
4384
+ }
4385
+ }
4386
+ else {
4387
+ if (y == p2.y && x <= p2.x) {
4388
+ p3 = pointList[(i + 2) % size];
4389
+ if (y >= Math.min(p1.y, p3.y) && y <= Math.max(p1.y, p3.y)) {
4390
+ // 若当前点的y坐标位于 p1和p3组成的线段关于y轴的投影中,则记为该点的射线只穿过端点一次。
4391
+ ++count;
4392
+ }
4393
+ else {
4394
+ // 若当前点的y坐标不能包含在p1和p3组成的线段关于y轴的投影中,则点射线通过的两条线段组成了一个弯折的部分,
4395
+ // 此时我们记射线穿过该端点两次
4396
+ count += 2;
4397
+ }
4398
+ }
4399
+ }
4400
+ }
4401
+ return count % 2 == 1;
4402
+ }
4403
+ /**
4404
+ * 用于判断三个点的方向的辅助方法
4405
+ */
4406
+ function orientation(p, q, r) {
4407
+ const val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
4408
+ if (val == 0)
4409
+ return 0; // colinear
4410
+ return val > 0 ? 1 : 2; // clock or counterclock wise
4411
+ }
4412
+ /**
4413
+ * 检查点q是否在线段pr上的辅助方法
4414
+ */
4415
+ function onSegment(p, q, r) {
4416
+ if (q.x <= Math.max(p.x, r.x) &&
4417
+ q.x >= Math.min(p.x, r.x) &&
4418
+ q.y <= Math.max(p.y, r.y) &&
4419
+ q.y >= Math.min(p.y, r.y)) {
4420
+ return true;
4421
+ }
4422
+ return false;
4423
+ }
4424
+ /**
4425
+ * 判断两条线段是否相交的方法
4426
+ */
4427
+ function doTwoLinesIntersect(p1, q1, p2, q2) {
4428
+ //处理p1和q1两个点相同的情况
4429
+ if (p1.x - q1.x == 0 && p1.y - q1.y == 0) {
4430
+ return false;
4431
+ }
4432
+ if (p2.x - q2.x == 0 && p2.y - q2.y == 0) {
4433
+ return false;
4434
+ }
4435
+ // 计算四个点的方向
4436
+ const o1 = orientation(p1, q1, p2);
4437
+ const o2 = orientation(p1, q1, q2);
4438
+ const o3 = orientation(p2, q2, p1);
4439
+ const o4 = orientation(p2, q2, q1);
4440
+ // 一般情况,如果四个方向两两不同,则线段相交
4441
+ if (o1 != o2 && o3 != o4) {
4442
+ return true;
4443
+ }
4444
+ // 特殊情况,当线段的端点在另一条线段上时
4445
+ if (o1 == 0 && onSegment(p1, q1, p2))
4446
+ return true;
4447
+ if (o2 == 0 && onSegment(p1, q1, q2))
4448
+ return true;
4449
+ if (o3 == 0 && onSegment(p2, q2, p1))
4450
+ return true;
4451
+ if (o4 == 0 && onSegment(p2, q2, q1))
4452
+ return true;
4453
+ // 如果以上情况都不满足,则线段不相交
4454
+ return false;
4455
+ }
4456
+ /**
4457
+ * 判断多点折线是否相交
4458
+ */
4459
+ function doIntersect(points1, points2) {
4460
+ if (points1 == null || points2 == null || points1.length < 3 || points2.length < 3) {
4461
+ return false;
4462
+ }
4463
+ for (let i = 0; i < points1.length - 1; i++) {
4464
+ for (let j = 0; j < points2.length - 1; j++) {
4465
+ if (doTwoLinesIntersect(points1[i], points1[i + 1], points2[j], points2[j + 1])) {
4466
+ return true;
4467
+ }
4468
+ }
4469
+ }
4470
+ return false;
4471
+ }
4472
+ /**
4473
+ * 两个图形是否完全分离,互相不包含
4474
+ */
4475
+ function isOutsideToEachOther(points1, points2) {
4476
+ // 相交关系
4477
+ if (doIntersect(points1, points2)) {
4478
+ return false;
4479
+ }
4480
+ // 点关系,判断每个图形的点都在另一个图形外部
4481
+ for (let point of points1) {
4482
+ if (isPointIn(point.x, point.y, points2, true)) {
4483
+ // Log.i("ycf", "isOutsideToEachOther: mapPoint1=" + mapPoint);
4484
+ return false;
4485
+ }
4486
+ }
4487
+ for (let point of points2) {
4488
+ if (isPointIn(point.x, point.y, points1, true)) {
4489
+ // Log.i("ycf", "isOutsideToEachOther: mapPoint2=" + mapPoint);
4490
+ return false;
4491
+ }
4492
+ }
4493
+ return true;
4494
+ }
1626
4495
 
1627
4496
  /**
1628
4497
  * 按Python逻辑创建路径段:根据连续的两点之间的关系确定线段类型
@@ -1631,7 +4500,7 @@ function createPathSegmentsByType(list) {
1631
4500
  const segments = {
1632
4501
  edge: [],
1633
4502
  mowing: [],
1634
- trans: []
4503
+ trans: [],
1635
4504
  };
1636
4505
  if (list.length < 2)
1637
4506
  return segments;
@@ -1641,12 +4510,16 @@ function createPathSegmentsByType(list) {
1641
4510
  for (const currentPoint of list) {
1642
4511
  const currentCoord = {
1643
4512
  x: currentPoint.postureX,
1644
- y: currentPoint.postureY
4513
+ y: currentPoint.postureY,
1645
4514
  };
1646
4515
  if (lastPoint !== null) {
1647
4516
  // 判断上一个点和当前点是否需要绘制 (iso端逻辑)
1648
- const lastShouldDraw = lastPoint.pathType === '00' || lastPoint.pathType === '01' || lastPoint.knifeRotation === '01';
1649
- const currentShouldDraw = currentPoint.pathType === '00' || currentPoint.pathType === '01' || currentPoint.knifeRotation === '01';
4517
+ const lastShouldDraw = lastPoint.pathType === '00' ||
4518
+ lastPoint.pathType === '01' ||
4519
+ lastPoint.knifeRotation === '01';
4520
+ const currentShouldDraw = currentPoint.pathType === '00' ||
4521
+ currentPoint.pathType === '01' ||
4522
+ currentPoint.knifeRotation === '01';
1650
4523
  let segmentType;
1651
4524
  if (lastShouldDraw && currentShouldDraw) {
1652
4525
  // 需要绘制的两点之间用实线连接
@@ -1665,9 +4538,9 @@ function createPathSegmentsByType(list) {
1665
4538
  currentSegment = [
1666
4539
  {
1667
4540
  x: lastPoint.postureX,
1668
- y: lastPoint.postureY
4541
+ y: lastPoint.postureY,
1669
4542
  },
1670
- currentCoord
4543
+ currentCoord,
1671
4544
  ];
1672
4545
  currentSegmentType = segmentType;
1673
4546
  }
@@ -1885,6 +4758,136 @@ function calculateMapGpsCenter(mapData) {
1885
4758
  };
1886
4759
  }
1887
4760
 
4761
+ /**
4762
+ * 并查集(Union-Find)是一种非常高效的数据结构,用于处理动态连通性问题。
4763
+ * 它可以快速判断网络中任意两点是否连通,并能将不连通的集合合并。
4764
+ */
4765
+ class UnionFind {
4766
+ /**
4767
+ * 构造函数,n为图的节点总数
4768
+ * @param {number} n - 节点总数
4769
+ */
4770
+ constructor(n) {
4771
+ this.count = n; // 连通分量的数量
4772
+ this.parent = new Array(n); // parent[i]表示第i个元素所指向的父节点
4773
+ // 初始时,每个节点的父节点是自己
4774
+ for (let i = 0; i < n; i++) {
4775
+ this.parent[i] = i;
4776
+ }
4777
+ }
4778
+ /**
4779
+ * 查找元素p所对应的集合编号(根节点)
4780
+ * @param {number} p - 要查找的元素
4781
+ * @returns {number} 根节点的编号
4782
+ */
4783
+ find(p) {
4784
+ while (p !== this.parent[p]) {
4785
+ this.parent[p] = this.parent[this.parent[p]]; // 路径压缩
4786
+ p = this.parent[p];
4787
+ }
4788
+ return p;
4789
+ }
4790
+ /**
4791
+ * 判断元素p和元素q是否属于同一集合
4792
+ * @param {number} p - 第一个元素
4793
+ * @param {number} q - 第二个元素
4794
+ * @returns {boolean} 是否连通
4795
+ */
4796
+ isConnected(p, q) {
4797
+ return this.find(p) === this.find(q);
4798
+ }
4799
+ /**
4800
+ * 合并元素p和元素q所属的集合
4801
+ * @param {number} p - 第一个元素
4802
+ * @param {number} q - 第二个元素
4803
+ */
4804
+ union(p, q) {
4805
+ const rootP = this.find(p);
4806
+ const rootQ = this.find(q);
4807
+ if (rootP === rootQ) {
4808
+ return; // 已经在同一个集合中
4809
+ }
4810
+ // 将较小的根节点作为父节点(按秩合并的简化版本)
4811
+ if (rootP < rootQ) {
4812
+ this.parent[rootQ] = rootP;
4813
+ }
4814
+ else {
4815
+ this.parent[rootP] = rootQ;
4816
+ }
4817
+ // 两个集合合并成一个集合,连通分量减1
4818
+ this.count--;
4819
+ }
4820
+ /**
4821
+ * 获取当前的连通分量个数
4822
+ * @returns {number} 连通分量数量
4823
+ */
4824
+ getCount() {
4825
+ return this.count;
4826
+ }
4827
+ /**
4828
+ * 获取联通的组
4829
+ * @param {Array} list - 原始元素列表
4830
+ * @returns {Array<Set>} 联通组列表
4831
+ */
4832
+ getConnectedGroup(list) {
4833
+ if (!list || list.length === 0 || !this.parent || this.parent.length === 0) {
4834
+ return null;
4835
+ }
4836
+ if (list.length !== this.parent.length) {
4837
+ return null;
4838
+ }
4839
+ const map = new Map();
4840
+ // 遍历所有元素,按根节点分组
4841
+ for (let i = 0; i < this.parent.length; i++) {
4842
+ const root = this.parent[i];
4843
+ if (!map.has(root)) {
4844
+ map.set(root, new Set());
4845
+ }
4846
+ map.get(root).add(list[i]);
4847
+ }
4848
+ return Array.from(map.values());
4849
+ }
4850
+ /**
4851
+ * 重置并查集
4852
+ * @param {number} n - 新的节点总数
4853
+ */
4854
+ reset(n) {
4855
+ this.count = n;
4856
+ this.parent = new Array(n);
4857
+ for (let i = 0; i < n; i++) {
4858
+ this.parent[i] = i;
4859
+ }
4860
+ }
4861
+ }
4862
+
4863
+ function isTunnelConnected(a, b, connectIds) {
4864
+ if (!a || !b)
4865
+ return false;
4866
+ if (!connectIds || connectIds?.length === 0)
4867
+ return false;
4868
+ const temp = [a?.id, b?.id];
4869
+ temp.sort();
4870
+ return connectIds?.includes(temp?.join('-'));
4871
+ }
4872
+ function isOverlayConnected(a, b) {
4873
+ if (!a || !b) {
4874
+ return false;
4875
+ }
4876
+ if (!a?.points?.length || !b?.points?.length) {
4877
+ return false;
4878
+ }
4879
+ const aPoints = a?.points?.map(item => ({ x: item[0], y: item[1] }));
4880
+ const bPoints = b?.points?.map(item => ({ x: item[0], y: item[1] }));
4881
+ try {
4882
+ if (isOutsideToEachOther(aPoints, bPoints)) {
4883
+ return false;
4884
+ }
4885
+ }
4886
+ catch (error) {
4887
+ console.log('error->', error);
4888
+ }
4889
+ return true;
4890
+ }
1888
4891
  /**
1889
4892
  * 通过 mapData 和 pathData 生成所有 boundary 的数据
1890
4893
  * @param mapData 地图数据
@@ -1893,11 +4896,12 @@ function calculateMapGpsCenter(mapData) {
1893
4896
  */
1894
4897
  function generateBoundaryData(mapData, pathData) {
1895
4898
  const boundaryData = [];
4899
+ let chargingPileBoundary = undefined;
1896
4900
  if (!mapData || !mapData.sub_maps) {
1897
4901
  return boundaryData;
1898
4902
  }
1899
4903
  // 第一步:收集所有TUNNEL数据的connection信息
1900
- const connectedBoundaryIds = new Set();
4904
+ const connectIds = [];
1901
4905
  // 遍历mapData中的tunnels字段
1902
4906
  if (mapData.tunnels && Array.isArray(mapData.tunnels)) {
1903
4907
  for (const tunnel of mapData.tunnels) {
@@ -1905,10 +4909,8 @@ function generateBoundaryData(mapData, pathData) {
1905
4909
  if (connection) {
1906
4910
  // connection可能是单个数字或数组
1907
4911
  if (Array.isArray(connection)) {
1908
- connection.forEach(id => connectedBoundaryIds.add(id));
1909
- }
1910
- else if (typeof connection === 'number') {
1911
- connectedBoundaryIds.add(connection);
4912
+ connection.sort();
4913
+ connectIds.push(connection.join('-'));
1912
4914
  }
1913
4915
  }
1914
4916
  }
@@ -1919,9 +4921,9 @@ function generateBoundaryData(mapData, pathData) {
1919
4921
  if (!subMap.elements)
1920
4922
  continue;
1921
4923
  // 每个sub_map的elements是边界坐标,没有sub_map只有一个boundary数据
1922
- const boundaryElement = subMap.elements.find(element => element.type === 'BOUNDARY');
4924
+ const boundaryElement = subMap.elements.find((element) => element.type === 'BOUNDARY');
1923
4925
  // 如果当前subMap存在充电桩且充电桩存在tunnel,说明当前subMap中的boundary是初始boundary,这个boundary不为孤立区域
1924
- const hasTunnelToChargingPile = subMap.elements.some(element => element.type === 'CHARGING_PILE' && element.tunnel);
4926
+ const hasTunnelToChargingPile = subMap.elements.some((element) => element.type === 'CHARGING_PILE' && element.tunnel);
1925
4927
  // 创建基础的 boundary 数据(来自 mapData)
1926
4928
  const boundary = {
1927
4929
  // 从 BOUNDARY 元素复制属性
@@ -1930,8 +4932,6 @@ function generateBoundaryData(mapData, pathData) {
1930
4932
  area: subMap?.area,
1931
4933
  points: convertPointsFormat(boundaryElement?.points) || [],
1932
4934
  type: boundaryElement.type,
1933
- // 判断是否为孤立子区域
1934
- isIsolated: hasTunnelToChargingPile ? false : !connectedBoundaryIds.has(boundaryElement.id)
1935
4935
  };
1936
4936
  // 如果有 pathData,尝试匹配对应的分区数据
1937
4937
  if (pathData) {
@@ -1947,8 +4947,33 @@ function generateBoundaryData(mapData, pathData) {
1947
4947
  boundary.endTime = partitionData.endTime;
1948
4948
  }
1949
4949
  }
4950
+ if (hasTunnelToChargingPile) {
4951
+ chargingPileBoundary = boundary;
4952
+ }
1950
4953
  boundaryData.push(boundary);
1951
4954
  }
4955
+ const unionFind = new UnionFind(boundaryData?.length);
4956
+ for (let i = 0; i < boundaryData?.length - 1; i++) {
4957
+ for (let j = i + 1; j < boundaryData?.length; j++) {
4958
+ const boundary1 = boundaryData[i];
4959
+ const boundary2 = boundaryData[j];
4960
+ const isChannelConnect = isTunnelConnected(boundary1, boundary2, connectIds);
4961
+ const isOverlayConnect = isOverlayConnected(boundary1, boundary2);
4962
+ if (isChannelConnect || isOverlayConnect) {
4963
+ unionFind.union(i, j);
4964
+ }
4965
+ }
4966
+ }
4967
+ const tunnelAndOverlayList = unionFind.getConnectedGroup(boundaryData);
4968
+ const chargingPileConnectBoundarys = tunnelAndOverlayList?.find(item => item?.has(chargingPileBoundary));
4969
+ for (let boundary of boundaryData) {
4970
+ if (chargingPileConnectBoundarys?.has(boundary)) {
4971
+ boundary.isIsolated = false;
4972
+ }
4973
+ else {
4974
+ boundary.isIsolated = true;
4975
+ }
4976
+ }
1952
4977
  return boundaryData;
1953
4978
  }
1954
4979
 
@@ -1960,100 +4985,9 @@ var RealTimeDataType;
1960
4985
  RealTimeDataType[RealTimeDataType["STATUS"] = 4] = "STATUS";
1961
4986
  })(RealTimeDataType || (RealTimeDataType = {}));
1962
4987
 
1963
- /**
1964
- * 射线法判断点是否在多边形内部
1965
- * @param x 点的x坐标
1966
- * @param y 点的y坐标
1967
- * @param pointList 多边形顶点列表,格式:[[x1, y1], [x2, y2], ...]
1968
- * @param isAllowInBoundary 是否允许点在边界上的判断
1969
- * @returns true-点在多边形内部,false-点在多边形外部
1970
- */
1971
- function isPointIn(x, y, pointList, isAllowInBoundary = false) {
1972
- let count = 0;
1973
- const size = pointList.length;
1974
- for (let i = 0; i < size; i++) {
1975
- const p1 = pointList[i];
1976
- const p2 = pointList[(i + 1) % size];
1977
- // 检查点坐标是否有效
1978
- if (p1[1] === null || p2[1] === null || p1[0] === null || p2[0] === null) {
1979
- continue;
1980
- }
1981
- // 跳过水平线段
1982
- if (p1[1] === p2[1]) {
1983
- continue;
1984
- }
1985
- // 检查射线是否与当前边相交
1986
- if (y > Math.min(p1[1], p2[1]) && y < Math.max(p1[1], p2[1])) {
1987
- // 计算射线与边的交点x坐标
1988
- const interX = ((y - p1[1]) * (p2[0] - p1[0])) / (p2[1] - p1[1]) + p1[0];
1989
- if (interX >= x) {
1990
- count++;
1991
- }
1992
- else if (interX === x) {
1993
- return isAllowInBoundary; // 点在边界上
1994
- }
1995
- }
1996
- else {
1997
- // 处理特殊情况:点在边的端点上
1998
- if (y === p2[1] && x <= p2[0]) {
1999
- const p3 = pointList[(i + 2) % size];
2000
- if (y >= Math.min(p1[1], p3[1]) && y <= Math.max(p1[1], p3[1])) {
2001
- // 若当前点的y坐标位于 p1和p3组成的线段关于y轴的投影中,
2002
- // 则记为该点的射线只穿过端点一次
2003
- count++;
2004
- }
2005
- else {
2006
- // 若当前点的y坐标不能包含在p1和p3组成的线段关于y轴的投影中,
2007
- // 则点射线通过的两条线段组成了一个弯折的部分,
2008
- // 此时我们记射线穿过该端点两次
2009
- count += 2;
2010
- }
2011
- }
2012
- }
2013
- }
2014
- // 奇数个交点表示在内部,偶数个交点表示在外部
2015
- return count % 2 === 1;
2016
- }
2017
- // 使用示例
2018
- // const boundaryPoints: number[][] = [
2019
- // [0, 0],
2020
- // [10, 0],
2021
- // [10, 10],
2022
- // [0, 10]
2023
- // ];
2024
- // const testPoint = [5, 5];
2025
- // // 判断点是否在边界内
2026
- // const isInside = isPointIn(testPoint[0], testPoint[1], boundaryPoints);
2027
- // console.log(`点 (${testPoint[0]}, ${testPoint[1]}) 是否在边界内: ${isInside}`);
2028
- // // 带间距检查的包含判断
2029
- // const isInsideWithSpace = contains(testPoint[0], testPoint[1], boundaryPoints, true);
2030
- // console.log(`点 (${testPoint[0]}, ${testPoint[1]}) 是否在边界内(带间距): ${isInsideWithSpace}`);
2031
- // // 查找包含点的边界
2032
- // const boundaryLayers = [
2033
- // { id: 1, pointList: [[0, 0], [10, 0], [10, 10], [0, 10]] },
2034
- // { id: 2, pointList: [[20, 20], [30, 20], [30, 30], [20, 30]] }
2035
- // ];
2036
- // const foundBoundary = findContainsBoundary(testPoint[0], testPoint[1], boundaryLayers);
2037
- // if (foundBoundary) {
2038
- // console.log(`找到包含点的边界,ID: ${foundBoundary.id}`);
2039
- // } else {
2040
- // console.log('未找到包含点的边界');
2041
- // }
2042
-
2043
4988
  // src/utils/handleRealTime.ts
2044
4989
  // import { BoundaryDataBuilder } from '../processor/builder/BoundaryDataBuilder';
2045
4990
  // import { MapElement } from '../types/elements';
2046
- // 根据postureX和postureY,结合射线法,获取到分区id
2047
- const getPartitionId = (partitionBoundary, postureX, postureY) => {
2048
- if (!postureX || !postureY) {
2049
- return null;
2050
- }
2051
- // 射线法,判断当前的点在哪个分区里
2052
- const partitionId = partitionBoundary.find((item) => {
2053
- return isPointIn(postureX, postureY, item.points || []);
2054
- })?.id;
2055
- return partitionId;
2056
- };
2057
4991
  /**
2058
4992
  * 处理实时数据的消息,这里的实时数据消息有两种,一种是实时轨迹,一种是割草进度,其中这两种下发的时间频次不一样
2059
4993
  * 实时轨迹的路径需要依靠割草进度时候的割草状态判断,目前只能根据上一次获取到的割草进度的状态来处理,如果一开始没有割草的状态,则默认为不割草,后续会根据割草进度来更新
@@ -2061,7 +4995,9 @@ const getPartitionId = (partitionBoundary, postureX, postureY) => {
2061
4995
  * @param param0
2062
4996
  * @returns
2063
4997
  */
2064
- const handleMultipleRealTimeData = ({ realTimeData, isMowing, pathData, partitionBoundary, }) => {
4998
+ const handleMultipleRealTimeData = ({ realTimeData, isMowing, pathData,
4999
+ // partitionBoundary,
5000
+ currentMowingPartition, }) => {
2065
5001
  // 先将数据进行倒排,这样好插入数据
2066
5002
  if (realTimeData.length > 0) {
2067
5003
  realTimeData.reverse();
@@ -2071,15 +5007,16 @@ const handleMultipleRealTimeData = ({ realTimeData, isMowing, pathData, partitio
2071
5007
  // 目前的方式是,如果是location数据,判断是否割草,取决于前一次的process数据的割草状态+本次的location的verchState
2072
5008
  // 关于location的分区,需要通过地图数据,结合射线法,判断当前的点在哪个分区里
2073
5009
  let mowingStatus = isMowing || false;
5010
+ let newCurrentMowingPartitionId = currentMowingPartition;
5011
+ console.info('handleMultipleRealTimeData==newCurrentMowingPartitionId=================', newCurrentMowingPartitionId);
2074
5012
  realTimeData.forEach((item) => {
2075
5013
  // 这里需要区分,是割草进度还是割草轨迹
2076
5014
  if (item.type === REAL_TIME_DATA_TYPE.LOCATION) {
2077
5015
  // 割草轨迹
2078
5016
  const { postureX, postureY, vehicleState } = item;
2079
- const currentPartitionId = getPartitionId(partitionBoundary, Number(postureX), Number(postureY));
2080
- if (currentPartitionId && newPathData?.[currentPartitionId]) {
2081
- const currentPathData = newPathData[currentPartitionId];
2082
- newPathData[currentPartitionId] = {
5017
+ if (newCurrentMowingPartitionId && newPathData?.[newCurrentMowingPartitionId]) {
5018
+ const currentPathData = newPathData[newCurrentMowingPartitionId];
5019
+ newPathData[newCurrentMowingPartitionId] = {
2083
5020
  ...currentPathData,
2084
5021
  points: [
2085
5022
  ...(currentPathData?.points || []),
@@ -2089,7 +5026,7 @@ const handleMultipleRealTimeData = ({ realTimeData, isMowing, pathData, partitio
2089
5026
  knifeRotation: mowingStatus && vehicleState === RobotStatus.MOWING ? '01' : '00', // "knifeRotation": "01",//刀盘是否转动 00-否 01-是
2090
5027
  // knifeRotation: '01', // "knifeRotation": "01",//刀盘是否转动 00-否 01-是
2091
5028
  pathType: '', //"pathType": "01",//路径类型 : 00-巡边 01-弓字型割草 02-地图测试 03-转移路径 04-避障路径 05-恢复/脱困路径
2092
- partitionId: currentPartitionId.toString(), // TODO:不知道为什么这里的id需要是字符串类型?
5029
+ partitionId: newCurrentMowingPartitionId.toString(), // TODO:不知道为什么这里的id需要是字符串类型?
2093
5030
  },
2094
5031
  ],
2095
5032
  };
@@ -2106,17 +5043,22 @@ const handleMultipleRealTimeData = ({ realTimeData, isMowing, pathData, partitio
2106
5043
  else {
2107
5044
  mowingStatus = false;
2108
5045
  }
2109
- const currentPartitionId = currentMowBoundary ? currentMowBoundary.toString() : null;
2110
- if (currentMowProgress && currentPartitionId && newPathData?.[currentPartitionId]) {
2111
- newPathData[currentPartitionId].partitionPercentage = currentMowProgress / 100;
2112
- newPathData[currentPartitionId].finishedArea =
2113
- (newPathData[currentPartitionId].area * currentMowProgress) / 10000;
5046
+ newCurrentMowingPartitionId = currentMowBoundary
5047
+ ? currentMowBoundary.toString()
5048
+ : newCurrentMowingPartitionId || '';
5049
+ if (currentMowProgress &&
5050
+ newCurrentMowingPartitionId &&
5051
+ newPathData?.[newCurrentMowingPartitionId]) {
5052
+ newPathData[newCurrentMowingPartitionId].partitionPercentage = currentMowProgress / 100;
5053
+ newPathData[newCurrentMowingPartitionId].finishedArea =
5054
+ (newPathData[newCurrentMowingPartitionId].area * currentMowProgress) / 10000;
2114
5055
  }
2115
5056
  }
2116
5057
  });
2117
5058
  return {
2118
5059
  pathData: newPathData,
2119
5060
  isMowing: mowingStatus,
5061
+ currentMowingPartition: newCurrentMowingPartitionId || '',
2120
5062
  };
2121
5063
  };
2122
5064
  /**
@@ -2125,11 +5067,12 @@ const handleMultipleRealTimeData = ({ realTimeData, isMowing, pathData, partitio
2125
5067
  * @param isMowing 上一次的割草状态
2126
5068
  * @returns 新的割草状态
2127
5069
  */
2128
- const getProcessMowingDataFromRealTimeData = ({ realTimeData, isMowing, pathData, }) => {
5070
+ const getProcessMowingDataFromRealTimeData = ({ realTimeData, isMowing, pathData, currentMowingPartition, }) => {
2129
5071
  let newMowingStatus = isMowing;
2130
5072
  let newPathData = pathData || {};
2131
5073
  // 找到返回的第一个实时进度的点
2132
5074
  const firstProcessData = realTimeData.find((item) => item.type === RealTimeDataType.PROCESS);
5075
+ const currentMowBoundary = firstProcessData?.currentMowBoundary || currentMowingPartition || '';
2133
5076
  if (firstProcessData) {
2134
5077
  // console.log('firstProcessData==', firstProcessData);
2135
5078
  const { action, subAction, currentMowBoundary, currentMowProgress } = firstProcessData;
@@ -2150,6 +5093,7 @@ const getProcessMowingDataFromRealTimeData = ({ realTimeData, isMowing, pathData
2150
5093
  return {
2151
5094
  isMowing: newMowingStatus,
2152
5095
  pathData: newPathData,
5096
+ currentMowingPartition: currentMowBoundary || '',
2153
5097
  };
2154
5098
  };
2155
5099
 
@@ -2165,13 +5109,15 @@ var hNoPosition = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEoAAABKCAYAAAA
2165
5109
 
2166
5110
  var hDisabled = "data:image/png;base64,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";
2167
5111
 
5112
+ var x3Edger = "data:image/png;base64,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";
5113
+
2168
5114
  var x3Mower = "data:image/png;base64,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";
2169
5115
 
2170
5116
  var x3NoPosition = "data:image/png;base64,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";
2171
5117
 
2172
5118
  var x3Disabled = "data:image/png;base64,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";
2173
5119
 
2174
- function getMowerImageByModal(mowerModal) {
5120
+ function getMowerImageByModal(mowerModal, hasEdger) {
2175
5121
  if (mowerModal.includes('i')) {
2176
5122
  return iMower;
2177
5123
  }
@@ -2179,7 +5125,7 @@ function getMowerImageByModal(mowerModal) {
2179
5125
  return hMower;
2180
5126
  }
2181
5127
  else if (mowerModal.includes('x3')) {
2182
- return x3Mower;
5128
+ return hasEdger ? x3Edger : x3Mower;
2183
5129
  }
2184
5130
  return iMower;
2185
5131
  }
@@ -2207,12 +5153,12 @@ function getNoPositionMowerImageByModal(mowerModal) {
2207
5153
  }
2208
5154
  return iNoPosition;
2209
5155
  }
2210
- function getMowerImage(positonConfig, modelType) {
5156
+ function getMowerImage(positonConfig, modelType, hasEdger) {
2211
5157
  if (!positonConfig)
2212
5158
  return '';
2213
5159
  const model = modelType?.toLowerCase() || 'i';
2214
5160
  const state = positonConfig.vehicleState;
2215
- const mowerImage = getMowerImageByModal(model);
5161
+ const mowerImage = getMowerImageByModal(model, hasEdger);
2216
5162
  const disabledImage = getDisabledMowerImageByModal(model);
2217
5163
  const noPositionImage = getNoPositionMowerImageByModal(model);
2218
5164
  const positonOutOfRange = isOutOfRange(positonConfig);
@@ -2262,7 +5208,7 @@ var freeSelf = typeof self == 'object' && self && self.Object === Object && self
2262
5208
  var root = freeGlobal || freeSelf || Function('return this')();
2263
5209
 
2264
5210
  /** Built-in value references. */
2265
- var Symbol = root.Symbol;
5211
+ var Symbol$1 = root.Symbol;
2266
5212
 
2267
5213
  /** Used for built-in method references. */
2268
5214
  var objectProto$9 = Object.prototype;
@@ -2278,7 +5224,7 @@ var hasOwnProperty$7 = objectProto$9.hasOwnProperty;
2278
5224
  var nativeObjectToString$1 = objectProto$9.toString;
2279
5225
 
2280
5226
  /** Built-in value references. */
2281
- var symToStringTag$1 = Symbol ? Symbol.toStringTag : undefined;
5227
+ var symToStringTag$1 = Symbol$1 ? Symbol$1.toStringTag : undefined;
2282
5228
 
2283
5229
  /**
2284
5230
  * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
@@ -2333,7 +5279,7 @@ var nullTag = '[object Null]',
2333
5279
  undefinedTag = '[object Undefined]';
2334
5280
 
2335
5281
  /** Built-in value references. */
2336
- var symToStringTag = Symbol ? Symbol.toStringTag : undefined;
5282
+ var symToStringTag = Symbol$1 ? Symbol$1.toStringTag : undefined;
2337
5283
 
2338
5284
  /**
2339
5285
  * The base implementation of `getTag` without fallbacks for buggy environments.
@@ -2450,7 +5396,7 @@ function arrayMap(array, iteratee) {
2450
5396
  var isArray = Array.isArray;
2451
5397
 
2452
5398
  /** Used to convert symbols to primitives and strings. */
2453
- var symbolProto = Symbol ? Symbol.prototype : undefined,
5399
+ var symbolProto = Symbol$1 ? Symbol$1.prototype : undefined,
2454
5400
  symbolToString = symbolProto ? symbolProto.toString : undefined;
2455
5401
 
2456
5402
  /**
@@ -4694,8 +7640,8 @@ var PathSegmentType;
4694
7640
  */
4695
7641
  var UnitsType;
4696
7642
  (function (UnitsType) {
4697
- UnitsType["Metric"] = "metric";
4698
- UnitsType["Imperial"] = "imperial";
7643
+ UnitsType["Metric"] = "Metric";
7644
+ UnitsType["Imperial"] = "Imperial";
4699
7645
  })(UnitsType || (UnitsType = {}));
4700
7646
  /**
4701
7647
  * 面积单位类型枚举
@@ -4982,6 +7928,12 @@ class BoundaryBorderLayer extends BaseLayer {
4982
7928
  this.mowingBoundarys = mowingBoundarys;
4983
7929
  }
4984
7930
  }
7931
+ /**
7932
+ * 获取当前割草任务的边界
7933
+ */
7934
+ getMowingBoundarys() {
7935
+ return this.mowingBoundarys;
7936
+ }
4985
7937
  /**
4986
7938
  * SVG渲染方法
4987
7939
  */
@@ -4990,13 +7942,31 @@ class BoundaryBorderLayer extends BaseLayer {
4990
7942
  return;
4991
7943
  }
4992
7944
  this.scale = scale;
4993
- console.log('draw boundary border->', this.elements, this.mowingBoundarys);
4994
- // 只渲染边界边框类型的元素
7945
+ // 将元素分为两组:非割草边界和割草边界
7946
+ const nonMowingElements = [];
7947
+ const mowingElements = [];
7948
+ // 只处理边界边框类型的元素
4995
7949
  for (const element of this.elements) {
4996
7950
  if (element.type === 'boundary_border') {
4997
- this.renderBoundaryBorder(svgGroup, element);
7951
+ const { originalData } = element;
7952
+ const { id } = originalData || {};
7953
+ // 检查是否为割草边界
7954
+ if (this.mowingBoundarys.includes(Number(id))) {
7955
+ mowingElements.push(element);
7956
+ }
7957
+ else {
7958
+ nonMowingElements.push(element);
7959
+ }
4998
7960
  }
4999
7961
  }
7962
+ // 先渲染非割草边界
7963
+ for (const element of nonMowingElements) {
7964
+ this.renderBoundaryBorder(svgGroup, element);
7965
+ }
7966
+ // 再渲染割草边界(放在最后)
7967
+ for (const element of mowingElements) {
7968
+ this.renderBoundaryBorder(svgGroup, element);
7969
+ }
5000
7970
  }
5001
7971
  /**
5002
7972
  * 渲染边界边框
@@ -5801,7 +8771,7 @@ class MapDataProcessor {
5801
8771
  result.push(boundaryBorderElement);
5802
8772
  // 将边界边框存储到 store 中,以分区ID为key
5803
8773
  if (element.id) {
5804
- const { addSubBoundaryBorder } = useSubBoundaryBorderStore.getState();
8774
+ const { addSubBoundaryBorder } = usePartitionDataStore.getState();
5805
8775
  addSubBoundaryBorder(element.id.toString(), {
5806
8776
  ...boundaryBorderElement,
5807
8777
  });
@@ -5820,7 +8790,7 @@ class MapDataProcessor {
5820
8790
  const obstacleElement = ObstacleDataBuilder.fromMapElement(mapElement, this.mapConfig.obstacle);
5821
8791
  if (obstacleElement) {
5822
8792
  result.push(obstacleElement);
5823
- const { addObstacles } = useSubBoundaryBorderStore.getState();
8793
+ const { addObstacles } = usePartitionDataStore.getState();
5824
8794
  addObstacles(`obstacle-${obstacleElement.originalData.id}`, {
5825
8795
  ...obstacleElement,
5826
8796
  });
@@ -5873,6 +8843,7 @@ class MapDataProcessor {
5873
8843
  break;
5874
8844
  }
5875
8845
  case 'TIME_LIMIT_OBSTACLE': {
8846
+ console.info('TIME_LIMIT_OBSTACLE', element);
5876
8847
  try {
5877
8848
  // 如果有SVG数据,直接创建SVG绘制元素
5878
8849
  if ('svg' in element &&
@@ -5887,7 +8858,7 @@ class MapDataProcessor {
5887
8858
  const svgElement = SvgElementDataBuilder.fromMapElement(mapElement, this.mapConfig.doodle);
5888
8859
  if (svgElement) {
5889
8860
  result.push(svgElement);
5890
- const { addSvgElements } = useSubBoundaryBorderStore.getState();
8861
+ const { addSvgElements } = usePartitionDataStore.getState();
5891
8862
  addSvgElements(`time-limit-obstacle-${svgElement.originalData.id}`, {
5892
8863
  ...svgElement,
5893
8864
  });
@@ -5902,7 +8873,7 @@ class MapDataProcessor {
5902
8873
  const polygonElement = ObstacleDataBuilder.createTimeLimitObstacle(mapElement, this.mapConfig.obstacle);
5903
8874
  if (polygonElement) {
5904
8875
  result.push(polygonElement);
5905
- const { addObstacles } = useSubBoundaryBorderStore.getState();
8876
+ const { addObstacles } = usePartitionDataStore.getState();
5906
8877
  addObstacles(`time-limit-obstacle-${polygonElement.originalData.id}`, {
5907
8878
  ...polygonElement,
5908
8879
  });
@@ -6065,7 +9036,7 @@ class PathDataProcessor {
6065
9036
  * 专门处理边界标签的创建、定位和管理
6066
9037
  */
6067
9038
  class BoundaryLabelsManager {
6068
- constructor(svgView, boundaryData) {
9039
+ constructor(svgView, boundaryData, { unitType, language }) {
6069
9040
  this.container = null;
6070
9041
  this.overlayDiv = null;
6071
9042
  this.globalClickHandler = null;
@@ -6076,6 +9047,8 @@ class BoundaryLabelsManager {
6076
9047
  this.svgView = svgView;
6077
9048
  this.boundaryData = boundaryData;
6078
9049
  this.initializeContainer();
9050
+ this.unitType = unitType;
9051
+ this.language = language;
6079
9052
  }
6080
9053
  /**
6081
9054
  * 初始化容器
@@ -6141,7 +9114,7 @@ class BoundaryLabelsManager {
6141
9114
  labelDiv.setAttribute('data-boundary-id', boundary.id.toString());
6142
9115
  // 样式设置
6143
9116
  labelDiv.style.position = 'absolute';
6144
- labelDiv.style.backgroundColor = 'rgba(30, 30, 31, 0.3)';
9117
+ labelDiv.style.backgroundColor = 'rgba(30, 30, 31, 0.6)';
6145
9118
  labelDiv.style.color = 'rgba(255, 255, 255, 1)';
6146
9119
  labelDiv.style.padding = '6px';
6147
9120
  labelDiv.style.borderRadius = '12px';
@@ -6158,7 +9131,7 @@ class BoundaryLabelsManager {
6158
9131
  labelDiv.style.zIndex = BoundaryLabelsManager.Z_INDEX.DEFAULT.toString();
6159
9132
  // 计算进度
6160
9133
  const progress = boundary.finishedArea && boundary.area
6161
- ? `${Math.round((boundary.finishedArea / boundary.area) * 100)}%`
9134
+ ? `${Math.floor((boundary.finishedArea / boundary.area) * 100)}%`
6162
9135
  : '0%';
6163
9136
  // 基础内容(始终显示)
6164
9137
  const baseContent = document.createElement('div');
@@ -6175,12 +9148,15 @@ class BoundaryLabelsManager {
6175
9148
  this.currentExpandedBoundaryId === boundary.id ? 'block' : 'none';
6176
9149
  extendedContent.style.borderTop = '1px solid rgba(255,255,255,0.2)';
6177
9150
  extendedContent.style.paddingTop = '6px';
9151
+ const boundaryLayer = this.svgView.getLayer(LAYER_DEFAULT_TYPE.BOUNDARY_BORDER);
9152
+ const mowingBoundarys = boundaryLayer.getMowingBoundarys();
6178
9153
  // 面积信息
6179
- const totalArea = convertAreaByUnits(boundary.area || 0, 'metric');
6180
- const finishedArea = convertAreaByUnits(boundary.finishedArea || 0, 'metric');
9154
+ const totalArea = convertAreaByUnits(boundary.area || 0, this.unitType);
9155
+ const finishedArea = convertAreaByUnits(boundary.finishedArea || 0, this.unitType);
6181
9156
  const coverageText = `Coverage: ${finishedArea.value}/${totalArea.value}`;
9157
+ const isMowing = mowingBoundarys.includes(boundary.id);
6182
9158
  // 日期信息
6183
- const dateText = formatBoundaryDateText(boundary.endTime || 0);
9159
+ const dateText = formatBoundaryDateText(isMowing ? Date.now() / 1000 : boundary.endTime || 0);
6184
9160
  const covertHtml = `<div style="margin-bottom: 3px; font-weight: bold;">${coverageText}</div>`;
6185
9161
  const dateHtml = `<div>${dateText}</div>`;
6186
9162
  extendedContent.innerHTML = boundary.finishedArea > 0 ? `${covertHtml}${dateHtml}` : covertHtml;
@@ -6298,7 +9274,6 @@ class BoundaryLabelsManager {
6298
9274
  // 计算边界中心点的地图坐标
6299
9275
  const mapCenter = this.calculatePolygonCentroid(boundary.points);
6300
9276
  if (!mapCenter) {
6301
- console.warn(`BoundaryLabelsManager: 无法计算边界 ${boundary.name} (ID: ${boundary.id}) 的中心点`);
6302
9277
  return;
6303
9278
  }
6304
9279
  // 直接使用预计算的数据进行坐标转换
@@ -6383,7 +9358,6 @@ class BoundaryLabelsManager {
6383
9358
  area = area / 2;
6384
9359
  // 如果面积为0,回退到简单的平均值计算
6385
9360
  if (Math.abs(area) < 1e-10) {
6386
- console.warn('BoundaryLabelsManager: 多边形面积为0,使用平均值计算重心');
6387
9361
  return this.calculateAverageCenter(validPoints);
6388
9362
  }
6389
9363
  centroidX = centroidX / (6 * area);
@@ -7294,6 +10268,10 @@ class MowerPositionManager {
7294
10268
  getElement() {
7295
10269
  return this.container;
7296
10270
  }
10271
+ //
10272
+ setEdger(edger) {
10273
+ this.hasEdger = edger;
10274
+ }
7297
10275
  /**
7298
10276
  * 根据最后一次有效的位置更新数据
7299
10277
  */
@@ -7317,7 +10295,6 @@ class MowerPositionManager {
7317
10295
  postureY = chargingPilesPositionConfig.postureY || 0;
7318
10296
  postureTheta = chargingPilesPositionConfig.postureTheta || 0;
7319
10297
  }
7320
- console.log('updatePositionByLastPosition->', postureX, postureY, postureTheta, chargingPilesPositionConfig);
7321
10298
  // 检查是否需要更新图片
7322
10299
  this.updateMowerImage(chargingPilesPositionConfig);
7323
10300
  // 立即更新位置
@@ -7334,7 +10311,6 @@ class MowerPositionManager {
7334
10311
  const postureX = positionConfig?.postureX || this.lastPosition?.x || 0;
7335
10312
  const postureY = positionConfig?.postureY || this.lastPosition?.y || 0;
7336
10313
  const postureTheta = positionConfig?.postureTheta || this.lastPosition?.rotation || 0;
7337
- console.log('updatePosition manager', JSON.stringify(this.currentPosition), this.currentPosition, !this.currentPosition, positionConfig, this.lastPosition, animationTime);
7338
10314
  // 停止当前动画(如果有)
7339
10315
  this.stopAnimation();
7340
10316
  // 第一个点
@@ -7344,7 +10320,6 @@ class MowerPositionManager {
7344
10320
  y: postureY,
7345
10321
  rotation: postureTheta,
7346
10322
  };
7347
- console.log('updatePosition first->', this.currentPosition);
7348
10323
  this.setElementPosition(this.currentPosition.x, this.currentPosition.y, this.currentPosition.rotation);
7349
10324
  return;
7350
10325
  }
@@ -7366,7 +10341,7 @@ class MowerPositionManager {
7366
10341
  const imgElement = this.mowerElement.querySelector('img');
7367
10342
  if (!imgElement)
7368
10343
  return;
7369
- const imageSrc = getMowerImage(positonConfig, this.modelType);
10344
+ const imageSrc = getMowerImage(positonConfig, this.modelType, this.hasEdger);
7370
10345
  if (imageSrc) {
7371
10346
  imgElement.src = imageSrc;
7372
10347
  imgElement.style.display = 'block';
@@ -7453,12 +10428,10 @@ class MowerPositionManager {
7453
10428
  y: this.onlyUpdateTheta ? 0 : this.targetPosition.y - this.startPosition.y,
7454
10429
  rotation: radNormalize(targetTheta - startTheta),
7455
10430
  };
7456
- console.log('startAnimationToPosition-->', this.deltaPosition, this.onlyUpdateTheta, this.targetPosition, this.startPosition);
7457
10431
  // 开始动画循环
7458
10432
  this.animateStep();
7459
10433
  }
7460
10434
  forceUpdatePosition() {
7461
- console.log('forceUpdatePosition-->', this.currentPosition, this.targetPosition, this.startPosition);
7462
10435
  this.animateStep();
7463
10436
  }
7464
10437
  /**
@@ -7553,13 +10526,6 @@ class MowerPositionManager {
7553
10526
  }
7554
10527
  }
7555
10528
 
7556
- // 记录割草状态,状态变更的时候,变量不触发重新渲染
7557
- const useProcessMowingState = create((set) => ({
7558
- processStateIsMowing: false,
7559
- updateProcessStateIsMowing: (isMowing) => set({ processStateIsMowing: isMowing }),
7560
- resetProcessStateIsMowing: () => set({ processStateIsMowing: false }),
7561
- }));
7562
-
7563
10529
  /**
7564
10530
  * 高级节流函数
7565
10531
  * @param func 要节流的函数
@@ -7599,15 +10565,52 @@ function throttleAdvanced(func, delay, options = { leading: true, trailing: true
7599
10565
  }
7600
10566
  };
7601
10567
  }
10568
+ /**
10569
+ * 检测当前设备是否为移动设备
10570
+ * @returns {boolean} 如果是移动设备返回true,否则返回false
10571
+ */
10572
+ function isMobileDevice() {
10573
+ // 确保在浏览器环境中运行
10574
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
10575
+ return false;
10576
+ }
10577
+ // 检查用户代理字符串
10578
+ const userAgent = navigator.userAgent.toLowerCase();
10579
+ const mobileKeywords = [
10580
+ 'android', 'webos', 'iphone', 'ipad', 'ipod',
10581
+ 'blackberry', 'windows phone', 'mobile'
10582
+ ];
10583
+ const isMobileUserAgent = mobileKeywords.some(keyword => userAgent.includes(keyword));
10584
+ // 检查触摸屏支持
10585
+ const hasTouchScreen = 'ontouchstart' in window ||
10586
+ (navigator.maxTouchPoints && navigator.maxTouchPoints > 0);
10587
+ // 检查屏幕尺寸(移动设备通常屏幕较小)
10588
+ const isSmallScreen = window.innerWidth <= 768;
10589
+ // 综合判断:用户代理包含移动设备关键词,或者有触摸屏且屏幕较小
10590
+ return isMobileUserAgent || (hasTouchScreen && isSmallScreen);
10591
+ }
10592
+
10593
+ // 记录割草状态,状态变更的时候,变量不触发重新渲染
10594
+ const useCurrentMowingDataStore = create((set) => ({
10595
+ // 当前进度数据返回的割草状态是否为在割草
10596
+ processStateIsMowing: false,
10597
+ updateProcessStateIsMowing: (isMowing) => set({ processStateIsMowing: isMowing }),
10598
+ resetProcessStateIsMowing: () => set({ processStateIsMowing: false }),
10599
+ // 当前割草的分区id
10600
+ currentMowingPartitionId: '',
10601
+ updateCurrentMowingPartitionId: (partitionId) => set({ currentMowingPartitionId: partitionId }),
10602
+ resetCurrentMowingPartitionId: () => set({ currentMowingPartitionId: '' }),
10603
+ }));
7602
10604
 
7603
10605
  // Google Maps 叠加层类 - 带编辑功能
7604
10606
  class MowerMapOverlay {
7605
- constructor(bounds, mapData, partitionBoundary, mowerPositionConfig, modelType, pathData, isEditMode = false, mapConfig = {}, antennaConfig = {}, mowPartitionData = null, defaultTransform, onMapLoad, onPathLoad, dragCallbacks) {
10607
+ constructor(bounds, mapData, partitionBoundary, mowerPositionConfig, modelType, pathData, isEditMode = false, unitType = UnitsType.Imperial, language = 'en', mapConfig = {}, antennaConfig = {}, mowPartitionData = null, defaultTransform, onMapLoad, onPathLoad, dragCallbacks) {
7606
10608
  this.div = null;
7607
10609
  this.svgMapView = null;
7608
10610
  this.offscreenContainer = null;
7609
10611
  this.overlayView = null;
7610
10612
  this.defaultTransform = { x: 0, y: 0, rotation: 0 };
10613
+ this.hasEdger = false;
7611
10614
  // boundary数据
7612
10615
  this.boundaryData = [];
7613
10616
  // 边界标签管理器
@@ -7650,6 +10653,8 @@ class MowerMapOverlay {
7650
10653
  this.partitionBoundary = partitionBoundary;
7651
10654
  this.pathData = pathData;
7652
10655
  this.isEditMode = isEditMode;
10656
+ this.unitType = unitType;
10657
+ this.language = language;
7653
10658
  this.mapConfig = mapConfig;
7654
10659
  this.antennaConfig = antennaConfig;
7655
10660
  this.onMapLoad = onMapLoad;
@@ -7696,7 +10701,6 @@ class MowerMapOverlay {
7696
10701
  this.isUserAnimation = animationTime > 0;
7697
10702
  // 更新割草机位置配置
7698
10703
  this.mowerPositionConfig = positionConfig;
7699
- console.log('updatePosition overlay', positionConfig);
7700
10704
  // 更新割草机位置管理器
7701
10705
  if (this.mowerPositionManager) {
7702
10706
  this.mowerPositionManager.updatePosition(positionConfig, animationTime);
@@ -7714,6 +10718,12 @@ class MowerMapOverlay {
7714
10718
  this.overlayView.setMap(map);
7715
10719
  }
7716
10720
  }
10721
+ setEdger(edger) {
10722
+ this.hasEdger = edger;
10723
+ if (this.mowerPositionManager) {
10724
+ this.mowerPositionManager.setEdger(edger);
10725
+ }
10726
+ }
7717
10727
  getMap() {
7718
10728
  return this.overlayView ? this.overlayView.getMap() : null;
7719
10729
  }
@@ -7741,7 +10751,6 @@ class MowerMapOverlay {
7741
10751
  this.svgMapView?.renderLayer(LAYER_DEFAULT_TYPE.BOUNDARY_BORDER);
7742
10752
  }
7743
10753
  onAdd() {
7744
- console.log('onAdd');
7745
10754
  // 创建包含SVG的div
7746
10755
  this.div = document.createElement('div');
7747
10756
  this.div.style.borderStyle = 'none';
@@ -7809,7 +10818,10 @@ class MowerMapOverlay {
7809
10818
  if (!this.div || !this.svgMapView)
7810
10819
  return;
7811
10820
  // 创建边界标签管理器
7812
- this.boundaryLabelsManager = new BoundaryLabelsManager(this.svgMapView, this.boundaryData);
10821
+ this.boundaryLabelsManager = new BoundaryLabelsManager(this.svgMapView, this.boundaryData, {
10822
+ unitType: this.unitType,
10823
+ language: this.language,
10824
+ });
7813
10825
  // 设置叠加层div引用
7814
10826
  this.boundaryLabelsManager.setOverlayDiv(this.div);
7815
10827
  // 添加所有边界标签
@@ -7853,11 +10865,10 @@ class MowerMapOverlay {
7853
10865
  if (!this.div || !this.svgMapView)
7854
10866
  return;
7855
10867
  // 创建割草机位置管理器,传入动画完成回调
7856
- this.mowerPositionManager = new MowerPositionManager(this.svgMapView, this.mowerPositionConfig, this.modelType, this.div, () => {
7857
- console.log('动画完成');
7858
- }, this.updatePathDataByMowingPositionThrottled.bind(this));
10868
+ this.mowerPositionManager = new MowerPositionManager(this.svgMapView, this.mowerPositionConfig, this.modelType, this.div, () => { }, this.updatePathDataByMowingPositionThrottled.bind(this));
7859
10869
  // 设置叠加层div引用
7860
10870
  this.mowerPositionManager.setOverlayDiv(this.div);
10871
+ this.mowerPositionManager.setEdger(this.hasEdger);
7861
10872
  // 获取容器并添加到主div
7862
10873
  const container = this.mowerPositionManager.getElement();
7863
10874
  if (container) {
@@ -7937,7 +10948,7 @@ class MowerMapOverlay {
7937
10948
  this.rotateHandle.style.pointerEvents = 'auto';
7938
10949
  this.rotateHandle.innerHTML = DEFAULT_ROTATE_ICON;
7939
10950
  this.editContainer.appendChild(this.rotateHandle);
7940
- // 创建拖拽手柄(左上角)
10951
+ // 创建拖拽手柄(左下角)- 仅在移动设备上显示
7941
10952
  this.dragHandle = document.createElement('div');
7942
10953
  this.dragHandle.style.position = 'absolute';
7943
10954
  this.dragHandle.style.bottom = '-20px';
@@ -7948,6 +10959,10 @@ class MowerMapOverlay {
7948
10959
  this.dragHandle.style.zIndex = EDIT_STYLES.Z_INDEX.HANDLE;
7949
10960
  this.dragHandle.style.pointerEvents = 'auto';
7950
10961
  this.dragHandle.innerHTML = DEFAULT_DRAG_ICON;
10962
+ // 在PC设备上隐藏拖拽手柄
10963
+ if (!isMobileDevice()) {
10964
+ this.dragHandle.style.display = 'none';
10965
+ }
7951
10966
  this.editContainer.appendChild(this.dragHandle);
7952
10967
  // 将编辑容器添加到主div
7953
10968
  this.div.appendChild(this.editContainer);
@@ -7989,7 +11004,6 @@ class MowerMapOverlay {
7989
11004
  this.boundaryLabelsManager.collapseAllLabels();
7990
11005
  }
7991
11006
  this.dragCallbacks?.onDragStart?.(this.getCurrentDragState());
7992
- console.log('开始旋转操作');
7993
11007
  });
7994
11008
  // 旋转手柄的触摸事件
7995
11009
  this.rotateHandle.addEventListener('touchstart', (e) => {
@@ -8005,39 +11019,41 @@ class MowerMapOverlay {
8005
11019
  this.boundaryLabelsManager.collapseAllLabels();
8006
11020
  }
8007
11021
  this.dragCallbacks?.onDragStart?.(this.getCurrentDragState());
8008
- console.log('开始旋转操作(触摸)');
8009
- }, { passive: false });
8010
- // 拖拽手柄的鼠标事件
8011
- this.dragHandle.addEventListener('mousedown', (e) => {
8012
- e.preventDefault();
8013
- e.stopPropagation();
8014
- e.stopImmediatePropagation();
8015
- this.isDragging = true;
8016
- this.startPos = { x: e.clientX, y: e.clientY };
8017
- this.dragHandle.style.cursor = 'grabbing';
8018
- // 开始编辑时关闭所有展开的边界标签
8019
- if (this.boundaryLabelsManager) {
8020
- this.boundaryLabelsManager.collapseAllLabels();
8021
- }
8022
- this.dragCallbacks?.onDragStart?.(this.getCurrentDragState());
8023
- console.log('开始拖动操作(通过手柄)');
8024
- });
8025
- // 拖拽手柄的触摸事件
8026
- this.dragHandle.addEventListener('touchstart', (e) => {
8027
- e.preventDefault();
8028
- e.stopPropagation();
8029
- e.stopImmediatePropagation();
8030
- this.isDragging = true;
8031
- const touch = e.touches[0];
8032
- this.startPos = { x: touch.clientX, y: touch.clientY };
8033
- this.dragHandle.style.cursor = 'grabbing';
8034
- this.dragCallbacks?.onDragStart?.(this.getCurrentDragState());
8035
- console.log('开始拖动操作(通过手柄,触摸)');
8036
11022
  }, { passive: false });
11023
+ // 拖拽手柄的鼠标事件 - 仅在移动设备上启用
11024
+ if (isMobileDevice()) {
11025
+ this.dragHandle.addEventListener('mousedown', (e) => {
11026
+ e.preventDefault();
11027
+ e.stopPropagation();
11028
+ e.stopImmediatePropagation();
11029
+ this.isDragging = true;
11030
+ this.startPos = { x: e.clientX, y: e.clientY };
11031
+ this.dragHandle.style.cursor = 'grabbing';
11032
+ // 开始编辑时关闭所有展开的边界标签
11033
+ if (this.boundaryLabelsManager) {
11034
+ this.boundaryLabelsManager.collapseAllLabels();
11035
+ }
11036
+ this.dragCallbacks?.onDragStart?.(this.getCurrentDragState());
11037
+ });
11038
+ // 拖拽手柄的触摸事件
11039
+ this.dragHandle.addEventListener('touchstart', (e) => {
11040
+ e.preventDefault();
11041
+ e.stopPropagation();
11042
+ e.stopImmediatePropagation();
11043
+ this.isDragging = true;
11044
+ const touch = e.touches[0];
11045
+ this.startPos = { x: touch.clientX, y: touch.clientY };
11046
+ this.dragHandle.style.cursor = 'grabbing';
11047
+ this.dragCallbacks?.onDragStart?.(this.getCurrentDragState());
11048
+ }, { passive: false });
11049
+ }
8037
11050
  // 编辑容器的鼠标事件(整个区域拖拽)
8038
11051
  this.editContainer.addEventListener('mousedown', (e) => {
8039
- console.log('开始拖动操作(整个叠加层)');
8040
- if (e.target === this.dragHandle || e.target === this.rotateHandle) {
11052
+ // 在移动设备上,检查是否点击了拖拽手柄或旋转手柄
11053
+ // 在PC设备上,只检查旋转手柄(拖拽手柄已隐藏)
11054
+ const isDragHandleClick = isMobileDevice() && e.target === this.dragHandle;
11055
+ const isRotateHandleClick = e.target === this.rotateHandle;
11056
+ if (isDragHandleClick || isRotateHandleClick) {
8041
11057
  return;
8042
11058
  }
8043
11059
  e.preventDefault();
@@ -8054,8 +11070,11 @@ class MowerMapOverlay {
8054
11070
  });
8055
11071
  // 编辑容器的触摸事件(整个区域拖拽)
8056
11072
  this.editContainer.addEventListener('touchstart', (e) => {
8057
- console.log('开始拖动操作(整个叠加层,触摸)');
8058
- if (e.target === this.dragHandle || e.target === this.rotateHandle) {
11073
+ // 在移动设备上,检查是否点击了拖拽手柄或旋转手柄
11074
+ // 在PC设备上,只检查旋转手柄(拖拽手柄已隐藏)
11075
+ const isDragHandleClick = isMobileDevice() && e.target === this.dragHandle;
11076
+ const isRotateHandleClick = e.target === this.rotateHandle;
11077
+ if (isDragHandleClick || isRotateHandleClick) {
8059
11078
  return;
8060
11079
  }
8061
11080
  e.preventDefault();
@@ -8115,7 +11134,6 @@ class MowerMapOverlay {
8115
11134
  e.preventDefault();
8116
11135
  e.stopPropagation();
8117
11136
  e.stopImmediatePropagation();
8118
- console.log('结束编辑操作');
8119
11137
  }
8120
11138
  // 如果是拖拽结束,将像素偏移量转换为地理坐标偏移量
8121
11139
  if (this.isDragging) {
@@ -8137,7 +11155,6 @@ class MowerMapOverlay {
8137
11155
  e.preventDefault();
8138
11156
  e.stopPropagation();
8139
11157
  e.stopImmediatePropagation();
8140
- console.log('结束编辑操作(触摸)');
8141
11158
  }
8142
11159
  // 如果是拖拽结束,将像素偏移量转换为地理坐标偏移量
8143
11160
  if (this.isDragging) {
@@ -8249,7 +11266,6 @@ class MowerMapOverlay {
8249
11266
  this.div.style.transform = transform;
8250
11267
  // 更新鼠标起始位置为当前位置,为下次计算做准备
8251
11268
  this.startPos = { x: mouseCurrentX, y: mouseCurrentY };
8252
- console.log('旋转角度:', this.currentRotation, '角度增量:', angleDifferenceDegrees);
8253
11269
  }
8254
11270
  // 将像素偏移量转换为地理坐标偏移量
8255
11271
  convertPixelOffsetToLatLng() {
@@ -8279,14 +11295,6 @@ class MowerMapOverlay {
8279
11295
  // 累积更新地理坐标偏移量(不是直接赋值!)
8280
11296
  this.latLngOffset.lat += latOffset;
8281
11297
  this.latLngOffset.lng += lngOffset;
8282
- console.log('精确转换偏移量:', {
8283
- pixelOffset: this.tempPixelOffset,
8284
- centerLatLng: { lat: centerLatLng.lat(), lng: centerLatLng.lng() },
8285
- offsetLatLng: { lat: offsetLatLng.lat(), lng: offsetLatLng.lng() },
8286
- latOffset,
8287
- lngOffset,
8288
- newLatLngOffset: this.latLngOffset,
8289
- });
8290
11298
  // 重置临时像素偏移量
8291
11299
  this.tempPixelOffset = { x: 0, y: 0 };
8292
11300
  this.draw();
@@ -8324,8 +11332,6 @@ class MowerMapOverlay {
8324
11332
  editData: editData,
8325
11333
  timestamp: new Date().toISOString(),
8326
11334
  };
8327
- // 在这里可以添加保存逻辑,比如发送到服务器
8328
- console.log('保存编辑数据:', saveData);
8329
11335
  // 显示保存成功提示
8330
11336
  this.showSaveSuccess();
8331
11337
  return saveData;
@@ -8418,6 +11424,9 @@ class MowerMapOverlay {
8418
11424
  y: transform.y,
8419
11425
  rotation: transform.rotation,
8420
11426
  };
11427
+ // defaultTransform的x对应经度偏移量,y对应纬度偏移量
11428
+ this.latLngOffset.lng = this.defaultTransform.x;
11429
+ this.latLngOffset.lat = this.defaultTransform.y;
8421
11430
  this.setManagerRotation(this.currentRotation);
8422
11431
  this.draw();
8423
11432
  }
@@ -8457,7 +11466,6 @@ class MowerMapOverlay {
8457
11466
  if (this.pathData && this.svgMapView) {
8458
11467
  this.loadPathData(this.pathData, this.mowPartitionData);
8459
11468
  }
8460
- console.log('initializeSvgMapView');
8461
11469
  // 刷新绘制图层
8462
11470
  this.svgMapView.refresh();
8463
11471
  // 获取生成的SVG并添加到叠加层div中
@@ -8574,8 +11582,10 @@ class MowerMapOverlay {
8574
11582
  */
8575
11583
  updatePathDataByMowingPosition(position) {
8576
11584
  // 找到当前position所在的分区id,将该点更新到pathData中
8577
- const currentPartitionId = getPartitionId(this.partitionBoundary, position.x, position.y);
8578
- const processStateIsMowing = useProcessMowingState.getState().processStateIsMowing;
11585
+ // 先查找当前的分区id是多少,然后确定当前的点是否在当前分区id中,如果不在,则重新获取分区id并重新设置
11586
+ const currentPartitionId = useCurrentMowingDataStore.getState().currentMowingPartitionId;
11587
+ console.info('updatePathDataByMowingPosition==currentPartitionId=================', currentPartitionId);
11588
+ const processStateIsMowing = useCurrentMowingDataStore.getState().processStateIsMowing;
8579
11589
  if (currentPartitionId && this.pathData?.[currentPartitionId]) {
8580
11590
  const currentPathData = this.pathData[currentPartitionId];
8581
11591
  this.pathData[currentPartitionId] = {
@@ -8613,7 +11623,6 @@ class MowerMapOverlay {
8613
11623
  this.boundaryLabelsManager?.updateBoundaryData(boundaryData);
8614
11624
  }
8615
11625
  draw() {
8616
- console.log('ondraw');
8617
11626
  // 防御性检查:如果this.div为null,说明onAdd还没被调用,直接返回
8618
11627
  if (!this.div) {
8619
11628
  return;
@@ -8881,7 +11890,7 @@ const getValidGpsBounds = (mapData, rotation = 0) => {
8881
11890
  // 默认配置
8882
11891
  const defaultMapConfig = DEFAULT_STYLES;
8883
11892
  // 地图渲染器组件
8884
- const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJson, pathJson, realTimeData, antennaConfig, onMapLoad, onPathLoad, onError, className, style, googleMapInstance, isEditMode = false, dragCallbacks, defaultTransform, debug = false, }, ref) => {
11893
+ const MowerMapRenderer = React.forwardRef(({ edger = false, unitType = UnitsType.Imperial, language = 'en', mapConfig, modelType, mapRef, mapJson, pathJson, realTimeData, antennaConfig, onMapLoad, onPathLoad, onError, className, style, googleMapInstance, isEditMode = false, dragCallbacks, defaultTransform, debug = false, }, ref) => {
8885
11894
  const [elementCount, setElementCount] = React.useState(0);
8886
11895
  const [pathCount, setPathCount] = React.useState(0);
8887
11896
  const [currentError, setCurrentError] = React.useState(null);
@@ -8889,9 +11898,10 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
8889
11898
  // const mapRef = useMap();
8890
11899
  const [isGoogleMapsReady, setIsGoogleMapsReady] = React.useState(false);
8891
11900
  const [hasInitializedBounds, setHasInitializedBounds] = React.useState(false);
8892
- const { clearSubBoundaryBorder, clearObstacles } = useSubBoundaryBorderStore();
11901
+ const { clearSubBoundaryBorder, clearObstacles, clearSvgElements } = usePartitionDataStore();
11902
+ const { resetCurrentMowingPartitionId } = useCurrentMowingDataStore();
8893
11903
  const currentProcessMowingStatusRef = React.useRef(false);
8894
- const { updateProcessStateIsMowing, processStateIsMowing } = useProcessMowingState();
11904
+ const { updateProcessStateIsMowing, processStateIsMowing, updateCurrentMowingPartitionId, currentMowingPartitionId, } = useCurrentMowingDataStore();
8895
11905
  const [mowPartitionData, setMowPartitionData] = React.useState(null);
8896
11906
  // Debug相关状态
8897
11907
  const [debugInfo, setDebugInfo] = React.useState({});
@@ -8923,7 +11933,7 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
8923
11933
  postureX: 0,
8924
11934
  postureY: 0,
8925
11935
  postureTheta: 0,
8926
- vehicleState: RobotStatus.PARKED,
11936
+ vehicleState: RobotStatus.DISCONNECTED,
8927
11937
  };
8928
11938
  let currentPositionData;
8929
11939
  if (realTimeData.length === 1 && realTimeData[0].type === RealTimeDataType.LOCATION) {
@@ -8949,10 +11959,9 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
8949
11959
  lastPostureY: currentPositionData?.lastPostureY
8950
11960
  ? Number(currentPositionData.lastPostureY)
8951
11961
  : 0,
8952
- vehicleState: currentPositionData?.vehicleState || RobotStatus.CHARGING,
11962
+ vehicleState: currentPositionData?.vehicleState || RobotStatus.DISCONNECTED,
8953
11963
  };
8954
11964
  }, [realTimeData, modelType]);
8955
- console.log('mowerPositionData', mowerPositionData);
8956
11965
  // 处理错误
8957
11966
  const handleError = (error) => {
8958
11967
  setCurrentError(error);
@@ -8977,7 +11986,6 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
8977
11986
  const googleBounds = new window.google.maps.LatLngBounds(new window.google.maps.LatLng(swLat, swLng), // 西南角
8978
11987
  new window.google.maps.LatLng(neLat, neLng) // 东北角
8979
11988
  );
8980
- console.log('fitBounds----->', googleBounds);
8981
11989
  mapRef.fitBounds(googleBounds);
8982
11990
  }, [mapJson, mapRef, defaultTransform]);
8983
11991
  // 初始化Google Maps叠加层
@@ -9018,9 +12026,8 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9018
12026
  overlayRef.current.setMap(null);
9019
12027
  overlayRef.current = null;
9020
12028
  }
9021
- console.log('initializeGoogleMapsOverlay', mowPartitionData);
9022
12029
  // 创建叠加层
9023
- const overlay = new MowerMapOverlay(googleBounds, mapJson, partitionBoundary, mowerPositionData, modelType, pathJson || {}, isEditMode, mergedMapConfig, mergedAntennaConfig, null, defaultTransform, (count) => {
12030
+ const overlay = new MowerMapOverlay(googleBounds, mapJson, partitionBoundary, mowerPositionData, modelType, pathJson || {}, isEditMode, unitType, language, mergedMapConfig, mergedAntennaConfig, null, defaultTransform, (count) => {
9024
12031
  setElementCount(count);
9025
12032
  onMapLoad?.(count);
9026
12033
  }, (count) => {
@@ -9030,6 +12037,7 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9030
12037
  // 设置地图
9031
12038
  overlay.setMap(mapInstance);
9032
12039
  overlayRef.current = overlay;
12040
+ overlay.setEdger(edger);
9033
12041
  // 只在首次初始化时自适应视图
9034
12042
  if (!hasInitializedBounds) {
9035
12043
  mapInstance.fitBounds(googleBounds);
@@ -9053,7 +12061,7 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9053
12061
  postureY: chargingPiles?.originalData.position[1],
9054
12062
  postureTheta: chargingPiles?.originalData.direction - Math.PI || 0,
9055
12063
  }, 0);
9056
- }, [mapJson]);
12064
+ }, [mapJson, mowerPositionData]);
9057
12065
  // 初始化效果
9058
12066
  React.useEffect(() => {
9059
12067
  initializeGoogleMapsOverlay();
@@ -9061,6 +12069,8 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9061
12069
  return () => {
9062
12070
  clearSubBoundaryBorder();
9063
12071
  clearObstacles();
12072
+ clearSvgElements();
12073
+ resetCurrentMowingPartitionId();
9064
12074
  updateProcessStateIsMowing(false);
9065
12075
  currentProcessMowingStatusRef.current = false;
9066
12076
  if (overlayRef.current) {
@@ -9092,7 +12102,6 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9092
12102
  const isOffLine = mowerPositionData.vehicleState === RobotStatus.DISCONNECTED;
9093
12103
  const isInChargingPile = inChargingPiles.includes(mowerPositionData.vehicleState);
9094
12104
  // 如果在充电桩上,则直接更新位置到充电桩的位置
9095
- console.log('usefeect mowerPositionData----->', mowerPositionData);
9096
12105
  if (isInChargingPile) {
9097
12106
  overlayRef.current.updatePosition({
9098
12107
  ...mowerPositionData,
@@ -9125,7 +12134,6 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9125
12134
  }
9126
12135
  }
9127
12136
  else {
9128
- console.log('hook updatePosition----->', mowerPositionData);
9129
12137
  overlayRef.current.updatePosition(mowerPositionData, isStandby ? 0 : 2000);
9130
12138
  }
9131
12139
  }
@@ -9244,7 +12252,6 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9244
12252
  if (!realTimeData || realTimeData.length === 0 || !Array.isArray(realTimeData)) {
9245
12253
  return;
9246
12254
  }
9247
- console.log('usefeect realTimeData----->', realTimeData, mapJson, pathJson, overlayRef.current);
9248
12255
  let curMowPartitionData = mowPartitionData;
9249
12256
  // realtime中包含当前割草任务的数据,根据数据进行path路径和边界的高亮操作,
9250
12257
  const mowingPartition = realTimeData.find((item) => item.type === RealTimeDataType.PARTITION);
@@ -9252,9 +12259,8 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9252
12259
  setMowPartitionData(mowingPartition);
9253
12260
  curMowPartitionData = mowingPartition;
9254
12261
  }
9255
- const positionData = realTimeData?.find(item => item?.type === RealTimeDataType.LOCATION);
9256
- const statusData = realTimeData?.find(item => item?.type === RealTimeDataType.STATUS);
9257
- console.log('current->1', positionData, statusData);
12262
+ const positionData = realTimeData?.find((item) => item?.type === RealTimeDataType.LOCATION);
12263
+ const statusData = realTimeData?.find((item) => item?.type === RealTimeDataType.STATUS);
9258
12264
  if (statusData || positionData) {
9259
12265
  const currentStatus = statusData?.vehicleState || positionData?.vehicleState;
9260
12266
  // 车辆回桩不会回传最后的park的位置,所以根据实时数据的状态数据判断车辆回到桩上
@@ -9264,32 +12270,28 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9264
12270
  else if (currentStatus === RobotStatus.WORKING) {
9265
12271
  // 兜底收不到割草地块的实时数据,使用状态来兜底
9266
12272
  overlayRef.current.resetBorderLayerHighlight();
9267
- setMowPartitionData(null);
9268
- curMowPartitionData = null;
12273
+ setMowPartitionData({});
12274
+ curMowPartitionData = {};
9269
12275
  }
9270
- else if (currentStatus === RobotStatus.MOWING && (curMowPartitionData && !curMowPartitionData?.partitionIds)) {
12276
+ else if (currentStatus === RobotStatus.MOWING &&
12277
+ curMowPartitionData &&
12278
+ !curMowPartitionData?.partitionIds) {
9271
12279
  // 如果当前是割草状态,但是地块数据初始化过且不存在则认为是全局割草,则把所有地块都高亮
9272
- const allPartitionIds = mapJson?.sub_maps?.map(item => item?.id);
9273
- console.log('allPartitionIds->', allPartitionIds, mapJson);
12280
+ const allPartitionIds = mapJson?.sub_maps?.map((item) => item?.id);
9274
12281
  setMowPartitionData({
9275
- partitionIds: allPartitionIds
12282
+ partitionIds: allPartitionIds,
9276
12283
  });
9277
12284
  curMowPartitionData = {
9278
- partitionIds: allPartitionIds
12285
+ partitionIds: allPartitionIds,
9279
12286
  };
9280
12287
  }
9281
12288
  }
9282
- if (!mapJson ||
9283
- !pathJson ||
9284
- !overlayRef.current)
12289
+ if (!mapJson || !pathJson || !overlayRef.current)
9285
12290
  return;
9286
12291
  // 根据后端推送的实时数据,进行不同处理
9287
- // TODO:需要根据返回的数据,处理车辆的移动位置
9288
- console.log('realTimeData----->', realTimeData, curMowPartitionData);
9289
12292
  if (curMowPartitionData) {
9290
12293
  const isMowing = curMowPartitionData?.partitionIds && curMowPartitionData.partitionIds.length > 0;
9291
12294
  overlayRef.current.updateMowPartitionData(curMowPartitionData);
9292
- console.log('isMowing', isMowing, curMowPartitionData);
9293
12295
  if (!isMowing) {
9294
12296
  overlayRef.current.resetBorderLayerHighlight();
9295
12297
  }
@@ -9297,28 +12299,36 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9297
12299
  overlayRef.current.setBorderLayerHighlight(curMowPartitionData);
9298
12300
  }
9299
12301
  }
12302
+ // 处理实时路径数据
9300
12303
  // 如果一次性推送的是多条数据,则把多条数据处理后存入pathData,然后更新路径数据和边界标签信息
9301
- // 如果一次只推送一条数据,则只解析里面的进度数据,然后更新边界标签信息,剩下的实时轨迹数据由车辆运动产生时存入pathData
12304
+ // 如果一次只推送一条数据,则只解析里面的进度数据,然后更新边界标签信息,剩下的实时轨迹数据由车辆运动产生时存入pathData(在MowerMapOverlay中updatePathDataByMowingPosition处理)
9302
12305
  if (realTimeData.length > 1) {
9303
- const { pathData, isMowing } = handleMultipleRealTimeData({
12306
+ const { pathData, isMowing, currentMowingPartition } = handleMultipleRealTimeData({
9304
12307
  realTimeData,
9305
12308
  isMowing: processStateIsMowing,
9306
12309
  pathData: pathJson,
9307
- partitionBoundary,
12310
+ currentMowingPartition: currentMowingPartitionId,
9308
12311
  });
9309
12312
  updateProcessStateIsMowing(isMowing);
12313
+ if (currentMowingPartition !== currentMowingPartitionId) {
12314
+ updateCurrentMowingPartitionId(currentMowingPartition);
12315
+ }
9310
12316
  if (pathData) {
9311
12317
  overlayRef.current.updatePathData(pathData, curMowPartitionData);
9312
12318
  overlayRef.current.updateBoundaryLabelInfo(pathData);
9313
12319
  }
9314
12320
  }
9315
12321
  else {
9316
- const { isMowing, pathData } = getProcessMowingDataFromRealTimeData({
12322
+ const { isMowing, pathData, currentMowingPartition } = getProcessMowingDataFromRealTimeData({
9317
12323
  realTimeData,
9318
12324
  isMowing: processStateIsMowing,
9319
12325
  pathData: pathJson,
12326
+ currentMowingPartition: currentMowingPartitionId,
9320
12327
  });
9321
12328
  updateProcessStateIsMowing(isMowing);
12329
+ if (currentMowingPartition !== currentMowingPartitionId) {
12330
+ updateCurrentMowingPartitionId(currentMowingPartition);
12331
+ }
9322
12332
  overlayRef.current.updatePathData(pathData, curMowPartitionData);
9323
12333
  // 更新进度数据
9324
12334
  if (pathData) {
@@ -9327,7 +12337,6 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9327
12337
  }
9328
12338
  }, [realTimeData, mapJson, pathJson]);
9329
12339
  React.useEffect(() => {
9330
- console.log('defaultTransform----->', defaultTransform, overlayRef.current, mapJson);
9331
12340
  if (!overlayRef.current || !defaultTransform)
9332
12341
  return;
9333
12342
  overlayRef.current?.setTransform(defaultTransform);
@@ -9342,6 +12351,11 @@ const MowerMapRenderer = React.forwardRef(({ mapConfig, modelType, mapRef, mapJs
9342
12351
  );
9343
12352
  mapRef.fitBounds(googleBounds);
9344
12353
  }, [defaultTransform]);
12354
+ React.useEffect(() => {
12355
+ if (!overlayRef || !overlayRef.current)
12356
+ return;
12357
+ overlayRef.current.setEdger(edger);
12358
+ }, [edger]);
9345
12359
  // 提供ref方法
9346
12360
  React.useImperativeHandle(ref, () => ({
9347
12361
  fitToView: () => {