1 // atis.cxx - routines to generate the ATIS info string
2 // This is the implementation of the FGATIS class
4 // Written by David Luff, started October 2001.
6 // Copyright (C) 2001 David C Luff - david.luff@nottingham.ac.uk
8 // This program is free software; you can redistribute it and/or
9 // modify it under the terms of the GNU General Public License as
10 // published by the Free Software Foundation; either version 2 of the
11 // License, or (at your option) any later version.
13 // This program is distributed in the hope that it will be useful, but
14 // WITHOUT ANY WARRANTY; without even the implied warranty of
15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 // General Public License for more details.
18 // You should have received a copy of the GNU General Public License
19 // along with this program; if not, write to the Free Software
20 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23 ///// TODO: _Cumulative_ sky coverage.
24 ///// TODO: wind _gust_
25 ///// TODO: more-sensible encoding of voice samples
26 ///// u-law? outright synthesis?
34 #include "atis_lexicon.hxx"
36 #include <simgear/compiler.h>
37 #include <simgear/math/sg_random.h>
38 #include <simgear/misc/sg_path.hxx>
40 #include <stdlib.h> // atoi()
41 #include <stdio.h> // sprintf
45 #include <boost/tuple/tuple.hpp>
46 #include <boost/algorithm/string.hpp>
47 #include <boost/algorithm/string/case_conv.hpp>
49 #include <Environment/environment_mgr.hxx>
50 #include <Environment/environment.hxx>
51 #include <Environment/atmosphere.hxx>
53 #include <Main/fg_props.hxx>
54 #include <Main/globals.hxx>
55 #include <Airports/runways.hxx>
56 #include <Airports/dynamics.hxx>
58 #include <ATC/CommStation.hxx>
60 #include "ATCutils.hxx"
61 #include "ATISmgr.hxx"
69 using flightgear::CommStation;
71 FGATIS::FGATIS(const std::string& name, int num) :
74 _cb_attention(this, &FGATIS::attend, fgGetNode("/environment/attention", true)),
84 _time_before_search_sec(0),
87 _root = fgGetNode("/instrumentation", true)->getNode(_name, num, true);
88 _volume = _root->getNode("volume",true);
89 _serviceable = _root->getNode("serviceable",true);
93 // only drive "operable" for non-nav instruments (nav radio drives this separately)
94 _operable = _root->getNode("operable",true);
95 _operable->setBoolValue(false);
98 _electrical = fgGetNode("/systems/electrical/outputs",true)->getNode(_name,num, true);
99 _atis = _root->getNode("atis",true);
100 _freq = _root->getNode("frequencies/selected-mhz",true);
103 _lon_node = fgGetNode("/position/longitude-deg", true);
104 _lat_node = fgGetNode("/position/latitude-deg", true);
105 _elev_node = fgGetNode("/position/altitude-ft", true);
107 // backward compatibility: some properties may not exist (but default to "ON")
108 if (!_serviceable->hasValue())
109 _serviceable->setBoolValue(true);
110 if (!_electrical->hasValue())
111 _electrical->setDoubleValue(24.0);
114 // FIXME: This would be more flexible and more extensible
115 // if the mappings were taken from an XML file, not hard-coded ...
116 // ... although having it in a .hxx file is better than nothing.
118 // Load the remap list from the .hxx file:
121 # define REMAP(from,to) _remap[#from] = to;
122 # include "atis_remap.hxx"
127 SG_LOG(SG_ATC, SG_ALERT, "ATIS initialized");
132 // http://localhost:5400/environment/attention?value=1&submit=update
134 FGATCVoice* FGATIS::GetVoicePointer()
136 FGATISMgr* pAtisMgr = globals->get_ATIS_mgr();
139 SG_LOG(SG_ATC, SG_ALERT, "ERROR! No ATIS manager! Oops...");
143 return pAtisMgr->GetVoicePointer(ATIS);
146 void FGATIS::init() {
147 // Nothing to see here. Move along.
151 FGATIS::attend(SGPropertyNode* node)
153 _attention = node->getBoolValue();
155 int flag = fgGetInt("/sim/logging/atmo");
157 FGAltimeter().check_model();
158 FGAltimeter().dump_stack();
164 // Main update function - checks whether we are displaying or not the correct message.
165 void FGATIS::update(double dt) {
166 cur_time = globals->get_time_params()->get_cur_time();
167 msg_OK = (msg_time < cur_time);
170 if (msg_OK || _display != _prev_display) {
171 cout << "ATIS Update: " << _display << " " << _prev_display
172 << " len: " << transmission.length()
173 << " oldvol: " << old_volume
174 << " dt: " << dt << endl;
180 if ((_electrical->getDoubleValue() > 8) && _serviceable->getBoolValue())
182 _time_before_search_sec -= dt;
183 // radio is switched on and OK
184 if (_operable.valid())
185 _operable->setBoolValue(true);
187 // Search the tuned frequencies
192 volume = _volume->getDoubleValue();
198 if (_operable.valid())
199 _operable->setBoolValue(false);
200 _time_before_search_sec = 0;
205 // Check if we need to update the message
206 // - basically every hour and if the weather changes significantly at the station
207 // If !_prev_display, the radio had been detuned for a while and our
208 // "transmission" variable was lost when we were de-instantiated.
209 int changed = genTransmission(!_prev_display, _attention);
211 // update output property
214 if (changed || volume != old_volume) {
215 // audio output enabled
216 Render(transmission, volume, _name, true);
219 _prev_display = _display;
223 _prev_display = false;
230 string uppercase(const string &s) {
232 for(string::iterator p = rslt.begin(); p != rslt.end(); p++){
238 // Replace all occurrences of a given word.
239 // Words in the original string must be separated by hyphens (not spaces).
240 // We check for the word as given, and for the all-caps version thereof.
241 string replace_word(const string _orig, const string _www, const string _nnn){
242 // The following are so we can match words at the beginning
243 // and end of the string.
244 string orig = "-" + _orig + "-";
245 string www = "-" + _www + "-";
246 string nnn = "-" + _nnn + "-";
249 for ( ; (where = orig.find(www, where)) != string::npos ; ) {
250 orig.replace(where, www.length(), nnn);
251 where += nnn.length();
254 www = uppercase(www);
255 for ( ; (where = orig.find(www, where)) != string::npos ; ) {
256 orig.replace(where, www.length(), nnn);
257 where += nnn.length();
259 where = orig.length();
260 return orig.substr(1, where-2);
263 // Normally the interval is 1 hour,
264 // but you can shorten it for testing.
265 const int minute(60); // measured in seconds
267 const int ATIS_interval(2*minute);
269 const int ATIS_interval(60*minute);
272 // FIXME: This is heuristic. It gets the right answer for
273 // more than 90% of the world's airports, which is a lot
274 // better than nothing ... but it's not 100%.
275 // We know "most" of the world uses millibars,
276 // but the US, Canada and *some* other places use inches of mercury,
277 // but (a) we have not implemented a reliable method of
278 // ascertaining which airports are in the US, let alone
279 // (b) ascertaining which other places use inches.
281 bool Apt_US_CA(const string id) {
282 // Assume all IDs have length 3 or 4.
283 // No counterexamples have been seen.
284 if (id.length() == 4) {
285 if (id.substr(0,1) == "K") return true;
286 if (id.substr(0,2) == "CY") return true;
288 for (string::const_iterator ptr = id.begin(); ptr != id.end(); ptr++) {
289 if (isdigit(*ptr)) return true;
294 static string BRK = ".\n";
295 static string PAUSE = " / ";
297 /** Generate the actual broadcast ATIS transmission.
298 * 'regen' triggers a regeneration of the /current/ transmission.
299 * 'special' generates a new transmission, with a new sequence.
300 * Returns 1 if we actually generated something.
302 int FGATIS::genTransmission(const int regen, const bool special) {
303 using namespace atmodel;
306 // ATIS updated hourly, AWOS updated more frequently
307 int interval = _type == ATIS ? ATIS_interval : 2*minute;
309 FGAirport* apt = FGAirport::findByIdent(ident);
310 int sequence = apt->getDynamics()->updateAtisSequence(interval, special);
311 if (!regen && sequence > LTRS) {
312 //xx if (msg_OK) cout << "ATIS: no change: " << sequence << endl;
313 //xx msg_time = cur_time;
314 return 0; // no change since last time
319 bool US_CA = Apt_US_CA(ident);
321 // UK CAA radiotelephony manual indicates ATIS transmissions start
322 // with "This is ...", while US just starts with airport name.
323 transmission += This_is + " ";
330 // ATIS phraseology starts with "... airport information"
331 transmission += airport_information + " ";
334 transmission += Automated_weather_observation + " ";
337 string phonetic_seq_string = GetPhoneticLetter(sequence); // Add the sequence letter
338 transmission += phonetic_seq_string + BRK;
344 // Sounds better with a pause in there:
345 transmission += PAUSE;
350 genTemperatureInfo(Tsl, US_CA);
354 genPressureInfo(US_CA, Tsl);
356 if (_type == ATIS /* as opposed to AWOS */) {
357 genRunwayInfo(phonetic_seq_string);
360 // Pause in between two messages must be 3-5 seconds
361 transmission += PAUSE + PAUSE + PAUSE + PAUSE;
363 /////////////////////////////////////////////////////////
365 /////////////////////////////////////////////////////////
366 transmission_readable = transmission;
368 // Take the previous readable string and munge it to
369 // be relatively-more acceptable to the primitive tts system.
370 // Note that : ; and . are among the token-delimiters recognized
371 // by the tts system.
372 for (size_t where;;) {
373 where = transmission.find_first_of(":.");
374 if (where == string::npos) break;
375 transmission.replace(where, 1, PAUSE);
381 void FGATIS::genTimeInfo(void)
383 using namespace atmodel;
387 string time_str = fgGetString("sim/time/gmt-string");
389 // Warning - this is fragile if the time string format changes
390 hours = time_str.substr(0,2).c_str();
391 mins = time_str.substr(3,2).c_str();
393 // speak each digit separately:
394 transmission += ConvertNumToSpokenDigits(hours + mins);
395 transmission += " " + zulu + " " + weather + BRK;
398 void FGATIS::genVisibilityInfo(void)
400 using namespace atmodel;
405 transmission += Visibility + ": ";
406 double visibility = fgGetDouble("/environment/config/boundary/entry[0]/visibility-m");
407 visibility /= atmodel::sm; // convert to statute miles
408 if (visibility < 0.25) {
409 transmission += less_than_one_quarter;
410 } else if (visibility < 0.5) {
411 transmission += one_quarter;
412 } else if (visibility < 0.75) {
413 transmission += one_half;
414 } else if (visibility < 1.0) {
415 transmission += three_quarters;
416 } else if (visibility >= 1.5 && visibility < 2.0) {
417 transmission += one_and_one_half;
420 if (visibility > 10) visibility = 10;
421 sprintf(buf, "%i", int(.5 + visibility));
422 transmission += ConvertNumToSpokenDigits(buf);
427 void FGATIS::genTemperatureInfo(double& Tsl, bool US_CA)
429 using namespace atmodel;
434 transmission += Temperature + ": ";
435 Tsl = fgGetDouble("/environment/temperature-sea-level-degc");
436 int temp = int(SGMiscd::round(FGAtmo().fake_T_vs_a_us(_geod.getElevationFt(), Tsl)));
438 transmission += lex::minus + " ";
440 snprintf(buf, bs, "%i", abs(temp));
441 transmission += ConvertNumToSpokenDigits(buf);
442 if (US_CA) transmission += " " + Celsius;
443 transmission += " " + dewpoint + " ";
444 double dpsl = fgGetDouble("/environment/dewpoint-sea-level-degc");
445 temp = int(SGMiscd::round(FGAtmo().fake_dp_vs_a_us(dpsl, _geod.getElevationFt())));
447 transmission += lex::minus + " ";
449 snprintf(buf, bs, "%i", abs(temp));
450 transmission += ConvertNumToSpokenDigits(buf);
451 if (US_CA) transmission += " " + Celsius;
455 void FGATIS::genCloudInfo(void)
457 using namespace atmodel;
463 bool did_some = false;
464 bool did_ceiling = false;
466 for (int layer = 0; layer <= 4; layer++) {
467 snprintf(buf, bs, "/environment/clouds/layer[%i]/coverage", layer);
468 string coverage = fgGetString(buf);
469 if (coverage == clear) continue;
470 snprintf(buf, bs, "/environment/clouds/layer[%i]/thickness-ft", layer);
471 if (fgGetDouble(buf) == 0) continue;
472 snprintf(buf, bs, "/environment/clouds/layer[%i]/elevation-ft", layer);
473 double ceiling = int(fgGetDouble(buf) - _geod.getElevationFt());
474 if (ceiling > 12000) continue;
476 // BEWARE: At the present time, the environment system has no
477 // way (so far as I know) to represent a "thin broken" or
478 // "thin overcast" layer. If/when such things are implemented
479 // in the environment system, code will have to be written here
482 // First, do the prefix if any:
483 if (coverage == scattered || coverage == few) {
485 transmission += " " + Sky_condition + ": ";
488 } else /* must be a ceiling */ if (!did_ceiling) {
489 transmission += " " + Ceiling + ": ";
493 transmission += " "; // no prefix required
495 int cig00 = int(SGMiscd::round(ceiling/100)); // hundreds of feet
497 int cig000 = cig00/10;
498 cig00 -= cig000*10; // just the hundreds digit
500 snprintf(buf, bs, "%i", cig000);
501 transmission += ConvertNumToSpokenDigits(buf);
502 transmission += " " + thousand + " ";
505 snprintf(buf, bs, "%i", cig00);
506 transmission += ConvertNumToSpokenDigits(buf);
507 transmission += " " + hundred + " ";
510 // Should this be "sky obscured?"
511 transmission += " " + zero + " "; // not "zero hundred"
513 transmission += coverage + BRK;
515 if (!did_some) transmission += " " + Sky + " " + clear + BRK;
518 void FGATIS::genFacilityInfo(void)
520 // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name);
522 // Note that at this point, multi-word facility names
523 // will sometimes contain hyphens, not spaces.
524 vector<string> name_words;
525 boost::split(name_words, name, boost::is_any_of(" -"));
527 for (vector<string>::const_iterator wordp = name_words.begin();
528 wordp != name_words.end(); wordp++) {
530 // Remap some abbreviations that occur in apt.dat, to
531 // make things nicer for the text-to-speech system:
532 for (MSS::const_iterator replace = _remap.begin();
533 replace != _remap.end(); replace++) {
534 // Due to inconsistent capitalisation in the apt.dat file, we need
535 // to do a case-insensitive comparison here.
536 string tmp1 = word, tmp2 = replace->first;
537 boost::algorithm::to_lower(tmp1);
538 boost::algorithm::to_lower(tmp2);
540 word = replace->second;
544 transmission += word + " ";
548 void FGATIS::genWindInfo(void)
550 using namespace atmodel;
553 transmission += wind + ": ";
555 double wind_speed = fgGetDouble("/environment/config/boundary/entry[0]/wind-speed-kt");
556 double wind_dir = fgGetDouble("/environment/config/boundary/entry[0]/wind-from-heading-deg");
557 while (wind_dir <= 0) wind_dir += 360;
558 // The following isn't as bad a kludge as it might seem.
559 // It combines the magvar at the /aircraft/ location with
560 // the wind direction in the environment/config array.
561 // But if the aircraft is close enough to the station to
562 // be receiving the ATIS signal, this should be a good-enough
563 // approximation. For more-distant aircraft, the wind_dir
564 // shouldn't be corrected anyway.
565 // The less-kludgy approach would be to use the magvar associated
566 // with the station, but that is not tabulated in the stationweather
567 // structure as it stands, and computing it would be expensive.
568 // Also note that as it stands, there is only one environment in
569 // the entire FG universe, so the aircraft environment is the same
570 // as the station environment anyway.
571 wind_dir -= fgGetDouble("/environment/magnetic-variation-deg"); // wind_dir now magnetic
572 if (wind_speed == 0) {
573 // Force west-facing rwys to be used in no-wind situations
574 // which is consistent with Flightgear's initial setup:
576 transmission += " " + light_and_variable;
581 // FIXME: get gust factor in somehow
582 snprintf(buf, bs, "%03.0f", 5*SGMiscd::round(wind_dir/5));
583 transmission += ConvertNumToSpokenDigits(buf);
585 snprintf(buf, bs, "%1.0f", wind_speed);
586 transmission += " " + at + " " + ConvertNumToSpokenDigits(buf) + BRK;
590 void FGATIS::genPressureInfo(bool US_CA, double Tsl)
592 using namespace atmodel;
595 double Psl = fgGetDouble("/environment/pressure-sea-level-inhg");
602 tie(press, temp) = PT_vs_hpt(_geod.getElevationM(), Psl*inHg, Tsl + freezing);
604 SG_LOG(SG_ATC, SG_ALERT, "Field P: " << press << " T: " << temp);
605 SG_LOG(SG_ATC, SG_ALERT, "based on elev " << elev
609 myQNH = FGAtmo().QNH(_geod.getElevationM(), press);
612 // Convert to millibars for most of the world (not US, not CA)
613 if((!US_CA) && fgGetBool("/sim/atc/use-millibars")) {
614 transmission += QNH + ": ";
616 snprintf(buf, bs, "%03.0f", myQNH);
617 transmission += ConvertNumToSpokenDigits(buf);
618 // TODO Extend voice samples so we can replace "millibars" with "hectopascal" (new ATIS standard since 2011)
620 transmission += " " + millibars; // "hectopascal" (millibars) spoken for values below 1000 only (to avoid confusion with inHg)
623 transmission += Altimeter + ": ";
624 double asetting = myQNH / inHg; // use inches of mercury
625 asetting *= 100.; // shift two decimal places
626 snprintf(buf, bs, "%04.0f", asetting);
627 transmission += ConvertNumToSpokenDigits(buf) + BRK;
631 void FGATIS::genRunwayInfo(const string& phonetic_seq_string)
633 using namespace atmodel;
636 const FGAirport* apt = fgFindAirportID(ident);
638 FGRunway* rwy = apt->getActiveRunwayForUsage();
641 string rwy_no = rwy->ident();
643 transmission += Landing_and_departing_runway + " ";
644 transmission += ConvertRwyNumToSpokenString(rwy_no) + BRK;
648 cout << "In atis.cxx, r.rwy_no: " << rwy_no
649 << " wind_dir: " << wind_dir << endl;
655 transmission += On_initial_contact_advise_you_have_information + " ";
656 transmission += phonetic_seq_string;
657 transmission += "... " + BRK;
660 // Put the transmission into the property tree.
661 // You can see it by pointing a web browser
662 // at the property tree. The second comm radio is:
663 // http://localhost:5400/instrumentation/comm[1]
665 // (Also, if in debug mode, dump it to the console.)
666 void FGATIS::TreeOut(int msg_OK)
668 _atis->setStringValue("<pre>\n" + transmission_readable + "</pre>\n");
669 SG_LOG(SG_ATC, SG_DEBUG, "**** ATIS active on: " << _name <<
670 "transmission: " << transmission_readable);
675 class RangeFilter : public CommStation::Filter
678 RangeFilter( const SGGeod & pos ) :
679 CommStation::Filter(),
680 _cart(SGVec3d::fromGeod(pos)),
685 virtual bool pass(FGPositioned* aPos) const
687 flightgear::CommStation * stn = dynamic_cast<flightgear::CommStation*>(aPos);
691 // do the range check in cartesian space, since the distances are potentially
692 // large enough that the geodetic functions become unstable
693 // (eg, station on opposite side of the planet)
694 double rangeM = SGMiscd::max( stn->rangeNm(), 10.0 ) * SG_NM_TO_METER;
695 double d2 = distSqr( aPos->cart(), _cart);
697 return d2 <= (rangeM * rangeM);
700 virtual CommStation::Type minType() const
702 return CommStation::FREQ_ATIS;
705 virtual CommStation::Type maxType() const
707 return CommStation::FREQ_AWOS;
715 // Search for ATC stations by frequency
716 void FGATIS::search(void)
718 double frequency = _freq->getDoubleValue();
720 // Note: 122.375 must be rounded DOWN to 122370
721 // in order to be consistent with apt.dat et cetera.
722 int freqKhz = 10 * static_cast<int>(frequency * 100 + 0.25);
724 // throttle frequency searches
725 if ((freqKhz == _last_frequency)&&(_time_before_search_sec > 0))
728 _last_frequency = freqKhz;
729 _time_before_search_sec = 4.0;
731 // Position of the Users Aircraft
732 SGGeod aircraftPos = SGGeod::fromDegFt(_lon_node->getDoubleValue(),
733 _lat_node->getDoubleValue(),
734 _elev_node->getDoubleValue());
736 RangeFilter rangeFilter(aircraftPos );
737 CommStation* sta = CommStation::findByFreq(freqKhz, aircraftPos, &rangeFilter );
739 if (sta && sta->airport())
741 SG_LOG(SG_ATC, SG_DEBUG, "FGATIS " << _name << ": " << sta->airport()->name());
745 SG_LOG(SG_ATC, SG_DEBUG, "FGATIS " << _name << ": no station.");