-
-
Notifications
You must be signed in to change notification settings - Fork 69
Expand file tree
/
Copy pathPolyline.lua
More file actions
877 lines (814 loc) · 34.5 KB
/
Polyline.lua
File metadata and controls
877 lines (814 loc) · 34.5 KB
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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
---@class Polyline
Polyline = CpObject()
---@param vertices table[] array of tables with x, y (Vector, Vertex, State3D or just plain {x, y}
function Polyline:init(vertices)
if vertices then
for i, v in ipairs(vertices) do
self[i] = Vertex(v.x, v.y, i)
end
end
self.logger = Logger('Polyline', Logger.level.debug)
self:calculateProperties()
end
--- Append a single vertex to the end of the polyline.
--- Calling calculateProperties() is the responsibility of the caller
---@param v table|Vertex table with x, y (Vector, Vertex, State3D or just plain {x, y}
--- if v is a Vertex, it will be cloned, otherwise a new vertex created
function Polyline:append(v)
if v.is_a and v:is_a(Vertex) then
table.insert(self, v:clone())
self[#self].ix = #self
else
table.insert(self, Vertex(v.x, v.y, #self + 1))
end
end
--- Append multiple vertices to the end of the polyline.
--- Calling calculateProperties() is the responsibility of the caller
--- if elements of p are Vertex, they will be cloned, otherwise a new vertex created for each
---@param p table|Vertex[]
function Polyline:appendMany(p)
for _, v in ipairs(p) do
self:append(v)
end
end
--- Add v as the first vertex.
--- Calling calculateProperties() is the responsibility of the caller
---@param v table table with x, y (Vector, Vertex, State3D or just plain {x, y}
function Polyline:prepend(v)
if v:is_a(Vertex) then
table.insert(self, 1, v:clone())
else
table.insert(self, 1, Vertex(v.x, v.y, #self + 1))
end
end
function Polyline:_getNewInstance()
return Polyline()
end
function Polyline:clone()
local clone = self:_getNewInstance()
for _, v in ipairs(self) do
clone:append(v:clone())
end
clone:calculateProperties()
return clone
end
function Polyline:getRawIndex(n)
return n
end
--- Returns the vertex at position n. In the derived polygon, will wrap around the ends, that is, will return
--- a valid vertex for -#self < n < 2 * #self.
function Polyline:at(n)
return self[self:getRawIndex(n)]
end
--- Sets the vertex at position n. In the derived polygon, will wrap around the ends, that is, will return
--- a valid vertex for -#self < n < 2 * #self.
function Polyline:set(n, v)
self[self:getRawIndex(n)] = v
end
--- Upper limit when iterating through the vertices, starting with 1 to fwdIterationLimit() (inclusive)
--- using i and i + 1 vertex in the loop. This will not wrap around the end.
function Polyline:fwdIterationLimit()
return #self - 1
end
--- Get the center of the polyline (centroid, average of all vertices)
function Polyline:getCenter()
local center = Vector(0, 0)
for _, v in ipairs(self) do
center = center + v
end
return center / #self
end
--- Get the bounding box
function Polyline:getBoundingBox()
local xMin, xMax, yMin, yMax = math.huge, -math.huge, math.huge, -math.huge
for _, v in ipairs(self) do
xMin = math.min(xMin, v.x)
yMin = math.min(yMin, v.y)
xMax = math.max(xMax, v.x)
yMax = math.max(yMax, v.y)
end
return xMin, yMin, xMax, yMax
end
function Polyline:getUnpackedVertices()
local unpackedVertices = {}
for _, v in ipairs(self) do
table.insert(unpackedVertices, v.x)
table.insert(unpackedVertices, v.y)
end
return unpackedVertices
end
--- vertex iterator
---@return number, Vertex, Vertex, Vertex the index, the vertex at index, the previous, and the next vertex.
--- previous and next may be nil
function Polyline:vertices(from, to)
local i = from and from - 1 or 0
local last = to or #self
return function()
i = i + 1
if i > last then
return nil, nil
else
return i, self[i], self[i - 1], self[i + 1]
end
end
end
--- edge iterator
---@param startIx number|nil start the iteration at the edge starting at the startIx vertex (or at the first)
---@param endIx number|nil the last edge to return is the one starting at endIx (or the last vertex)
---@return number, CourseGenerator.LineSegment, Vertex
function Polyline:edges(startIx, endIx)
local i = startIx and startIx - 1 or 0
local last = endIx or #self
return function()
i = i + 1
if i >= last then
return nil, nil, nil
else
return i, self[i]:getExitEdge() or CourseGenerator.LineSegment.fromVectors(self[i], self[i + 1]), self[i]
end
end
end
--- edge iterator backwards
---@return number, CourseGenerator.LineSegment, Vertex
function Polyline:edgesBackwards(startIx)
local i = startIx and (startIx + 1) or (#self + 1)
return function()
i = i - 1
if i < 2 then
return nil, nil, nil
else
return i, self[i]:getEntryEdge() or CourseGenerator.LineSegment.fromVectors(self[i], self[i - 1]), self[i]
end
end
end
--- Get the length of the shortest edge (distance between vertices)
---@return number
function Polyline:getShortestEdgeLength()
local shortest = math.huge
for _, e in self:edges() do
shortest = math.min(shortest, e:getLength())
end
return shortest
end
function Polyline:reverse()
CourseGenerator.reverseArray(self)
self:calculateProperties()
return self
end
function Polyline:getLength()
-- we cache the full length, and if it exists, return it
if not self.length then
-- otherwise calculate length
local length = 0
for _, e in self:edges() do
length = length + e:getLength()
end
self.length = length
end
return self.length
end
function Polyline:getLengthBetween(startIx, endIx)
local length = 0
for _, e in self:edges(startIx, endIx) do
length = length + e:getLength()
end
return length
end
---@return number index of the first vertex which is at least d distance from ix
---(can be nil if the end of line reached before d)
function Polyline:moveForward(ix, d)
local i, dElapsed = ix, 0
while i < #self do
if dElapsed >= d then
return i
end
dElapsed = dElapsed + self:at(i):getExitEdge():getLength()
i = i + 1
end
return nil
end
---@param length number the polyline is extended forward (last vertex moved)
function Polyline:extendEnd(length)
local newEntryEdge = self[#self]:getEntryEdge()
newEntryEdge:extend(length)
self[#self] = Vertex.fromVector(newEntryEdge:getEnd())
self:calculateProperties(#self - 1)
return self
end
---@param length number the polyline is extended backwards (first vertex moved).
function Polyline:extendStart(length)
local newExitEdge = self[1]:getExitEdge()
newExitEdge:extend(-length)
self[1] = Vertex.fromVector(newExitEdge:getBase())
self:calculateProperties(1, 2)
return self
end
---@param length number shorten the Polyline at the last vertex
function Polyline:cutEnd(length)
if #self == 2 then
local theOnlyEdge = self[1]:getExitEdge()
theOnlyEdge:setLength(theOnlyEdge:getLength() - length)
self[2] = Vertex.fromVector(theOnlyEdge:getEnd())
self:calculateProperties(1, 2)
else
local d = length
while d > 0 and #self > 2 do
d = d - self[#self]:getEntryEdge():getLength()
self[#self] = nil
end
-- this still exists, pointing to the last vertex we just removed
local newExitEdge = self[#self]:getExitEdge()
newExitEdge:setLength(-d)
self[#self] = Vertex.fromVector(newExitEdge:getEnd())
self:calculateProperties(#self - 1)
end
end
---
---@param length number shorten the polyline at the first vertex
function Polyline:cutStart(length)
if #self == 2 then
local theOnlyEdge = self[1]:getExitEdge()
theOnlyEdge:setLength(length)
self[1] = Vertex.fromVector(theOnlyEdge:getEnd())
else
local d, lastEdge = length, self[1]:getExitEdge()
while d > 0 and #self > 2 do
lastEdge = self[1]:getExitEdge()
d = d - lastEdge:getLength()
table.remove(self, 1)
end
lastEdge:setLength(lastEdge:getLength() - (-d))
self[1] = Vertex.fromVector(lastEdge:getEnd())
end
self:calculateProperties(1, 2)
end
--- Cut all vertices from the first vertex up to but not including ix, shortening the polyline at the start
---@param ix number
function Polyline:cutStartAtIx(ix)
if ix >= #self then
return
end
ix = math.min(ix - 1, #self - 2)
for _ = 1, ix do
table.remove(self, 1)
end
self:calculateProperties(1, 2)
end
--- Cut all vertices from ix (not including) to the last vertex, shortening the polyline at the end
---@param ix number
function Polyline:cutEndAtIx(ix)
for _ = #self, ix + 1, -1 do
table.remove(self)
end
self:calculateProperties(ix - 1, ix)
end
--- Cut this polyline where it first intersects with other and keep the longer part
---@param other Polyline
function Polyline:trimAtFirstIntersection(other)
local intersections = self:getIntersections(other)
if #intersections == 0 then
return
end
-- where is the longer part?
local lengthFromIntersectionToEnd = self:getLengthBetween(intersections[1].ixA)
if lengthFromIntersectionToEnd < self:getLength() / 2 then
-- shorter part towards the end
self:cutEndAtIx(intersections[1].ixA)
else
-- shorter part towards the start
self:cutStartAtIx(intersections[1].ixA + 1)
end
end
--- Calculate all interesting properties we may need later for more advanced functions
---@param from number index of vertex to start the calculation, default 1
---@param to number index of last vertex to use in the calculation, default #self
function Polyline:calculateProperties(from, to)
for i, current, previous, next in self:vertices(from, to) do
current.ix = i
current:calculateProperties(previous, next)
end
-- mark dirty
self.length = nil
end
--- If there is a sudden direction change almost 180 degrees at a vertex, remove that vertex.
function Polyline:removeGlitches()
local i = 1
while i < #self do
local dA = self:at(i).dA
if dA and math.abs(dA) > math.pi - 0.2 then
table.remove(self, i)
else
i = i + 1
end
end
self:calculateProperties()
end
function Polyline:_canRemoveVertex(i, maxDeltaAngle)
if not maxDeltaAngle then
return true
else
-- only remove vertices which aren't around a corner
return math.abs(self:at(i + 1).dA) < maxDeltaAngle and math.abs(self:at(i).dA) < maxDeltaAngle
end
end
--- If two vertices are closer than minimumLength, replace them with one between.
---@param minimumLength number After this operation, no two vertices will be closer than minimumLength
---@param maxDeltaAngle number|nil when specified, vertices where the delta angle is bigger than this are not removed,
--- thus, corners are preserved
function Polyline:ensureMinimumEdgeLength(minimumLength, maxDeltaAngle)
local i = 1
while i < #self do
if (self:at(i + 1) - self:at(i)):length() < minimumLength and self:_canRemoveVertex(i, maxDeltaAngle) then
table.remove(self, i + 1)
else
i = i + 1
end
end
self:calculateProperties()
end
--- If two vertices are further than maximumLength apart, add a vertex between them. If the
--- delta angle at the first vertex is less than maxDeltaAngleForOffset, also offset the new vertex
--- to the left/right from the edge in an effort trying to follow a curve.
--- Use this to fix a polyline with many vertices where some edges may be slightly longer than the
--- maximum. Use splitEdges() instead if you have just a few (as little as 2) vertices and
--- need a vertex at every given distance
---@param maximumLength number|nil default CourseGenerator.cMaxEdgeLength,
---@param maxDeltaAngleForOffset number|nil default CourseGenerator.cMaxDeltaAngleForMaxEdgeLength
function Polyline:ensureMaximumEdgeLength(maximumLength, maxDeltaAngleForOffset)
maximumLength = maximumLength or CourseGenerator.cMaxEdgeLength
maxDeltaAngleForOffset = maxDeltaAngleForOffset or CourseGenerator.cMaxDeltaAngleForMaxEdgeLength
local i = 1
while i <= self:fwdIterationLimit() do
local exitEdge = CourseGenerator.LineSegment.fromVectors(self:at(i), self:at(i + 1))
if exitEdge:getLength() > maximumLength then
if math.abs(self:at(i).dA) < maxDeltaAngleForOffset then
-- for higher angles, like corners, we don't want to round them out here.
exitEdge:setHeading(exitEdge:getHeading() - self:at(i).dA / 2)
end
exitEdge:setLength(exitEdge:getLength() / 2)
local v = exitEdge:getEnd()
table.insert(self, i + 1, Vertex(v.x, v.y, i + 1))
self:calculateProperties(i, i + 2)
self.logger:trace('ensureMaximumEdgeLength: added a vertex after %d', i)
i = i + 2
else
i = i + 1
end
end
end
--- Use splitEdges() if you have just a few (as little as 2) vertices and
--- need a vertex at every given distance
---@param maximumLength number if an edge is longer than maximumLength, split it into multiple
--- edges so none of the resulting edges will be longer than maximumLength
function Polyline:splitEdges(maximumLength)
local i = 1
while i <= self:fwdIterationLimit() do
local exitEdge = CourseGenerator.LineSegment.fromVectors(self:at(i), self:at(i + 1))
local totalLength = exitEdge:getLength()
if totalLength > maximumLength then
local length = maximumLength
while length < totalLength do
exitEdge:setLength(length)
table.insert(self, i + 1, Vertex.fromVector(exitEdge:getEnd(), i + 1))
length = length + math.min(totalLength - length, maximumLength)
i = i + 1
end
end
i = i + 1
end
self:calculateProperties()
end
---@param offsetVector Vector offset to move the edges, relative to the edge's direction
---@return CourseGenerator.LineSegment[] an array of edges parallel to the existing ones, same length
--- but offset by offsetVector
function Polyline:generateOffsetEdges(offsetVector)
local offsetEdges = {}
for _, e in self:edges() do
local newOffsetEdge = e:clone()
newOffsetEdge:offset(offsetVector.x, offsetVector.y)
table.insert(offsetEdges, newOffsetEdge)
end
return offsetEdges
end
function Polyline:_cleanEdges(edges, startIx, cleanEdges, previousEdge, minEdgeLength, preserveCorners)
for i = startIx, #edges do
local currentEdge = edges[i]
local gapFiller = CourseGenerator.LineSegment.connect(previousEdge, currentEdge, minEdgeLength, preserveCorners)
if gapFiller then
table.insert(cleanEdges, gapFiller)
end
table.insert(cleanEdges, currentEdge)
previousEdge = currentEdge
end
return cleanEdges
end
--- Make sure the edges are properly connected, their ends touch nicely without gaps and never
--- extend beyond the vertex
---@param edges CourseGenerator.LineSegment[]
function Polyline:cleanEdges(edges, minEdgeLength, preserveCorners)
return self:_cleanEdges(edges, 2, { edges[1] }, edges[1], minEdgeLength, preserveCorners)
end
--- Generate a polyline parallel to this one, offset by the offsetVector. Note that this works only
--- with either very low offsets or simple polylines with vertices far apart when using bigger offsets.
--- Use CourseGenerator.Offset.generate() if you want to avoid creating loops on the offset polyline.
---@param offsetVector Vector offset to move the edges, relative to the edge's direction
---@param minEdgeLength number see LineSegment.connect()
---@param preserveCorners number see LineSegment.connect()
function Polyline:createOffset(offsetVector, minEdgeLength, preserveCorners)
local offsetPolyline = Polyline()
return self:_createOffset(offsetPolyline, offsetVector, minEdgeLength, preserveCorners)
end
--- Ensure there are no sudden direction changes in the polyline, that is, at each vertex a vehicle
--- with turning radius r would be able to follow the line with less than cMaxCrossTrackError distance
--- from the corner vertex.
--- When such a corner is found, either make it rounder according to r, or make it sharp and mark it
--- as a turn waypoint.
---@param r number turning radius
---@param makeCorners boolean if true, make corners for turn maneuvers instead of rounding them.
---@param maxCrossTrackError number|nil maximum cross track error allowed before we start adjusting corners,
---defaults to CourseGenerator.cMaxCrossTrackError
function Polyline:ensureMinimumRadius(r, makeCorners, maxCrossTrackError)
---@param entry CourseGenerator.Slider
---@param exit CourseGenerator.Slider
local function makeArc(entry, exit)
local from = entry:getBaseAsState3D()
local to = exit:getBaseAsState3D()
return CourseGenerator.AnalyticHelper.getDubinsSolutionAsVertices(from, to, r)
end
---@param entry CourseGenerator.Slider
---@param exit CourseGenerator.Slider
local function makeCorner(entry, exit)
entry:extendTo(exit)
local corner = Vertex.fromVector(entry:getEnd())
corner.isCorner = true
return { corner }
end
local wrappedAround = false
local currentIx
local nextIx = 1
repeat
local debugId = CourseGenerator.getDebugId()
currentIx = nextIx
nextIx = currentIx + 1
local xte = self:at(currentIx):getXte(r)
local radius = self:at(currentIx):getRadius()
if xte > (maxCrossTrackError or CourseGenerator.cMaxCrossTrackError) then
self.logger:debug('ensureMinimumRadius (%s): found a corner at %d with r: %.1f, r: %.1f, xte: %.1f',
debugId, currentIx, radius, r, xte)
-- looks like we can't make this turn without deviating too much from the course,
local entry = CourseGenerator.Slider(self, currentIx, 0)
local exit = CourseGenerator.Slider(self, currentIx, 0)
local rMin
-- we can move back a lot when rounding corners, but otherwise, limit that as we may end up
-- being outside of the field with the corner...
local step, totalMoved, maxDistanceToMove = 0.2, 0, r * (makeCorners and 2 or 10)
repeat
-- from the corner, start widening the gap until we can fit an
-- arc with r between
entry:move(-step)
exit:move(step)
totalMoved = totalMoved + step
rMin = entry:getRadiusTo(exit)
-- use a limit a bit over r, to make sure the Dubins generated won't end
-- up as a loop because it can't fit between entry and exit
until rMin >= 1.01 * r or totalMoved > maxDistanceToMove
-- entry and exit are now far enough, so use the Dubins solver to effortlessly create a nice
-- arc between the two, or, to make it a sharp corner, find the intersection of entry and exit
local adjustedCornerVertices
if makeCorners then
if totalMoved < maxDistanceToMove then
adjustedCornerVertices = makeCorner(entry, exit)
else
-- there are cases, for instance in narrow nooks which already have turns, where
-- it does not make sense trying to sharpen as we'll end up a very small angle with a
-- corner point very far away.
self[currentIx].isCorner = true
self.logger:warning(
'ensureMinimumRadius (%s): will not sharpen this corner, had to move too far (%.1f) back',
debugId, totalMoved)
CourseGenerator.addDebugPoint(entry:getBase(), debugId .. ' entry')
CourseGenerator.addDebugPoint(exit:getBase(), debugId .. ' exit')
CourseGenerator.addDebugPoint(self[currentIx], debugId .. ' center')
end
else
adjustedCornerVertices = makeArc(entry, exit)
end
if adjustedCornerVertices and #adjustedCornerVertices >= 1 then
-- remember the size before the replacement
local sizeBeforeReplace = #self
-- replace the section with an arc or a corner
nextIx, wrappedAround = self:replace(entry.ix, exit.ix + 1, adjustedCornerVertices)
self.logger:debug('ensureMinimumRadius (%s): replaced corner vertices between %d to %d with %d waypoint(s), continue at %d (of %d), wrapped around %s',
debugId, entry.ix, exit.ix, #adjustedCornerVertices, nextIx, #self, wrappedAround)
if #self < sizeBeforeReplace then
self:calculateProperties(entry.ix - (sizeBeforeReplace - #self), nextIx)
else
self:calculateProperties(entry.ix, nextIx)
end
else
self.logger:debug('ensureMinimumRadius (%s): could not calculate adjusted corner vertices', debugId)
end
end
until wrappedAround or currentIx >= #self
self:ensureMinimumEdgeLength(CourseGenerator.cMinEdgeLength)
if makeCorners then
self:ensureMaximumEdgeLength(CourseGenerator.cMaxEdgeLength)
end
self:calculateProperties()
end
--- Find the first two intersections with another polyline or polygon and replace the section
--- between those points with the vertices of the other polyline or polygon.
---@param other Polyline
---@param startIx number index of the vertex we want to start looking for intersections.
---@param circle boolean when true, make a full circle on the other polygon, else just go around and continue
---@return boolean, number true if there were two intersections and we actually went around other (circle or not),
--- then the second return value is the index of last vertex, this is the startIx of the next call to goAround() should
--- be to continue looking for more intersections with other.
--- If false, and there was one intersection (meaning either the start or end of this polyline is within other, then return
--- the index on the polyline where it intersects other.
--- after the bypass
function Polyline:goAround(other, startIx, circle)
local intersections = self:getIntersections(other, startIx)
local is1, is2 = intersections[1], intersections[2]
if is1 and is2 then
-- we cross other completely, none of our ends are within other, there may be more intersections with other though
return self:goAroundBetweenIntersections(other, circle, is1, is2)
else
-- there is one intersection only, one of our ends is within other, and there are no more intersections with other
return false
end
end
function Polyline:goAroundBetweenIntersections(other, circle, is1, is2)
local pathA, pathB = other:_getPathBetweenIntersections(is1.ixB, is2.ixB)
local path
if pathA and pathB then
local shortPath = pathA:getLength() < pathB:getLength() and pathA or pathB
local longPath = pathA:getLength() >= pathB:getLength() and pathA or pathB
self.logger:debug('path A: %.1f, path B: %.1f', pathA:getLength(), pathB:getLength())
shortPath:setAttribute(nil, CourseGenerator.WaypointAttributes.setHeadlandPassNumber,
self:at(is1.ixA):getAttributes():getHeadlandPassNumber())
longPath:setAttribute(nil, CourseGenerator.WaypointAttributes.setHeadlandPassNumber,
self:at(is1.ixA):getAttributes():getHeadlandPassNumber())
if circle then
path = shortPath:clone()
path:setAttribute(nil, CourseGenerator.WaypointAttributes.setIslandBypass)
longPath:reverse()
path:appendMany(longPath)
-- mark this roundtrip as island bypass
path:setAttributes(#path - #longPath, #path, CourseGenerator.WaypointAttributes.setIslandBypass)
path:appendMany(shortPath)
self.logger:debug('Circled around, %d waypoints', #path)
else
path = shortPath
self.logger:debug('Took the shorter path, no circle')
end
else
path = pathA
end
table.insert(path, 1, Vertex.fromVector(is1.is))
table.insert(path, Vertex.fromVector(is2.is))
if path then
local lastIx = self:replace(is1.ixA, is2.ixA + 1, path)
-- make the transitions a little smoother
self:calculateProperties()
-- size may change after smoothing
local oldSize = #self
lastIx = CourseGenerator.SplineHelper.smooth(self, 1, is1.ixA, lastIx)
self:calculateProperties()
return true, lastIx
else
self.logger:warning('No path around other polygon found')
return false
end
end
---@param lineSegment CourseGenerator.LineSegment
---@return Vertex, number, Vertex, number the vertex closest to lineSegment, its distance, the vertex
--- farthest from the lineSegment, its distance
function Polyline:findClosestAndFarthestVertexToLineSegment(lineSegment)
local dMin, closestVertex = math.huge, nil
local dMax, farthestVertex = -math.huge, nil
for _, v in self:vertices() do
local thisD = lineSegment:getDistanceFrom(v)
if thisD < dMin then
dMin = thisD
closestVertex = v
end
if thisD > dMax then
dMax = thisD
farthestVertex = v
end
end
return closestVertex, dMin, farthestVertex, dMax
end
---@param point Vector
---@param isValidFunc function optional function accepting a Vertex and returning bool to determine if this
--- vertex should be considered at all
---@return Vertex
---@return number distance of the closest vertex from point
---@return number|nil distance of the point from exit (or if it does not exist, the entry) edge of the closest vertex
function Polyline:findClosestVertexToPoint(point, isValidFunc)
local d, closestVertex = math.huge, nil
for _, v in self:vertices() do
if not isValidFunc or isValidFunc(v) then
local dFromV = (point - v):length()
if dFromV < d then
d = dFromV
closestVertex = v
end
end
end
local distanceFromEdge
if closestVertex then
if closestVertex:getExitEdge() then
distanceFromEdge = closestVertex:getExitEdge():getDistanceFrom(point)
elseif closestVertex:getEntryEdge() then
distanceFromEdge = closestVertex:getEntryEdge():getDistanceFrom(point)
end
return closestVertex, d, distanceFromEdge
end
end
--- Does this line intersects the other?
---
--- This is a faster version of getIntersections() for the case where we only want to
--- know if they intersect or not.
---
---@param other Polyline
---@return boolean
function Polyline:intersects(other)
for _, edge, _ in self:edges() do
for _, otherEdge in other:edges() do
local is = edge:intersects(otherEdge)
if is then
return true
end
end
end
return false
end
------------------------------------------------------------------------------------------------------------------------
--- Private functions
------------------------------------------------------------------------------------------------------------------------
--- Get all intersections with other, in the order we would meet them traversing self in the increasing index direction
---@param other Polyline
---@param startIx number index to start looking for intersections with other
---@param userData any user data to add to the Intersection objects (to later identify them)
---@return CourseGenerator.Intersection[] list of intersections
function Polyline:getIntersections(other, startIx, userData)
local intersections = {}
for i, edge, _ in self:edges(startIx) do
for j, otherEdge in other:edges() do
local is = edge:intersects(otherEdge)
if is then
-- do not add an intersection twice if it goes exactly through a vertex
if #intersections == 0 or (intersections[#intersections].is ~= is) then
table.insert(intersections, CourseGenerator.Intersection(i, j, is, edge, otherEdge, userData))
end
end
end
end
table.sort(intersections)
return intersections
end
--- Is this polyline entering the other polygon at intersection point is?
---@param other Polygon
---@param is CourseGenerator.Intersection
---@param clockwiseOverride boolean use this clockwise setting instead of other's isClockwise() function
---@return boolean true if self is entering the other polygon, false when exiting, when moving in
--- the direction of increasing indexes
function Polyline:isEntering(other, is, clockwiseOverride)
local otherEdge = other:at(is.ixB):getExitEdge()
return self:at(is.ixA):getExitEdge():isEntering(clockwiseOverride or other:isClockwise(), otherEdge)
end
--- Replace all vertices between fromIx and toIx (excluding) with the entries in vertices
---@param fromIx number index of last vertex to keep
---@param toIx number index of first vertex to keep, toIx must be >= fromIx, unless wrapping around on a Polygon
---@param vertices Vector[] new vertices to put between fromIx and toIx
---@return number, boolean index of the next vertex after the replaced ones (may be more or less than toIx depending on
--- how many entries vertices had and how many elements were there originally between fromIx and toIx. The boolean
--- is true when wrapped around the end (for a polygon)
function Polyline:replace(fromIx, toIx, vertices)
-- mark the ones we need to replace/remove. this is to make the rollover case (for polygons) easier
for i = fromIx + 1, toIx - 1 do
self[self:getRawIndex(i)].toBeReplaced = true
end
local sourceIx = 1
local destIx = CourseGenerator.WrapAroundIndex(self, fromIx + 1)
while self[destIx:get()] and self[destIx:get()].toBeReplaced do
if sourceIx <= #vertices then
local newVertex = vertices[sourceIx]:clone()
newVertex.color = { 0, 1, 0 } -- for debug only
self[destIx:get()] = newVertex
self.logger:trace('Replaced %d with %s', destIx:get(), newVertex)
destIx = destIx + 1
else
table.remove(self, destIx:get())
self.logger:trace('Removed %d', destIx:get())
-- just in case we happened to remove the last element of the table, reset destIx to the max index
destIx:set(destIx:get())
end
sourceIx = sourceIx + 1
end
while sourceIx <= #vertices do
-- we have some vertices left, but there is no room for them
local newVertex = vertices[sourceIx]:clone()
newVertex.color = { 1, 0, 0 } -- for debug only
table.insert(self, destIx:get(), newVertex)
self.logger:trace('Adding %s at %d', newVertex, destIx:get())
sourceIx = sourceIx + 1
destIx = destIx + 1
end
return destIx:get(), destIx:get() < fromIx
end
--- Get a reference to a contiguous segment of vertices of a polyline. Note that
--- these are references of the original vertices, not copies!
---@param fromIx number index of first vertex in the segment, not including? or is it?
---@param toIx number index of last vertex in the segment, not including? or is it?
---@return Polyline
function Polyline:_getPathBetween(fromIx, toIx)
local segment = Polyline()
local step = fromIx <= toIx and 1 or -1
for i = fromIx, toIx, step do
table.insert(segment, self:at(i))
end
return segment
end
--- If fromIx and toIx are the vertex indices where edges intersecting another polyline _start_,
--- get the vertices between the two intersection points.
---@param fromIx number
---@param toIx number
function Polyline:_getPathBetweenIntersections(fromIx, toIx)
local from, to
if fromIx <= toIx then
-- toIx is the start of an edge, add the end of that edge too
from, to = fromIx + 1, toIx
else
-- fromIx is the start of an edge, add the end of that edge too
from, to = fromIx, toIx + 1
end
return self:_getPathBetween(from, to)
end
--- Set an attribute for a series of vertices
---@param first number | nil index of first vertex to set the attribute for
---@param last number | nil index of last vertex
---@param setter CourseGenerator.WaypointAttributes function to call on each vertex' attributes
---@param ... any arguments for setter
function Polyline:setAttributes(first, last, setter, ...)
first = first or 1
last = last or #self
for i = first, last do
setter(self:at(i):getAttributes(), ...)
end
end
--- Set an attribute for a single vertex (or all)
---@param ix number|nil index of vertex to set the attribute for, if nil, the attribute is set for all vertices
---@param setter CourseGenerator.WaypointAttributes function to call on each vertex' attributes
---@param ... any arguments for setter
function Polyline:setAttribute(ix, setter, ...)
self:setAttributes(ix, ix, setter, ...)
end
--- Remove all existing vertices
---@param ix number optional start index
function Polyline:_reset(ix)
for i = ix or 1, #self do
self[i] = nil
end
self.deltaAngle, self.area, self.length = nil, nil, nil
end
--- Private function to use derived classes, in order to instantiate a result which is an instance of
--- the derived class.
---@param result Polyline
function Polyline:_createOffset(result, offsetVector, minEdgeLength, preserveCorners)
local offsetEdges = self:generateOffsetEdges(offsetVector)
local cleanOffsetEdges = self:cleanEdges(offsetEdges, minEdgeLength, preserveCorners)
for _, e in ipairs(cleanOffsetEdges) do
result:append(e:getBase())
end
result:append(cleanOffsetEdges[#cleanOffsetEdges]:getEnd())
result:calculateProperties()
return result
end
------ Cut a polyline at is1 and is2, keeping the section between the two. is1 and is2 becomes the start and
--- end of the cut polyline.
---@param is1 CourseGenerator.Intersection
---@param is2 CourseGenerator.Intersection
---@param section Polyline|nil an optional, initialized object that will become the section
---@return Polyline
function Polyline:_cutAtIntersections(is1, is2, section)
section = section or Polyline()
section:append(is1.is)
local src = is1.ixA + 1
while src < is2.ixA do
section:append(self[src])
src = src + 1
end
section:append(is2.is)
section:calculateProperties()
return section
end
function Polyline:__tostring()
local result = ''
for i, v in ipairs(self) do
result = result .. string.format('%d %s %s\n', i, v, v.xte)
end
return result
end