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
1 change: 1 addition & 0 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
cd scripts/courseGenerator/test
lua BlockSequencerTest.lua
lua CacheMapTest.lua
lua CenterTest.lua
lua FieldTest.lua
lua FieldworkCourseTest.lua
lua FieldworkCourseMultiVehicleTest.lua
Expand Down
53 changes: 28 additions & 25 deletions scripts/courseGenerator/Center.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ function Center:init(context, boundary, headland, startLocation, bigIslands)
if headland == nil then
-- if there are no headlands, we generate a virtual one, from the field boundary
-- so using this later is equivalent of having an actual headland
-- using the nominal (without overlap) working the width for the headland, since the rows adjacent to the
-- headland must not extend beyond the field boundary
local virtualHeadland = CourseGenerator.FieldworkCourseHelper.createVirtualHeadland(boundary, self.context.headlandClockwise,
self.context:getHeadlandWorkingWidth())
self.context.workingWidth)
if self.context.sharpenCorners then
virtualHeadland:sharpenCorners(self.context.turningRadius)
end
Expand Down Expand Up @@ -238,8 +240,7 @@ function Center:_generateStraightUpDownRows(rowAngle, suppressLog)
end
-- move the baseline to the edge of the area we want to cover
baseline = baseline:createNext(dMin)
local rowOffsets = self:_calculateRowDistribution(
self.context:getCenterRowSpacing(), dMax - dMin, self.context.evenRowDistribution, overlapLast)
local rowOffsets = self:_calculateRowDistribution(dMax - dMin, overlapLast)

