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