1 // FGAILocalTraffic - AIEntity derived class with enough logic to
2 // fly and interact with the traffic pattern.
4 // Written by David Luff, started March 2002.
6 // Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk
8 // This program is free software; you can redistribute it and/or
9 // modify it under the terms of the GNU General Public License as
10 // published by the Free Software Foundation; either version 2 of the
11 // License, or (at your option) any later version.
13 // This program is distributed in the hope that it will be useful, but
14 // WITHOUT ANY WARRANTY; without even the implied warranty of
15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 // General Public License for more details.
18 // You should have received a copy of the GNU General Public License
19 // along with this program; if not, write to the Free Software
20 // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
26 #include <simgear/scene/model/location.hxx>
28 #include <Airports/runways.hxx>
29 #include <Main/globals.hxx>
30 #include <Scenery/scenery.hxx>
31 #include <Scenery/tilemgr.hxx>
32 #include <simgear/math/point3d.hxx>
33 #include <simgear/math/sg_geodesy.hxx>
34 #include <simgear/misc/sg_path.hxx>
41 #include "AILocalTraffic.hxx"
42 #include "ATCutils.hxx"
44 FGAILocalTraffic::FGAILocalTraffic() {
45 ATC = globals->get_ATC_mgr();
47 // TODO - unhardwire this - possibly let the AI manager set the callsign
48 plane.callsign = "Trainer-two-five-charlie";
49 plane.type = GA_SINGLE;
55 //Hardwire initialisation for now - a lot of this should be read in from config eventually
57 best_rate_of_climb_speed = 70.0;
59 //nominal_climb_speed;
61 //nominal_circuit_speed;
64 nominal_descent_rate = 500.0;
65 nominal_final_speed = 65.0;
66 //nominal_approach_speed;
67 //stall_speed_landing_config;
68 nominalTaxiSpeed = 7.5;
70 wheelOffset = 1.45; // Warning - hardwired to the C172 - we need to read this in from file.
72 // Init the property nodes
73 wind_from_hdg = fgGetNode("/environment/wind-from-heading-deg", true);
74 wind_speed_knots = fgGetNode("/environment/wind-speed-kt", true);
77 taxiRequestPending = false;
78 taxiRequestCleared = false;
80 clearedToLineUp = false;
81 clearedToTakeOff = false;
82 reportReadyForDeparture = false;
84 contactGround = false;
87 targetDescentRate = 0.0;
90 FGAILocalTraffic::~FGAILocalTraffic() {
94 // Get details of the active runway
95 // It is assumed that by the time this is called the tower control and airport code will have been set up.
96 void FGAILocalTraffic::GetRwyDetails() {
97 //cout << "GetRwyDetails called" << endl;
99 rwy.rwyID = tower->GetActiveRunway();
101 // Now we need to get the threshold position and rwy heading
104 bool rwyGood = globals->get_runways()->search(airportID, rwy.rwyID,
107 // Get the threshold position
108 hdg = runway.heading; // TODO - check - is this our heading we are setting here, and if so should we be?
109 //cout << "hdg reset to " << hdg << '\n';
110 double other_way = hdg - 180.0;
111 while(other_way <= 0.0) {
115 // move to the +l end/center of the runway
116 //cout << "Runway center is at " << runway.lon << ", " << runway.lat << '\n';
117 Point3D origin = Point3D(runway.lon, runway.lat, aptElev);
118 Point3D ref = origin;
119 double tshlon, tshlat, tshr;
120 double tolon, tolat, tor;
121 rwy.length = runway.length * SG_FEET_TO_METER;
122 geo_direct_wgs_84 ( aptElev, ref.lat(), ref.lon(), other_way,
123 rwy.length / 2.0 - 25.0, &tshlat, &tshlon, &tshr );
124 geo_direct_wgs_84 ( aptElev, ref.lat(), ref.lon(), hdg,
125 rwy.length / 2.0 - 25.0, &tolat, &tolon, &tor );
126 // Note - 25 meters in from the runway end is a bit of a hack to put the plane ahead of the user.
127 // now copy what we need out of runway into rwy
128 rwy.threshold_pos = Point3D(tshlon, tshlat, aptElev);
129 Point3D takeoff_end = Point3D(tolon, tolat, aptElev);
130 //cout << "Threshold position = " << tshlon << ", " << tshlat << ", " << aptElev << '\n';
131 //cout << "Takeoff position = " << tolon << ", " << tolat << ", " << aptElev << '\n';
133 // Set the projection for the local area
134 ortho.Init(rwy.threshold_pos, rwy.hdg);
135 rwy.end1ortho = ortho.ConvertToLocal(rwy.threshold_pos); // should come out as zero
136 rwy.end2ortho = ortho.ConvertToLocal(takeoff_end);
138 SG_LOG(SG_ATC, SG_ALERT, "Help - can't get good runway in FGAILocalTraffic!!\n");
144 There are two possible scenarios during initialisation:
145 The first is that the user is flying towards the airport, and hence the traffic
146 could be initialised anywhere, as long as the AI planes are consistent with
148 The second is that the user has started the sim at or close to the airport, and
149 hence the traffic must be initialised with respect to the user as well as each other.
150 To a certain extent it's FGAIMgr that has to worry about this, but we need to provide
151 sufficient initialisation functionality within the plane classes to allow the manager
152 to initialy position them where and how required.
154 bool FGAILocalTraffic::Init(string ICAO, OperatingState initialState, PatternLeg initialLeg) {
155 //cout << "FGAILocalTraffic.Init(...) called" << endl;
156 // Hack alert - Hardwired path!!
157 string planepath = "Aircraft/c172/Models/c172-dpm.ac";
158 ssgBranch *model = sgLoad3DModel( globals->get_fg_root(),
160 globals->get_props(),
161 globals->get_sim_time_sec() );
163 aip.setVisible(false); // This will be set to true once a valid ground elevation has been determined
164 globals->get_scenery()->get_scene_graph()->addKid(aip.getSceneGraph());
166 // Find the tower frequency - this is dependent on the ATC system being initialised before the AI system
169 if(ATC->GetAirportATCDetails(airportID, &a)) {
170 if(a.tower_freq) { // Has a tower
171 tower = (FGTower*)ATC->GetATCPointer(airportID, TOWER); // Maybe need some error checking here
173 // Something has gone wrong - abort or carry on with un-towered operation?
176 freq = (double)tower->get_freq() / 100.0;
177 ground = tower->GetGroundPtr();
179 // Something has gone wrong :-(
180 SG_LOG(SG_ATC, SG_ALERT, "ERROR - can't get a ground pointer from tower control in FGAILocalTraffic::Init() :-(");
182 } else if((initialState == PARKED) || (initialState == TAXIING)) {
183 freq = (double)ground->get_freq() / 100.0;
185 //cout << "AILocalTraffic freq is " << freq << '\n';
187 // TODO - Check CTAF, unicom etc
190 //cout << "Unable to find airport details in FGAILocalTraffic::Init()\n";
193 // Get the airport elevation
194 aptElev = dclGetAirportElev(airportID.c_str()) * SG_FEET_TO_METER;
195 //cout << "Airport elev in AILocalTraffic = " << aptElev << '\n';
196 // WARNING - we use this elev for the whole airport - some assumptions in the code
197 // might fall down with very slopey airports.
199 //cout << "In Init(), initialState = " << initialState << endl;
200 operatingState = initialState;
201 switch(operatingState) {
203 tuned_station = ground;
204 ourGate = ground->GetGateNode();
205 if(ourGate == NULL) {
206 // Implies no available gates - what shall we do?
207 // For now just vanish the plane - possibly we can make this more elegant in the future
208 SG_LOG(SG_ATC, SG_ALERT, "No gate found by FGAILocalTraffic whilst attempting Init at " << airportID << '\n');
216 pos.setelev(aptElev);
217 hdg = ourGate->heading;
219 // Now we've set the position we can do the ground elev
220 elevInitGood = false;
227 tuned_station = ground;
228 // FIXME - implement this case properly
229 return(false); // remove this line when fixed!
232 // For now we'll always start the in_pattern case on the threshold ready to take-off
233 // since we've got the implementation for this case already.
234 // TODO - implement proper generic in_pattern startup.
236 tuned_station = tower;
238 // Get the active runway details (and copy them into rwy)
241 // Initial position on threshold for now
242 pos.setlat(rwy.threshold_pos.lat());
243 pos.setlon(rwy.threshold_pos.lon());
244 pos.setelev(rwy.threshold_pos.elev());
247 // Now we've set the position we can do the ground elev
248 // This might not always be necessary if we implement in-air start
249 elevInitGood = false;
259 circuitsToFly = 0; // ie just fly this circuit and then stop
261 // FIXME TODO - pattern direction is still hardwired
262 patternDirection = -1; // Left
263 // At the bare minimum we ought to make sure it goes the right way at dual parallel rwy airports!
264 if(rwy.rwyID.size() == 3) {
265 patternDirection = (rwy.rwyID.substr(2,1) == "R" ? 1 : -1);
268 operatingState = IN_PATTERN;
273 SG_LOG(SG_ATC, SG_ALERT, "Attempt to set unknown operating state in FGAILocalTraffic.Init(...)\n");
282 // Return what type of landing we're doing on this circuit
283 LandingType FGAILocalTraffic::GetLandingOption() {
284 //cout << "circuitsToFly = " << circuitsToFly << '\n';
286 return(touchAndGo ? TOUCH_AND_GO : STOP_AND_GO);
293 // Commands to do something from higher level logic
294 void FGAILocalTraffic::FlyCircuits(int numCircuits, bool tag) {
295 //cout << "FlyCircuits called" << endl;
297 switch(operatingState) {
299 circuitsToFly += numCircuits;
303 // TODO - For now we'll punt this and do nothing
306 circuitsToFly = numCircuits; // Note that one too many circuits gets flown because we only test and decrement circuitsToFly after landing
307 // thus flying one too many circuits. TODO - Need to sort this out better!
313 // Run the internal calculations
314 void FGAILocalTraffic::Update(double dt) {
315 //cout << "A" << flush;
316 //double responseTime = 10.0; // seconds - this should get more sophisticated at some point
317 responseCounter += dt;
318 if((contactTower) && (responseCounter >= 8.0)) {
319 // Acknowledge request before changing frequency so it gets rendered if the user is on the same freq
320 string trns = "Tower ";
321 double f = globals->get_ATC_mgr()->GetFrequency(airportID, TOWER) / 100.0;
323 sprintf(buf, "%.2f", f);
326 trns += plane.callsign;
327 pending_transmission = trns;
328 ConditionalTransmit(30.0);
329 responseCounter = 0.0;
330 contactTower = false;
332 changeFreqType = TOWER;
335 if((changeFreq) && (responseCounter > 8.0)) {
336 switch(changeFreqType) {
338 tuned_station = tower;
339 freq = (double)tower->get_freq() / 100.0;
341 // Contact the tower, even if only virtually
343 pending_transmission = plane.callsign;
344 pending_transmission += " at hold short for runway ";
345 pending_transmission += ConvertRwyNumToSpokenString(rwy.rwyID);
346 pending_transmission += " traffic pattern ";
348 pending_transmission += ConvertNumToSpokenDigits(circuitsToFly + 1);
349 pending_transmission += " circuits touch and go";
351 pending_transmission += " one circuit to full stop";
356 tuned_station = ground;
357 freq = (double)ground->get_freq() / 100.0;
359 // And to avoid compiler warnings...
360 case APPROACH: break;
363 case DEPARTURE: break;
368 //cout << "." << flush;
370 switch(operatingState) {
372 //cout << "In IN_PATTERN\n";
373 if(!inAir) DoGroundElev();
375 if(aip.getSGLocation()->get_cur_elev_m() > -9990.0) {
376 pos.setelev(aip.getSGLocation()->get_cur_elev_m() + wheelOffset);
377 //cout << "TAKEOFF_ROLL, POS = " << pos.lon() << ", " << pos.lat() << ", " << pos.elev() << '\n';
379 aip.setVisible(true);
380 //cout << "Making plane visible!\n";
384 FlyTrafficPattern(dt);
388 //cout << "In TAXIING\n";
389 //cout << "*" << flush;
392 if(aip.getSGLocation()->get_cur_elev_m() > -9990.0) {
393 pos.setelev(aip.getSGLocation()->get_cur_elev_m() + wheelOffset);
395 aip.setVisible(true);
397 //cout << "Making plane visible!\n";
402 //cout << "," << flush;
403 if(!((holdingShort) && (!clearedToLineUp))) {
404 //cout << "|" << flush;
407 //cout << ";" << flush;
408 if((clearedToTakeOff) && (responseCounter >= 8.0)) {
409 // possible assumption that we're at the hold short here - may not always hold
410 // TODO - sort out the case where we're cleared to line-up first and then cleared to take-off on the rwy.
411 taxiState = TD_LINING_UP;
412 path = ground->GetPath(holdShortNode, rwy.rwyID);
414 cout << "path returned was:" << endl;
415 for(unsigned int i=0; i<path.size(); ++i) {
416 switch(path[i]->struct_type) {
418 cout << "NODE " << ((node*)(path[i]))->nodeID << endl;
426 clearedToTakeOff = false; // We *are* still cleared - this simply stops the response recurring!!
427 holdingShort = false;
428 string trns = "Cleared for take-off ";
429 trns += plane.callsign;
430 pending_transmission = trns;
434 //cout << "^" << flush;
438 //cout << "In PARKED\n";
441 if(aip.getSGLocation()->get_cur_elev_m() > -9990.0) {
442 pos.setelev(aip.getSGLocation()->get_cur_elev_m() + wheelOffset);
444 aip.setVisible(true);
446 //cout << "Making plane visible!\n";
452 if((taxiRequestPending) && (taxiRequestCleared)) {
453 //cout << "&" << flush;
454 // Get the active runway details (and copy them into rwy)
457 // Get the takeoff node for the active runway, get a path to it and start taxiing
458 path = ground->GetPathToHoldShort(ourGate, rwy.rwyID);
459 if(path.size() < 2) {
460 // something has gone wrong
461 SG_LOG(SG_ATC, SG_ALERT, "Invalid path from gate to theshold in FGAILocalTraffic::FlyCircuits\n");
465 cout << "path returned was:\n";
466 for(unsigned int i=0; i<path.size(); ++i) {
467 switch(path[i]->struct_type) {
469 cout << "NODE " << ((node*)(path[i]))->nodeID << endl;
477 path.erase(path.begin()); // pop the gate - we're here already!
478 taxiState = TD_OUTBOUND;
479 taxiRequestPending = false;
480 holdShortNode = (node*)(*(path.begin() + path.size()));
482 } else if(!taxiRequestPending) {
483 //cout << "(" << flush;
484 // Do some communication
485 // airport name + tower + airplane callsign + location + request taxi for + operation type + ?
487 trns += tower->get_name();
489 trns += plane.callsign;
490 trns += " on apron parking request taxi for traffic pattern";
491 //cout << "trns = " << trns << endl;
492 pending_transmission = trns;
494 taxiRequestCleared = false;
495 taxiRequestPending = true;
499 //cout << "!" << flush;
501 // Maybe the below should be set when we get to the threshold and prepare for TO?
502 // FIXME TODO - pattern direction is still hardwired
503 patternDirection = -1; // Left
504 // At the bare minimum we ought to make sure it goes the right way at dual parallel rwy airports!
505 if(rwy.rwyID.size() == 3) {
506 patternDirection = (rwy.rwyID.substr(2,1) == "R" ? 1 : -1);
510 //cout << ")" << flush;
515 //cout << "I " << flush;
517 // Convienience output for AI debugging user the property logger
518 fgSetDouble("/AI/Local1/ortho-x", (ortho.ConvertToLocal(pos)).x());
519 fgSetDouble("/AI/Local1/ortho-y", (ortho.ConvertToLocal(pos)).y());
520 fgSetDouble("/AI/Local1/elev", pos.elev() * SG_METER_TO_FEET);
522 // And finally, call parent for transmission rendering
523 FGAIPlane::Update(dt);
526 void FGAILocalTraffic::RegisterTransmission(int code) {
528 case 1: // taxi request cleared
529 taxiRequestCleared = true;
530 SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " cleared to taxi...");
532 case 2: // contact tower
535 SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " told to contact tower...");
537 case 3: // Cleared to line up
539 clearedToLineUp = true;
540 SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " cleared to line-up...");
542 case 4: // cleared to take-off
544 clearedToTakeOff = true;
545 SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " cleared to take-off...");
547 // case 13: // Go around!
548 // responseCounter = 0;
550 // SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " told to go-around!!");
557 // Fly a traffic pattern
558 // FIXME - far too much of the mechanics of turning, rolling, accellerating, descending etc is in here.
559 // Move it out to FGAIPlane and have FlyTrafficPattern just specify what to do, not the implementation.
560 void FGAILocalTraffic::FlyTrafficPattern(double dt) {
561 // Need to differentiate between in-air (IAS governed) and on-ground (vel governed)
562 // Take-off is an interesting case - we are on the ground but takeoff speed is IAS governed.
564 static bool transmitted = false; // FIXME - this is a hack
567 // Wind has two effects - a mechanical one in that IAS translates to a different vel, and the hdg != track,
568 // but also a piloting effect, in that the AI must be able to descend at a different rate in order to hit the threshold.
570 //cout << "dt = " << dt << '\n';
572 // ack - I can't remember how long a rate 1 turn is meant to take.
573 double turn_time = 60.0; // seconds - TODO - check this guess
574 double turn_circumference;
576 Point3D orthopos = ortho.ConvertToLocal(pos); // ortho position of the plane
577 //cout << "runway elev = " << rwy.threshold_pos.elev() << ' ' << rwy.threshold_pos.elev() * SG_METER_TO_FEET << '\n';
578 //cout << "elev = " << pos.elev() << ' ' << pos.elev() * SG_METER_TO_FEET << '\n';
580 // HACK FOR TESTING - REMOVE
581 //cout << "Calling ExitRunway..." << endl;
582 //ExitRunway(orthopos);
587 double wind_from = wind_from_hdg->getDoubleValue();
588 double wind_speed = wind_speed_knots->getDoubleValue();
600 if(aip.getSGLocation()->get_cur_elev_m() > -9990.0) {
601 pos.setelev(aip.getSGLocation()->get_cur_elev_m() + wheelOffset);
603 IAS = vel + (cos((hdg - wind_from) * DCL_DEGREES_TO_RADIANS) * wind_speed);
607 IAS = best_rate_of_climb_speed;
609 slope = 6.0; // Reduced it slightly since it's climbing a lot steeper than I can in the JSBSim C172.
615 // Turn to crosswind if above 600ft AND if other traffic allows
616 // (decided in FGTower and accessed through GetCrosswindConstraint(...)).
617 if((pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 600) {
619 if(tower->GetCrosswindConstraint(cc)) {
620 if(orthopos.y() > cc) {
621 cout << "Turning to crosswind, distance from threshold = " << orthopos.y() << '\n';
624 } else if(orthopos.y() > 1500.0) { // Added this constraint as a hack to prevent turning too early when going around.
625 cout << "Turning to crosswind, distance from threshold = " << orthopos.y() << '\n';
629 // Need to check for levelling off in case we can't turn crosswind as soon
630 // as we would like due to other traffic.
631 if((pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 1000) {
634 IAS = 80.0; // FIXME - use smooth transistion to new speed and attitude.
638 track += (360.0 / turn_time) * dt * patternDirection;
639 Bank(25.0 * patternDirection);
640 if((track < (rwy.hdg - 89.0)) || (track > (rwy.hdg + 89.0))) {
646 track = rwy.hdg + (90.0 * patternDirection);
647 if((pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 1000) {
650 IAS = 80.0; // FIXME - use smooth transistion to new speed
652 // turn 1000m out for now, taking other traffic into accout
653 if(fabs(orthopos.x()) > 980) {
655 if(tower->GetDownwindConstraint(dd)) {
656 if(fabs(orthopos.x()) > fabs(dd)) {
657 cout << "Turning to downwind, distance from centerline = " << fabs(orthopos.x()) << '\n';
661 cout << "Turning to downwind, distance from centerline = " << fabs(orthopos.x()) << '\n';
667 track += (360.0 / turn_time) * dt * patternDirection;
668 Bank(25.0 * patternDirection);
669 // just in case we didn't make height on crosswind
670 if((pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 1000) {
673 IAS = 80.0; // FIXME - use smooth transistion to new speed
675 if((track < (rwy.hdg - 179.0)) || (track > (rwy.hdg + 179.0))) {
683 track = rwy.hdg - (180 * patternDirection); //should tend to bring track back into the 0->360 range
684 // just in case we didn't make height on crosswind
685 if((pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 1000) {
688 IAS = 90.0; // FIXME - use smooth transistion to new speed
690 if((orthopos.y() < 0) && (!transmitted)) {
691 TransmitPatternPositionReport();
694 if((orthopos.y() < -100) && (!descending)) {
695 // Maybe we should think about when to start descending.
696 // For now we're assuming that we aim to follow the same glidepath regardless of wind.
699 CalculateSoD((tower->GetBaseConstraint(d1) ? d1 : -1000.0), (tower->GetDownwindConstraint(d2) ? d2 : 1000.0 * patternDirection), (patternDirection ? true : false));
700 if(SoD.leg == DOWNWIND) {
701 descending = (orthopos.y() < SoD.y ? true : false);
706 slope = -5.5; // FIXME - calculate to descent at 500fpm and hit the desired point on the runway (taking wind into account as well!!)
711 // Try and arrange to turn nicely onto base
712 turn_circumference = IAS * 0.514444 * turn_time;
713 //Hmmm - this is an interesting one - ground vs airspeed in relation to turn radius
714 //We'll leave it as a hack with IAS for now but it needs revisiting.
715 turn_radius = turn_circumference / (2.0 * DCL_PI);
716 if(orthopos.y() < -1000.0 + turn_radius) {
717 //if(orthopos.y() < -980) {
719 if(tower->GetBaseConstraint(bb)) {
720 if(fabs(orthopos.y()) > fabs(bb)) {
721 cout << "Turning to base, distance from threshold = " << fabs(orthopos.y()) << '\n';
727 cout << "Turning to base, distance from threshold = " << fabs(orthopos.y()) << '\n';
735 track += (360.0 / turn_time) * dt * patternDirection;
736 Bank(25.0 * patternDirection);
737 if(fabs(rwy.hdg - track) < 91.0) {
744 TransmitPatternPositionReport();
750 // Make downwind leg position artifically large to avoid any chance of SoD being returned as
751 // on downwind when we are already on base.
752 CalculateSoD((tower->GetBaseConstraint(d1) ? d1 : -1000.0), (10000.0 * patternDirection), (patternDirection ? true : false));
753 if(SoD.leg == BASE) {
754 descending = (fabs(orthopos.y()) < fabs(SoD.y) ? true : false);
759 slope = -5.5; // FIXME - calculate to descent at 500fpm and hit the threshold (taking wind into account as well!!)
764 track = rwy.hdg - (90 * patternDirection);
766 // Try and arrange to turn nicely onto final
767 turn_circumference = IAS * 0.514444 * turn_time;
768 //Hmmm - this is an interesting one - ground vs airspeed in relation to turn radius
769 //We'll leave it as a hack with IAS for now but it needs revisiting.
770 turn_radius = turn_circumference / (2.0 * DCL_PI);
771 if(fabs(orthopos.x()) < (turn_radius + 50)) {
778 track += (360.0 / turn_time) * dt * patternDirection;
779 Bank(25.0 * patternDirection);
780 if(fabs(track - rwy.hdg) < 0.6) {
782 vel = nominal_final_speed;
788 TransmitPatternPositionReport();
792 // Make base leg position artifically large to avoid any chance of SoD being returned as
793 // on base or downwind when we are already on final.
794 CalculateSoD(-10000.0, (1000.0 * patternDirection), (patternDirection ? true : false));
795 if(SoD.leg == FINAL) {
796 descending = (fabs(orthopos.y()) < fabs(SoD.y) ? true : false);
801 slope = -5.5; // FIXME - calculate to descent at 500fpm and hit the threshold (taking wind into account as well!!)
805 // Try and track the extended centreline
806 track = rwy.hdg - (0.2 * orthopos.x());
807 //cout << "orthopos.x() = " << orthopos.x() << " hdg = " << hdg << '\n';
808 if(pos.elev() < (rwy.threshold_pos.elev()+20.0+wheelOffset)) {
809 DoGroundElev(); // Need to call it here expicitly on final since it's only called
810 // for us in update(...) when the inAir flag is false.
812 if(pos.elev() < (rwy.threshold_pos.elev()+10.0+wheelOffset)) {
813 if(aip.getSGLocation()->get_cur_elev_m() > -9990.0) {
814 if((aip.getSGLocation()->get_cur_elev_m() + wheelOffset) > pos.elev()) {
820 } // else need a fallback position based on arpt elev in case ground elev determination fails?
826 if(aip.getSGLocation()->get_cur_elev_m() > -9990.0) {
827 pos.setelev(aip.getSGLocation()->get_cur_elev_m() + wheelOffset);
832 // FIXME - differentiate between touch and go and full stops
834 //cout << "Vel <= 15.0, circuitsToFly = " << circuitsToFly << endl;
835 if(circuitsToFly <= 0) {
836 //cout << "Calling ExitRunway..." << endl;
837 ExitRunway(orthopos);
840 //cout << "Taking off again..." << endl;
851 // FIXME - at the moment this is a bit screwy
852 // The velocity correction is applied based on the relative headings.
853 // Then the heading is changed based on the velocity.
854 // Which comes first, the chicken or the egg?
855 // Does it really matter?
857 // Apply wind to ground-relative velocity if in the air
858 vel = IAS - (cos((hdg - wind_from) * DCL_DEGREES_TO_RADIANS) * wind_speed);
859 //crab = f(track, wind, vel);
860 // The vector we need to fly is our desired vector minus the wind vector
861 // TODO - we probably ought to use plib's built in vector types and operations for this
862 // ie. There's almost *certainly* a better way to do this!
863 double gxx = vel * sin(track * DCL_DEGREES_TO_RADIANS); // Plane desired velocity x component wrt ground
864 double gyy = vel * cos(track * DCL_DEGREES_TO_RADIANS); // Plane desired velocity y component wrt ground
865 double wxx = wind_speed * sin((wind_from + 180.0) * DCL_DEGREES_TO_RADIANS); // Wind velocity x component
866 double wyy = wind_speed * cos((wind_from + 180.0) * DCL_DEGREES_TO_RADIANS); // Wind velocity y component
867 double axx = gxx - wxx; // Plane in-air velocity x component
868 double ayy = gyy - wyy; // Plane in-air velocity y component
869 // Now we want the angle between gxx and axx (which is the crab)
870 double maga = sqrt(axx*axx + ayy*ayy);
871 double magg = sqrt(gxx*gxx + gyy*gyy);
872 crab = acos((axx*gxx + ayy*gyy) / (maga * magg));
873 // At this point this works except we're getting the modulus of the angle
874 //cout << "crab = " << crab << '\n';
876 // Make sure both headings are in the 0->360 circle in order to get sane differences
877 dclBoundHeading(wind_from);
878 dclBoundHeading(track);
879 if(track > wind_from) {
880 if((track - wind_from) <= 180) {
884 if((wind_from - track) >= 180) {
888 } else { // on the ground - crab dosen't apply
893 dist = vel * 0.514444 * dt;
894 pos = dclUpdatePosition(pos, track, slope, dist);
897 // Pattern direction is true for right, false for left
898 void FGAILocalTraffic::CalculateSoD(double base_leg_pos, double downwind_leg_pos, bool pattern_direction) {
899 // For now we'll ignore wind and hardwire the glide angle.
900 double ga = 5.5; //degrees
901 double pa = 1000.0 * SG_FEET_TO_METER; // pattern altitude in meters
902 // FIXME - get glideslope angle and pattern altitude agl from airport details if available
904 // For convienience, we'll have +ve versions of the input distances
905 double blp = fabs(base_leg_pos);
906 double dlp = fabs(downwind_leg_pos);
908 //double turn_allowance = 150.0; // Approximate distance in meters that a 90deg corner is shortened by turned in a light plane.
910 double stod = pa / tan(ga * DCL_DEGREES_TO_RADIANS); // distance in meters from touchdown point to start descent
911 //cout << "Descent to start = " << stod << " meters out\n";
912 if(stod < blp) { // Start descending on final
916 } else if(stod < (blp + dlp)) { // Start descending on base leg
919 SoD.x = (pattern_direction ? (stod - dlp) : (stod - dlp) * -1.0);
920 } else { // Start descending on downwind leg
922 SoD.x = (pattern_direction ? dlp : dlp * -1.0);
923 SoD.y = (blp - (stod - (blp + dlp))) * -1.0;
927 void FGAILocalTraffic::TransmitPatternPositionReport(void) {
928 // airport name + "traffic" + airplane callsign + pattern direction + pattern leg + rwy + ?
932 trns += tower->get_name();
934 trns += plane.callsign;
935 if(patternDirection == 1) {
941 // We could probably get rid of this whole switch statement and just pass a string containing the leg from the FlyPattern function.
942 switch(leg) { // We'll assume that transmissions in turns are intended for next leg - do pilots ever call out that they are in the turn?
944 // Fall through to CROSSWIND
945 case CROSSWIND: // I don't think this case will be used here but it can't hurt to leave it in
946 trns += "crosswind ";
949 // Fall through to DOWNWIND
955 // Fall through to BASE
960 // Fall through to FINAL
961 case FINAL: // maybe this should include long/short final if appropriate?
965 default: // Hopefully this won't be used
969 trns += ConvertRwyNumToSpokenString(rwy.rwyID);
971 // And add the airport name again
972 trns += tower->get_name();
974 pending_transmission = trns; // FIXME - make up pending_transmission natively
975 ConditionalTransmit(90.0, code); // Assume a report of this leg will be invalid if we can't transmit within a minute and a half.
979 // TODO - Really should enumerate these coded values.
980 void FGAILocalTraffic::ProcessCallback(int code) {
981 // 1 - Request Departure from ground
982 // 2 - Report at hold short
983 // 10 - report crosswind
984 // 11 - report downwind
988 ground->RequestDeparture(plane, this);
989 } else if(code == 2) {
990 tower->ContactAtHoldShort(plane, this, CIRCUIT);
991 } else if(code == 11) {
992 tower->ReportDownwind(plane.callsign);
993 } else if(code == 13) {
994 tower->ReportFinal(plane.callsign);
998 void FGAILocalTraffic::ExitRunway(Point3D orthopos) {
999 //cout << "In ExitRunway" << endl;
1000 //cout << "Runway ID is " << rwy.ID << endl;
1001 node_array_type exitNodes = ground->GetExits(rwy.rwyID); //I suppose we ought to have some fallback for rwy with no defined exits?
1003 cout << "Node ID's of exits are ";
1004 for(unsigned int i=0; i<exitNodes.size(); ++i) {
1005 cout << exitNodes[i]->nodeID << ' ';
1009 if(exitNodes.size()) {
1010 //Find the next exit from orthopos.y
1012 double dist = 100000; //ie. longer than any runway in existance
1013 double backdist = 100000;
1014 node_array_iterator nItr = exitNodes.begin();
1015 node* rwyExit = *(exitNodes.begin());
1016 //int gateID; //This might want to be more persistant at some point
1017 while(nItr != exitNodes.end()) {
1018 d = ortho.ConvertToLocal((*nItr)->pos).y() - ortho.ConvertToLocal(pos).y(); //FIXME - consider making orthopos a class variable
1025 if(fabs(d) < backdist) {
1027 //TODO - need some logic here that if we don't get a forward exit we turn round and store the backwards one
1032 ourGate = ground->GetGateNode();
1033 if(ourGate == NULL) {
1034 // Implies no available gates - what shall we do?
1035 // For now just vanish the plane - possibly we can make this more elegant in the future
1036 SG_LOG(SG_ATC, SG_ALERT, "No gate found by FGAILocalTraffic whilst landing at " << airportID << '\n');
1037 aip.setVisible(false);
1038 operatingState = PARKED;
1041 path = ground->GetPath(rwyExit, ourGate);
1043 cout << "path returned was:" << endl;
1044 for(unsigned int i=0; i<path.size(); ++i) {
1045 switch(path[i]->struct_type) {
1047 cout << "NODE " << ((node*)(path[i]))->nodeID << endl;
1055 taxiState = TD_INBOUND;
1058 // Something must have gone wrong with the ground network file - or there is only a rwy here and no exits defined
1059 SG_LOG(SG_ATC, SG_ALERT, "No exits found by FGAILocalTraffic from runway " << rwy.rwyID << " at " << airportID << '\n');
1060 // What shall we do - just remove the plane from sight?
1061 aip.setVisible(false);
1062 operatingState = PARKED;
1066 // Set the class variable nextTaxiNode to the next node in the path
1067 // and update taxiPathPos, the class variable path iterator position
1068 // TODO - maybe should return error codes to the calling function if we fail here
1069 void FGAILocalTraffic::GetNextTaxiNode() {
1070 //cout << "GetNextTaxiNode called " << endl;
1071 //cout << "taxiPathPos = " << taxiPathPos << endl;
1072 ground_network_path_iterator pathItr = path.begin() + taxiPathPos;
1073 if(pathItr == path.end()) {
1074 SG_LOG(SG_ATC, SG_ALERT, "ERROR IN AILocalTraffic::GetNextTaxiNode - no more nodes in path\n");
1076 if((*pathItr)->struct_type == NODE) {
1077 //cout << "ITS A NODE" << endl;
1078 //*pathItr = new node;
1079 nextTaxiNode = (node*)*pathItr;
1083 //cout << "ITS NOT A NODE" << endl;
1084 //The first item in found must have been an arc
1085 //Assume for now that it was straight
1088 if(pathItr == path.end()) {
1089 SG_LOG(SG_ATC, SG_ALERT, "ERROR IN AILocalTraffic::GetNextTaxiNode - path ended with an arc\n");
1090 } else if((*pathItr)->struct_type == NODE) {
1091 nextTaxiNode = (node*)*pathItr;
1094 //OOPS - two non-nodes in a row - that shouldn't happen ATM
1095 SG_LOG(SG_ATC, SG_ALERT, "ERROR IN AILocalTraffic::GetNextTaxiNode - two non-nodes in sequence\n");
1101 // StartTaxi - set up the taxiing state - call only at the start of taxiing
1102 void FGAILocalTraffic::StartTaxi() {
1103 //cout << "StartTaxi called" << endl;
1104 operatingState = TAXIING;
1107 //Set the desired heading
1108 //Assume we are aiming for first node on path
1109 //Eventually we may need to consider the fact that we might start on a curved arc and
1110 //not be able to head directly for the first node.
1111 GetNextTaxiNode(); // sets the class variable nextTaxiNode to the next taxi node!
1112 desiredTaxiHeading = GetHeadingFromTo(pos, nextTaxiNode->pos);
1113 //cout << "First taxi heading is " << desiredTaxiHeading << endl;
1116 // speed in knots, headings in degrees, radius in meters.
1117 static double TaxiTurnTowardsHeading(double current_hdg, double desired_hdg, double speed, double radius, double dt) {
1118 // wrap heading - this prevents a logic bug where the plane would just go round in circles!!
1119 while(current_hdg < 0.0) {
1120 current_hdg += 360.0;
1122 while(current_hdg > 360.0) {
1123 current_hdg -= 360.0;
1125 if(fabs(current_hdg - desired_hdg) > 0.1) {
1126 // Which is the quickest direction to turn onto heading?
1127 if(desired_hdg > current_hdg) {
1128 if((desired_hdg - current_hdg) <= 180) {
1130 current_hdg += ((speed * 0.514444 * dt) / (radius * DCL_PI)) * 180.0;
1131 // TODO - check that increments are less than the delta that we check for the right direction
1132 // Probably need to reduce convergence speed as convergence is reached
1134 current_hdg -= ((speed * 0.514444 * dt) / (radius * DCL_PI)) * 180.0;
1137 if((current_hdg - desired_hdg) <= 180) {
1139 current_hdg -= ((speed * 0.514444 * dt) / (radius * DCL_PI)) * 180.0;
1140 // TODO - check that increments are less than the delta that we check for the right direction
1141 // Probably need to reduce convergence speed as convergence is reached
1143 current_hdg += ((speed * 0.514444 * dt) / (radius * DCL_PI)) * 180.0;
1147 return(current_hdg);
1150 void FGAILocalTraffic::Taxi(double dt) {
1151 //cout << "Taxi called" << endl;
1152 // Logic - if we are further away from next point than turn radius then head for it
1153 // If we have reached turning point then get next point and turn onto that heading
1154 // Look out for the finish!!
1156 //Point3D orthopos = ortho.ConvertToLocal(pos); // ortho position of the plane
1157 desiredTaxiHeading = GetHeadingFromTo(pos, nextTaxiNode->pos);
1159 bool lastNode = (taxiPathPos == path.size() ? true : false);
1161 //cout << "LAST NODE\n";
1164 // HACK ALERT! - for now we will taxi at constant speed for straights and turns
1166 // Remember that hdg is always equal to track when taxiing so we don't have to consider them both
1167 double dist_to_go = dclGetHorizontalSeparation(pos, nextTaxiNode->pos); // we may be able to do this more cheaply using orthopos
1168 //cout << "dist_to_go = " << dist_to_go << endl;
1169 if((nextTaxiNode->type == GATE) && (dist_to_go <= 0.1)) {
1170 // This might be more robust to outward paths starting with a gate if we check for either
1171 // last node or TD_INBOUND ?
1173 operatingState = PARKED;
1174 } else if(((dist_to_go > taxiTurnRadius) || (nextTaxiNode->type == GATE)) && (!liningUp)){
1175 // if the turn radius is r, and speed is s, then in a time dt we turn through
1176 // ((s.dt)/(PI.r)) x 180 degrees
1177 // or alternatively (s.dt)/r radians
1178 //cout << "hdg = " << hdg << " desired taxi heading = " << desiredTaxiHeading << '\n';
1179 hdg = TaxiTurnTowardsHeading(hdg, desiredTaxiHeading, nominalTaxiSpeed, taxiTurnRadius, dt);
1180 double vel = nominalTaxiSpeed;
1181 //cout << "vel = " << vel << endl;
1182 double dist = vel * 0.514444 * dt;
1183 //cout << "dist = " << dist << endl;
1185 //cout << "track = " << track << endl;
1187 pos = dclUpdatePosition(pos, track, slope, dist);
1188 //cout << "Updated position...\n";
1189 if(aip.getSGLocation()->get_cur_elev_m() > -9990) {
1190 pos.setelev(aip.getSGLocation()->get_cur_elev_m() + wheelOffset);
1191 } // else don't change the elev until we get a valid ground elev again!
1192 } else if(lastNode) {
1193 if(taxiState == TD_LINING_UP) {
1194 if((!liningUp) && (dist_to_go <= taxiTurnRadius)) {
1198 hdg = TaxiTurnTowardsHeading(hdg, rwy.hdg, nominalTaxiSpeed, taxiTurnRadius, dt);
1199 double vel = nominalTaxiSpeed;
1200 //cout << "vel = " << vel << endl;
1201 double dist = vel * 0.514444 * dt;
1202 //cout << "dist = " << dist << endl;
1204 //cout << "track = " << track << endl;
1206 pos = dclUpdatePosition(pos, track, slope, dist);
1207 //cout << "Updated position...\n";
1208 if(aip.getSGLocation()->get_cur_elev_m() > -9990) {
1209 pos.setelev(aip.getSGLocation()->get_cur_elev_m() + wheelOffset);
1210 } // else don't change the elev until we get a valid ground elev again!
1211 if(fabs(hdg - rwy.hdg) <= 1.0) {
1212 operatingState = IN_PATTERN;
1218 } else if(taxiState == TD_OUTBOUND) {
1219 // Pause awaiting further instructions
1220 // and for now assume we've reached the hold-short node
1221 holdingShort = true;
1222 } // else at the moment assume TD_INBOUND always ends in a gate in which case we can ignore it
1224 // Time to turn (we've already checked it's not the end we're heading for).
1225 // set the target node to be the next node which will prompt automatically turning onto
1226 // the right heading in the stuff above, with the usual provisos applied.
1228 // For now why not just recursively call this function?
1234 // Warning - ground elev determination is CPU intensive
1235 // Either this function or the logic of how often it is called
1236 // will almost certainly change.
1237 void FGAILocalTraffic::DoGroundElev() {
1239 // It would be nice if we could set the correct tile center here in order to get a correct
1240 // answer with one call to the function, but what I tried in the two commented-out lines
1241 // below only intermittently worked, and I haven't quite groked why yet.
1242 //SGBucket buck(pos.lon(), pos.lat());
1243 //aip.getSGLocation()->set_tile_center(Point3D(buck.get_center_lon(), buck.get_center_lat(), 0.0));
1245 double visibility_meters = fgGetDouble("/environment/visibility-m");
1246 //globals->get_tile_mgr()->prep_ssg_nodes( acmodel_location,
1247 globals->get_tile_mgr()->prep_ssg_nodes( aip.getSGLocation(), visibility_meters );
1248 Point3D scenery_center = globals->get_scenery()->get_center();
1249 globals->get_tile_mgr()->update( aip.getSGLocation(), visibility_meters, (aip.getSGLocation())->get_absolute_view_pos( scenery_center ) );
1250 // save results of update in SGLocation for fdm...
1252 //if ( globals->get_scenery()->get_cur_elev() > -9990 ) {
1253 // acmodel_location->
1254 // set_cur_elev_m( globals->get_scenery()->get_cur_elev() );
1257 // The need for this here means that at least 2 consecutive passes are needed :-(
1258 aip.getSGLocation()->set_tile_center( globals->get_scenery()->get_next_center() );
1260 //cout << "Transform Elev is " << globals->get_scenery()->get_cur_elev() << '\n';
1261 aip.getSGLocation()->set_cur_elev_m(globals->get_scenery()->get_cur_elev());
1262 //return(globals->get_scenery()->get_cur_elev());