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