Event handlers
onTick(callback)
Called every simulation tick (~0.5 seconds by default).
Parameters:
callback: (ctx: APIContext) => void- Function called each frame
Context Object (ctx):
{
time: number, // Simulation time (seconds)
dt: number, // Time step (seconds, usually 0.5)
traffic: TrafficView,
weather: WeatherView,
sectors: SectorsView,
fixes: FixesView
}Example:
onTick(({ traffic, time }) => {
const flights = traffic.all();
log(`Time: ${time}s, Aircraft: ${flights.length}`);
flights.forEach(ac => {
// Check altitude and issue climb clearance
if (ac.altFL < 300) {
ac.climb(fl(350)); // Climb to FL350
}
});
});onSpawn(callback)
Called when a new aircraft enters your sector.
Parameters:
callback: (aircraft: Aircraft) => void
Example:
onSpawn((aircraft) => {
log(`${aircraft.cs} (${aircraft.type}) at FL${aircraft.altFL}`);
log(`Exit: ${aircraft.plan.exitPoint} at FL${aircraft.plan.exitFL}`);
// Initial clearance
aircraft.speed(400);
// Climb to cruise altitude if needed
if (aircraft.plan.cruiseFL > aircraft.altFL) {
aircraft.climb(fl(aircraft.plan.cruiseFL));
}
// Set up handover at exit fix
aircraft.over(aircraft.plan.exitPoint, (ac) => {
ac.handover();
});
// Ensure aircraft reaches exit altitude before exit fix
if (aircraft.plan.exitAltitude) {
aircraft.reach(aircraft.plan.exitPoint).altitude(aircraft.plan.exitAltitude);
}
});onConflict(callback)
Called when two aircraft are predicted to lose separation (< 5nm lateral or < 1000ft vertical).
Parameters:
callback: (pair: ConflictEvent) => void
ConflictEvent object:
{
a: string, // First aircraft callsign
b: string, // Second aircraft callsign
predMinSepNm: number, // Predicted minimum separation
timeToCPA: number, // Time to closest point (seconds)
severity: string, // "critical" | "warning" | "caution"
cpaVerticalFt: number, // Vertical separation at CPA
advisories: ConflictAdvisories // Resolution advisories (optional)
}ConflictAdvisories object:
{
forA: ResolutionAdvisory[], // Advisories for aircraft A
forB: ResolutionAdvisory[], // Advisories for aircraft B
recommended: { // Best overall resolution
callsign: string,
advisory: ResolutionAdvisory
} | null,
available: boolean // Whether any resolution is possible
}ResolutionAdvisory object:
{
callsign: string, // Aircraft this applies to
resolutionType: string, // "vertical" | "lateral" | "speed"
maneuver: ResolutionManeuver,
effectiveness: number, // 0-1 score (1 = fully resolves)
projectedSeparationNm: number,
projectedSeparationFt: number,
timeToEffect: number // Seconds until separation improves
}ResolutionManeuver Types:
// Vertical
{ type: "climb", targetAltFt: number }
{ type: "descend", targetAltFt: number }
// Lateral
{ type: "turn", direction: "left" | "right", degrees: number, targetHdgDeg: number }
// Speed
{ type: "speed", targetKts: number }Example - Basic conflict handling:
onConflict((pair) => {
log(`Conflict: ${pair.a} and ${pair.b}`);
log(`Min separation: ${pair.predMinSepNm.toFixed(1)}nm in ${pair.timeToCPA}s`);
// Resolve with vertical separation
const acA = traffic.byCallsign(pair.a);
const acB = traffic.byCallsign(pair.b);
if (acA && acB) {
const verticalSep = Math.abs(acA.altFt - acB.altFt);
if (verticalSep < 1000) {
// Climb the higher aircraft
if (acA.altFt > acB.altFt) {
acA.climb(acA.altFt + 2000);
} else {
acB.climb(acB.altFt + 2000);
}
}
}
});Example - Using resolution advisories:
onConflict((pair) => {
// Check if advisories are available
if (!pair.advisories?.available) {
log(`Conflict ${pair.a}/${pair.b} - no resolution available`);
return;
}
// Get the recommended resolution
const rec = pair.advisories.recommended;
if (rec) {
const { callsign, advisory } = rec;
const ac = traffic.byCallsign(callsign);
if (!ac) return;
// Apply the recommended maneuver
switch (advisory.maneuver.type) {
case 'climb':
ac.climb(advisory.maneuver.targetAltFt);
break;
case 'descend':
ac.descend(advisory.maneuver.targetAltFt);
break;
case 'turn':
ac.heading(advisory.maneuver.targetHdgDeg);
break;
case 'speed':
ac.speed(advisory.maneuver.targetKts);
break;
}
log(`Applied ${advisory.resolutionType} resolution to ${callsign}`);
log(`Projected separation: ${advisory.projectedSeparationNm.toFixed(1)}nm`);
}
});Example - Auto-resolution with priority:
// Automatically resolve conflicts, preferring vertical separation
onConflict((pair) => {
if (!pair.advisories?.available) return;
// Find best vertical advisory (most effective)
const allAdvisories = [
...pair.advisories.forA,
...pair.advisories.forB
];
const verticalAdvisories = allAdvisories
.filter(a => a.resolutionType === 'vertical')
.sort((a, b) => b.effectiveness - a.effectiveness);
if (verticalAdvisories.length > 0) {
const best = verticalAdvisories[0];
const ac = traffic.byCallsign(best.callsign);
if (ac && best.maneuver.type === 'climb') {
ac.climb(best.maneuver.targetAltFt);
} else if (ac && best.maneuver.type === 'descend') {
ac.descend(best.maneuver.targetAltFt);
}
}
});onMeterAlert(callback) Not Implemented
Called when demand is high at a metering fix.
Real-world usage: Traffic Management Initiatives (TMI) use metering to control arrival/departure rates and manage sector demand. When too many aircraft converge on a fix, controllers implement miles-in-trail restrictions, speed control, or reroutes to smooth the flow. This can apply to arrival fixes at busy airports, sector boundaries, or any congested waypoint.
Example:
onMeterAlert((fixName) => {
log(`High demand at ${fixName} - implementing metering`);
// Get all aircraft heading to this fix
const inbound = traffic.all().filter(ac =>
ac.plan.route.includes(fixName)
);
// Implement 10nm miles-in-trail restriction
inbound.sort((a, b) => {
const distA = distance(a.pos, fixes.get(fixName).pos);
const distB = distance(b.pos, fixes.get(fixName).pos);
return distA - distB;
});
inbound.forEach((ac, i) => {
if (i > 0) {
const lead = inbound[i - 1];
const spacing = distance(ac.pos, lead.pos);
if (spacing < 10) {
// Reduce speed to achieve 10nm spacing
ac.speed(Math.max(lead.targetIASKts - 40, 250));
}
}
});
});onHandoffRequest(callback) Not Implemented
Called when an aircraft needs handoff to next sector.
Real-world usage: Handoffs are coordinated between controllers, often with verbal or automated coordination. The receiving controller must accept the handoff before the transferring controller switches the aircraft to the new frequency. Controllers verify the aircraft meets all crossing restrictions and separation standards before handoff.
Example:
onHandoffRequest((req) => {
log(`${req.cs} requesting handoff to ${req.nextSector}`);
const ac = traffic.byCallsign(req.cs);
if (!ac) return;
// Check aircraft is stable and meets requirements
const isStable = ac.altFt === ac.targetAltFt;
const meetsAltitude = !ac.plan.exitFL || ac.altFL === ac.plan.exitFL;
const atExitFix = ac.plan.exitPoint &&
distance(ac.pos, fixes.get(ac.plan.exitPoint).pos) < 5;
if (isStable && meetsAltitude && atExitFix) {
ac.handover();
log(`${ac.cs} handed to ${req.nextSector}`);
} else {
log(`${ac.cs} not ready - waiting for compliance`);
// Ensure aircraft meets exit requirements
if (!meetsAltitude && ac.plan.exitFL) {
if (ac.altFL < ac.plan.exitFL) {
ac.climb(fl(ac.plan.exitFL));
} else {
ac.descend(fl(ac.plan.exitFL));
}
}
}
});onDepartureControllable(callback)
Called when a departure aircraft reaches 10,000 feet and becomes controllable.
Real-world usage: In the real world, departure aircraft are handed off from tower to approach/departure control, then to center as they climb. Aircraft below 10,000 feet are typically under local approach control and follow standard departure procedures. Once reaching a certain altitude (often around 10,000 feet), they're handed off to en-route (center) control where they can receive direct routing and altitude changes.
Parameters:
callback: (aircraft: Aircraft) => void- Function called when a departure aircraft reaches 10,000ft
Example:
onDepartureControllable((aircraft) => {
log(`${aircraft.cs} now controllable at FL${aircraft.altFL}`);
// Issue initial en-route clearance
aircraft.climb(fl(aircraft.plan.cruiseFL));
aircraft.speed(420);
// Direct to first waypoint if available
if (aircraft.plan.route.length > 0) {
aircraft.direct(aircraft.plan.route[0]);
}
// Set up handover at exit fix
aircraft.over(aircraft.plan.exitPoint, (ac) => {
ac.handover();
});
// Ensure aircraft reaches exit altitude
if (aircraft.plan.exitAltitude) {
aircraft.reach(aircraft.plan.exitPoint).altitude(aircraft.plan.exitAltitude);
}
});Departure aircraft
Departure aircraft spawn on airport runways and progress through flight phases:
- Takeoff - Accelerating on runway
- Departure - Climbing (NOT controllable until 10,000ft)
- Cruise - Normal en-route (controllable)
During the takeoff and departure phases (below 10,000ft), aircraft:
- Are dimmed in the flight strip panel
- Cannot receive altitude, speed, heading, or route clearances
- Automatically maintain runway heading until 3,000ft
- Follow standard speed restrictions (250 knots below 10,000ft)
Once reaching 10,000ft, the onDepartureControllable event fires and the aircraft transitions to cruise phase.
Airport departure cooldown
Airports have a 2-minute cooldown between departures. This simulates realistic runway usage and departure sequencing.
onApproachCleared(callback)
Called when an aircraft is cleared for an ILS approach.
Parameters:
callback: (event: { cs: string, airport: string, runway: string }) => void
onILSEstablished(callback)
Called when an aircraft establishes on the localizer.
Parameters:
callback: (event: { cs: string, runway: string }) => void
onTowerContact(callback)
Called when an aircraft is handed off to tower.
Parameters:
callback: (event: { cs: string, runway: string }) => void
onLanding(callback)
Called when an aircraft touches down.
Parameters:
callback: (event: { cs: string, airport: string, runway: string }) => void
onGoAround(callback)
Called when an aircraft executes a go-around.
Parameters:
callback: (event: { cs: string, reason: string }) => void
onUnableILS(callback)
Called when an aircraft cannot accept an ILS clearance.
Parameters:
callback: (event: { cs: string, runway: string, reason: string }) => void
Reason values: 'heading', 'too_close', 'too_far', 'too_high', 'wrong_side', 'no_ils'
onAircraftRemoved(callback)
Called when an aircraft leaves the simulation.
Parameters:
callback: (event: { callsign: string, reason: string }) => void
onHandoffOffered(callback)
Called when a handoff is initiated to an adjacent sector.
Parameters:
callback: (event: { cs: string, targetSector: string }) => void
onHandoffAccepted(callback)
Called when an adjacent sector accepts the handoff.
Parameters:
callback: (event: { cs: string, targetSector: string }) => void
onHandoffRejected(callback)
Called when an adjacent sector rejects the handoff.
Parameters:
callback: (event: { cs: string, targetSector: string, reason?: string }) => void
onHandoffFrequencyChange(callback)
Called when the pilot switches frequency during handoff.
Parameters:
callback: (event: { cs: string }) => void
onHandoffTransferred(callback)
Called when a handoff is fully completed.
Parameters:
callback: (event: { cs: string, targetSector: string }) => void
onApproachingExit(callback)
Called when an aircraft is approaching its exit fix.
Parameters:
callback: (event: { cs: string, exitFix: string, distanceNm: number }) => void
onDeviationRequest(callback)
Called when a pilot requests deviation around weather.
Parameters:
callback: (request: DeviationRequest) => boolean | void- Return
trueto approve - Return
falseto deny - Return nothing to leave pending (controller decides via UI)
- Return
DeviationRequest object:
{
id: string, // Request ID
callsign: string, // Aircraft callsign
direction: string, // "left" | "right"
deviationNm: number, // Requested deviation distance
reason: string, // Pilot phraseology
weatherCellId: string // ID of threatening weather cell
}Example:
// Auto-approve all deviation requests
onDeviationRequest((request) => {
log(`${request.callsign} requesting ${request.direction} deviation ${request.deviationNm}nm`);
log(`Reason: ${request.reason}`);
return true; // Approve
});
// Conditional approval based on traffic
onDeviationRequest((request) => {
const ac = traffic.byCallsign(request.callsign);
if (!ac) return false;
// Check if deviation conflicts with other traffic
const conflicting = traffic.all().some(other => {
if (other.cs === ac.cs) return false;
const lateral = distance(ac.pos, other.pos);
return lateral < 10 && Math.abs(ac.altFt - other.altFt) < 2000;
});
if (conflicting) {
log(`Denying ${request.callsign} deviation - traffic`);
return false;
}
return true;
});onConstraintSatisfied(callback)
Called when an aircraft crosses a procedure constraint fix and meets the altitude/speed requirement.
Parameters:
callback: (event) => void
Event object:
{
cs: string, // Aircraft callsign
fix: string, // Waypoint name (e.g., "GREKI")
procedureId: string, // Procedure name (e.g., "PARCH4")
altFt: number // Altitude at crossing
}Example:
onConstraintSatisfied((e) => {
log(`${e.cs} met constraint at ${e.fix} (${e.procedureId}) - ${Math.round(e.altFt)}ft`);
});onConstraintBusted(callback)
Called when an aircraft crosses a procedure constraint fix and fails to meet the altitude/speed requirement.
Parameters:
callback: (event) => void
Event object:
{
cs: string, // Aircraft callsign
fix: string, // Waypoint name
procedureId: string, // Procedure name
altFt: number, // Altitude at crossing
reason: string // Why it was busted (e.g., "Expected at or below 24000ft, was 26000ft")
}Example:
onConstraintBusted((e) => {
log(`${e.cs} BUSTED constraint at ${e.fix}: ${e.reason}`);
});onProcedureComplete(callback)
Called when all constraints on a procedure have been resolved (either satisfied or busted). Use this to track overall procedure compliance.
Parameters:
callback: (event) => void
Event object:
{
cs: string, // Aircraft callsign
procedureId: string, // Procedure name
satisfied: number, // Number of constraints met
busted: number // Number of constraints busted
}Example:
onProcedureComplete((e) => {
if (e.busted === 0) {
log(`${e.cs} completed ${e.procedureId} perfectly (${e.satisfied}/${e.satisfied} constraints met)`);
} else {
log(`${e.cs} completed ${e.procedureId} with ${e.busted} busted constraints`);
}
});