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 <Main/globals.hxx>
27 #include <Main/location.hxx>
28 #include <Scenery/scenery.hxx>
29 #include <simgear/math/point3d.hxx>
30 #include <simgear/math/sg_geodesy.hxx>
31 #include <simgear/misc/sg_path.hxx>
38 #include "AILocalTraffic.hxx"
39 #include "ATCutils.hxx"
41 FGAILocalTraffic::FGAILocalTraffic() {
42 //Hardwire initialisation for now - a lot of this should be read in from config eventually
44 best_rate_of_climb_speed = 70.0;
46 //nominal_climb_speed;
48 //nominal_circuit_speed;
51 nominal_descent_rate = 500.0;
52 nominal_final_speed = 65.0;
53 //nominal_approach_speed;
54 //stall_speed_landing_config;
55 nominalTaxiSpeed = 8.0;
57 // Init the property nodes
58 wind_from_hdg = fgGetNode("/environment/wind-from-heading-deg", true);
59 wind_speed_knots = fgGetNode("/environment/wind-speed-kts", true);
63 FGAILocalTraffic::~FGAILocalTraffic() {
66 void FGAILocalTraffic::Init() {
67 // Hack alert - Hardwired path!!
68 string planepath = "Aircraft/c172/Models/c172-dpm.ac";
69 SGPath path = globals->get_fg_root();
70 path.append(planepath);
71 aip.init(planepath.c_str());
73 globals->get_scenery()->get_scene_graph()->addKid(aip.getSceneGraph());
74 // is it OK to leave it like this until the first time transform is called?
75 // Really ought to be started in a parking space unless otherwise specified?
77 // Find the tower frequency - this is dependent on the ATC system being initialised before the AI system
78 // FIXME - ATM this is hardwired.
81 if(globals->get_ATC_mgr()->GetAirportATCDetails((string)airportID, &a)) {
82 if(a.tower_freq) { // Has a tower
83 tower = (FGTower*)globals->get_ATC_mgr()->GetATCPointer((string)airportID, TOWER); // Maybe need some error checking here
84 freq = (double)tower->get_freq() / 100.0;
85 //cout << "***********************************AILocalTraffic freq = " << freq << '\n';
87 // Check CTAF, unicom etc
90 //cout << "Unable to find airport details in FGAILocalTraffic::Init()\n";
93 // Initiallise the FGAirportData structure
94 // This needs a complete overhaul soon - what happens if we have 2 AI planes at same airport - they don't both need a structure
95 // This needs to be handled by the ATC manager or similar so only one set of physical data per airport is instantiated
96 // ie. TODO TODO FIXME FIXME
101 // Commands to do something from higher level logic
102 void FGAILocalTraffic::FlyCircuits(int numCircuits, bool tag) {
103 circuitsToFly += numCircuits - 1; // Hack (-1) because we only test and decrement circuitsToFly after landing
104 // thus flying one to many circuits. TODO - Need to sort this out better!
107 //At the moment we'll assume that we are always finished previous circuits when called,
108 //And just teleport to the threshold to start.
109 //This is a hack though, we need to check where we are and taxi out if appropriate.
110 operatingState = IN_PATTERN;
112 // Hardwire to KEMT for now
113 // Hardwired points at each end of KEMT runway
114 Point3D P010(-118.037483, 34.081358, 296 * SG_FEET_TO_METER);
115 Point3D P190(-118.032308, 34.090456, 299.395263 * SG_FEET_TO_METER);
117 bool d010 = true; // use this to change the hardwired runway direction
119 rwy.threshold_pos = P010;
121 rwy.hdg = 25.32; //from default.apt
123 patternDirection = -1; // Left
124 pos.setelev(rwy.threshold_pos.elev() + (-8.5 * SG_FEET_TO_METER)); // This is a complete hack - the rendered runway takes the underlying scenery elev rather than the published runway elev so I should use height above terrain or something.
126 rwy.threshold_pos = P190;
130 patternDirection = 1; // Right
131 pos.setelev(rwy.threshold_pos.elev() + (-0.0 * SG_FEET_TO_METER)); // This is a complete hack - the rendered runway takes the underlying scenery elev rather than the published runway elev so I should use height above terrain or something.
134 //rwy.threshold_pos.setlat(34.081358);
135 //rwy.threshold_pos.setlon(-118.037483);
136 //rwy.mag_hdg = 12.0;
137 //rwy.mag_var = 14.0;
138 //rwy.hdg = rwy.mag_hdg + rwy.mag_var;
139 //rwy.threshold_pos.setelev(296 * SG_FEET_TO_METER);
141 // Initial position on threshold for now
142 // TODO - check wind / default runway
143 pos.setlat(rwy.threshold_pos.lat());
144 pos.setlon(rwy.threshold_pos.lon());
153 // Now set the position of the plane and then re-get the elevation!! (Didn't work - elev always returned as zero) :-(
154 //aip.setPosition(pos.lon(), pos.lat(), pos.elev() * SG_METER_TO_FEET);
155 //cout << "*********************** elev in FGAILocalTraffic = " << aip.getFGLocation()->get_cur_elev_m() << '\n';
157 // Set the projection for the local area
158 ortho.Init(rwy.threshold_pos, rwy.hdg);
159 rwy.end1ortho = ortho.ConvertToLocal(rwy.threshold_pos); // should come out as zero
160 // Hardwire to KEMT for now
161 rwy.end2ortho = ortho.ConvertToLocal(takeoff_end);
162 //cout << "*********************************************************************************\n";
163 //cout << "*********************************************************************************\n";
164 //cout << "*********************************************************************************\n";
165 //cout << "end1ortho = " << rwy.end1ortho << '\n';
166 //cout << "end2ortho = " << rwy.end2ortho << '\n'; // end2ortho.x() should be zero or thereabouts
171 // Run the internal calculations
172 void FGAILocalTraffic::Update(double dt) {
173 //std::cout << "In FGAILocalTraffic::Update\n";
174 // Hardwire flying traffic pattern for now - eventually also needs to be able to taxi to and from runway and GA parking area.
175 switch(operatingState) {
177 FlyTrafficPattern(dt);
190 //cout << "elev in FGAILocalTraffic = " << aip.getFGLocation()->get_cur_elev_m() << '\n';
191 // This should become if(the plane has moved) then Transform()
194 // Fly a traffic pattern
195 // FIXME - far too much of the mechanics of turning, rolling, accellerating, descending etc is in here.
196 // Move it out to FGAIPlane and have FlyTrafficPattern just specify what to do, not the implementation.
197 void FGAILocalTraffic::FlyTrafficPattern(double dt) {
198 // Need to differentiate between in-air (IAS governed) and on-ground (vel governed)
199 // Take-off is an interesting case - we are on the ground but takeoff speed is IAS governed.
200 bool inAir = true; // FIXME - possibly make into a class variable
202 static bool transmitted = false; // FIXME - this is a hack
205 // Wind has two effects - a mechanical one in that IAS translates to a different vel, and the hdg != track,
206 // but also a piloting effect, in that the AI must be able to descend at a different rate in order to hit the threshold.
208 //cout << "dt = " << dt << '\n';
210 // ack - I can't remember how long a rate 1 turn is meant to take.
211 double turn_time = 60.0; // seconds - TODO - check this guess
212 double turn_circumference;
214 Point3D orthopos = ortho.ConvertToLocal(pos); // ortho position of the plane
215 //cout << "runway elev = " << rwy.threshold_pos.elev() << ' ' << rwy.threshold_pos.elev() * SG_METER_TO_FEET << '\n';
216 //cout << "elev = " << pos.elev() << ' ' << pos.elev() * SG_METER_TO_FEET << '\n';
218 // HACK FOR TESTING - REMOVE
219 //cout << "Calling ExitRunway..." << endl;
220 //ExitRunway(orthopos);
225 double wind_from = wind_from_hdg->getDoubleValue();
226 double wind_speed = wind_speed_knots->getDoubleValue();
236 IAS = vel + (cos((hdg - wind_from) * DCL_DEGREES_TO_RADIANS) * wind_speed);
240 IAS = best_rate_of_climb_speed;
246 if((pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 600) {
251 track += (360.0 / turn_time) * dt * patternDirection;
252 Bank(25.0 * patternDirection);
253 if((track < (rwy.hdg - 89.0)) || (track > (rwy.hdg + 89.0))) {
259 track = rwy.hdg + (90.0 * patternDirection);
260 if((pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 1000) {
263 IAS = 80.0; // FIXME - use smooth transistion to new speed
265 // turn 1000m out for now
266 if(fabs(orthopos.x()) > 980) {
271 track += (360.0 / turn_time) * dt * patternDirection;
272 Bank(25.0 * patternDirection);
273 // just in case we didn't make height on crosswind
274 if((pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 1000) {
277 IAS = 80.0; // FIXME - use smooth transistion to new speed
279 if((track < (rwy.hdg - 179.0)) || (track > (rwy.hdg + 179.0))) {
287 track = rwy.hdg - (180 * patternDirection); //should tend to bring track back into the 0->360 range
288 // just in case we didn't make height on crosswind
289 if((pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 1000) {
292 IAS = 90.0; // FIXME - use smooth transistion to new speed
294 if((orthopos.y() < 0) && (!transmitted)) {
295 TransmitPatternPositionReport();
298 if(orthopos.y() < -480) {
299 slope = -4.0; // FIXME - calculate to descent at 500fpm and hit the threshold (taking wind into account as well!!)
303 if(orthopos.y() < -980) {
311 track += (360.0 / turn_time) * dt * patternDirection;
312 Bank(25.0 * patternDirection);
313 if(fabs(rwy.hdg - track) < 91.0) {
320 TransmitPatternPositionReport();
323 track = rwy.hdg - (90 * patternDirection);
324 slope = -6.0; // FIXME - calculate to descent at 500fpm and hit the threshold
326 IAS = 70.0; // FIXME - slowdown gradually
327 // Try and arrange to turn nicely onto base
328 turn_circumference = IAS * 0.514444 * turn_time;
329 //Hmmm - this is an interesting one - ground vs airspeed in relation to turn radius
330 //We'll leave it as a hack with IAS for now but it needs revisiting.
332 turn_radius = turn_circumference / (2.0 * DCL_PI);
333 if(fabs(orthopos.x()) < (turn_radius + 50)) {
340 track += (360.0 / turn_time) * dt * patternDirection;
341 Bank(25.0 * patternDirection);
342 if(fabs(track - rwy.hdg) < 0.6) {
344 vel = nominal_final_speed;
350 TransmitPatternPositionReport();
353 // Try and track the extended centreline
354 track = rwy.hdg - (0.2 * orthopos.x());
355 //cout << "orthopos.x() = " << orthopos.x() << " hdg = " << hdg << '\n';
356 if(pos.elev() <= rwy.threshold_pos.elev()) {
357 pos.setelev(rwy.threshold_pos.elev());// + (-8.5 * SG_FEET_TO_METER)); // This is a complete hack - the rendered runway takes the underlying scenery elev rather than the published runway elev so I should use height above terrain or something.
366 double dveldt = -5.0;
368 // FIXME - differentiate between touch and go and full stops
370 //cout << "Vel <= 15.0, circuitsToFly = " << circuitsToFly << endl;
371 if(circuitsToFly <= 0) {
372 //cout << "Calling ExitRunway..." << endl;
373 ExitRunway(orthopos);
376 //cout << "Taking off again..." << endl;
385 // FIXME - at the moment this is a bit screwy
386 // The velocity correction is applied based on the relative headings.
387 // Then the heading is changed based on the velocity.
388 // Which comes first, the chicken or the egg?
389 // Does it really matter?
391 // Apply wind to ground-relative velocity if in the air
392 vel = IAS - (cos((hdg - wind_from) * DCL_DEGREES_TO_RADIANS) * wind_speed);
393 //crab = f(track, wind, vel);
394 // The vector we need to fly is our desired vector minus the wind vector
395 // TODO - we probably ought to use plib's built in vector types and operations for this
396 // ie. There's almost *certainly* a better way to do this!
397 double gxx = vel * sin(track * DCL_DEGREES_TO_RADIANS); // Plane desired velocity x component wrt ground
398 double gyy = vel * cos(track * DCL_DEGREES_TO_RADIANS); // Plane desired velocity y component wrt ground
399 double wxx = wind_speed * sin((wind_from + 180.0) * DCL_DEGREES_TO_RADIANS); // Wind velocity x component
400 double wyy = wind_speed * cos((wind_from + 180.0) * DCL_DEGREES_TO_RADIANS); // Wind velocity y component
401 double axx = gxx - wxx; // Plane in-air velocity x component
402 double ayy = gyy - wyy; // Plane in-air velocity y component
403 // Now we want the angle between gxx and axx (which is the crab)
404 double maga = sqrt(axx*axx + ayy*ayy);
405 double magg = sqrt(gxx*gxx + gyy*gyy);
406 crab = acos((axx*gxx + ayy*gyy) / (maga * magg));
407 // At this point this works except we're getting the modulus of the angle
408 //cout << "crab = " << crab << '\n';
410 // Make sure both headings are in the 0->360 circle in order to get sane differences
411 dclBoundHeading(wind_from);
412 dclBoundHeading(track);
413 if(track > wind_from) {
414 if((track - wind_from) <= 180) {
418 if((wind_from - track) >= 180) {
422 } else { // on the ground - crab dosen't apply
427 dist = vel * 0.514444 * dt;
428 pos = dclUpdatePosition(pos, track, slope, dist);
431 void FGAILocalTraffic::TransmitPatternPositionReport(void) {
432 // airport name + "traffic" + airplane callsign + pattern direction + pattern leg + rwy + ?
435 trns += tower->get_name();
437 // FIXME - add the callsign to the class variables
438 trns += "Trainer-two-five-charlie ";
439 if(patternDirection == 1) {
445 // We could probably get rid of this whole switch statement and just pass a string containing the leg from the FlyPattern function.
446 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?
448 // Fall through to CROSSWIND
449 case CROSSWIND: // I don't think this case will be used here but it can't hurt to leave it in
450 trns += "crosswind ";
453 // Fall through to DOWNWIND
458 // Fall through to BASE
463 // Fall through to FINAL
464 case FINAL: // maybe this should include long/short final if appropriate?
467 default: // Hopefully this won't be used
471 // FIXME - I've hardwired the runway call as well!! (We could work this out from rwy heading and mag deviation)
472 trns += ConvertRwyNumToSpokenString(1);
474 // And add the airport name again
475 trns += tower->get_name();
480 void FGAILocalTraffic::ExitRunway(Point3D orthopos) {
481 //cout << "In ExitRunway" << endl;
482 //cout << "Runway ID is " << rwy.ID << endl;
483 node_array_type exitNodes = airport.GetExits(rwy.ID); //I suppose we ought to have some fallback for rwy with no defined exits?
485 cout << "Node ID's of exits are ";
486 for(unsigned int i=0; i<exitNodes.size(); ++i) {
487 cout << exitNodes[i]->nodeID << ' ';
491 if(exitNodes.size()) {
492 //Find the next exit from orthopos.y
494 double dist = 100000; //ie. longer than any runway in existance
495 double backdist = 100000;
496 node_array_iterator nItr = exitNodes.begin();
497 node* rwyExit = *(exitNodes.begin());
498 //int gateID; //This might want to be more persistant at some point
499 while(nItr != exitNodes.end()) {
500 d = ortho.ConvertToLocal((*nItr)->pos).y() - ortho.ConvertToLocal(pos).y(); //FIXME - consider making orthopos a class variable
507 if(fabs(d) < backdist) {
509 //TODO - need some logic here that if we don't get a forward exit we turn round and store the backwards one
514 in_dest = airport.GetGateNode();
515 // TODO - add a NULL pointer (no available gates) check for in_dest and have a fallback position
516 // (possibly taxi off runway and disappear?)
517 path = airport.GetPath(rwyExit, in_dest);
518 //cout << "path returned was:" << endl;
520 for(unsigned int i=0; i<path.size(); ++i) {
521 switch(path[i]->struct_type) {
523 cout << "NODE " << ((node*)(path[i]))->nodeID << endl;
531 taxiState = TD_INBOUND;
534 // Something must have gone wrong with the ground network file - or there is only a rwy here and no exits defined
535 SG_LOG(SG_GENERAL, SG_ALERT, "No exits found by FGAILocalTraffic from runway " << rwy.ID << " at " << airportID << '\n');
536 // What shall we do - just remove the plane from sight?
537 aip.setVisible(false);
538 operatingState = PARKED;
542 // Set the class variable nextTaxiNode to the next node in the path
543 // and update taxiPathPos, the class variable path iterator position
544 // TODO - maybe should return error codes to the calling function if we fail here
545 void FGAILocalTraffic::GetNextTaxiNode() {
546 //cout << "GetNextTaxiNode called " << endl;
547 //cout << "taxiPathPos = " << taxiPathPos << endl;
548 ground_network_path_iterator pathItr = path.begin() + taxiPathPos;
549 if(pathItr == path.end()) {
550 SG_LOG(SG_GENERAL, SG_ALERT, "ERROR IN AILocalTraffic::GetNextTaxiNode - no more nodes in path\n");
552 if((*pathItr)->struct_type == NODE) {
553 //cout << "ITS A NODE" << endl;
554 //*pathItr = new node;
555 nextTaxiNode = (node*)*pathItr;
559 //cout << "ITS NOT A NODE" << endl;
560 //The first item in found must have been an arc
561 //Assume for now that it was straight
564 if(pathItr == path.end()) {
565 SG_LOG(SG_GENERAL, SG_ALERT, "ERROR IN AILocalTraffic::GetNextTaxiNode - path ended with an arc\n");
566 } else if((*pathItr)->struct_type == NODE) {
567 nextTaxiNode = (node*)*pathItr;
570 //OOPS - two non-nodes in a row - that shouldn't happen ATM
571 SG_LOG(SG_GENERAL, SG_ALERT, "ERROR IN AILocalTraffic::GetNextTaxiNode - two non-nodes in sequence\n");
577 // StartTaxi - set up the taxiing state - call only at the start of taxiing
578 void FGAILocalTraffic::StartTaxi() {
579 //cout << "StartTaxi called" << endl;
580 operatingState = TAXIING;
583 //Set the desired heading
584 //Assume we are aiming for first node on path
585 //Eventually we may need to consider the fact that we might start on a curved arc and
586 //not be able to head directly for the first node.
587 GetNextTaxiNode(); // sets the class variable nextTaxiNode to the next taxi node!
588 desiredTaxiHeading = GetHeadingFromTo(pos, nextTaxiNode->pos);
589 //cout << "First taxi heading is " << desiredTaxiHeading << endl;
592 void FGAILocalTraffic::Taxi(double dt) {
593 //cout << "Taxi called" << endl;
594 // Logic - if we are further away from next point than turn radius then head for it
595 // If we have reached turning point then get next point and turn onto that heading
596 // Look out for the finish!!
598 Point3D orthopos = ortho.ConvertToLocal(pos); // ortho position of the plane
599 desiredTaxiHeading = GetHeadingFromTo(pos, nextTaxiNode->pos);
601 // HACK ALERT! - for now we will taxi at constant speed for straights and turns
603 // Remember that hdg is always equal to track when taxiing so we don't have to consider them both
604 double dist_to_go = dclGetHorizontalSeparation(pos, nextTaxiNode->pos); // we may be able to do this more cheaply using orthopos
605 //cout << "dist_to_go = " << dist_to_go << endl;
606 if((nextTaxiNode->type == GATE) && (dist_to_go <= 0.1)) {
610 operatingState = PARKED;
611 } else if((dist_to_go > taxiTurnRadius) || (nextTaxiNode->type == GATE)) {
612 // if the turn radius is r, and speed is s, then in a time dt we turn through
613 // ((s.dt)/(PI.r)) x 180 degrees
614 // or alternatively (s.dt)/r radians
615 //cout << "hdg = " << hdg << " desired taxi heading = " << desiredTaxiHeading << '\n';
616 if(fabs(hdg - desiredTaxiHeading) > 0.1) {
617 // Which is the quickest direction to turn onto heading?
618 if(desiredTaxiHeading > hdg) {
619 if((desiredTaxiHeading - hdg) <= 180) {
621 hdg += ((nominalTaxiSpeed * 0.514444 * dt) / (taxiTurnRadius * DCL_PI)) * 180.0;
622 // TODO - check that increments are less than the delta that we check for the right direction
623 // Probably need to reduce convergence speed as convergence is reached
625 hdg -= ((nominalTaxiSpeed * 0.514444 * dt) / (taxiTurnRadius * DCL_PI)) * 180.0;
628 if((hdg - desiredTaxiHeading) <= 180) {
630 hdg -= ((nominalTaxiSpeed * 0.514444 * dt) / (taxiTurnRadius * DCL_PI)) * 180.0;
631 // TODO - check that increments are less than the delta that we check for the right direction
632 // Probably need to reduce convergence speed as convergence is reached
634 hdg += ((nominalTaxiSpeed * 0.514444 * dt) / (taxiTurnRadius * DCL_PI)) * 180.0;
638 double vel = nominalTaxiSpeed;
639 //cout << "vel = " << vel << endl;
640 double dist = vel * 0.514444 * dt;
641 //cout << "dist = " << dist << endl;
643 //cout << "track = " << track << endl;
645 pos = dclUpdatePosition(pos, track, slope, dist);
646 //cout << "Updated position...\n";
647 // FIXME - HACK in absense of proper ground elevation determination
648 // Linearly interpolate altitude when taxiing between N and S extremes of orthopos
649 pos.setelev((287.5 + ((299.3 - 287.5) * fabs(orthopos.y() / 1000.0))) * SG_FEET_TO_METER);
651 // Time to turn (we've already checked it's not the end we're heading for).
652 // set the target node to be the next node which will prompt automatically turning onto
653 // the right heading in the stuff above, with the usual provisos applied.
655 // For now why not just recursively call this function?