How to Make a New Aircraft App

Table of contents


Overview

An aircraft app turns the performance section of one or more POHs into calculations for each phase of flight. Every aircraft app has the same structure, and most of each app's code is shared with the others, so a new app starts as a copy of an existing one. The steps are:

  1. Gather the documents for every model and option the app will support.
  2. Duplicate a similar app: a single for a single, a twin for a twin.
  3. Digitize the performance data: AI transcription for tables, the Digitizer for graphs.
  4. Create the model files that hold each model's data.
  5. Create the type data, the data and methods shared by every model.
  6. Create the options for modifications and optional equipment.
  7. Customize the app pages.
  8. Test the app against the POH.

In this guide <appId> is the new app's ID, typically the first word of the app's name, such as C182 for "C182 Performance", and <srcId> is the app it is copied from. Commands are run in the version5/ directory.

Gather the documents

Collect every POH, AFM, or PIM the app will support before writing any code: one for each model and serial number range. Also gather the supplements (AFMS) for the modifications and optional equipment, such as STC kits, floats, or engine conversions. Prefer the cleanest scans available, and note the revision of each.

Then work out how the documents relate:

The documents are not kept in the source repository as they are copyrighted material. POH Performance apps keep these in a separate repository. Every table and value in the app records the document and page it came from, so keep them where a reviewer can find them.

Duplicate a similar app

Choosing the app to copy

Copy the existing app closest to the new aircraft:

New aircraft Copy
Fixed-gear single piston C182, or C172 for a mix of fixed-pitch and constant-speed propellers
Retractable single piston Bonanza or PA28
Piston twin Baron or C310
Turboprop single Kodiak or TBM

Singles should be copied from singles and twins from twins. A twin has a One Engine Inoperative page (page/oei.js), accelerate-stop and accelerate-go data, single-engine climb data under climb.oei, and risk.singleEngine: false; a single has none of these. A turbine works in torque rather than manifold pressure and RPM.

Copying the files

An app's files are spread over several directories. Copy all of them:

src=C182 new=C206
cp -R source/app/$src source/app/$new
cp -R source/service/$src source/service/$new
cp -R source/cdv/$src source/cdv/$new
cp -R source/image/$src source/image/$new
cp source/airportData/dbFlags/$src.txt source/airportData/dbFlags/$new.txt

Then add the app to the app lists in the Makefile: apps, aircraftApps, testApps, and pubApps once it is ready to publish. An aircraft app is built by app.mk, so most aircraft apps need no other makefile. Some apps may need a custom <app>.mk makefile. See XXX.

Renaming the app

Choose an app ID for the app. Often this the type name (e.g., Bonanza) or the type designation (e.g., PA28). You should also pick a full app title. POH Performance apps use " Performance". The app ID is also the name of the app's directories, its server path, and its localStorage keys, so it must be changed everywhere:

Removing the old aircraft

Delete the copied app's model files, leaving typeData.js and options.js to be rewritten. Also remove:

First build

make <appId>.db
make <appId>.image
make <appId>.web

make <appId>.db builds the app's airport database from the source data already downloaded, and make <appId>.image generates the app's icons, splash screens, and store icons. The web build needs both. Once the model files exist, make serve runs the app; see Testing and debugging apps.

Digitize the performance data

The performance data for each model or option is captured in a single JS object. The object is often quite large to handle all the data, so app usually organize it into sub-objects. Here's a typical layout:

{
  maxAltitude: ...,   // Maximum altitude for perf data
  maxXwind: ...,      // Maximum crosswind component
  fuel: {...},        // Capacity and arms for each fuel tanks, taxi fuel
  wb: {...},          // Weight and balance: CG envelope, cabin configuration
  as: {...},          // Airspeed calibration, stall speeds
  tko: {...},         // Takeoff data
  climb: {...},       // Climb data. Vx, Vy, cruise or maxRate rate/time/fuel/distance
  cruise: {...},      // Cruise TAS, fuel flow, power
  descent: {...},     // Descent data. Vx, Vy, cruise or maxRate rate/time/fuel/distance
  ldg: {...},         // Landing data
  hold: {...},        // Holding data.
  glide: {...},       // Glide ratio and airspeeds.
}

Performance tables and graphs are captured using Ptable objects. See the Ptable Reference for more information. These provide a flexible way of capturing multi-dimensional data and interpolating a result.

