]> git.mxchange.org Git - flightgear.git/blob - src/ATCDCL/atis.cxx
VoiceSynthesizer: add some test/debug properties
[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 // Extended by Thorsten Brehm, October 2012.
6 //
7 // Copyright (C) 2001  David C Luff - david.luff@nottingham.ac.uk
8 //
9 // This program is free software; you can redistribute it and/or
10 // modify it under the terms of the GNU General Public License as
11 // published by the Free Software Foundation; either version 2 of the
12 // License, or (at your option) any later version.
13 //
14 // This program is distributed in the hope that it will be useful, but
15 // WITHOUT ANY WARRANTY; without even the implied warranty of
16 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17 // General Public License for more details.
18 //
19 // You should have received a copy of the GNU General Public License
20 // along with this program; if not, write to the Free Software
21 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
22
23 /////
24 ///// TODO:  _Cumulative_ sky coverage.
25 ///// TODO:  wind _gust_
26 ///// TODO:  more-sensible encoding of voice samples
27 /////       u-law?  outright synthesis?
28 /////
29
30 #ifdef HAVE_CONFIG_H
31 #  include <config.h>
32 #endif
33
34 #include "atis.hxx"
35 #include "atis_lexicon.hxx"
36
37 #include <simgear/compiler.h>
38 #include <simgear/math/sg_random.h>
39 #include <simgear/misc/sg_path.hxx>
40 #include <simgear/misc/strutils.hxx>
41
42 #include <stdlib.h> // atoi()
43 #include <stdio.h>  // sprintf
44 #include <string>
45 #include <iostream>
46
47 #include <boost/tuple/tuple.hpp>
48 #include <boost/algorithm/string.hpp>
49 #include <boost/algorithm/string/case_conv.hpp>
50
51 #include <Environment/environment_mgr.hxx>
52 #include <Environment/environment.hxx>
53 #include <Environment/atmosphere.hxx>
54
55 #include <Main/fg_props.hxx>
56 #include <Main/globals.hxx>
57 #include <Airports/runways.hxx>
58 #include <Airports/dynamics.hxx>
59
60 #include <ATC/CommStation.hxx>
61 #include <Navaids/navrecord.hxx>
62
63 #include "ATCutils.hxx"
64 #include "ATISmgr.hxx"
65
66 using std::string;
67 using std::map;
68 using std::cout;
69 using std::cout;
70 using boost::ref;
71 using boost::tie;
72 using flightgear::CommStation;
73
74 FGATIS::FGATIS(const std::string& name, int num) :
75   _name(name),
76   _num(num),
77   _cb_attention(this, &FGATIS::attend, fgGetNode("/environment/attention", true)),
78   transmission(""),
79   trans_ident(""),
80   old_volume(0),
81   atis_failed(false),
82   msg_time(0),
83   cur_time(0),
84   msg_OK(0),
85   _attention(false),
86   _check_transmission(true),
87   _prev_display(0),
88   _time_before_search_sec(0),
89   _last_frequency(0)
90 {
91   _root         = fgGetNode("/instrumentation", true)->getNode(_name, num, true);
92   _volume       = _root->getNode("volume",true);
93   _serviceable  = _root->getNode("serviceable",true);
94
95   if (name != "nav")
96   {
97       // only drive "operable" for non-nav instruments (nav radio drives this separately)
98       _operable = _root->getNode("operable",true);
99       _operable->setBoolValue(false);
100   }
101
102   _electrical   = fgGetNode("/systems/electrical/outputs",true)->getNode(_name,num, true);
103   _atis         = _root->getNode("atis",true);
104   _freq         = _root->getNode("frequencies/selected-mhz",true);
105
106   // current position
107   _lon_node  = fgGetNode("/position/longitude-deg", true);
108   _lat_node  = fgGetNode("/position/latitude-deg",  true);
109   _elev_node = fgGetNode("/position/altitude-ft",   true);
110
111   // backward compatibility: some properties may not exist (but default to "ON")
112   if (!_serviceable->hasValue())
113       _serviceable->setBoolValue(true);
114   if (!_electrical->hasValue())
115       _electrical->setDoubleValue(24.0);
116
117 ///////////////
118 // FIXME:  This would be more flexible and more extensible
119 // if the mappings were taken from an XML file, not hard-coded ...
120 // ... although having it in a .hxx file is better than nothing.
121 //
122 // Load the remap list from the .hxx file:
123   using namespace lex;
124
125   # define NIL ""
126   # define REMAP(from,to) _remap[#from] = to;
127   # include "atis_remap.hxx"
128   # undef REMAP
129   # undef NIL
130
131   #ifdef ATIS_TEST
132     SG_LOG(SG_ATC, SG_ALERT, "ATIS initialized");
133   #endif
134
135     _report.psl = 0;
136 }
137
138 // Hint:
139 // http://localhost:5400/environment/attention?value=1&submit=update
140
141 FGATCVoice* FGATIS::GetVoicePointer()
142 {
143     FGATISMgr* pAtisMgr = globals->get_ATIS_mgr();
144     if (!pAtisMgr)
145     {
146         SG_LOG(SG_ATC, SG_ALERT, "ERROR! No ATIS manager! Oops...");
147         return NULL;
148     }
149
150     return pAtisMgr->GetVoicePointer(ATIS);
151 }
152
153 void FGATIS::init()
154 {
155 // Nothing to see here.  Move along.
156 }
157
158 void FGATIS::reinit()
159 {
160     _time_before_search_sec = 0;
161     _check_transmission = true;
162 }
163
164 void
165 FGATIS::attend(SGPropertyNode* node)
166 {
167   if (node->getBoolValue())
168       _attention = true;
169 #ifdef ATMO_TEST
170   int flag = fgGetInt("/sim/logging/atmo");
171   if (flag) {
172     FGAltimeter().check_model();
173     FGAltimeter().dump_stack();
174   }
175 #endif
176 }
177
178
179 // Main update function - checks whether we are displaying or not the correct message.
180 void FGATIS::update(double dt) {
181   cur_time = globals->get_time_params()->get_cur_time();
182   msg_OK = (msg_time < cur_time);
183
184 #ifdef ATIS_TEST
185   if (msg_OK || _display != _prev_display) {
186     cout << "ATIS Update: " << _display << "  " << _prev_display
187       << "  len: " << transmission.length()
188       << "  oldvol: " << old_volume
189       << "  dt: " << dt << endl;
190     msg_time = cur_time;
191   }
192 #endif
193
194   double volume = 0;
195   if ((_electrical->getDoubleValue() > 8) && _serviceable->getBoolValue())
196   {
197       // radio is switched on and OK
198       if (_operable.valid())
199           _operable->setBoolValue(true);
200
201       _check_transmission |= search(dt);
202
203       if (_display)
204       {
205           volume = _volume->getDoubleValue();
206       }
207   }
208   else
209   {
210       // radio is OFF
211       if (_operable.valid())
212           _operable->setBoolValue(false);
213       _time_before_search_sec = 0;
214   }
215
216   if (volume > 0.05)
217   {
218     bool changed = false;
219     if (_check_transmission)
220     {
221         _check_transmission = false;
222         // Check if we need to update the message
223         // - basically every hour and if the weather changes significantly at the station
224         // If !_prev_display, the radio had been detuned for a while and our
225         // "transmission" variable was lost when we were de-instantiated.
226         if (genTransmission(!_prev_display, _attention))
227         {
228             // update output property
229             treeOut(msg_OK);
230             changed = true;
231         }
232     }
233
234     if (changed || volume != old_volume) {
235       // audio output enabled
236       Render(transmission, volume, _name, true);
237       old_volume = volume;
238     }
239     _prev_display = _display;
240   } else {
241     // silence
242     NoRender(_name);
243     _prev_display = false;
244   }
245   _attention = false;
246
247   FGATC::update(dt);
248 }
249
250 // Replace all occurrences of a given word.
251 // Words in the original string must be separated by hyphens (not spaces).
252 // We check for the word as given, and for the all-caps version thereof.
253 string replace_word(const string _orig, const string _www, const string _nnn){
254 // The following are so we can match words at the beginning
255 // and end of the string.
256   string orig = "-" + _orig + "-";
257   string www = "-" + _www + "-";
258   string nnn = "-" + _nnn + "-";
259
260   size_t where(0);
261   for ( ; (where = orig.find(www, where)) != string::npos ; ) {
262     orig.replace(where, www.length(), nnn);
263     where += nnn.length();
264   }
265   
266   www = simgear::strutils::uppercase(www);
267   for ( ; (where = orig.find(www, where)) != string::npos ; ) {
268     orig.replace(where, www.length(), nnn);
269     where += nnn.length();
270   }
271   where = orig.length();
272   return orig.substr(1, where-2);
273 }
274
275 // Normally the interval is 1 hour, 
276 // but you can shorten it for testing.
277 const int minute(60);           // measured in seconds
278 #ifdef ATIS_TEST
279   const int ATIS_interval(2*minute);
280 #else
281   const int ATIS_interval(60*minute);
282 #endif
283
284 // FIXME:  This is heuristic.  It gets the right answer for
285 // more than 90% of the world's airports, which is a lot
286 // better than nothing ... but it's not 100%.
287 // We know "most" of the world uses millibars,
288 // but the US, Canada and *some* other places use inches of mercury,
289 // but (a) we have not implemented a reliable method of
290 // ascertaining which airports are in the US, let alone
291 // (b) ascertaining which other places use inches.
292 //
293 bool Apt_US_CA(const string id)
294 {
295     // Assume all IDs have length 3 or 4.
296     // No counterexamples have been seen.
297     if (id.length() == 4) {
298         if (id.substr(0,1) == "K") return true;
299         if (id.substr(0,2) == "CY") return true;
300     }
301     for (string::const_iterator ptr = id.begin(); ptr != id.end();  ptr++) {
302         if (isdigit(*ptr)) return true;
303     }
304     return false;
305 }
306
307 // voice spacers
308 static const string BRK = ".\n";
309 static const string PAUSE = " / ";
310
311 /** Generate the actual broadcast ATIS transmission.
312 *   'regen' triggers a regeneration of the /current/ transmission.
313 *   'forceUpdate' generates a new transmission, with a new sequence.
314 *   Returns 1 if we actually generated something.
315 */
316 bool FGATIS::genTransmission(const int regen, bool forceUpdate)
317 {
318     using namespace lex;
319
320     // ATIS updated hourly, AWOS updated more frequently
321     int interval = _type == ATIS ? ATIS_interval : 2*minute;
322
323     // check if pressure has changed significantly and we need to update ATIS
324     double Psl = fgGetDouble("/environment/pressure-sea-level-inhg");
325     if (fabs(Psl-_report.psl) >= 0.15)
326         forceUpdate = true;
327
328     FGAirport* apt = FGAirport::findByIdent(ident);
329     int sequence = apt->getDynamics()->updateAtisSequence(interval, forceUpdate);
330     if (!regen && sequence > LTRS) {
331         //xx      if (msg_OK) cout << "ATIS:  no change: " << sequence << endl;
332         //xx    msg_time = cur_time;
333         return false;   // no change since last time
334     }
335
336     _report.psl = Psl;
337     transmission = "";
338
339     // collect data and create report
340     createReport(apt);
341
342     // add facility name
343     genFacilityInfo();
344
345     if (_type == ATIS) {
346         // ATIS phraseology starts with "... airport information"
347         transmission += airport_information + " ";
348     } else {
349         // AWOS
350         transmission += Automated_weather_observation + " ";
351     }
352
353     string phonetic_seq_string = GetPhoneticLetter(sequence);  // Add the sequence letter
354     transmission += phonetic_seq_string + BRK;
355
356     genTimeInfo();
357
358     // some warnings may appear at the beginning
359     genWarnings(-1);
360
361     if (_type == ATIS) // as opposed to AWOS
362         genRunwayInfo(apt);
363
364     // some warnings may appear after runway info
365     genWarnings(0);
366
367     // transition level
368     genTransitionLevel(apt);
369
370     // weather
371     if (!_report.concise)
372         transmission += Weather + BRK;
373
374     genWindInfo();
375
376     // clouds and visibility
377     {
378         string vis_info, cloud_info;
379         bool v = genVisibilityInfo(vis_info);
380         bool c = genCloudInfo(cloud_info);
381         _report.cavok = !(v || c);
382         if (!_report.cavok)
383         {
384             // there is some visibility or cloud restriction
385             transmission += vis_info + cloud_info;
386         }
387         else
388         {
389             // Abbreviation CAVOK vs full "clouds and visibility..." does not really depend on
390             // US vs rest of the world, it really seems to depend on the airport. Just use
391             // it as a heuristic.
392             if ((_report.US_CA)||(_report.concise))
393                 transmission += cav_ok + BRK;
394             else
395                 transmission += clouds_and_visibility_OK + BRK;
396         }
397     }
398
399     // precipitation
400     genPrecipitationInfo();
401
402     // temperature
403     genTemperatureInfo();
404
405     // pressure
406     genPressureInfo();
407
408     // TODO check whether "no significant change" applies - somehow...
409     transmission += No_sig + BRK; // sounds better with festival than "nosig"
410
411     // some warnings may appear at the very end
412     genWarnings(1);
413
414     if ((!_report.concise)|| _report.US_CA)
415         transmission += Advise_on_initial_contact_you_have_information;
416     else
417         transmission += information;
418     transmission += " " + phonetic_seq_string + ".";
419
420     if (!_report.US_CA)
421     {
422         // non-US ATIS ends with "out!"
423         transmission += " " + out;
424     }
425
426     // Pause in between two messages must be 3-5 seconds
427     transmission += " / / / / / / / / ";
428
429     /////////////////////////////////////////////////////////
430     // post-processing
431     /////////////////////////////////////////////////////////
432     transmission_readable = transmission;
433
434     // Take the previous readable string and munge it to
435     // be relatively-more acceptable to the primitive tts system.
436     // Note that : ; and . are among the token-delimiters recognized
437     // by the tts system.
438     for (size_t where;;) {
439         where = transmission.find_first_of(":.");
440         if (where == string::npos) break;
441         transmission.replace(where, 1, PAUSE);
442     }
443
444     return true;
445 }
446
447 /** Collect (most of) the data and create report.
448  */
449 void FGATIS::createReport(const FGAirport* apt)
450 {
451     // check country
452     _report.US_CA = Apt_US_CA(ident);
453
454     // switch to enable brief ATIS message (really depends on the airport)
455     _report.concise = fgGetBool("/sim/atis/concise-reports", false);
456
457     _report.ils = false;
458
459     // time information
460     string time_str = fgGetString("sim/time/gmt-string");
461     // Warning - this is fragile if the time string format changes
462     _report.hours = time_str.substr(0,2).c_str();
463     _report.mins  = time_str.substr(3,2).c_str();
464
465     // pressure/temperature
466     {
467         double press, temp;
468         double Tsl = fgGetDouble("/environment/temperature-sea-level-degc");
469         tie(press, temp) = PT_vs_hpt(_geod.getElevationM(), _report.psl*atmodel::inHg, Tsl + atmodel::freezing);
470   #if 0
471         SG_LOG(SG_ATC, SG_ALERT, "Field P: " << press << "  T: " << temp);
472         SG_LOG(SG_ATC, SG_ALERT, "based on elev " << elev
473                                   << "  Psl: " << Psl
474                                   << "  Tsl: " << Tsl);
475   #endif
476         _report.qnh = FGAtmo().QNH(_geod.getElevationM(), press);
477         _report.temp = int(SGMiscd::round(FGAtmo().fake_T_vs_a_us(_geod.getElevationFt(), Tsl)));
478     }
479
480     // dew point
481     double dpsl = fgGetDouble("/environment/dewpoint-sea-level-degc");
482     _report.dewpoint = int(SGMiscd::round(FGAtmo().fake_dp_vs_a_us(dpsl, _geod.getElevationFt())));
483
484     // precipitation
485     _report.rain_norm = fgGetDouble("environment/rain-norm");
486     _report.snow_norm = fgGetDouble("environment/snow-norm");
487
488     // NOTAMs
489     _report.notam = 0;
490     if (fgGetBool("/sim/atis/random-notams", true))
491     {
492         _report.notam = fgGetInt("/sim/atis/notam-id", 0); // fixed NOTAM for testing/debugging only
493         if (!_report.notam)
494         {
495             // select pseudo-random NOTAM (changes every hour, differs for each airport)
496             char cksum = 0;
497             string name = apt->getName();
498             for(string::iterator p = name.begin(); p != name.end(); p++)
499             {
500                 cksum += *p;
501             }
502             cksum ^= atoi(_report.hours.c_str());
503             _report.notam = cksum % 12; // 12 intentionally higher than number of available NOTAMs, so they don't appear too often
504             // debugging
505             //fgSetInt("/sim/atis/selected-notam", _report.notam);
506         }
507     }
508 }
509
510 void FGATIS::genPrecipitationInfo(void)
511 {
512     using namespace lex;
513
514     double rain_norm = _report.rain_norm;
515     double snow_norm = _report.snow_norm;
516
517     // report rain or snow - which ever is worse
518     if (rain_norm > 0.7)
519         transmission += heavy    + " " + rain + BRK;
520     else
521     if (snow_norm > 0.7)
522         transmission += heavy    + " " + snow + BRK;
523     else
524     if (rain_norm > 0.4)
525         transmission += moderate + " " + rain + BRK;
526     else
527     if (snow_norm > 0.4)
528         transmission += moderate + " " + snow + BRK;
529     else
530     if (rain_norm > 0.2)
531         transmission += light    + " " + rain + BRK;
532     else
533     if (snow_norm > 0.05)
534         transmission += light    + " " + snow + BRK;
535     else
536     if (rain_norm > 0.05)
537         transmission += light    + " " + drizzle + BRK;
538 }
539
540 void FGATIS::genTimeInfo(void)
541 {
542     using namespace lex;
543
544     if (!_report.concise)
545         transmission += Time + " ";
546
547     // speak each digit separately:
548     transmission += ConvertNumToSpokenDigits(_report.hours + _report.mins);
549     transmission += " " + zulu + BRK;
550 }
551
552 bool FGATIS::genVisibilityInfo(string& vis_info)
553 {
554     using namespace lex;
555
556     double visibility = fgGetDouble("/environment/config/boundary/entry[0]/visibility-m");
557     bool IsMax = false;
558     bool USE_KM = !_report.US_CA;
559
560     vis_info += Visibility + ": ";
561     if (USE_KM)
562     {
563         visibility /= 1000.0;    // convert to statute miles
564         // integer kilometers
565         if (visibility >= 9.5)
566         {
567             visibility = 10;
568             IsMax = true;
569         }
570         snprintf(buf, sizeof(buf), "%i", int(.5 + visibility));
571         // "kelometers" instead of "kilometers" since the festival language generator doesn't get it right otherwise
572         vis_info += ConvertNumToSpokenDigits(buf) + " " + kelometers;
573     }
574     else
575     {
576         visibility /= atmodel::sm;    // convert to statute miles
577         if (visibility < 0.25) {
578           vis_info += less_than_one_quarter;
579         } else if (visibility < 0.5) {
580           vis_info += one_quarter;
581         } else if (visibility < 0.75) {
582           vis_info += one_half;
583         } else if (visibility < 1.0) {
584           vis_info += three_quarters;
585         } else if (visibility >= 1.5 && visibility < 2.0) {
586           vis_info += one_and_one_half;
587         } else {
588           // integer miles
589           if (visibility > 9.5)
590           {
591               visibility = 10;
592               IsMax = true;
593           }
594           snprintf(buf, sizeof(buf), "%i", int(.5 + visibility));
595           vis_info += ConvertNumToSpokenDigits(buf);
596         }
597     }
598     if (IsMax)
599     {
600         vis_info += " " + or_more;
601     }
602     vis_info += BRK;
603     return !IsMax;
604 }
605
606 void FGATIS::addTemperature(int Temp)
607 {
608     if (Temp < 0)
609         transmission += lex::minus + " ";
610     else
611     if (Temp > 0)
612     {
613         transmission += lex::plus + " ";
614     }
615     snprintf(buf, sizeof(buf), "%i", abs(Temp));
616     transmission += ConvertNumToSpokenDigits(buf);
617     if (_report.US_CA)
618         transmission += " " + lex::Celsius;
619 }
620
621 void FGATIS::genTemperatureInfo()
622 {
623     // temperature
624     transmission += lex::Temperature + ": ";
625     addTemperature(_report.temp);
626
627     // dewpoint
628     transmission += BRK + lex::Dewpoint + ": ";
629     addTemperature(_report.dewpoint);
630
631     transmission += BRK;
632 }
633
634 bool FGATIS::genCloudInfo(string& cloud_info)
635 {
636     using namespace lex;
637
638     bool did_some = false;
639     bool did_ceiling = false;
640
641     for (int layer = 0; layer <= 4; layer++) {
642       snprintf(buf, sizeof(buf), "/environment/clouds/layer[%i]/coverage", layer);
643       string coverage = fgGetString(buf);
644       if (coverage == clear)
645           continue;
646       snprintf(buf, sizeof(buf), "/environment/clouds/layer[%i]/thickness-ft", layer);
647       if (fgGetDouble(buf) == 0)
648           continue;
649       snprintf(buf, sizeof(buf), "/environment/clouds/layer[%i]/elevation-ft", layer);
650       double ceiling = int(fgGetDouble(buf) - _geod.getElevationFt());
651       if (ceiling > 12000)
652           continue;
653
654   // BEWARE:  At the present time, the environment system has no
655   // way (so far as I know) to represent a "thin broken" or
656   // "thin overcast" layer.  If/when such things are implemented
657   // in the environment system, code will have to be written here
658   // to handle them.
659
660       // First, do the prefix if any:
661       if (coverage == scattered || coverage == few) {
662         if (!did_some) {
663           if (_report.concise)
664               cloud_info += Clouds + ": ";
665           else
666               cloud_info += Sky_condition + ": ";
667           did_some = true;
668         }
669       } else /* must be a ceiling */  if (!did_ceiling) {
670         cloud_info += "   " + Ceiling + ": ";
671         did_ceiling = true;
672         did_some = true;
673       } else {
674         cloud_info += "   ";    // no prefix required
675       }
676       int cig00  = int(SGMiscd::round(ceiling/100));  // hundreds of feet
677       if (cig00) {
678         int cig000 = cig00/10;
679         cig00 -= cig000*10;       // just the hundreds digit
680         if (cig000) {
681           snprintf(buf, sizeof(buf), "%i", cig000);
682           cloud_info += ConvertNumToSpokenDigits(buf);
683           cloud_info += " " + thousand + " ";
684         }
685         if (cig00) {
686           snprintf(buf, sizeof(buf), "%i", cig00);
687           cloud_info += ConvertNumToSpokenDigits(buf);
688           cloud_info += " " + hundred + " ";
689         }
690       } else {
691         // Should this be "sky obscured?"
692         cloud_info += " " + zero + " ";     // not "zero hundred"
693       }
694       cloud_info += coverage + BRK;
695     }
696     if (!did_some)
697         cloud_info += "   " + Sky + " " + clear + BRK;
698     return did_some;
699 }
700
701 void FGATIS::genFacilityInfo(void)
702 {
703     if ((!_report.US_CA)&&(!_report.concise))
704     {
705         // UK CAA radiotelephony manual indicates ATIS transmissions start
706         // with "This is ...", while US just starts with airport name.
707         transmission += lex::This_is + " ";
708     }
709
710     // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name);
711
712     // Note that at this point, multi-word facility names
713     // will sometimes contain hyphens, not spaces.
714     std::vector<std::string> name_words;
715     boost::split(name_words, name, boost::is_any_of(" -"));
716
717     for( std::vector<string>::const_iterator wordp = name_words.begin();
718                   wordp != name_words.end(); wordp++) {
719       string word(*wordp);
720     // Remap some abbreviations that occur in apt.dat, to
721     // make things nicer for the text-to-speech system:
722       for (MSS::const_iterator replace = _remap.begin();
723             replace != _remap.end(); replace++) {
724         // Due to inconsistent capitalisation in the apt.dat file, we need
725         // to do a case-insensitive comparison here.
726         string tmp1 = word, tmp2 = replace->first;
727         boost::algorithm::to_lower(tmp1);
728         boost::algorithm::to_lower(tmp2);
729         if (tmp1 == tmp2) {
730           word = replace->second;
731           break;
732         }
733       }
734       transmission += word + " ";
735     }
736 }
737
738 void FGATIS::genWindInfo(void)
739 {
740     using namespace lex;
741
742     transmission += Wind + ": ";
743
744     double wind_speed = fgGetDouble("/environment/config/boundary/entry[0]/wind-speed-kt");
745     double wind_dir = fgGetDouble("/environment/config/boundary/entry[0]/wind-from-heading-deg");
746     while (wind_dir <= 0) wind_dir += 360;
747     // The following isn't as bad a kludge as it might seem.
748     // It combines the magvar at the /aircraft/ location with
749     // the wind direction in the environment/config array.
750     // But if the aircraft is close enough to the station to
751     // be receiving the ATIS signal, this should be a good-enough
752     // approximation.  For more-distant aircraft, the wind_dir
753     // shouldn't be corrected anyway.
754     // The less-kludgy approach would be to use the magvar associated
755     // with the station, but that is not tabulated in the stationweather
756     // structure as it stands, and computing it would be expensive.
757     // Also note that as it stands, there is only one environment in
758     // the entire FG universe, so the aircraft environment is the same
759     // as the station environment anyway.
760     wind_dir -= fgGetDouble("/environment/magnetic-variation-deg");       // wind_dir now magnetic
761     if (wind_speed == 0) {
762         // Force west-facing rwys to be used in no-wind situations
763         // which is consistent with Flightgear's initial setup:
764         wind_dir = 270;
765         transmission += " " + light_and_variable;
766     } else {
767         // FIXME: get gust factor in somehow
768         snprintf(buf, sizeof(buf), "%03.0f", 5*SGMiscd::round(wind_dir/5));
769         transmission += ConvertNumToSpokenDigits(buf);
770         if (!_report.concise)
771             transmission += " " + degrees;
772         transmission += " ";
773         snprintf(buf, sizeof(buf), "%1.0f", wind_speed);
774         transmission += at + " " + ConvertNumToSpokenDigits(buf);
775         if (!_report.concise)
776             transmission += " " + knots;
777     }
778     transmission += BRK;
779 }
780
781 void FGATIS::genTransitionLevel(const FGAirport* apt)
782 {
783     double hPa = _report.qnh/atmodel::mbar;
784
785     /* Transition level is the flight level above which aircraft must use standard pressure and below
786      * which airport pressure settings must be used.
787      * Following definitions are taken from German ATIS:
788      *      QNH <=  977 hPa: TRL 80
789      *      QNH <= 1013 hPa: TRL 70
790      *      QNH >  1013 hPa: TRL 60
791      * (maybe differs slightly for other countries...)
792      */
793     int tl = 60;
794     if (hPa <= 977)
795         tl = 80;
796     else
797     if (hPa <= 1013)
798         tl = 70;
799
800     // add an offset to the transition level for high altitude airports (just guessing here,
801     // seems reasonable)
802     double elevationFt = apt->getElevation();
803     int e = int(elevationFt / 1000.0);
804     if (e >= 3)
805     {
806         // TL steps in 10(00)ft
807         tl += (e-2)*10;
808     }
809
810     snprintf(buf, sizeof(buf), "%02i", tl);
811     transmission += lex::Transition_level + ": " + ConvertNumToSpokenDigits(buf) + BRK;
812 }
813
814 void FGATIS::genPressureInfo(void)
815 {
816     using namespace lex;
817
818     // hectopascal for most of the world (not US, not CA)
819     if(!_report.US_CA) {
820         double hPa = _report.qnh/atmodel::mbar;
821         transmission += QNH + ": ";
822         snprintf(buf, sizeof(buf), "%03.0f", _report.qnh / atmodel::mbar);
823         transmission += ConvertNumToSpokenDigits(buf);
824         // "hectopascal" replaced "millibars" in new ATIS standard since 2011
825         if  ((!_report.concise)||(hPa < 1000))
826             transmission += " " + hectopascal; // "hectopascal" must be provided for values below 1000 (to avoid confusion with inHg)
827
828         // Many (European) airports (with lots of US traffic?) provide both, hPa and inHg announcements.
829         // Europeans keep the "decimal" in inHg readings to make the distinction to hPa even more apparent.
830         // hPa/inHg separated by "equals" or "or" with some airports
831         if (_report.concise)
832             transmission += " " + equals + " ";
833         else
834             transmission += " " + Or + " ";
835         snprintf(buf, sizeof(buf), "%04.2f", _report.qnh / atmodel::inHg);
836         transmission += ConvertNumToSpokenDigits(buf);
837         if (!_report.concise)
838             transmission += " " + inches;
839         transmission += BRK;
840     } else {
841         // use inches of mercury for US/CA
842         transmission += Altimeter + ": ";
843         double asetting = _report.qnh / atmodel::inHg;
844         // shift two decimal places, US/CA airports omit the "decimal" in inHg settings
845         asetting *= 100.;
846         snprintf(buf, sizeof(buf), "%04.0f", asetting);
847         transmission += ConvertNumToSpokenDigits(buf);
848     }
849
850     transmission += BRK;
851 }
852
853 void FGATIS::genRunwayInfo(const FGAirport* apt)
854 {
855     using namespace lex;
856
857     if (!apt)
858         return;
859
860     FGRunway* rwy = apt->getActiveRunwayForUsage();
861     if (!rwy)
862         return;
863
864     string rwy_no = rwy->ident();
865     if(rwy_no != "NN")
866     {
867         FGNavRecord* ils = rwy->ILS();
868         if (ils)
869         {
870             _report.ils = true;
871             transmission += Expect_I_L_S_approach + " "+ runway + " "+ConvertRwyNumToSpokenString(rwy_no) + BRK;
872             if (fgGetBool("/sim/atis/announce-ils-frequency", false))
873             {
874                 // this is handy - but really non-standard (so disabled by default)
875                 snprintf(buf, sizeof(buf), "%5.2f", ils->get_freq()/100.0);
876                 transmission += I_L_S + " " + ConvertNumToSpokenDigits(buf) + BRK;
877             }
878         }
879         else
880         {
881             transmission += Expect_visual_approach + " "+ runway + " "+ConvertRwyNumToSpokenString(rwy_no) + BRK;
882         }
883
884         transmission += Landing_and_departing_runway + " ";
885         transmission += ConvertRwyNumToSpokenString(rwy_no) + BRK;
886     #ifdef ATIS_TEST
887         if (msg_OK) {
888           msg_time = cur_time;
889           cout << "In atis.cxx, r.rwy_no: " << rwy_no
890              << " wind_dir: " << wind_dir << endl;
891         }
892     #endif
893     }
894 }
895
896 void FGATIS::genWarnings(int position)
897 {
898     using namespace lex;
899     bool dayTime = (fgGetDouble("/sim/time/sun-angle-rad") < 1.57);
900
901     if (position == -1) // warnings at beginning of ATIS
902     {
903         // bird related warnings at day-time only (birds are VFR-only! ;-) )
904         if (dayTime)
905         {
906             if (_report.notam == 1)
907                 transmission += Attention + ": " + flock_of_birds + " " + in_the_vicinity_of_the_airport + BRK;
908             else
909             if (_report.notam == 2)
910                 transmission += Attention + ": " + bird_activity + " " + in_the_vicinity_of_the_airport + BRK;
911         }
912     }
913     else
914     if (position == 0)  // warnings after runway messages
915     {
916         if ((_report.notam == 3)&&(_report.ils))
917         {
918             // "__I_LS_" necessary to trick the language generator into pronouncing it properly
919             transmission += Attention + ": " + short_time__I_LS_interference_possible_by_taxiing_aircraft + BRK;
920         }
921     }
922     else
923     if (position == 1) // warnings at the end of the report
924     {
925         // "runway wet-wet-wet" warning in heavy rain
926         if (_report.rain_norm > 0.6)
927         {
928             // "wet" is repeated 3 times in ATIS warnings, since the word is difficult
929             // to understand over radio - but the message is important.
930             transmission += runway_wet + " " + wet + " " + wet + BRK;
931         }
932
933         if (_report.notam == 4)
934         {
935             // intentional: "reed" instead of "read" since festival gets it wrong otherwise
936             transmission += reed_back_all_runway_hold_instructions + BRK;
937         }
938         else
939         if ((_report.notam == 5)&& _report.cavok && dayTime &&
940             (_report.rain_norm == 0) && (_report.snow_norm == 0)) // ;-)
941         {
942             transmission += Attention + ": " + glider_operation_in_sector + BRK;
943         }
944     }
945 }
946
947 // Put the transmission into the property tree.
948 // You can see it by pointing a web browser
949 // at the property tree.  The second comm radio is:
950 // http://localhost:5400/instrumentation/comm[1]
951 //
952 // (Also, if in debug mode, dump it to the console.)
953 void FGATIS::treeOut(int msg_OK)
954 {
955     _atis->setStringValue("<pre>\n" + transmission_readable + "</pre>\n");
956     SG_LOG(SG_ATC, SG_DEBUG, "**** ATIS active on: " << _name <<
957            "transmission: " << transmission_readable);
958 }
959
960
961 class RangeFilter : public CommStation::Filter
962 {
963 public:
964     RangeFilter( const SGGeod & pos ) :
965       CommStation::Filter(),
966       _cart(SGVec3d::fromGeod(pos)),
967       _pos(pos)
968     {
969     }
970
971     virtual bool pass(FGPositioned* aPos) const
972     {
973         flightgear::CommStation * stn = dynamic_cast<flightgear::CommStation*>(aPos);
974         if( NULL == stn )
975             return false;
976
977         // do the range check in cartesian space, since the distances are potentially
978         // large enough that the geodetic functions become unstable
979         // (eg, station on opposite side of the planet)
980         double rangeM = SGMiscd::max( stn->rangeNm(), 10.0 ) * SG_NM_TO_METER;
981         double d2 = distSqr( aPos->cart(), _cart);
982
983         return d2 <= (rangeM * rangeM);
984     }
985
986     virtual CommStation::Type minType() const
987     {
988       return CommStation::FREQ_ATIS;
989     }
990
991     virtual CommStation::Type maxType() const
992     {
993       return CommStation::FREQ_AWOS;
994     }
995
996 private:
997     SGVec3d _cart;
998     SGGeod _pos;
999 };
1000
1001 // Search for ATC stations by frequency
1002 bool FGATIS::search(double dt)
1003 {
1004     double frequency = _freq->getDoubleValue();
1005
1006     // Note:  122.375 must be rounded DOWN to 122370
1007     // in order to be consistent with apt.dat et cetera.
1008     int freqKhz = 10 * static_cast<int>(frequency * 100 + 0.25);
1009
1010     // only search tuned frequencies when necessary
1011     _time_before_search_sec -= dt;
1012
1013     // throttle frequency searches
1014     if ((freqKhz == _last_frequency)&&(_time_before_search_sec > 0))
1015         return false;
1016
1017     _last_frequency = freqKhz;
1018     _time_before_search_sec = 4.0;
1019
1020     // Position of the Users Aircraft
1021     SGGeod aircraftPos = SGGeod::fromDegFt(_lon_node->getDoubleValue(),
1022                                            _lat_node->getDoubleValue(),
1023                                            _elev_node->getDoubleValue());
1024
1025     RangeFilter rangeFilter(aircraftPos );
1026     CommStation* sta = CommStation::findByFreq(freqKhz, aircraftPos, &rangeFilter );
1027     SetStation(sta);
1028
1029     return true;
1030 }