Skip to content
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
4 changes: 2 additions & 2 deletions CLAUDE.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ Most of these have been offered to the official project too. When one is merged
- **Headland gaps.** With "Drive across a headland gap up to" set on the U-turn tab, a pass that crosses a narrow tip of the field is driven on with the sections off, and the turn is made at the far end.
- **U-turns with no room.** When no proper turn fits, the simple turn is moved back into the field instead of crossing the boundary. Where the headland clips a corner off the pass, AutoSteer drives on across it and says so (a thin spike of the headland line doesn't count).
- **U-turn beside a fence that comes in.** With a hard boundary, a turn that had to be moved back to keep the implement off the fence at the end of the pass could be made far too early instead, where the fence comes in beside the pass: it landed on the next pass inside its crop and left a strip of it unsprayed. Now it is moved back from the end of the pass only.
- **Skip worked passes.** A pass more than half done but left short of the end the tractor is at (its 20 m next to that end mostly not covered) counts as still to do there, so the turn can go onto it. When the order's next pass is more than four passes over (the ones in between were done before), the turn goes onto the nearest pass still to do instead of along the headland to it, and the order starts again from there. (The turn onto the nearest pass with room is official since #807.)
- **AutoSteer and a hard boundary.** At the end of a pass with no U-turn there (no pass with room at this end, no turn that fits, U-turn off), AutoSteer goes off with the alarm a metre into the headland, so there is the headland to turn by hand; before, it steered on through the fence. And with no U-turn planned, when an end of the implement is heading out across a hard boundary (AutoSteer joining a line from a corner, for example), AutoSteer goes off with the alarm. It looks along the curve the tractor is steering, so on the first lap of a boundary curve it no longer goes off at a bend with the tractor on the line. A U-turn planned for one end is dropped when the tractor turns round on the pass, so these still work at the other end.
- **Skip worked passes.** A pass more than half done but left short of the end the tractor is at (its 20 m next to that end mostly not covered) counts as still to do there, so the turn can go onto it. When the order's next pass is more than four passes over (the ones in between were done before), the turn goes onto the nearest pass still to do instead of along the headland to it, and the order starts again from there. A pass counts as done by what is painted on the map, not by a section having been on somewhere on it: where a bay cuts the passes, doing the short piece no longer marks the whole pass done, and the long piece gets its turn from the other end. (The turn onto the nearest pass with room is official since #807.)
- **AutoSteer and a hard boundary.** At the end of a pass with no U-turn there (no pass with room at this end, no turn that fits, U-turn off), AutoSteer goes off with the alarm a metre into the headland, so there is the headland to turn by hand; before, it steered on through the fence. This also works on a pass bent round trees in the headland (Go around), which leaves the crop at an angle. And with no U-turn planned, when an end of the implement is heading out across a hard boundary (AutoSteer joining a line from a corner, for example), AutoSteer goes off with the alarm. It looks along the curve the tractor is steering, so on the first lap of a boundary curve it no longer goes off at a bend with the tractor on the line. A U-turn planned for one end is dropped when the tractor turns round on the pass, so these still work at the other end.
- **AutoSteer flickering on and off.** With the steer switch / button set to Button, when the app took AutoSteer off just as it went on (a hard boundary warning, for example), the steer module's answer to the engage turned it on again, and it went on and off for seconds with the alarm. The app now ignores the module's "steering" for 1 s after it takes AutoSteer off itself.
- **No line outside a hard boundary.** A pass that lies wholly outside a hard boundary (a lap of a boundary curve outside the fence, say) is no longer shown when the tractor is outside the field with AutoSteer off: the nearest pass inside is shown. Moving the line onto such a pass is refused with a message.
- **Moving onto a line beside a hard boundary.** When AutoSteer is turned on, or the line is moved or changed, with the tractor off the new line and the implement near a hard boundary, AutoSteer now drives a smooth S onto the line that keeps the implement's ends inside the fence. A front implement swings out ahead of the tractor as it straightens, so its S is longer (an 8 m front boom 4.5 m ahead, moving one pass at 8 km/h: about 60 m; a rear implement: about 20 m). A tractor already heading steeply towards the line is first turned along it. The S is drawn on the map. When no S fits (the end of the field is too near), AutoSteer goes off with the alarm and says to move over by hand. Checked by replaying a simulator drive, not tried on a tractor yet.
Expand Down
3 changes: 2 additions & 1 deletion Shared/AgOpenWeb.Services/Pipeline/GpsPipelineService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1350,7 +1350,8 @@ private void ProcessCycle(GpsData data)
{
_headlandEntryArmed = false;
if (autoSteerEngaged && !_isReverse && fence.IsPointInside(driftedEasting, driftedNorthing)
&& _youTurnStateMachine.EndOfPassWithoutTurn(in tickCtx, _guidanceWorking, _youTurn))
&& _youTurnStateMachine.EndOfPassWithoutTurn(in tickCtx, _guidanceWorking, _youTurn,
_goAround.Current is { GoneRound.Count: > 0 } bend ? bend.Points : null))
{
autoSteerEngaged = false;
autoSteerDisengaged = true;
Expand Down
134 changes: 112 additions & 22 deletions Shared/AgOpenWeb.Services/YouTurn/YouTurnPathingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -507,10 +507,14 @@ bool RunsOnInside(double t)
/// for <see cref="MinRunAfterGap"/>? So the tractor, in the headland, is at the end of its pass
/// heading out, not heading into the field or across a gap with more field past it. False when
/// the fence is not met within <see cref="MaxWalkToFence"/>, the pass (a curve) ends first, or
/// there is no usable outer boundary. Fork only.
/// there is no usable outer boundary. With <paramref name="steeredLine"/> (the pass bent round
/// Go around obstacles, the line the tractor follows) that line is walked instead, from its
/// point nearest the tractor: the straight pass beside a bend can still be in the crop where
/// the bend has left it. Fork only.
/// </summary>
public bool PassLeavesFieldAhead(Models.Track.Track track, double abHeading, int pathsAway,
Boundary? boundary, IReadOnlyList<Vec3>? headlandLine, Position position, bool headingSameWay)
Boundary? boundary, IReadOnlyList<Vec3>? headlandLine, Position position, bool headingSameWay,
IReadOnlyList<Vec3>? steeredLine = null)
{
var outer = boundary?.OuterBoundary;
if (outer == null || !outer.IsValid || track.Points.Count < 2) return false;
Expand All @@ -520,6 +524,12 @@ public bool PassLeavesFieldAhead(Models.Track.Track track, double abHeading, int
// The pass's points ahead, a metre apart, from abreast of the tractor.
IEnumerable<Vec2> Ahead()
{
if (steeredLine is { Count: >= 2 })
{
foreach (var p in WalkAhead(steeredLine, position, headingSameWay ? abHeading : abHeading + Math.PI, Step, MaxWalkToFence))
yield return p;
yield break;
}
if (track.Points.Count > 2)
{
var curve = CurveProcessing.CreateOffsetCurve(track.Points, distAway);
Expand Down Expand Up @@ -574,6 +584,52 @@ IEnumerable<Vec2> Ahead()
return false;
}

/// <summary>
/// Points of <paramref name="line"/> every <paramref name="step"/> m from its point nearest
/// <paramref name="position"/>, the way that runs with <paramref name="travel"/> (compass
/// radians), up to <paramref name="maxWalk"/> m or the line's end.
/// </summary>
private static IEnumerable<Vec2> WalkAhead(IReadOnlyList<Vec3> line, Position position, double travel,
double step, double maxWalk)
{
int seg = 0;
double bestT = 0, best = double.MaxValue;
for (int i = 0; i < line.Count - 1; i++)
{
double ax = line[i].Easting, ay = line[i].Northing;
double dx = line[i + 1].Easting - ax, dy = line[i + 1].Northing - ay;
double len2 = dx * dx + dy * dy;
if (len2 < 1e-12) continue;
double t = Math.Clamp(((position.Easting - ax) * dx + (position.Northing - ay) * dy) / len2, 0, 1);
double ex = ax + dx * t - position.Easting, ey = ay + dy * t - position.Northing;
double d = ex * ex + ey * ey;
if (d < best) { best = d; seg = i; bestT = t; }
}
double sdx = line[seg + 1].Easting - line[seg].Easting, sdy = line[seg + 1].Northing - line[seg].Northing;
int dir = sdx * Math.Sin(travel) + sdy * Math.Cos(travel) >= 0 ? 1 : -1;

// Walk from the nearest point, vertex to vertex, putting out a point every step.
double curE = line[seg].Easting + sdx * bestT, curN = line[seg].Northing + sdy * bestT;
int next = dir > 0 ? seg + 1 : seg;
double walked = 0, carry = 0;
while (next >= 0 && next < line.Count && walked < maxWalk)
{
double ne = line[next].Easting, nn = line[next].Northing;
double len = Math.Sqrt((ne - curE) * (ne - curE) + (nn - curN) * (nn - curN));
double along = step - carry;
while (along <= len && walked < maxWalk)
{
double f = along / len;
walked += step;
yield return new Vec2(curE + (ne - curE) * f, curN + (nn - curN) * f);
along += step;
}
carry = len - (along - step);
curE = ne; curN = nn;
next += dir;
}
}

private static bool InsideFence(double e, double n, Boundary? boundary)
=> boundary?.OuterBoundary is { IsValid: true } outer && outer.IsPointInside(e, n);

Expand Down Expand Up @@ -747,11 +803,15 @@ public void AdvanceSnakeSequence(YouTurnWorkingState turn, int pass, Func<int, b
}

/// <summary>
/// Is pass <paramref name="pass"/> worked? It is when sections were on while the tractor was
/// on it (<see cref="Models.Track.Track.WorkedPaths"/>), or when the job's coverage covers at
/// least half of it inside the cultivated area, as the map shows it: a pass worked before the
/// field was reopened, or while guidance was on another pass. A U-turn painted across the
/// end of a pass leaves the pass unworked.
/// Is pass <paramref name="pass"/> worked? With the job's map, when it covers at least half of
/// the pass inside the cultivated area: a pass worked before the field was reopened, or while
/// guidance was on another pass, counts, and a U-turn painted across the end of a pass leaves
/// it unworked. With no map, when sections were on while the tractor was on it
/// (<see cref="Models.Track.Track.WorkedPaths"/>). With a map that mark is not asked: it is set
/// by a section on anywhere along the pass, so a pass split by a bay into a short piece and the
/// main one counted worked once its short piece was done, and the main piece was never turned
/// onto (official #815, the job harness on Baetas: "End of field reached" with 53 % of the
/// field undone). Fork only.
/// </summary>
public bool IsPassWorked(
Models.Track.Track track,
Expand All @@ -761,8 +821,7 @@ public bool IsPassWorked(
IReadOnlyList<Vec3>? headlandLine,
Func<double, double, bool>? isPointCovered)
{
if (track.IsPathWorked(pass)) return true;
if (isPointCovered == null || track.Points.Count < 2) return false;
if (isPointCovered == null || track.Points.Count < 2) return track.IsPathWorked(pass);

double widthMinusOverlap = _configStore.ActualToolWidth - _configStore.Tool.Overlap;
double distAway = PassGeometry.Offset(track, pass, widthMinusOverlap, nudgeOffset);
Expand Down Expand Up @@ -875,11 +934,40 @@ public bool IsPassOpenAtThisEnd(Models.Track.Track track, double abHeading, int
{
if (isPointCovered == null || track.Points.Count < 2) return false;
var stretch = EndStretch(track, abHeading, pathsAway, nudgeOffset, boundary, headlandLine, position, headingSameWay, headlandAhead);
if (stretch.Count == 0) return false;
return stretch.Count > 0 && CoveredCount(stretch, isPointCovered) * 2 < stretch.Count;
}

/// <summary>
/// The longest piece of a pass <see cref="IsPieceDoneAtThisEnd"/> walks, m.
/// </summary>
private const double MaxPieceLength = 2000.0;

/// <summary>
/// Is pass <paramref name="pathsAway"/>, still to do on the whole (<see cref="IsPassWorked"/>),
/// done at the end the tractor is heading for? It is when the piece of it a turn here lands on,
/// from that end back the way the tractor came until the pass leaves the cultivated area, is at
/// least half covered on the job's map: a bay or a headland band cuts the pass, and its piece on
/// this side of it is worked. A turn onto it here would drive that piece again and stop at the
/// band; the rest is done from the far end. A pass with no band across it is one piece, so the
/// whole pass is judged, as <see cref="IsPassWorked"/> does. False with no map, or with no piece
/// found near this end. Fork only (official #815).
/// </summary>
public bool IsPieceDoneAtThisEnd(Models.Track.Track track, double abHeading, int pathsAway, double nudgeOffset,
Boundary? boundary, IReadOnlyList<Vec3>? headlandLine, Position position, bool headingSameWay, double headlandAhead,
Func<double, double, bool>? isPointCovered)
{
if (isPointCovered == null || track.Points.Count < 2) return false;
var piece = EndStretch(track, abHeading, pathsAway, nudgeOffset, boundary, headlandLine, position, headingSameWay,
headlandAhead, MaxPieceLength, step: 2.0);
return piece.Count > 0 && CoveredCount(piece, isPointCovered) * 2 >= piece.Count;
}

private static int CoveredCount(List<Vec2> points, Func<double, double, bool> isPointCovered)
{
int covered = 0;
foreach (var p in stretch)
foreach (var p in points)
if (isPointCovered(p.Easting, p.Northing)) covered++;
return covered * 2 < stretch.Count;
return covered;
}

/// <summary>
Expand Down Expand Up @@ -937,16 +1025,18 @@ public bool TurnReachesUnworkedGround(IReadOnlyList<Vec3> turnPath, Boundary? bo
}

/// <summary>
/// Points every metre along the first <see cref="OpenEndStretch"/> of pass
/// <paramref name="pathsAway"/> inside the cultivated area back from the end the tractor is
/// heading for: from where the pass leaves the area at that end (found from the point abreast
/// of the end, forward to the edge when that point is inside, back to the area when it is
/// past the pass's end, as <see cref="LandingLength"/>), back the way the tractor came.
/// Points every <paramref name="step"/> m along the first <paramref name="length"/> m
/// (<see cref="OpenEndStretch"/> unless given) of pass <paramref name="pathsAway"/> inside the
/// cultivated area back from the end the tractor is heading for: from where the pass leaves the
/// area at that end (found from the point abreast of the end, forward to the edge when that
/// point is inside, back to the area when it is past the pass's end, as
/// <see cref="LandingLength"/>), back the way the tractor came, as far as the pass stays inside.
/// </summary>
private List<Vec2> EndStretch(Models.Track.Track track, double abHeading, int pathsAway, double nudgeOffset,
Boundary? boundary, IReadOnlyList<Vec3>? headlandLine, Position position, bool headingSameWay, double headlandAhead)
Boundary? boundary, IReadOnlyList<Vec3>? headlandLine, Position position, bool headingSameWay, double headlandAhead,
double length = OpenEndStretch, double step = 1.0)
{
const double Step = 1.0;
double Step = step;
const double Search = 300.0;
var points = new List<Vec2>();
if (headlandLine is not { Count: >= 3 } && boundary?.OuterBoundary is not { IsValid: true }) return points;
Expand Down Expand Up @@ -997,12 +1087,12 @@ double Dist(int a, int b)
if (!found) return points;
}
double sinceLast = Step;
for (double length = 0; InRange(at) && length <= OpenEndStretch && InsideAt(at); at -= fwd)
for (double back = 0; InRange(at) && back <= length && InsideAt(at); at -= fwd)
{
if (sinceLast >= Step) { points.Add(new Vec2(curve[at].Easting, curve[at].Northing)); sinceLast = 0; }
if (!InRange(at - fwd)) break;
double d = Dist(at, at - fwd);
length += d;
back += d;
sinceLast += d;
}
return points;
Expand Down Expand Up @@ -1033,7 +1123,7 @@ double Dist(int a, int b)
if (InsideAtT(tAt - s * d)) { tAt -= s * d; found = true; break; }
if (!found) return points;
}
for (double d = Step / 2; d <= OpenEndStretch; d += Step)
for (double d = Step / 2; d <= length; d += Step)
{
double t = tAt - s * d;
if (!InsideAtT(t)) break;
Expand Down
Loading
Loading