]> git.mxchange.org Git - flightgear.git/blob - src/ATCDCL/atis.cxx
Fix finding ATIS CommStation
[flightgear.git] / src / ATCDCL / atis.cxx
1 // atis.cxx - routines to generate the ATIS info string
2 // This is the implementation of the FGATIS class
3 //
4 // Written by David Luff, started October 2001.
5 //
6 // Copyright (C) 2001  David C Luff - david.luff@nottingham.ac.uk
7 //
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.
12 //
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.
17 //
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.
21
22 /////
23 ///// TODO:  _Cumulative_ sky coverage.
24 ///// TODO:  wind _gust_
25 ///// TODO:  more-sensible encoding of voice samples
26 /////       u-law?  outright synthesis?
27 /////
28
29 #ifdef HAVE_CONFIG_H
30 #  include <config.h>
31 #endif
32
33 #include "atis.hxx"
34 #include "atis_lexicon.hxx"
35
36 #include <simgear/compiler.h>
37 #include <simgear/math/sg_random.h>
38 #include <simgear/misc/sg_path.hxx>
39
40 #include <stdlib.h> // atoi()
41 #include <stdio.h>  // sprintf
42 #include <string>
43 #include <iostream>
44
45 #include <boost/tuple/tuple.hpp>
46 #include <boost/algorithm/string.hpp>
47 #include <boost/algorithm/string/case_conv.hpp>
48
49 #include <Environment/environment_mgr.hxx>
50 #include <Environment/environment.hxx>
51 #include <Environment/atmosphere.hxx>
52
53 #include <Main/fg_props.hxx>
54 #include <Main/globals.hxx>
55 #include <Airports/runways.hxx>
56 #include <Airports/dynamics.hxx>
57
58 #include <ATC/CommStation.hxx>
59
60 #include "ATCutils.hxx"
61 #include "ATISmgr.hxx"
62
63 using std::string;
64 using std::map;
65 using std::cout;
66 using std::cout;
67 using boost::ref;
68 using boost::tie;
69 using flightgear::CommStation;
70
71 FGATIS::FGATIS(const std::string& name, int num) :
72   _name(name),
73   _num(num),
74   _cb_attention(this, &FGATIS::attend, fgGetNode("/environment/attention", true)),
75   transmission(""),
76   trans_ident(""),
77   old_volume(0),
78   atis_failed(false),
79   msg_OK(0),
80   _attention(false),
81   _prev_display(0),
82   _time_before_search_sec(0),
83   _last_frequency(0)
84 {
85   _root         = fgGetNode("/instrumentation", true)->getNode(_name, num, true);
86   _volume       = _root->getNode("volume",true);
87   _serviceable  = _root->getNode("serviceable",true);
88
89   if (name != "nav")
90   {
91       // only drive "operable" for non-nav instruments (nav radio drives this separately)
92       _operable = _root->getNode("operable",true);
93       _operable->setBoolValue(false);
94   }
95
96   _electrical   = fgGetNode("/systems/electrical/outputs",true)->getNode(_name,num, true);
97   _atis         = _root->getNode("atis",true);
98   _freq         = _root->getNode("frequencies/selected-mhz",true);
99
100   // current position
101   _lon_node  = fgGetNode("/position/longitude-deg", true);
102   _lat_node  = fgGetNode("/position/latitude-deg",  true);
103   _elev_node = fgGetNode("/position/altitude-ft",   true);
104
105   // backward compatibility: some properties may not exist (but default to "ON")
106   if (!_serviceable->hasValue())
107       _serviceable->setBoolValue(true);
108   if (!_electrical->hasValue())
109       _electrical->setDoubleValue(24.0);
110
111 ///////////////
112 // FIXME:  This would be more flexible and more extensible
113 // if the mappings were taken from an XML file, not hard-coded ...
114 // ... although having it in a .hxx file is better than nothing.
115 //
116 // Load the remap list from the .hxx file:
117   using namespace lex;
118 # define NIL ""
119 # define REMAP(from,to) _remap[#from] = to;
120 # include "atis_remap.hxx"
121 # undef REMAP
122 # undef NIL
123
124 #ifdef ATIS_TEST
125   SG_LOG(SG_ATC, SG_ALERT, "ATIS initialized");
126 #endif
127 }
128
129 // Hint:
130 // http://localhost:5400/environment/attention?value=1&submit=update
131
132 FGATCVoice* FGATIS::GetVoicePointer()
133 {
134     FGATISMgr* pAtisMgr = globals->get_ATIS_mgr();
135     if (!pAtisMgr)
136     {
137         SG_LOG(SG_ATC, SG_ALERT, "ERROR! No ATIS manager! Oops...");
138         return NULL;
139     }
140
141     return pAtisMgr->GetVoicePointer(ATIS);
142 }
143
144 void FGATIS::init() {
145 // Nothing to see here.  Move along.
146 }
147
148 void
149 FGATIS::attend(SGPropertyNode* node)
150 {
151   _attention = node->getBoolValue();
152 #ifdef ATMO_TEST
153   int flag = fgGetInt("/sim/logging/atmo");
154   if (flag) {
155     FGAltimeter().check_model();
156         FGAltimeter().dump_stack();
157   }
158 #endif
159 }
160
161
162 // Main update function - checks whether we are displaying or not the correct message.
163 void FGATIS::update(double dt) {
164   cur_time = globals->get_time_params()->get_cur_time();
165   msg_OK = (msg_time < cur_time);
166
167 #ifdef ATIS_TEST
168   if (msg_OK || _display != _prev_display) {
169     cout << "ATIS Update: " << _display << "  " << _prev_display
170       << "  len: " << transmission.length()
171       << "  oldvol: " << old_volume
172       << "  dt: " << dt << endl;
173     msg_time = cur_time;
174   }
175 #endif
176
177   double volume = 0;
178   if ((_electrical->getDoubleValue()>8) && _serviceable->getBoolValue())
179   {
180       _time_before_search_sec -= dt;
181       // radio is switched on and OK
182       if (_operable.valid())
183           _operable->setBoolValue(true);
184
185       // Search the tuned frequencies
186       search();
187
188       if (_display)
189       {
190           volume = _volume->getDoubleValue();
191       }
192   }
193   else
194   {
195       // radio is OFF
196       if (_operable.valid())
197           _operable->setBoolValue(false);
198       _time_before_search_sec = 0;
199   }
200
201   if (volume > 0.05)
202   {
203     // Check if we need to update the message
204     // - basically every hour and if the weather changes significantly at the station
205     // If !_prev_display, the radio had been detuned for a while and our
206     // "transmission" variable was lost when we were de-instantiated.
207     int changed = GenTransmission(!_prev_display, _attention);
208
209     // update output property
210     TreeOut(msg_OK);
211
212     if (changed || volume != old_volume) {
213       // audio output enabled
214       Render(transmission, volume, _name, true);
215       old_volume = volume;
216     }
217     _prev_display = _display;
218   } else {
219     // silence
220     NoRender(_name);
221     _prev_display = false;
222   }
223   _attention = false;
224 }
225
226 string uppercase(const string &s) {
227   string rslt(s);
228   for(string::iterator p = rslt.begin(); p != rslt.end(); p++){
229     *p = toupper(*p);
230   }
231   return rslt;
232 }
233
234 // Replace all occurrences of a given word.
235 // Words in the original string must be separated by hyphens (not spaces).
236 // We check for the word as given, and for the all-caps version thereof.
237 string replace_word(const string _orig, const string _www, const string _nnn){
238 // The following are so we can match words at the beginning
239 // and end of the string.
240   string orig = "-" + _orig + "-";
241   string www = "-" + _www + "-";
242   string nnn = "-" + _nnn + "-";
243
244   size_t where(0);
245   for ( ; (where = orig.find(www, where)) != string::npos ; ) {
246     orig.replace(where, www.length(), nnn);
247     where += nnn.length();
248   }
249   
250   www = uppercase(www);
251   for ( ; (where = orig.find(www, where)) != string::npos ; ) {
252     orig.replace(where, www.length(), nnn);
253     where += nnn.length();
254   }
255   where = orig.length();
256   return orig.substr(1, where-2);
257 }
258
259 // Normally the interval is 1 hour, 
260 // but you can shorten it for testing.
261 const int minute(60);           // measured in seconds
262 #ifdef ATIS_TEST
263   const int ATIS_interval(2*minute);
264 #else
265   const int ATIS_interval(60*minute);
266 #endif
267
268 // FIXME:  This is heuristic.  It gets the right answer for
269 // more than 90% of the world's airports, which is a lot
270 // better than nothing ... but it's not 100%.
271 // We know "most" of the world uses millibars,
272 // but the US, Canada and *some* other places use inches of mercury,
273 // but (a) we have not implemented a reliable method of
274 // ascertaining which airports are in the US, let alone
275 // (b) ascertaining which other places use inches.
276 //
277 int Apt_US_CA(const string id) {
278 // Assume all IDs have length 3 or 4.
279 // No counterexamples have been seen.
280   if (id.length() == 4) {
281     if (id.substr(0,1) == "K") return 1;
282     if (id.substr(0,2) == "CY") return 1;
283   }
284   for (string::const_iterator ptr = id.begin(); ptr != id.end();  ptr++) {
285     if (isdigit(*ptr)) return 1;
286   }
287   return 0;
288 }
289
290 // Generate the actual broadcast ATIS transmission.
291 // Regen means regenerate the /current/ transmission.
292 // Special means generate a new transmission, with a new sequence.
293 // Returns 1 if we actually generated something.
294 int FGATIS::GenTransmission(const int regen, const bool special) {
295   using namespace atmodel;
296   using namespace lex;
297
298   string BRK = ".\n";
299   string PAUSE = " / ";
300
301   int interval = _type == ATIS ?
302         ATIS_interval   // ATIS updated hourly
303       : 2*minute;       // AWOS updated more frequently
304
305   FGAirport* apt = FGAirport::findByIdent(ident);
306   int sequence = apt->getDynamics()->updateAtisSequence(interval, special);
307   if (!regen && sequence > LTRS) {
308 //xx      if (msg_OK) cout << "ATIS:  no change: " << sequence << endl;
309 //xx      msg_time = cur_time;
310     return 0;   // no change since last time
311   }
312
313   const int bs(100);
314   char buf[bs];
315   string time_str = fgGetString("sim/time/gmt-string");
316   string hours, mins;
317   string phonetic_seq_string;
318
319   transmission = "";
320
321   int US_CA = Apt_US_CA(ident);
322
323   if (!US_CA) {
324 // UK CAA radiotelephony manual indicates ATIS transmissions start
325 // with "This is ..." 
326     transmission += This_is + " ";
327   } else {
328     // In the US they just start with the airport name.
329   }
330
331   // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name);
332
333 // Note that at this point, multi-word facility names
334 // will sometimes contain hyphens, not spaces.
335   
336   vector<string> name_words;
337   boost::split(name_words, name, boost::is_any_of(" -"));
338
339   for (vector<string>::const_iterator wordp = name_words.begin();
340                 wordp != name_words.end(); wordp++) {
341     string word(*wordp);
342 // Remap some abbreviations that occur in apt.dat, to
343 // make things nicer for the text-to-speech system:
344     for (MSS::const_iterator replace = _remap.begin();
345           replace != _remap.end(); replace++) {
346       // Due to inconsistent capitalisation in the apt.dat file, we need
347       // to do a case-insensitive comparison here.
348       string tmp1 = word, tmp2 = replace->first;
349       boost::algorithm::to_lower(tmp1);
350       boost::algorithm::to_lower(tmp2);
351       if (tmp1 == tmp2) {
352         word = replace->second;
353         break;
354       }
355     }
356     transmission += word + " ";
357   }
358
359   if (_type == ATIS /* as opposed to AWOS */) {
360     transmission += airport_information + " ";
361   } else {
362     transmission += Automated_weather_observation + " ";
363   }
364
365   phonetic_seq_string = GetPhoneticLetter(sequence);  // Add the sequence letter
366   transmission += phonetic_seq_string + BRK;
367
368 // Warning - this is fragile if the time string format changes
369   hours = time_str.substr(0,2).c_str();
370   mins  = time_str.substr(3,2).c_str();
371 // speak each digit separately:
372   transmission += ConvertNumToSpokenDigits(hours + mins);
373   transmission += " " + zulu + " " + weather + BRK;
374
375   transmission += wind + ": ";
376
377   double wind_speed = fgGetDouble("/environment/config/boundary/entry[0]/wind-speed-kt");
378   double wind_dir = fgGetDouble("/environment/config/boundary/entry[0]/wind-from-heading-deg");
379   while (wind_dir <= 0) wind_dir += 360;
380 // The following isn't as bad a kludge as it might seem.
381 // It combines the magvar at the /aircraft/ location with
382 // the wind direction in the environment/config array.
383 // But if the aircraft is close enough to the station to
384 // be receiving the ATIS signal, this should be a good-enough
385 // approximation.  For more-distant aircraft, the wind_dir
386 // shouldn't be corrected anyway.
387 // The less-kludgy approach would be to use the magvar associated
388 // with the station, but that is not tabulated in the stationweather
389 // structure as it stands, and computing it would be expensive.
390 // Also note that as it stands, there is only one environment in
391 // the entire FG universe, so the aircraft environment is the same
392 // as the station environment anyway.
393   wind_dir -= fgGetDouble("/environment/magnetic-variation-deg");       // wind_dir now magnetic
394   if (wind_speed == 0) {
395 // Force west-facing rwys to be used in no-wind situations
396 // which is consistent with Flightgear's initial setup:
397       wind_dir = 270;
398       transmission += " " + light_and_variable;
399   } else {
400       // FIXME: get gust factor in somehow
401       snprintf(buf, bs, "%03.0f", 5*SGMiscd::round(wind_dir/5));
402       transmission += ConvertNumToSpokenDigits(buf);
403
404       snprintf(buf, bs, "%1.0f", wind_speed);
405       transmission += " " + at + " " + ConvertNumToSpokenDigits(buf) + BRK;
406   }
407
408 // Sounds better with a pause in there:
409   transmission += PAUSE;
410
411   int did_some(0);
412   int did_ceiling(0);
413
414   for (int layer = 0; layer <= 4; layer++) {
415     snprintf(buf, bs, "/environment/clouds/layer[%i]/coverage", layer);
416     string coverage = fgGetString(buf);
417     if (coverage == clear) continue;
418     snprintf(buf, bs, "/environment/clouds/layer[%i]/thickness-ft", layer);
419     if (fgGetDouble(buf) == 0) continue;
420     snprintf(buf, bs, "/environment/clouds/layer[%i]/elevation-ft", layer);
421     double ceiling = int(fgGetDouble(buf) - _geod.getElevationFt());
422     if (ceiling > 12000) continue;
423
424 // BEWARE:  At the present time, the environment system has no
425 // way (so far as I know) to represent a "thin broken" or
426 // "thin overcast" layer.  If/when such things are implemented
427 // in the environment system, code will have to be written here
428 // to handle them.
429
430 // First, do the prefix if any:
431     if (coverage == scattered || coverage == few) {
432       if (!did_some) {
433         transmission += "   " + Sky_condition + ": ";
434         did_some++;
435       }
436     } else /* must be a ceiling */  if (!did_ceiling) {
437       transmission += "   " + Ceiling + ": ";
438       did_ceiling++;
439       did_some++;
440     } else {
441       transmission += "   ";    // no prefix required
442     }
443     int cig00  = int(SGMiscd::round(ceiling/100));  // hundreds of feet
444     if (cig00) {
445       int cig000 = cig00/10;
446       cig00 -= cig000*10;       // just the hundreds digit
447       if (cig000) {
448         snprintf(buf, bs, "%i", cig000);
449         transmission += ConvertNumToSpokenDigits(buf);
450         transmission += " " + thousand + " ";
451       }
452       if (cig00) {
453         snprintf(buf, bs, "%i", cig00);
454         transmission += ConvertNumToSpokenDigits(buf);
455         transmission += " " + hundred + " ";
456       }
457     } else {
458       // Should this be "sky obscured?"
459       transmission += " " + zero + " ";     // not "zero hundred"
460     }
461     transmission += coverage + BRK;
462   }
463   if (!did_some) transmission += "   " + Sky + " " + clear + BRK;
464
465   transmission += Temperature + ": ";
466   double Tsl = fgGetDouble("/environment/temperature-sea-level-degc");
467   int temp = int(SGMiscd::round(FGAtmo().fake_T_vs_a_us(_geod.getElevationFt(), Tsl)));
468   if(temp < 0) {
469       transmission += lex::minus + " ";
470   }
471   snprintf(buf, bs, "%i", abs(temp));
472   transmission += ConvertNumToSpokenDigits(buf);
473   if (US_CA) transmission += " " + Celsius;
474   transmission += " " + dewpoint + " ";
475   double dpsl = fgGetDouble("/environment/dewpoint-sea-level-degc");
476   temp = int(SGMiscd::round(FGAtmo().fake_dp_vs_a_us(dpsl, _geod.getElevationFt())));
477   if(temp < 0) {
478       transmission += lex::minus + " ";
479   }
480   snprintf(buf, bs, "%i", abs(temp));
481   transmission += ConvertNumToSpokenDigits(buf);
482   if (US_CA) transmission += " " + Celsius;
483   transmission += BRK;
484
485   transmission += Visibility + ": ";
486   double visibility = fgGetDouble("/environment/config/boundary/entry[0]/visibility-m");
487   visibility /= atmodel::sm;    // convert to statute miles
488   if (visibility < 0.25) {
489     transmission += less_than_one_quarter;
490   } else if (visibility < 0.5) {
491     transmission += one_quarter;
492   } else if (visibility < 0.75) {
493     transmission += one_half;
494   } else if (visibility < 1.0) {
495     transmission += three_quarters;
496   } else if (visibility >= 1.5 && visibility < 2.0) {
497     transmission += one_and_one_half;
498   } else {
499     // integer miles
500     if (visibility > 10) visibility = 10;
501     sprintf(buf, "%i", int(.5 + visibility));
502     transmission += ConvertNumToSpokenDigits(buf);
503   }
504   transmission += BRK;
505
506   double myQNH;
507   double Psl = fgGetDouble("/environment/pressure-sea-level-inhg");
508   {
509     double press, temp;
510     
511     tie(press, temp) = PT_vs_hpt(_geod.getElevationM(), Psl*inHg, Tsl + freezing);
512 #if 0
513     SG_LOG(SG_ATC, SG_ALERT, "Field P: " << press << "  T: " << temp);
514     SG_LOG(SG_ATC, SG_ALERT, "based on elev " << elev 
515                                 << "  Psl: " << Psl
516                                 << "  Tsl: " << Tsl);
517 #endif
518     myQNH = FGAtmo().QNH(_geod.getElevationM(), press);
519   }
520
521 // Convert to millibars for most of the world (not US, not CA)
522   if((!US_CA) && fgGetBool("/sim/atc/use-millibars")) {
523     transmission += QNH + ": ";
524     myQNH /= mbar;
525     snprintf(buf, bs, "%03.0f", myQNH);
526     transmission += ConvertNumToSpokenDigits(buf);
527     // TODO Extend voice samples so we can replace "millibars" with "hectopascal" (new ATIS standard since 2011)
528     if  (myQNH < 1000)
529         transmission += " " + millibars; // "hectopascal" (millibars) spoken for values below 1000 only (to avoid confusion with inHg)
530     transmission += BRK;
531   } else {
532     transmission += Altimeter + ": ";
533     double asetting = myQNH / inHg;         // use inches of mercury
534     asetting *= 100.;                       // shift two decimal places
535     snprintf(buf, bs, "%04.0f", asetting);
536     transmission += ConvertNumToSpokenDigits(buf) + BRK;
537   }
538
539   if (_type == ATIS /* as opposed to AWOS */) {
540     const FGAirport* apt = fgFindAirportID(ident);
541     if (apt) {
542       string rwy_no = apt->getActiveRunwayForUsage()->ident();
543       if(rwy_no != "NN") {
544         transmission += Landing_and_departing_runway + " ";
545         transmission += ConvertRwyNumToSpokenString(rwy_no) + BRK;
546 #ifdef ATIS_TEST
547         if (msg_OK) {
548           msg_time = cur_time;
549           cout << "In atis.cxx, r.rwy_no: " << rwy_no
550              << " wind_dir: " << wind_dir << endl;
551         }
552 #endif
553       }
554     }
555     transmission += On_initial_contact_advise_you_have_information + " ";
556     transmission += phonetic_seq_string;
557     transmission += "... " + BRK;
558     // Pause in between two ATIS messages must be 3-5 seconds
559     transmission += PAUSE + PAUSE + PAUSE + PAUSE;
560   }
561   transmission_readable = transmission;
562 // Take the previous readable string and munge it to
563 // be relatively-more acceptable to the primitive tts system.
564 // Note that : ; and . are among the token-delimiters recognized
565 // by the tts system.
566   for (size_t where;;) {
567     where = transmission.find_first_of(":.");
568     if (where == string::npos) break;
569     transmission.replace(where, 1, PAUSE);
570   }
571   return 1;
572 }
573
574 // Put the transmission into the property tree.
575 // You can see it by pointing a web browser
576 // at the property tree.  The second comm radio is:
577 // http://localhost:5400/instrumentation/comm[1]
578 //
579 // (Also, if in debug mode, dump it to the console.)
580 void FGATIS::TreeOut(int msg_OK)
581 {
582     _atis->setStringValue("<pre>\n" + transmission_readable + "</pre>\n");
583     SG_LOG(SG_ATC, SG_DEBUG, "**** ATIS active on: " << _name <<
584            "transmission: " << transmission_readable);
585 }
586
587
588
589 class RangeFilter : public CommStation::Filter
590 {
591 public:
592     RangeFilter( const SGGeod & pos ) :
593       CommStation::Filter(),
594       _cart(SGVec3d::fromGeod(pos)),
595       _pos(pos)
596     {
597     }
598
599     virtual bool pass(FGPositioned* aPos) const
600     {
601         flightgear::CommStation * stn = dynamic_cast<flightgear::CommStation*>(aPos);
602         if( NULL == stn )
603             return false;
604
605         // do the range check in cartesian space, since the distances are potentially
606         // large enough that the geodetic functions become unstable
607         // (eg, station on opposite side of the planet)
608         double rangeM = SGMiscd::max( stn->rangeNm(), 10.0 ) * SG_NM_TO_METER;
609         double d2 = distSqr( aPos->cart(), _cart);
610
611         return d2 <= (rangeM * rangeM);
612     }
613
614     virtual CommStation::Type minType() const
615     {
616       return CommStation::FREQ_ATIS;
617     }
618
619     virtual CommStation::Type maxType() const
620     {
621       return CommStation::FREQ_AWOS;
622     }
623
624 private:
625     SGVec3d _cart;
626     SGGeod _pos;
627 };
628
629 // Search for ATC stations by frequency
630 void FGATIS::search(void)
631 {
632     double frequency = _freq->getDoubleValue();
633
634     // Note:  122.375 must be rounded DOWN to 122370
635     // in order to be consistent with apt.dat et cetera.
636     int freqKhz = 10 * static_cast<int>(frequency * 100 + 0.25);
637
638     // throttle frequency searches
639     if ((freqKhz == _last_frequency)&&(_time_before_search_sec > 0))
640         return;
641
642     _last_frequency = freqKhz;
643     _time_before_search_sec = 4.0;
644
645     // Position of the Users Aircraft
646     SGGeod aircraftPos = SGGeod::fromDegFt(_lon_node->getDoubleValue(),
647                                            _lat_node->getDoubleValue(),
648                                            _elev_node->getDoubleValue());
649
650     RangeFilter rangeFilter(aircraftPos );
651     CommStation* sta = CommStation::findByFreq(freqKhz, aircraftPos, &rangeFilter );
652     SetStation(sta);
653     if (sta && sta->airport())
654     {
655         SG_LOG(SG_ATC, SG_DEBUG, "FGATIS " << _name << ": " << sta->airport()->name());
656     }
657     else
658     {
659         SG_LOG(SG_ATC, SG_DEBUG, "FGATIS " << _name << ": no station.");
660     }
661 }