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