/
detectPaths.ts
195 lines (187 loc) · 7.47 KB
/
detectPaths.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/**
* A function to detect connected paths on the plane given by a random set of segments.
*
* Note that is algorithm operates IN PLACE and will ALTER your INPUT array.
*
* @author Ikaros Kappler
* @version 1.0.0
* @date 2022-10-17
*/
import { Path, PathSegment } from "../../interfaces";
import { GenericPath } from "../datastructures/GenericPath";
/**
* An interface/datascructure for keeping track of visited path segments.
*/
interface Visitation {
visited: boolean;
hasPredecessor: boolean;
hasSuccessor: boolean;
}
/**
* A private helper function that initializes the visitation tracking and reverses
* dangling path ends to connect them with preceding paths.
*
* Note that this function alters those dangling path segments IN PLACE!
*
* @param segments
* @param epsilon
* @returns
*/
const initVisitationArray = (segments: Array<PathSegment>, epsilon: number): Array<Visitation> => {
const result: Array<Visitation> = []; // { visited: false, hasPredecessor: false }
for (var i = 0; i < segments.length; i++) {
result.push({ visited: false, hasPredecessor: false, hasSuccessor: false });
for (var j = 0; j < segments.length; j++) {
if (i === j) {
continue;
}
if (
segments[i].getStartPoint().distance(segments[j].getEndPoint()) < epsilon ||
segments[i].getStartPoint().distance(segments[j].getStartPoint()) < epsilon
) {
result[i].hasPredecessor = true;
}
if (
segments[i].getEndPoint().distance(segments[j].getEndPoint()) < epsilon ||
segments[i].getEndPoint().distance(segments[j].getStartPoint()) < epsilon
) {
result[i].hasSuccessor = true;
}
}
}
// After first initialization make sure that no loose path ends are dangling around
// if the affected path segment has a predecessor (but nu successor).
//
// Revert those segments. This makes the actual detection algorithm much easier.
for (var i = 0; i < segments.length; i++) {
if (!result[i].hasSuccessor && result[i].hasPredecessor) {
segments[i] = segments[i].reverse();
result[i].hasPredecessor = false;
result[i].hasSuccessor = true;
}
}
return result;
};
/**
* Find the next unvisited path segment (private helper function).
*
* There is a special order: starting path segments (those without any predecessor)
* will be preferred. If no more open paths are available (no starting path segments),
* then no more unvisited paths are available or all remaining paths are loops (without
* determined start/end).
*
* Doing this keeps us from the need to run a final loop to connect detected sub paths.
*
* @param {Array<PathSegment>} segments - The path segments to search in.
* @param { Array<Visitation>} isSegmentVisited
* @returns {number} The index of the next unvisited path segment or -1 if no more are available.
*/
const locateUnvisitedSegment = (segments: Array<PathSegment>, isSegmentVisited: Array<Visitation>): number => {
// First run: detect beginnings
for (var i = 0; i < segments.length; i++) {
if (!isSegmentVisited[i].hasPredecessor && !isSegmentVisited[i].visited) {
return i;
}
}
// Second run: if no beginnings exist -> use inner path segment
for (var i = 0; i < segments.length; i++) {
if (!isSegmentVisited[i].visited) {
return i;
}
}
return -1;
};
/**
* Get the next adjacent path segment to the given (current) segment. This is a private
* helper function.
*
* Note that the function will revert the adjacent path segment if required, so the next
* starting point 'equals' the current ending point.
*
* The visitation tracker will be updated if the adjacent segment was found.
*
* @param {Array<PathSegment>} segments - The total set of available path segments (visited and invisited).
* @param {Array<Visitation>} isSegmentVisited - A tracker of visited path segments so far.
* @param {PathSegment} currentSegment - The current path segment to find the adjacent segment for.
* @param {number} epsilon - The epsilon to use to detect 'equal' start/end points. Must be >= 0.
* @returns {PathSegment | null} The next adjacent path segment of null if no such exists.
*/
const getAdjacentSegment = (
segments: Array<PathSegment>,
isSegmentVisited: Array<Visitation>,
currentSegment: PathSegment,
epsilon: number
): PathSegment | null => {
for (var j = 0; j < segments.length; j++) {
if (isSegmentVisited[j].visited) {
continue;
}
var nextSegment = segments[j];
// [start]---[end] [start]---[end]
if (currentSegment.getEndPoint().distance(nextSegment.getStartPoint()) < epsilon) {
isSegmentVisited[j].visited = true;
return nextSegment;
}
// [start]---[end] [end]---[start]
else if (currentSegment.getEndPoint().distance(nextSegment.getEndPoint()) < epsilon) {
isSegmentVisited[j].visited = true;
return nextSegment.reverse();
}
}
return null;
};
/**
* A private helper function to find the adjacent full path for the given path segment,
* considering the current path segment is a starting segment (or one inside a loop).
*
* @param {Array<PathSegment>} segments - The total set of available path segments.
* @param { Array<Visitation>} isSegmentVisited - A tracker to determine which segments have already been visited.
* @param {number} currentSegmentIndex - The index of the current segments to find the containing path for.
* @param {number} epsilon - The epsilon to use to determine 'equal' path points. Must be >= 0.
* @returns {GenericPath} The dected path which consists at least of the current path segment.
*/
const detectAdjacentPath = (
segments: Array<PathSegment>,
isSegmentVisited: Array<Visitation>,
currentSegmentIndex: number,
epsilon: number
): GenericPath => {
var currentSegment: PathSegment | null = segments[currentSegmentIndex];
isSegmentVisited[currentSegmentIndex].visited = true;
const path = new GenericPath(currentSegment); // { segments: [currentSegment], reverse };
var i: number = 0;
// A safety break if something goes wrong
while (i < segments.length && currentSegment) {
currentSegment = getAdjacentSegment(segments, isSegmentVisited, currentSegment, epsilon);
if (currentSegment) {
path.segments.push(currentSegment);
}
i++;
}
return path;
};
/**
* Run a path detection on the given set of path segments.
*
* Note that the array and some path segments may be altered (like reversal) IN PLACE.
*
* @param {Array<PathSegment>} segments - The total set (array) of available path segments.
* @param {number=1.0} epsilon - (optional) An epsilon to use to tell if two plane points should be considered 'equal'.
* @returns {Array<GenericPath>} An array containing all detected path (consisting of adjacent path segments of the original set).
*/
export const detectPaths = (segments: Array<PathSegment>, epsilon?: number): Array<GenericPath> => {
const eps = typeof epsilon === "undefined" || epsilon < 0 ? 1.0 : epsilon;
const isSegmentVisited: Array<Visitation> = initVisitationArray(segments, eps);
const resultPaths: Array<GenericPath> = [];
var nextSegmentIndex: number = -1;
var i = 0;
while ((nextSegmentIndex = locateUnvisitedSegment(segments, isSegmentVisited)) !== -1 && i < segments.length) {
isSegmentVisited[nextSegmentIndex].visited = true;
// A safety break (to avoid infinited loops during development).
i++;
const path = detectAdjacentPath(segments, isSegmentVisited, nextSegmentIndex, eps);
i += path.getSegmentCount() - 1;
resultPaths.push(path);
}
return resultPaths;
};