In general, the Ptable data should be entered and arranged so as to make it easy to verify against a source document. For example, the values in a table column or the samples for a graph curve should be sequential. You may have to choose a particular order of inputs that matches the sources. Patbles also provides unit conversion, so the app may compute in knots, yet you may enter data in miles per hour to accommodate older models. In most cases you can avoid any arithmetic on the source value. However, some cases require it. In those cases, make the arithmetic explicit in the code so that the number in the source document is preserved. Here's an example of a typical runway wind adjustment factor:

rollWindFactorTbl: new Ptable({
  title: `Takeoff ground roll wind factor`,
  inputs: [{key:'headwind'}],
  a: [
    {p:-10, v:1 - (-10 * 0.10 / 2)},  // +10% per 2kts tailwind
    {p:0,   v:1},
    {p:30,  v:1 - (30 * 0.10 / 9)},   // -10% per 9kts headwind
  ],
}),

Tables

Tables can be transcribed with an AI assistant, such as Claude, working from page images:

  1. Render the POH pages to images, for example with pdftoppm -r 300. Render smudged or small digits at a higher resolution, up to 1200 dpi, rather than guessing.
  2. Have the assistant transcribe each table directly into a Ptable in the model file, in the POH's units, with the POH page in a comment. Give it an existing model file as the pattern to follow. You may have to instruct the assistant to reorder inputs so that the values appear in an order that's easy to check against the source.
  3. Check each table:

When two documents share some rows, transcribe the shared rows once, prove they are equal by spot checks, and transcribe only the rows that differ.

The Digitizer can also read a table, by OCR, when the scan is clean.

Graphs

Graphs are digitized with the POH Performance Digitizer. Calibrate the graph's grid, name the inputs and output with the standard Ptable keys, sample along the published lines, and export the result as a Ptable to paste into the model file. Use one Digitizer project per POH and keep the .ppdig project files in digitized/<appId>/: they record how each table was sampled, and Import Ptable reloads a table onto its graph to check it later.

Rules for performance data

The app is a pilot tool, and every number in it must be verifiable against the POH:

Create the model files

Each internal model is a file in buildStamp/aircraftData/, usually one per POH, that holds the model's data and its tests. A model may serve several POHs whose data is identical, with one createModel() call for each; see C182Q.js. A large type may group its files in directories, as the Bonanza does.

The model data object

A model's data is an object whose sub-objects are the phases of flight. The C182S model in C182/buildStamp/aircraftData/C182S.js is a complete example:

The base methods that compute the results from these tables, such as tko.roll(conditions), are in the type data, and a model overrides only the methods its data needs. maxAltitude must be no higher than the top of both the model's climb and its cruise tables: the enroute calculation climbs to the cruise altitude, so a ceiling above the climb data leaves the cruise outputs blank.

Creating the model

The file ends by creating the model:

acType.createModel({
	modelId: 'C182S',
	title: '182S',
	dataSource: `Cessna 182S Skylane Information Manual, November 2001`,
	serialNumbers: '80001-80944',
	serialPrefix: '182',
	data: c182sData,
});

The serial numbers are the POH's. See Creating aircraft models and createModel(). Then import the file in page/aircraft.js, after typeData.js and before options.js.

Inheriting from another model

When models share data, the later model holds only what differs:

Inherit a table only when the two POHs publish the same table. A model with its own climb or cruise data declares its own maxAltitude rather than inheriting the parent's.

Model tests

The bottom of each model file has its tests. A model needs:

Page tests, the third kind, are described under Page tests.

Create the type data

buildStamp/aircraftData/typeData.js holds the data and methods shared by every model of the type. It calls acType.setTypeData(), and acType.init() merges it under each model, so the type's methods are every model's defaults. See Type-wide data and setTypeData().

The copied typeData.js is the starting point. Review each part for the new type:

The common code reads some type data directly. typeData.js must provide what the copied app's does, including risk, weatherRadius, maxXwind, the runway surfaces, the aircraft icon, the serial number width, and for a radar app radar.

Singles, twins, and turbines

Create the options

An option is a modification or optional equipment that changes a model's data, such as a speed kit, floats, or a gross weight increase. Options are created in buildStamp/aircraftData/options.js, or in the model file when they apply to only one model:

acType.createOption({
	title: `Factory Air Conditioning system`,
	optionId: 'FactoryAirConditioner',
	optionOn: 'G58:-;^58:-',
	optClass: 'airConditioner',
	includes: 'AirConditioner',
	dataSource: `Beechcraft Model G58 ... Supplement for the Air Conditioning System ...`,
	data: (modelData) => ({
		cruise: {
			tasAcFactor: (1 - 0.03),		// 3% degradation in speed and range
			...
		},
	}),
});

