// atis.cxx - routines to generate the ATIS info string // This is the implementation of the FGATIS class // // Written by David Luff, started October 2001. // // Copyright (C) 2001 David C Luff - david.luff@nottingham.ac.uk // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as // published by the Free Software Foundation; either version 2 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ///// ///// TODO: _Cumulative_ sky coverage. ///// TODO: wind _gust_ ///// TODO: more-sensible encoding of voice samples ///// u-law? outright synthesis? ///// TODO: AWOS and ASOS implementation. ///// #ifdef HAVE_CONFIG_H # include #endif #include #include // atoi() #include // sprintf #include SG_USING_STD(string); #include STL_IOSTREAM SG_USING_STD(cout); #include #include #include #include #include
#include
#include #include "atis.hxx" #include "commlist.hxx" #include "ATCutils.hxx" #include "ATCmgr.hxx" FGATIS::FGATIS() : transmission(""), trans_ident(""), atis_failed(false), refname("atis"), _prev_display(0), old_volume(0), attention(0) { _vPtr = globals->get_ATC_mgr()->GetVoicePointer(ATIS); _voiceOK = (_vPtr == NULL ? false : true); if (!(_type != ATIS || _type == AWOS)) { SG_LOG(SG_ATC, SG_ALERT, "ERROR - _type not ATIS or AWOS in atis.cxx"); } fgTie("/environment/attention", this, (int_getter)0, &FGATIS::attend); } // Hint: // http://localhost:5400/environment/attention?value=1&submit=update FGATIS::~FGATIS() { fgUntie("/environment/attention"); } void FGATIS::Init() { // Nothing to see here. Move along. } void FGATIS::attend (int attn) { attention = attn; } // Main update function - checks whether we are displaying or not the correct message. void FGATIS::Update(double dt) { #if 0 Called from: :#1 0x080ca378 in FGATIS::Update (this=0xa66d2a38, dt=0.050000000000000003) at atis.cxx:77 :#2 0x080b174e in FGATCMgr::update (this=0xb38f130, dt=0.025000000000000001) at ATCmgr.cxx:175 :#3 0x0805e92a in fgMainLoop () at main.cxx:458 :#4 0x0809bce1 in GLUTidle () at fg_os.cxx:122 :#5 0xb7b06d3a in glutMainLoop () from /usr/lib/libglut.so.3 :#6 0x0805bbad in fgMainInit (argc=1, argv=0xbffce9e4) at main.cxx:1029 #endif cur_time = globals->get_time_params()->get_cur_time(); msg_OK = (msg_time < cur_time); #if 0 if (msg_OK || _display != _prev_display) { cout << "ATIS Update: " << _display << " " << _prev_display << " len: " << transmission.length() << " oldvol: " << old_volume << " dt: " << dt << endl; msg_time = cur_time; } #endif if(_display) { double volume(0); for (map::iterator act = active_on.begin(); act != active_on.end(); act++) { string prop = "/instrumentation/" + act->first + "/volume"; volume += globals->get_props()->getDoubleValue(prop.c_str()); } // Check if we need to update the message // - basically every hour and if the weather changes significantly at the station // If !_prev_display, the radio had been detuned for a while and our // "transmission" variable was lost when we were de-instantiated. int rslt = GenTransmission(!_prev_display, attention); if (rslt || volume != old_volume) { //cout << "ATIS calling ATC::render volume: " << volume << endl; Render(transmission, volume, refname, true); old_volume = volume; } } else { // We shouldn't be displaying //cout << "ATIS.CXX - calling NoRender()..." << endl; NoRender(refname); } _prev_display = _display; attention = 0; } string replace(const string orig, const string ooo, const string nnn){ string work = orig; size_t where(0); for ( ; (where = work.find(ooo, where)) != string::npos ; ) { work.replace(where, ooo.length(), nnn); where += nnn.length(); } return work; } // Generate the actual broadcast ATIS transmission. // Regen means regenerate the /current/ transmission. // Special means generate a new transmission, with a new sequence. // Returns 1 if we actually generated something. int FGATIS::GenTransmission(const int regen, const int special) { double tstamp = atof(fgGetString("sim/time/elapsed-sec")); string BRK = ".\n"; int sequence = current_commlist->GetAtisSequence(ident, tstamp, special); if (!regen && sequence > LTRS) { //xx if (msg_OK) cout << "ATIS: no change: " << sequence << endl; //xx msg_time = cur_time; return 0; // no change since last time } const int bs(100); char buf[bs]; string time_str = fgGetString("sim/time/gmt-string"); string hours, mins; string phonetic_seq_string; FGEnvironment stationweather = ((FGEnvironmentMgr *)globals->get_subsystem("environment")) ->getEnvironment(lat, lon, 0.0); transmission = ""; // UK CAA radiotelephony manual indicated ATIS transmissions start with "This is ..." // In the US they just start with the airport name. /// transmission += "This_is "; // transmitted station name. //cout << "In atis.cxx, airport name = " << name << endl; /////////////// // FIXME: This would be more flexible and more extensible // if the mappings were taken from an XML file, not hard-coded. /////////////// // Remap some abbreviations that occur in apt.dat, to // make things nicer for the text-to-speech system: string name2 = name + " "; name2 = replace(name2, "Intl ", "International "); name2 = replace(name2, "Rgnl ", "Field "); name2 = replace(name2, "Muni ", "Municipal "); name2 = replace(name2, "Mem ", "Memorial "); name2 = replace(name2, "Fld ", "Field "); name2 = replace(name2, "AFB ", "Air Force Base "); name2 = replace(name2, "AAF ", "Army Air Field "); name2 = replace(name2, "MCAS ", "Marine Corps Air Station "); transmission += name2; if (_type == ATIS /* as opposed to AWOS */) { transmission += "airport information "; // Add the sequence letter phonetic_seq_string = GetPhoneticLetter(sequence); transmission += phonetic_seq_string + BRK; } transmission += "Automated weather observation "; // Warning - this is fragile if the time string format changes hours = time_str.substr(0,2).c_str(); mins = time_str.substr(3,2).c_str(); // speak each digit separately: transmission += ConvertNumToSpokenDigits(hours + mins); transmission += " zulu weather" + BRK; transmission += "Wind: "; double speed = stationweather.get_wind_speed_kt(); double wind_dir = stationweather.get_wind_from_heading_deg(); while (wind_dir <= 0) wind_dir += 360; // The following isn't as bad a kludge as it might seem. // It combines the magvar at the /aircraft/ location with // the wind direction in the stationweather environment. // But if the aircraft is close enough to the station to // be receiving the ATIS signal, this should be a good-enough // approximation. For more-distant aircraft, the wind_dir // shouldn't be corrected anyway. // The less-kludgy approach would be to use the magvar associated // with the station, but that is not tabulated in the stationweather // structure as it stands, and computing it would be expensive. // Also note that as it stands, there is only one environment in // the entire FG universe, so the aircraft environment is the same // as the station environment anyway. wind_dir -= fgGetDouble("/environment/magnetic-variation-deg"); // wind_dir now magnetic if (speed == 0) { // Force west-facing rwys to be used in no-wind situations // which is consistent with Flightgear's initial setup: wind_dir = 270; transmission += " light_and_variable"; } else { // FIXME: get gust factor in somehow snprintf(buf, bs, "%03.0f", 5*round(wind_dir/5)); transmission += ConvertNumToSpokenDigits(buf); snprintf(buf, bs, "%1.0f", speed); transmission += " at " + ConvertNumToSpokenDigits(buf) + BRK; } int did_some(0); int did_ceiling(0); double ceiling; for (int layer = 0; layer <= 4; layer++) { snprintf(buf, bs, "/environment/clouds/layer[%i]/coverage", layer); string coverage = fgGetString(buf); if (coverage == "clear") continue; snprintf(buf, bs, "/environment/clouds/layer[%i]/thickness-ft", layer); if (fgGetDouble(buf) == 0) continue; snprintf(buf, bs, "/environment/clouds/layer[%i]/elevation-ft", layer); double ceiling = int(fgGetDouble(buf) - elev); if (ceiling > 12000) continue; if (coverage == "scattered") { if (!did_some) transmission += " Sky condition: "; did_some++; } else /* must be a ceiling */ if (!did_ceiling) { transmission += " Ceiling: "; did_ceiling++; did_some++; } else { transmission += " "; } int cig00 = int(round(ceiling/100)); // hundreds of feet if (cig00) { int cig000 = cig00/10; cig00 -= cig000*10; // just the hundreds digit if (cig000) { snprintf(buf, bs, "%i", cig000); transmission += ConvertNumToSpokenDigits(buf); transmission += " thousand "; } if (cig00) { snprintf(buf, bs, "%i", cig00); transmission += ConvertNumToSpokenDigits(buf); transmission += " hundred "; } } else { // Should this be "sky obscured?" transmission += " zero "; // not "zero hundred" } transmission += coverage + BRK; } transmission += "Temperature: "; int temp = int(round(FGAtmo().fake_t_vs_a_us(elev))); if(temp < 0) { transmission += "minus "; } snprintf(buf, bs, "%i", abs(temp)); transmission += ConvertNumToSpokenDigits(buf); transmission += " dewpoint "; double dpsl = fgGetDouble("/environment/dewpoint-sea-level-degc"); temp = int(round(FGAtmo().fake_dp_vs_a_us(dpsl, elev))); if(temp < 0) { transmission += "minus "; } snprintf(buf, bs, "%i", abs(temp)); transmission += ConvertNumToSpokenDigits(buf) + BRK; transmission += "Visibility: "; double visibility = stationweather.get_visibility_m(); visibility /= atmodel::sm; // convert to statute miles if (visibility < 0.25) { transmission += "less than one quarter"; } else if (visibility < 0.5) { transmission += "one quarter"; } else if (visibility < 0.75) { transmission += "one half"; } else if (visibility < 1.0) { transmission += "three quarters"; } else if (visibility >= 1.5 && visibility < 2.0) { transmission += "one and one half"; } else { // integer miles if (visibility > 10) visibility = 10; sprintf(buf, "%i", int(.5 + visibility)); transmission += ConvertNumToSpokenDigits(buf); } transmission += BRK; transmission += "Altimeter: "; double QNH; double Psl = fgGetDouble("/environment/pressure-sea-level-inhg"); { using namespace atmodel; QNH = FGAtmo().qnh(elev, FGAtmo().p_vs_a(elev*foot, Psl*inHg) / inHg); } if(ident.substr(0,2) == "EG" && fgGetBool("/sim/atc/use-millibars")) { // Convert to millibars for the UK! QNH *= 33.864; if (QNH > 1000) QNH -= 1000; } else { QNH *= 100; } snprintf(buf, bs, "%.0f", QNH); transmission += ConvertNumToSpokenDigits(buf) + BRK; if (_type == ATIS /* as opposed to AWOS */) { string rwy_no = globals->get_runways()->search(ident, int(wind_dir)); if(rwy_no != "NN") { transmission += "Landing and departing runway "; transmission += ConvertRwyNumToSpokenString(rwy_no) + BRK; if (msg_OK) { msg_time = cur_time; //cout << "In atis.cxx, r.rwy_no: " << rwy_no // << " wind_dir: " << wind_dir << endl; } } transmission += "On initial contact advise you have information "; transmission += phonetic_seq_string; transmission += "... " + BRK; } #ifdef ATIS_TEST cout << "**** ATIS active on:"; #endif for (map::iterator act = active_on.begin(); act != active_on.end(); act++){ string prop = "/instrumentation/" + act->first + "/atis"; globals->get_props()->setStringValue(prop.c_str(), ("
\n" + transmission + "
\n").c_str()); #ifdef ATIS_TEST cout << " " << prop; #endif } #ifdef ATIS_TEST cout << " ****" << endl; cout << transmission << endl; // Note that even if we aren't outputting the transmission // on stdout, you can still see it by pointing a web browser // at the property tree. The second comm radio is: // http://localhost:5400/instrumentation/comm%5B1%5D #endif // Take the previous English-looking string and munge it to // be relatively-more acceptable to the primitive tts system. // Note that : ; and . are among the token-delimeters recognized // by the tts system. for (unsigned int where;;) { where = transmission.find_first_of(":."); if (where == string::npos) break; transmission.replace(where, 1, " /_ "); } return 1; }