WPF .NET 8 desktop app for VFR flight navigation planning. Built for a student pilot at Flight School Teuge (EHTE, Teuge, Netherlands).
- WPF .NET 8 (
net8.0-windows,<UseWPF>true</UseWPF>) - Mapsui.Wpf 4.1.9 — interactive OpenStreetMap / ESRI satellite map
- CommunityToolkit.Mvvm 8.4.0 — MVVM (
ObservableObject,[ObservableProperty],[RelayCommand]) - NetTopologySuite — route polyline geometry (transitive Mapsui dependency)
- BruTile 5.0.6 — tile source abstraction (transitive Mapsui dependency);
ArcGisTileSourceused for satellite view
NavigationPlan/
├── NavigationPlan.sln
├── make_icon.ps1 # PowerShell script that generates NavigationPlan.ico
└── NavigationPlan/
├── NavigationPlan.ico # App icon (compass rose + airplane, multi-size)
├── Models/
│ ├── Waypoint.cs # lat/lon, name, IsFixed, Index
│ ├── NavLeg.cs # one row in the nav plan table (ObservableObject)
│ └── FlightPlan.cs # aircraft + flight + wind + QNH data container
├── Services/
│ ├── NavigationCalculator.cs # all aviation math (static, pure)
│ ├── AirportLookupService.cs # downloads/caches OurAirports CSV, ICAO lookup
│ └── PrintService.cs # WPF FixedDocument print (A4 landscape)
├── ViewModels/
│ └── MainViewModel.cs # all state, commands, save/open/RT logic
├── Views/
│ ├── InputDialog.xaml # waypoint name prompt dialog
│ ├── DepartureDialog.xaml # set departure airport name + lat/lon
│ └── RtCallsDialog.xaml # scrollable RT calls display dialog
├── Themes/
│ └── DarkTheme.xaml # dark colour palette ResourceDictionary
├── MainWindow.xaml # 3-panel layout (left inputs | map | bottom DataGrid)
├── MainWindow.xaml.cs # Mapsui map lifecycle, click/right-click handlers
├── App.xaml # merges DarkTheme.xaml
└── pic/
└── NavPlan.jpg # reference scan of the Flight School Teuge paper form
- Left panel (~280 px): Aircraft, Flight, Wind & Pressure, Performance input groups; Find airport search bar; Map type selector; action buttons
- Right panel (fills rest): Mapsui
MapControlwith OpenStreetMap or ESRI satellite tiles - Bottom panel (250 px, full width):
DataGridwith nav plan legs
Waypoint | Freq | Level | TAS | TT | TH | WCA | MH | GS | Dist | Time | ETA | ATA | Remarks
- Freq — radio frequency per waypoint (editable)
- ATA and Remarks are user-editable in-flight
- Freq, ATA, Remarks are preserved across Calculate and saved/restored via
[NOTES]section in the.txtfile
- TT (True Track): forward azimuth via
atan2 - Dist: Haversine in nautical miles
- WCA (Wind Correction Angle):
asin(WS/TAS * sin(windTrack − TT)) - GS (Ground Speed):
TAS * cos(WCA) + WS * cos(angle) - TH = TT + WCA, MH = TH − magnetic variation
- Time:
(Dist / GS) * 60rounded to whole minutes - ETA: departure time + cumulative leg times (
TimeOnly.AddMinutes) - Magnetic variation default: 2.5° East (Netherlands)
- Default view: Netherlands bounding box (zoomed on first
SizeChanged) - Departure pin is fixed (blue, cannot be removed); default is EDLS Stadtlohn-Vreden Airport (ARP N51°59′45″ E006°50′26″)
- Left-click empty map →
InputDialog→ adds numbered waypoint pin (red) - Right-click pin → confirm → removes waypoint (departure pin protected)
- Route drawn as blue polyline via
GeometryFeature+VectorStyle - Find airport search bar — type ICAO, press Enter → map zooms to airport; "Set as Dep." button sets found airport as fixed departure
- Map type ComboBox — Street (OpenStreetMap) or Satellite (ESRI World Imagery via
ArcGisTileSource) - Dep. Airport… button →
DepartureDialogto manually set departure name + coordinates
- Default: EDLS (Stadtlohn-Vreden, Germany) at 51.995833°N, 6.840556°E
- Changeable at runtime via: Find → Set as Dep., or Dep. Airport… button (manual lat/lon entry)
MainViewModel.SetDeparture(name, lat, lon)replaces the fixed waypoint inWaypoints[0];Fromfield syncs automatically- Clear command preserves the current departure airport
- On first run: downloads
airports.csv(~6 MB, ~85 000 airports) from OurAirports and caches to%LOCALAPPDATA%\NavigationPlan\airports.csv - Subsequent launches load from cache (fast)
Lookup(icao)returnsAirportInfo(Ident, Name, Lat, Lon)by dictionary lookup- Download starts in background on
Window_Loaded; status shown in result label below the Find box
- Save .txt: saves form fields, waypoints (with coordinates,
fixedflag),[NOTES]section (Freq/ATA/Remarks per waypoint), and the full nav plan table - Open…: parses a saved
.txtand restores all fields + waypoints + notes on the map - Notes are stored in
_pendingNotes(dict populated byOpen(), consumed byCalculate()) - Saved files (
NavPlan_*.txt) are excluded from git via.gitignore
- Page 1 — Nav plan form:
PrintService.Printbuilds a WPFFixedDocument(A4 landscape, 1122×794 px) replicating the NavPlan.jpg form layout - Page 2 — Navigation chart (optional, toggled by "Print chart" checkbox): auto-zooms map to fit all waypoints (
MRect.Grow(10000)), captures viaRenderTargetBitmap, then restores original viewport. Title includes Wind and QNH. - Mapsui viewport save/restore uses
nav.Viewport.CenterX/CenterY/Resolutionandnav.CenterOnAndZoomTo()
Generates a full ATC radio telephony script for the flight, structured in three sections:
- Pre-Departure —
{from}Radio ({depFrq}): radio check, departure info request, take-off clearance, frequency change.depFrqcomes fromNavLegs[0].Frequency; defaults to[DEP FREQ]if not set. - En Route — Dutch Mil Info (132.350): initial contact with ETA, position reports at each intermediate waypoint, freq change approaching destination
- Arrival — destination radio: inbound call, circuit join, finals, vacated
Displayed in RtCallsDialog (scrollable, monospace, Copy to Clipboard button). Called from RtButton_Click in code-behind.
- Generated by
make_icon.ps1(PowerShell +System.Drawing) - Design: dark navy gradient circle, gold outer ring, 8-pointed compass rose (white N/S/E/W + gold diagonals), gold north-hat triangle, white top-down airplane silhouette, gold center dot
- Sizes: 16, 24, 32, 48, 256 px (PNG-compressed within ICO)
- Set as
ApplicationIconin.csprojandIcon="NavigationPlan.ico"inMainWindow.xaml
cd NavigationPlan
dotnet run
Or open NavigationPlan.slnx in Visual Studio 2022+.
- Viewport properties:
CenterX,CenterY,Resolution,Rotation,Width,Height(no.CenterMPoint) - Navigator:
CenterOnAndZoomTo(MPoint, double),ZoomToBox(MRect)— noNavigateTo MRect.Grow(n)expands bynmetres on all four sidesRenderTargetBitmapcaptures at WPF logical pixels (96 DPI), not physical pixels — true 1:1 map capture is not achievable- Satellite layer:
new TileLayer(new ArcGisTileSource(url, new GlobalSphericalMercator(), null, null)) MapControl.Map.Layers[0]is the base tile layer; swap it byRemove+Insert(0, newLayer)SelectionChangedon a ComboBox fires during XAML init before the map is ready — guard withMapControl?.Map?.Layers?.Contains(_baseTileLayer) != true