X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FATCDCL%2Fatis.cxx;h=3bc9e2f1f5fe2500ffd1a3bc5e7da7771ee857ae;hb=6bf47cd248ed388e6a4dd3ffa2d00977b00b62fb;hp=95acc757b3dd33e01b05270faa915cd77fcd3465;hpb=a2922e2e23837683e41480680174015e7035ec02;p=flightgear.git diff --git a/src/ATCDCL/atis.cxx b/src/ATCDCL/atis.cxx index 95acc757b..3bc9e2f1f 100644 --- a/src/ATCDCL/atis.cxx +++ b/src/ATCDCL/atis.cxx @@ -2,6 +2,7 @@ // This is the implementation of the FGATIS class // // Written by David Luff, started October 2001. +// Extended by Thorsten Brehm, October 2012. // // Copyright (C) 2001 David C Luff - david.luff@nottingham.ac.uk // @@ -36,6 +37,7 @@ #include #include #include +#include #include // atoi() #include // sprintf @@ -55,6 +57,8 @@ #include #include +#include +#include #include "ATCutils.hxx" #include "ATISmgr.hxx" @@ -65,18 +69,50 @@ using std::cout; using std::cout; using boost::ref; using boost::tie; +using flightgear::CommStation; -FGATIS::FGATIS(const string& commbase) : +FGATIS::FGATIS(const std::string& name, int num) : + _name(name), + _num(num), + _cb_attention(this, &FGATIS::attend, fgGetNode("/environment/attention", true)), transmission(""), trans_ident(""), old_volume(0), atis_failed(false), + msg_time(0), + cur_time(0), msg_OK(0), - attention(0), + _attention(false), + _check_transmission(true), _prev_display(0), - _commbase(commbase) + _time_before_search_sec(0), + _last_frequency(0) { - fgTie("/environment/attention", this, (int_getter)0, &FGATIS::attend); + _root = fgGetNode("/instrumentation", true)->getNode(_name, num, true); + _volume = _root->getNode("volume",true); + _serviceable = _root->getNode("serviceable",true); + + if (name != "nav") + { + // only drive "operable" for non-nav instruments (nav radio drives this separately) + _operable = _root->getNode("operable",true); + _operable->setBoolValue(false); + } + + _electrical = fgGetNode("/systems/electrical/outputs",true)->getNode(_name,num, true); + _atis = _root->getNode("atis",true); + _freq = _root->getNode("frequencies/selected-mhz",true); + + // current position + _lon_node = fgGetNode("/position/longitude-deg", true); + _lat_node = fgGetNode("/position/latitude-deg", true); + _elev_node = fgGetNode("/position/altitude-ft", true); + + // backward compatibility: some properties may not exist (but default to "ON") + if (!_serviceable->hasValue()) + _serviceable->setBoolValue(true); + if (!_electrical->hasValue()) + _electrical->setDoubleValue(24.0); /////////////// // FIXME: This would be more flexible and more extensible @@ -85,24 +121,23 @@ FGATIS::FGATIS(const string& commbase) : // // Load the remap list from the .hxx file: using namespace lex; -# define NIL "" -# define REMAP(from,to) _remap[#from] = to; -# include "atis_remap.hxx" -# undef REMAP -# undef NIL -#ifdef ATIS_TEST - SG_LOG(SG_ATC, SG_ALERT, "ATIS initialized"); -#endif + # define NIL "" + # define REMAP(from,to) _remap[#from] = to; + # include "atis_remap.hxx" + # undef REMAP + # undef NIL + + #ifdef ATIS_TEST + SG_LOG(SG_ATC, SG_ALERT, "ATIS initialized"); + #endif + + _report.psl = 0; } // Hint: // http://localhost:5400/environment/attention?value=1&submit=update -FGATIS::~FGATIS() { - fgUntie("/environment/attention"); -} - FGATCVoice* FGATIS::GetVoicePointer() { FGATISMgr* pAtisMgr = globals->get_ATIS_mgr(); @@ -115,26 +150,34 @@ FGATCVoice* FGATIS::GetVoicePointer() return pAtisMgr->GetVoicePointer(ATIS); } -void FGATIS::Init() { +void FGATIS::init() +{ // Nothing to see here. Move along. } +void FGATIS::reinit() +{ + _time_before_search_sec = 0; + _check_transmission = true; +} + void -FGATIS::attend (int attn) +FGATIS::attend(SGPropertyNode* node) { - attention = attn; + if (node->getBoolValue()) + _attention = true; #ifdef ATMO_TEST int flag = fgGetInt("/sim/logging/atmo"); if (flag) { FGAltimeter().check_model(); - FGAltimeter().dump_stack(); + FGAltimeter().dump_stack(); } #endif } // Main update function - checks whether we are displaying or not the correct message. -void FGATIS::Update(double dt) { +void FGATIS::update(double dt) { cur_time = globals->get_time_params()->get_cur_time(); msg_OK = (msg_time < cur_time); @@ -148,37 +191,60 @@ void FGATIS::Update(double dt) { } #endif - if(_display) + double volume = 0; + if ((_electrical->getDoubleValue() > 8) && _serviceable->getBoolValue()) + { + // radio is switched on and OK + if (_operable.valid()) + _operable->setBoolValue(true); + + _check_transmission |= search(dt); + + if (_display) + { + volume = _volume->getDoubleValue(); + } + } + else { - string prop = _commbase + "/volume"; - double 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 changed = GenTransmission(!_prev_display, attention); - TreeOut(msg_OK); + // radio is OFF + if (_operable.valid()) + _operable->setBoolValue(false); + _time_before_search_sec = 0; + } + + if (volume > 0.05) + { + bool changed = false; + if (_check_transmission) + { + _check_transmission = false; + // 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. + if (genTransmission(!_prev_display, _attention)) + { + // update output property + treeOut(msg_OK); + changed = true; + } + } + if (changed || volume != old_volume) { - //cout << "ATIS calling ATC::render volume: " << volume << endl; - Render(transmission, volume, _commbase, true); + // audio output enabled + Render(transmission, volume, _name, true); old_volume = volume; } + _prev_display = _display; } else { -// We shouldn't be displaying - //cout << "ATIS.CXX - calling NoRender()..." << endl; - NoRender(_commbase); + // silence + NoRender(_name); + _prev_display = false; } - _prev_display = _display; - attention = 0; -} + _attention = false; -string uppercase(const string &s) { - string rslt(s); - for(string::iterator p = rslt.begin(); p != rslt.end(); p++){ - *p = toupper(*p); - } - return rslt; + FGATC::update(dt); } // Replace all occurrences of a given word. @@ -197,7 +263,7 @@ string replace_word(const string _orig, const string _www, const string _nnn){ where += nnn.length(); } - www = uppercase(www); + www = simgear::strutils::uppercase(www); for ( ; (where = orig.find(www, where)) != string::npos ; ) { orig.replace(where, www.length(), nnn); where += nnn.length(); @@ -224,296 +290,658 @@ const int minute(60); // measured in seconds // ascertaining which airports are in the US, let alone // (b) ascertaining which other places use inches. // -int Apt_US_CA(const string id) { -// Assume all IDs have length 3 or 4. -// No counterexamples have been seen. - if (id.length() == 4) { - if (id.substr(0,1) == "K") return 1; - if (id.substr(0,2) == "CY") return 1; - } - for (string::const_iterator ptr = id.begin(); ptr != id.end(); ptr++) { - if (isdigit(*ptr)) return 1; - } - return 0; +bool Apt_US_CA(const string id) +{ + // Assume all IDs have length 3 or 4. + // No counterexamples have been seen. + if (id.length() == 4) { + if (id.substr(0,1) == "K") return true; + if (id.substr(0,2) == "CY") return true; + } + for (string::const_iterator ptr = id.begin(); ptr != id.end(); ptr++) { + if (isdigit(*ptr)) return true; + } + return false; } -// 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) { - using namespace atmodel; - using namespace lex; +// voice spacers +static const string BRK = ".\n"; +static const string PAUSE = " / "; - string BRK = ".\n"; - string PAUSE = " / "; +/** Generate the actual broadcast ATIS transmission. +* 'regen' triggers a regeneration of the /current/ transmission. +* 'forceUpdate' generates a new transmission, with a new sequence. +* Returns 1 if we actually generated something. +*/ +bool FGATIS::genTransmission(const int regen, bool forceUpdate) +{ + using namespace lex; - int interval = _type == ATIS ? - ATIS_interval // ATIS updated hourly - : 2*minute; // AWOS updated more frequently + // ATIS updated hourly, AWOS updated more frequently + int interval = _type == ATIS ? ATIS_interval : 2*minute; - FGAirport* apt = FGAirport::findByIdent(ident); - int sequence = apt->getDynamics()->updateAtisSequence(interval, 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 - } + // check if pressure has changed significantly and we need to update ATIS + double Psl = fgGetDouble("/environment/pressure-sea-level-inhg"); + if (fabs(Psl-_report.psl) >= 0.15) + forceUpdate = true; - const int bs(100); - char buf[bs]; - string time_str = fgGetString("sim/time/gmt-string"); - string hours, mins; - string phonetic_seq_string; + FGAirport* apt = FGAirport::findByIdent(ident); + int sequence = apt->getDynamics()->updateAtisSequence(interval, forceUpdate); + if (!regen && sequence > LTRS) { + //xx if (msg_OK) cout << "ATIS: no change: " << sequence << endl; + //xx msg_time = cur_time; + return false; // no change since last time + } - transmission = ""; + _report.psl = Psl; + transmission = ""; - int US_CA = Apt_US_CA(ident); + // collect data and create report + createReport(apt); - if (!US_CA) { -// UK CAA radiotelephony manual indicates ATIS transmissions start -// with "This is ..." - transmission += This_is + " "; - } else { - // In the US they just start with the airport name. - } + // add facility name + genFacilityInfo(); - // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name); + if (_type == ATIS) { + // ATIS phraseology starts with "... airport information" + transmission += airport_information + " "; + } else { + // AWOS + transmission += Automated_weather_observation + " "; + } -// Note that at this point, multi-word facility names -// will sometimes contain hyphens, not spaces. - - vector name_words; - boost::split(name_words, name, boost::is_any_of(" -")); - - for (vector::const_iterator wordp = name_words.begin(); - wordp != name_words.end(); wordp++) { - string word(*wordp); -// Remap some abbreviations that occur in apt.dat, to -// make things nicer for the text-to-speech system: - for (MSS::const_iterator replace = _remap.begin(); - replace != _remap.end(); replace++) { - // Due to inconsistent capitalisation in the apt.dat file, we need - // to do a case-insensitive comparison here. - string tmp1 = word, tmp2 = replace->first; - boost::algorithm::to_lower(tmp1); - boost::algorithm::to_lower(tmp2); - if (tmp1 == tmp2) { - word = replace->second; - break; + string phonetic_seq_string = GetPhoneticLetter(sequence); // Add the sequence letter + transmission += phonetic_seq_string + BRK; + + genTimeInfo(); + + // some warnings may appear at the beginning + genWarnings(-1); + + if (_type == ATIS) // as opposed to AWOS + genRunwayInfo(apt); + + // some warnings may appear after runway info + genWarnings(0); + + // transition level + genTransitionLevel(apt); + + // weather + if (!_report.concise) + transmission += Weather + BRK; + + genWindInfo(); + + // clouds and visibility + { + string vis_info, cloud_info; + bool v = genVisibilityInfo(vis_info); + bool c = genCloudInfo(cloud_info); + _report.cavok = !(v || c); + if (!_report.cavok) + { + // there is some visibility or cloud restriction + transmission += vis_info + cloud_info; + } + else + { + // Abbreviation CAVOK vs full "clouds and visibility..." does not really depend on + // US vs rest of the world, it really seems to depend on the airport. Just use + // it as a heuristic. + if ((_report.US_CA)||(_report.concise)) + transmission += cav_ok + BRK; + else + transmission += clouds_and_visibility_OK + BRK; + } + } + + // precipitation + genPrecipitationInfo(); + + // temperature + genTemperatureInfo(); + + // pressure + genPressureInfo(); + + // TODO check whether "no significant change" applies - somehow... + transmission += No_sig + BRK; // sounds better with festival than "nosig" + + // some warnings may appear at the very end + genWarnings(1); + + if ((!_report.concise)|| _report.US_CA) + transmission += Advise_on_initial_contact_you_have_information; + else + transmission += information; + transmission += " " + phonetic_seq_string + "."; + + if (!_report.US_CA) + { + // non-US ATIS ends with "out!" + transmission += " " + out; + } + + // Pause in between two messages must be 3-5 seconds + transmission += " / / / / / / / / "; + + ///////////////////////////////////////////////////////// + // post-processing + ///////////////////////////////////////////////////////// + transmission_readable = transmission; + + // Take the previous readable string and munge it to + // be relatively-more acceptable to the primitive tts system. + // Note that : ; and . are among the token-delimiters recognized + // by the tts system. + for (size_t where;;) { + where = transmission.find_first_of(":."); + if (where == string::npos) break; + transmission.replace(where, 1, PAUSE); + } + + return true; +} + +/** Collect (most of) the data and create report. + */ +void FGATIS::createReport(const FGAirport* apt) +{ + // check country + _report.US_CA = Apt_US_CA(ident); + + // switch to enable brief ATIS message (really depends on the airport) + _report.concise = fgGetBool("/sim/atis/concise-reports", false); + + _report.ils = false; + + // time information + string time_str = fgGetString("sim/time/gmt-string"); + // Warning - this is fragile if the time string format changes + _report.hours = time_str.substr(0,2).c_str(); + _report.mins = time_str.substr(3,2).c_str(); + + // pressure/temperature + { + double press, temp; + double Tsl = fgGetDouble("/environment/temperature-sea-level-degc"); + tie(press, temp) = PT_vs_hpt(_geod.getElevationM(), _report.psl*atmodel::inHg, Tsl + atmodel::freezing); + #if 0 + SG_LOG(SG_ATC, SG_ALERT, "Field P: " << press << " T: " << temp); + SG_LOG(SG_ATC, SG_ALERT, "based on elev " << elev + << " Psl: " << Psl + << " Tsl: " << Tsl); + #endif + _report.qnh = FGAtmo().QNH(_geod.getElevationM(), press); + _report.temp = int(SGMiscd::round(FGAtmo().fake_T_vs_a_us(_geod.getElevationFt(), Tsl))); + } + + // dew point + double dpsl = fgGetDouble("/environment/dewpoint-sea-level-degc"); + _report.dewpoint = int(SGMiscd::round(FGAtmo().fake_dp_vs_a_us(dpsl, _geod.getElevationFt()))); + + // precipitation + _report.rain_norm = fgGetDouble("environment/rain-norm"); + _report.snow_norm = fgGetDouble("environment/snow-norm"); + + // NOTAMs + _report.notam = 0; + if (fgGetBool("/sim/atis/random-notams", true)) + { + _report.notam = fgGetInt("/sim/atis/notam-id", 0); // fixed NOTAM for testing/debugging only + if (!_report.notam) + { + // select pseudo-random NOTAM (changes every hour, differs for each airport) + char cksum = 0; + string name = apt->getName(); + for(string::iterator p = name.begin(); p != name.end(); p++) + { + cksum += *p; + } + cksum ^= atoi(_report.hours.c_str()); + _report.notam = cksum % 12; // 12 intentionally higher than number of available NOTAMs, so they don't appear too often + // debugging + //fgSetInt("/sim/atis/selected-notam", _report.notam); + } + } +} + +void FGATIS::genPrecipitationInfo(void) +{ + using namespace lex; + + double rain_norm = _report.rain_norm; + double snow_norm = _report.snow_norm; + + // report rain or snow - which ever is worse + if (rain_norm > 0.7) + transmission += heavy + " " + rain + BRK; + else + if (snow_norm > 0.7) + transmission += heavy + " " + snow + BRK; + else + if (rain_norm > 0.4) + transmission += moderate + " " + rain + BRK; + else + if (snow_norm > 0.4) + transmission += moderate + " " + snow + BRK; + else + if (rain_norm > 0.2) + transmission += light + " " + rain + BRK; + else + if (snow_norm > 0.05) + transmission += light + " " + snow + BRK; + else + if (rain_norm > 0.05) + transmission += light + " " + drizzle + BRK; +} + +void FGATIS::genTimeInfo(void) +{ + using namespace lex; + + if (!_report.concise) + transmission += Time + " "; + + // speak each digit separately: + transmission += ConvertNumToSpokenDigits(_report.hours + _report.mins); + transmission += " " + zulu + BRK; +} + +bool FGATIS::genVisibilityInfo(string& vis_info) +{ + using namespace lex; + + double visibility = fgGetDouble("/environment/config/boundary/entry[0]/visibility-m"); + bool IsMax = false; + bool USE_KM = !_report.US_CA; + + vis_info += Visibility + ": "; + if (USE_KM) + { + visibility /= 1000.0; // convert to statute miles + // integer kilometers + if (visibility >= 9.5) + { + visibility = 10; + IsMax = true; + } + snprintf(buf, sizeof(buf), "%i", int(.5 + visibility)); + // "kelometers" instead of "kilometers" since the festival language generator doesn't get it right otherwise + vis_info += ConvertNumToSpokenDigits(buf) + " " + kelometers; + } + else + { + visibility /= atmodel::sm; // convert to statute miles + if (visibility < 0.25) { + vis_info += less_than_one_quarter; + } else if (visibility < 0.5) { + vis_info += one_quarter; + } else if (visibility < 0.75) { + vis_info += one_half; + } else if (visibility < 1.0) { + vis_info += three_quarters; + } else if (visibility >= 1.5 && visibility < 2.0) { + vis_info += one_and_one_half; + } else { + // integer miles + if (visibility > 9.5) + { + visibility = 10; + IsMax = true; + } + snprintf(buf, sizeof(buf), "%i", int(.5 + visibility)); + vis_info += ConvertNumToSpokenDigits(buf); + } + } + if (IsMax) + { + vis_info += " " + or_more; + } + vis_info += BRK; + return !IsMax; +} + +void FGATIS::addTemperature(int Temp) +{ + if (Temp < 0) + transmission += lex::minus + " "; + else + if (Temp > 0) + { + transmission += lex::plus + " "; + } + snprintf(buf, sizeof(buf), "%i", abs(Temp)); + transmission += ConvertNumToSpokenDigits(buf); + if (_report.US_CA) + transmission += " " + lex::Celsius; +} + +void FGATIS::genTemperatureInfo() +{ + // temperature + transmission += lex::Temperature + ": "; + addTemperature(_report.temp); + + // dewpoint + transmission += BRK + lex::Dewpoint + ": "; + addTemperature(_report.dewpoint); + + transmission += BRK; +} + +bool FGATIS::genCloudInfo(string& cloud_info) +{ + using namespace lex; + + bool did_some = false; + bool did_ceiling = false; + + for (int layer = 0; layer <= 4; layer++) { + snprintf(buf, sizeof(buf), "/environment/clouds/layer[%i]/coverage", layer); + string coverage = fgGetString(buf); + if (coverage == clear) + continue; + snprintf(buf, sizeof(buf), "/environment/clouds/layer[%i]/thickness-ft", layer); + if (fgGetDouble(buf) == 0) + continue; + snprintf(buf, sizeof(buf), "/environment/clouds/layer[%i]/elevation-ft", layer); + double ceiling = int(fgGetDouble(buf) - _geod.getElevationFt()); + if (ceiling > 12000) + continue; + + // BEWARE: At the present time, the environment system has no + // way (so far as I know) to represent a "thin broken" or + // "thin overcast" layer. If/when such things are implemented + // in the environment system, code will have to be written here + // to handle them. + + // First, do the prefix if any: + if (coverage == scattered || coverage == few) { + if (!did_some) { + if (_report.concise) + cloud_info += Clouds + ": "; + else + cloud_info += Sky_condition + ": "; + did_some = true; + } + } else /* must be a ceiling */ if (!did_ceiling) { + cloud_info += " " + Ceiling + ": "; + did_ceiling = true; + did_some = true; + } else { + cloud_info += " "; // no prefix required } + int cig00 = int(SGMiscd::round(ceiling/100)); // hundreds of feet + if (cig00) { + int cig000 = cig00/10; + cig00 -= cig000*10; // just the hundreds digit + if (cig000) { + snprintf(buf, sizeof(buf), "%i", cig000); + cloud_info += ConvertNumToSpokenDigits(buf); + cloud_info += " " + thousand + " "; + } + if (cig00) { + snprintf(buf, sizeof(buf), "%i", cig00); + cloud_info += ConvertNumToSpokenDigits(buf); + cloud_info += " " + hundred + " "; + } + } else { + // Should this be "sky obscured?" + cloud_info += " " + zero + " "; // not "zero hundred" + } + cloud_info += coverage + BRK; } - transmission += word + " "; - } + if (!did_some) + cloud_info += " " + Sky + " " + clear + BRK; + return did_some; +} - if (_type == ATIS /* as opposed to AWOS */) { - transmission += airport_information + " "; - } else { - transmission += Automated_weather_observation + " "; - } +void FGATIS::genFacilityInfo(void) +{ + if ((!_report.US_CA)&&(!_report.concise)) + { + // UK CAA radiotelephony manual indicates ATIS transmissions start + // with "This is ...", while US just starts with airport name. + transmission += lex::This_is + " "; + } - phonetic_seq_string = GetPhoneticLetter(sequence); // Add the sequence letter - transmission += phonetic_seq_string + BRK; - -// 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 wind_speed = fgGetDouble("/environment/config/boundary/entry[0]/wind-speed-kt"); - double wind_dir = fgGetDouble("/environment/config/boundary/entry[0]/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 environment/config array. -// 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 (wind_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*SGMiscd::round(wind_dir/5)); - transmission += ConvertNumToSpokenDigits(buf); + // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name); - snprintf(buf, bs, "%1.0f", wind_speed); - transmission += " " + at + " " + ConvertNumToSpokenDigits(buf) + BRK; - } + // Note that at this point, multi-word facility names + // will sometimes contain hyphens, not spaces. + vector name_words; + boost::split(name_words, name, boost::is_any_of(" -")); -// Sounds better with a pause in there: - transmission += PAUSE; - - int did_some(0); - int did_ceiling(0); - - 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) - _geod.getElevationFt()); - if (ceiling > 12000) continue; - -// BEWARE: At the present time, the environment system has no -// way (so far as I know) to represent a "thin broken" or -// "thin overcast" layer. If/when such things are implemented -// in the environment system, code will have to be written here -// to handle them. - -// First, do the prefix if any: - if (coverage == scattered || coverage == few) { - if (!did_some) { - transmission += " " + Sky_condition + ": "; - did_some++; + for (vector::const_iterator wordp = name_words.begin(); + wordp != name_words.end(); wordp++) { + string word(*wordp); + // Remap some abbreviations that occur in apt.dat, to + // make things nicer for the text-to-speech system: + for (MSS::const_iterator replace = _remap.begin(); + replace != _remap.end(); replace++) { + // Due to inconsistent capitalisation in the apt.dat file, we need + // to do a case-insensitive comparison here. + string tmp1 = word, tmp2 = replace->first; + boost::algorithm::to_lower(tmp1); + boost::algorithm::to_lower(tmp2); + if (tmp1 == tmp2) { + word = replace->second; + break; + } } - } else /* must be a ceiling */ if (!did_ceiling) { - transmission += " " + Ceiling + ": "; - did_ceiling++; - did_some++; + transmission += word + " "; + } +} + +void FGATIS::genWindInfo(void) +{ + using namespace lex; + + transmission += Wind + ": "; + + double wind_speed = fgGetDouble("/environment/config/boundary/entry[0]/wind-speed-kt"); + double wind_dir = fgGetDouble("/environment/config/boundary/entry[0]/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 environment/config array. + // 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 (wind_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 { - transmission += " "; // no prefix required - } - int cig00 = int(SGMiscd::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); + // FIXME: get gust factor in somehow + snprintf(buf, sizeof(buf), "%03.0f", 5*SGMiscd::round(wind_dir/5)); transmission += ConvertNumToSpokenDigits(buf); - transmission += " " + thousand + " "; - } - if (cig00) { - snprintf(buf, bs, "%i", cig00); + if (!_report.concise) + transmission += " " + degrees; + transmission += " "; + snprintf(buf, sizeof(buf), "%1.0f", wind_speed); + transmission += at + " " + ConvertNumToSpokenDigits(buf); + if (!_report.concise) + transmission += " " + knots; + } + transmission += BRK; +} + +void FGATIS::genTransitionLevel(const FGAirport* apt) +{ + double hPa = _report.qnh/atmodel::mbar; + + /* Transition level is the flight level above which aircraft must use standard pressure and below + * which airport pressure settings must be used. + * Following definitions are taken from German ATIS: + * QNH <= 977 hPa: TRL 80 + * QNH <= 1013 hPa: TRL 70 + * QNH > 1013 hPa: TRL 60 + * (maybe differs slightly for other countries...) + */ + int tl = 60; + if (hPa <= 977) + tl = 80; + else + if (hPa <= 1013) + tl = 70; + + // add an offset to the transition level for high altitude airports (just guessing here, + // seems reasonable) + double elevationFt = apt->getElevation(); + int e = int(elevationFt / 1000.0); + if (e >= 3) + { + // TL steps in 10(00)ft + tl += (e-2)*10; + } + + snprintf(buf, sizeof(buf), "%02i", tl); + transmission += lex::Transition_level + ": " + ConvertNumToSpokenDigits(buf) + BRK; +} + +void FGATIS::genPressureInfo(void) +{ + using namespace lex; + + // hectopascal for most of the world (not US, not CA) + if(!_report.US_CA) { + double hPa = _report.qnh/atmodel::mbar; + transmission += QNH + ": "; + snprintf(buf, sizeof(buf), "%03.0f", _report.qnh / atmodel::mbar); transmission += ConvertNumToSpokenDigits(buf); - transmission += " " + hundred + " "; - } + // "hectopascal" replaced "millibars" in new ATIS standard since 2011 + if ((!_report.concise)||(hPa < 1000)) + transmission += " " + hectopascal; // "hectopascal" must be provided for values below 1000 (to avoid confusion with inHg) + + // Many (European) airports (with lots of US traffic?) provide both, hPa and inHg announcements. + // Europeans keep the "decimal" in inHg readings to make the distinction to hPa even more apparent. + // hPa/inHg separated by "equals" or "or" with some airports + if (_report.concise) + transmission += " " + equals + " "; + else + transmission += " " + Or + " "; + snprintf(buf, sizeof(buf), "%04.2f", _report.qnh / atmodel::inHg); + transmission += ConvertNumToSpokenDigits(buf); + if (!_report.concise) + transmission += " " + inches; + transmission += BRK; } else { - // Should this be "sky obscured?" - transmission += " " + zero + " "; // not "zero hundred" + // use inches of mercury for US/CA + transmission += Altimeter + ": "; + double asetting = _report.qnh / atmodel::inHg; + // shift two decimal places, US/CA airports omit the "decimal" in inHg settings + asetting *= 100.; + snprintf(buf, sizeof(buf), "%04.0f", asetting); + transmission += ConvertNumToSpokenDigits(buf); } - transmission += coverage + BRK; - } - if (!did_some) transmission += " " + Sky + " " + clear + BRK; - transmission += Temperature + ": "; - double Tsl = fgGetDouble("/environment/temperature-sea-level-degc"); - int temp = int(SGMiscd::round(FGAtmo().fake_T_vs_a_us(_geod.getElevationFt(), Tsl))); - if(temp < 0) { - transmission += lex::minus + " "; - } - snprintf(buf, bs, "%i", abs(temp)); - transmission += ConvertNumToSpokenDigits(buf); - if (US_CA) transmission += " " + Celsius; - transmission += " " + dewpoint + " "; - double dpsl = fgGetDouble("/environment/dewpoint-sea-level-degc"); - temp = int(SGMiscd::round(FGAtmo().fake_dp_vs_a_us(dpsl, _geod.getElevationFt()))); - if(temp < 0) { - transmission += lex::minus + " "; - } - snprintf(buf, bs, "%i", abs(temp)); - transmission += ConvertNumToSpokenDigits(buf); - if (US_CA) transmission += " " + Celsius; - transmission += BRK; - - transmission += Visibility + ": "; - double visibility = fgGetDouble("/environment/config/boundary/entry[0]/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 += BRK; +} - double myQNH; - double Psl = fgGetDouble("/environment/pressure-sea-level-inhg"); - { - double press, temp; - - tie(press, temp) = PT_vs_hpt(_geod.getElevationM(), Psl*inHg, Tsl + freezing); -#if 0 - SG_LOG(SG_ATC, SG_ALERT, "Field P: " << press << " T: " << temp); - SG_LOG(SG_ATC, SG_ALERT, "based on elev " << elev - << " Psl: " << Psl - << " Tsl: " << Tsl); -#endif - myQNH = FGAtmo().QNH(_geod.getElevationM(), press); - } +void FGATIS::genRunwayInfo(const FGAirport* apt) +{ + using namespace lex; -// Convert to millibars for most of the world (not US, not CA) - if((!US_CA) && fgGetBool("/sim/atc/use-millibars")) { - transmission += QNH + ": "; - myQNH /= mbar; - if (myQNH > 1000) myQNH -= 1000; // drop high digit - snprintf(buf, bs, "%03.0f", myQNH); - transmission += ConvertNumToSpokenDigits(buf) + " " + millibars + BRK; - } else { - transmission += Altimeter + ": "; - double asetting = myQNH / inHg; // use inches of mercury - asetting *= 100.; // shift two decimal places - snprintf(buf, bs, "%04.0f", asetting); - transmission += ConvertNumToSpokenDigits(buf) + BRK; - } + if (!apt) + return; + + FGRunway* rwy = apt->getActiveRunwayForUsage(); + if (!rwy) + return; + + string rwy_no = rwy->ident(); + if(rwy_no != "NN") + { + FGNavRecord* ils = rwy->ILS(); + if (ils) + { + _report.ils = true; + transmission += Expect_I_L_S_approach + " "+ runway + " "+ConvertRwyNumToSpokenString(rwy_no) + BRK; + if (fgGetBool("/sim/atis/announce-ils-frequency", false)) + { + // this is handy - but really non-standard (so disabled by default) + snprintf(buf, sizeof(buf), "%5.2f", ils->get_freq()/100.0); + transmission += I_L_S + " " + ConvertNumToSpokenDigits(buf) + BRK; + } + } + else + { + transmission += Expect_visual_approach + " "+ runway + " "+ConvertRwyNumToSpokenString(rwy_no) + BRK; + } - if (_type == ATIS /* as opposed to AWOS */) { - const FGAirport* apt = fgFindAirportID(ident); - if (apt) { - string rwy_no = apt->getActiveRunwayForUsage()->ident(); - if(rwy_no != "NN") { transmission += Landing_and_departing_runway + " "; transmission += ConvertRwyNumToSpokenString(rwy_no) + BRK; -#ifdef ATIS_TEST + #ifdef ATIS_TEST if (msg_OK) { msg_time = cur_time; cout << "In atis.cxx, r.rwy_no: " << rwy_no << " wind_dir: " << wind_dir << endl; } -#endif - } + #endif + } +} + +void FGATIS::genWarnings(int position) +{ + using namespace lex; + bool dayTime = (fgGetDouble("/sim/time/sun-angle-rad") < 1.57); + + if (position == -1) // warnings at beginning of ATIS + { + // bird related warnings at day-time only (birds are VFR-only! ;-) ) + if (dayTime) + { + if (_report.notam == 1) + transmission += Attention + ": " + flock_of_birds + " " + in_the_vicinity_of_the_airport + BRK; + else + if (_report.notam == 2) + transmission += Attention + ": " + bird_activity + " " + in_the_vicinity_of_the_airport + BRK; + } + } + else + if (position == 0) // warnings after runway messages + { + if ((_report.notam == 3)&&(_report.ils)) + { + // "__I_LS_" necessary to trick the language generator into pronouncing it properly + transmission += Attention + ": " + short_time__I_LS_interference_possible_by_taxiing_aircraft + BRK; + } + } + else + if (position == 1) // warnings at the end of the report + { + // "runway wet-wet-wet" warning in heavy rain + if (_report.rain_norm > 0.6) + { + // "wet" is repeated 3 times in ATIS warnings, since the word is difficult + // to understand over radio - but the message is important. + transmission += runway_wet + " " + wet + " " + wet + BRK; + } + + if (_report.notam == 4) + { + // intentional: "reed" instead of "read" since festival gets it wrong otherwise + transmission += reed_back_all_runway_hold_instructions + BRK; + } + else + if ((_report.notam == 5)&& _report.cavok && dayTime && + (_report.rain_norm == 0) && (_report.snow_norm == 0)) // ;-) + { + transmission += Attention + ": " + glider_operation_in_sector + BRK; + } } - transmission += On_initial_contact_advise_you_have_information + " "; - transmission += phonetic_seq_string; - transmission += "... " + BRK + PAUSE + PAUSE; - } - transmission_readable = transmission; -// Take the previous readable 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 (size_t where;;) { - where = transmission.find_first_of(":."); - if (where == string::npos) break; - transmission.replace(where, 1, PAUSE); - } - return 1; } // Put the transmission into the property tree. @@ -522,10 +950,89 @@ int FGATIS::GenTransmission(const int regen, const int special) { // http://localhost:5400/instrumentation/comm[1] // // (Also, if in debug mode, dump it to the console.) -void FGATIS::TreeOut(int msg_OK){ - string prop = _commbase + "/atis"; - globals->get_props()->setStringValue(prop.c_str(), - ("
\n" + transmission_readable + "
\n").c_str()); - SG_LOG(SG_ATC, SG_DEBUG, "**** ATIS active on: " << prop << +void FGATIS::treeOut(int msg_OK) +{ + _atis->setStringValue("
\n" + transmission_readable + "
\n"); + SG_LOG(SG_ATC, SG_DEBUG, "**** ATIS active on: " << _name << "transmission: " << transmission_readable); } + + +class RangeFilter : public CommStation::Filter +{ +public: + RangeFilter( const SGGeod & pos ) : + CommStation::Filter(), + _cart(SGVec3d::fromGeod(pos)), + _pos(pos) + { + } + + virtual bool pass(FGPositioned* aPos) const + { + flightgear::CommStation * stn = dynamic_cast(aPos); + if( NULL == stn ) + return false; + + // do the range check in cartesian space, since the distances are potentially + // large enough that the geodetic functions become unstable + // (eg, station on opposite side of the planet) + double rangeM = SGMiscd::max( stn->rangeNm(), 10.0 ) * SG_NM_TO_METER; + double d2 = distSqr( aPos->cart(), _cart); + + return d2 <= (rangeM * rangeM); + } + + virtual CommStation::Type minType() const + { + return CommStation::FREQ_ATIS; + } + + virtual CommStation::Type maxType() const + { + return CommStation::FREQ_AWOS; + } + +private: + SGVec3d _cart; + SGGeod _pos; +}; + +// Search for ATC stations by frequency +bool FGATIS::search(double dt) +{ + double frequency = _freq->getDoubleValue(); + + // Note: 122.375 must be rounded DOWN to 122370 + // in order to be consistent with apt.dat et cetera. + int freqKhz = 10 * static_cast(frequency * 100 + 0.25); + + // only search tuned frequencies when necessary + _time_before_search_sec -= dt; + + // throttle frequency searches + if ((freqKhz == _last_frequency)&&(_time_before_search_sec > 0)) + return false; + + _last_frequency = freqKhz; + _time_before_search_sec = 4.0; + + // Position of the Users Aircraft + SGGeod aircraftPos = SGGeod::fromDegFt(_lon_node->getDoubleValue(), + _lat_node->getDoubleValue(), + _elev_node->getDoubleValue()); + + RangeFilter rangeFilter(aircraftPos ); + CommStation* sta = CommStation::findByFreq(freqKhz, aircraftPos, &rangeFilter ); + SetStation(sta); + if (sta && sta->airport()) + { + SG_LOG(SG_ATC, SG_DEBUG, "FGATIS " << _name << ": " << sta->airport()->name()); + } + else + { + SG_LOG(SG_ATC, SG_DEBUG, "FGATIS " << _name << ": no station."); + } + + return true; +}