local rows = {}
local row = baseline:createNext(rowOffsets[1])
Expand Down Expand Up @@ -329,17 +330,21 @@ end
--- 3. leave the width of all rows the same working width. Here, part of the first or last row will be
--- outside of the field (work width * number of rows > field width). We always do this if there is a headland,
--- as the remainder will overlap with the headland.
---@param centerWorkingWidth number working width on the up/down rows in the center
---@param fieldWidth number distance between the headland centerlines we need to fill with rows. If there is no
--- headland, this is the distance between the virtual headland centerlines, which is half working width wider than
--- the actual field boundary.
---@param sameWidth boolean make all rows of the same width (#1 above)
---@param overlapLast boolean where should the overlapping row be in the sequence we create, true if at the end,
--- false at the beginning
---@return number, number, number, number number of rows, offset of first row from the field edge, offset of
--- rows from the previous row for the next rows, offset of last row from the next to last row.
function Center:_calculateRowDistribution(centerWorkingWidth, fieldWidth, sameWidth, overlapLast)
local nRows = math.floor(fieldWidth / centerWorkingWidth)
---@return number[] offset of each row from the previous, the first offset is from the baseline
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
-- 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
if nRows == 0 then
-- only one row fits between the headlands
if overlapLast then
Expand All @@ -348,39 +353,37 @@ function Center:_calculateRowDistribution(centerWorkingWidth, fieldWidth, sameWi
return { fieldWidth - centerWorkingWidth / 2 }
end
else
local width
if sameWidth then
if self.context.evenRowDistribution then
-- #1
width = (fieldWidth - centerWorkingWidth) / (nRows - 1)
else
-- #2 and #3
width = centerWorkingWidth
centerWorkingWidth = (fieldWidth - centerWorkingWidth) / (nRows - 1)
end
local firstRowOffset
local rowOffsets = {}
-- the first/last row's offset from the surrounding headland centerline
local outermostRowOffset = headlandWorkingWidth / 2 + centerWorkingWidth / 2
if self.mayOverlapHeadland then
-- #3 we have headlands
if overlapLast then
firstRowOffset = centerWorkingWidth
firstRowOffset = outermostRowOffset
else
firstRowOffset = fieldWidth - (centerWorkingWidth + width * (nRows - 1))
firstRowOffset = fieldWidth - outermostRowOffset - (centerWorkingWidth * (nRows - 1))
end
rowOffsets = { firstRowOffset }
for _ = firstRowOffset, fieldWidth, width do
table.insert(rowOffsets, width)
for _ = 2, nRows do
table.insert(rowOffsets, centerWorkingWidth)
end
else
-- #2, no headlands
for _ = centerWorkingWidth, fieldWidth - centerWorkingWidth, width do
table.insert(rowOffsets, width)
rowOffsets = { outermostRowOffset }
for _ = 2, nRows - 1 do
table.insert(rowOffsets, centerWorkingWidth)
end
if overlapLast then
table.insert(rowOffsets, fieldWidth - (centerWorkingWidth + width * #rowOffsets))
table.insert(rowOffsets, fieldWidth - 2 * outermostRowOffset - (centerWorkingWidth * (nRows - 2)))
else
rowOffsets[2] = fieldWidth - (centerWorkingWidth + width * #rowOffsets)
table.insert(rowOffsets, width)
rowOffsets[2] = fieldWidth - 2 * outermostRowOffset - (centerWorkingWidth * (nRows - 2))
table.insert(rowOffsets, centerWorkingWidth)
end

end
return rowOffsets
end
Expand Down
2 changes: 2 additions & 0 deletions scripts/courseGenerator/FieldworkCourseMultiVehicle.lua
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ function FieldworkCourseMultiVehicle:generateCenter()
-- if there are no headlands, or there are, but we start working in the middle, then use the
-- designated start location, otherwise the point where the innermost headland ends.
if #self.headlands == 0 then
-- the virtual headland the center uses is the combined working width
self.context:setHeadlandWidthForAdjustment(self.context.nVehicles * self.context.workingWidth)
self.center = CourseGenerator.Center(self.context, self.boundary, nil, self.context.startLocation, self.bigIslands)
else
-- The center is generated with the combined width of all vehicles and it assumes that the headland
Expand Down
142 changes: 142 additions & 0 deletions scripts/courseGenerator/test/CenterTest.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
require('include')

lu.EPS = 0.01

local function printRowOffsets(rowOffsets)
local y = 0
for i, offset in ipairs(rowOffsets) do
y = y + offset
print(i, offset, y)
end
end

local function createContext(headlandWidth, centerRowSpacing, evenRowDistribution)
local mockContext = {
evenRowDistribution = evenRowDistribution,
workingWidth = headlandWidth,
getHeadlandWorkingWidth = function()
return headlandWidth
end,
getCenterRowSpacing = function()
return centerRowSpacing
end
}
return mockContext
end

function testRowDistributionExactMultiple()
local rowOffsets
local center = {context = createContext(5, 5, false), mayOverlapHeadland = true}
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, false)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5)
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, false)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5)
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 100, true)
lu.assertEquals(#rowOffsets, 19)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[10], 5)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5)
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 100, false)
lu.assertEquals(#rowOffsets, 19)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[10], 5)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5)
end

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

function testRowDistributionNarrow()
local rowOffsets
local center = {context = createContext(5, 5, false), mayOverlapHeadland = true}
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 10, true)
lu.assertEquals(#rowOffsets, 1)
lu.assertAlmostEquals(rowOffsets[1], 5)
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 7, true)
lu.assertEquals(#rowOffsets, 1)
lu.assertAlmostEquals(rowOffsets[1], 5)
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 7, false)
lu.assertEquals(#rowOffsets, 1)
lu.assertAlmostEquals(rowOffsets[1], 2)
-- calculated nRows will be 0, as we reduce the field width by a centimeter
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 5, false)
lu.assertEquals(#rowOffsets, 1)
lu.assertAlmostEquals(rowOffsets[1], 2.5)
center.mayOverlapHeadland = false
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 5, false)
lu.assertEquals(#rowOffsets, 1)
lu.assertAlmostEquals(rowOffsets[1], 2.5)
end

function testRowDistributionNoOverlap()
local rowOffsets
local center = {context = createContext(5, 5, false), mayOverlapHeadland = false}

rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 49, true)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[2], 5)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 4)
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 49, false)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[2], 4)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5)
-- same with exact multiple
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, true)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[2], 5)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5)
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, false)
lu.assertEquals(#rowOffsets, 9)
lu.assertAlmostEquals(rowOffsets[1], 5)
lu.assertAlmostEquals(rowOffsets[2], 5)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5)
end

function testRowDistributionMultiVehicleWithHeadland()
local rowOffsets
local center = {context = createContext(5, 10, false), mayOverlapHeadland = true}
rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, true)
lu.assertEquals(#rowOffsets, 5)
lu.assertAlmostEquals(rowOffsets[1], 7.5)
lu.assertAlmostEquals(rowOffsets[2], 10)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 10)
end

function testRowDistributionMultiVehicleNoHeadland()
local center = {context = createContext(5, 10, false), mayOverlapHeadland = false}
local rowOffsets

rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, true)
lu.assertEquals(#rowOffsets, 5)
lu.assertAlmostEquals(rowOffsets[1], 7.5)
lu.assertAlmostEquals(rowOffsets[2], 10)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 5)

rowOffsets = CourseGenerator.Center._calculateRowDistribution(center, 50, false)
lu.assertEquals(#rowOffsets, 5)
lu.assertAlmostEquals(rowOffsets[1], 7.5)
lu.assertAlmostEquals(rowOffsets[2], 5)
lu.assertAlmostEquals(rowOffsets[#rowOffsets], 10)

end


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