// along with this program; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-/*****************************************************************
-*
-* WARNING - Curt has some ideas about AI traffic so anything in here
-* may get rewritten or scrapped. Contact Curt curt@flightgear.org
-* before spending any time or effort on this code!!!
-*
-******************************************************************/
+#include <simgear/misc/sg_path.hxx>
-
-
-#include <Main/fgfs.hxx>
#include <Main/fg_props.hxx>
+#include <Main/globals.hxx>
+#include <simgear/math/sg_random.h>
#include <list>
+#ifdef _MSC_VER
+# include <io.h>
+#else
+# include <sys/types.h> // for directory reading
+# include <dirent.h> // for directory reading
+#endif
+
+#include <Environment/environment_mgr.hxx>
+#include <Environment/environment.hxx>
+
#include "AIMgr.hxx"
#include "AILocalTraffic.hxx"
+#include "AIGAVFRTraffic.hxx"
+#include "ATCutils.hxx"
+#include "commlist.hxx"
SG_USING_STD(list);
-
+SG_USING_STD(cout);
FGAIMgr::FGAIMgr() {
+ ATC = globals->get_ATC_mgr();
+ initDone = false;
+ ai_callsigns_used["CFGFS"] = 1; // so we don't inadvertently use this
+ // TODO - use the proper user callsign when it becomes user settable.
+ removalList.clear();
+ activated.clear();
+ _havePiperModel = true;
}
FGAIMgr::~FGAIMgr() {
+ ssgDeRefDelete(_defaultModel);
+ if(_havePiperModel) ssgDeRefDelete(_piperModel);
}
void FGAIMgr::init() {
- // Hard wire some local traffic for now.
- // This is regardless of location and hence *very* ugly but it is a start.
- FGAILocalTraffic* local_traffic = new FGAILocalTraffic;
- local_traffic->Init();
- ai_list.push_back(local_traffic);
+ //cout << "AIMgr::init called..." << endl;
+
+ // Pointers to user's position
+ lon_node = fgGetNode("/position/longitude-deg", true);
+ lat_node = fgGetNode("/position/latitude-deg", true);
+ elev_node = fgGetNode("/position/altitude-ft", true);
+
+ lon = lon_node->getDoubleValue();
+ lat = lat_node->getDoubleValue();
+ elev = elev_node->getDoubleValue();
+
+ // Load up models at the start to avoid pausing later
+ // Hack alert - Hardwired paths!!
+ string planepath = "Aircraft/c172p/Models/c172p.xml";
+ bool _loadedDefaultOK = true;
+ try {
+ _defaultModel = sgLoad3DModel( globals->get_fg_root(),
+ planepath.c_str(),
+ globals->get_props(),
+ globals->get_sim_time_sec() );
+ } catch(sg_exception& e) {
+ _loadedDefaultOK = false;
+ }
+
+ if(!_loadedDefaultOK ) {
+ // Just load the same 3D model as the default user plane - that's *bound* to exist!
+ // TODO - implement robust determination of availability of GA AI aircraft models
+ planepath = "Aircraft/c172p/Models/c172p.ac";
+ _defaultModel = sgLoad3DModel( globals->get_fg_root(),
+ planepath.c_str(),
+ globals->get_props(),
+ globals->get_sim_time_sec() );
+ }
+
+ planepath = "Aircraft/pa28-161/Models/pa28-161.ac";
+ try {
+ _piperModel = sgLoad3DModel( globals->get_fg_root(),
+ planepath.c_str(),
+ globals->get_props(),
+ globals->get_sim_time_sec() );
+ } catch(sg_exception& e) {
+ _havePiperModel = false;
+ }
+
+ // We need to keep one ref of the models open to stop ssg deleting them behind our back!
+ _defaultModel->ref();
+ if(_havePiperModel) _piperModel->ref();
+
+ // go through the $FG_ROOT/ATC directory and find all *.taxi files
+ SGPath path(globals->get_fg_root());
+ path.append("ATC/");
+ string dir = path.dir();
+ string ext;
+ string file, f_ident;
+ int pos;
+
+ ulDir *d;
+ struct ulDirEnt *de;
+
+ if ( (d = ulOpenDir( dir.c_str() )) == NULL ) {
+ SG_LOG(SG_ATC, SG_WARN, "cannot open directory " << dir);
+ } else {
+ // load all .taxi files
+ while ( (de = ulReadDir(d)) != NULL ) {
+ file = de->d_name;
+ pos = file.find(".");
+ ext = file.substr(pos + 1);
+ if(ext == "taxi") {
+ f_ident = file.substr(0, pos);
+ FGAirport a;
+ if(dclFindAirportID(f_ident, &a)) {
+ SGBucket sgb(a.getLongitude(), a.getLatitude());
+ int idx = sgb.gen_index();
+ if(facilities.find(idx) != facilities.end()) {
+ facilities[idx]->push_back(f_ident);
+ } else {
+ ID_list_type* apts = new ID_list_type;
+ apts->push_back(f_ident);
+ facilities[idx] = apts;
+ }
+ SG_LOG(SG_ATC, SG_BULK, "Mapping " << f_ident << " to bucket " << idx);
+ }
+ }
+ }
+ ulCloseDir(d);
+ }
+
+ // See if are in range at startup and activate if necessary
+ SearchByPos(15.0);
+
+ initDone = true;
+
+ //cout << "AIMgr::init done..." << endl;
+
+ /*
+ // TESTING
+ FGATCAlignedProjection ortho;
+ ortho.Init(dclGetAirportPos("KEMT"), 205.0); // Guess of rwy19 heading
+ //Point3D ip = ortho.ConvertFromLocal(Point3D(6000, 1000, 1000)); // 90 deg entry
+ //Point3D ip = ortho.ConvertFromLocal(Point3D(-7000, 3000, 1000)); // 45 deg entry
+ Point3D ip = ortho.ConvertFromLocal(Point3D(1000, -7000, 1000)); // straight-in
+ ATC->AIRegisterAirport("KEMT");
+ FGAIGAVFRTraffic* p = new FGAIGAVFRTraffic();
+ p->SetModel(_defaultModel);
+ p->Init(ip, "KEMT", GenerateShortForm(GenerateUniqueCallsign()));
+ ai_list.push_back(p);
+ traffic[ident].push_back(p);
+ activated["KEMT"] = 1;
+ */
}
void FGAIMgr::bind() {
void FGAIMgr::unbind() {
}
-void FGAIMgr::update(int dt) {
- // Traverse the list of active planes and run all their update methods
- ai_list_itr = ai_list.begin();
- while(ai_list_itr != ai_list.end()) {
- (*ai_list_itr)->Update();
- ++ai_list_itr;
- }
-}
\ No newline at end of file
+void FGAIMgr::update(double dt) {
+ if(!initDone) {
+ init();
+ SG_LOG(SG_ATC, SG_WARN, "Warning - AIMgr::update(...) called before AIMgr::init()");
+ }
+
+ //cout << activated.size() << '\n';
+
+ Point3D userPos = Point3D(lon_node->getDoubleValue(), lat_node->getDoubleValue(), elev_node->getDoubleValue());
+
+ // TODO - make these class variables!!
+ static int i = 0;
+ static int j = 0;
+
+ // Don't update any planes for first 50 runs through - this avoids some possible initialisation anomalies
+ // Might not need it now we have fade-in though?
+ if(i < 50) {
+ ++i;
+ return;
+ }
+
+ if(j == 215) {
+ SearchByPos(25.0);
+ j = 0;
+ } else if(j == 200) {
+ // Go through the list of activated airports and remove those out of range
+ //cout << "The following airports have been activated by the AI system:\n";
+ ai_activated_map_iterator apt_itr = activated.begin();
+ while(apt_itr != activated.end()) {
+ //cout << "FIRST IS " << (*apt_itr).first << '\n';
+ if(dclGetHorizontalSeparation(userPos, dclGetAirportPos((*apt_itr).first)) > (35.0 * 1600.0)) {
+ // Then get rid of it and make sure the iterator is left pointing to the next one!
+ string s = (*apt_itr).first;
+ if(traffic.find(s) != traffic.end()) {
+ //cout << "s = " << s << ", traffic[s].size() = " << traffic[s].size() << '\n';
+ if(traffic[s].size()) {
+ apt_itr++;
+ } else {
+ //cout << "Erasing " << (*apt_itr).first << " and traffic" << '\n';
+ activated.erase(apt_itr++);
+ traffic.erase(s);
+ }
+ } else {
+ //cout << "Erasing " << (*apt_itr).first << ' ' << (*apt_itr).second << '\n';
+ activated.erase(apt_itr++);
+ }
+ } else {
+ apt_itr++;
+ }
+ }
+ } else if(j == 180) {
+ // Go through the list of activated airports and do the random airplane generation
+ ai_traffic_map_iterator it = traffic.begin();
+ while(it != traffic.end()) {
+ string s = (*it).first;
+ //cout << "s = " << s << " size = " << (*it).second.size() << '\n';
+ // Only generate extra traffic if within a certain distance of the user,
+ // TODO - maybe take users's tuned freq into account as well.
+ double d = dclGetHorizontalSeparation(userPos, dclGetAirportPos(s));
+ if(d < (15.0 * 1600.0)) {
+ double cd = 0.0;
+ bool gen = false;
+ //cout << "Size of list is " << (*it).second.size() << " at " << s << '\n';
+ if((*it).second.size()) {
+ FGAIEntity* e = *((*it).second.rbegin()); // Get the last airplane currently scheduled to arrive at this airport.
+ cd = dclGetHorizontalSeparation(e->GetPos(), dclGetAirportPos(s));
+ if(cd < (d < 5000 ? 10000 : d + 5000)) {
+ gen = true;
+ }
+ } else {
+ gen = true;
+ cd = 0.0;
+ }
+ if(gen) {
+ //cout << "Generating extra traffic at airport " << s << ", at least " << cd << " meters out\n";
+ //GenerateSimpleAirportTraffic(s, cd);
+ GenerateSimpleAirportTraffic(s, cd + 3000.0); // The random seems a bit wierd - traffic could get far too bunched without the +3000.
+ // TODO - make the anti-random constant variable depending on the ai-traffic level.
+ }
+ }
+ ++it;
+ }
+ }
+
+ ++j;
+
+ //cout << "Size of AI list is " << ai_list.size() << '\n';
+
+ // TODO - need to add a check of if any activated airports have gone out of range
+
+ string rs; // plane to be removed, if one.
+ if(removalList.size()) {
+ rs = *(removalList.begin());
+ removalList.pop_front();
+ } else {
+ rs = "";
+ }
+
+ // Traverse the list of active planes and run all their update methods
+ // TODO - spread the load - not all planes should need updating every frame.
+ // Note that this will require dt to be calculated for each plane though
+ // since they rely on it to calculate distance travelled.
+ ai_list_itr = ai_list.begin();
+ while(ai_list_itr != ai_list.end()) {
+ FGAIEntity *e = *ai_list_itr;
+ if(rs.size() && e->GetCallsign() == rs) {
+ //cout << "Removing " << rs << " from ai_list\n";
+ ai_list_itr = ai_list.erase(ai_list_itr);
+ delete e;
+ // This is a hack - we should deref this plane from the airport count!
+ } else {
+ e->Update(dt);
+ ++ai_list_itr;
+ }
+ }
+
+ //cout << "Size of AI list is " << ai_list.size() << '\n';
+}
+
+void FGAIMgr::ScheduleRemoval(const string& s) {
+ //cout << "Scheduling removal of plane " << s << " from AIMgr\n";
+ removalList.push_back(s);
+}
+
+// Activate AI traffic at an airport
+void FGAIMgr::ActivateAirport(const string& ident) {
+ ATC->AIRegisterAirport(ident);
+ // TODO - need to start the traffic more randomly
+ FGAILocalTraffic* local_traffic = new FGAILocalTraffic;
+ local_traffic->SetModel(_defaultModel); // currently hardwired to cessna.
+ //local_traffic->Init(ident, IN_PATTERN, TAKEOFF_ROLL);
+ local_traffic->Init(GenerateShortForm(GenerateUniqueCallsign()), ident);
+ local_traffic->FlyCircuits(1, true); // Fly 2 circuits with touch & go in between
+ ai_list.push_back(local_traffic);
+ traffic[ident].push_back(local_traffic);
+ //cout << "******** ACTIVATING AIRPORT, ident = " << ident << '\n';
+ activated[ident] = 1;
+}
+
+// Hack - Generate AI traffic at an airport with no facilities file
+void FGAIMgr::GenerateSimpleAirportTraffic(const string& ident, double min_dist) {
+ // Ugly hack - don't let VFR Cessnas operate at a hardwired list of major airports
+ // This will go eventually once airport .xml files specify the traffic profile
+ if(ident == "KSFO" || ident == "KDFW" || ident == "EGLL" || ident == "KORD" || ident == "KJFK"
+ || ident == "KMSP" || ident == "KLAX" || ident == "KBOS" || ident == "KEDW"
+ || ident == "KSEA" || ident == "EHAM") {
+ return;
+ }
+
+ /*
+ // TODO - check for military airports - this should be in the current data.
+ // UGGH - there's no point at the moment - everything is labelled civil in basic.dat!
+ FGAirport a;
+ if(dclFindAirportID(ident, &a)) {
+ cout << "CODE IS " << a.code << '\n';
+ } else {
+ // UG - can't find the airport!
+ return;
+ }
+ */
+
+ Point3D aptpos = dclGetAirportPos(ident); // TODO - check for elev of -9999
+ //cout << "ident = " << ident << ", elev = " << aptpos.elev() << '\n';
+
+ // Operate from airports at 3000ft and below only to avoid the default cloud layers and since we don't degrade AI performance with altitude.
+ if(aptpos.elev() > 3000) {
+ //cout << "High alt airports not yet supported - returning\n";
+ return;
+ }
+
+ // Rough hack for plane type - make 70% of the planes cessnas, the rest pipers.
+ bool cessna = true;
+
+ // Get the time and only operate VFR in the (approximate) daytime.
+ struct tm *t = globals->get_time_params()->getGmt();
+ int loc_time = t->tm_hour + int(aptpos.lon() / (360.0 / 24.0));
+ while (loc_time < 0)
+ loc_time += 24;
+ while (loc_time >= 24)
+ loc_time -= 24;
+
+ //cout << "loc_time = " << loc_time << '\n';
+ if(loc_time < 7 || loc_time > 19) return;
+
+ // Check that the visibility is OK for IFR operation.
+ double visibility;
+ FGEnvironment stationweather =
+ ((FGEnvironmentMgr *)globals->get_subsystem("environment"))
+ ->getEnvironment(aptpos.lat(), aptpos.lon(), aptpos.elev()); // TODO - check whether this should take ft or m for elev.
+ visibility = stationweather.get_visibility_m();
+ // Technically we can do VFR down to 1 mile (1600m) but that's pretty murky!
+ //cout << "vis = " << visibility << '\n';
+ if(visibility < 3000) return;
+
+ ATC->AIRegisterAirport(ident);
+
+ // Next - get the distance from user to the airport.
+ Point3D userpos = Point3D(lon_node->getDoubleValue(), lat_node->getDoubleValue(), elev_node->getDoubleValue());
+ double d = dclGetHorizontalSeparation(userpos, aptpos); // in meters
+
+ int lev = fgGetInt("/sim/ai-traffic/level");
+ if(lev < 1 || lev > 3) lev = 2;
+ if(visibility < 6000) lev = 1;
+ //cout << "level = " << lev << '\n';
+
+ // Next - generate any local / circuit traffic
+
+ /*
+ // --------------------------- THIS BLOCK IS JUST FOR TESTING - COMMENT OUT BEFORE RELEASE ---------------
+ // Finally - generate VFR approaching traffic
+ //if(d > 2000) {
+ if(ident == "KPOC") {
+ double ad = 2000.0;
+ double avd = 3000.0; // average spacing of arriving traffic in meters - relate to airport business and AI density setting one day!
+ //while(ad < (d < 10000 ? 12000 : d + 2000)) {
+ for(int i=0; i<8; ++i) {
+ double dd = sg_random() * avd;
+ // put a minimum spacing in for now since I don't think tower will cope otherwise!
+ if(dd < 1500) dd = 1500;
+ //ad += dd;
+ ad += dd;
+ double dir = int(sg_random() * 36);
+ if(dir == 36) dir--;
+ dir *= 10;
+ //dir = 180;
+ if(sg_random() < 0.3) cessna = false;
+ else cessna = true;
+ string s = GenerateShortForm(GenerateUniqueCallsign(), (cessna ? "Cessna-" : "Piper-"));
+ FGAIGAVFRTraffic* t = new FGAIGAVFRTraffic();
+ t->SetModel(cessna ? _defaultModel : _piperModel);
+ //cout << "Generating VFR traffic " << s << " inbound to " << ident << " " << ad << " meters out from " << dir << " degrees\n";
+ Point3D tpos = dclUpdatePosition(aptpos, dir, 6.0, ad);
+ if(tpos.elev() > (aptpos.elev() + 3000.0)) tpos.setelev(aptpos.elev() + 3000.0);
+ t->Init(tpos, ident, s);
+ ai_list.push_back(t);
+ }
+ }
+ activated[ident] = 1;
+ return;
+ //---------------------------------------------------------------------------------------------------
+ */
+
+ double ad; // Minimum distance out of first arriving plane in meters.
+ double mind; // Minimum spacing of traffic in meters
+ double avd; // average spacing of arriving traffic in meters - relate to airport business and AI density setting one day!
+ // Finally - generate VFR approaching traffic
+ //if(d > 2000) {
+ if(1) {
+ if(lev == 3) {
+ ad = 5000.0;
+ mind = 2000.0;
+ avd = 6000.0;
+ } else if(lev == 2) {
+ ad = 8000.0;
+ mind = 4000.0;
+ avd = 10000.0;
+ } else {
+ ad = 9000.0; // Start the first aircraft at least 9K out for now.
+ mind = 6000.0;
+ avd = 15000.0;
+ }
+ /*
+ // Check if there is already arriving traffic at this airport
+ cout << "BING A " << ident << '\n';
+ if(traffic.find(ident) != traffic.end()) {
+ cout << "BING B " << ident << '\n';
+ ai_list_type lst = traffic[ident];
+ cout << "BING C " << ident << '\n';
+ if(lst.size()) {
+ cout << "BING D " << ident << '\n';
+ double cd = dclGetHorizontalSeparation(aptpos, (*lst.rbegin())->GetPos());
+ cout << "ident = " << ident << ", cd = " << cd << '\n';
+ if(cd > ad) ad = cd;
+ }
+ }
+ */
+ if(min_dist != 0) ad = min_dist;
+ //cout << "ident = " << ident << ", ad = " << ad << '\n';
+ while(ad < (d < 5000 ? 15000 : d + 10000)) {
+ double dd = mind + (sg_random() * (avd - mind));
+ ad += dd;
+ double dir = int(sg_random() * 36);
+ if(dir == 36) dir--;
+ dir *= 10;
+
+ if(sg_random() < 0.3) cessna = false;
+ else cessna = true;
+ string s = GenerateShortForm(GenerateUniqueCallsign(), (cessna ? "Cessna-" : "Piper-"));
+ FGAIGAVFRTraffic* t = new FGAIGAVFRTraffic();
+ t->SetModel(cessna ? _defaultModel : (_havePiperModel ? _piperModel : _defaultModel));
+ //cout << "Generating VFR traffic " << s << " inbound to " << ident << " " << ad << " meters out from " << dir << " degrees\n";
+ Point3D tpos = dclUpdatePosition(aptpos, dir, 6.0, ad);
+ if(tpos.elev() > (aptpos.elev() + 3000.0)) tpos.setelev(aptpos.elev() + 3000.0); // FEET yuk :-(
+ t->Init(tpos, ident, s);
+ ai_list.push_back(t);
+ traffic[ident].push_back(t);
+ }
+ }
+}
+
+/*
+// Generate a VFR arrival at airport apt, at least distance d (meters) out.
+void FGAIMgr::GenerateVFRArrival(const string& apt, double d) {
+}
+*/
+
+// Search for valid airports in the vicinity of the user and activate them if necessary
+void FGAIMgr::SearchByPos(double range) {
+ //cout << "In SearchByPos(...)" << endl;
+
+ // get bucket number for plane position
+ lon = lon_node->getDoubleValue();
+ lat = lat_node->getDoubleValue();
+ elev = elev_node->getDoubleValue() * SG_FEET_TO_METER;
+ SGBucket buck(lon, lat);
+
+ // get neigboring buckets
+ int bx = (int)( range*SG_NM_TO_METER / buck.get_width_m() / 2);
+ //cout << "bx = " << bx << endl;
+ int by = (int)( range*SG_NM_TO_METER / buck.get_height_m() / 2 );
+ //cout << "by = " << by << endl;
+
+ // Search for airports with facitities files --------------------------
+ // loop over bucket range
+ for ( int i=-bx; i<=bx; i++) {
+ //cout << "i loop\n";
+ for ( int j=-by; j<=by; j++) {
+ //cout << "j loop\n";
+ buck = sgBucketOffset(lon, lat, i, j);
+ long int bucket = buck.gen_index();
+ //cout << "bucket is " << bucket << endl;
+ if(facilities.find(bucket) != facilities.end()) {
+ ID_list_type* apts = facilities[bucket];
+ ID_list_iterator current = apts->begin();
+ ID_list_iterator last = apts->end();
+
+ //cout << "Size of apts is " << apts->size() << endl;
+
+ //double rlon = lon * SGD_DEGREES_TO_RADIANS;
+ //double rlat = lat * SGD_DEGREES_TO_RADIANS;
+ //Point3D aircraft = sgGeodToCart( Point3D(rlon, rlat, elev) );
+ //Point3D airport;
+ for(; current != last; ++current) {
+ //cout << "Found " << *current << endl;;
+ if(activated.find(*current) == activated.end()) {
+ //cout << "Activating " << *current << endl;
+ //FGAirport a;
+ //if(dclFindAirportID(*current, &a)) {
+ // // We can do something here based on distance from the user if we wish.
+ //}
+ //string s = *current;
+ //cout << "s = " << s << '\n';
+ ActivateAirport(*current);
+ //ActivateSimpleAirport(*current); // TODO - put this back to ActivateAirport when that code is done.
+ //cout << "Activation done" << endl;
+ } else {
+ //cout << *current << " already activated" << endl;
+ }
+ }
+ }
+ }
+ }
+ //-------------------------------------------------------------
+
+ // Search for any towered airports in the vicinity ------------
+ comm_list_type towered;
+ comm_list_iterator twd_itr;
+
+ int num_twd = current_commlist->FindByPos(lon, lat, elev, range, &towered, TOWER);
+ if (num_twd != 0) {
+ double closest = 1000000;
+ string s = "";
+ for(twd_itr = towered.begin(); twd_itr != towered.end(); twd_itr++) {
+ // Only activate the closest airport not already activated each time.
+ if(activated.find(twd_itr->ident) == activated.end()) {
+ double sep = dclGetHorizontalSeparation(Point3D(lon, lat, elev), dclGetAirportPos(twd_itr->ident));
+ if(sep < closest) {
+ closest = sep;
+ s = twd_itr->ident;
+ }
+
+ }
+ }
+ if(s.size()) {
+ // TODO - find out why empty strings come through here when all in-range airports done.
+ GenerateSimpleAirportTraffic(s);
+ //cout << "**************ACTIVATING SIMPLE AIRPORT, ident = " << s << '\n';
+ activated[s] = 1;
+ }
+ }
+}
+
+string FGAIMgr::GenerateCallsign() {
+ // For now we'll just generate US callsigns until we can regionally identify airports.
+ string s = "N";
+ // Add 3 to 5 numbers and make up to 5 with letters.
+ //sg_srandom_time();
+ double d = sg_random();
+ int n = int(d * 3);
+ if(n == 3) --n;
+ //cout << "First n, n = " << n << '\n';
+ int j = 3 + n;
+ //cout << "j = " << j << '\n';
+ for(int i=0; i<j; ++i) {
+ int n = int(sg_random() * 10);
+ if(n == 10) --n;
+ s += (char)('0' + n);
+ }
+ for(int i=j; i<5; ++i) {
+ int n = int(sg_random() * 26);
+ if(n == 26) --n;
+ //cout << "Alpha, n = " << n << '\n';
+ s += (char)('A' + n);
+ }
+ //cout << "s = " << s << '\n';
+ return(s);
+}
+
+string FGAIMgr::GenerateUniqueCallsign() {
+ while(1) {
+ string s = GenerateCallsign();
+ if(!ai_callsigns_used[s]) {
+ ai_callsigns_used[s] = 1;
+ return(s);
+ }
+ }
+}
+
+// This will be moved somewhere else eventually!!!!
+string FGAIMgr::GenerateShortForm(const string& callsign, const string& plane_str, bool local) {
+ //cout << callsign << '\n';
+ string s;
+ if(local) s = "Trainer-";
+ else s = plane_str;
+ for(int i=3; i>0; --i) {
+ char c = callsign[callsign.size() - i];
+ //cout << c << '\n';
+ string tmp = "";
+ tmp += c;
+ if(isalpha(c)) s += GetPhoneticIdent(c);
+ else s += ConvertNumToSpokenDigits(tmp);
+ if(i > 1) s += '-';
+ }
+ return(s);
+}