// 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
//
#include "atis_lexicon.hxx"
#include <simgear/compiler.h>
+#include <simgear/math/sg_random.h>
+#include <simgear/misc/sg_path.hxx>
+#include <simgear/misc/strutils.hxx>
#include <stdlib.h> // atoi()
#include <stdio.h> // sprintf
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/case_conv.hpp>
-
-#include <simgear/misc/sg_path.hxx>
-
#include <Environment/environment_mgr.hxx>
#include <Environment/environment.hxx>
#include <Environment/atmosphere.hxx>
#include <Main/fg_props.hxx>
#include <Main/globals.hxx>
#include <Airports/runways.hxx>
+#include <Airports/dynamics.hxx>
+#include <ATC/CommStation.hxx>
+#include <Navaids/navrecord.hxx>
-#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),
+ msg_time(0),
+ cur_time(0),
msg_OK(0),
- attention(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
//
// 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();
+ 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);
+
#ifdef ATIS_TEST
if (msg_OK || _display != _prev_display) {
cout << "ATIS Update: " << _display << " " << _prev_display
msg_time = cur_time;
}
#endif
- if(_display) {
- double volume(0);
- for (map<string,int>::iterator act = active_on.begin();
- act != active_on.end(); act++) {
- string prop = "/instrumentation/" + act->first + "/volume";
- volume += globals->get_props()->getDoubleValue(prop.c_str());
+
+ 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;
+ }
}
-// 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);
- TreeOut(msg_OK);
- if (rslt || volume != old_volume) {
- //cout << "ATIS calling ATC::render volume: " << volume << endl;
- Render(transmission, volume, refname, 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.
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();
// 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 = " / ";
-
- double tstamp = atof(fgGetString("sim/time/elapsed-sec"));
- int interval = _type == ATIS ?
- ATIS_interval // ATIS updated hourly
- : 2*minute; // AWOS updated more 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
- }
+/** 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;
+
+ // ATIS updated hourly, AWOS updated more frequently
+ int interval = _type == ATIS ? ATIS_interval : 2*minute;
- const int bs(100);
- char buf[bs];
- string time_str = fgGetString("sim/time/gmt-string");
- string hours, mins;
- string phonetic_seq_string;
+ // 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;
- transmission = "";
+ 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
+ }
- int US_CA = Apt_US_CA(ident);
+ _report.psl = Psl;
+ transmission = "";
- 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.
- }
+ // collect data and create report
+ createReport(apt);
- // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name);
+ // add facility name
+ genFacilityInfo();
-// Note that at this point, multi-word facility names
-// will sometimes contain hyphens, not spaces.
-
- vector<string> name_words;
- boost::split(name_words, name, boost::is_any_of(" -"));
-
- for (vector<string>::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;
+ 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;
+
+ 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.
+ std::vector<std::string> 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( std::vector<string>::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
+ // 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 += 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;
}
- 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);
+
+ 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 += " " + thousand + " ";
- }
- if (cig00) {
- snprintf(buf, bs, "%i", cig00);
+ // "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);
- transmission += " " + hundred + " ";
- }
+ 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
}
- 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,
-// possibly in multiple places if multiple radios
-// are tuned to the same ATIS.
+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]
//
// (Also, if in debug mode, dump it to the console.)
-void FGATIS::TreeOut(int msg_OK){
- for (map<string,int>::iterator act = active_on.begin();
- act != active_on.end();
- act++){
- string prop = "/instrumentation/" + act->first + "/atis";
- globals->get_props()->setStringValue(prop.c_str(),
- ("<pre>\n" + transmission_readable + "</pre>\n").c_str());
-#ifdef ATIS_TEST
- if (msg_OK) cout << "**** ATIS active on: " << prop << endl;
-#endif
- }
-#ifdef ATIS_TEST
- if (msg_OK) cout << transmission_readable << endl;
-#endif
+void FGATIS::treeOut(int msg_OK)
+{
+ _atis->setStringValue("<pre>\n" + transmission_readable + "</pre>\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<flightgear::CommStation*>(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<int>(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);
+
+ return true;
}