optionOn is an applicability string giving the models and serial numbers that can have the option, and installedOn which have it as standard. An option's data is merged over the model's, so it holds only the tables and methods it changes. Options in the same optClass exclude each other, includes installs another option, and incompatibleWith and enabledBy express the other relations. See Options and createOption().

The aircraft page lists only the options the user has a choice about: an option installed by the model or included by another option is not shown. A greyed-out option is one that another selection has ruled out.

Test an option by the values it changes, not by its presence in acData.options.installed, which doesn't prove its data was merged. Pick inputs where the option's table differs from the model's and check the value through acData. Use setAc(), selectAcOption(), and unselectAcOption() from page/aircraft.js to set up the aircraft, since they complete the merge.

Customize the app pages

Most of each page is shared by every aircraft app. What differs is the aircraft-specific presentation, the option-dependent cards, the help text, and the tests.

The aircraft pages are near-copies across the apps, so a fix made in one app usually belongs in all of them. Check the other apps when changing a copied page.

app.js

buildStamp/app.js imports the pages and starts the app. Change:

buildStamp/svFormat.js holds the formats of widely-used state variables, such as the fuel quantity and weight limits. Adjust their ranges for the new aircraft.

Aircraft page

page/aircraft.js defines the aircraft model, serial number, and options inputs. Change the model file imports, the default model (dflt), the default serial prefix, and the empty weight and arm ranges. Its "Aircraft" test sets up every model in turn and checks that no page has an invalid input or output; it should need few changes.

Weight and balance

The weight and balance data is in the model files, not the page. wb.seats and wb.loads define the seats and loads, wb.combinedWeightLimits the shared compartment limits, and wb.renderPayload() lays them out with the WbSeatRow, WbBenchRow, and WbCargoRow components that page/weightBalance.js defines. The page draws the CgEnvelope with the takeoff, landing, and zero fuel CGs. A new seating arrangement usually needs only new data; a different cabin may need new rows in the page.

page/quickSetup.js tripIds() lists the state variables a saved trip records. It must include every seat, load, and tank state variable in typeData.js.

Performance pages

page/departure.js, page/enroute.js, page/destination.js, page/approachProcedure.js, and page/departureProcedure.js differ most between apps. Look for:

A twin also has page/oei.js, which computes the single-engine driftdown from climb.oei.

Pages copied unchanged

page/flightPerformance.js, page/glide.js, page/quickSetup.js (apart from tripIds()), the page CSS files, and the common pages in source/app/common/page/ need no changes beyond help wording. app.js chooses the common pages: risk, hold entry, trips, people, terms of use, settings, and the developer pages, test and Ptable test.

Help text

Each page's help is its renderHelp(). Update the aircraft names and the "Model-specific notes" sections, which explain where the POHs differ. The help is checked by the "Page references" self-test, which fails on a reference to a missing topic.

Page tests

The tests at the bottom of each page set up a scenario as a user would, with setAc(), the empty weight, the occupants, the fuel, and the airports, then check the page's outputs. These are the most complete tests, since they exercise the models, the methods, the tables, and the pages together. Every expected value comes from the copied aircraft, so each must be recomputed from the new POH: weights and CGs, speeds, takeoff and landing distances, and the enroute times, fuel, and true airspeeds. Work out the expected values by hand from the POH tables rather than copying them from the app.

Test the app

make <appId>.test
make lint

make <appId>.test runs the app's self-tests headlessly; see Unit tests. A complete set of tests for a model has all three kinds: page tests, method tests, and the tables' test specs.

Also:

Graphics

The app needs an aircraft top view, which the runway diagrams, the splash screen, and the icons use. Draw it as vector art from a POH or other top view: the Baron's is the standard. The outline should be symmetric about the centerline, apart from features such as trim tabs, with straight leading and trailing edges. Put the full-size image in source/image/<appId>/<appId>-top-view.png and a small copy, about 120 pixels wide, in buildStamp/image/<appId>-top-view.png. Then run make <appId>.image to generate the icons and splash screens. See Graphics.

Native apps

The native iOS and Android apps are built in the private release repository, from the Cordova image that make <appId>.cdv builds. A new app is added to that repository's acApps, versions, and apps/<appId>/ files, as its Makefile documentation describes, and needs a new app record, with the bundle ID com.pohperformance.<appId>, in App Store Connect and the Google Play Console.