Skip to content

Commit

Permalink
Merge pull request #3526 from Courseplay/3524-bug_sp-headland-overlap…
Browse files Browse the repository at this point in the history
…-moves-implement-outside-of-field

fix: first headland width w/o overlap
  • Loading branch information
Tensuko authored Oct 13, 2024
2 parents 25134e3 + 1c87221 commit b447f74
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 32 deletions.
14 changes: 8 additions & 6 deletions scripts/courseGenerator/Center.lua
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,9 @@ function Center:_calculateRowDistribution(fieldWidth, overlapLast)
local centerWorkingWidth = self.context:getCenterRowSpacing()
-- only use the overlap-corrected headland width if we have headlands, otherwise, must use the
-- nominal working width to avoid generating rows extending outside of the field
local headlandWorkingWidth = self.mayOverlapHeadland and self.context:getHeadlandWorkingWidth() or
self.context.workingWidth
local headlandWorkingWidth = self.mayOverlapHeadland and
self.context:getHeadlandWorkingWidth() * (1 - self.context:getHeadlandOverlap()) or
self.context:getHeadlandWorkingWidth()
-- making the field width 1 cm less to avoid generating the last row exactly on the headland if
-- the field width is an exact multiple of the working width
local nRows = math.floor((fieldWidth - headlandWorkingWidth - 0.01) / centerWorkingWidth) + 1
Expand All @@ -353,14 +354,15 @@ function Center:_calculateRowDistribution(fieldWidth, overlapLast)
return { fieldWidth - centerWorkingWidth / 2 }
end
else
if self.context.evenRowDistribution then
-- #1
centerWorkingWidth = (fieldWidth - headlandWorkingWidth) / nRows
end
local firstRowOffset
local rowOffsets = {}
-- the first/last row's offset from the surrounding headland centerline
local outermostRowOffset = headlandWorkingWidth / 2 + centerWorkingWidth / 2
if self.context.evenRowDistribution then
-- #1, calculate this after the outermost row offset, so that one uses the real working
-- width for the first and last row to not go outside of the field
centerWorkingWidth = (fieldWidth - headlandWorkingWidth) / nRows
end
if self.mayOverlapHeadland then
-- #3 we have headlands
if overlapLast then
Expand Down
15 changes: 10 additions & 5 deletions scripts/courseGenerator/FieldworkContext.lua
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ function FieldworkContext:log()
self.fieldCornerRadius, self.sharpenCorners, self.bypassIslands, self.nIslandHeadlands, self.islandHeadlandClockwise)
self.logger:debug('row pattern: %s, row angle auto: %s, %.1fº, even row distribution: %s, use baseline edge: %s, small overlaps: %s',
self.rowPattern, self.autoRowAngle, math.deg(self.rowAngle), self.evenRowDistribution, self.useBaselineEdge, self.enableSmallOverlapsWithHeadland)
self.logger:debug('start location %s, baseline edge %s, vehicles %d, same turn width %s',
self.startLocation, self.baselineEdge, self.nVehicles, self.useSameTurnWidth)
self.logger:debug('start location %s, baseline edge %s, vehicles %d, same turn width %s, headland overlap %.1f',
self.startLocation, self.baselineEdge, self.nVehicles, self.useSameTurnWidth, self.overlap * 100)
end

function FieldworkContext:addError(logger, ...)
Expand Down Expand Up @@ -225,15 +225,20 @@ function FieldworkContext:setFieldMargin(margin)
return self
end

--- Override the working width for headland passes
--- Override the working width for headland passes (if not the same as the working width)
---@param w number
function FieldworkContext:setHeadlandWorkingWidth(w)
self.headlandWorkingWidth = w
end

---@return number width of a headland pass in meters. Default is the working width less the overlap.
---@return number width of a headland pass in meters
function FieldworkContext:getHeadlandWorkingWidth()
return self.headlandWorkingWidth or self.workingWidth * (1 - self.overlap)
return self.headlandWorkingWidth or self.workingWidth
end

---@return number headland overlap
function FieldworkContext:getHeadlandOverlap()
return self.overlap
end

