X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FATCDCL%2Fatis.cxx;h=3bc9e2f1f5fe2500ffd1a3bc5e7da7771ee857ae;hb=6bf47cd248ed388e6a4dd3ffa2d00977b00b62fb;hp=67c108f4bfaba8774a5ed6da6fb35cbc65ddba74;hpb=6fcd9f967bf659cc0dcf7288fc82a111e3eaee24;p=flightgear.git diff --git a/src/ATCDCL/atis.cxx b/src/ATCDCL/atis.cxx index 67c108f4b..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 // @@ -31,18 +32,21 @@ #endif #include "atis.hxx" +#include "atis_lexicon.hxx" #include +#include +#include +#include #include // atoi() #include // sprintf #include #include - #include - -#include +#include +#include #include #include @@ -51,64 +55,133 @@ #include
#include
#include +#include +#include +#include -#include "commlist.hxx" #include "ATCutils.hxx" -#include "ATCmgr.hxx" +#include "ATISmgr.hxx" +using std::string; +using std::map; using std::cout; using std::cout; using boost::ref; using boost::tie; +using flightgear::CommStation; -FGATIS::FGATIS() : +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), - attention(0), + msg_time(0), + cur_time(0), + msg_OK(0), + _attention(false), + _check_transmission(true), _prev_display(0), - refname("atis") + _time_before_search_sec(0), + _last_frequency(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"); + _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); } - fgTie("/environment/attention", this, (int_getter)0, &FGATIS::attend); + + _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 +// if the mappings were taken from an XML file, not hard-coded ... +// ... although having it in a .hxx file is better than nothing. +// +// 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 + + _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(); + if (!pAtisMgr) + { + SG_LOG(SG_ATC, SG_ALERT, "ERROR! No ATIS manager! Oops..."); + return NULL; + } + + 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); -#if 0 + +#ifdef ATIS_TEST if (msg_OK || _display != _prev_display) { cout << "ATIS Update: " << _display << " " << _prev_display << " len: " << transmission.length() @@ -117,39 +190,61 @@ void FGATIS::Update(double dt) { 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); + + 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 + { + // 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) { + // 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(refname); + // 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. @@ -168,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(); @@ -186,266 +281,758 @@ const int minute(60); // measured in seconds const int ATIS_interval(60*minute); #endif -// 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; - - string BRK = ".\n"; - - double tstamp = atof(fgGetString("sim/time/elapsed-sec")); - int interval = ATIS ? ATIS_interval : 2*minute; // AWOS updated frequently - int sequence = current_commlist->GetAtisSequence(ident, - tstamp, 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 - } +// FIXME: This is heuristic. It gets the right answer for +// more than 90% of the world's airports, which is a lot +// better than nothing ... but it's not 100%. +// We know "most" of the world uses millibars, +// but the US, Canada and *some* other places use inches of mercury, +// but (a) we have not implemented a reliable method of +// ascertaining which airports are in the US, let alone +// (b) ascertaining which other places use inches. +// +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; +} - const int bs(100); - char buf[bs]; - string time_str = fgGetString("sim/time/gmt-string"); - string hours, mins; - string phonetic_seq_string; +// voice spacers +static const string BRK = ".\n"; +static const string PAUSE = " / "; - transmission = ""; +/** 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; -// UK CAA radiotelephony manual indicated ATIS transmissions start -// with "This is ..." -// In the US they just start with the airport name. -// transmission += "This_is "; + // ATIS updated hourly, AWOS updated more frequently + int interval = _type == ATIS ? ATIS_interval : 2*minute; - // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name); + // 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; -// Note that at this point, multi-word facility names -// will sometimes contain hyphens, not spaces. -// Force the issue, just to be safe: + 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 + } - string name2 = name; - for(string::iterator p = name2.begin(); p != name2.end(); p++){ - if (*p == ' ') *p = '-'; - } + _report.psl = Psl; + transmission = ""; -/////////////// -// FIXME: This would be more flexible and more extensible -// if the mappings were taken from an XML file, not hard-coded. -/////////////// + // collect data and create report + createReport(apt); -// Remap some abbreviations that occur in apt.dat, to -// make things nicer for the text-to-speech system: - name2 = replace_word(name2, "Intl", "International"); - name2 = replace_word(name2, "Rgnl", "Regional"); - name2 = replace_word(name2, "Co", "County"); - name2 = replace_word(name2, "Muni", "Municipal"); - name2 = replace_word(name2, "Mem", "Memorial"); - name2 = replace_word(name2, "Fld", "Field"); - name2 = replace_word(name2, "AFB", "Air-Force-Base"); - name2 = replace_word(name2, "AAF", "Army-Air-Field"); - name2 = replace_word(name2, "MCAS", "Marine-Corps-Air-Station"); - transmission += name2 + " "; - if (_type == ATIS /* as opposed to AWOS */) { - transmission += "airport_information "; - phonetic_seq_string = GetPhoneticLetter(sequence); // Add the sequence letter + // add facility name + genFacilityInfo(); + + if (_type == ATIS) { + // ATIS phraseology starts with "... airport information" + transmission += airport_information + " "; + } else { + // AWOS + transmission += Automated_weather_observation + " "; + } + + string phonetic_seq_string = GetPhoneticLetter(sequence); // Add the sequence letter 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 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); - snprintf(buf, bs, "%1.0f", wind_speed); - transmission += " at " + ConvertNumToSpokenDigits(buf) + BRK; - } + genTimeInfo(); - 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; - 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(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); + // 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); - transmission += " thousand "; + 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) { - snprintf(buf, bs, "%i", cig00); - transmission += ConvertNumToSpokenDigits(buf); - transmission += " hundred "; + 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; + } + if (!did_some) + cloud_info += " " + Sky + " " + clear + BRK; + return did_some; +} + +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 + " "; + } + + // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name); + + // 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; + } + } + 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 { - // Should this be "sky obscured?" - transmission += " zero "; // not "zero hundred" + // FIXME: get gust factor in somehow + snprintf(buf, sizeof(buf), "%03.0f", 5*SGMiscd::round(wind_dir/5)); + transmission += ConvertNumToSpokenDigits(buf); + if (!_report.concise) + transmission += " " + degrees; + transmission += " "; + snprintf(buf, sizeof(buf), "%1.0f", wind_speed); + transmission += at + " " + ConvertNumToSpokenDigits(buf); + if (!_report.concise) + transmission += " " + knots; } - transmission += coverage + BRK; - } + transmission += 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 += "minus "; - } - snprintf(buf, bs, "%i", abs(temp)); - transmission += ConvertNumToSpokenDigits(buf); - 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 += "minus "; - } - snprintf(buf, bs, "%i", abs(temp)); - transmission += ConvertNumToSpokenDigits(buf) + 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; +void FGATIS::genTransitionLevel(const FGAirport* apt) +{ + double hPa = _report.qnh/atmodel::mbar; - transmission += "Altimeter: "; - 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); - } - if(ident.substr(0,2) == "EG" && fgGetBool("/sim/atc/use-millibars")) { - // Convert to millibars for the UK! - myQNH /= mbar; - if (myQNH > 1000) myQNH -= 1000; // drop high digit - snprintf(buf, bs, "%03.0f", myQNH); - } else { - myQNH /= inHg; - myQNH *= 100.; // shift two decimal places - snprintf(buf, bs, "%04.0f", myQNH); - } - transmission += ConvertNumToSpokenDigits(buf) + BRK; - - if (_type == ATIS /* as opposed to AWOS */) { - const FGAirport* apt = fgFindAirportID(ident); - assert(apt); - string rwy_no = apt->getActiveRunwayForUsage()->ident(); - if(rwy_no != "NN") { - transmission += "Landing_and_departing_runway "; + /* 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); + // "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 { + // 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 += BRK; +} + +void FGATIS::genRunwayInfo(const FGAirport* apt) +{ + using namespace lex; + + 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; + } + + transmission += Landing_and_departing_runway + " "; transmission += ConvertRwyNumToSpokenString(rwy_no) + BRK; + #ifdef ATIS_TEST if (msg_OK) { msg_time = cur_time; - //cout << "In atis.cxx, r.rwy_no: " << rwy_no - // << " wind_dir: " << wind_dir << endl; + cout << "In atis.cxx, r.rwy_no: " << rwy_no + << " wind_dir: " << wind_dir << endl; } + #endif } - 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 +} + +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; + } + } +} + +// Put the transmission into the property tree. +// You can see it by pointing a web browser // at the property tree. The second comm radio is: // http://localhost:5400/instrumentation/comm[1] -#endif +// +// (Also, if in debug mode, dump it to the console.) +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); +} -// 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 (size_t where;;) { - where = transmission.find_first_of(":."); - if (where == string::npos) break; - transmission.replace(where, 1, " /_ "); - } - return 1; + +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; }