A ForeFlight content pack that draws FAA textual obstacle departure procedures (ODPs) as a planview KML layer, nationwide. ForeFlight shows a textual ODP only as text in the FAA's Takeoff Minimums PDF under Airport → Procedures; this pack extracts that text from the born-digital PDF, parses it against NASR data, then draws the routing — climb, turns, and navaid tracking — directly on the map. Rebuilt every 28-day NASR cycle by GitHub Actions.
On the iOS device that has ForeFlight installed, open this link in Safari:
When the preview appears, long-press or pull the page down and tap Open in ForeFlight. The pack downloads into More → Downloads and the layer turns on under Maps → Layers.
The link goes through GitHub's releases/latest redirect, so bookmarking
it once keeps pulling the current NASR cycle — when ForeFlight flags the
pack as expired (every 28 days), just tap it again. A daily CI job polls
the FAA and publishes a new release within a day of each cycle's effective
date.
For each airport with a DEPARTURE PROCEDURE or VCOA section that resolves with certainty:
- Initial climb, climbing turns, and turns at a fix or altitude.
- Direct-to-navaid and heading legs flown to an altitude, including
"climb runway heading", which follows the runway's own course
(
rwy hdg), and a climb that names no heading at all ("climb to 1200 before turning left"), drawn straight out along the runway and labelled with its altitude alone. - Radial intercepts and tracking to an altitude, fix, DME distance, or the navaid; an altitude climbed to on the way to a fix ("to 3000 via FSM R-064 to FSM VORTAC") is labelled where the leg ends. A heading printed with no terminator is held until the radial that follows it is intercepted ("heading 170°, thence ... on RZS R-185"). A turn to intercept a radial with no heading printed ("climbing left turn to intercept PUB R-274") turns the published way onto a schematic, unlabelled 45° intercept heading, on the side of the radial the turn leaves the aircraft. A turn that would roll out on the radial's course within 0.5 NM of it rolls out there and converges on the radial at 20°. It is not drawn when no turn direction is printed, when the published turn onto the 45° or 20° heading and the one onto the radial's course differ by more than 45° (as for a route already converging on the radial), nor when that turn exceeds 270°, so a short turn the other way would reach the heading.
- "Proceed on course" after a runway's legs as a short dashed stub, bent toward any published turn restriction. A published direction ("before proceeding northbound", read against true north) turns the stub onto it in dashes, the shorter way unless the text names the side; two directions ("before turning west or northwest") name no one course, so the route ends without a stub. A VCOA that only proceeds on course draws just its circle, and the stub leaving it when the text names one direction.
- Alternative climbs for one runway that differ only in altitude and turn direction ("climb heading 167° to 2700 before turning right. Climb heading 167° to 3400 before turning left."), each drawn to its own stub.
- Climb-in-hold racetracks.
- Heading ranges ("climb on a heading between 350° CW to 162° from DER")
as a fan from the turn-start point, beside the route flown on "all
other courses": the route splits there into a line along each limiting
heading, each turning off the arriving course as any other turn does
(the published way; else the way that stays within the range, where only
one does; else the shorter) and ending in an arrowhead, with a
light dashed arc between them. Each fan is labelled with its runways
and the range as printed, keeping every heading, CW/CCW, printed turn
and altitude, on as few lines as ForeFlight shows whole (it elides the
middle of a label over 22 characters): the runways lead the first line
(
35L/R hdg 313° CW 172°), which drops itshdgonly to make room for them or to save a line (16L/R 213° CCW 353°,11 320° CW 220° 3000'); otherwise they stand on a line of their own. Alternative sectors stay stacked under the first (25 hdg 317° CW 083°overor 206° CCW 083°). Parallel runways that depart alike share one fan, drawn from between their turn-start points. The label stands in or just beyond its own fan, clear of other labels, lines and fans where it can. A diverse departure limited to a sector ("diverse departures authorized 300° to 120° CW") is drawn the same way. A minimum climb gradient given for the other headings instead ("or min. climb of 415 ft per NM to 1600 for headings 101° through 314°") names no route, so only the fan is drawn. - VCOA (visual climb over airport) circles, including airports whose only
procedure is a VCOA and visual climbs written into the departure
procedure itself ("..., or for climb in visual conditions: cross ...").
A published crossing direction joins the label where it still fits in
ForeFlight's 22 characters (
VCOA (≥8200' SE bound)), and a speed limit on the visual climb is labelled below the circle. The label stands at the circle's north point; a second altitude around the same circle stands at its south point (then east, then west), never on top of it. - Altitude labels in plain text (ForeFlight renders neither rich text nor
combining marks):
7000'for "climb to,"≥7000'for "at or above,"≤7000'for "at or below," andat 7000'for a mandatory altitude; holds readHold 246° RT ≥9300'(inbound course, turns, altitude), orHold 246° RT ≥MEA/MCAwhen the text gives an en-route minimum rather than a figure (≥4000/MEAfor "at or above 4000 or MEA"). With--label-style fmsthey take the FMS form:AandBsuffixes, and plain digits for "climb to" and "at." - Labels for everything a pilot programs: headings (
hdg 120°), radials with their navaid (SAU R-035), DME terminators (BAM 10 DME), and speed limits (≤200 KIAS until 9000', or just≤200 KIASwhen the limit ends anywhere but at an altitude). Fixes aren't labelled because ForeFlight's own chart names them. Leg lengths aren't labelled. - Each airport's lines take one of six colors, chosen so that no two airports within 40 NM share a color. Where an airport draws routes for more than one runway group, each group's lines take their own shade of that color, and the lines the routes share (a shared tail, a VCOA) keep the color itself. Labels are drawn in the airport's color too.
Not drawn: takeoff minimums (ceiling/visibility and climb gradient tables), obstacle notes, diverse vector areas (DVAs), procedures that reference a graphic ODP instead of text ("use LUNDI DEPARTURE"), and airway routing after the ODP ends. A runway that flies a graphic ODP is drawn from none of its text; an airport whose every runway does is left out of the coverage percentage and counted on its own in the report, since ForeFlight charts graphic ODPs itself. A speed limit printed in a runway's takeoff minimums ("do not exceed 210K until intercepting the ENI R-073") is not read either, so it withholds that runway's route and visual climb.
A route is drawn only when the pipeline is certain of it: its text,
and every sentence that may apply to it, parses completely, every navaid
and fix reference resolves to exactly one match in NASR, and the geometry
hits no degenerate case (a radial parallel to a course, a turn with no
defined radius, and so on). An unread sentence under a runway's header
applies to that runway's routes and VCOAs, including a VCOA for all
runways. One before the first runway header, after the last, or ending
in a colon ("When tethered balloon ... is flying:") may apply to every
departure, so nothing at that airport is drawn. Anything less certain is
never drawn; it becomes a Finding instead, listed in
report.md on the release for that cycle and filed as a GitHub issue,
grouped by failure signature so one recurring problem across airports and
cycles produces one issue. Coverage improves cycle by cycle as those
findings get fixed.
Certainty is judged one runway route at a time: each runway's route,
together with the shared tail it continues into, and each VCOA. When some
of an airport's routes are certain and others are not, the certain ones
are drawn and a label near the airport names the rest (ODP NOT SHOWN
over RWY 17L/17R, VCOA), so a missing line is never read as a runway
without an ODP. The parts are packed into lines ForeFlight shows whole,
and the label stands within 2 NM of the airport where it is least
cluttered by the airport's lines, labels and name. An airport whose ODP
text draws nothing at all gets the label alone (ODP NOT SHOWN), in the
first palette color, so its empty map is never read as an airport
without an ODP. A route is never drawn in part, and the report
counts airports drawn in part separately.
This pack is an educational aid, not a tool for navigation. The published FAA text and charts govern; always fly from those, not from this layer.
Every drawn shape follows the same aircraft and rendering assumptions, used whenever the text does not otherwise constrain the geometry:
| Assumption | Value |
|---|---|
| Display true airspeed | 150 kt |
| Turn radius | 25° bank or standard rate, whichever is larger |
| Turn start | 400 ft above the DER (departure end of runway) |
| Climb gradient for altitude legs | Published minimum up to the altitude it is published to, 200 ft/NM above it or when none is published |
| Climb naming no heading or route ("climb to 1200 before turning left") | Straight out along the runway's course while every earlier leg has held it; refused anywhere else |
| Intercept heading not printed ("climbing left turn to intercept PUB R-274") | 45° to the radial, on the side of it the turn leaves the aircraft: the optimum angle FAA Order 8260.3F ¶13-3-3b(2) takes "when a heading is not defined" (minimum 15°, optimum 30°–45°). Not drawn if the turn onto it exceeds 270°, or differs by more than 45° from the turn onto the radial's course |
| Turn rolling out close beside the radial to intercept | Within 0.5 NM of it: roll out on the radial's course, then converge on the radial at 20° |
| Printed heading already on its radial | Joined there when a runway's route has rolled out within 0.5 NM and 3° of the radial, inside the half-scale deflection at which a pilot counts as established on a course |
| Radial sense not printed, flown to an altitude | Within 60° of the printed heading, else of a runway route's course, or 60°–120° into its stated turn (all routes into a shared tail agreeing) |
| Course printed as a compass direction ("before proceeding northbound") | The direction's true bearing, turned onto the shorter way unless the side is printed; no stub for two directions ("west or northwest") |
| Hold leg length | 1 minute (1.5 minutes above 14,000 ft; 1 minute to an unstated MEA, MCA or MOCA) |
| VCOA circle | 2 NM schematic radius |
| Heading-range wedge | 1 NM schematic radius from the turn start, or 0.3 NM beyond where the widest turn onto a limiting heading ends |
| Wedge edges | Turn off the arriving course at the standard turn radius, like every other turn: the published way, else the way that passes only through headings inside the range where just one does, else the shorter |
| Shared heading-range wedge | Turn starts within 1.5 NM of each other |
| Wedge label clearance | Labels sized for about 20 NM across an iPad screen |
| Runway-group shades | Same perceived lightness as the airport's color; hue within 15° of it and at most a third of the way to a comparably vivid palette color; chroma down to 60% from four groups on |
| Magnetic headings | Airport's variation of record (WMM if unpublished) |
| Magnetic radials | Referenced navaid's own station declination |
- FAA d-TPP Takeoff Minimums PDFs and metafile (aeronav.faa.gov/d-tpp) for the procedure text.
- FAA NASR 28-day subscription CSV — APT (airports), NAV (navaids), FIX (fixes), and HPF (holding patterns) — for identifiers, positions, and magnetic variation.
- FAA Orders 8260.3G (TERPS) and 8260.46K, and Instrument Flight Procedures Information Bulletin (IFP IB / IAC) 7, for the construction rules a textual ODP follows. The intercept-angle rule quoted under Depiction assumptions comes from the previous edition, 8260.3F ¶13-3-3b(2).
FAA aeronautical data is in the public domain.
pyenv virtualenv 3.14.7 odp
pyenv activate odp
pip install -r requirements-dev.txt
brew install poppler# Resolve and print the current and next NASR cycle
python generate_odp_kml.py --print-cycles
# Build a KML for a couple of airports only, skipping the pack wrapper
python generate_odp_kml.py --airports TPH,ALB --kml-only
# Full nationwide build (writes "Textual ODPs.zip" and report.json/.md)
python generate_odp_kml.py -v
# Also dump each airport's normalized sections (input to draft_golden.py)
python generate_odp_kml.py --dump-sections sections.jsonRun the test suite and linter before committing:
pytest
ruff check --fix . && ruff format ..github/workflows/build-pack.yml polls the FAA daily. The currently
effective NASR cycle is published as the latest GitHub release; the
upcoming cycle, once its data is available, is published as a prerelease
and promoted to latest on its effective date. The workflow keeps the six
most recent cycle releases and prunes older ones, and syncs findings from
the current cycle's build into GitHub issues.
GitHub disables a scheduled workflow after 60 days with no repository activity — push a commit (or run it manually) if releases stop appearing.
tools/compare_builds.py checks a grammar or geometry change against every
airport in a cycle, not just the fixtures. It snapshots each airport's
outcome (drawn whole, drawn in part, charted DPs only, or not drawn), its
findings and a digest of its shapes, then names every airport gained, lost
or redrawn between two snapshots:
python tools/compare_builds.py snapshot --cycle 2026-09-03 --out before.json
# ... change the code ...
python tools/compare_builds.py snapshot --cycle 2026-09-03 --out after.json
python tools/compare_builds.py diff before.json after.json --state COIt reuses the build's data_cache and is never imported by, or run as part
of, the build.
tools/draft_golden.py is an offline aid for drafting golden-fixture ASTs
from real ODP text, using the Claude API (ANTHROPIC_API_KEY required;
pip install -r requirements-tools.txt). It drafts a parse for each
airport's section and checks it mechanically, but every draft is reviewed
by a human before it becomes a fixture in tests/fixtures/golden/. The
test suite then requires the grammar to parse each fixture's text to exactly
its reviewed AST, or to refuse it. The tool is never imported by, or run as
part of, the build.
Unlicense — public domain. Provided as-is, with no warranty; verify any procedure against current FAA publications before using it for navigation.