]> git.mxchange.org Git - flightgear.git/blob - src/ATCDCL/atis.cxx
Remove hard-coded values wherever possible;
[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
59 #include "ATCutils.hxx"
60 #include "ATCmgr.hxx"
61
62 using std::string;
63 using std::map;
64 using std::cout;
65 using std::cout;
66 using boost::ref;
67 using boost::tie;
68
69 FGATIS::FGATIS() :
70   transmission(""),
71   trans_ident(""),
72   old_volume(0),
73   atis_failed(false),
74   msg_OK(0),
75   attention(0),
76   _prev_display(0),
77   refname("atis")
78 {
79   FGATCMgr* pAtcMgr = globals->get_ATC_mgr();
80   if (!pAtcMgr)
81   {
82       SG_LOG(SG_ATC, SG_ALERT, "ERROR! No ATC manager! Oops...");
83       _vPtr = NULL;
84   }
85   else
86       _vPtr = pAtcMgr->GetVoicePointer(ATIS);
87   _voiceOK = (_vPtr == NULL ? false : true);
88   if (!(_type != ATIS || _type == AWOS)) {
89        SG_LOG(SG_ATC, SG_ALERT, "ERROR - _type not ATIS or AWOS in atis.cxx");
90   }
91   fgTie("/environment/attention", this, (int_getter)0, &FGATIS::attend);
92
93 ///////////////
94 // FIXME:  This would be more flexible and more extensible
95 // if the mappings were taken from an XML file, not hard-coded ...
96 // ... although having it in a .hxx file is better than nothing.
97 //
98 // Load the remap list from the .hxx file:
99   using namespace lex;
100 # define NIL ""
101 # define REMAP(from,to) _remap[#from] = to;
102 # include "atis_remap.hxx"
103 # undef REMAP
104 # undef NIL
105
106 #ifdef ATIS_TEST
107   SG_LOG(SG_ATC, SG_ALERT, "ATIS initialized");
108 #endif
109 }
110
111 // Hint:
112 // http://localhost:5400/environment/attention?value=1&submit=update
113
114 FGATIS::~FGATIS() {
115   fgUntie("/environment/attention");
116 }
117
118 void FGATIS::Init() {
119 // Nothing to see here.  Move along.
120 }
121
122 void
123 FGATIS::attend (int attn)
124 {
125   attention = attn;
126 #ifdef ATMO_TEST
127   int flag = fgGetInt("/sim/logging/atmo");
128   if (flag) {
129     FGAltimeter().check_model();
130         FGAltimeter().dump_stack();
131   }
132 #endif
133 }
134
135
136 // Main update function - checks whether we are displaying or not the correct message.
137 void FGATIS::Update(double dt) {
138   cur_time = globals->get_time_params()->get_cur_time();
139   msg_OK = (msg_time < cur_time);
140 #ifdef ATIS_TEST
141   if (msg_OK || _display != _prev_display) {
142     cout << "ATIS Update: " << _display << "  " << _prev_display
143       << "  len: " << transmission.length()
144       << "  oldvol: " << old_volume
145       << "  dt: " << dt << endl;
146     msg_time = cur_time;
147   }
148 #endif
149   if(_display) {
150     double volume(0);
151     for (map<string,int>::iterator act = active_on.begin();
152     act != active_on.end(); act++) {
153       string prop = "/instrumentation/" + act->first + "/volume";
154       volume += globals->get_props()->getDoubleValue(prop.c_str());
155     }
156
157 // Check if we need to update the message
158 // - basically every hour and if the weather changes significantly at the station
159 // If !_prev_display, the radio had been detuned for a while and our
160 // "transmission" variable was lost when we were de-instantiated.
161     int rslt = GenTransmission(!_prev_display, attention);
162     TreeOut(msg_OK);
163     if (rslt || volume != old_volume) {
164       //cout << "ATIS calling ATC::render  volume: " << volume << endl;
165       Render(transmission, volume, refname, true);
166       old_volume = volume;
167     }
168   } else {
169 // We shouldn't be displaying
170     //cout << "ATIS.CXX - calling NoRender()..." << endl;
171     NoRender(refname);
172   }
173   _prev_display = _display;
174   attention = 0;
175 }
176
177 string uppercase(const string &s) {
178   string rslt(s);
179   for(string::iterator p = rslt.begin(); p != rslt.end(); p++){
180     *p = toupper(*p);
181   }
182   return rslt;
183 }
184
185 // Replace all occurrences of a given word.
186 // Words in the original string must be separated by hyphens (not spaces).
187 // We check for the word as given, and for the all-caps version thereof.
188 string replace_word(const string _orig, const string _www, const string _nnn){
189 // The following are so we can match words at the beginning
190 // and end of the string.
191   string orig = "-" + _orig + "-";
192   string www = "-" + _www + "-";
193   string nnn = "-" + _nnn + "-";
194
195   size_t where(0);
196   for ( ; (where = orig.find(www, where)) != string::npos ; ) {
197     orig.replace(where, www.length(), nnn);
198     where += nnn.length();
199   }
200   
201   www = uppercase(www);
202   for ( ; (where = orig.find(www, where)) != string::npos ; ) {
203     orig.replace(where, www.length(), nnn);
204     where += nnn.length();
205   }
206   where = orig.length();
207   return orig.substr(1, where-2);
208 }
209
210 // Normally the interval is 1 hour, 
211 // but you can shorten it for testing.
212 const int minute(60);           // measured in seconds
213 #ifdef ATIS_TEST
214   const int ATIS_interval(2*minute);
215 #else
216   const int ATIS_interval(60*minute);
217 #endif
218
219 // FIXME:  This is heuristic.  It gets the right answer for
220 // more than 90% of the world's airports, which is a lot
221 // better than nothing ... but it's not 100%.
222 // We know "most" of the world uses millibars,
223 // but the US, Canada and *some* other places use inches of mercury,
224 // but (a) we have not implemented a reliable method of
225 // ascertaining which airports are in the US, let alone
226 // (b) ascertaining which other places use inches.
227 //
228 int Apt_US_CA(const string id) {
229 // Assume all IDs have length 3 or 4.
230 // No counterexamples have been seen.
231   if (id.length() == 4) {
232     if (id.substr(0,1) == "K") return 1;
233     if (id.substr(0,2) == "CY") return 1;
234   }
235   for (string::const_iterator ptr = id.begin(); ptr != id.end();  ptr++) {
236     if (isdigit(*ptr)) return 1;
237   }
238   return 0;
239 }
240
241 // Generate the actual broadcast ATIS transmission.
242 // Regen means regenerate the /current/ transmission.
243 // Special means generate a new transmission, with a new sequence.
244 // Returns 1 if we actually generated something.
245 int FGATIS::GenTransmission(const int regen, const int special) {
246   using namespace atmodel;
247   using namespace lex;
248
249   string BRK = ".\n";
250   string PAUSE = " / ";
251
252   int interval = _type == ATIS ?
253         ATIS_interval   // ATIS updated hourly
254       : 2*minute;       // AWOS updated more frequently
255
256   FGAirport* apt = FGAirport::findByIdent(ident);
257   int sequence = apt->getDynamics()->updateAtisSequence(interval, special);
258   if (!regen && sequence > LTRS) {
259 //xx      if (msg_OK) cout << "ATIS:  no change: " << sequence << endl;
260 //xx      msg_time = cur_time;
261     return 0;   // no change since last time
262   }
263
264   const int bs(100);
265   char buf[bs];
266   string time_str = fgGetString("sim/time/gmt-string");
267   string hours, mins;
268   string phonetic_seq_string;
269
270   transmission = "";
271
272   int US_CA = Apt_US_CA(ident);
273
274   if (!US_CA) {
275 // UK CAA radiotelephony manual indicates ATIS transmissions start
276 // with "This is ..." 
277     transmission += This_is + " ";
278   } else {
279     // In the US they just start with the airport name.
280   }
281
282   // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name);
283
284 // Note that at this point, multi-word facility names
285 // will sometimes contain hyphens, not spaces.
286   
287   vector<string> name_words;
288   boost::split(name_words, name, boost::is_any_of(" -"));
289
290   for (vector<string>::const_iterator wordp = name_words.begin();
291                 wordp != name_words.end(); wordp++) {
292     string word(*wordp);
293 // Remap some abbreviations that occur in apt.dat, to
294 // make things nicer for the text-to-speech system:
295     for (MSS::const_iterator replace = _remap.begin();
296           replace != _remap.end(); replace++) {
297       // Due to inconsistent capitalisation in the apt.dat file, we need
298       // to do a case-insensitive comparison here.
299       string tmp1 = word, tmp2 = replace->first;
300       boost::algorithm::to_lower(tmp1);
301       boost::algorithm::to_lower(tmp2);
302       if (tmp1 == tmp2) {
303         word = replace->second;
304         break;
305       }
306     }
307     transmission += word + " ";
308   }
309
310   if (_type == ATIS /* as opposed to AWOS */) {
311     transmission += airport_information + " ";
312   } else {
313     transmission += Automated_weather_observation + " ";
314   }
315
316   phonetic_seq_string = GetPhoneticLetter(sequence);  // Add the sequence letter
317   transmission += phonetic_seq_string + BRK;
318
319 // Warning - this is fragile if the time string format changes
320   hours = time_str.substr(0,2).c_str();
321   mins  = time_str.substr(3,2).c_str();
322 // speak each digit separately:
323   transmission += ConvertNumToSpokenDigits(hours + mins);
324   transmission += " " + zulu + " " + weather + BRK;
325
326   transmission += wind + ": ";
327
328   double wind_speed = fgGetDouble("/environment/config/boundary/entry[0]/wind-speed-kt");
329   double wind_dir = fgGetDouble("/environment/config/boundary/entry[0]/wind-from-heading-deg");
330   while (wind_dir <= 0) wind_dir += 360;
331 // The following isn't as bad a kludge as it might seem.
332 // It combines the magvar at the /aircraft/ location with
333 // the wind direction in the environment/config array.
334 // But if the aircraft is close enough to the station to
335 // be receiving the ATIS signal, this should be a good-enough
336 // approximation.  For more-distant aircraft, the wind_dir
337 // shouldn't be corrected anyway.
338 // The less-kludgy approach would be to use the magvar associated
339 // with the station, but that is not tabulated in the stationweather
340 // structure as it stands, and computing it would be expensive.
341 // Also note that as it stands, there is only one environment in
342 // the entire FG universe, so the aircraft environment is the same
343 // as the station environment anyway.
344   wind_dir -= fgGetDouble("/environment/magnetic-variation-deg");       // wind_dir now magnetic
345   if (wind_speed == 0) {
346 // Force west-facing rwys to be used in no-wind situations
347 // which is consistent with Flightgear's initial setup:
348       wind_dir = 270;
349       transmission += " " + light_and_variable;
350   } else {
351       // FIXME: get gust factor in somehow
352       snprintf(buf, bs, "%03.0f", 5*SGMiscd::round(wind_dir/5));
353       transmission += ConvertNumToSpokenDigits(buf);
354
355       snprintf(buf, bs, "%1.0f", wind_speed);
356       transmission += " " + at + " " + ConvertNumToSpokenDigits(buf) + BRK;
357   }
358
359 // Sounds better with a pause in there:
360   transmission += PAUSE;
361
362   int did_some(0);
363   int did_ceiling(0);
364
365   for (int layer = 0; layer <= 4; layer++) {
366     snprintf(buf, bs, "/environment/clouds/layer[%i]/coverage", layer);
367     string coverage = fgGetString(buf);
368     if (coverage == clear) continue;
369     snprintf(buf, bs, "/environment/clouds/layer[%i]/thickness-ft", layer);
370     if (fgGetDouble(buf) == 0) continue;
371     snprintf(buf, bs, "/environment/clouds/layer[%i]/elevation-ft", layer);
372     double ceiling = int(fgGetDouble(buf) - _geod.getElevationFt());
373     if (ceiling > 12000) continue;
374
375 // BEWARE:  At the present time, the environment system has no
376 // way (so far as I know) to represent a "thin broken" or
377 // "thin overcast" layer.  If/when such things are implemented
378 // in the environment system, code will have to be written here
379 // to handle them.
380
381 // First, do the prefix if any:
382     if (coverage == scattered || coverage == few) {
383       if (!did_some) {
384         transmission += "   " + Sky_condition + ": ";
385         did_some++;
386       }
387     } else /* must be a ceiling */  if (!did_ceiling) {
388       transmission += "   " + Ceiling + ": ";
389       did_ceiling++;
390       did_some++;
391     } else {
392       transmission += "   ";    // no prefix required
393     }
394     int cig00  = int(SGMiscd::round(ceiling/100));  // hundreds of feet
395     if (cig00) {
396       int cig000 = cig00/10;
397       cig00 -= cig000*10;       // just the hundreds digit
398       if (cig000) {
399         snprintf(buf, bs, "%i", cig000);
400         transmission += ConvertNumToSpokenDigits(buf);
401         transmission += " " + thousand + " ";
402       }
403       if (cig00) {
404         snprintf(buf, bs, "%i", cig00);
405         transmission += ConvertNumToSpokenDigits(buf);
406         transmission += " " + hundred + " ";
407       }
408     } else {
409       // Should this be "sky obscured?"
410       transmission += " " + zero + " ";     // not "zero hundred"
411     }
412     transmission += coverage + BRK;
413   }
414   if (!did_some) transmission += "   " + Sky + " " + clear + BRK;
415
416   transmission += Temperature + ": ";
417   double Tsl = fgGetDouble("/environment/temperature-sea-level-degc");
418   int temp = int(SGMiscd::round(FGAtmo().fake_T_vs_a_us(_geod.getElevationFt(), Tsl)));
419   if(temp < 0) {
420       transmission += lex::minus + " ";
421   }
422   snprintf(buf, bs, "%i", abs(temp));
423   transmission += ConvertNumToSpokenDigits(buf);
424   if (US_CA) transmission += " " + Celsius;
425   transmission += " " + dewpoint + " ";
426   double dpsl = fgGetDouble("/environment/dewpoint-sea-level-degc");
427   temp = int(SGMiscd::round(FGAtmo().fake_dp_vs_a_us(dpsl, _geod.getElevationFt())));
428   if(temp < 0) {
429       transmission += lex::minus + " ";
430   }
431   snprintf(buf, bs, "%i", abs(temp));
432   transmission += ConvertNumToSpokenDigits(buf);
433   if (US_CA) transmission += " " + Celsius;
434   transmission += BRK;
435
436   transmission += Visibility + ": ";
437   double visibility = fgGetDouble("/environment/config/boundary/entry[0]/visibility-m");
438   visibility /= atmodel::sm;    // convert to statute miles
439   if (visibility < 0.25) {
440     transmission += less_than_one_quarter;
441   } else if (visibility < 0.5) {
442     transmission += one_quarter;
443   } else if (visibility < 0.75) {
444     transmission += one_half;
445   } else if (visibility < 1.0) {
446     transmission += three_quarters;
447   } else if (visibility >= 1.5 && visibility < 2.0) {
448     transmission += one_and_one_half;
449   } else {
450     // integer miles
451     if (visibility > 10) visibility = 10;
452     sprintf(buf, "%i", int(.5 + visibility));
453     transmission += ConvertNumToSpokenDigits(buf);
454   }
455   transmission += BRK;
456
457   double myQNH;
458   double Psl = fgGetDouble("/environment/pressure-sea-level-inhg");
459   {
460     double press, temp;
461     
462     tie(press, temp) = PT_vs_hpt(_geod.getElevationM(), Psl*inHg, Tsl + freezing);
463 #if 0
464     SG_LOG(SG_ATC, SG_ALERT, "Field P: " << press << "  T: " << temp);
465     SG_LOG(SG_ATC, SG_ALERT, "based on elev " << elev 
466                                 << "  Psl: " << Psl
467                                 << "  Tsl: " << Tsl);
468 #endif
469     myQNH = FGAtmo().QNH(_geod.getElevationM(), press);
470   }
471
472 // Convert to millibars for most of the world (not US, not CA)
473   if((!US_CA) && fgGetBool("/sim/atc/use-millibars")) {
474     transmission += QNH + ": ";
475     myQNH /= mbar;
476     if  (myQNH > 1000) myQNH -= 1000;       // drop high digit
477     snprintf(buf, bs, "%03.0f", myQNH);
478     transmission += ConvertNumToSpokenDigits(buf) + " " + millibars + BRK;
479   } else {
480     transmission += Altimeter + ": ";
481     double asetting = myQNH / inHg;         // use inches of mercury
482     asetting *= 100.;                       // shift two decimal places
483     snprintf(buf, bs, "%04.0f", asetting);
484     transmission += ConvertNumToSpokenDigits(buf) + BRK;
485   }
486
487   if (_type == ATIS /* as opposed to AWOS */) {
488     const FGAirport* apt = fgFindAirportID(ident);
489     if (apt) {
490       string rwy_no = apt->getActiveRunwayForUsage()->ident();
491       if(rwy_no != "NN") {
492         transmission += Landing_and_departing_runway + " ";
493         transmission += ConvertRwyNumToSpokenString(rwy_no) + BRK;
494 #ifdef ATIS_TEST
495         if (msg_OK) {
496           msg_time = cur_time;
497           cout << "In atis.cxx, r.rwy_no: " << rwy_no
498              << " wind_dir: " << wind_dir << endl;
499         }
500 #endif
501       }
502     }
503     transmission += On_initial_contact_advise_you_have_information + " ";
504     transmission += phonetic_seq_string;
505     transmission += "... " + BRK + PAUSE + PAUSE;
506   }
507   transmission_readable = transmission;
508 // Take the previous readable string and munge it to
509 // be relatively-more acceptable to the primitive tts system.
510 // Note that : ; and . are among the token-delimeters recognized
511 // by the tts system.
512   for (size_t where;;) {
513     where = transmission.find_first_of(":.");
514     if (where == string::npos) break;
515     transmission.replace(where, 1, PAUSE);
516   }
517   return 1;
518 }
519
520 // Put the transmission into the property tree,
521 // possibly in multiple places if multiple radios
522 // are tuned to the same ATIS.
523 // You can see it by pointing a web browser
524 // at the property tree.  The second comm radio is:
525 // http://localhost:5400/instrumentation/comm[1]
526 //
527 // (Also, if in debug mode, dump it to the console.)
528 void FGATIS::TreeOut(int msg_OK){
529   for (map<string,int>::iterator act = active_on.begin();
530                 act != active_on.end();
531                 act++){
532     string prop = "/instrumentation/" + act->first + "/atis";
533     globals->get_props()->setStringValue(prop.c_str(),
534       ("<pre>\n" + transmission_readable + "</pre>\n").c_str());
535 #ifdef ATIS_TEST
536     if (msg_OK) cout << "**** ATIS active on: " << prop << endl;
537 #endif
538   }
539 #ifdef ATIS_TEST
540   if (msg_OK) cout << transmission_readable << endl;
541 #endif
542 }