Source Code (JavaScript) of How Airplanes follow the Curvature of the Earth, Physics Simulation.
#INCLUDE NewtonSolver.inc #INCLUDE JsGraph.inc #INCLUDE ControlPanel.inc #INCLUDE Sim.inc var V2 = JsgVect2; function rad( x ) { return x * Math.PI / 180; } function deg( x ) { return x * 180 / Math.PI; } function ms_ftmin( x ) { return x * 196.85; } function ftmin_ms( x ) { return x / 196.85; } function ms_kt( x ) { return x * 1.94384; } function kt_ms( x ) { return x / 1.94384; } function m_ft( x ) { return x * 3.28084; } function ft_m( x ) { return x / 3.28084; } function ms_kmh( x ) { return x * 3.6; } function kmh_ms( x ) { return x / 3.6; } ////////////////////////////////////////////////////////// // Tables with reference values used in the // BaroModel standard atmospheric model. var BaroConst = { hIx: 0, // air level data of standard atmosphere model hLimitTab: [ 11000, 20000, 32000, 47000, 51000, 71000, 84852, 0 ], hRefTab: [ 0, 11000, 20000, 32000, 47000, 51000, 71000, NaN ], alphaTab: [ -0.0065, 0, 0.001, 0.0028, 0, -0.0028, -0.002, NaN ], TRefTab: [ 288.15, 216.65, 216.65, 228.65, 270.65, 270.65, 214.65, NaN ], rhoRefTab: [ 1.225, 0.363918, 0.0880348, 0.013225, 0.00142753, 0.000861605, 0.000064211, NaN ], pRefTab: [ 101325, 22632.1, 5474.89, 868.019, 110.906, 66.9389, 3.95642, NaN ], // some general constants g: 9.80665, R: 8.31446, RS: 287.058, kappa: 1.4, M: 28.9644, T0: 288.15, p0: 101325, rho0: 1.225, a0: 340.294, // private function defIx: function(i) { return xDefNum( i, this.hIx ); }, // set altitude range for following functions SetAltRange: function( h ) { for (var i = 0; i < this.hLimitTab.length; i++) { if (h <= this.hLimitTab[i]) { this.hIx = i; return; } } this.hIx = this.hLimitTab.length - 1; }, SetAltRangeFromP: function( p ) { var nPrefs = this.pRefTab.length - 2; for (var i = nPrefs; i >= 0; i--) { if (p <= this.pRefTab[i]) { this.hIx = i; return; } } this.hIx = 0; }, // query level dependent constants // default for i is this.hIx, see SetAltRange(h) hRef: function(i) { return this.hRefTab[this.defIx(i)]; }, alpha: function(i) { return this.alphaTab[this.defIx(i)]; }, TRef: function(i) { return this.TRefTab[this.defIx(i)]; }, rhoRef: function(i) { return this.rhoRefTab[this.defIx(i)]; }, pRef: function(i) { return this.pRefTab[this.defIx(i)]; } }; ////////////////////////////////////////////////////////// // model of standard atmosphere to calculate // temperature, pressure, density, temperature gradient // for any altitude up to 84852 m. // // usage: // BaroModel.Update( h ); // set reference altitude h // print( BaroModel.T, ..., BaroModel.alpha ); var BaroModel = { h: 0, // m T: 0, // K p: 0, // Pa rho: 0, // kg/m^3 alpha: 0, // C/m TempOfH: function( h, updateH ) { updateH = xDefBool( updateH, true ); if (updateH) BaroConst.SetAltRange( h ); return BaroConst.TRef() + BaroConst.alpha() * (h - BaroConst.hRef()); }, TempGradOfH: function( h, updateH ) { updateH = xDefBool( updateH, true ); if (updateH) BaroConst.SetAltRange( h ); return BaroConst.alpha(); }, PressureOfH: function( h, updateH ) { updateH = xDefBool( updateH, true ); if (updateH) BaroConst.SetAltRange( h ); var alpha = BaroConst.alpha(); if (alpha == 0) { // isoterm var hs = BaroConst.RS * BaroConst.TRef() / BaroConst.g; var p = BaroConst.pRef() * Math.exp( -(h - BaroConst.hRef()) / hs ); return p; } else { var beta = BaroConst.g / BaroConst.RS / alpha; var p = BaroConst.pRef() * Math.pow( 1 + alpha * (h - BaroConst.hRef()) / BaroConst.TRef(), -beta ); return p; } }, DensityOfH: function( h, updateH ) { updateH = xDefBool( updateH, true ); if (updateH) BaroConst.SetAltRange( h ); var alpha = BaroConst.alpha(); if (alpha == 0) { // isoterm var hs = BaroConst.RS * BaroConst.TRef() / BaroConst.g; var r = BaroConst.rhoRef() * Math.exp( -(h - BaroConst.hRef()) / hs ); return r; } else { var beta = BaroConst.g / BaroConst.RS / alpha; var r = BaroConst.rhoRef() * Math.pow( 1 + alpha * (h - BaroConst.hRef()) / BaroConst.TRef(), -beta-1 ); return r; } }, HofPressure: function( p ) { // calculates the height for a certain pressure (inverse of PressureFromH()) // usefull to calculate atmospheric parameters if pressure is not standard: // // BaroModel.Update( h ); // compute standard parameters for altitude h // var p = BaroModel.p; // standard pressure for h // var rho = BaroModel.rho; // standard density for h // var deltaH = BaroModel.HofPressure( p+deltaP ) - h; // apparent change in altitude due to non standard pressure // BaroModel.Update( h + deltaH ); // compute parameters for new altitude h+deltaH // var deltaRho = BaroModel.rho - rho; // corrected density due to non standard pressure p+deltaP BaroConst.SetAltRangeFromP( p ); var alpha = BaroConst.alpha(); if (alpha == 0) { // isoterm var hs = BaroConst.RS * BaroConst.TRef() / BaroConst.g; var h = BaroConst.hRef() - hs * Math.log( p / BaroConst.pRef() ); } else { // linear temperature gradient var beta = BaroConst.g / BaroConst.RS / alpha; var h = ((Math.pow( p / BaroConst.pRef(), -1/beta ) - 1) * BaroConst.TRef() + alpha * BaroConst.hRef()) / alpha; } return h; }, Update: function( h ) { this.h = h; if (this.h > 84852) this.h = 84852; if (this.h < 0) this.h = 0; this.T = this.TempOfH( this.h, true ); this.p = this.PressureOfH( this.h, false ); this.rho = this.DensityOfH( this.h, false ); this.alpha = this.TempGradOfH( this.h, false ); }, }; ////////////////////////////////////////////////////////// // Model to calculate and convert between the air speeds // tas, cas, eas, mach. Calculates also dynamic pressures (q,qc), // atmospheric parameters (T,p,rho) and speed of sound (a) for any // altitude (h). // // usage: // AirSpeedModel.Update( h, tas ); // print( AirSpeedModel.cas, ... ); var AirSpeedModel = { h: 0, tas: 0, cas: 0, eas: 0, mach: 0, q: 0, qc: 0, T: 0, p: 0, rho: 0, a: 0, AofH: function( h ) { // require TempOfH() is called return Math.sqrt( BaroConst.kappa * BaroConst.RS * this.T ); }, AtmosphereOfH: function() { this.T = BaroModel.TempOfH( this.h, true ); this.p = BaroModel.PressureOfH( this.h, false ); this.rho = BaroModel.DensityOfH( this.h, false ); this.a = this.AofH( this.h, false ); }, GetTASofCAS: function( cas ) { var k1 = (BaroConst.kappa - 1) / 2; var k2 = BaroConst.kappa / (BaroConst.kappa - 1); var v = cas / BaroConst.a0; var qc = BaroConst.p0 * ( Math.pow( k1 * v * v + 1, k2 ) - 1 ); var a = this.AofH( this.h ); var tas = a * Math.sqrt( (1/k1) * ( Math.pow( qc / this.p + 1 , 1/k2 ) - 1 ) ); return tas; }, GetTASofMach: function( mach ) { return this.a * mach; }, AllOfTas: function() { this.mach = this.tas / this.a; this.eas = Math.sqrt( this.rho / BaroConst.rho0 ) * this.tas; var k1 = (BaroConst.kappa - 1) / 2; var k2 = BaroConst.kappa / (BaroConst.kappa - 1); var v = this.tas / this.a; this.qc = this.p * ( Math.pow( k1 * v * v + 1 , k2 ) - 1 ); this.cas = BaroConst.a0 * Math.sqrt( (1/k1) * ( Math.pow( this.qc / BaroConst.p0 + 1 , 1/k2 ) - 1 ) ); this.q = 0.5 * this.rho * this.tas * this.tas; }, Update: function( h, tas ) { this.h = h; this.tas = tas; this.AtmosphereOfH(); this.AllOfTas(); } }; ////////////////////////////////////////////////////////// // Simple customizable model of a turbo fan engine. // Optionally change some default parameters, call Init() // and then use Compute( n1, tas ) to calculate all // dependant parameters and the thrust. // // var engine = new TurbofanModel(); // engine.Init(); // engine.Compute( n1, tas ); // print( engine.F ); // thrust function TurbofanModel() { // data sheet this.F_max = 117900; // N this.m0_max = 340.0; // kg/s (more thrust at high speed, old: 397.6) this.mu_max = 6; // parameters from other sources this.F_idle = 3500; // N this.theta = 60.0 / 180.0 * Math.PI; this.v9_max = 343; // m/s = a0 this.N1_max = 1.0; this.N1_idle = 0.15; // (shallower curve, old: 0.215) this.N1_maxrev= 0.8; this.N2_max = 0.972; this.N2_idle = 0.607; // input parameters this.N1 = this.N1_idle; this.v0 = 0.0; // simulated values as functions of N1 and v0 this.F = 0.0; // N this.F_rev = 0.0; // N // simulated internal values as functions of N1 and v0 this.N2 = 0.0; this.F1 = 0.0; // N this.F2 = 0.0; // N this.m1 = 0.0; // kg/s this.m2 = 0.0; // ks/s this.v9 = 0.0; // m/s this.v19 = 0.0; // m/s this.mu = 0.0; this.kv = 0.0; // internal constant parameters, computed in Init() this.k = 0.0; this.r = 0.0; this.k1 = 0.0; this.r1 = 0.0; this.a = 0.0; this.b = 0.0; this.km1 = 0.0; this.km2 = 0.0; this.m1_max = 0.0; this.m1_idle = 0.0; this.m2_max = 0.0; this.kv_max = 0.0; this.kv_idle = 0.0; this.F1_max = 0.0; this.F1_idle = 0.0; this.v9_idle = 0.0; } TurbofanModel.prototype.Init = function(){ this.m1_max = this.m0_max / (this.mu_max + 1); this.m2_max = this.m1_max * this.mu_max; this.a = (this.N2_max - this.N2_idle) / (this.N1_max - this.N1_idle); this.b = this.N2_idle - this.a * this.N1_idle; this.k = this.SolveForK( this.N1_max, 1.0, this.N1_idle, this.F_idle/this.F_max ); this.r = this.N1_max / Math.log( (1.0 / this.k) + 1.0 ); this.km1 = this.m0_max / ((this.mu_max + 1.0) * (this.a + this.b)); this.km2 = this.m0_max * this.mu_max / (this.mu_max + 1.0); this.kv_max = ((this.F_max * (1.0 + this.mu_max)) / (this.m0_max * this.mu_max * this.v9_max)) - (1.0 / this.mu_max); this.kv_idle = this.KVofN1( this.N1_idle ); this.F1_max = this.m1_max * this.v9_max; this.m1_idle = this.M1ofN1( this.N1_idle ); this.v9_idle = this.FofN1(this.N1_idle) / ( this.M1ofN1(this.N1_idle) + this.M2ofN1(this.N1_idle) * this.kv_idle ); this.F1_idle = this.m1_idle * this.v9_idle; this.k1 = this.SolveForK( this.N2_max, 1.0, this.N2_idle, this.F1_idle/this.F1_max ); this.r1 = this.N2_max / Math.log( (1.0 / this.k1) + 1.0 ); } TurbofanModel.prototype.Compute = function( n1, v ) { if (xDef(n1)) { this.N1 = n1; } if (xDef(v)) { this.v0 = v; } this.N2 = this.N2ofN1(this.N1); this.F1 = this.F1ofN1(this.N1); this.F2 = this.F2ofN1(this.N1); this.m1 = this.M1ofN1(this.N1); this.m2 = this.M2ofN1(this.N1); this.v9 = this.F1 / this.m1; this.v19 = (this.m2 < 0.01) ? 0.0 : (this.F2 / this.m2); this.mu = this.m2 / this.m1; this.kv = this.v19 / this.v9; this.F = this.m1 * (this.v9 - this.v0) + this.m2 * (this.v19 - this.v0); this.F_rev = this.m1 * (this.v9 - this.v0) - this.m2 * (Math.cos(this.theta) * this.v19 + this.v0); } TurbofanModel.prototype.FofN1 = function( n ) { // require k, r, is computed return this.k * (Math.exp(n/this.r) - 1.0) * this.F_max; } TurbofanModel.prototype.F1ofN1 = function( n ) { // require k1, r1, F1_max is computed return this.k1 * (Math.exp(this.N2ofN1(n)/this.r1) - 1.0) * this.F1_max; } TurbofanModel.prototype.F2ofN1 = function( n ) { // require k, r, k1, r1, F1_max is computed return this.FofN1(n) - this.F1ofN1(n); } TurbofanModel.prototype.N2ofN1 = function( n ) { return this.a * n + this.b; } TurbofanModel.prototype.M1ofN1 = function( n ) { // require km1 is computed! return this.km1 * this.N2ofN1(n); } TurbofanModel.prototype.M2ofN1 = function( n ) { // require km2 is computed! return this.km2 * n; } TurbofanModel.prototype.KVofN1 = function( n ) { // require kv_max is computed! return (this.kv_max - 1.0) * n + 1.0; } TurbofanModel.prototype.SolveForK = function( N1, F1, N2, F2 ) { var myFunction = function(x) { var t1 = N1 * Math.log( (F2 / x) + 1 ); var t2 = N2 * Math.log( (F1 / x) + 1 ); return t1 - t2; }; var k = SolveWithNewton( myFunction, 0, 0.0000001, 0.0000001 ); if (NewtonSolver.Status != '') { alert( 'NewtonSolver Error: ' + NewtonSolver.Status ); } return k; } ////////////////////////////////////////////////////////// // Model to calculate lift and drag coefficients over the // whole range of angle of attack -pi..pi. var ClModel = { aoa1: -5, aoa2: 18, cl1: 0.2, cl2: 5.16, cd1: 0.02, cd2: 0.026, clpOffset: 0.1, q: 25, CL: function( aoa ) { // lift coefficient for wing profile var result = this.cl1 + this.cl2 * aoa; return result; }, CD: function( aoa ) { // drag coefficient for wing profile var result = this.cd1 + this.cd2 * Math.pow( this.CL(aoa), 2 ); return result; }, CLP: function( aoa ) { // lift coefficient for flat wing var result = Math.sin( 2 * aoa ) + this.clpOffset; return result; }, CDP: function( aoa ) { // drag coefficient for flat wing var result = 1 - Math.cos( 2 * aoa ); return result; }, Up: function( x, pos ) { // ramp up from 0 to 1 at pos var result = 1 / (1 + Math.exp( -this.q * (x - pos))); return result; }, Down: function( x, pos ) { // ramp down from 0 to 1 at pos var result = 1 / (1 + Math.exp( this.q * (x - pos))); return result; }, Win: function( x, pos1, pos2 ) { // ramp up from 0 to 1 at pos1 and down again at pos 2 var result = this.Up( x, pos1 ) * this.Down( x, pos2 ); return result; }, // public CLT: function( aoa ) { // lift coefficient for whole range of AoA // aoa in rad if (aoa > Math.PI/2) aoa -= Math.PI; if (aoa < -Math.PI/2) aoa += Math.PI; var aoa1 = rad(this.aoa1); var aoa2 = rad(this.aoa2); var result = this.Down( aoa, aoa1 ) * this.CLP( aoa ) + this.Win( aoa, aoa1, aoa2 ) * this.CL( aoa ) + this.Up( aoa, aoa2) * this.CLP( aoa ); return result; }, CLT_deg: function( aoaDeg ) { // lift coefficient for whole range of AoA return this.CLT( rad(aoaDeg) ); }, CDT: function( aoa ) { // drag coefficient for whole range of AoA // aoa in rad if (aoa > Math.PI/2) aoa -= Math.PI; if (aoa < -Math.PI/2) aoa += Math.PI; var aoa1 = rad(this.aoa1); var aoa2 = rad(this.aoa2); var result = this.Down( aoa, aoa1 ) * this.CDP( aoa ) + this.Win( aoa, aoa1, aoa2 ) * this.CD( aoa ) + this.Up( aoa, aoa2) * this.CDP( aoa ); return result; }, CDT_deg: function( aoaDeg ) { return this.CDT( rad(aoaDeg) ); }, }; ////////////////////////////////////////////////////////// var AircraftModel = { // aircraft state State: { // [ x, y, pitch ] Accel: [ 0, 0, 0 ], // m/s^2 Speed: [ 0, 0, 0 ], // m/s Pos: [ 0, 0, 0 ], // m OldAccel: [ 0, 0, 0 ], OldSpeed: [ 0, 0, 0 ], }, TAS: 0, // m/s CAS: 0, // m/s Mach: 0, Altitude: 0, // m pressure altitude Elevation: 0, // m above sea level VS: 0, // m/s Pitch: 0, // rad PitchRate: 0, // rad/s PitchElevMod:0, // modulation of pitch due to pitch rate if pitch damping is enabled AoA: 0, // rad N1: 0, // % TimeOffset: 0, // s, see ResetTime() LowestManeuverSpeed: 0, StallSpeed: 0, MaxOperatingSpeed: 0, ShowAircraft: true, ShowFlightPath: true, ShowForces: true, ShowTrend: true, ShowCoordSys: true, ShowSpeed: true, ShowAccel: true, ShowFxTimes5: true, PitchDamping: true, LimitExceeded: '', Engines: null, AirTemperature: 0, AirDensity: 0, AirPressure: 0, SpeedOfSound: 0, // m/s at current altitude // initial state InitMass: 55200, // kg InitCG: 0, // -1..+1 -> 0% MAC..75% MAC -> MinCG..MaxCG InitTAS: kt_ms(443), // InitAltitude: ft_m(37000), InitVS: 0, InitPitch: rad(3.10), InitThrust: 0.9060, // thrust lever position InitTrim: rad(3.76), InitElevator: 0, InitDeltaPressure: 0, // Pa InitEarthRadius: 6371000/20, // aircraft parameters and commands Mass: 0, // kg ThrustLever: 0, // 0...1 TrimLever: 0, // MinTrimAngle...MaxTrimAngle ElevatorLever: 0, // MinElevatorAngle...MaxElevatorAngle DeltaPressure: 0, // +/- 3000 Pa Cl0: 0.2, ClAoA: 5.16, Cd0: 0.02, Cd1: 0.0264, Cm0: 0, CmAoA: 0, Ct: 5, // lift coefficients for trim and elevator flaps Ce: 2.5, Cpd: 10, // pitch damping coefficient, 2*(1/2*cross_section_AC_top_view+ElevatorArea) MAC: 4.1935, WingArea: 122.4, WingSpan: 34.09, AircraftLength: 37.6, TrimAngleRate: 0.02, // rad/s ElevatorAngleRate:0.35, // rad/s EngineSpoolRate: 0.1, // %N1/s AoAoffset: 0, // if aircraft is flying pitch = 0, this is the angle of attack ElevatorArea: 31, MomentOfInertia: 3*2400000, // kg*m^2 Trägheitsmoment I, analog zu masse https://www.researchgate.net/figure/Parameters-of-icing-research-airbus_tbl1_339115153 // aircraft geometry with respect to Center of Lift = AircraftZeroPos // X points forwards, Z points upwards AircraftZeroPos: [ -16.5, 2.8 ], // [m] = CenterOfLift, measured from the bottom left of the sideview, CenterOfLift: [ 0.0, 0.0 ], CenterOfGravity: [ 1.0, 0.0 ], ElevatorPos: [ -17.5, 1.4 ], EnginePos: [ 4.0, -1.6 ], CenterGravityPos: 1, // forward from center of lift modulated by MinCG..MaxCG // aircraft limits MinMass: 38600, // kg MaxMass: 73600, // kg MinN1: 0.40, MaxN1: 1, MinTrimAngle: rad(-4), MaxTrimAngle: rad(13.5), MinElevatorAngle: rad(-17), MaxElevatorAngle: rad(30), MaxCG: 0.8, // 1 GC MinCG: -0.8, // -1 CG MinAoA: rad(-5), // used for stall indication MaxAoA: rad(15), // used to calculate min speeds and stall indication MaxTAS: kt_ms(580), // used for length of force arrows scaling LimitCAS: kt_ms(350), // used for Vmo LimitMach: 0.82, // used for Vmo // aircraft unit vectors in ECEF coordinates Xsurface: [ 0, 0 ], Zsurface: [ 0, 0 ], Xaircraft: [ 0, 0 ], Zaircraft: [ 0, 0 ], SpeedDir: [ 0, 0 ], SpeedPerp: [ 0, 0 ], // forces, vectors in ECEF coordinates FWeight: [ 0, 0 ], FLift: [ 0, 0 ], FThrust: [ 0, 0 ], FDrag: [ 0, 0 ], FTrim: [ 0, 0 ], Ftot: [ 0, 0 ], PitchTorque: 0, // Drehmoment M, analog zu Kraft // for trend vectors FLiftOld: [ 0, 0 ], FThrustOld: [ 0, 0 ], FDragOld: [ 0, 0 ], FTrimOld: [ 0, 0 ], TASold: 0, Thrust: 0, N1Command: 0, TrimAngle: 0, TrimAngleCommand: 0, ElevatorAngle: 0, ElevatorAngleCommand: 0, DynamicPressure: 0, // q(h,tas) for use in lift equation EarthRadius: 6371000/20, g: 9.806, // m/s^2 gravitational acceleration // referenced objects Sim: null, // simulator Jsg: null, // graphic // sim callbacks Init: function( jsg, sim ) { this.Jsg = jsg; this.Sim = sim; this.Engines = new TurbofanModel(); this.Engines.Init(); this.OnSimReset(); // create flight path vector symbol jsg.AddMarkerDef( 'FPSymbol', [ { type: 'Circle', x: 0, y: 0, r: -0.5 }, { type: 'Line', x1: -1.5, y1: 0, x2: -0.5, y2: 0 }, { type: 'Line', x1: 1.5, y1: 0, x2: 0.5, y2: 0 }, { type: 'Line', x1: 0, y1: -0.5, x2: 0, y2: -1.0 } ] ); }, OnSimReset: function() { this.Mass = this.InitMass; this.CG = this.InitCG; this.TAS = this.InitTAS; this.TASold = this.TAS; this.Altitude = this.InitAltitude; this.VS = this.InitVS; this.Pitch = this.InitPitch; this.ThrustLever = this.InitThrust; this.N1Command = (this.MaxN1-this.MinN1) * this.ThrustLever + this.MinN1; this.N1 = this.N1Command; this.TrimLever = this.InitTrim; this.TrimAngleCommand = this.TrimLever; this.TrimAngle = this.TrimAngleCommand; this.ElevatorLever = this.InitElevator; this.ElevatorAngleCommand = this.ElevatorLever; this.ElevatorAngle = this.ElevatorAngleCommand; this.DeltaPressure = this.InitDeltaPressure; this.TimeOffset = 0; this.ShowAircraft = true; this.ShowFlightPath = true; this.ShowForces = true; this.ShowTrend = true; this.ShowCoordSys = true; this.ShowSpeed = true; this.ShowAccel = true; this.ShowFxTimes5 = true; this.ComputeInitialState(); this.CompCoordSysAxes(); this.UpdateAircraftState(); this.Sim.InitStates( this.State ); this.SaveOldState(); this.Update(); }, OnSimUpdate: function() { // is called by simulator to calculate the next AC state var elev = V2.Length( this.State.Pos ) - this.EarthRadius; if (elev <= 0) { this.Sim.Reset(); this.Sim.Run(); return; } this.UpdateAircraftState(); }, OnSimDraw: function() { // is called by simulator to draw currect AC state this.Update(); }, UpdateAircraftState: function() { this.LimitExceeded = ''; this.CompAccel(); this.Sim.CompNewStates( this.State ); }, Update: function( field ) { if (ControlPanels.MatchesField( field, 'EarthRadius' )) { var currentAlt = this.Elevation; var newPos = V2.Scale( V2.Norm( this.State.Pos ), this.EarthRadius + currentAlt ); this.State.Pos[0] = newPos[0]; this.State.Pos[1] = newPos[1]; } ControlPanels.Update(); this.Draw(); }, ResetPos: function() { var angle = V2.Angle( V2.Norm(this.State.Pos), [0,1] ); this.State.Pos[0] = 0; this.State.Pos[1] = this.EarthRadius + this.Elevation; this.State.Pos[2] = this.Pitch; var newSpeedVect = V2.Rotate( this.State.Speed, angle ); this.State.Speed[0] = newSpeedVect[0]; this.State.Speed[1] = newSpeedVect[1]; this.CompCoordSysAxes(); this.UpdateAircraftState(); this.Update(); }, ResetTime: function() { this.TimeOffset = this.Sim.SimulTime; this.Update(); }, ComputeInitialState: function() { this.State.Pos[0] = 0; this.State.Pos[1] = this.EarthRadius + this.Altitude; this.State.Pos[2] = this.Pitch; this.State.Speed[0] = this.TAS; this.State.Speed[1] = this.VS; this.State.Speed[2] = 0; // pitch rate }, CompCoordSysAxes: function() { // require state is computed this.Zsurface = V2.Norm( this.State.Pos ); this.Xsurface = V2.Rotate( this.Zsurface, -Math.PI/2 ); this.Xaircraft = V2.Rotate( this.Xsurface, this.Pitch ); this.Zaircraft = V2.Rotate( this.Xaircraft, Math.PI/2 ); this.SpeedDir = V2.Norm( this.State.Speed ); this.SpeedPerp = V2.Rotate( this.SpeedDir, Math.PI/2 ); }, CompSpeedsAndBaro: function() { // Computes barometric parameters like air pressure, density, temperature // speed of sound for current Elevation. // Computes the air parameter dependent different air speeds: // TAS, Density(Elevation) -> CAS, Mach, speed of sound (a) // Use BaroModel to calculate all air parameters for Elevation BaroModel.Update( this.Elevation ); // Compute Pressure Altitude due to non standard pressure DeltaPressure if (this.DeltaPressure == 0) { this.Altitude = this.Elevation; } else { var p = BaroModel.p; // standard pressure for Elevation // compute pressure altitude this.Altitude = BaroModel.HofPressure( p + this.DeltaPressure ); // compute parameters from pressure altitude (Elevation+DeltaPressure) BaroModel.Update( this.Altitude ); } // Use AirSpeedModel to calculate various air speeds from TAS AirSpeedModel.Update( this.Altitude, this.TAS ); this.CAS = AirSpeedModel.cas; this.Mach = AirSpeedModel.mach; this.SpeedOfSound = AirSpeedModel.a; // Save air parameters in AircraftModel this.AirPressure = AirSpeedModel.p; this.AirTemperature = AirSpeedModel.T - 273.15; // K to °C this.AirDensity = AirSpeedModel.rho; this.DynamicPressure = AirSpeedModel.q; // compute stall speed and LowestManeuveringSpeed for speed tape red bar var Clmax = this.CL( 0.9 * this.MaxAoA ); var stallSpeed = Math.sqrt( 2 * this.Mass * this.Gravity(this.Elevation) / (this.AirDensity * Clmax * this.WingArea) ); AirSpeedModel.Update( this.Altitude, stallSpeed ); // convert to CAS this.StallSpeed = AirSpeedModel.cas; this.LowestManeuverSpeed = this.StallSpeed * 1.15; // indicate SPEED and STALL if (this.CAS < this.StallSpeed) { this.LimitExceeded = 'STALL'; } else if (this.CAS < this.LowestManeuverSpeed) { this.LimitExceeded = 'SPEED'; } // compute max operating speed as the smaller of // max CAS limit and altitude dependent Mach limit // mach limit for altitude var mach = AirSpeedModel.AofH( this.Altitude ) * this.LimitMach; AirSpeedModel.Update( this.Altitude, mach ); // convert to CAS var vmo = AirSpeedModel.cas; if (vmo > this.LimitCAS) vmo = this.LimitCAS; this.MaxOperatingSpeed = vmo; // indicate overspeed as SPEED if (this.CAS > this.MaxOperatingSpeed) { this.LimitExceeded = 'SPEED'; } }, CL: function( a ) { // Compute lift coefficient; a = AoA in rad // return this.Cl0 + this.ClAoA * a; return ClModel.CLT(a); }, CD: function( a ) { // Compute drag coefficient; a = AoA in rad // var cl = this.CL( a ); // return this.Cd0 + this.Cd1 * cl * cl; return ClModel.CDT(a); }, CM: function( a ) { // Compute momentum coeffiecient; a = AoA in rad return this.Cm0 + this.CmAoA * a; }, CT: function( trimAngle, elevatorAngle ) { // Compute trim lift coefficient for trimAngle and elevator angle return this.Ct * trimAngle + this.Ce * elevatorAngle; }, Gravity: function( h ) { return this.g * Math.pow( this.EarthRadius, 2 ) / Math.pow( this.EarthRadius + h, 2 ); }, CompAccel: function() { // Compute all forces, moments and the acceleration and PitchRate of the aircraft. // save previous forces for calculation of trend vectors this.SaveOldState(); // compute all forces for current aircraft state this.CompForces(); // Compute acceleration from sum of all forces var accel = V2.Scale( this.Ftot, 1 / this.Mass ); var pitchAccel = this.PitchTorque / this.MomentOfInertia; this.State.Accel[0] = accel[0]; this.State.Accel[1] = accel[1]; this.State.Accel[2] = pitchAccel; }, SaveOldState: function() { this.FLiftOld = V2.Copy( this.FLift ); this.FDragOld = V2.Copy( this.FDrag ); this.FTrimOld = V2.Copy( this.FTrim ); this.FThrustOld = V2.Copy( this.FThrust ); }, LinearRampTo: function( command, current, rate ) { if (command < current) { // ramp down current -= this.Sim.DeltaTime * rate; if (current < command) current = command; } else if (command > current) { // ramp up current += this.Sim.DeltaTime * rate; if (current > command) current = command; } return current; }, TorqueAroundCG: function( forceOrigin, forceVect ) { // computes the torque of a force with origin not at the center of gravity // around the center of gravity CG. // The torque is the cross product (= vector product) of the vector forceOrigin-CG and // the forceVect. This product in 3D only has a component in the wing axis Y. // This Y component is just our torque. I can use the V2.VectProd() function to // calculate the Y component. // transform forceVect from ECEF into aircraft coordinate system var forceVect = [ V2.ScalarProd( this.Xaircraft, forceVect ), V2.ScalarProd( this.Zaircraft, forceVect ) ]; var torque = V2.VectProd( V2.Sub( forceOrigin, this.CenterOfGravity ), forceVect ); return torque; }, CompForces: function() { // get state this.Elevation = V2.Length( this.State.Pos ) - this.EarthRadius; this.Pitch = this.State.Pos[2]; this.TASold = this.TAS; this.TAS = V2.Length( this.State.Speed ); this.VS = V2.ScalarProd( this.Zsurface, this.State.Speed ); this.PitchRate = this.State.Speed[2]; // limit pitch while (this.Pitch <= -rad(180)) { this.Pitch += rad(360); } while (this.Pitch > rad(180)) { this.Pitch -= rad(360); } this.State.Pos[2] = this.Pitch; // Compute the coordinate systems this.CompCoordSysAxes(); // Compute the center of gravity from CG lever var centerGravityMAC = (this.MaxCG - this.MinCG) * (this.CG+1)/2 + this.MinCG; this.CenterOfGravity[0] = this.CenterGravityPos + centerGravityMAC; // TrimLever position is translated into a TrimAngleCommand. // TrimAngle is set in a ramp defined by TrimAngleRate, // because the horizontal stabilizer trim moved driven by a motor // at a contstant speed. this.TrimAngleCommand = this.TrimLever; this.TrimAngle = this.LinearRampTo( this.TrimAngleCommand, this.TrimAngle, this.TrimAngleRate ); // ElevatorLever position is translated into a ElevatorAngleCommand. // ElevatorAngle is set in a ramp defined by ElevatorAngleRate // Modulate elevator depending on vertical speed and pitch rate // to dampen pitch rate and Phugoid oscillation. // Damping Factors empirically derived. this.ElevatorAngleCommand = this.ElevatorLever; var elevAngle = this.ElevatorAngleCommand; if (this.PitchDamping) { var vsAngleModulationLimit = 0.8 * this.MaxElevatorAngle; var vsMaxModulation = this.VS / ftmin_ms(6000); vsElevAngleMod = vsAngleModulationLimit * vsMaxModulation * Math.abs(vsMaxModulation); if (vsElevAngleMod > vsAngleModulationLimit) vsElevAngleMod = vsAngleModulationLimit; if (vsElevAngleMod < -vsAngleModulationLimit) vsElevAngleMod = -vsAngleModulationLimit; this.PitchElevMod = 2.25 * this.PitchRate; elevAngle -= this.PitchElevMod + vsElevAngleMod; if (elevAngle > this.MaxElevatorAngle) elevAngle = this.MaxElevatorAngle; if (elevAngle < this.MinElevatorAngle) elevAngle = this.MinElevatorAngle; } this.ElevatorAngle = this.LinearRampTo( elevAngle, this.ElevatorAngle, this.ElevatorAngleRate ); // Compute air parameters for current altitude and depending on them // the various air speeds: TAS, Altitude -> CAS, Mach, speed of sound a // Note: if air pressure is not standard, elevation and altitude are not the same. // Elevation is the geometric distance from the surface of sea level. // Altitude is the altitude derived from air pressure measurements. this.CompSpeedsAndBaro(); // ThrustLever positions are translated into N1Commands. // N1 change is set in a ramp given by EngineSpoolRate. this.N1Command = (this.MaxN1-this.MinN1) * this.ThrustLever + this.MinN1; this.N1 = this.LinearRampTo( this.N1Command, this.N1, this.EngineSpoolRate ); // compute enging thrust from N1 (rotation rate in % of max N1). // Effective thrust force depends on TAS acting in the X axis of the airplane. // Only on TAS = 0 max thrust is possible. // On low N1 the engines create drag (negative thrust force). // v0 is TAS component in the X axis of the airplane. var v0 = V2.ScalarProd( this.Xaircraft, this.State.Speed ); this.Engines.Compute( this.N1, v0 ); this.Thrust = 2 * this.Engines.F; // correct thrust for density and dynamic pressure this.Thrust *= this.CAS / this.TAS; // Compute Angle of Attack AoA from direction of flight through the air // (SpeedDir and the direction the X-axis of the airplane is pointing. // As the model does not implement an autopilot that implements flight // envelope protection, AoA is hardcoded to be limited to a valid range // to prevent simulation instabilities. // The stall behavior is therefore not simulated accurately. var windNoseAngle = V2.Angle( this.SpeedDir, this.Xaircraft ); this.AoA = windNoseAngle + this.AoAoffset; while (this.AoA <= -rad(180)) { this.AoA += rad(360); } while (this.AoA > rad(180)) { this.AoA -= rad(360); } if (this.AoA < this.MinAoA) { this.LimitExceeded = 'AoA'; } else if (this.AoA > this.MaxAoA) { this.LimitExceeded = 'STALL'; } // compute the different forces from the current aircraft state this.FWeight = V2.Scale( this.Zsurface, -this.Mass * this.Gravity(this.Elevation) ); this.FThrust = V2.Scale( this.Xaircraft, this.Thrust ); var lift = this.DynamicPressure * this.CL(this.AoA) * this.WingArea; this.FLift = V2.Scale( this.SpeedPerp, lift ); var effTrimAngle = this.TrimAngle - windNoseAngle; var effElevAngle = this.ElevatorAngle + this.TrimAngle - windNoseAngle; var trimLift = this.DynamicPressure * this.CT( effTrimAngle, effElevAngle ) * this.ElevatorArea; var trimLiftVect = V2.Rotate( this.Xaircraft, -this.TrimAngle - Math.PI/2 ); this.FTrim = V2.Scale( trimLiftVect, trimLift ); var drag = this.DynamicPressure * this.CD(this.AoA) * this.WingArea; this.FDrag = V2.Scale( this.SpeedDir, -drag ); // Compute sum of all forces var fTot = this.FWeight; fTot = V2.Add( fTot, this.FLift ); fTot = V2.Add( fTot, this.FThrust ); fTot = V2.Add( fTot, this.FDrag ); fTot = V2.Add( fTot, this.FTrim ); this.Ftot = fTot; // compute the various torques and total PitchTorque var liftMoment = this.DynamicPressure * this.CM(this.AoA) * this.WingArea * this.MAC; var liftTorque = this.TorqueAroundCG( this.CenterOfLift, this.FLift ); var trimTorque = this.TorqueAroundCG( this.ElevatorPos, this.FTrim ); var thrustTorque = this.TorqueAroundCG( this.EnginePos, this.FThrust ); // force daming up/down oscillations depending on PitchRate // acting on the horizontal stabilizer var horizStabDistFromCG = this.CenterOfGravity[0] - this.ElevatorPos[0]; var horizStabVertSpeed = this.PitchRate * horizStabDistFromCG; var pitchDampingForce = -0.5 * this.AirDensity * Math.pow(horizStabVertSpeed,2) * this.Cpd * this.ElevatorArea; var pitchDampingTorque = horizStabDistFromCG * pitchDampingForce; // total pitch torque this.PitchTorque = liftMoment + liftTorque + trimTorque + thrustTorque + pitchDampingTorque; }, Draw: function() { g = this.Jsg; g.Reset(); g.SetAngleMeasure( 'rad' ); var vpX = 0.7 * g.CanvasHeight; var vpY = 0; var vpWidth = g.CanvasWidth - 0.7 * g.CanvasHeight; var vpHeight = g.CanvasHeight; g.SetViewport( vpX, vpY, vpWidth, vpHeight, false, true ); var winXcenter = 0; var winYcenter = 0; var winWidth = 1.5 * this.AircraftLength; var winHeight = 1.5 * this.AircraftLength; g.MapWindow( winXcenter, winYcenter, winWidth, winHeight, 0 ); this.DrawHorizon( g ); this.DrawAircraft( g ); // elevator trim indication var dspWidth = vpWidth / 6; var dspHeight = dspWidth; var vpX = 0.7 * g.CanvasHeight; var vpY = g.CanvasHeight - dspHeight; this.DrawElevatorTrimIndication( g, vpX, vpY, dspWidth, dspHeight ); // engine indication var dspWidth = vpWidth / 6; var dspHeight = dspWidth; var vpX = 0.7 * g.CanvasHeight + dspWidth; var vpY = g.CanvasHeight - dspHeight; this.DrawEngineIndication( g, vpX, vpY, dspWidth, dspHeight ); // primary flight display PFD var vpWidth = 0.7 * g.CanvasHeight; var vpHeight = 5/6 * vpWidth; var vpX = 0; var vpY = g.CanvasHeight - vpHeight; this.DrawPfd( g, vpX, vpY, vpWidth, vpHeight ); // Forces Display var vpHeight = g.CanvasHeight - 5/6 * vpWidth; var vpX = 0; var vpY = 0; this.DrawDataDisplay( g, vpX, vpY, vpWidth, vpHeight ); }, DrawHorizon: function( g ) { var surfaceAngle = V2.Angle( [0,1], this.Zsurface ); g.TransRotate( surfaceAngle ); // horizon drops according to AC altitude and size of the earth var dropScale = -(1/6) * Math.log10( this.EarthRadius ) + (10/6); var horizonDrop = 7 * dropScale * g.WinHeight * Math.sqrt( 2 * this.Elevation / 6371000 ); // draw earth g.SetAlpha( 0.6 ); var grad = g.CreateLinearGradient( { X1: 0, Y1: -horizonDrop, X2: 0, Y2: -0.4*g.WinWidth, Stops: [ { Pos: 0 , Color: '#b6b3ff' }, { Pos: 0.45, Color: '#354d8b' }, { Pos: 1, Color: '#223d36' } ] } ); g.SetAreaAttr( grad, grad, 1 ); g.Rect( -1.5 * g.WinWidth, -1.5 * g.WinWidth, 1.5 * g.WinWidth, -horizonDrop, 2 ); // draw sky g.SetAlpha( 1 ); var grad = g.CreateLinearGradient( { X1: 0, Y1: 1*g.WinWidth, X2: 0, Y2: -horizonDrop, Stops: [ { Pos: 0, Color: '#3a51f7' }, { Pos: 1, Color: '#ffffff' } ] } ); g.SetAreaAttr( grad, grad, 1 ); g.Rect( -1.5 * g.WinWidth, -horizonDrop, 1.5 * g.WinWidth, 1.5 * g.WinWidth, 2 ); g.SetAlpha( 1 ); g.ResetTrans(); }, DrawAircraft: function( g ) { // The airplane in this function is scaled to a length of 1 unit // and graphuc transformations are used to scale, move and rotate the airplane. // scale and center aircraft at center of screen g.TransScale( -this.AircraftLength, this.AircraftLength ); g.TransMove( -this.AircraftZeroPos[0], -this.AircraftZeroPos[1] ); // set pitch var surfaceAngle = V2.Angle( [0,1], this.Zsurface ); g.TransRotate( this.Pitch + surfaceAngle ); // move vertical to indicate vertical speed // using atan to limit the vertical displacement var vsDisplacement = 4 * Math.atan( this.VS / 20 ); var vsDisplacementVect = V2.Scale( this.Zsurface, vsDisplacement ); g.TransMove( vsDisplacementVect ); if (!this.ShowAircraft) { g.SetAlpha( 0.15 ); } g.SetAreaAttr( 'white', 'black', 1 ); function op(){g.OpenPath();} function mt(x,y){g.MoveTo(x,y);} function lt(x,y){g.LineTo(x,y);} function pt(m){g.Path(m);} function bc(cx1,cy1,cx2,cy2,x,y){g.BezierCurveTo(cx1,cy1,cx2,cy2,x,y);} function ln(x1,y1,x2,y2){g.Line(x1,y1,x2,y2);} function rc(x,y,w,h,m){g.RectWH(x,y,w,h,m);} function po(x,y,m){g.Polygon(x,y,m);} // draw aircraft op(); mt( 0.765, 0.141 ); bc( 0.783, 0.145, 0.796, 0.148, 0.806, 0.158 ); bc( 0.816, 0.167, 0.917, 0.279, 0.923, 0.286 ); bc( 0.929, 0.293, 0.934, 0.293, 0.934, 0.293 ); lt( 0.980, 0.293 ); bc( 0.980, 0.293, 0.980, 0.291, 0.979, 0.283 ); bc( 0.978, 0.275, 0.937, 0.132, 0.937, 0.132 ); pt( 7 ); ln( 0.964, 0.293, 0.891, 0.135 ); op(); mt( 0.684, 0.0329 ); bc( 0.666, 0.0326, 0.154, 0.0329, 0.107, 0.0333 ); bc( 0.0603, 0.0336, -4.73e-17, 0.0513, 0.000147, 0.0703 ); bc( 0.000294, 0.0892, 0.0321, 0.0990, 0.0335, 0.101 ); bc( 0.0349, 0.103, 0.0433, 0.116, 0.0622, 0.127 ); bc( 0.0811, 0.137, 0.101, 0.141, 0.129, 0.141 ); bc( 0.157, 0.141, 0.566, 0.141, 0.605, 0.141 ); bc( 0.643, 0.141, 0.744, 0.141, 0.779, 0.141 ); bc( 0.814, 0.140, 0.875, 0.139, 0.894, 0.137 ); bc( 0.913, 0.136, 0.927, 0.134, 0.937, 0.133 ); bc( 0.948, 0.132, 0.999, 0.128, 0.999, 0.114 ); bc( 1.00, 0.100, 0.926, 0.0784, 0.889, 0.0700 ); bc( 0.853, 0.0616, 0.761, 0.0416, 0.735, 0.0374 ); bc( 0.709, 0.0332, 0.702, 0.0333, 0.684, 0.0329 ); pt( 7 ); op(); mt( 0.432, 0.0280 ); bc( 0.512, 0.0280, 0.545, 0.0322, 0.556, 0.0364 ); bc( 0.566, 0.0406, 0.573, 0.0483, 0.572, 0.0525 ); bc( 0.570, 0.0567, 0.563, 0.0616, 0.523, 0.0665 ); bc( 0.483, 0.0714, 0.437, 0.0826, 0.400, 0.0798 ); bc( 0.363, 0.0770, 0.353, 0.0784, 0.325, 0.0735 ); bc( 0.297, 0.0686, 0.288, 0.0630, 0.287, 0.0483 ); bc( 0.286, 0.0336, 0.339, 0.0280, 0.371, 0.0273 ); bc( 0.403, 0.0266, 0.432, 0.0280, 0.432, 0.0280 ); pt( 7 ); op(); mt( 0.297, 0.0609 ); bc( 0.311, 0.0660, 0.320, 0.0664, 0.335, 0.0674 ); bc( 0.340, 0.0613, 0.401, 0.0656, 0.426, 0.0655 ); bc( 0.451, 0.0654, 0.477, 0.0639, 0.477, 0.0639 ); bc( 0.477, 0.0639, 0.478, 0.0609, 0.475, 0.0582 ); bc( 0.473, 0.0555, 0.452, 0.0402, 0.437, 0.0398 ); bc( 0.422, 0.0394, 0.385, 0.0392, 0.385, 0.0392 ); lt( 0.303, 0.0576 ); lt( 0.297, 0.0609 ); pt( 7 ); op(); mt( 0.280, 0.0111 ); mt( 0.280, 0.0552 ); bc( 0.280, 0.0583, 0.287, 0.0611, 0.291, 0.0611 ); bc( 0.296, 0.0611, 0.323, 0.0610, 0.331, 0.0610 ); bc( 0.340, 0.0609, 0.343, 0.0593, 0.343, 0.0566 ); bc( 0.343, 0.0540, 0.343, 0.00738, 0.343, 0.00457 ); bc( 0.343, 0.00176, 0.341, 7.57e-17, 0.327, 0.000597 ); bc( 0.312, 0.00119, 0.303, 0.00154, 0.297, 0.00293 ); bc( 0.290, 0.00431, 0.280, 0.00751, 0.280, 0.0111 ); pt( 7 ); ln( 0.310, 0.0610, 0.310, 0.00186 ); op(); mt( 0.344, 0.00399 ); mt( 0.344, 0.0572 ); bc( 0.370, 0.0576, 0.376, 0.0506, 0.376, 0.0506 ); lt( 0.376, 0.0140 ); bc( 0.376, 0.0140, 0.368, 0.00420, 0.344, 0.00399 ); pt( 7 ); op(); mt( 0.376, 0.0174 ); mt( 0.376, 0.0455 ); bc( 0.386, 0.0455, 0.392, 0.0401, 0.392, 0.0380 ); lt( 0.392, 0.0246 ); bc( 0.392, 0.0246, 0.387, 0.0176, 0.376, 0.0174 ); pt( 7 ); po( [ 0.392, 0.392, 0.392, 0.415], [ 0.0372, 0.0255, 0.0253, 0.0305], 7 ); op(); mt( 0.616, 0.109 ); bc( 0.616, 0.111, 0.618, 0.112, 0.620, 0.112 ); bc( 0.621, 0.112, 0.623, 0.111, 0.623, 0.109 ); bc( 0.623, 0.108, 0.623, 0.103, 0.623, 0.101 ); bc( 0.623, 0.100, 0.622, 0.0994, 0.620, 0.0994 ); bc( 0.618, 0.0994, 0.616, 0.100, 0.616, 0.101 ); pt( 7 ); op(); mt( 0.709, 0.109 ); bc( 0.709, 0.111, 0.711, 0.112, 0.712, 0.112 ); bc( 0.714, 0.112, 0.716, 0.111, 0.716, 0.109 ); bc( 0.716, 0.108, 0.716, 0.103, 0.716, 0.101 ); bc( 0.716, 0.100, 0.714, 0.0994, 0.712, 0.0994 ); bc( 0.711, 0.0994, 0.709, 0.100, 0.709, 0.101 ); pt( 7 ); op(); mt( 0.629, 0.109 ); bc( 0.629, 0.111, 0.631, 0.112, 0.633, 0.112 ); bc( 0.635, 0.112, 0.636, 0.111, 0.636, 0.109 ); bc( 0.636, 0.108, 0.636, 0.103, 0.636, 0.101 ); bc( 0.636, 0.100, 0.635, 0.0994, 0.633, 0.0994 ); bc( 0.631, 0.0994, 0.629, 0.100, 0.629, 0.101 ); pt( 7 ); op(); mt( 0.722, 0.109 ); bc( 0.722, 0.111, 0.724, 0.112, 0.726, 0.112 ); bc( 0.727, 0.112, 0.729, 0.111, 0.729, 0.109 ); bc( 0.729, 0.108, 0.729, 0.103, 0.729, 0.101 ); bc( 0.729, 0.100, 0.728, 0.0994, 0.726, 0.0994 ); bc( 0.724, 0.0994, 0.722, 0.100, 0.722, 0.101 ); pt( 7 ); op(); mt( 0.643, 0.109 ); bc( 0.643, 0.111, 0.644, 0.112, 0.646, 0.112 ); bc( 0.648, 0.112, 0.650, 0.111, 0.650, 0.109 ); bc( 0.650, 0.108, 0.650, 0.103, 0.650, 0.101 ); bc( 0.650, 0.100, 0.648, 0.0994, 0.646, 0.0994 ); bc( 0.644, 0.0994, 0.643, 0.100, 0.643, 0.101 ); pt( 7 ); op(); mt( 0.656, 0.109 ); bc( 0.656, 0.111, 0.658, 0.112, 0.659, 0.112 ); bc( 0.661, 0.112, 0.663, 0.111, 0.663, 0.109 ); bc( 0.663, 0.108, 0.663, 0.103, 0.663, 0.101 ); bc( 0.663, 0.100, 0.661, 0.0994, 0.659, 0.0994 ); bc( 0.657, 0.0994, 0.656, 0.100, 0.656, 0.101 ); pt( 7 ); op(); mt( 0.669, 0.109 ); bc( 0.669, 0.111, 0.671, 0.112, 0.673, 0.112 ); bc( 0.675, 0.112, 0.676, 0.111, 0.676, 0.109 ); bc( 0.676, 0.108, 0.676, 0.103, 0.676, 0.101 ); bc( 0.676, 0.100, 0.675, 0.0994, 0.673, 0.0994 ); bc( 0.671, 0.0994, 0.669, 0.100, 0.669, 0.101 ); pt( 7 ); op(); mt( 0.683, 0.109 ); bc( 0.683, 0.111, 0.684, 0.112, 0.686, 0.112 ); bc( 0.688, 0.112, 0.690, 0.111, 0.690, 0.109 ); bc( 0.690, 0.108, 0.690, 0.103, 0.690, 0.101 ); bc( 0.690, 0.100, 0.688, 0.0994, 0.686, 0.0994 ); bc( 0.684, 0.0994, 0.683, 0.100, 0.683, 0.101 ); pt( 7 ); op(); mt( 0.603, 0.109 ); bc( 0.603, 0.111, 0.604, 0.112, 0.606, 0.112 ); bc( 0.608, 0.112, 0.610, 0.111, 0.610, 0.109 ); bc( 0.610, 0.108, 0.610, 0.103, 0.610, 0.101 ); bc( 0.610, 0.100, 0.608, 0.0994, 0.606, 0.0994 ); bc( 0.604, 0.0994, 0.603, 0.100, 0.603, 0.101 ); pt( 7 ); op(); mt( 0.696, 0.109 ); bc( 0.696, 0.111, 0.698, 0.112, 0.699, 0.112 ); bc( 0.701, 0.112, 0.703, 0.111, 0.703, 0.109 ); bc( 0.703, 0.108, 0.703, 0.103, 0.703, 0.101 ); bc( 0.703, 0.100, 0.701, 0.0994, 0.699, 0.0994 ); bc( 0.697, 0.0994, 0.696, 0.100, 0.696, 0.101 ); pt( 7 ); op(); mt( 0.418, 0.109 ); bc( 0.418, 0.111, 0.419, 0.112, 0.421, 0.112 ); bc( 0.423, 0.112, 0.425, 0.111, 0.425, 0.109 ); bc( 0.425, 0.108, 0.425, 0.103, 0.425, 0.101 ); bc( 0.425, 0.100, 0.423, 0.0994, 0.421, 0.0994 ); bc( 0.419, 0.0994, 0.418, 0.100, 0.418, 0.101 ); pt( 7 ); op(); mt( 0.511, 0.109 ); bc( 0.511, 0.111, 0.512, 0.112, 0.514, 0.112 ); bc( 0.516, 0.112, 0.518, 0.111, 0.518, 0.109 ); bc( 0.518, 0.108, 0.518, 0.103, 0.518, 0.101 ); bc( 0.518, 0.100, 0.516, 0.0994, 0.514, 0.0994 ); bc( 0.512, 0.0994, 0.511, 0.100, 0.511, 0.101 ); pt( 7 ); op(); mt( 0.431, 0.109 ); bc( 0.431, 0.111, 0.433, 0.112, 0.434, 0.112 ); bc( 0.436, 0.112, 0.438, 0.111, 0.438, 0.109 ); bc( 0.438, 0.108, 0.438, 0.103, 0.438, 0.101 ); bc( 0.438, 0.100, 0.436, 0.0994, 0.434, 0.0994 ); bc( 0.433, 0.0994, 0.431, 0.100, 0.431, 0.101 ); pt( 7 ); op(); mt( 0.524, 0.109 ); bc( 0.524, 0.111, 0.525, 0.112, 0.527, 0.112 ); bc( 0.529, 0.112, 0.531, 0.111, 0.531, 0.109 ); bc( 0.531, 0.108, 0.531, 0.103, 0.531, 0.101 ); bc( 0.531, 0.100, 0.529, 0.0994, 0.527, 0.0994 ); bc( 0.525, 0.0994, 0.524, 0.100, 0.524, 0.101 ); pt( 7 ); op(); mt( 0.444, 0.109 ); bc( 0.444, 0.111, 0.446, 0.112, 0.448, 0.112 ); bc( 0.450, 0.112, 0.451, 0.111, 0.451, 0.109 ); bc( 0.451, 0.108, 0.451, 0.103, 0.451, 0.101 ); bc( 0.451, 0.100, 0.450, 0.0994, 0.448, 0.0994 ); bc( 0.446, 0.0994, 0.444, 0.100, 0.444, 0.101 ); pt( 7 ); op(); mt( 0.537, 0.109 ); bc( 0.537, 0.111, 0.539, 0.112, 0.540, 0.112 ); bc( 0.542, 0.112, 0.544, 0.111, 0.544, 0.109 ); bc( 0.544, 0.108, 0.544, 0.103, 0.544, 0.101 ); bc( 0.544, 0.100, 0.542, 0.0994, 0.540, 0.0994 ); bc( 0.538, 0.0994, 0.537, 0.100, 0.537, 0.101 ); pt( 7 ); op(); mt( 0.458, 0.109 ); bc( 0.458, 0.111, 0.459, 0.112, 0.461, 0.112 ); bc( 0.463, 0.112, 0.465, 0.111, 0.465, 0.109 ); bc( 0.465, 0.108, 0.465, 0.103, 0.465, 0.101 ); bc( 0.465, 0.100, 0.463, 0.0994, 0.461, 0.0994 ); bc( 0.459, 0.0994, 0.458, 0.100, 0.458, 0.101 ); pt( 7 ); op(); mt( 0.550, 0.109 ); bc( 0.550, 0.111, 0.552, 0.112, 0.554, 0.112 ); bc( 0.555, 0.112, 0.557, 0.111, 0.557, 0.109 ); bc( 0.557, 0.108, 0.557, 0.103, 0.557, 0.101 ); bc( 0.557, 0.100, 0.555, 0.0994, 0.554, 0.0994 ); bc( 0.552, 0.0994, 0.550, 0.100, 0.550, 0.101 ); pt( 7 ); op(); mt( 0.471, 0.109 ); bc( 0.471, 0.111, 0.473, 0.112, 0.474, 0.112 ); bc( 0.476, 0.112, 0.478, 0.111, 0.478, 0.109 ); bc( 0.478, 0.108, 0.478, 0.103, 0.478, 0.101 ); bc( 0.478, 0.100, 0.476, 0.0994, 0.474, 0.0994 ); bc( 0.472, 0.0994, 0.471, 0.100, 0.471, 0.101 ); pt( 7 ); op(); mt( 0.563, 0.109 ); bc( 0.563, 0.111, 0.565, 0.112, 0.567, 0.112 ); bc( 0.568, 0.112, 0.570, 0.111, 0.570, 0.109 ); bc( 0.570, 0.108, 0.570, 0.103, 0.570, 0.101 ); bc( 0.570, 0.100, 0.569, 0.0994, 0.567, 0.0994 ); bc( 0.565, 0.0994, 0.563, 0.100, 0.563, 0.101 ); pt( 7 ); op(); mt( 0.484, 0.109 ); bc( 0.484, 0.111, 0.486, 0.112, 0.488, 0.112 ); bc( 0.490, 0.112, 0.491, 0.111, 0.491, 0.109 ); bc( 0.491, 0.108, 0.491, 0.103, 0.491, 0.101 ); bc( 0.491, 0.100, 0.490, 0.0994, 0.488, 0.0994 ); bc( 0.486, 0.0994, 0.484, 0.100, 0.484, 0.101 ); pt( 7 ); op(); mt( 0.576, 0.109 ); bc( 0.576, 0.111, 0.578, 0.112, 0.580, 0.112 ); bc( 0.582, 0.112, 0.583, 0.111, 0.583, 0.109 ); bc( 0.583, 0.108, 0.583, 0.103, 0.583, 0.101 ); bc( 0.583, 0.100, 0.582, 0.0994, 0.580, 0.0994 ); bc( 0.578, 0.0994, 0.576, 0.100, 0.576, 0.101 ); pt( 7 ); op(); mt( 0.590, 0.109 ); bc( 0.590, 0.111, 0.591, 0.112, 0.593, 0.112 ); bc( 0.595, 0.112, 0.596, 0.111, 0.597, 0.109 ); bc( 0.597, 0.108, 0.596, 0.103, 0.596, 0.101 ); bc( 0.596, 0.100, 0.595, 0.0994, 0.593, 0.0994 ); bc( 0.591, 0.0994, 0.590, 0.100, 0.590, 0.101 ); pt( 7 ); op(); mt( 0.404, 0.109 ); bc( 0.404, 0.111, 0.406, 0.112, 0.408, 0.112 ); bc( 0.410, 0.112, 0.411, 0.111, 0.411, 0.109 ); bc( 0.411, 0.108, 0.411, 0.103, 0.411, 0.101 ); bc( 0.411, 0.100, 0.410, 0.0994, 0.408, 0.0994 ); bc( 0.406, 0.0994, 0.404, 0.100, 0.404, 0.101 ); pt( 7 ); op(); mt( 0.498, 0.109 ); bc( 0.498, 0.111, 0.499, 0.112, 0.501, 0.112 ); bc( 0.503, 0.112, 0.504, 0.111, 0.504, 0.109 ); bc( 0.504, 0.108, 0.504, 0.103, 0.504, 0.101 ); bc( 0.504, 0.100, 0.503, 0.0994, 0.501, 0.0994 ); bc( 0.499, 0.0994, 0.498, 0.100, 0.498, 0.101 ); pt( 7 ); op(); mt( 0.172, 0.109 ); bc( 0.172, 0.111, 0.174, 0.112, 0.175, 0.112 ); bc( 0.177, 0.112, 0.179, 0.111, 0.179, 0.109 ); bc( 0.179, 0.108, 0.179, 0.103, 0.179, 0.101 ); bc( 0.179, 0.100, 0.177, 0.0994, 0.175, 0.0994 ); bc( 0.174, 0.0994, 0.172, 0.100, 0.172, 0.101 ); pt( 7 ); op(); mt( 0.265, 0.109 ); bc( 0.265, 0.111, 0.267, 0.112, 0.269, 0.112 ); bc( 0.270, 0.112, 0.272, 0.111, 0.272, 0.109 ); bc( 0.272, 0.108, 0.272, 0.103, 0.272, 0.101 ); bc( 0.272, 0.100, 0.271, 0.0994, 0.269, 0.0994 ); bc( 0.267, 0.0994, 0.265, 0.100, 0.265, 0.101 ); pt( 7 ); op(); mt( 0.185, 0.109 ); bc( 0.185, 0.111, 0.187, 0.112, 0.189, 0.112 ); bc( 0.191, 0.112, 0.192, 0.111, 0.192, 0.109 ); bc( 0.192, 0.108, 0.192, 0.103, 0.192, 0.101 ); bc( 0.192, 0.100, 0.191, 0.0994, 0.189, 0.0994 ); bc( 0.187, 0.0994, 0.185, 0.100, 0.185, 0.101 ); pt( 7 ); op(); mt( 0.278, 0.109 ); bc( 0.278, 0.111, 0.280, 0.112, 0.282, 0.112 ); bc( 0.284, 0.112, 0.285, 0.111, 0.285, 0.109 ); bc( 0.285, 0.108, 0.285, 0.103, 0.285, 0.101 ); bc( 0.285, 0.100, 0.284, 0.0994, 0.282, 0.0994 ); bc( 0.280, 0.0994, 0.278, 0.100, 0.278, 0.101 ); pt( 7 ); op(); mt( 0.199, 0.109 ); bc( 0.199, 0.111, 0.200, 0.112, 0.202, 0.112 ); bc( 0.204, 0.112, 0.206, 0.111, 0.206, 0.109 ); bc( 0.206, 0.108, 0.206, 0.103, 0.206, 0.101 ); bc( 0.206, 0.100, 0.204, 0.0994, 0.202, 0.0994 ); bc( 0.200, 0.0994, 0.199, 0.100, 0.199, 0.101 ); pt( 7 ); op(); mt( 0.292, 0.109 ); bc( 0.292, 0.111, 0.293, 0.112, 0.295, 0.112 ); bc( 0.297, 0.112, 0.298, 0.111, 0.298, 0.109 ); bc( 0.298, 0.108, 0.298, 0.103, 0.298, 0.101 ); bc( 0.298, 0.100, 0.297, 0.0994, 0.295, 0.0994 ); bc( 0.293, 0.0994, 0.292, 0.100, 0.292, 0.101 ); pt( 7 ); op(); mt( 0.212, 0.109 ); bc( 0.212, 0.111, 0.214, 0.112, 0.215, 0.112 ); bc( 0.217, 0.112, 0.219, 0.111, 0.219, 0.109 ); bc( 0.219, 0.108, 0.219, 0.103, 0.219, 0.101 ); bc( 0.219, 0.100, 0.217, 0.0994, 0.215, 0.0994 ); bc( 0.214, 0.0994, 0.212, 0.100, 0.212, 0.101 ); pt( 7 ); op(); mt( 0.305, 0.109 ); bc( 0.305, 0.111, 0.306, 0.112, 0.308, 0.112 ); bc( 0.310, 0.112, 0.312, 0.111, 0.312, 0.109 ); bc( 0.312, 0.108, 0.312, 0.103, 0.312, 0.101 ); bc( 0.312, 0.100, 0.310, 0.0994, 0.308, 0.0994 ); bc( 0.306, 0.0994, 0.305, 0.100, 0.305, 0.101 ); pt( 7 ); op(); mt( 0.225, 0.109 ); bc( 0.225, 0.111, 0.227, 0.112, 0.229, 0.112 ); bc( 0.231, 0.112, 0.232, 0.111, 0.232, 0.109 ); bc( 0.232, 0.108, 0.232, 0.103, 0.232, 0.101 ); bc( 0.232, 0.100, 0.231, 0.0994, 0.229, 0.0994 ); bc( 0.227, 0.0994, 0.225, 0.100, 0.225, 0.101 ); pt( 7 ); op(); mt( 0.318, 0.109 ); bc( 0.318, 0.111, 0.320, 0.112, 0.321, 0.112 ); bc( 0.323, 0.112, 0.325, 0.111, 0.325, 0.109 ); bc( 0.325, 0.108, 0.325, 0.103, 0.325, 0.101 ); bc( 0.325, 0.100, 0.323, 0.0994, 0.321, 0.0994 ); bc( 0.319, 0.0994, 0.318, 0.100, 0.318, 0.101 ); pt( 7 ); op(); mt( 0.239, 0.109 ); bc( 0.239, 0.111, 0.240, 0.112, 0.242, 0.112 ); bc( 0.244, 0.112, 0.246, 0.111, 0.246, 0.109 ); bc( 0.246, 0.108, 0.246, 0.103, 0.246, 0.101 ); bc( 0.246, 0.100, 0.244, 0.0994, 0.242, 0.0994 ); bc( 0.240, 0.0994, 0.239, 0.100, 0.239, 0.101 ); pt( 7 ); op(); mt( 0.331, 0.109 ); bc( 0.331, 0.111, 0.333, 0.112, 0.334, 0.112 ); bc( 0.336, 0.112, 0.338, 0.111, 0.338, 0.109 ); bc( 0.338, 0.108, 0.338, 0.103, 0.338, 0.101 ); bc( 0.338, 0.100, 0.336, 0.0994, 0.334, 0.0994 ); bc( 0.333, 0.0994, 0.331, 0.100, 0.331, 0.101 ); pt( 7 ); op(); mt( 0.344, 0.109 ); bc( 0.344, 0.111, 0.346, 0.112, 0.348, 0.112 ); bc( 0.349, 0.112, 0.351, 0.111, 0.351, 0.109 ); bc( 0.351, 0.108, 0.351, 0.103, 0.351, 0.101 ); bc( 0.351, 0.100, 0.350, 0.0994, 0.348, 0.0994 ); bc( 0.346, 0.0994, 0.344, 0.100, 0.344, 0.101 ); pt( 7 ); po( [ 0.0666, 0.0813, 0.0826, 0.0825, 0.0786, 0.0666], [ 0.0996, 0.0996, 0.101, 0.111, 0.115, 0.116], 7 ); po( [ 0.0576, 0.0495, 0.0578, 0.0649, 0.0649], [ 0.0998, 0.103, 0.115, 0.115, 0.0998], 7 ); po( [ 0.0475, 0.0559, 0.0475, 0.0358], [ 0.103, 0.115, 0.115, 0.103], 7 ); op(); mt( 0.159, 0.109 ); bc( 0.159, 0.111, 0.160, 0.112, 0.162, 0.112 ); bc( 0.164, 0.112, 0.166, 0.111, 0.166, 0.109 ); bc( 0.166, 0.108, 0.166, 0.103, 0.166, 0.101 ); bc( 0.166, 0.100, 0.164, 0.0994, 0.162, 0.0994 ); bc( 0.160, 0.0994, 0.159, 0.100, 0.159, 0.101 ); pt( 7 ); op(); mt( 0.252, 0.109 ); bc( 0.252, 0.111, 0.254, 0.112, 0.255, 0.112 ); bc( 0.257, 0.112, 0.259, 0.111, 0.259, 0.109 ); bc( 0.259, 0.108, 0.259, 0.103, 0.259, 0.101 ); bc( 0.259, 0.100, 0.257, 0.0994, 0.255, 0.0994 ); bc( 0.253, 0.0994, 0.252, 0.100, 0.252, 0.101 ); pt( 7 ); op(); mt( 0.334, 0.0677 ); bc( 0.376, 0.0760, 0.413, 0.0778, 0.448, 0.0851 ); bc( 0.487, 0.0932, 0.527, 0.0994, 0.536, 0.101 ); bc( 0.541, 0.102, 0.579, 0.105, 0.580, 0.102 ); bc( 0.581, 0.100, 0.490, 0.0752, 0.490, 0.0752 ); lt( 0.477, 0.0638 ); bc( 0.439, 0.0633, 0.373, 0.0636, 0.363, 0.0635 ); bc( 0.353, 0.0633, 0.335, 0.0634, 0.334, 0.0677 ); pt( 7 ); op(); mt( 0.853, 0.107 ); bc( 0.853, 0.104, 0.859, 0.100, 0.867, 0.100 ); bc( 0.876, 0.0998, 0.906, 0.102, 0.921, 0.104 ); bc( 0.937, 0.106, 0.948, 0.105, 0.950, 0.106 ); lt( 0.979, 0.126 ); bc( 0.979, 0.126, 0.957, 0.128, 0.949, 0.127 ); bc( 0.945, 0.127, 0.922, 0.121, 0.901, 0.116 ); bc( 0.877, 0.112, 0.853, 0.109, 0.853, 0.107 ); pt( 7 ); op(); mt( 0.468, 0.0799 ); bc( 0.468, 0.0778, 0.503, 0.0748, 0.523, 0.0753 ); bc( 0.542, 0.0758, 0.552, 0.0775, 0.551, 0.0796 ); bc( 0.551, 0.0817, 0.538, 0.0827, 0.529, 0.0827 ); bc( 0.521, 0.0827, 0.525, 0.0828, 0.518, 0.0827 ); bc( 0.511, 0.0826, 0.506, 0.0810, 0.502, 0.0809 ); lt( 0.468, 0.0799 ); pt( 7 ); op(); mt( 0.442, 0.0731 ); bc( 0.442, 0.0711, 0.476, 0.0680, 0.496, 0.0685 ); bc( 0.515, 0.0690, 0.521, 0.0706, 0.521, 0.0727 ); bc( 0.520, 0.0748, 0.511, 0.0759, 0.502, 0.0759 ); bc( 0.494, 0.0759, 0.498, 0.0760, 0.491, 0.0759 ); bc( 0.484, 0.0759, 0.479, 0.0742, 0.476, 0.0741 ); lt( 0.442, 0.0731 ); pt( 7 ); op(); mt( 0.421, 0.0633 ); bc( 0.421, 0.0613, 0.456, 0.0582, 0.475, 0.0587 ); bc( 0.495, 0.0592, 0.501, 0.0608, 0.500, 0.0629 ); bc( 0.500, 0.0650, 0.490, 0.0661, 0.482, 0.0661 ); bc( 0.473, 0.0661, 0.478, 0.0662, 0.471, 0.0661 ); bc( 0.464, 0.0661, 0.459, 0.0644, 0.455, 0.0643 ); lt( 0.421, 0.0633 ); pt( 7 ); rc( 0.361, 0.115, 0.0151, -0.0297, 3 ); rc( 0.385, 0.115, 0.0151, -0.0297, 3 ); rc( 0.769, 0.128, 0.0261, -0.0512, 3 ); rc( 0.113, 0.130, 0.0261, -0.0512, 3 ); op(); mt( 0.365, 0.109 ); bc( 0.365, 0.111, 0.367, 0.112, 0.368, 0.112 ); bc( 0.370, 0.112, 0.372, 0.111, 0.372, 0.109 ); bc( 0.372, 0.108, 0.372, 0.103, 0.372, 0.101 ); bc( 0.372, 0.100, 0.370, 0.0994, 0.368, 0.0994 ); bc( 0.367, 0.0994, 0.365, 0.100, 0.365, 0.101 ); lt( 0.365, 0.109 ); pt( 7 ); op(); mt( 0.389, 0.109 ); bc( 0.389, 0.111, 0.391, 0.112, 0.392, 0.112 ); bc( 0.394, 0.112, 0.396, 0.111, 0.396, 0.109 ); bc( 0.396, 0.108, 0.396, 0.103, 0.396, 0.101 ); bc( 0.396, 0.100, 0.394, 0.0994, 0.392, 0.0994 ); bc( 0.390, 0.0994, 0.389, 0.100, 0.389, 0.101 ); lt( 0.389, 0.109 ); pt( 7 ); g.SetAlpha( 1 ); g.ResetTrans(); if (this.ShowForces || this.ShowSpeed || this.ShowAccel || this.ShowCoordSys) { this.DrawForces( g, vsDisplacementVect ); } }, DrawForces: function( g, vsDisplacementVect ) { // depending on the vertical speed the airplane is displaced up or down // by vsDisplacementVect. Force vectors are given in ECEF coordinates, // so the arrows have to be rotated to surfaceAngle+pitch angle. function drawArrow( from, vect, oldVect, scale, color, type ) { // use oldVect = null if no old vector is available type = xDefNum( type, 0 ); // compute from and to arrow coordinates // rotate and move start arrow position (from) relative to CenterOfGravity from = V2.Add( me.CenterOfGravity, V2.Rotate( from, pitch+surfaceAngle ) ); var to = V2.Add( from, V2.Scale( vect, scale ) ); // calculate arrow symbol size depending on vect length var symbolSize = 10; var arrowLength = g.WinToPixX( V2.Length( V2.Sub( to, from ) ) ); if (arrowLength < 2*symbolSize) { symbolSize *= arrowLength/(2*symbolSize); if (symbolSize < 4 ) symbolSize = 4; } // draw arrow depending on type var sym = 'Arrow1'; var lineWidth = 1.5; if (type == 1) { sym = 'Arrow2'; lineWidth = 1; } g.SetMarkerAttr( sym, symbolSize, color, color, lineWidth ); g.Arrow( from, to, 1, 3 ); // 1 = end arrow, 3 = border + fill // draw trend arrow if (me.ShowTrend && oldVect) { // compute trend magnitude from vect and oldVect // trend = delta force / delta time = how fast does the force change var currMagnitude = V2.Length(vect); var oldMagnitude = V2.Length(oldVect); trend = (currMagnitude - oldMagnitude) / me.Sim.DeltaTime; if (Math.abs(trend) > 10 && currMagnitude > 100) { var trendArrowLength = 1.2 * g.PixToWinX(symbolSize); var trendDir = V2.Norm( V2.Sub( to, from ) ); var trendPos1 = V2.Add( to, V2.Scale( trendDir, 0.5 * trendArrowLength ) ); var trendPos2 = V2.Add( to, V2.Scale( trendDir, 1.5 * trendArrowLength ) ); symbolSize *= 0.75; if (symbolSize < 4) symbolSize = 4; g.SetMarkerAttr( sym, symbolSize, color, color, 1 ); if (trend > 0) { g.Arrow( trendPos1, trendPos2, 5, 3 ); // 5 = end arrow + no line, 3 = border + fill } else { g.Arrow( trendPos2, trendPos1, 5, 3 ); // 5 = end arrow + no line, 3 = border + fill } } } } var me = this; var pitch = this.Pitch; var surfaceAngle = V2.Angle( [0,1], this.Zsurface ); var oldAutoScalePix = g.SetAutoScalePix( true ); g.TransMove( -this.CenterOfGravity[0], this.CenterOfGravity[1] ); g.TransMove( vsDisplacementVect ); // draw coordinate system axes if (this.ShowCoordSys) { if (this.ShowSpeed || this.ShowForces || this.ShowAccel) { g.SetAlpha( 0.5 ); } drawArrow( this.CenterOfLift, this.Xsurface, null, 35, 'gray', 1 ); drawArrow( this.CenterOfLift, this.Zsurface, null, 25, 'gray', 1 ); drawArrow( this.CenterOfLift, this.Xaircraft, null, 32, 'brown', 1 ); g.SetAlpha( 1 ); } if (this.ShowSpeed) { // compute arrow start pos in front of the aircraft nose // and length proportional to TAS var speedScale = 0.3 / this.MaxTAS * this.AircraftLength; var arrowStart = 0.45 * this.AircraftLength; var from = V2.Add( this.CenterOfGravity, V2.Scale( this.SpeedDir, arrowStart ) ); var to = V2.Add( this.CenterOfGravity, V2.Scale( this.SpeedDir, arrowStart + this.TAS * speedScale ) ); // draw the arrow lines (red if limits are exceeded) var color = 'green'; if (this.LimitExceeded != '') color = 'red'; g.SetLineAttr( color, 5 ); g.Line( from, to ); g.SetLineAttr( 'white', 3 ) g.Line( from, to ); // displace the arrow tip to = V2.Add( to, V2.Scale( this.SpeedDir, g.PixToWinX(8) ) ); // draw arrow tip g.SetAreaAttr( 'white', color, 1 ); g.SetMarkerSize( 12 ); g.SetMarkerSymbol( 'ArrowRight' ); g.Arrow( from, to, 5, 3 ); // 5 = end arrow + no line, 3 = border + fill // draw speed trend arrow // compute trend magnitude from vect and oldVect // trend = delta TAS / delta time = how fast does the speed change var trend = (this.TAS - this.TASold) / this.Sim.DeltaTime; if (this.ShowTrend && Math.abs(trend) > 0.01) { var trendArrowLength = g.PixToWinX(12); var trendDir = V2.Norm( V2.Sub( to, from ) ); var trendPos1 = V2.Add( to, V2.Scale( trendDir, 0.5 * trendArrowLength ) ); var trendPos2 = V2.Add( to, V2.Scale( trendDir, 1.5 * trendArrowLength ) ); g.SetMarkerSize( 9 ); if (trend > 0) { g.Arrow( trendPos1, trendPos2, 5, 3 ); // 5 = end arrow + no line, 3 = border + fill } else { g.Arrow( trendPos2, trendPos1, 5, 3 ); // 5 = end arrow + no line, 3 = border + fill } } } if (this.ShowAccel) { var accel = V2.Length( this.State.Accel ); if (accel > 0.005) { // compute arrow start pos and length proportional to acceleration accelDir = V2.Norm( this.State.Accel ); var scale = Math.sqrt( accel / 10 ) * this.AircraftLength / 5; // compute a multiplicator curve depending on accelDir in range 0..1 var mult = 0.5 * Math.cos( 2 * V2.Angle( this.Xaircraft, accelDir ) ) + 0.5; var n = 3; // arrow in Z axis is 1/n times nearer than in X axis var arrowStart = 0.57 * this.AircraftLength * (mult + n - 1) / n; var from = V2.Add( this.CenterOfGravity, V2.Scale( accelDir, arrowStart ) ); var to = V2.Add( this.CenterOfGravity, V2.Scale( accelDir, arrowStart + scale ) ); // draw the arrow lines var color = 'red'; g.SetLineAttr( color, 3 ); g.Line( from, to ); g.SetLineAttr( 'white', 1 ) g.Line( from, to ); // displace the arrow tip to = V2.Add( to, V2.Scale( accelDir, g.PixToWinX(9) ) ); // draw arrow tip g.SetAreaAttr( 'white', color, 1 ); g.SetMarkerSize( 9 ); g.SetMarkerSymbol( 'ArrowRight' ); g.Arrow( from, to, 5, 3 ); // 5 = end arrow + no line, 3 = border + fill } } if (this.ShowForces) { var fScale = 1.5/this.MaxMass; var hfScale = fScale; if (this.ShowFxTimes5) { hfScale *= 5; // horizontal forces are scaled more than vertical forces } drawArrow( this.CenterOfLift, this.FLift, this.FLiftOld, fScale, 'blue' ); drawArrow( this.CenterOfGravity, this.FWeight, null, fScale, 'green' ); drawArrow( this.ElevatorPos, this.FTrim, this.FTrimOld, fScale, 'blue' ); drawArrow( this.CenterOfLift, this.FDrag, this.FDragOld, hfScale, 'red' ); drawArrow( this.EnginePos, this.FThrust, this.FThrustOld, hfScale, 'orange'); } g.SetAutoScalePix( oldAutoScalePix ); g.ResetTrans(); }, DrawElevatorTrimIndication: function( g, x, y, w, h ) { // x,y,w,h as viewport coordinates g.SetViewport( x, y, w, h, false, true ); g.SetWindowWH( -2, -3.5, 4, 7 ); var oldLineCap = g.LineCap; // elevator indication // elevator scale var xPos = -1; g.SetLineCap( 'square' ); g.SetLineAttr( 'black', g.ScalePix(2) ); g.Line( xPos, -2, xPos, 2 ); g.Line( xPos, 2, xPos-0.2, 2 ); g.Line( xPos, -2, xPos-0.2, -2 ); var yPos = 4 * (rad(-10)-this.MinElevatorAngle) / (this.MaxElevatorAngle-this.MinElevatorAngle) - 2; g.Line( xPos, yPos, xPos-0.1, yPos ); var yPos = 4 * (-this.MinElevatorAngle) / (this.MaxElevatorAngle-this.MinElevatorAngle) - 2; g.Line( xPos, yPos, xPos-0.2, yPos ); var yPos = 4 * (rad(10)-this.MinElevatorAngle) / (this.MaxElevatorAngle-this.MinElevatorAngle) - 2; g.Line( xPos, yPos, xPos-0.1, yPos ); var yPos = 4 * (rad(20)-this.MinElevatorAngle) / (this.MaxElevatorAngle-this.MinElevatorAngle) - 2; g.Line( xPos, yPos, xPos-0.1, yPos ); // pitch modulation indication due to pitch rate damping if (this.PitchDamping) { var yPos = 4 * (this.ElevatorAngle-this.MinElevatorAngle) / (this.MaxElevatorAngle-this.MinElevatorAngle) - 2; var pitchMod = yPos - Math.atan( 500 * this.PitchElevMod ) * 4 / Math.PI; if (pitchMod > 2) pitchMod = 2; if (pitchMod < -2) pitchMod = -2; g.SetLineCap( oldLineCap ); g.SetLineAttr( 'lightgreen', g.ScalePix(2) ); g.Line( xPos, yPos, xPos, pitchMod ); } // elevator angle indicator pointer var yPos = 4 * (this.ElevatorAngle-this.MinElevatorAngle) / (this.MaxElevatorAngle-this.MinElevatorAngle) - 2; g.SetMarkerAttr( 'ArrowLeft', g.WinToPixX(0.4), 'black', 'lightgreen', 1 ); g.Marker( xPos+0.05, yPos, 3 ); // elevator command indicator pointer var yPos = 4 * (this.ElevatorAngleCommand-this.MinElevatorAngle) / (this.MaxElevatorAngle-this.MinElevatorAngle) - 2; g.SetMarkerAttr( 'ArrowRight', g.WinToPixX(0.4), 'black', 'Magenta', 1 ); g.Marker( xPos-0.05, yPos, 3 ); // elevator angle value yPos = -2.75; g.SetAreaAttr( 'black', 'black', 1 ); g.RectWH( xPos-0.75, yPos-0.5, 1.5, 1, 3 ); g.SetTextAttrS( 'Arial', g.WinToPixY(1), 'lightgreen', 'C' ); g.Text( this.FormatValue( deg(this.ElevatorAngle), '', 2 ), xPos, -2.8 ); // trim indication // trim scale var xPos = 1; g.SetLineCap( 'square' ); g.SetLineAttr( 'black', g.ScalePix(2) ); g.Line( xPos, -2, xPos, 2 ); g.Line( xPos, -2, xPos-0.2, -2 ); g.Line( xPos, -1, xPos-0.1, -1 ); g.Line( xPos, 0, xPos-0.2, 0 ); g.Line( xPos, 1, xPos-0.1, 1 ); g.Line( xPos, 2, xPos-0.2, 2 ); // trim angle indicator pointer var yPos = 4 * (this.TrimAngle-this.MinTrimAngle) / (this.MaxTrimAngle-this.MinTrimAngle) - 2; g.SetMarkerAttr( 'ArrowLeft', g.WinToPixX(0.4), 'black', 'lightgreen', 1 ); g.Marker( xPos+0.05, yPos, 3 ); // trim command indicator pointer var yPos = 4 * (this.TrimAngleCommand-this.MinTrimAngle) / (this.MaxTrimAngle-this.MinTrimAngle) - 2; g.SetMarkerAttr( 'ArrowRight', g.WinToPixX(0.4), 'black', 'Magenta', 1 ); g.Marker( xPos-0.05, yPos, 3 ); // trim angle value yPos = -2.75; g.SetAreaAttr( 'black', 'black', 1 ); g.RectWH( xPos-0.75, yPos-0.5, 1.5, 1, 3 ); g.SetTextAttrS( 'Arial', g.WinToPixY(1), 'lightgreen', 'C' ); g.Text( this.FormatValue( deg(this.TrimAngle), '', 2 ), xPos, -2.8 ); // labels g.SetTextAttrS( 'Arial', g.WinToPixY(1), 'black', 'bC' ); g.Text( 'ELEV', -1, 2.75 ); g.Text( 'TRIM', 1, 2.75 ); g.SetTextAttrS( 'Arial', g.WinToPixY(0.9), 'black', 'bC' ); g.Text( 'UP', 0, 1.75 ); g.Text( 'DW', 0, -1.75 ); g.SetLineCap( oldLineCap ); }, DrawEngineIndication: function( g, x, y, w, h ) { // x,y,w,h as viewport coordinates g.SetViewport( x, y, w, h, false, true ); g.SetWindowWH( -3, -2.5, 6, 6 ); var oldLineCap = g.LineCap; // arc scale var r = 2; g.SetLineAttr( 'black', g.ScalePix(2) ); g.Arc( 0, 0, r, rad(36), rad(216), 1 ); g.SetLineAttr( 'red', g.ScalePix(2) ); g.Arc( 0, 0, r, 0, rad(36), 1 ); function tic(a) { var c = Math.cos(rad(a)); var s = Math.sin(rad(a)); g.Line( r2*c, r2*s, r*c, r*s ); } function val(a,v) { var c = Math.cos(rad(a)); var s = Math.sin(rad(a)); g.Text( v, r*c, r*s ); } var r2 = 0.85 * r; tic( 0 ); g.SetLineAttr( 'black', g.ScalePix(2) ); tic( 36 ); tic( 72 ); tic( 108 ); tic( 144 ); tic( 216 ); r = 1.35; g.SetTextAttrS( 'Arial', g.WinToPixX(0.5), 'black', 'C' ); val( 36, 10 ); val( 72, 8 ); val( 108, 6 ); val( 144, 4 ); // N1 pointer var a = 216 - this.N1 * 180; r = 2.2; r2 = 0; g.SetLineAttr( 'black', g.ScalePix(4) ); tic( a ); g.SetLineAttr( 'lightgreen', g.ScalePix(2) ); tic( a ); // N1 command indicator a = 216 - this.N1Command * 180; r = 2.4; var c = Math.cos(rad(a)); var s = Math.sin(rad(a)); g.SetMarkerAttr( 'Circle', g.WinToPixX(0.3), 'black', 'magenta', g.ScalePix(1) ); g.Marker( r*c, r*s, 3 ); // N1 value yPos = -1.95; g.SetAreaAttr( 'black', 'black', 1 ); g.RectWH( -1.2, yPos-0.35, 2.4, 0.95, 2.7 ); g.SetTextAttrS( 'Arial', g.WinToPixY(0.86), 'lightgreen', 'C' ); g.Text( this.FormatValue( this.N1*100, '', 2 ), 0, yPos+0.055 ); // N1 label g.SetTextAttrS( 'Arial', g.WinToPixX(0.78), 'black', 'bC' ); g.Text( 'N1', 0, 2.8 ); g.SetLineCap( oldLineCap ); }, DrawPfd: function( g, x, y, w, h ) { // PFD = primary flight display // x,y,w,h as viewport coordinates g.SetViewport( x, y, w, h, false, true ); // PFD background var bg = g.GetFrameRect(); g.SetAreaAttr( 'black', 'black', 1 ); g.Rect( bg, 3 ); // PFD is rastered with a raster width of dspUnitDiv var dspUnitDiv = w / 6; var dspMargin = dspUnitDiv / 15; // draw Speed Tape var dspX = x + dspMargin; var dspY = y + dspMargin; var dspW = dspUnitDiv - 2 * dspMargin; var textH = dspW / 3; var dspH = h - 2 * dspMargin - 1.5 * textH; this.DrawSpeedTape( g, dspX, dspY, dspW, dspH ); g.SetViewport( x, y, w, h, false, true ); // draw Mach number below speed tape g.SetTextAttrS( 'Arial', textH, 'white', 'bC' ); var oldTrans = g.SelectTrans( 'viewport' ); var mach = this.Mach.toPrecision(3); g.Text( ''+mach, dspX + 0.5 * dspW, dspH + 1.1*textH ); g.SelectTrans( oldTrans ); // draw VS Tape (VS = vertical speed) var dspX = x + 5 * dspUnitDiv + dspMargin; var dspY = y + dspMargin; var dspW = dspUnitDiv - 2 * dspMargin; var textH = dspW / 3; var dspH = h - 2 * dspMargin - 1.5 * textH; this.DrawVSTape( g, dspX, dspY, dspW, dspH ); g.SetViewport( x, y, w, h, false, true ); // Altimeter Tape var dspX = x + 4 * dspUnitDiv + dspMargin; var dspY = y + dspMargin; var dspW = dspUnitDiv - 2 * dspMargin; var textH = dspW / 3; var dspH = h - 2 * dspMargin - 1.5 * textH; this.DrawAltTape( g, dspX, dspY, dspW, dspH ); g.SetViewport( x, y, w, h, false, true ); // draw vertical speed VS below Altitude tape g.SetTextAttrS( 'Arial', textH, 'white', 'bCW' ); var oldTrans = g.SelectTrans( 'viewport' ); var vs = Math.floor( ms_ftmin(this.VS) + 0.5 ); g.Text( 'VS '+vs, dspX, dspH + 1.1*textH ); g.SelectTrans( oldTrans ); // draw attitude indicator (AI) var dspX = x + dspUnitDiv + dspMargin; var dspY = y + 0.8 * dspUnitDiv + dspMargin; var dspW = 3 * dspUnitDiv - 2 * dspMargin; var dspH = 3 * dspUnitDiv - 2 * dspMargin; this.DrawAI( g, dspX, dspY, dspW, dspH ); g.SetViewport( x, y, w, h, false, true ); // TAS var textMargin = g.ScalePix(3); var oldTrans = g.SelectTrans( 'viewport' ); var textH = dspUnitDiv / 3.5; var tas = ms_kt(this.TAS); g.SetTextAttrS( 'Arial', textH, 'white', 'bNW', textMargin ); g.Text( 'TAS '+Math.floor(tas+0.5), dspUnitDiv, 3 ); // Time var time = NumFormatter.Format( (this.Sim.SimulTime-this.TimeOffset)/3600, 'hms', 4 ); g.SetTextAttrS( 'Arial', textH, 'white', 'NW', textMargin ); g.Text( ''+time, dspUnitDiv, 3+1.3*textH ); // AoA var AoA = deg(this.AoA); g.SetTextAttrS( 'Arial', textH, 'white', 'bNE', textMargin ); g.Text( 'AoA '+AoA.toFixed(1)+'°', 4 * dspUnitDiv, 3 ); // Trim Angle var trim = deg(this.TrimAngle); g.SetTextAttrS( 'Arial', textH, 'white', 'NE', textMargin ); g.Text( 'Trim '+trim.toFixed(1)+'°', 4 * dspUnitDiv, 3+1.3*textH ); // Temperature var temp = this.AirTemperature; var dspY = 0.75 * dspUnitDiv + dspMargin; var ty = dspY + 3 * dspUnitDiv; g.SetTextAttrS( 'Arial', textH, 'white', 'NW', textMargin ); g.Text( temp.toFixed(1)+'°C', 1 * dspUnitDiv, ty ); // Pressure var p = this.AirPressure / 100; var ty = dspY + 3 * dspUnitDiv + 1.3 * textH; g.SetTextAttrS( 'Arial', textH, 'white', 'NW', textMargin ); g.Text( p.toFixed(1)+' hPa', 1 * dspUnitDiv, ty ); // N1 var n1 = this.N1 * 100; var ty = dspY + 3 * dspUnitDiv; g.SetTextAttrS( 'Arial', textH, 'white', 'NE', textMargin ); g.Text( 'N1 '+n1.toFixed(1)+'%', 4 * dspUnitDiv, ty ); // G-Force var gAtElevation = this.Gravity( this.Elevation ); var gForce = V2.Add( V2.Scale(this.Zsurface, -gAtElevation), V2.Scale( this.State.Accel, -1 ) ); var gForceSign = V2.ScalarProd( this.Zaircraft, gForce ) > 0 ? -1 : 1; var gForceMagnitude = gForceSign * V2.Length( gForce ) / this.g; var ty = dspY + 3 * dspUnitDiv + 1.3 * textH; g.SetTextAttrS( 'Arial', textH, 'white', 'NE', textMargin ); g.Text( 'G '+gForceMagnitude.toFixed(3), 4 * dspUnitDiv, ty ); // indicate Limit Exceeded if (this.LimitExceeded != '') { g.SetTextAttrS( 'Arial', textH, 'red', 'bNC', g.ScalePix(2) ); g.Text( this.LimitExceeded, 5.5 * dspUnitDiv, 3 ); } g.SelectTrans( oldTrans ); }, DrawSpeedTape: function( g, x, y, w, h ) { // x,y,w,h as viewport coordinates g.SetViewport( x, y, w, h, false, true ); var CAS = ms_kt(this.CAS); // backgroung var bg = g.GetFrameRect(); g.SetAreaAttr( '#666', '#666', 1 ); g.Rect( bg, 3 ); // draw ticks var range = 120; var low = CAS - range / 2; var high = CAS + range / 2; var tickSpacing = 10; // kt CAS g.SetWindowWH( 0, low, 5, high-low ); g.SetLineAttr( 'white', g.ScalePix(2) ); var first = (Math.floor( low / tickSpacing ) - 1 ) * tickSpacing; var last = (Math.floor( high / tickSpacing ) + 2) * tickSpacing; for (var tic = first; tic < last; tic += tickSpacing) { g.Line( 4, tic, 5, tic ); } // draw tick values g.SetTextAttrS( 'Arial', w/4, 'white', 'C' ); var tickSpacing = 20; var first = (Math.floor( low / tickSpacing ) - 1) * tickSpacing; var last = (Math.floor( high / tickSpacing ) + 2) * tickSpacing; for (var tic = first; tic < last; tic += tickSpacing) { if (tic >= 0) { g.Text( ''+(tic), 2.5, tic ); } } // draw current value var textH = w / 3; var boxH2 = g.PixToWinY( textH ); var boxB = CAS - boxH2; var boxT = CAS + boxH2; g.SetTextAttrS( 'Arial', textH, 'white', 'bW', g.ScalePix(2) ); g.SetAreaAttr( 'black', 'white', g.ScalePix(1) ); g.Polygon( [ 0, 3, 4, 3, 0 ], [ boxB, boxB, CAS, boxT, boxT ], 7 ); // 7 = border+fill+close var CASint = Math.floor( CAS + 0.5 ); g.Text( ''+CASint, 0, CAS ); // draw stall speed bar var stallSpeed = ms_kt(this.StallSpeed); g.SetAreaAttr( 'red', 'red', g.ScalePix(1) ); g.Rect( 4.5, 0, 5, stallSpeed, 2 ); // 2 = fill var maneuvreSpeed = ms_kt(this.LowestManeuverSpeed); g.Rect( 4.5, 0, 5, maneuvreSpeed, 1 ); // 1 = border // draw max operating speed bar var vmo = ms_kt(this.MaxOperatingSpeed); g.SetAreaAttr( 'red', 'red', g.ScalePix(1) ); g.Rect( 4.5, vmo, 5, 1000 , 3 ); }, DrawVSTape: function( g, x, y, w, h ) { // x,y,w,h as viewport coordinates g.SetViewport( x, y+h/10, w, 8/10*h, false, true ); var vs = ms_ftmin(this.VS); // backgroung var range = 12000; var low = -range / 2; var high = range / 2; g.SetWindowWH( 0, low, 8, range ); g.SetAreaAttr( '#666', '#666', 1 ); g.Polygon( [ 0, 4, 8, 8, 4, 0 ], [ -high, -high, -high/2, high/2, high, high ], 7 ); // draw ticks var tickSpacing = 500; // ft g.SetWindowWH( 0, low+tickSpacing/2, 8, range-tickSpacing ); g.SetLineAttr( 'white', g.ScalePix(2) ); for (var tic = low; tic < high; tic += tickSpacing) { g.Line( 2.9, tic, 3.9, tic ); } // draw tick values g.SetTextAttrS( 'Arial', w/5, 'white', 'E', g.ScalePix(2) ); var tickSpacing = 1000; for (var tic = low+tickSpacing; tic < high; tic += tickSpacing) { g.Text( ''+Math.floor(tic/1000), 2.6, tic ); } // draw needle var vsDsp = vs; if (vsDsp < -range/2) vsDsp = -range/2; if (vsDsp > range/2) vsDsp = range/2; g.Line( 4, vsDsp, 8, vsDsp/2 ); }, DrawAltTape: function( g, x, y, w, h ) { // x,y,w,h as viewport coordinates g.SetViewport( x, y, w, h, false, true ); var Alt = m_ft(this.Altitude); // backgroung var bg = g.GetFrameRect(); g.SetAreaAttr( '#666', '#666', 1 ); g.Rect( bg, 3 ); // draw ticks var range = 1050; var low = Alt - range / 2; var high = Alt + range / 2; var tickSpacing = 100; // ft g.SetWindowWH( 0, low, 5, high-low ); g.SetLineAttr( 'white', g.ScalePix(2) ); var first = (Math.floor( low / tickSpacing ) - 1)* tickSpacing; var last = (Math.floor( high / tickSpacing ) + 2) * tickSpacing; for (var tic = first; tic < last; tic += tickSpacing) { g.Line( 0, tic, 1, tic ); } // draw tick values g.SetTextAttrS( 'Arial', w/4, 'white', 'E', g.ScalePix(2) ); var tickSpacing = 200; var first = (Math.floor( low / tickSpacing ) - 1) * tickSpacing; var last = (Math.floor( high / tickSpacing ) + 2) * tickSpacing; for (var tic = first; tic < last; tic += tickSpacing) { if (tic >= 0) { g.Text( ''+Math.floor(tic/1), 5, tic ); } } // draw current value g.SetClipping(''); var textH = w / 3; var boxH2 = g.PixToWinY( textH ); var boxB = Alt - boxH2; var boxT = Alt + boxH2; g.SetTextAttrS( 'Arial', textH, 'white', 'bE', g.ScalePix(2) ); g.SetAreaAttr( 'black', 'white', g.ScalePix(1) ); g.Polygon( [ 0, 1, 6, 6, 1 ], [ Alt, boxB, boxB, boxT, boxT ], 7 ); var AltInt = Math.floor( Alt + 0.5 ); g.Text( ''+AltInt, 6, Alt ); }, DrawAI: function( g, x, y, w, h ) { // x,y,w,h as viewport coordinates g.SetViewport( x, y, w, h, false, true ); var pitch = deg(this.Pitch); var range = 45; var range2 = range / 2; var low = pitch - range2; var high = pitch + range2; g.SetWindowWH( -7, low, 14, high-low ); // draw background (ground and sky depending on pitch angle) g.SetAreaAttr( 'blue', 'blue', 1 ); g.Rect( -7, -270, 7, -180, 2 ); g.Rect( -7, 0, 7, 180, 2 ); g.SetAreaAttr( 'brown', 'brown', 1 ); g.Rect( -7, -180, 7, 0, 2 ); g.Rect( -7, 180, 7, 270, 2 ); g.SetLineAttr( 'white', g.ScalePix(2) ); g.Line( -7, -180, 7, -180 ); g.Line( -7, 0, 7, 0 ); g.Line( -7, 180, 7, 180 ); // draw ticks var tickSpacing = 2.5; // deg g.SetLineAttr( 'white', g.ScalePix(1.5) ); var first = Math.floor( low / tickSpacing ) * tickSpacing; var last = (Math.floor( high / tickSpacing ) + 1) * tickSpacing; for (var tic = first; tic < last; tic += tickSpacing) { var ticAbs = Math.abs( tic ) / 5; var rem = ticAbs - Math.floor( ticAbs + 0.001 ); if (rem > 0.001) { g.Line( -1, tic, 1, tic ); } } var tickSpacing = 5; // deg var first = Math.floor( low / tickSpacing ) * tickSpacing; var last = (Math.floor( high / tickSpacing ) + 1) * tickSpacing; for (var tic = first; tic < last; tic += tickSpacing) { var ticAbs = Math.abs( tic ) / 10; var rem = ticAbs - Math.floor( ticAbs + 0.001 ); if (rem > 0.001) { g.Line( -2, tic, 2, tic ); } } var tickSpacing = 10; // deg var first = Math.floor( low / tickSpacing ) * tickSpacing; var last = (Math.floor( high / tickSpacing ) + 1) * tickSpacing; for (var tic = first; tic < last; tic += tickSpacing) { g.Line( -3, tic, 3, tic ); } // draw tick values var tickSpacing = 10; var first = Math.floor( (low / tickSpacing) - 1 ) * tickSpacing; var last = Math.floor( (high / tickSpacing) + 2 ) * tickSpacing; g.SetTextAttrS( 'Arial', w/12, 'white', 'CE', g.ScalePix(4) ); for (var tic = first; tic < last; tic += tickSpacing) { var ticv = tic; if (ticv > 90) ticv = 180 - ticv; if (ticv < -90) ticv = -180 - ticv; if (ticv != 0) { g.Text( ''+(Math.floor(ticv)), -3, tic ); } } g.SetTextAttrS( 'Arial', w/12, 'white', 'CW', g.ScalePix(4) ); for (var tic = first; tic < last; tic += tickSpacing) { var ticv = tic; if (ticv > 90) ticv = 180 - ticv; if (ticv < -90) ticv = -180 - ticv; if (ticv != 0) { g.Text( ''+(Math.floor(ticv)), 3, tic ); } } // draw aircraft symbol var asC = (high + low) / 2; var asH = (high - low) / 80; var asW = asH * 14 / (high-low); var asL = -6; var asR = -3; g.SetAreaAttr( 'black', 'white', g.ScalePix(1.5) ); g.Polygon( [ asL, asR-asW, asR-asW, asR+asW, asR+asW, asL ], [ asC-asH, asC-asH, asC-3*asH, asC-3*asH, asC+asH, asC+asH ], 7 ); var asL = 3; var asR = 6; g.Polygon( [ asL-asW, asL+asW, asL+asW, asR, asR, asL-asW ], [ asC-3*asH, asC-3*asH, asC-asH, asC-asH, asC+asH, asC+asH ], 7 ); g.Rect( -asW, asC-asH, asW, asC+asH, 3 ); // draw up-down arrows if (pitch > 20 && pitch < 90) { g.SetMarkerAttr( 'ArrowDown', g.WinToPixX(1), 'black', 'yellow', 1 ); g.Marker( -6, low+1.5, 3 ); g.Marker( 6, low+1.5, 3 ); } if (pitch < -10 || pitch >= 90) { g.SetMarkerAttr( 'ArrowUp', g.WinToPixX(1), 'black', 'yellow', 1 ); g.Marker( -6, high-1.5, 3 ); g.Marker( 6, high-1.5, 3 ); } // draw flight path vector symbol if (this.ShowFlightPath) { var r = w / 20; var angle = pitch - deg(V2.Angle( this.SpeedDir, this.Xaircraft )); g.SetMarkerAttr( 'FPSymbol', 2*r, 'orange', 'orange', g.ScalePix(2) ); g.Marker( 0, angle, 1 ); } g.SetLineAttr( 'black', 1 ); g.Frame(); }, DrawDataDisplay: function( g, x, y, w, h ) { g.SetViewport( x, y, w, h, false, true ); g.SetWindowWH( 0, h, w, -h ); g.SetAreaAttr( '#888', 'black', 1 ); g.Frame(3); this.DrawVectValue( g, 0, 0, 'Lift', this.FLift, 'N', 'blue', w, h ); this.DrawVectValue( g, 1, 0, 'Trim', this.FTrim, 'N', 'blue', w, h ); this.DrawVectValue( g, 2, 0, 'Weight', this.FWeight, 'N', 'green', w, h ); this.DrawVectValue( g, 3, 0, 'Thrust', this.FThrust, 'N', 'orange', w, h ); this.DrawVectValue( g, 4, 0, 'Drag', this.FDrag, 'N', 'red', w, h ); this.DrawVectValue( g, 0, 1, 'Ftot', this.Ftot, 'N', 'magenta', w, h ); this.DrawVectValue( g, 1, 1, 'Acc', this.State.Accel, 'm/s^2', 'magenta', w, h ); this.DrawVectValue( g, 2, 1, 'Speed', this.State.Speed, 'm/s', 'magenta', w, h ); this.DrawScalarValue( g, 3, 1, 'Speed of Sound', this.SpeedOfSound, 'm/s', 4, 'lightblue', w, h ); this.DrawScalarValue( g, 4, 1, 'Air Density', this.AirDensity, 'kg/m^3', 4, 'lightblue', w, h ); var groundSpeed = V2.ScalarProd( this.Xsurface, this.State.Speed ); this.DrawScalarValue( g, 0, 2, 'Pitch', deg(this.Pitch), '°', 2, 'brown', w, h ); this.DrawScalarValue( g, 1, 2, 'Pitch Rate', deg(this.PitchRate), '°/s', 3, 'brown', w, h ); this.DrawScalarValue( g, 2, 2, 'Ground Speed', ms_kmh(groundSpeed), 'km/h', 3, 'black', w, h ); this.DrawScalarValue( g, 3, 2, 'Elevation', m_ft(this.Elevation), 'ft', 4, 'black', w, h ); this.DrawScalarValue( g, 4, 2, 'Elevation', this.Elevation, 'm', 4, 'black', w, h ); }, DrawScalarValue: function( g, row, col, label, val, unit, digits, color, w, h ) { var textSize = 0.065 * h; var margin = g.ScalePix(5); var lineHeight = textSize + g.ScalePix(3); var textW = ( w / 3 ) - margin; var textH = ( h / 5 ) - margin; var txtL = col * (textW + margin) + margin/2; var txtT = row * (textH + margin) + g.ScalePix(margin/2); var boxHeight = 2 * lineHeight + g.ScalePix(2); g.SetAreaAttr( 'white', color, g.ScalePix(2) ); g.RectWH( txtL, txtT, textW, boxHeight, 3 ); var txtX = txtL + 0.5 * textW; var txtY = txtT; g.SetTextAttrS( 'Arial', textSize, 'black', 'NC', g.ScalePix(2) ); g.Text( label, txtX, txtY ); txt = this.FormatValue( val, unit, digits ); g.Text( txt, txtX, txtY + lineHeight ); }, DrawVectValue: function( g, row, col, label, vect, unit, color, w, h ) { var textSize = 0.065 * h; var margin = g.ScalePix(5); var lineHeight = textSize + g.ScalePix(3); var textW = ( w / 3 ) - margin; var textH = ( h / 5 ) - margin; var txtL = col * (textW + margin) + margin/2; var txtT = row * (textH + margin) + g.ScalePix(margin/2); var boxHeight = 2 * lineHeight + g.ScalePix(2); g.SetAreaAttr( 'white', color, g.ScalePix(2) ); g.RectWH( txtL, txtT, textW, boxHeight, 3 ); var txtX = txtL + 0.5 * textW; var txtY = txtT; var value = V2.Length( vect ); var txt = label + ' = ' + this.FormatValue( value, unit ); g.SetTextAttrS( 'Arial', textSize, 'black', 'NC', g.ScalePix(2) ); g.Text( txt, txtX, txtY ); var x = V2.ScalarProd( this.Xsurface, vect ); var y = V2.ScalarProd( this.Zsurface, vect ); txt = '( ' + this.FormatValue( x ) + ' ' + this.FormatValue( y ) + ' )'; g.Text( txt, txtX, txtY + lineHeight ); }, FormatValue: function( value, unit, digits ) { digits = xDefNum( digits, 2 ); if (xStr(unit)) { unit = ' ' + unit; } else { unit = ''; } var sign = value < 0 ? '-' : ' '; value = Math.abs( value ); if (value < Math.pow(10,-digits)/2) sign = ' '; if (value < 1) { return sign + NumFormatter.Format( value, 'fix0', digits, unit ); } else { if (unit == ' m' && value >= 999500) { return sign + NumFormatter.Format( value/1000, 'fix0', 0, ' km' ); } else { return sign + NumFormatter.Format( value, 'unit', digits+1, unit ); } } }, }; /////////////////////////////////////////////////// var AircraftGraph = NewGraph2D( { Id: 'AircraftGraph', Width: '100%', Height: '50%', DrawFunc: function(){ AircraftModel.Draw(); }, AutoReset: true, AutoClear: true, AutoScalePix: false, } ); var AircraftSim = new Sim( { SimObj: AircraftModel, ResetFuncs: function(sim) { sim.SimObj.OnSimReset(); }, TimeStepFuncs: function(sim) { sim.SimObj.OnSimUpdate(); }, FrameFuncs: function(sim) { sim.SimObj.OnSimDraw(); }, } ); AircraftModel.Init( AircraftGraph, AircraftSim ); AircraftSim.Run( true ); // Tabs Button Handlers xOnLoad( function() { Tabs.AddButtonClickHandler( 'ControlsTab', 'TabsPauseButton', PauseSimulation ); Tabs.AddButtonClickHandler( 'ControlsTab', 'TabsResetTimeButton', ResetTime ); Tabs.AddButtonClickHandler( 'ControlsTab', 'TabsResetPosButton', ResetPosition ); Tabs.AddButtonClickHandler( 'ControlsTab', 'TabsResetButton', ResetSimulation ); } ); function ResetSimulation( buttonData ) { if (AircraftSim.Running) { AircraftSim.Run(true); } else { AircraftSim.Reset(); } } function ResetTime( buttonData ) { AircraftModel.ResetTime(); } function ResetPosition( buttonData ) { AircraftModel.ResetPos(); } function PauseSimulation( buttonData ) { AircraftSim.Pause(); Tabs.SetButtonText( buttonData, AircraftSim.Running ? 'Stop' : 'Run' ); } ///////////////////////////////////////////////////////////////// ControlPanels.NewSliderPanel( { Name: 'FlightControls', ModelRef: 'AircraftModel', NCols: 1, ValuePos: 'left', OnModelChange: function(field){ AircraftModel.Update(field); }, Format: 'fix0', Digits: 2, ReadOnly: false, PanelFormat: 'InputWidth75' } ).AddValueSliderField( { Name: 'ElevatorLever', Label: 'Elevator', Color: 'black', Min: AircraftModel.MinElevatorAngle, Max: AircraftModel.MaxElevatorAngle, LowerLimit: AircraftModel.MinElevatorAngle, UpperLimit: AircraftModel.MaxElevatorAngle, SnapTo: 0, Mult: rad(1), Units: '°', Inc: 1, } ).AddValueSliderField( { Name: 'TrimLever', Label: 'Trim', Color: 'green', Min: AircraftModel.MinTrimAngle, Max: AircraftModel.MaxTrimAngle, LowerLimit: AircraftModel.MinTrimAngle, UpperLimit: AircraftModel.MaxTrimAngle, SnapTo: (AircraftModel.MinTrimAngle+AircraftModel.MaxTrimAngle)/2, Mult: rad(1), Units: '°', Inc: 1, } ).AddValueSliderField( { Name: 'ThrustLever', Label: 'Thrust', Color: 'red', Min: 0, Max: 1, LowerLimit: 0, UpperLimit: 1, Mult: 0.01, Units: '%', Inc: 1, } ).AddValueSliderField( { Name: 'Mass', Color: 'gray', Min: 38600, Max: 73600, LowerLimit: 38600, UpperLimit: 73600, Units: 't', Mult: 1000, Inc: 1, } ).AddValueSliderField( { Name: 'CG', Color: 'gray', Min: -1, Max: 1, LowerLimit: -1, UpperLimit: 1, SnapTo: 0, } ).AddValueSliderField( { Name: 'DeltaPressure', Label: 'ΔP', Color: 'blue', Min: -3000, Max: 3000, Units: 'hPa', Mult: 100, SnapTo: 0, LowerLimit: -3000, UpperLimit: 3000, Inc: 1, } ).AddValueSliderField( { Name: 'TimeSpeed', ValueRef: 'AircraftSim.TimeSpeed', Label: 'Time x', Color: 'black', Min: 0, Max: 2, LowerLimit: 1, UpperLimit: 100, LogScale: true, SnapTo: [ 0.30103, 0.69897, 1, 1.30103, 1.69897 ], SnapRange: 50, } ).Render(); /////////////////////////////////////////////////////// ControlPanels.NewPanel( { Name: 'OptionsPanel', ModelRef: 'AircraftModel', OnModelChange: function(field){ AircraftModel.Update(field); }, NCols: 2, Format: 'fix0', Digits: 0, PanelFormat: 'InputWidth75' } ).AddCheckboxField( { Name: 'OptionsShow', Label: 'Show', NCols: 8, ColSpan: 3, Items: [ { Name: 'ShowAircraft', Text: 'Aircraft', }, { Name: 'ShowForces', Text: 'Forces', }, { Name: 'ShowSpeed', Text: 'Speed', }, { Name: 'ShowAccel', Text: 'Accel', }, { Name: 'ShowTrend', Text: 'Trend', }, { Name: 'ShowCoordSys', Text: 'Coords', }, { Name: 'ShowFxTimes5', Text: 'Fx × 5', }, { Name: 'ShowFlightPath', Text: 'FPV', }, ] } ).AddCheckboxField( { Name: 'OptionsPhugDamp', Label: 'Enable', NCols: 1, Items: [ { Name: 'PitchDamping', Text: 'Pitch Damping', }, ] } ).AddTextField( { Name: 'EarthRadius', Label: 'Earth Radius', Units: '% of 6371 km', Mult: 63710, LowerLimit: 63710, UpperLimit: 6.371e9, Inc: 10, } ).Render(); /////////////////////////////////////////////////////// ControlPanels.NewPanel( { Name: 'InitValuesPanel', ModelRef: 'AircraftModel', NCols: 2, Format: 'fix0', Digits: 2, ReadOnly: false, PanelFormat: 'InputNormalWidth' } ).AddTextField( { Name: 'InitTAS', Label: 'TAS', LowerLimit: 0, UpperLimit: 340, Mult: kt_ms(1), Units: 'kt', } ).AddTextField( { Name: 'InitVS', Label: 'VS', LowerLimit: ftmin_ms(-10000), UpperLimit: ftmin_ms(10000), Mult: ftmin_ms(1), Units: 'ft/min', } ).AddTextField( { Name: 'InitAltitude', Label: 'Altitude', LowerLimit: 0, UpperLimit: 50000, // instable above Mult: ft_m(1), Units: 'ft', } ).AddTextField( { Name: 'InitPitch', Label: 'Pitch', LowerLimit: rad(-45), UpperLimit: rad(45), Mult: rad(1), Units: '°', } ).AddTextField( { Name: 'InitTrim', Label: 'Trim', LowerLimit: AircraftModel.MinTrimAngle, UpperLimit: AircraftModel.MaxTrimAngle, Mult: rad(1), Units: '°', } ).AddTextField( { Name: 'InitElevator', Label: 'Elevator', LowerLimit: AircraftModel.MinElevatorAngle, UpperLimit: AircraftModel.MaxElevatorAngle, Mult: rad(1), Units: '°', } ).AddTextField( { Name: 'InitThrust', Label: 'Thrust', LowerLimit: 0, UpperLimit: 1, Mult: 0.01, Units: '%', } ).AddTextField( { Name: 'InitMass', Label: 'Mass', LowerLimit: 38600, UpperLimit: 73600, Units: 't', Mult: 1000, } ).AddTextField( { Name: 'InitCG', Label: 'CG', LowerLimit: -1, UpperLimit: 1, } ).AddTextField( { Name: 'InitDeltaPressure', Label: 'ΔP', Units: 'hPa', Mult: 100, LowerLimit: -3000, UpperLimit: 3000, } ).AddTextField( { Name: 'InitEarthRadius', Label: 'Earth Radius', Units: '% of 6371 km', Mult: 63710, LowerLimit: 63710, UpperLimit: 6.371e9, Inc: 10, } ).Render();