Skip to content

Commit f02eefd

Browse files
authored
fix(flight-planner): preserve viewport during waypoint redraws (#28)
* fix(flight-planner): preserve viewport across waypoint redraws * test(flight-planner): cover waypoint viewport stability * test(flight-planner): exercise deferred viewport repair * docs(porting): record waypoint viewport handoff
1 parent fb78961 commit f02eefd

3 files changed

Lines changed: 148 additions & 0 deletions

File tree

Controls/FlightPlannerMap.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ private readonly
5454
private int _dragIndex = -1;
5555
private bool _pressedOnWaypoint;
5656
private readonly List<MPoint> _mapOverlayPoints = [];
57+
private int _waypointRedrawVersion;
5758

5859
private readonly HashSet<int> _groupSet = new();
5960
private readonly Dictionary<int, (double Lat, double Lng)> _groupSnapshot = new();
@@ -135,6 +136,11 @@ public void SetWaypoints(
135136
IReadOnlyList<(int Seq, double Lat, double Lng, ushort Cmd, double P1, double P2, double P3,
136137
double P4)> wps,
137138
double wpRadius, double loiterRadius, Firmwares firmware) {
139+
var viewportBeforeRedraw = Map.Navigator.Viewport;
140+
bool preserveViewport = _centered && viewportBeforeRedraw.HasSize()
141+
&& double.IsFinite(viewportBeforeRedraw.Resolution)
142+
&& viewportBeforeRedraw.Resolution > 0;
143+
int redrawVersion = ++_waypointRedrawVersion;
138144
_wpRadius = RadiusInMeters(wpRadius, CurrentState.multiplierdist);
139145
_loiterRadius = RadiusInMeters(loiterRadius, CurrentState.multiplierdist);
140146
_firmware = firmware;
@@ -154,6 +160,29 @@ public void SetWaypoints(
154160
double res = 156543.03392804097 / Math.Pow(2, 17);
155161
Map.Navigator.CenterOnAndZoomTo(new MPoint(x, y), res);
156162
_centered = true;
163+
} else if (preserveViewport) {
164+
RestoreViewport(viewportBeforeRedraw);
165+
// Mapsui may finish an extent-reactive refresh after this input callback returns.
166+
Dispatcher.UIThread.Post(() => {
167+
if (redrawVersion == _waypointRedrawVersion) {
168+
RestoreViewport(viewportBeforeRedraw);
169+
}
170+
});
171+
}
172+
}
173+
174+
private void RestoreViewport(Viewport expected) {
175+
Viewport current = Map.Navigator.Viewport;
176+
if (current.CenterX.Equals(expected.CenterX)
177+
&& current.CenterY.Equals(expected.CenterY)
178+
&& current.Resolution.Equals(expected.Resolution)
179+
&& current.Rotation.Equals(expected.Rotation)) {
180+
return;
181+
}
182+
Map.Navigator.CenterOnAndZoomTo(
183+
new MPoint(expected.CenterX, expected.CenterY), expected.Resolution);
184+
if (!Map.Navigator.Viewport.Rotation.Equals(expected.Rotation)) {
185+
Map.Navigator.RotateTo(expected.Rotation);
157186
}
158187
}
159188

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using Avalonia.Controls;
2+
using Avalonia.Headless.XUnit;
3+
using Avalonia.Threading;
4+
using Mapsui.Layers;
5+
using Mapsui.Nts;
6+
using Mapsui.Styles;
7+
using MissionPlanner.ArduPilot;
8+
using MissionPlanner.Controls;
9+
10+
namespace MissionPlanner.Tests;
11+
12+
public class FlightPlannerViewportTests {
13+
[AvaloniaFact]
14+
public void Waypoint_redraw_preserves_an_initialized_viewport() {
15+
var map = new FlightPlannerMap();
16+
var window = new Window { Width = 1200, Height = 800, Content = map };
17+
try {
18+
window.Show();
19+
Dispatcher.UIThread.RunJobs();
20+
21+
map.CenterOnAndZoom(50.344, 30.88, 14);
22+
map.SetHome(37.619373, -122.376637, 5.28);
23+
map.SetWaypoints(KyivWaypoints(2), 50, 80, Firmwares.ArduPlane);
24+
Dispatcher.UIThread.RunJobs();
25+
Mapsui.Viewport expected = map.Map.Navigator.Viewport;
26+
27+
WritableLayer route = RouteLayer(map);
28+
route.DataChanged += (_, _) => {
29+
if (route.GetFeatures().OfType<GeometryFeature>().Count() == 2) {
30+
Dispatcher.UIThread.Post(() => map.Map.Navigator.ZoomToBox(route.Extent));
31+
}
32+
};
33+
34+
map.SetWaypoints(KyivWaypoints(3), 50, 80, Firmwares.ArduPlane);
35+
Dispatcher.UIThread.RunJobs();
36+
Mapsui.Viewport actual = map.Map.Navigator.Viewport;
37+
38+
Assert.Equal(expected.CenterX, actual.CenterX, 3);
39+
Assert.Equal(expected.CenterY, actual.CenterY, 3);
40+
Assert.Equal(expected.Resolution, actual.Resolution, 6);
41+
} finally {
42+
window.Close();
43+
}
44+
}
45+
46+
[AvaloniaFact]
47+
public void Distant_home_closing_route_remains_solid() {
48+
var map = new FlightPlannerMap();
49+
map.SetHome(37.619373, -122.376637, 5.28);
50+
map.SetWaypoints(KyivWaypoints(3), 50, 80, Firmwares.ArduPlane);
51+
52+
GeometryFeature homeRoute = HomeRoute(map);
53+
Assert.True(Assert.IsType<Mapsui.MRect>(homeRoute.Extent).Width > 10_000_000);
54+
Assert.Equal(PenStyle.Solid, LineStyle(homeRoute));
55+
}
56+
57+
[AvaloniaFact]
58+
public void Nearby_home_closing_route_remains_dashed() {
59+
var map = new FlightPlannerMap();
60+
map.SetHome(50.3440, 30.8940, 100);
61+
map.SetWaypoints(KyivWaypoints(3), 50, 80, Firmwares.ArduPlane);
62+
63+
Assert.Equal(PenStyle.Dash, LineStyle(HomeRoute(map)));
64+
}
65+
66+
private static GeometryFeature HomeRoute(FlightPlannerMap map) {
67+
GeometryFeature[] features = RouteLayer(map).GetFeatures().OfType<GeometryFeature>().ToArray();
68+
Assert.Equal(2, features.Length);
69+
return Assert.Single(features,
70+
feature => Assert.Single(feature.Styles.OfType<VectorStyle>()).Line?.Width == 2);
71+
}
72+
73+
private static PenStyle LineStyle(GeometryFeature feature) =>
74+
Assert.IsType<Pen>(Assert.Single(feature.Styles.OfType<VectorStyle>()).Line).PenStyle;
75+
76+
private static WritableLayer RouteLayer(FlightPlannerMap map) =>
77+
Assert.IsAssignableFrom<WritableLayer>(
78+
Assert.Single(map.Map.Layers, candidate => candidate.Name == "Route"));
79+
80+
private static IReadOnlyList<(
81+
int Seq, double Lat, double Lng, ushort Cmd, double P1, double P2, double P3, double P4)>
82+
KyivWaypoints(int count) => new[] {
83+
Waypoint(0, 50.3443012, 30.8947693),
84+
Waypoint(1, 50.3475125, 30.8815415),
85+
Waypoint(2, 50.3468588, 30.8318016),
86+
}.Take(count).ToArray();
87+
88+
private static (
89+
int Seq, double Lat, double Lng, ushort Cmd, double P1, double P2, double P3, double P4)
90+
Waypoint(int seq, double lat, double lng) =>
91+
(seq, lat, lng, (ushort)MAVLink.MAV_CMD.WAYPOINT, 0, 0, 0, 0);
92+
}

Porting/STATUS.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,33 @@
22

33
Updated: **2026-08-31**.
44

5+
## Flight Planner waypoint viewport stability
6+
7+
- Dedicated branch `fix/flight-planner-waypoint-viewport-stability` starts from merged `master`
8+
`fb7896112`. Commit `788d49d32` preserves an already initialized Flight Planner viewport across
9+
waypoint redraws; commit `27a4a899a` adds the SFO-home/Kyiv-mission regression and official
10+
closing-route style checks; reviewer follow-up `cd13dfbfd` exercises the deferred repair ordering.
11+
- `SetWaypoints` snapshots centre, resolution and rotation only after the planner has established a
12+
valid viewport. It restores an inline redraw mutation immediately and posts one version-gated
13+
restore for an extent-reactive mutation queued by the same redraw. Initial startup and first-point
14+
centring remain unchanged. The official `last -> home -> first` route is still rendered: a distant
15+
route remains solid and a route whose two home legs are both under 5 km remains dashed.
16+
- Behavior, integration and test reviewers approve exact HEAD `cd13dfbfd`. The focused
17+
`FlightPlannerViewportTests` pass **3/3**, and `PlannerPortParityTests` pass **59/59**.
18+
`dotnet build MissionPlanner.csproj -c Release -m:1 --no-restore` succeeds with **0 warnings / 0
19+
errors**. The complete Avalonia suite ran **1558** cases: **1557 passed** and only the existing
20+
environment-sensitive `VideoSourceResolverTests.NormalizesCommonStreamSources` `/dev/video0`
21+
case failed; neither the resolver nor its test changes on this branch.
22+
- The worktree still contains the user's five pre-existing modifications in
23+
`Drivers/inf2cat.bat`, `Drivers/uninstall_drivers.bat`, `ExtLibs/Mavlink/regenerate.bat`,
24+
`ExtLibs/Mavlink/updatexmls.bat` and `graphs/updatexmls.bat`; none is staged or included in these
25+
commits. Claude remains disabled.
26+
- The currently running Debug process loaded older commit `8f63d8d1`, so it must be stopped and
27+
relaunched from the repository root before manual acceptance. Keep a connected SFO home, pan Plan
28+
to Kyiv, add at least three waypoints and confirm the local centre/zoom does not change; the long
29+
solid closing route must remain rendered. The separately observed Flight Data bearing-axis
30+
behavior while zooming remains excluded and is the next independent bug.
31+
532
## Flight Data vehicle-overlay marker rendering
633

734
- Dedicated branch `fix/flight-data-vehicle-overlay-rendering` starts from pulled `master`

0 commit comments

Comments
 (0)