Planning Form - msupino/NavigationApp GitHub Wiki
🇮🇱 עברית
The wind-triangle sheet: calibrated airspeed in, compass heading out, one row per segment. It is the document an Israeli CVFR written exercise asks for, and the one a pilot works a real departure out on.
Open it with 📐 Flight planning form in the Charts menu. Like the flight plan it needs a route: with no legs it says No legs yet — drop at least two waypoints first and opens nothing.

They are different documents and both are kept.
| Flight plan | Flight planning form | |
|---|---|---|
| What it is | the route as flown: magnetic heading, distance, time, fuel | the arithmetic between an indicated airspeed and a compass heading |
| Wind | none, deliberately — a printed plan must not change because a weather fetch landed | the whole point of it |
| Where it is used | in the air | at the desk, before the flight |
That zero-wind rule is why this exists at all rather than as twelve more columns on the plan.
LEG · From · To · CAS · PA · Temp · TAS · W kt · W dir · TT · Drift · TH · Var · MH · Dev · CH · GS · Dist · Time · Cum time · FF · Fuel · Cum fuel
In that order, with those Hebrew names, because that is the order and the wording of the sheet the exercise hands out. Climb rows are tinted green, descent rows amber, so the three kinds of segment read apart at a glance.
Every one of these reproduces a published answer sheet. Where an obvious alternative exists, it is named here because it does not.
True airspeed is CAS ÷ √σ, where σ is the density ratio at the segment's density altitude:
ISA temp at PA = 15 − 2 °C per 1000 ft
density altitude = PA + 120 × (OAT − ISA temp)
σ = (1 − 6.8756e−6 × DA) ^ 4.2561
Two degrees per thousand feet, not the physical 1.98: the figure the exercise is worked in and the one a pilot can check without a calculator. The two differ in the first decimal of a TAS at one place on the reference sheet — 6,000 ft, where 1.98 gives 98.24 and 2 gives 98.26, printing 98.2 and 98.3. The in-flight altimetry correction keeps 1.98; it is flown against rather than checked by hand.
Where each segment reads its air. The climb takes two thirds of the height gained, above the field it left; the descent takes half the height lost, above the field it is landing at. Both fractions are fields in the form, in percent, and both start at the exercise's figures.
The met table and the compass card are read at their nearest entry, never interpolated. This is not an approximation: the reference sheet's MH 050 takes the 060 entry's deviation and prints 049, where interpolating between 030 and 060 gives 048. Ties go to the lower row, so the choice is deterministic.
Drift is the side the wind pushes you, which is the opposite sign to the wind-correction angle
the app computes — and it is derived from the two columns beside it, so the sheet adds up: track −
drift = true heading. Deviation is written the way variation is (2E, not +2) because east
subtracts in both and a pilot applies them the same way.
One top of climb. The climb off the departure field is the only ramp: it is the one place the aeroplane demonstrably leaves a known elevation. A later leg planned higher or lower is flown at its own level and gets no ramp of its own, because you have said what each leg is flown at, not where inside it the level changes — and a sheet that guesses prints a TOC at every waypoint. The descent onto the destination is the ramp at the other end.
Climb and descent are bounded by time, not distance: a rate and a height. Their distance is whatever ground speed they make, which is why a climb can outlast the first leg and carry on down the next leg's track.
A sheet that is merely short still adds up, and says nothing about why. So:
- a crosswind stronger than the airspeed prints no solution in the ground-speed cell;
- a climb with no rate of climb prints no rate;
- a descent that does not fit in the route marks its last row, because the aeroplane arrives above the field;
and a note under the sheet says which of these happened, in the colour the app warns in.
Typed, because an exercise hands you one. Sorted by altitude, however it arrives.
Add a level adds a level this flight actually reads its air at — the first of the altitudes the sheet asks the forecast for that has no row yet, falling back to a thousand feet above the top once the flight is covered. The new row's temperature is ISA for that altitude, not a flat 15 °C: every true airspeed on the sheet comes out of that column, so a wrong number there is a wrong number everywhere. The wind stays calm, which claims nothing. The caret lands in the new row's altitude, and the button says which level it is about to add.
Fetch forecast fills it from the same Open-Meteo pressure-level data the wind overlay and the density-altitude panel use — at the middle of the route, at the hour the shared look-ahead clock is pointing at, for exactly the altitudes the sheet reads its air at (every row's pressure altitude, bracketed by one level above and below so the nearest-row lookup always has a neighbour). Planning is done for a departure that has not happened yet, which is why the slider and not "now" decides.
It fills the table, never the arithmetic directly: what was fetched is then visible, editable and saved, and each temperature and wind cell says in its tooltip where it came from — the typed table, the route wind, or the standard atmosphere. A forecast a pilot cannot see or correct is not planning data. A fetch that fails leaves the table exactly as it was.
Twelve marks at the 30° headings a card is swung on, laid out two pairs of columns wide as it is printed. Only the "steer" column is typed, in three digits like the magnetic column beside it, and it stays on the circle: type 364 and it reads 004, step up from 359 and it reads 000. There is no maximum on the box, because a maximum would make the spinner stop at 359 and a compass does not stop.
Deviation starts at zero everywhere: a made-up card looks exactly like data. Each steer box has its own ↺, lit only while that heading carries a deviation, and the one beside the card's title clears the whole table back to steer-what-you-are-told. Emptying a box does the same for its row.
| Field | Where it comes from |
|---|---|
| Departure / destination elevation | the airfield dataset, when the endpoint is a field we know |
| Cruise altitude | the route's own legs — there is no field for it, because two places to set one altitude is two numbers to disagree |
| Variation | the app's own magneticVariationDeg, negated: the app signs it magnetic = true + variation, so its −5 is 5E on a sheet |
| Cruise burn | the aircraft profile |
A typed value is a decision and outlives the route. The ↺ beside a field hands it back to what the app knows; it is always there, dimmed while there is nothing to undo, and emptying the box does the same thing.
The exercise file — the route (5 points, legs at 6,000 ft), the met table, the compass card, 4°E variation, and the climb and descent figures the sheet states. Open it with Import/Export → Open: the route lands on the map with its altitudes, and the form fills in.
Saving it. GitHub serves that link as plain text, so a browser shows it rather than
downloading it — right-click the link → Save link as…, or open it and save the page. Or copy
it out of the block below into a file ending in .json; the app reads the text, not the name.
The file, to copy
{
"waypoints": [
{
"lat": 32.17944,
"lng": 34.83444,
"name": "LLHZ"
},
{
"lat": 32.5,
"lng": 35,
"name": "א"
},
{
"lat": 32.66667,
"lng": 35.5,
"name": "ב"
},
{
"lat": 32.96667,
"lng": 35.16667,
"name": "ג"
},
{
"lat": 32.98111,
"lng": 35.57194,
"name": "LLIB"
}
],
"legs": [
{
"inboundAltitude": 6000,
"outboundAltitude": 6000,
"flightSpeed": 90,
"outboundSpeed": 90,
"inLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"outLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"speedAuto": 1,
"retSpeedAuto": 1,
"cumLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"cumLabelRet": {
"a": 0,
"_default": 1,
"_m": 1
}
},
{
"inboundAltitude": 6000,
"outboundAltitude": 6000,
"flightSpeed": 90,
"outboundSpeed": 90,
"inLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"outLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"speedAuto": 1,
"retSpeedAuto": 1,
"cumLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"cumLabelRet": {
"a": 0,
"_default": 1,
"_m": 1
}
},
{
"inboundAltitude": 6000,
"outboundAltitude": 6000,
"flightSpeed": 90,
"outboundSpeed": 90,
"inLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"outLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"speedAuto": 1,
"retSpeedAuto": 1,
"cumLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"cumLabelRet": {
"a": 0,
"_default": 1,
"_m": 1
}
},
{
"inboundAltitude": 6000,
"outboundAltitude": 6000,
"flightSpeed": 90,
"outboundSpeed": 90,
"inLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"outLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"speedAuto": 1,
"retSpeedAuto": 1,
"cumLabel": {
"a": 0,
"_default": 1,
"_m": 1
},
"cumLabelRet": {
"a": 0,
"_default": 1,
"_m": 1
}
}
],
"notes": [],
"title": "Herzliya - Rosh Pina (CVFR exercise)",
"navlog": {
"cruiseAltFt": 6000,
"variationDeg": 4,
"depElevFt": 100,
"destElevFt": 900,
"cas": {
"climb": 70,
"cruise": 90,
"descent": 100
},
"rates": {
"climbFpm": 800,
"descentFpm": 1000
},
"fuel": {
"climbGal": 7,
"cruiseGph": 8
},
"met": [
{
"alt": 2000,
"dir": 315,
"kt": 17,
"tempC": 10
},
{
"alt": 3000,
"dir": 290,
"kt": 17,
"tempC": 8
},
{
"alt": 4000,
"dir": 300,
"kt": 20,
"tempC": 6
},
{
"alt": 5000,
"dir": 300,
"kt": 22,
"tempC": 4
},
{
"alt": 6000,
"dir": 320,
"kt": 25,
"tempC": 2
},
{
"alt": 7000,
"dir": 325,
"kt": 27,
"tempC": 0
}
],
"deviation": [
{
"mh": 0,
"ch": 358
},
{
"mh": 30,
"ch": 27
},
{
"mh": 60,
"ch": 59
},
{
"mh": 90,
"ch": 90
},
{
"mh": 120,
"ch": 122
},
{
"mh": 150,
"ch": 152
},
{
"mh": 180,
"ch": 180
},
{
"mh": 210,
"ch": 209
},
{
"mh": 240,
"ch": 241
},
{
"mh": 270,
"ch": 272
},
{
"mh": 300,
"ch": 300
},
{
"mh": 330,
"ch": 332
}
]
}
}It is the sheet the arithmetic here was checked against, so what comes out should match the published answers cell for cell — 74.2 / 98.3 / 105.2 KCAS→TAS, pressure altitudes 4033 and 3450, TOC inside the first leg, 14.4 gallons at the bottom.
An exercise is a file: a route, a met table, a compass card and a set of assumptions. It opens
through the app's own Open, in Import/Export — one door, one place to look. A document
carrying a navlog block is recognised there, and if it also validates as a route the route takes
the ordinary route path first, so the plan lands complete with its legs and planned altitudes.
Save as a file writes the same shape back.
It is somebody else's JSON, so it is read as such: coordinates must be coordinates, the met table is capped at 60 levels and the card at 72 entries, names are clipped, and a file with neither a route nor a single assumption in it is refused with a toast rather than half-loaded. Replacing a drawn route is asked first — and saying no still lands the met table and the card.
Dragged by its title bar and remembered where you put it (navaid.navlogPos), resizable by its
corner, and it comes back after a reload or a language switch like every other chart window.
Opening the flight plan closes it: one chart on screen.
It opens at the sheet's width rather than the screen's, and the sheet scrolls inside the window while the title and its ✕ stay put. The 23 columns scroll sideways on a narrow screen, with a shadow at whichever edge has more table past it — a phone's scrollbar only appears while it is moving, so without that the sheet simply looked cut off.
The figures line up on an edge rather than centred, and the columns are ruled into the three things they are: what you were given (to TAS), the wind triangle worked through it (to CH), and what comes out (from GS). Every abbreviated heading — TT, FF, Dev — names itself in a tooltip.
| Key | Default | What it does |
|---|---|---|
featureNavLog |
true |
Offer the form at all. The gist can withdraw it |
magneticVariationDeg |
-5 |
The variation the form starts from |
The arithmetic is in docs/app/core.js — navLogRows() and the pure functions around it
(tasFromCas, navLogSegmentAltFt, navLogMetRow, navLogDeviation, navLogHeadings), with no
DOM and no storage. The window is docs/app/navlog.js. navaid.navlog holds only what was typed:
everything else is re-derived, which is what lets an untouched elevation follow the route.
tests/navlog-golden.spec.js reproduces a published exercise cell by cell from
tests/fixtures/navlog-herzliya-rosh-pina.json; navlog-rules.spec.js covers each rule and the
cases a real route does not reach; navlog-view.spec.js drives the window.