--- Disable sequencing of blocks, just generate them, with the rows and then stop.
Expand Down
48 changes: 35 additions & 13 deletions scripts/courseGenerator/FieldworkCourse.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function FieldworkCourse:init(context)
-- connect the headlands first as the center needs to start where the headlands finish
self.logger:debug('### Connecting headlands (%d) from the outside towards the inside ###', #self.headlands)
self.headlandPath = CourseGenerator.HeadlandConnector.connectHeadlandsFromOutside(self.headlands,
context.startLocation, self.context:getHeadlandWorkingWidth(), self.context.turningRadius)
context.startLocation, self:_getHeadlandWorkingWidth(), self.context.turningRadius)
self:routeHeadlandsAroundSmallIslands(self.headlandPath)
self.logger:debug('### Generating up/down rows ###')
self:generateCenter()
Expand All @@ -39,7 +39,7 @@ function FieldworkCourse:init(context)
local endOfLastRow = self:generateCenter()
self.logger:debug('### Connecting headlands (%d) from the inside towards the outside ###', #self.headlands)
self.headlandPath = CourseGenerator.HeadlandConnector.connectHeadlandsFromInside(self.headlands,
endOfLastRow, self.context:getHeadlandWorkingWidth(), self.context.turningRadius)
endOfLastRow, self:_getHeadlandWorkingWidth(), self.context.turningRadius)
self:routeHeadlandsAroundSmallIslands(self.headlandPath)
end

Expand Down Expand Up @@ -132,17 +132,17 @@ end
--- Generate the headlands based on the current context
function FieldworkCourse:generateHeadlands()
self.headlands = {}
self.logger:debug('generating %d headlands with round corners, then %d with sharp corners',
self.logger:debug('generating %d headland(s) with round corners, then %d with sharp corners',
self.nHeadlandsWithRoundCorners, self.nHeadlands - self.nHeadlandsWithRoundCorners)
if self.nHeadlandsWithRoundCorners > 0 then
self:generateHeadlandsFromInside()
if self.nHeadlands > self.nHeadlandsWithRoundCorners and #self.headlands < self.nHeadlands then
self:generateHeadlandsFromOutside(self.boundary,
(self.nHeadlandsWithRoundCorners + 0.5) * self.context:getHeadlandWorkingWidth(),
self:_getHeadlandOffset(self.nHeadlandsWithRoundCorners + 1),
#self.headlands + 1)
end
elseif self.nHeadlands > 0 then
self:generateHeadlandsFromOutside(self.boundary, self.context:getHeadlandWorkingWidth() / 2, 1)
self:generateHeadlandsFromOutside(self.boundary, self:_getHeadlandOffset(1), 1)
end
end

Expand All @@ -153,8 +153,8 @@ end
---@param startIx number index of the first headland to generate
function FieldworkCourse:generateHeadlandsFromOutside(boundary, firstHeadlandWidth, startIx)

self.logger:debug('generating %d sharp headlands from the outside, min radius %.1f',
self.nHeadlands - startIx + 1, self.context.turningRadius)
self.logger:debug('generating %d sharp headlands from the outside, first width %.1f, start at %d, min radius %.1f',
self.nHeadlands - startIx + 1, firstHeadlandWidth, startIx, self.context.turningRadius)
-- outermost headland is offset from the field boundary by half width
self.headlands[startIx] = CourseGenerator.Headland(boundary, self.context.headlandClockwise, startIx, firstHeadlandWidth, false, nil)
if not self.headlands[startIx]:isValid() then
Expand All @@ -166,7 +166,7 @@ function FieldworkCourse:generateHeadlandsFromOutside(boundary, firstHeadlandWid
end
for i = startIx + 1, self.nHeadlands do
self.headlands[i] = CourseGenerator.Headland(self.headlands[i - 1]:getPolygon(), self.context.headlandClockwise, i,
self.context:getHeadlandWorkingWidth(), false, self.headlands[1]:getPolygon())
self:_getHeadlandWorkingWidth(i), false, self.headlands[1]:getPolygon())
if self.headlands[i]:isValid() then
if self.context.sharpenCorners then
self.headlands[i]:sharpenCorners(self.context.turningRadius)
Expand All @@ -189,7 +189,7 @@ function FieldworkCourse:generateHeadlandsFromInside()
-- headlands may be more than what actually fits into the field)
while self.nHeadlandsWithRoundCorners > 0 do
self.headlands[self.nHeadlandsWithRoundCorners] = CourseGenerator.Headland(self.boundary, self.context.headlandClockwise,
self.nHeadlandsWithRoundCorners, (self.nHeadlandsWithRoundCorners - 0.5) * self.context:getHeadlandWorkingWidth(),
self.nHeadlandsWithRoundCorners, self:_getHeadlandOffset(self.nHeadlandsWithRoundCorners),
false, self.boundary)
if self.headlands[self.nHeadlandsWithRoundCorners]:isValid() then
self.headlands[self.nHeadlandsWithRoundCorners]:roundCorners(self.context.turningRadius)
Expand All @@ -202,7 +202,7 @@ function FieldworkCourse:generateHeadlandsFromInside()
end
for i = self.nHeadlandsWithRoundCorners - 1, 1, -1 do
self.headlands[i] = CourseGenerator.Headland(self.headlands[i + 1]:getPolygon(), self.context.headlandClockwise, i,
self.context:getHeadlandWorkingWidth(), true, self.boundary)
self:_getHeadlandWorkingWidth(i), true, self.boundary)
self.headlands[i]:roundCorners(self.context.turningRadius)
end
end
Expand Down Expand Up @@ -314,7 +314,7 @@ function FieldworkCourse:circleBigIslands(path, vehicle)

