JSBSim Flight Dynamics Model  1.1.11 (13 Feb 2022)
An Open Source Flight Dynamics and Control Software Library in C++
FGLGear.cpp
1 /*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2 
3  Module: FGLGear.cpp
4  Author: Jon S. Berndt
5  Norman H. Princen
6  Bertrand Coconnier
7  Date started: 11/18/99
8  Purpose: Encapsulates the landing gear elements
9  Called by: FGAircraft
10 
11  ------------- Copyright (C) 1999 Jon S. Berndt (jon@jsbsim.org) -------------
12 
13  This program is free software; you can redistribute it and/or modify it under
14  the terms of the GNU Lesser General Public License as published by the Free
15  Software Foundation; either version 2 of the License, or (at your option) any
16  later version.
17 
18  This program is distributed in the hope that it will be useful, but WITHOUT
19  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
20  FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
21  details.
22 
23  You should have received a copy of the GNU Lesser General Public License along
24  with this program; if not, write to the Free Software Foundation, Inc., 59
25  Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 
27  Further information about the GNU Lesser General Public License can also be
28  found on the world wide web at http://www.gnu.org.
29 
30 FUNCTIONAL DESCRIPTION
31 --------------------------------------------------------------------------------
32 
33 HISTORY
34 --------------------------------------------------------------------------------
35 11/18/99 JSB Created
36 01/30/01 NHP Extended gear model to properly simulate steering and braking
37 07/08/09 BC Modified gear model to support large angles between aircraft
38  and ground
39 
40 /%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
41 INCLUDES
42 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
43 
44 #include "FGLGear.h"
45 #include "models/FGGroundReactions.h"
46 #include "math/FGTable.h"
47 #include "input_output/FGXMLElement.h"
48 #include "models/FGInertial.h"
49 
50 using namespace std;
51 
52 namespace JSBSim {
53 
54 /*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
55 DEFINITIONS
56 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
57 
58 /*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
59 GLOBAL DATA
60 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
61 
62 // Body To Structural (body frame is rotated 180 deg about Y and lengths are
63 // given in ft instead of inches)
64 const FGMatrix33 FGLGear::Tb2s(-1./inchtoft, 0., 0., 0., 1./inchtoft, 0., 0., 0., -1./inchtoft);
65 const FGMatrix33 FGLGear::Ts2b(-inchtoft, 0., 0., 0., inchtoft, 0., 0., 0., -inchtoft);
66 
67 /*%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
68 CLASS IMPLEMENTATION
69 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
70 
71 FGLGear::FGLGear(Element* el, FGFDMExec* fdmex, int number, const struct Inputs& inputs) :
72  FGSurface(fdmex, number),
73  FGForce(fdmex),
74  in(inputs),
75  GearNumber(number),
76  SteerAngle(0.0),
77  Castered(false),
78  StaticFriction(false),
79  eSteerType(stSteer)
80 {
81  kSpring = bDamp = bDampRebound = dynamicFCoeff = staticFCoeff = rollingFCoeff = maxSteerAngle = 0;
82  isRetractable = false;
83  eDampType = dtLinear;
84  eDampTypeRebound = dtLinear;
85 
86  name = el->GetAttributeValue("name");
87  string sContactType = el->GetAttributeValue("type");
88  if (sContactType == "BOGEY") {
89  eContactType = ctBOGEY;
90  } else if (sContactType == "STRUCTURE") {
91  eContactType = ctSTRUCTURE;
92  } else {
93  // Unknown contact point types will be treated as STRUCTURE.
94  eContactType = ctSTRUCTURE;
95  }
96 
97  // Default values for structural contact points
98  if (eContactType == ctSTRUCTURE) {
99  kSpring = in.EmptyWeight;
100  bDamp = kSpring;
101  bDampRebound = kSpring * 10;
102  staticFCoeff = 1.0;
103  dynamicFCoeff = 1.0;
104  }
105 
106  PropertyManager = fdmex->GetPropertyManager();
107 
108  fStrutForce = 0;
109  Element* strutForce = el->FindElement("strut_force");
110  if (strutForce) {
111  Element* springFunc = strutForce->FindElement("function");
112  fStrutForce = new FGFunction(fdmex, springFunc);
113  }
114  else {
115  if (el->FindElement("spring_coeff"))
116  kSpring = el->FindElementValueAsNumberConvertTo("spring_coeff", "LBS/FT");
117  if (el->FindElement("damping_coeff")) {
118  Element* dampCoeff = el->FindElement("damping_coeff");
119  if (dampCoeff->GetAttributeValue("type") == "SQUARE") {
120  eDampType = dtSquare;
121  bDamp = el->FindElementValueAsNumberConvertTo("damping_coeff", "LBS/FT2/SEC2");
122  } else {
123  bDamp = el->FindElementValueAsNumberConvertTo("damping_coeff", "LBS/FT/SEC");
124  }
125  }
126 
127  if (el->FindElement("damping_coeff_rebound")) {
128  Element* dampCoeffRebound = el->FindElement("damping_coeff_rebound");
129  if (dampCoeffRebound->GetAttributeValue("type") == "SQUARE") {
130  eDampTypeRebound = dtSquare;
131  bDampRebound = el->FindElementValueAsNumberConvertTo("damping_coeff_rebound", "LBS/FT2/SEC2");
132  } else {
133  bDampRebound = el->FindElementValueAsNumberConvertTo("damping_coeff_rebound", "LBS/FT/SEC");
134  }
135  } else {
136  bDampRebound = bDamp;
137  eDampTypeRebound = eDampType;
138  }
139  }
140 
141  if (el->FindElement("dynamic_friction"))
142  dynamicFCoeff = el->FindElementValueAsNumber("dynamic_friction");
143  if (el->FindElement("static_friction"))
144  staticFCoeff = el->FindElementValueAsNumber("static_friction");
145  if (el->FindElement("rolling_friction"))
146  rollingFCoeff = el->FindElementValueAsNumber("rolling_friction");
147  if (el->FindElement("retractable"))
148  isRetractable = ((unsigned int)el->FindElementValueAsNumber("retractable"))>0.0?true:false;
149 
150  if (el->FindElement("max_steer"))
151  maxSteerAngle = el->FindElementValueAsNumberConvertTo("max_steer", "DEG");
152 
153  Element* castered_el = el->FindElement("castered");
154 
155  if ((maxSteerAngle == 360 && !castered_el)
156  || (castered_el && castered_el->GetDataAsNumber() != 0.0)) {
157  eSteerType = stCaster;
158  Castered = true;
159  }
160  else if (maxSteerAngle == 0.0) {
161  eSteerType = stFixed;
162  }
163  else
164  eSteerType = stSteer;
165 
166  GroundReactions = fdmex->GetGroundReactions();
167 
168  ForceY_Table = 0;
169  Element* force_table = el->FindElement("table");
170  while (force_table) {
171  string force_type = force_table->GetAttributeValue("name");
172  if (force_type == "CORNERING_COEFF") {
173  ForceY_Table = new FGTable(PropertyManager, force_table);
174  break;
175  } else {
176  cerr << "Undefined force table for " << name << " contact point" << endl;
177  }
178  force_table = el->FindNextElement("table");
179  }
180 
181  Element* element = el->FindElement("location");
182  if (element) vXYZn = element->FindElementTripletConvertTo("IN");
183  else {
184  stringstream s;
185  s << "No location given for contact " << name;
186  cerr << endl << s.str() << endl;
187  throw BaseException(s.str());
188  }
189  SetTransformType(FGForce::tCustom);
190 
191  element = el->FindElement("orientation");
192  if (element && (eContactType == ctBOGEY)) {
193  FGQuaternion quatFromEuler(element->FindElementTripletConvertTo("RAD"));
194 
195  mTGear = quatFromEuler.GetT();
196  }
197  else {
198  mTGear(1,1) = 1.;
199  mTGear(2,2) = 1.;
200  mTGear(3,3) = 1.;
201  }
202 
203  string sBrakeGroup = el->FindElementValue("brake_group");
204 
205  if (sBrakeGroup == "LEFT" ) eBrakeGrp = bgLeft;
206  else if (sBrakeGroup == "RIGHT" ) eBrakeGrp = bgRight;
207  else if (sBrakeGroup == "CENTER") eBrakeGrp = bgCenter;
208  else if (sBrakeGroup == "NOSE" ) eBrakeGrp = bgCenter; // Nose brake is not supported by FGFCS
209  else if (sBrakeGroup == "TAIL" ) eBrakeGrp = bgCenter; // Tail brake is not supported by FGFCS
210  else if (sBrakeGroup == "NONE" ) eBrakeGrp = bgNone;
211  else if (sBrakeGroup.empty() ) eBrakeGrp = bgNone;
212  else {
213  cerr << "Improper braking group specification in config file: "
214  << sBrakeGroup << " is undefined." << endl;
215  }
216 
217 // Add some AI here to determine if gear is located properly according to its
218 // brake group type ??
219 
220  useFCSGearPos = false;
221  ReportEnable = true;
222  TakeoffReported = LandingReported = false;
223 
224  // Set Pacejka terms
225 
226  Stiffness = 0.06;
227  Shape = 2.8;
228  Peak = staticFCoeff;
229  Curvature = 1.03;
230 
231  ResetToIC();
232 
233  Debug(0);
234 }
235 
236 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
237 
239 {
240  delete ForceY_Table;
241  delete fStrutForce;
242  Debug(1);
243 }
244 
245 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
246 
247 void FGLGear::ResetToIC(void)
248 {
249  GearPos = 1.0;
250 
251  WOW = lastWOW = false;
252  FirstContact = false;
253  StartedGroundRun = false;
254  LandingDistanceTraveled = TakeoffDistanceTraveled = TakeoffDistanceTraveled50ft = 0.0;
255  MaximumStrutForce = MaximumStrutTravel = 0.0;
256  SinkRate = GroundSpeed = 0.0;
257  SteerAngle = 0.0;
258 
259  vWhlVelVec.InitMatrix();
260 
261  compressLength = 0.0;
262  compressSpeed = 0.0;
263  maxCompLen = 0.0;
264 
265  WheelSlip = 0.0;
266 
267  // Initialize Lagrange multipliers
268  for (int i=0; i < 3; i++) {
269  LMultiplier[i].ForceJacobian.InitMatrix();
270  LMultiplier[i].LeverArm.InitMatrix();
271  LMultiplier[i].Min = 0.0;
272  LMultiplier[i].Max = 0.0;
273  LMultiplier[i].value = 0.0;
274  }
275 }
276 
277 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
278 
279 const FGColumnVector3& FGLGear::GetBodyForces(FGSurface *surface)
280 {
281  double gearPos = 1.0;
282 
283  vFn.InitMatrix();
284 
285  if (isRetractable) gearPos = GetGearUnitPos();
286 
287  if (gearPos > 0.99) { // Gear DOWN
288  FGColumnVector3 normal, terrainVel, dummy;
289  FGLocation gearLoc, contact;
290  FGColumnVector3 vWhlBodyVec = Ts2b * (vXYZn - in.vXYZcg);
291 
292  vLocalGear = in.Tb2l * vWhlBodyVec; // Get local frame wheel location
293  gearLoc = in.Location.LocalToLocation(vLocalGear);
294 
295  // Compute the height of the theoretical location of the wheel (if strut is
296  // not compressed) with respect to the ground level
297  double height = fdmex->GetInertial()->GetContactPoint(gearLoc, contact,
298  normal, terrainVel,
299  dummy);
300 
301  // Does this surface contact point interact with another surface?
302  if (surface) {
303  if (!fdmex->GetTrimStatus())
304  height -= (*surface).GetBumpHeight();
305  staticFFactor = (*surface).GetStaticFFactor();
306  rollingFFactor = (*surface).GetRollingFFactor();
307  maximumForce = (*surface).GetMaximumForce();
308  isSolid = (*surface).GetSolid();
309  }
310 
311  FGColumnVector3 vWhlDisplVec;
312  double LGearProj = 1.0;
313 
314  if (height < 0.0) {
315  WOW = true;
316  vGroundNormal = in.Tec2b * normal;
317 
318  // The height returned by GetGroundCallback() is the AGL and is expressed
319  // in the Z direction of the local coordinate frame. We now need to
320  // transform this height in actual compression of the strut (BOGEY) or in
321  // the normal direction to the ground (STRUCTURE)
322  double normalZ = (in.Tec2l*normal)(eZ);
323  LGearProj = -(mTGear.Transposed() * vGroundNormal)(eZ);
324 
325  // The following equations use the vector to the tire contact patch
326  // including the strut compression.
327  switch(eContactType) {
328  case ctBOGEY:
329  if (isSolid) {
330  compressLength = LGearProj > 0.0 ? height * normalZ / LGearProj : 0.0;
331  vWhlDisplVec = mTGear * FGColumnVector3(0., 0., -compressLength);
332  } else {
333  // Gears don't (or hardly) compress in liquids
334  WOW = false;
335  }
336  break;
337  case ctSTRUCTURE:
338  compressLength = height * normalZ / DotProduct(normal, normal);
339  vWhlDisplVec = compressLength * vGroundNormal;
340  break;
341  }
342  }
343  else
344  WOW = false;
345 
346  if (WOW) {
347  FGColumnVector3 vWhlContactVec = vWhlBodyVec + vWhlDisplVec;
348  vActingXYZn = vXYZn + Tb2s * vWhlDisplVec;
349  FGColumnVector3 vBodyWhlVel = in.PQR * vWhlContactVec;
350  vBodyWhlVel += in.UVW - in.Tec2b * terrainVel;
351  vWhlVelVec = mTGear.Transposed() * vBodyWhlVel;
352 
353  InitializeReporting();
354  ComputeSteeringAngle();
355  ComputeGroundFrame();
356 
357  vGroundWhlVel = mT.Transposed() * vBodyWhlVel;
358 
359  if (fdmex->GetTrimStatus() || in.TotalDeltaT == 0.0)
360  compressSpeed = 0.0; // Steady state is sought during trimming
361  else {
362  compressSpeed = -vGroundWhlVel(eZ);
363  if (eContactType == ctBOGEY)
364  compressSpeed /= LGearProj;
365 
366  // If the gear is entering in contact with the ground during the current
367  // time step, the compression speed might actually be lower than the
368  // aircraft velocity projected along the gear leg (compressSpeed).
369  double maxCompressSpeed = compressLength/in.TotalDeltaT;
370  if (fabs(compressSpeed) > maxCompressSpeed)
371  compressSpeed = sign(compressSpeed)*maxCompressSpeed;
372  }
373 
374  ComputeVerticalStrutForce();
375 
376  // Compute the friction coefficients in the wheel ground plane.
377  if (eContactType == ctBOGEY) {
378  ComputeSlipAngle();
379  ComputeBrakeForceCoefficient();
380  ComputeSideForceCoefficient();
381  }
382 
383  // Prepare the Jacobians and the Lagrange multipliers for later friction
384  // forces calculations.
385  ComputeJacobian(vWhlContactVec);
386  } else { // Gear is NOT compressed
387  compressLength = 0.0;
388  compressSpeed = 0.0;
389  WheelSlip = 0.0;
390  StrutForce = 0.0;
391  vWhlDisplVec.InitMatrix();
392 
393  LMultiplier[ftRoll].value = 0.0;
394  LMultiplier[ftSide].value = 0.0;
395  LMultiplier[ftDynamic].value = 0.0;
396 
397  // Return to neutral position between 1.0 and 0.8 gear pos.
398  SteerAngle *= max(gearPos-0.8, 0.0)/0.2;
399 
400  ResetReporting();
401  }
402  }
403 
404  if (!WOW) {
405  // Let wheel spin down slowly
406  vWhlVelVec(eX) -= 13.0 * in.TotalDeltaT;
407  if (vWhlVelVec(eX) < 0.0) vWhlVelVec(eX) = 0.0;
408  }
409 
410  if (!fdmex->GetTrimStatus()) {
411  ReportTakeoffOrLanding();
412 
413  // Require both WOW and LastWOW to be true before checking crash conditions
414  // to allow the WOW flag to be used in terminating a scripted run.
415  if (WOW && lastWOW) CrashDetect();
416 
417  lastWOW = WOW;
418  }
419 
420  return FGForce::GetBodyForces();
421 }
422 
423 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
424 // Build a local "ground" coordinate system defined by
425 // eX : projection of the rolling direction on the ground
426 // eY : projection of the sliping direction on the ground
427 // eZ : normal to the ground
428 
429 void FGLGear::ComputeGroundFrame(void)
430 {
431  FGColumnVector3 roll = mTGear * FGColumnVector3(cos(SteerAngle), sin(SteerAngle), 0.);
432  FGColumnVector3 side = vGroundNormal * roll;
433 
434  roll -= DotProduct(roll, vGroundNormal) * vGroundNormal;
435  roll.Normalize();
436  side.Normalize();
437 
438  mT(eX,eX) = roll(eX);
439  mT(eY,eX) = roll(eY);
440  mT(eZ,eX) = roll(eZ);
441  mT(eX,eY) = side(eX);
442  mT(eY,eY) = side(eY);
443  mT(eZ,eY) = side(eZ);
444  mT(eX,eZ) = vGroundNormal(eX);
445  mT(eY,eZ) = vGroundNormal(eY);
446  mT(eZ,eZ) = vGroundNormal(eZ);
447 }
448 
449 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
450 // Calculate tire slip angle.
451 
452 void FGLGear::ComputeSlipAngle(void)
453 {
454 // Check that the speed is non-null otherwise keep the current angle
455  if (vGroundWhlVel.Magnitude(eX,eY) > 1E-3)
456  WheelSlip = -atan2(vGroundWhlVel(eY), fabs(vGroundWhlVel(eX)))*radtodeg;
457 }
458 
459 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
460 // Compute the steering angle in any case.
461 // This will also make sure that animations will look right.
462 
463 void FGLGear::ComputeSteeringAngle(void)
464 {
465  if (Castered) {
466  // Check that the speed is non-null otherwise keep the current angle
467  if (vWhlVelVec.Magnitude(eX,eY) > 0.1)
468  SteerAngle = atan2(vWhlVelVec(eY), fabs(vWhlVelVec(eX)));
469  }
470 }
471 
472 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
473 // Reset reporting functionality after takeoff
474 
475 void FGLGear::ResetReporting(void)
476 {
477  if (in.DistanceAGL > 200.0) {
478  FirstContact = false;
479  StartedGroundRun = false;
480  LandingReported = false;
481  TakeoffReported = true;
482  LandingDistanceTraveled = 0.0;
483  MaximumStrutForce = MaximumStrutTravel = 0.0;
484  }
485 }
486 
487 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
488 
489 void FGLGear::InitializeReporting(void)
490 {
491  // If this is the first time the wheel has made contact, remember some values
492  // for later printout.
493 
494  if (!FirstContact) {
495  FirstContact = true;
496  SinkRate = compressSpeed;
497  GroundSpeed = in.Vground;
498  TakeoffReported = false;
499  }
500 
501  // If the takeoff run is starting, initialize.
502 
503  if ((in.Vground > 0.1) &&
504  (in.BrakePos[bgLeft] == 0) &&
505  (in.BrakePos[bgRight] == 0) &&
506  (in.TakeoffThrottle && !StartedGroundRun))
507  {
508  TakeoffDistanceTraveled = 0;
509  TakeoffDistanceTraveled50ft = 0;
510  StartedGroundRun = true;
511  }
512 }
513 
514 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
515 // Takeoff and landing reporting functionality
516 
517 void FGLGear::ReportTakeoffOrLanding(void)
518 {
519  if (FirstContact)
520  LandingDistanceTraveled += in.Vground * in.TotalDeltaT;
521 
522  if (StartedGroundRun) {
523  TakeoffDistanceTraveled50ft += in.Vground * in.TotalDeltaT;
524  if (WOW) TakeoffDistanceTraveled += in.Vground * in.TotalDeltaT;
525  }
526 
527  if ( ReportEnable
528  && in.Vground <= 0.05
529  && !LandingReported
530  && in.WOW)
531  {
532  if (debug_lvl > 0) Report(erLand);
533  }
534 
535  if ( ReportEnable
536  && !TakeoffReported
537  && (in.DistanceAGL - vLocalGear(eZ)) > 50.0
538  && !in.WOW)
539  {
540  if (debug_lvl > 0) Report(erTakeoff);
541  }
542 
543  if (lastWOW != WOW)
544  {
545  ostringstream buf;
546  buf << "GEAR_CONTACT: " << fdmex->GetSimTime() << " seconds: " << name;
547  PutMessage(buf.str(), WOW);
548  }
549 }
550 
551 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
552 // Crash detection logic (really out-of-bounds detection)
553 
554 void FGLGear::CrashDetect(void)
555 {
556  if ( (compressLength > 500.0 ||
557  vFn.Magnitude() > 100000000.0 ||
558  GetMoments().Magnitude() > 5000000000.0 ||
559  SinkRate > 1.4666*30 ) && !fdmex->IntegrationSuspended())
560  {
561  ostringstream buf;
562  buf << "*CRASH DETECTED* " << fdmex->GetSimTime() << " seconds: " << name;
563  PutMessage(buf.str());
564  // fdmex->SuspendIntegration();
565  }
566 }
567 
568 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
569 // The following needs work regarding friction coefficients and braking and
570 // steering The BrakeFCoeff formula assumes that an anti-skid system is used.
571 // It also assumes that we won't be turning and braking at the same time.
572 // Will fix this later.
573 // [JSB] The braking force coefficients include normal rolling coefficient +
574 // a percentage of the static friction coefficient based on braking applied.
575 
576 void FGLGear::ComputeBrakeForceCoefficient(void)
577 {
578  BrakeFCoeff = rollingFFactor * rollingFCoeff;
579 
580  if (eBrakeGrp != bgNone)
581  BrakeFCoeff += in.BrakePos[eBrakeGrp] * staticFFactor * (staticFCoeff - rollingFCoeff);
582 }
583 
584 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
585 // Compute the sideforce coefficients using Pacejka's Magic Formula.
586 //
587 // y(x) = D sin {C arctan [Bx - E(Bx - arctan Bx)]}
588 //
589 // Where: B = Stiffness Factor (0.06, here)
590 // C = Shape Factor (2.8, here)
591 // D = Peak Factor (0.8, here)
592 // E = Curvature Factor (1.03, here)
593 
594 void FGLGear::ComputeSideForceCoefficient(void)
595 {
596  if (ForceY_Table) {
597  FCoeff = ForceY_Table->GetValue(WheelSlip);
598  } else {
599  double StiffSlip = Stiffness*WheelSlip;
600  FCoeff = Peak * sin(Shape*atan(StiffSlip - Curvature*(StiffSlip - atan(StiffSlip))));
601  }
602  FCoeff *= staticFFactor;
603 }
604 
605 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
606 // Compute the vertical force on the wheel using square-law damping (per comment
607 // in paper AIAA-2000-4303 - see header prologue comments). We might consider
608 // allowing for both square and linear damping force calculation. Also need to
609 // possibly give a "rebound damping factor" that differs from the compression
610 // case.
611 
612 void FGLGear::ComputeVerticalStrutForce()
613 {
614  if (fStrutForce)
615  StrutForce = min(fStrutForce->GetValue(), (double)0.0);
616  else {
617  double springForce = -compressLength * kSpring;
618  double dampForce = 0;
619 
620  if (compressSpeed >= 0.0) {
621 
622  if (eDampType == dtLinear)
623  dampForce = -compressSpeed * bDamp;
624  else
625  dampForce = -compressSpeed * compressSpeed * bDamp;
626 
627  } else {
628 
629  if (eDampTypeRebound == dtLinear)
630  dampForce = -compressSpeed * bDampRebound;
631  else
632  dampForce = compressSpeed * compressSpeed * bDampRebound;
633 
634  }
635 
636  StrutForce = min(springForce + dampForce, (double)0.0);
637  if (StrutForce > maximumForce) {
638  StrutForce = maximumForce;
639  compressLength = -StrutForce / kSpring;
640  }
641  }
642 
643  // The reaction force of the wheel is always normal to the ground
644  switch (eContactType) {
645  case ctBOGEY:
646  // Project back the strut force in the local coordinate frame of the ground
647  vFn(eZ) = StrutForce / (mTGear.Transposed()*vGroundNormal)(eZ);
648  break;
649  case ctSTRUCTURE:
650  vFn(eZ) = -StrutForce;
651  break;
652  }
653 
654  // Remember these values for reporting
655  MaximumStrutForce = max(MaximumStrutForce, fabs(StrutForce));
656  MaximumStrutTravel = max(MaximumStrutTravel, fabs(compressLength));
657 }
658 
659 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
660 
661 double FGLGear::GetGearUnitPos(void) const
662 {
663  // hack to provide backward compatibility to gear/gear-pos-norm property
664  if( useFCSGearPos || in.FCSGearPos != 1.0 ) {
665  useFCSGearPos = true;
666  return in.FCSGearPos;
667  }
668  return GearPos;
669 }
670 
671 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
672 // Compute the jacobian entries for the friction forces resolution later
673 // in FGPropagate
674 
675 void FGLGear::ComputeJacobian(const FGColumnVector3& vWhlContactVec)
676 {
677  // When the point of contact is moving, dynamic friction is used
678  // This type of friction is limited to ctSTRUCTURE elements because their
679  // friction coefficient is the same in every directions
680  if ((eContactType == ctSTRUCTURE) && (vGroundWhlVel.Magnitude(eX,eY) > 1E-3)) {
681 
682  FGColumnVector3 velocityDirection = vGroundWhlVel;
683 
684  StaticFriction = false;
685 
686  velocityDirection(eZ) = 0.;
687  velocityDirection.Normalize();
688 
689  LMultiplier[ftDynamic].ForceJacobian = mT * velocityDirection;
690  LMultiplier[ftDynamic].Max = 0.;
691  LMultiplier[ftDynamic].Min = -fabs(staticFFactor * dynamicFCoeff * vFn(eZ));
692  LMultiplier[ftDynamic].LeverArm = vWhlContactVec;
693 
694  // The Lagrange multiplier value obtained from the previous iteration is
695  // kept. This is supposed to accelerate the convergence of the projected
696  // Gauss-Seidel algorithm. The code just below is to make sure that the
697  // initial value is consistent with the current friction coefficient and
698  // normal reaction.
699  LMultiplier[ftDynamic].value = Constrain(LMultiplier[ftDynamic].Min, LMultiplier[ftDynamic].value, LMultiplier[ftDynamic].Max);
700 
701  GroundReactions->RegisterLagrangeMultiplier(&LMultiplier[ftDynamic]);
702  }
703  else {
704  // Static friction is used for ctSTRUCTURE when the contact point is not
705  // moving. It is always used for ctBOGEY elements because the friction
706  // coefficients of a tyre depend on the direction of the movement (roll &
707  // side directions). This cannot be handled properly by the so-called
708  // "dynamic friction".
709  StaticFriction = true;
710 
711  LMultiplier[ftRoll].ForceJacobian = mT * FGColumnVector3(1.,0.,0.);
712  LMultiplier[ftSide].ForceJacobian = mT * FGColumnVector3(0.,1.,0.);
713  LMultiplier[ftRoll].LeverArm = vWhlContactVec;
714  LMultiplier[ftSide].LeverArm = vWhlContactVec;
715 
716  switch(eContactType) {
717  case ctBOGEY:
718  LMultiplier[ftRoll].Max = fabs(BrakeFCoeff * vFn(eZ));
719  LMultiplier[ftSide].Max = fabs(FCoeff * vFn(eZ));
720  break;
721  case ctSTRUCTURE:
722  LMultiplier[ftRoll].Max = fabs(staticFFactor * staticFCoeff * vFn(eZ));
723  LMultiplier[ftSide].Max = LMultiplier[ftRoll].Max;
724  break;
725  }
726 
727  LMultiplier[ftRoll].Min = -LMultiplier[ftRoll].Max;
728  LMultiplier[ftSide].Min = -LMultiplier[ftSide].Max;
729 
730  // The Lagrange multiplier value obtained from the previous iteration is
731  // kept. This is supposed to accelerate the convergence of the projected
732  // Gauss-Seidel algorithm. The code just below is to make sure that the
733  // initial value is consistent with the current friction coefficient and
734  // normal reaction.
735  LMultiplier[ftRoll].value = Constrain(LMultiplier[ftRoll].Min, LMultiplier[ftRoll].value, LMultiplier[ftRoll].Max);
736  LMultiplier[ftSide].value = Constrain(LMultiplier[ftSide].Min, LMultiplier[ftSide].value, LMultiplier[ftSide].Max);
737 
738  GroundReactions->RegisterLagrangeMultiplier(&LMultiplier[ftRoll]);
739  GroundReactions->RegisterLagrangeMultiplier(&LMultiplier[ftSide]);
740  }
741 }
742 
743 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
744 // This routine is called after the Lagrange multiplier has been computed in
745 // the FGAccelerations class. The friction forces of the landing gear are then
746 // updated accordingly.
747 void FGLGear::UpdateForces(void)
748 {
749  if (StaticFriction) {
750  vFn(eX) = LMultiplier[ftRoll].value;
751  vFn(eY) = LMultiplier[ftSide].value;
752  }
753  else {
754  FGColumnVector3 forceDir = mT.Transposed() * LMultiplier[ftDynamic].ForceJacobian;
755  vFn(eX) = LMultiplier[ftDynamic].value * forceDir(eX);
756  vFn(eY) = LMultiplier[ftDynamic].value * forceDir(eY);
757  }
758 }
759 
760 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
761 
762 void FGLGear::SetstaticFCoeff(double coeff)
763 {
764  staticFCoeff = coeff;
765  Peak = coeff;
766 }
767 
768 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
769 
770 void FGLGear::bind(void)
771 {
772  string property_name;
773  string base_property_name;
774 
775  switch(eContactType) {
776  case ctBOGEY:
777  eSurfaceType = FGSurface::ctBOGEY;
778  base_property_name = CreateIndexedPropertyName("gear/unit", GearNumber);
779  break;
780  case ctSTRUCTURE:
781  eSurfaceType = FGSurface::ctSTRUCTURE;
782  base_property_name = CreateIndexedPropertyName("contact/unit", GearNumber);
783  break;
784  default:
785  return;
786  }
787  FGSurface::bind();
788 
789  property_name = base_property_name + "/WOW";
790  PropertyManager->Tie( property_name.c_str(), &WOW );
791  property_name = base_property_name + "/x-position";
792  PropertyManager->Tie( property_name.c_str(), (FGForce*)this,
793  &FGForce::GetLocationX, &FGForce::SetLocationX);
794  property_name = base_property_name + "/y-position";
795  PropertyManager->Tie( property_name.c_str(), (FGForce*)this,
796  &FGForce::GetLocationY, &FGForce::SetLocationY);
797  property_name = base_property_name + "/z-position";
798  PropertyManager->Tie( property_name.c_str(), (FGForce*)this,
799  &FGForce::GetLocationZ, &FGForce::SetLocationZ);
800  property_name = base_property_name + "/compression-ft";
801  PropertyManager->Tie( property_name.c_str(), &compressLength );
802  property_name = base_property_name + "/compression-velocity-fps";
803  PropertyManager->Tie( property_name.c_str(), &compressSpeed );
804  property_name = base_property_name + "/static_friction_coeff";
805  PropertyManager->Tie( property_name.c_str(), (FGLGear*)this,
806  &FGLGear::GetstaticFCoeff, &FGLGear::SetstaticFCoeff);
807  property_name = base_property_name + "/dynamic_friction_coeff";
808  PropertyManager->Tie( property_name.c_str(), &dynamicFCoeff );
809 
810  if (eContactType == ctBOGEY) {
811  property_name = base_property_name + "/slip-angle-deg";
812  PropertyManager->Tie( property_name.c_str(), &WheelSlip );
813  property_name = base_property_name + "/wheel-speed-fps";
814  PropertyManager->Tie( property_name.c_str(), (FGLGear*)this,
815  &FGLGear::GetWheelRollVel);
816  property_name = base_property_name + "/side_friction_coeff";
817  PropertyManager->Tie( property_name.c_str(), &FCoeff );
818  property_name = base_property_name + "/rolling_friction_coeff";
819  PropertyManager->Tie( property_name.c_str(), &rollingFCoeff );
820 
821  if (eSteerType == stCaster) {
822  property_name = base_property_name + "/steering-angle-deg";
823  PropertyManager->Tie( property_name.c_str(), this, &FGLGear::GetSteerAngleDeg );
824  property_name = base_property_name + "/castered";
825  PropertyManager->Tie( property_name.c_str(), &Castered);
826  }
827  }
828 
829  if( isRetractable ) {
830  property_name = base_property_name + "/pos-norm";
831  PropertyManager->Tie( property_name.c_str(), &GearPos );
832  }
833 
834  if (eSteerType != stFixed) {
835  // This property allows the FCS to override the steering position angle that
836  // is set by the property fcs/steer-cmd-norm. The prefix fcs/ has been kept
837  // for backward compatibility.
838  string tmp = CreateIndexedPropertyName("fcs/steer-pos-deg", GearNumber);
839  PropertyManager->Tie(tmp.c_str(), this, &FGLGear::GetSteerAngleDeg, &FGLGear::SetSteerAngleDeg);
840  }
841 }
842 
843 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
844 
845 void FGLGear::Report(ReportType repType)
846 {
847  if (fabs(TakeoffDistanceTraveled) < 0.001) return; // Don't print superfluous reports
848 
849  switch(repType) {
850  case erLand:
851  cout << endl << "Touchdown report for " << name << " (WOW at time: "
852  << fdmex->GetSimTime() << " seconds)" << endl;
853  cout << " Sink rate at contact: " << SinkRate << " fps, "
854  << SinkRate*0.3048 << " mps" << endl;
855  cout << " Contact ground speed: " << GroundSpeed*.5925 << " knots, "
856  << GroundSpeed*0.3048 << " mps" << endl;
857  cout << " Maximum contact force: " << MaximumStrutForce << " lbs, "
858  << MaximumStrutForce*4.448 << " Newtons" << endl;
859  cout << " Maximum strut travel: " << MaximumStrutTravel*12.0 << " inches, "
860  << MaximumStrutTravel*30.48 << " cm" << endl;
861  cout << " Distance traveled: " << LandingDistanceTraveled << " ft, "
862  << LandingDistanceTraveled*0.3048 << " meters" << endl;
863  LandingReported = true;
864  break;
865  case erTakeoff:
866  cout << endl << "Takeoff report for " << name << " (Liftoff at time: "
867  << fdmex->GetSimTime() << " seconds)" << endl;
868  cout << " Distance traveled: " << TakeoffDistanceTraveled
869  << " ft, " << TakeoffDistanceTraveled*0.3048 << " meters" << endl;
870  cout << " Distance traveled (over 50'): " << TakeoffDistanceTraveled50ft
871  << " ft, " << TakeoffDistanceTraveled50ft*0.3048 << " meters" << endl;
872  cout << " [Altitude (ASL): " << in.DistanceASL << " ft. / "
873  << in.DistanceASL*FGJSBBase::fttom << " m | Temperature: "
874  << in.Temperature - 459.67 << " F / "
875  << RankineToCelsius(in.Temperature) << " C]" << endl;
876  cout << " [Velocity (KCAS): " << in.VcalibratedKts << "]" << endl;
877  TakeoffReported = true;
878  break;
879  case erNone:
880  break;
881  }
882 }
883 
884 //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
885 // The bitmasked value choices are as follows:
886 // unset: In this case (the default) JSBSim would only print
887 // out the normally expected messages, essentially echoing
888 // the config files as they are read. If the environment
889 // variable is not set, debug_lvl is set to 1 internally
890 // 0: This requests JSBSim not to output any messages
891 // whatsoever.
892 // 1: This value explicity requests the normal JSBSim
893 // startup messages
894 // 2: This value asks for a message to be printed out when
895 // a class is instantiated
896 // 4: When this value is set, a message is displayed when a
897 // FGModel object executes its Run() method
898 // 8: When this value is set, various runtime state variables
899 // are printed out periodically
900 // 16: When set various parameters are sanity checked and
901 // a message is printed out when they go out of bounds
902 
903 void FGLGear::Debug(int from)
904 {
905  static const char* sSteerType[] = {"STEERABLE", "FIXED", "CASTERED" };
906  static const char* sBrakeGroup[] = {"NONE", "LEFT", "RIGHT", "CENTER", "NOSE", "TAIL"};
907  static const char* sContactType[] = {"BOGEY", "STRUCTURE" };
908 
909  if (debug_lvl <= 0) return;
910 
911  if (debug_lvl & 1) { // Standard console startup message output
912  if (from == 0) { // Constructor - loading and initialization
913  cout << " " << sContactType[eContactType] << " " << name << endl;
914  cout << " Location: " << vXYZn << endl;
915  cout << " Spring Constant: " << kSpring << endl;
916 
917  if (eDampType == dtLinear)
918  cout << " Damping Constant: " << bDamp << " (linear)" << endl;
919  else
920  cout << " Damping Constant: " << bDamp << " (square law)" << endl;
921 
922  if (eDampTypeRebound == dtLinear)
923  cout << " Rebound Damping Constant: " << bDampRebound << " (linear)" << endl;
924  else
925  cout << " Rebound Damping Constant: " << bDampRebound << " (square law)" << endl;
926 
927  cout << " Dynamic Friction: " << dynamicFCoeff << endl;
928  cout << " Static Friction: " << staticFCoeff << endl;
929  if (eContactType == ctBOGEY) {
930  cout << " Rolling Friction: " << rollingFCoeff << endl;
931  cout << " Steering Type: " << sSteerType[eSteerType] << endl;
932  cout << " Grouping: " << sBrakeGroup[eBrakeGrp] << endl;
933  cout << " Max Steer Angle: " << maxSteerAngle << endl;
934  cout << " Retractable: " << isRetractable << endl;
935  }
936  }
937  }
938  if (debug_lvl & 2 ) { // Instantiation/Destruction notification
939  if (from == 0) cout << "Instantiated: FGLGear" << endl;
940  if (from == 1) cout << "Destroyed: FGLGear" << endl;
941  }
942  if (debug_lvl & 4 ) { // Run() method entry print for FGModel-derived objects
943  }
944  if (debug_lvl & 8 ) { // Runtime state variables
945  }
946  if (debug_lvl & 16) { // Sanity checking
947  }
948  if (debug_lvl & 64) {
949  if (from == 0) { // Constructor
950  }
951  }
952 }
953 
954 } // namespace JSBSim
JSBSim::FGSurface
Base class for all surface properties.
Definition: FGSurface.h:61
JSBSim::FGFDMExec
Encapsulates the JSBSim simulation executive.
Definition: FGFDMExec.h:185
JSBSim::FGLocation::LocalToLocation
FGLocation LocalToLocation(const FGColumnVector3 &lvec) const
Conversion from Local frame coordinates to a location in the earth centered and fixed frame.
Definition: FGLocation.h:326
JSBSim::FGFunction
Represents a mathematical function.
Definition: FGFunction.h:752
JSBSim::FGColumnVector3
This class implements a 3 element column vector.
Definition: FGColumnVector3.h:63
JSBSim::FGLocation
FGLocation holds an arbitrary location in the Earth centered Earth fixed reference frame (ECEF).
Definition: FGLocation.h:151
JSBSim::FGFDMExec::GetInertial
FGInertial * GetInertial(void)
Returns the FGInertial pointer.
Definition: FGFDMExec.h:367
JSBSim::Element::GetAttributeValue
std::string GetAttributeValue(const std::string &key)
Retrieves an attribute.
Definition: FGXMLElement.cpp:260
JSBSim::Element::FindElement
Element * FindElement(const std::string &el="")
Searches for a specified element.
Definition: FGXMLElement.cpp:389
JSBSim::Element::FindElementValueAsNumber
double FindElementValueAsNumber(const std::string &el="")
Searches for the named element and returns the data belonging to it as a number.
Definition: FGXMLElement.cpp:429
JSBSim::FGTable
Lookup table class.
Definition: FGTable.h:233
JSBSim::FGLGear::Inputs
Definition: FGLGear.h:193
JSBSim::FGFDMExec::GetSimTime
double GetSimTime(void) const
Returns the cumulative simulation time in seconds.
Definition: FGFDMExec.h:542
JSBSim::Element::FindElementValueAsNumberConvertTo
double FindElementValueAsNumberConvertTo(const std::string &el, const std::string &target_units)
Searches for the named element and converts and returns the data belonging to it.
Definition: FGXMLElement.cpp:480
JSBSim::FGFDMExec::IntegrationSuspended
bool IntegrationSuspended(void) const
Returns the simulation suspension state.
Definition: FGFDMExec.h:555
JSBSim::FGFDMExec::GetGroundReactions
FGGroundReactions * GetGroundReactions(void)
Returns the FGGroundReactions pointer.
Definition: FGFDMExec.h:369
JSBSim::FGFDMExec::GetPropertyManager
FGPropertyManager * GetPropertyManager(void)
Returns a pointer to the property manager object.
Definition: FGFDMExec.cpp:1121
JSBSim::FGJSBBase::PutMessage
void PutMessage(const Message &msg)
Places a Message structure on the Message queue.
Definition: FGJSBBase.cpp:90
JSBSim::BaseException
Definition: FGJSBBase.h:59
JSBSim::FGColumnVector3::Magnitude
double Magnitude(void) const
Length of the vector.
Definition: FGColumnVector3.cpp:109
JSBSim::FGForce
Utility class that aids in the conversion of forces between coordinate systems and calculation of mom...
Definition: FGForce.h:221
JSBSim::Element::GetDataAsNumber
double GetDataAsNumber(void)
Converts the element data to a number.
Definition: FGXMLElement.cpp:341
JSBSim::FGMatrix33::Transposed
FGMatrix33 Transposed(void) const
Transposed matrix.
Definition: FGMatrix33.h:221
JSBSim::Element::FindElementValue
std::string FindElementValue(const std::string &el="")
Searches for the named element and returns the string data belonging to it.
Definition: FGXMLElement.cpp:468
JSBSim::FGQuaternion::GetT
const FGMatrix33 & GetT(void) const
Transformation matrix.
Definition: FGQuaternion.h:188
JSBSim::FGJSBBase::RankineToCelsius
static constexpr double RankineToCelsius(double rankine)
Converts from degrees Rankine to degrees Celsius.
Definition: FGJSBBase.h:209
JSBSim::FGLGear::~FGLGear
~FGLGear()
Destructor.
Definition: FGLGear.cpp:238
JSBSim::FGForce::FGForce
FGForce(FGFDMExec *FDMExec)
Constructor.
Definition: FGForce.cpp:53
JSBSim::FGJSBBase::Constrain
static constexpr double Constrain(double min, double value, double max)
Constrain a value between a minimum and a maximum value.
Definition: FGJSBBase.h:333
JSBSim::FGColumnVector3::Normalize
FGColumnVector3 & Normalize(void)
Normalize.
Definition: FGColumnVector3.cpp:116
JSBSim::FGLGear::FGLGear
FGLGear(Element *el, FGFDMExec *Executive, int number, const struct Inputs &input)
Constructor.
Definition: FGLGear.cpp:71
JSBSim::FGFunction::GetValue
double GetValue(void) const override
Retrieves the value of the function object.
Definition: FGFunction.cpp:925
JSBSim::FGQuaternion
Models the Quaternion representation of rotations.
Definition: FGQuaternion.h:86
JSBSim::Element::FindNextElement
Element * FindNextElement(const std::string &el="")
Searches for the next element as specified.
Definition: FGXMLElement.cpp:407
JSBSim::FGInertial::GetContactPoint
double GetContactPoint(const FGLocation &location, FGLocation &contact, FGColumnVector3 &normal, FGColumnVector3 &velocity, FGColumnVector3 &ang_velocity) const
Get terrain contact point information below the current location.
Definition: FGInertial.h:103
JSBSim::Element
Definition: FGXMLElement.h:143
JSBSim::FGPropertyManager::Tie
void Tie(const std::string &name, T *pointer)
Tie a property to an external variable.
Definition: FGPropertyManager.h:449
JSBSim::Element::FindElementTripletConvertTo
FGColumnVector3 FindElementTripletConvertTo(const std::string &target_units)
Composes a 3-element column vector for the supplied location or orientation.
Definition: FGXMLElement.cpp:589