1 // atis.cxx - routines to generate the ATIS info string
2 // This is the implementation of the FGATIS class
4 // Written by David Luff, started October 2001.
6 // Copyright (C) 2001 David C Luff - david.luff@nottingham.ac.uk
8 // This program is free software; you can redistribute it and/or
9 // modify it under the terms of the GNU General Public License as
10 // published by the Free Software Foundation; either version 2 of the
11 // License, or (at your option) any later version.
13 // This program is distributed in the hope that it will be useful, but
14 // WITHOUT ANY WARRANTY; without even the implied warranty of
15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 // General Public License for more details.
18 // You should have received a copy of the GNU General Public License
19 // along with this program; if not, write to the Free Software
20 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23 ///// TODO: _Cumulative_ sky coverage.
24 ///// TODO: wind _gust_
25 ///// TODO: more-sensible encoding of voice samples
26 ///// u-law? outright synthesis?
34 #include "atis_lexicon.hxx"
36 #include <simgear/compiler.h>
38 #include <stdlib.h> // atoi()
39 #include <stdio.h> // sprintf
43 #include <boost/tuple/tuple.hpp>
44 #include <boost/algorithm/string.hpp>
45 #include <boost/algorithm/string/case_conv.hpp>
48 #include <simgear/misc/sg_path.hxx>
50 #include <Environment/environment_mgr.hxx>
51 #include <Environment/environment.hxx>
52 #include <Environment/atmosphere.hxx>
54 #include <Main/fg_props.hxx>
55 #include <Main/globals.hxx>
56 #include <Airports/runways.hxx>
59 #include "commlist.hxx"
60 #include "ATCutils.hxx"
78 _vPtr = globals->get_ATC_mgr()->GetVoicePointer(ATIS);
79 _voiceOK = (_vPtr == NULL ? false : true);
80 if (!(_type != ATIS || _type == AWOS)) {
81 SG_LOG(SG_ATC, SG_ALERT, "ERROR - _type not ATIS or AWOS in atis.cxx");
83 fgTie("/environment/attention", this, (int_getter)0, &FGATIS::attend);
86 // FIXME: This would be more flexible and more extensible
87 // if the mappings were taken from an XML file, not hard-coded ...
88 // ... although having it in a .hxx file is better than nothing.
90 // Load the remap list from the .hxx file:
93 # define REMAP(from,to) _remap[#from] = to;
94 # include "atis_remap.hxx"
99 SG_LOG(SG_ATC, SG_ALERT, "ATIS initialized");
104 // http://localhost:5400/environment/attention?value=1&submit=update
107 fgUntie("/environment/attention");
110 void FGATIS::Init() {
111 // Nothing to see here. Move along.
115 FGATIS::attend (int attn)
119 int flag = fgGetInt("/sim/logging/atmo");
121 FGAltimeter().check_model();
122 FGAltimeter().dump_stack();
128 // Main update function - checks whether we are displaying or not the correct message.
129 void FGATIS::Update(double dt) {
130 cur_time = globals->get_time_params()->get_cur_time();
131 msg_OK = (msg_time < cur_time);
133 if (msg_OK || _display != _prev_display) {
134 cout << "ATIS Update: " << _display << " " << _prev_display
135 << " len: " << transmission.length()
136 << " oldvol: " << old_volume
137 << " dt: " << dt << endl;
143 for (map<string,int>::iterator act = active_on.begin();
144 act != active_on.end(); act++) {
145 string prop = "/instrumentation/" + act->first + "/volume";
146 volume += globals->get_props()->getDoubleValue(prop.c_str());
149 // Check if we need to update the message
150 // - basically every hour and if the weather changes significantly at the station
151 // If !_prev_display, the radio had been detuned for a while and our
152 // "transmission" variable was lost when we were de-instantiated.
153 int rslt = GenTransmission(!_prev_display, attention);
155 if (rslt || volume != old_volume) {
156 //cout << "ATIS calling ATC::render volume: " << volume << endl;
157 Render(transmission, volume, refname, true);
161 // We shouldn't be displaying
162 //cout << "ATIS.CXX - calling NoRender()..." << endl;
165 _prev_display = _display;
169 string uppercase(const string &s) {
171 for(string::iterator p = rslt.begin(); p != rslt.end(); p++){
177 // Replace all occurrences of a given word.
178 // Words in the original string must be separated by hyphens (not spaces).
179 // We check for the word as given, and for the all-caps version thereof.
180 string replace_word(const string _orig, const string _www, const string _nnn){
181 // The following are so we can match words at the beginning
182 // and end of the string.
183 string orig = "-" + _orig + "-";
184 string www = "-" + _www + "-";
185 string nnn = "-" + _nnn + "-";
188 for ( ; (where = orig.find(www, where)) != string::npos ; ) {
189 orig.replace(where, www.length(), nnn);
190 where += nnn.length();
193 www = uppercase(www);
194 for ( ; (where = orig.find(www, where)) != string::npos ; ) {
195 orig.replace(where, www.length(), nnn);
196 where += nnn.length();
198 where = orig.length();
199 return orig.substr(1, where-2);
202 // Normally the interval is 1 hour,
203 // but you can shorten it for testing.
204 const int minute(60); // measured in seconds
206 const int ATIS_interval(2*minute);
208 const int ATIS_interval(60*minute);
211 // FIXME: This is heuristic. It gets the right answer for
212 // more than 90% of the world's airports, which is a lot
213 // better than nothing ... but it's not 100%.
214 // We know "most" of the world uses millibars,
215 // but the US, Canada and *some* other places use inches of mercury,
216 // but (a) we have not implemented a reliable method of
217 // ascertaining which airports are in the US, let alone
218 // (b) ascertaining which other places use inches.
220 int Apt_US_CA(const string id) {
221 // Assume all IDs have length 3 or 4.
222 // No counterexamples have been seen.
223 if (id.length() == 4) {
224 if (id.substr(0,1) == "K") return 1;
225 if (id.substr(0,2) == "CY") return 1;
227 for (string::const_iterator ptr = id.begin(); ptr != id.end(); ptr++) {
228 if (isdigit(*ptr)) return 1;
233 // Generate the actual broadcast ATIS transmission.
234 // Regen means regenerate the /current/ transmission.
235 // Special means generate a new transmission, with a new sequence.
236 // Returns 1 if we actually generated something.
237 int FGATIS::GenTransmission(const int regen, const int special) {
238 using namespace atmodel;
242 string PAUSE = " / ";
244 double tstamp = atof(fgGetString("sim/time/elapsed-sec"));
245 int interval = _type == ATIS ?
246 ATIS_interval // ATIS updated hourly
247 : 2*minute; // AWOS updated more frequently
248 int sequence = current_commlist->GetAtisSequence(ident,
249 tstamp, interval, special);
250 if (!regen && sequence > LTRS) {
251 //xx if (msg_OK) cout << "ATIS: no change: " << sequence << endl;
252 //xx msg_time = cur_time;
253 return 0; // no change since last time
258 string time_str = fgGetString("sim/time/gmt-string");
260 string phonetic_seq_string;
264 int US_CA = Apt_US_CA(ident);
267 // UK CAA radiotelephony manual indicates ATIS transmissions start
268 // with "This is ..."
269 transmission += This_is + " ";
271 // In the US they just start with the airport name.
274 // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name);
276 // Note that at this point, multi-word facility names
277 // will sometimes contain hyphens, not spaces.
279 vector<string> name_words;
280 boost::split(name_words, name, boost::is_any_of(" -"));
282 for (vector<string>::const_iterator wordp = name_words.begin();
283 wordp != name_words.end(); wordp++) {
285 // Remap some abbreviations that occur in apt.dat, to
286 // make things nicer for the text-to-speech system:
287 for (MSS::const_iterator replace = _remap.begin();
288 replace != _remap.end(); replace++) {
289 // Due to inconsistent capitalisation in the apt.dat file, we need
290 // to do a case-insensitive comparison here.
291 string tmp1 = word, tmp2 = replace->first;
292 boost::algorithm::to_lower(tmp1);
293 boost::algorithm::to_lower(tmp2);
295 word = replace->second;
299 transmission += word + " ";
302 if (_type == ATIS /* as opposed to AWOS */) {
303 transmission += airport_information + " ";
305 transmission += Automated_weather_observation + " ";
308 phonetic_seq_string = GetPhoneticLetter(sequence); // Add the sequence letter
309 transmission += phonetic_seq_string + BRK;
311 // Warning - this is fragile if the time string format changes
312 hours = time_str.substr(0,2).c_str();
313 mins = time_str.substr(3,2).c_str();
314 // speak each digit separately:
315 transmission += ConvertNumToSpokenDigits(hours + mins);
316 transmission += " " + zulu + " " + weather + BRK;
318 transmission += wind + ": ";
320 double wind_speed = fgGetDouble("/environment/config/boundary/entry[0]/wind-speed-kt");
321 double wind_dir = fgGetDouble("/environment/config/boundary/entry[0]/wind-from-heading-deg");
322 while (wind_dir <= 0) wind_dir += 360;
323 // The following isn't as bad a kludge as it might seem.
324 // It combines the magvar at the /aircraft/ location with
325 // the wind direction in the environment/config array.
326 // But if the aircraft is close enough to the station to
327 // be receiving the ATIS signal, this should be a good-enough
328 // approximation. For more-distant aircraft, the wind_dir
329 // shouldn't be corrected anyway.
330 // The less-kludgy approach would be to use the magvar associated
331 // with the station, but that is not tabulated in the stationweather
332 // structure as it stands, and computing it would be expensive.
333 // Also note that as it stands, there is only one environment in
334 // the entire FG universe, so the aircraft environment is the same
335 // as the station environment anyway.
336 wind_dir -= fgGetDouble("/environment/magnetic-variation-deg"); // wind_dir now magnetic
337 if (wind_speed == 0) {
338 // Force west-facing rwys to be used in no-wind situations
339 // which is consistent with Flightgear's initial setup:
341 transmission += " " + light_and_variable;
343 // FIXME: get gust factor in somehow
344 snprintf(buf, bs, "%03.0f", 5*SGMiscd::round(wind_dir/5));
345 transmission += ConvertNumToSpokenDigits(buf);
347 snprintf(buf, bs, "%1.0f", wind_speed);
348 transmission += " " + at + " " + ConvertNumToSpokenDigits(buf) + BRK;
351 // Sounds better with a pause in there:
352 transmission += PAUSE;
357 for (int layer = 0; layer <= 4; layer++) {
358 snprintf(buf, bs, "/environment/clouds/layer[%i]/coverage", layer);
359 string coverage = fgGetString(buf);
360 if (coverage == clear) continue;
361 snprintf(buf, bs, "/environment/clouds/layer[%i]/thickness-ft", layer);
362 if (fgGetDouble(buf) == 0) continue;
363 snprintf(buf, bs, "/environment/clouds/layer[%i]/elevation-ft", layer);
364 double ceiling = int(fgGetDouble(buf) - _geod.getElevationFt());
365 if (ceiling > 12000) continue;
367 // BEWARE: At the present time, the environment system has no
368 // way (so far as I know) to represent a "thin broken" or
369 // "thin overcast" layer. If/when such things are implemented
370 // in the environment system, code will have to be written here
373 // First, do the prefix if any:
374 if (coverage == scattered || coverage == few) {
376 transmission += " " + Sky_condition + ": ";
379 } else /* must be a ceiling */ if (!did_ceiling) {
380 transmission += " " + Ceiling + ": ";
384 transmission += " "; // no prefix required
386 int cig00 = int(SGMiscd::round(ceiling/100)); // hundreds of feet
388 int cig000 = cig00/10;
389 cig00 -= cig000*10; // just the hundreds digit
391 snprintf(buf, bs, "%i", cig000);
392 transmission += ConvertNumToSpokenDigits(buf);
393 transmission += " " + thousand + " ";
396 snprintf(buf, bs, "%i", cig00);
397 transmission += ConvertNumToSpokenDigits(buf);
398 transmission += " " + hundred + " ";
401 // Should this be "sky obscured?"
402 transmission += " " + zero + " "; // not "zero hundred"
404 transmission += coverage + BRK;
406 if (!did_some) transmission += " " + Sky + " " + clear + BRK;
408 transmission += Temperature + ": ";
409 double Tsl = fgGetDouble("/environment/temperature-sea-level-degc");
410 int temp = int(SGMiscd::round(FGAtmo().fake_T_vs_a_us(_geod.getElevationFt(), Tsl)));
412 transmission += lex::minus + " ";
414 snprintf(buf, bs, "%i", abs(temp));
415 transmission += ConvertNumToSpokenDigits(buf);
416 if (US_CA) transmission += " " + Celsius;
417 transmission += " " + dewpoint + " ";
418 double dpsl = fgGetDouble("/environment/dewpoint-sea-level-degc");
419 temp = int(SGMiscd::round(FGAtmo().fake_dp_vs_a_us(dpsl, _geod.getElevationFt())));
421 transmission += lex::minus + " ";
423 snprintf(buf, bs, "%i", abs(temp));
424 transmission += ConvertNumToSpokenDigits(buf);
425 if (US_CA) transmission += " " + Celsius;
428 transmission += Visibility + ": ";
429 double visibility = fgGetDouble("/environment/config/boundary/entry[0]/visibility-m");
430 visibility /= atmodel::sm; // convert to statute miles
431 if (visibility < 0.25) {
432 transmission += less_than_one_quarter;
433 } else if (visibility < 0.5) {
434 transmission += one_quarter;
435 } else if (visibility < 0.75) {
436 transmission += one_half;
437 } else if (visibility < 1.0) {
438 transmission += three_quarters;
439 } else if (visibility >= 1.5 && visibility < 2.0) {
440 transmission += one_and_one_half;
443 if (visibility > 10) visibility = 10;
444 sprintf(buf, "%i", int(.5 + visibility));
445 transmission += ConvertNumToSpokenDigits(buf);
450 double Psl = fgGetDouble("/environment/pressure-sea-level-inhg");
454 tie(press, temp) = PT_vs_hpt(_geod.getElevationM(), Psl*inHg, Tsl + freezing);
456 SG_LOG(SG_ATC, SG_ALERT, "Field P: " << press << " T: " << temp);
457 SG_LOG(SG_ATC, SG_ALERT, "based on elev " << elev
461 myQNH = FGAtmo().QNH(_geod.getElevationM(), press);
464 // Convert to millibars for most of the world (not US, not CA)
465 if((!US_CA) && fgGetBool("/sim/atc/use-millibars")) {
466 transmission += QNH + ": ";
468 if (myQNH > 1000) myQNH -= 1000; // drop high digit
469 snprintf(buf, bs, "%03.0f", myQNH);
470 transmission += ConvertNumToSpokenDigits(buf) + " " + millibars + BRK;
472 transmission += Altimeter + ": ";
473 double asetting = myQNH / inHg; // use inches of mercury
474 asetting *= 100.; // shift two decimal places
475 snprintf(buf, bs, "%04.0f", asetting);
476 transmission += ConvertNumToSpokenDigits(buf) + BRK;
479 if (_type == ATIS /* as opposed to AWOS */) {
480 const FGAirport* apt = fgFindAirportID(ident);
482 string rwy_no = apt->getActiveRunwayForUsage()->ident();
484 transmission += Landing_and_departing_runway + " ";
485 transmission += ConvertRwyNumToSpokenString(rwy_no) + BRK;
489 cout << "In atis.cxx, r.rwy_no: " << rwy_no
490 << " wind_dir: " << wind_dir << endl;
495 transmission += On_initial_contact_advise_you_have_information + " ";
496 transmission += phonetic_seq_string;
497 transmission += "... " + BRK + PAUSE + PAUSE;
499 transmission_readable = transmission;
500 // Take the previous readable string and munge it to
501 // be relatively-more acceptable to the primitive tts system.
502 // Note that : ; and . are among the token-delimeters recognized
503 // by the tts system.
504 for (size_t where;;) {
505 where = transmission.find_first_of(":.");
506 if (where == string::npos) break;
507 transmission.replace(where, 1, PAUSE);
512 // Put the transmission into the property tree,
513 // possibly in multiple places if multiple radios
514 // are tuned to the same ATIS.
515 // You can see it by pointing a web browser
516 // at the property tree. The second comm radio is:
517 // http://localhost:5400/instrumentation/comm[1]
519 // (Also, if in debug mode, dump it to the console.)
520 void FGATIS::TreeOut(int msg_OK){
521 for (map<string,int>::iterator act = active_on.begin();
522 act != active_on.end();
524 string prop = "/instrumentation/" + act->first + "/atis";
525 globals->get_props()->setStringValue(prop.c_str(),
526 ("<pre>\n" + transmission_readable + "</pre>\n").c_str());
528 if (msg_OK) cout << "**** ATIS active on: " << prop << endl;
532 if (msg_OK) cout << transmission_readable << endl;