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