acType ReferencePtable ReferenceCgEnvelope ReferenceunitTest ReferenceAn 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:
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.
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.
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.
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.
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 "localStorage keys, so it must be changed everywhere:
source/app/<appId>/index.htmlapple-mobile-web-app-title meta (<appId> Perf), the description meta, the <title> (<appId> Performance), the -t1 <appId> in the globalIconArgs and faviconGlobalArgs comments, and the pohSplash_text1 splash text.source/app/<appId>/serviceWorker.jsconst app = '<appId>';source/app/<appId>/buildStamp/manifest.jsonname, short_name, and description.source/app/<appId>/buildStamp/app.jsappTitle, appId, appVersion, and versionText. See app.js.source/app/<appId>/buildStamp/aircraftData/typeData.js/image/<appId>-top-view.png.source/service/<appId>/.htaccessSetEnv POH_APP <appId> and RewriteBase /<appId>/. The common PHP services take the app from POH_APP, so nothing else on the server changes.source/cdv/<appId>/config.xmlid (com.pohperformance.<appId>), the <name> (exactly <appId> Performance, which the release build relies on), the short name, the description, and the -t1 <appId> in the icon and splash arguments.source/airportData/dbFlags/<appId>.txt-l <ft> sets the shortest runway the database keeps, -t keeps only hard and turf runways, and -w adds water runways. Choose a length that any model of the type could use in the best conditions. See Building the airport database.doc/dev/directory_layout.mdDelete the copied app's model files, leaving typeData.js and options.js to be rewritten. Also remove:
buildStamp/image/<srcId>-top-view.png and any other images the new app doesn't use. Add buildStamp/image/<appId>-top-view.png; see Graphics.page/aircraft.js. The copied upgradeSavedState() converts the old app's saved state to the current version; a new app has no users, so it reduces to return (true), and upgradeToV5(), upgradeAircraftToV5(), upgradeTripsToV5(), the ID translations, and their tests can be deleted.source/image/<appId>/, so that make image makes new ones with the new name. See Per-app-only targets.source/app/<appId>/externalData/ and buildStamp/internalData/airportData.js, which make <appId>.db rebuilds.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.
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 can be transcribed with an AI assistant, such as Claude, working from page images:
pdftoppm -r 300. Render smudged or small digits at a higher resolution, up to 1200 dpi, rather than guessing.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.test spec so the "Check aircraft data" self-test checks it for monotonicity. See Model tests.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 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.
The app is a pilot tool, and every number in it must be verifiable against the POH:
Ptable input or output a units key, such as units:'mph' or units:'dF', so the Ptable converts them when it is used. See Units.mphToKt(55 - 6) with a comment giving the page and the reason, never the computed result.limit, an input outside the table is an error. clamp, clampLo, and clampHi pin it to the table's edge, and offset extends the nearest line, as a pilot would. offset is valid only when the output has the same key as the first input, as in an adjustment table. See Range errors.rndMult rounds after the unit conversion. A table in pounds per hour with rndMult:1 rounds to whole gallons per hour. A factor table must not have a coarse rndMult.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.
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:
maxAltitude, maxXwindfuelPtable of arm by quantity.wbCgEnvelope, the standard empty weight CG, the seats and loads, and renderPayload(). See Weight and balance.astkoclimbclimb.oei.cruisedescent, ldg, glide, and for some types hold.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.
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.
When models share data, the later model holds only what differs:
modelDataMergeUnder(childData, parentData) fills in everything the child doesn't define from the parent. Use it when a model inherits whole sub-objects, such as wb or cruise.modelDataMergeOver(data, methods) overlays methods or data onto a model, such as a model's methods merged into its data.fuel: parentData.fuel, is fine for a few individual tables.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.
The bottom of each model file has its tests. A model needs:
test specstest property declares the trend each input must show, such as monoIncr for a ground roll by pressure altitude. Use a spec shared from common/aero/ptableTests.js, such as test: distancePtableTest. The "Check aircraft data" self-test checks every table that has one. See Self-test: Check aircraft data.unitTest.create('<model>', ...) group that calls the model's methods with an OutsideConditions, such as acData.tko.roll(conditions), and compares the results with the POH. Use the POH's worked examples where it has them, but the tables are authoritative when the two disagree. The type data's test batteries, such as testModelEnvelope(), check every output over the envelope.Page tests, the third kind, are described under Page tests.
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:
Ptable.defaultUnits().modelDataMergeOver() and modelDataMergeUnder() merge. Add any sub-object the new type has, such as climb.oei.tankIds, seatIds, seatLoadIds, and cargoIds: every fuel tank, seat, and load of every model. The weight and balance page creates a state variable for each.risk, the Risk page thresholds. risk.singleEngine is true for a single and false for a twin.weatherRadius, reserveClimb_ft, the runway surfaces the POH covers, the flap positions, and serialFixedWidth.fuel, wb, as, tko, climb, cruise, descent, ldg, and glide. Methods every model must supply call noMethod(), which reports the missing method.testModelEnvelope() and testModelCoverage(), and the table builders the model files import.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.
tko, and climb.oei for the single-engine speeds, rate, fuel flow, and ceiling.TBM also has cold start data.cruise.findPowerSettings() chooses manifold pressure and RPM by the selected power style.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.
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.jsbuildStamp/app.js imports the pages and starts the app. Change:
appTitle, appId, appVersion, and versionText../page/oei.js; an app with weather radar imports ./common/page/radar.js.navProps.layout, which places the pages in the navigation. A twin adds the OEI page; radar adds rdr.settings.setCards(), the Settings page cards.help.toc and the about and start help topics: the aircraft description, the supported models, and the acknowledgments.fuelUnitsSvInfo, as in the Kodiak.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.
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.
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.
page/departure.js, page/enroute.js, page/destination.js, page/approachProcedure.js, and page/departureProcedure.js differ most between apps. Look for:
acData.hasCsProp.keyErrorMap, the messages for a Ptable input out of range.A twin also has page/oei.js, which computes the single-engine driftdown from climb.oei.
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.
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.
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.
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:
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.
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.