-- 'inside' since with islands, everything is backwards
local headlandPath = CourseGenerator.HeadlandConnector.connectHeadlandsFromInside(islandHeadlands,
slider.ix, self.context:getHeadlandWorkingWidth(), self.context.turningRadius)
slider.ix, self:_getHeadlandWorkingWidth(), self.context.turningRadius)

-- from the row end to the start of the headland, we instruct the driver to use
-- the pathfinder.
Expand Down Expand Up @@ -366,9 +366,9 @@ end
--- the headland path and from the headland into the next row
function FieldworkCourse:findPathToNextRow(boundaryId, rowEnd, rowStart, minDistanceFromRowEnd)
local headlands = self:_getCachedHeadlands(boundaryId)
local headlandWidth = #headlands * self.context:getHeadlandWorkingWidth()
local headlandWidth = #headlands * self:_getHeadlandWorkingWidth()
local usableHeadlandWidth = headlandWidth - (minDistanceFromRowEnd or 0)
local headlandPassNumber = CourseGenerator.clamp(math.floor(usableHeadlandWidth / self.context:getHeadlandWorkingWidth()), 1, #headlands)
local headlandPassNumber = CourseGenerator.clamp(math.floor(usableHeadlandWidth / self:_getHeadlandWorkingWidth()), 1, #headlands)
local headland = headlands[headlandPassNumber]
if headland == nil then
return Polyline()
Expand Down Expand Up @@ -437,6 +437,28 @@ function FieldworkCourse:_getCachedHeadlands(boundaryId)
return headlands
end

---@param n number|nil index of the headland, 1 being the outermost one. If nil, it always returns the working width
--- corrected with the overlap.
function FieldworkCourse:_getHeadlandWorkingWidth(n)
if n == nil or n > 1 then
return self.context:getHeadlandWorkingWidth() * (1 - self.context:getHeadlandOverlap())
else
-- working width of the first headland has no overlap otherwise implements won't remain on the field
return self.context:getHeadlandWorkingWidth()
end
end

---@return number the offset of the nth headland from the field boundary, taking into account the overlap.
function FieldworkCourse:_getHeadlandOffset(n)
if n == 1 then
return self:_getHeadlandWorkingWidth(1) / 2
else
-- for n > 1, the headland width is with the overlap
print(n)
return self:_getHeadlandWorkingWidth(1) + (n - 1 - 0.5) * self:_getHeadlandWorkingWidth(n)
end
end

function FieldworkCourse:__tostring()
return string.format('%d/%d headland/center waypoints', #self:getHeadlandPath(), #self:getCenterPath())
end
Expand Down
6 changes: 3 additions & 3 deletions scripts/courseGenerator/FieldworkCourseMultiVehicle.lua
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function FieldworkCourseMultiVehicle:init(context)
-- create a headland path for each vehicle
self.headlandPaths[v] = CourseGenerator.HeadlandConnector.connectHeadlandsFromOutside(self.headlandsForVehicle[v],
-- TODO is this really the headland working width? Not the combined width?
self.context.startLocation, self.context:getHeadlandWorkingWidth(), self.context.turningRadius)
self.context.startLocation, self:_getHeadlandWorkingWidth(), self.context.turningRadius)
self:routeHeadlandsAroundSmallIslands(self.headlandPaths[v])
end
self.logger:debug('### Generating up/down rows ###')
Expand All @@ -93,7 +93,7 @@ function FieldworkCourseMultiVehicle:init(context)
-- create a headland path for each vehicle
self.headlandPaths[v] = CourseGenerator.HeadlandConnector.connectHeadlandsFromInside(self.headlandsForVehicle[v],
-- TODO is this really the headland working width? Not the combined width?
endOfLastRow, self.context:getHeadlandWorkingWidth(), self.context.turningRadius)
endOfLastRow, self:_getHeadlandWorkingWidth(), self.context.turningRadius)
self:routeHeadlandsAroundSmallIslands(self.headlandPaths[v])
end
end
Expand Down Expand Up @@ -249,7 +249,7 @@ function FieldworkCourseMultiVehicle:generateCenter()
centerBoundary = referenceHeadland
else
centerBoundary = CourseGenerator.Headland(referenceHeadland:getPolygon(), self.context.headlandClockwise,
#self.headlands - 1, self.context:getHeadlandWorkingWidth() / 2, false)
#self.headlands - 1, self:_getHeadlandWorkingWidth() / 2, false)
end
CourseGenerator.addDebugPolyline(centerBoundary:getPolygon())
local innerMostHeadlandPolygon = self.headlands[#self.headlands]:getPolygon()
Expand Down
6 changes: 4 additions & 2 deletions scripts/courseGenerator/Island.lua
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,16 @@ function Island:generateHeadlands(context, mustNotCross)
local headlands = {}
self.boundary = CourseGenerator.FieldworkCourseHelper.createUsableBoundary(self.boundary, self.context.islandHeadlandClockwise)
-- innermost headland is offset from the island by half width
headlands[1] = CourseGenerator.IslandHeadland(self, self.boundary, self.context.islandHeadlandClockwise, 1, self.context:getHeadlandWorkingWidth() / 2)
headlands[1] = CourseGenerator.IslandHeadland(self, self.boundary, self.context.islandHeadlandClockwise, 1,
self.context:getHeadlandWorkingWidth() / 2)
for i = 2, self.context.nIslandHeadlands do
if not headlands[i - 1]:isValid() then
self.logger:warning('headland %d is invalid, removing', i - 1)
headlands[i - 1] = nil
break
end
headlands[i] = CourseGenerator.IslandHeadland(self, headlands[i - 1]:getPolygon(), self.context.islandHeadlandClockwise, i, self.context:getHeadlandWorkingWidth())
headlands[i] = CourseGenerator.IslandHeadland(self, headlands[i - 1]:getPolygon(), self.context.islandHeadlandClockwise,
i, self.context:getHeadlandWorkingWidth() * (1 - self.context:getHeadlandOverlap()))
end
if headlands[1]:getPolygon():intersects(mustNotCross) then
self.logger:error('First headland intersects field boundary!')
Expand Down
39 changes: 36 additions & 3 deletions scripts/courseGenerator/test/CenterTest.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ local function printRowOffsets(rowOffsets)
end
end

local function createContext(headlandWidth, centerRowSpacing, evenRowDistribution)
local function createContext(headlandWidth, centerRowSpacing, evenRowDistribution, overlap)
local mockContext = {
evenRowDistribution = evenRowDistribution,
workingWidth = headlandWidth,
Expand All @@ -19,6 +19,9 @@ local function createContext(headlandWidth, centerRowSpacing, evenRowDistributio
end,
getCenterRowSpacing = function()
return centerRowSpacing
end,
getHeadlandOverlap = function()
return overlap or 0
end
}
return mockContext
Expand Down Expand Up @@ -137,18 +140,48 @@ function testRowDistributionMultiVehicleNoHeadland()
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 10)
end

function testEvenRowDistribution()
function testEvenRowDistributionWithHeadland()
local rowOffsets
local center = {context = createContext(5, 5, true), mayOverlapHeadland = true}
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 49, true)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 4.88)
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 49, false)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 4.88)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 4.88)

rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, false)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5)
center.mayOverlapHeadland = false
end

function testEvenRowDistributionWithNoHeadland()
local rowOffsets
local center = {context = createContext(5, 5, true), mayOverlapHeadland = false}
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 49, true)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[2], 4.88)
-- this should also be 4.88, no idea what we are missing, but is minimal, we'll address it when it becomes a problem
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 4.77)
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 49, false)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 4.94)
lu.assertAlmostEquals(rowOffsets[1], 5)
-- this should also be 4.88, no idea what we are missing, but is minimal, we'll address it when it becomes a problem
lu.assertAlmostEquals(rowOffsets[2], 4.77)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 4.88)

rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, false)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5)
center.mayOverlapHeadland = false
end



os.exit(lu.LuaUnit.run())

0 comments on commit b447f74

Please sign in to comment.