Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())