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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The readout was proposed to the official project in [AgOpenGPS-Official/AgOpenWe

Most of these have been offered to the official project too. When one is merged there, it comes off this list.

- **Obstacles.** In the boundary list, each obstacle gets its own choice: a U-turn before it, Go around (the pass bends round it), or Drive through. Trees can be added as circles with "+ Circles", and small obstacles made round ("Round all" rounds every obstacle up to 10 m across; a bigger one keeps its shape). The Go around curve radius and safety margin are on the U-turn tab. Go around is still being tested: don't use it with AutoSteer on a real tractor yet.
- **Obstacles.** In the boundary list, each obstacle gets its own choice: a U-turn before it, Go around (the pass bends round it), or Drive through. Trees can be added as circles with "+ Circles", and small obstacles made round ("Round all" rounds every obstacle up to 10 m across; a bigger one keeps its shape), and "Size all" gives every circle up to 10 m across the diameter in the box. The Go around curve radius and safety margin are on the U-turn tab. Go around is still being tested: don't use it with AutoSteer on a real tractor yet.
- **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.
Expand Down
1 change: 1 addition & 0 deletions Shared/AgOpenWeb.RemoteServer/wwwroot/css/tool-panels.css
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
/* Round obstacles: the diameter box and the circle buttons under the list. */
.bm-circ { display: flex; align-items: center; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
.bm-circlbl { font-size: 13px; color: var(--text-muted); min-width: 84px; }
.bm-brk { flex-basis: 100%; height: 0; } /* the tools on a row of their own, under the size */
.bm-circin { width: 64px; height: 40px; box-sizing: border-box; padding: 0 6px; font-size: 15px; text-align: right;
border: 1px solid var(--border); border-radius: 6px; background: rgba(var(--ov),0.06); color: inherit; }
.bm-cbtn { height: 40px; padding: 0 10px; border: 1px solid var(--border); border-radius: 6px; background: transparent;
Expand Down
6 changes: 6 additions & 0 deletions Shared/AgOpenWeb.RemoteServer/wwwroot/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
"Choose a page": "Choose a page",
"Choose a page above.": "Choose a page above.",
"Choose this VT in the implement's own settings.": "Choose this VT in the implement's own settings.",
"Circle size": "Circle size",
"Clear all points": "Clear all points",
"Click a key, then press the new key. A bound key replaces the map key it shares (F follow, 3 tilt, arrows/space simulator).": "Click a key, then press the new key. A bound key replaces the map key it shares (F follow, 3 tilt, arrows/space simulator).",
"Click Finish to save the settings and close the wizard": "Click Finish to save the settings and close the wizard",
Expand Down Expand Up @@ -352,6 +353,7 @@
"Error: Caster host is required": "Error: Caster host is required",
"Error: Mount point is required": "Error: Mount point is required",
"Every": "Every",
"Every circle at once: up to {max} across. Set a bigger one on its own": "Every circle at once: up to {max} across. Set a bigger one on its own",
"Every obstacle": "Every obstacle",
"Extend line at A (+5 m)": "Extend line at A (+5 m)",
"Extend line at B (+5 m)": "Extend line at B (+5 m)",
Expand Down Expand Up @@ -423,7 +425,9 @@
"GitHub Repository": "GitHub Repository",
"GitHub username": "GitHub username",
"Give all {n} obstacles \"{choice}\"?": "Give all {n} obstacles \"{choice}\"?",
"Give every circle up to a tree's size the diameter in the box": "Give every circle up to a tree's size the diameter in the box",
"Give every obstacle a U-turn": "Give every obstacle a U-turn",
"Give the {n} circles up to {max} across a diameter of {d}? Bigger circles and drawn obstacles keep their size. This cannot be undone.": "Give the {n} circles up to {max} across a diameter of {d}? Bigger circles and drawn obstacles keep their size. This cannot be undone.",
"GLONASS code-phase biases": "GLONASS code-phase biases",
"GLONASS ephemeris": "GLONASS ephemeris",
"GLONASS observations (legacy)": "GLONASS observations (legacy)",
Expand Down Expand Up @@ -670,6 +674,7 @@
"No boundaries yet — drive around the field to record one.": "No boundaries yet — drive around the field to record one.",
"No boundary": "No boundary",
"No boundary to offset": "No boundary to offset",
"No circle up to {max} across": "No circle up to {max} across",
"No cloud fields (Refresh to load).": "No cloud fields (Refresh to load).",
"No connection to AgOpenWeb": "No connection to AgOpenWeb",
"No connection: the report sends by itself when the device is back online.": "No connection: the report sends by itself when the device is back online.",
Expand Down Expand Up @@ -957,6 +962,7 @@
"Single": "Single",
"Single antenna": "Single antenna",
"Size": "Size",
"Size all": "Size all",
"Skip": "Skip",
"Slow": "Slow",
"Slow speed cutoff": "Slow speed cutoff",
Expand Down
5 changes: 4 additions & 1 deletion Shared/AgOpenWeb.RemoteServer/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1521,10 +1521,13 @@
<span>Hard</span></div>
<div id="bm-list" class="bm-list"></div>
<!-- Round obstacles (trees): the diameter is the selected circle's, else the one new circles
get; new circles are placed by a tap on each tree's centre on the map. -->
get; new circles are placed by a tap on each tree's centre on the map. "Size all" gives
every circle up to a tree's size the diameter in the box. -->
<div class="bm-circ">
<span id="bm-circ-lbl" class="bm-circlbl">New circles</span>
<input id="bm-circ-d" class="bm-circin" data-unit="m" type="number" step="0.1" min="0.5" max="100" inputmode="decimal" title="Diameter"><span class="u" data-unit="m"></span>
<button id="bm-circ-size" class="bm-cbtn" title="Give every circle up to a tree's size the diameter in the box">Size all</button>
<span class="bm-brk"></span>
<button id="bm-circ-add" class="bm-cbtn" title="Add circles: tap the centre of each tree on the map">+ Circles</button>
<button id="bm-circ-move" class="bm-cbtn" title="Tap the map at the selected circle's new centre">Move</button>
<button id="bm-circ-round" class="bm-cbtn" title="Make the selected obstacle round: the smallest circle that holds it">Round</button>
Expand Down
20 changes: 19 additions & 1 deletion Shared/AgOpenWeb.RemoteServer/wwwroot/js/hud-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,8 @@ for (const h of document.querySelectorAll('#boundarymenu .bm-colhdr')) h.addEven
// the next tree gets the size of the one just placed. "+ Circles" stays armed: every tap on the
// map puts a circle centred there, until it is tapped again or the menu closes. "Round" makes
// the selected drawn obstacle the smallest circle that holds it, "Round all" every one up to a
// tree's size. A tap on an obstacle already there selects it instead of adding a circle on it.
// tree's size, "Size all" every circle up to a tree's size the diameter in the box. A tap on an
// obstacle already there selects it instead of adding a circle on it.
const CIRCLE_D_KEY = 'aow.newCircleDiameter';
const ROUND_ALL_MAX_D = 10; // metres across: the host's ObstacleCircle.MaxRoundAllDiameter
let bmNewD = 6;
Expand Down Expand Up @@ -482,6 +483,23 @@ document.getElementById('bm-circ-all').addEventListener('pointerdown', ev => {
tr('Make the obstacles up to {d} across round? Each becomes the smallest circle that holds it; long, thin ones and bigger ones keep their shape. This cannot be undone.', { d: fmtUnit(ROUND_ALL_MAX_D, 'm', 0) }),
() => transport.send('boundary.circle.roundAll'));
});
document.getElementById('bm-circ-size').addEventListener('pointerdown', ev => {
ev.stopPropagation();
// Trees only: a bigger circle (a pond, a clump made round) keeps its size, and the diameter is
// a tree's too, so the trees can be changed again (the host checks both). No undo.
const d = readUnitInput(bmCircD);
if (!circleDiameterOk(d)) {
showToast(tr('A circle is {min} to {max} across', { min: fmtUnit(0.5, 'm', 1), max: fmtUnit(100, 'm', 0) }));
renderCircleBar(); return;
}
const max = fmtUnit(ROUND_ALL_MAX_D, 'm', 0);
if (d > ROUND_ALL_MAX_D + 0.001) { showToast(tr('Every circle at once: up to {max} across. Set a bigger one on its own', { max })); return; }
const n = boundary ? boundary.items.filter(i => i.isInner && i.circleD > 0 && i.circleD <= ROUND_ALL_MAX_D + 0.001).length : 0;
if (!n) { showToast(tr('No circle up to {max} across', { max })); return; }
showConfirm(tr('Circle size'),
tr('Give the {n} circles up to {max} across a diameter of {d}? Bigger circles and drawn obstacles keep their size. This cannot be undone.', { n, max, d: fmtUnit(d, 'm', 1) }),
() => transport.send('boundary.circle.diameterAll|' + d));
});
// The scene ring of the smallest obstacle holding the point (world metres), or -1.
function obstacleRingAt(e, n) {
if (!scene || !scene.boundaries) return -1;
Expand Down
4 changes: 4 additions & 0 deletions Shared/AgOpenWeb.RemoteWiring/RemoteServerWiring.cs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,10 @@ void CaptureBoundaryImagery(System.Collections.Generic.IReadOnlyList<(double E,
vm.RemoteSetObstacleCircleDiameter(bcr, bcdd);
return;
}
case "boundary.circle.diameterAll": // arg = d: every circle up to a
if (double.TryParse(arg, num, inv, out var bcda)) // tree's size
vm.RemoteSetObstacleCirclesDiameter(bcda); // gets it
return;
case "boundary.circle.move": // arg = "row:e,n": that obstacle becomes a
{ // circle centred there
var bcc = arg.IndexOf(':');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,47 @@ public void RemoteMakeObstaclesRound()
else StatusMessage = $"{made} obstacles made round";
}

/// <summary>
/// Every circle up to a tree's size (<see cref="ObstacleCircle.MaxRoundAllDiameter"/>, 10 m) gets
/// <paramref name="diameter"/>, round its own centre: the trees of a field put down at one size
/// and found to be another (a tester's field: every tree a 10 m circle, the crowns pruned so the
/// implement passes under all but the trunk's 1-2 m). A bigger circle (a pond, a clump made
/// round) keeps its size, and the diameter is a tree's too, at most 10 m, so the trees stay
/// within reach of the next change. Drawn obstacles are left as drawn. Fork only.
/// </summary>
public void RemoteSetObstacleCirclesDiameter(double diameter)
{
if (!CheckDiameter(diameter)) return;
if (diameter > ObstacleCircle.MaxRoundAllDiameter)
{
ReportFailure($"Every circle at once: up to {Metres(ObstacleCircle.MaxRoundAllDiameter)} across. Set a bigger one on its own");
return;
}
if (!TryLoadBoundaryForEdit(out var boundary, out var fieldPath)) return;
int changed = 0, trees = 0, kept = 0;
foreach (var inner in boundary.InnerBoundaries)
{
if (!inner.IsValid || inner.Circle is not { } circle) continue;
if (!(circle.Diameter <= ObstacleCircle.MaxRoundAllDiameter + 0.001))
{
kept++;
continue;
}
trees++;
if (Math.Abs(circle.Diameter - diameter) < 0.001) continue;
inner.SetCircle(circle with { Diameter = diameter });
changed++;
}
if (trees == 0)
{
ReportFailure(kept > 0 ? $"No circle up to {Metres(ObstacleCircle.MaxRoundAllDiameter)} across: {kept} bigger, kept as they are" : "The field has no circles");
return;
}
if (changed > 0) SaveObstacleChoice(boundary, fieldPath, SelectedBoundaryIndex);
if (kept > 0) ReportFailure($"{trees} circles {Metres(diameter)} across; {kept} over {Metres(ObstacleCircle.MaxRoundAllDiameter)} kept as they are");
else StatusMessage = $"{trees} circles {Metres(diameter)} across";
}

private bool CheckDiameter(double diameter)
{
if (ObstacleCircle.IsValidDiameter(diameter)) return true;
Expand Down
44 changes: 44 additions & 0 deletions Tests/AgOpenWeb.ViewModels.Tests/ObstacleCircleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,50 @@ public async Task ACircle_GetsANewDiameter_AndANewCentre()
Assert.That(OnDisk(_dir).InnerBoundaries[3].IsPointInside(80, 150), Is.False, "the polygon moved with it");
}

[Test]
public async Task SizeAll_GivesEveryTreeCircleTheDiameter_AndLeavesTheRest()
{
// Trees put down as 10 m circles, pruned so the implement passes under all but the trunk's
// 1-2 m (a tester's field), a pond made round (17 m) and the two trees drawn as squares.
var vm = await Open();
vm.RemoteAddObstacleCircle(80, 150, 10);
vm.RemoteAddObstacleCircle(100, 150, 10);
vm.RemoteAddObstacleCircle(120, 150, 6);
vm.RemoteAddObstacleCircle(30, 120, 17);

vm.RemoteSetObstacleCirclesDiameter(3);

var inner = OnDisk(_dir).InnerBoundaries;
Assert.That(inner.Skip(3).Select(i => i.Circle!.Value.Diameter), Is.EqualTo(new[] { 3.0, 3.0, 3.0, 17.0 }).AsCollection,
"the trees, not the 17 m circle");
Assert.That(inner[3].Circle!.Value.CenterEasting, Is.EqualTo(80).Within(0.005), "round each one's own centre");
Assert.That(inner[4].IsPointInside(100 + 1.6, 150), Is.False, "the polygon shrank with it");
Assert.That(inner.Take(3).Select(i => i.Circle.HasValue), Is.All.False, "drawn obstacles are left as drawn");
Assert.That(inner[1].Points, Has.Count.EqualTo(4));
Assert.That(vm.StatusMessage, Does.Contain("3 circles 3.0 m across").And.Contain("1 over 10.0 m kept"));
Assert.That(vm.BoundaryItems[4].CircleDiameter, Is.EqualTo(3), "the list shows it");

vm.RemoteSetObstacleCirclesDiameter(4); // the trees are still trees
Assert.That(OnDisk(_dir).InnerBoundaries.Skip(3).Select(i => i.Circle!.Value.Diameter),
Is.EqualTo(new[] { 4.0, 4.0, 4.0, 17.0 }).AsCollection);
}

[Test]
public async Task SizeAll_OverATreesSize_OrWithNoCircles_ChangesNothing()
{
var vm = await Open();

vm.RemoteSetObstacleCirclesDiameter(3);
Assert.That(vm.StatusMessage, Does.Contain("no circles"));

vm.RemoteAddObstacleCircle(80, 150, 6);
vm.RemoteSetObstacleCirclesDiameter(12);
Assert.That(vm.StatusMessage, Does.Contain("up to 10.0 m across"));
vm.RemoteSetObstacleCirclesDiameter(0.2);
Assert.That(vm.StatusMessage, Does.Contain("across"));
Assert.That(OnDisk(_dir).InnerBoundaries[3].Circle!.Value.Diameter, Is.EqualTo(6));
}

[Test]
public async Task TheOuterRow_CannotBeMadeRound()
{
Expand Down
Loading