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?
35 #include <simgear/compiler.h>
37 #include <stdlib.h> // atoi()
38 #include <stdio.h> // sprintf
42 #include <boost/tuple/tuple.hpp>
44 #include <simgear/misc/sg_path.hxx>
46 #include <Environment/environment_mgr.hxx>
47 #include <Environment/environment.hxx>
48 #include <Environment/atmosphere.hxx>
50 #include <Main/fg_props.hxx>
51 #include <Main/globals.hxx>
52 #include <Airports/runways.hxx>
55 #include "commlist.hxx"
56 #include "ATCutils.hxx"
74 _vPtr = globals->get_ATC_mgr()->GetVoicePointer(ATIS);
75 _voiceOK = (_vPtr == NULL ? false : true);
76 if (!(_type != ATIS || _type == AWOS)) {
77 SG_LOG(SG_ATC, SG_ALERT, "ERROR - _type not ATIS or AWOS in atis.cxx");
79 fgTie("/environment/attention", this, (int_getter)0, &FGATIS::attend);
83 // http://localhost:5400/environment/attention?value=1&submit=update
86 fgUntie("/environment/attention");
90 // Nothing to see here. Move along.
94 FGATIS::attend (int attn)
98 int flag = fgGetInt("/sim/logging/atmo");
100 FGAltimeter().check_model();
101 FGAltimeter().dump_stack();
107 // Main update function - checks whether we are displaying or not the correct message.
108 void FGATIS::Update(double dt) {
109 cur_time = globals->get_time_params()->get_cur_time();
110 msg_OK = (msg_time < cur_time);
112 if (msg_OK || _display != _prev_display) {
113 cout << "ATIS Update: " << _display << " " << _prev_display
114 << " len: " << transmission.length()
115 << " oldvol: " << old_volume
116 << " dt: " << dt << endl;
122 for (map<string,int>::iterator act = active_on.begin();
123 act != active_on.end(); act++) {
124 string prop = "/instrumentation/" + act->first + "/volume";
125 volume += globals->get_props()->getDoubleValue(prop.c_str());
128 // Check if we need to update the message
129 // - basically every hour and if the weather changes significantly at the station
130 // If !_prev_display, the radio had been detuned for a while and our
131 // "transmission" variable was lost when we were de-instantiated.
132 int rslt = GenTransmission(!_prev_display, attention);
133 if (rslt || volume != old_volume) {
134 //cout << "ATIS calling ATC::render volume: " << volume << endl;
135 Render(transmission, volume, refname, true);
139 // We shouldn't be displaying
140 //cout << "ATIS.CXX - calling NoRender()..." << endl;
143 _prev_display = _display;
147 string uppercase(const string &s) {
149 for(string::iterator p = rslt.begin(); p != rslt.end(); p++){
155 // Replace all occurrences of a given word.
156 // Words in the original string must be separated by hyphens (not spaces).
157 // We check for the word as given, and for the all-caps version thereof.
158 string replace_word(const string _orig, const string _www, const string _nnn){
159 // The following are so we can match words at the beginning
160 // and end of the string.
161 string orig = "-" + _orig + "-";
162 string www = "-" + _www + "-";
163 string nnn = "-" + _nnn + "-";
166 for ( ; (where = orig.find(www, where)) != string::npos ; ) {
167 orig.replace(where, www.length(), nnn);
168 where += nnn.length();
171 www = uppercase(www);
172 for ( ; (where = orig.find(www, where)) != string::npos ; ) {
173 orig.replace(where, www.length(), nnn);
174 where += nnn.length();
176 where = orig.length();
177 return orig.substr(1, where-2);
180 // Normally the interval is 1 hour,
181 // but you can shorten it for testing.
182 const int minute(60); // measured in seconds
184 const int ATIS_interval(2*minute);
186 const int ATIS_interval(60*minute);
189 // Generate the actual broadcast ATIS transmission.
190 // Regen means regenerate the /current/ transmission.
191 // Special means generate a new transmission, with a new sequence.
192 // Returns 1 if we actually generated something.
193 int FGATIS::GenTransmission(const int regen, const int special) {
194 using namespace atmodel;
198 double tstamp = atof(fgGetString("sim/time/elapsed-sec"));
199 int interval = ATIS ? ATIS_interval : 2*minute; // AWOS updated frequently
200 int sequence = current_commlist->GetAtisSequence(ident,
201 tstamp, interval, special);
202 if (!regen && sequence > LTRS) {
203 //xx if (msg_OK) cout << "ATIS: no change: " << sequence << endl;
204 //xx msg_time = cur_time;
205 return 0; // no change since last time
210 string time_str = fgGetString("sim/time/gmt-string");
212 string phonetic_seq_string;
216 if (ident.substr(0,2) == "EG") {
217 // UK CAA radiotelephony manual indicates ATIS transmissions start
218 // with "This is ..."
219 transmission += "This_is + ";
221 // In the US they just start with the airport name.
224 // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name);
226 // Note that at this point, multi-word facility names
227 // will sometimes contain hyphens, not spaces.
228 // Force the issue, just to be safe:
231 for(string::iterator p = name2.begin(); p != name2.end(); p++){
232 if (*p == ' ') *p = '-';
236 // FIXME: This would be more flexible and more extensible
237 // if the mappings were taken from an XML file, not hard-coded.
240 // Remap some abbreviations that occur in apt.dat, to
241 // make things nicer for the text-to-speech system:
242 name2 = replace_word(name2, "Intl", "International");
243 name2 = replace_word(name2, "Rgnl", "Regional");
244 name2 = replace_word(name2, "Co", "County");
245 name2 = replace_word(name2, "Muni", "Municipal");
246 name2 = replace_word(name2, "Mem", "Memorial");
247 name2 = replace_word(name2, "Fld", "Field");
248 name2 = replace_word(name2, "AFB", "Air-Force-Base");
249 name2 = replace_word(name2, "AAF", "Army-Air-Field");
250 name2 = replace_word(name2, "MCAS", "Marine-Corps-Air-Station");
251 transmission += name2 + " ";
252 if (_type == ATIS /* as opposed to AWOS */) {
253 transmission += "airport_information ";
255 transmission += "Automated_weather_observation ";
258 phonetic_seq_string = GetPhoneticLetter(sequence); // Add the sequence letter
259 transmission += phonetic_seq_string + BRK;
261 // Warning - this is fragile if the time string format changes
262 hours = time_str.substr(0,2).c_str();
263 mins = time_str.substr(3,2).c_str();
264 // speak each digit separately:
265 transmission += ConvertNumToSpokenDigits(hours + mins);
266 transmission += " zulu weather" + BRK;
268 transmission += "Wind: ";
270 double wind_speed = fgGetDouble("/environment/config/boundary/entry[0]/wind-speed-kt");
271 double wind_dir = fgGetDouble("/environment/config/boundary/entry[0]/wind-from-heading-deg");
272 while (wind_dir <= 0) wind_dir += 360;
273 // The following isn't as bad a kludge as it might seem.
274 // It combines the magvar at the /aircraft/ location with
275 // the wind direction in the environment/config array.
276 // But if the aircraft is close enough to the station to
277 // be receiving the ATIS signal, this should be a good-enough
278 // approximation. For more-distant aircraft, the wind_dir
279 // shouldn't be corrected anyway.
280 // The less-kludgy approach would be to use the magvar associated
281 // with the station, but that is not tabulated in the stationweather
282 // structure as it stands, and computing it would be expensive.
283 // Also note that as it stands, there is only one environment in
284 // the entire FG universe, so the aircraft environment is the same
285 // as the station environment anyway.
286 wind_dir -= fgGetDouble("/environment/magnetic-variation-deg"); // wind_dir now magnetic
287 if (wind_speed == 0) {
288 // Force west-facing rwys to be used in no-wind situations
289 // which is consistent with Flightgear's initial setup:
291 transmission += " light_and_variable";
293 // FIXME: get gust factor in somehow
294 snprintf(buf, bs, "%03.0f", 5*SGMiscd::round(wind_dir/5));
295 transmission += ConvertNumToSpokenDigits(buf);
297 snprintf(buf, bs, "%1.0f", wind_speed);
298 transmission += " at " + ConvertNumToSpokenDigits(buf) + BRK;
304 for (int layer = 0; layer <= 4; layer++) {
305 snprintf(buf, bs, "/environment/clouds/layer[%i]/coverage", layer);
306 string coverage = fgGetString(buf);
307 if (coverage == "clear") continue;
308 snprintf(buf, bs, "/environment/clouds/layer[%i]/thickness-ft", layer);
309 if (fgGetDouble(buf) == 0) continue;
310 snprintf(buf, bs, "/environment/clouds/layer[%i]/elevation-ft", layer);
311 double ceiling = int(fgGetDouble(buf) - _geod.getElevationFt());
312 if (ceiling > 12000) continue;
313 if (coverage == "scattered") {
314 if (!did_some) transmission += " Sky_condition: ";
316 } else /* must be a ceiling */ if (!did_ceiling) {
317 transmission += " Ceiling: ";
323 int cig00 = int(SGMiscd::round(ceiling/100)); // hundreds of feet
325 int cig000 = cig00/10;
326 cig00 -= cig000*10; // just the hundreds digit
328 snprintf(buf, bs, "%i", cig000);
329 transmission += ConvertNumToSpokenDigits(buf);
330 transmission += " thousand ";
333 snprintf(buf, bs, "%i", cig00);
334 transmission += ConvertNumToSpokenDigits(buf);
335 transmission += " hundred ";
338 // Should this be "sky obscured?"
339 transmission += " zero "; // not "zero hundred"
341 transmission += coverage + BRK;
344 transmission += "Temperature: ";
345 double Tsl = fgGetDouble("/environment/temperature-sea-level-degc");
346 int temp = int(SGMiscd::round(FGAtmo().fake_T_vs_a_us(_geod.getElevationFt(), Tsl)));
348 transmission += "minus ";
350 snprintf(buf, bs, "%i", abs(temp));
351 transmission += ConvertNumToSpokenDigits(buf);
352 transmission += " dewpoint ";
353 double dpsl = fgGetDouble("/environment/dewpoint-sea-level-degc");
354 temp = int(SGMiscd::round(FGAtmo().fake_dp_vs_a_us(dpsl, _geod.getElevationFt())));
356 transmission += "minus ";
358 snprintf(buf, bs, "%i", abs(temp));
359 transmission += ConvertNumToSpokenDigits(buf) + BRK;
362 transmission += "Visibility: ";
363 double visibility = fgGetDouble("/environment/config/boundary/entry[0]/visibility-m");
364 visibility /= atmodel::sm; // convert to statute miles
365 if (visibility < 0.25) {
366 transmission += "less than one quarter";
367 } else if (visibility < 0.5) {
368 transmission += "one quarter";
369 } else if (visibility < 0.75) {
370 transmission += "one half";
371 } else if (visibility < 1.0) {
372 transmission += "three quarters";
373 } else if (visibility >= 1.5 && visibility < 2.0) {
374 transmission += "one and one half";
377 if (visibility > 10) visibility = 10;
378 sprintf(buf, "%i", int(.5 + visibility));
379 transmission += ConvertNumToSpokenDigits(buf);
383 transmission += "Altimeter: ";
385 double Psl = fgGetDouble("/environment/pressure-sea-level-inhg");
389 tie(press, temp) = PT_vs_hpt(_geod.getElevationM(), Psl*inHg, Tsl + freezing);
391 SG_LOG(SG_ATC, SG_ALERT, "Field P: " << press << " T: " << temp);
392 SG_LOG(SG_ATC, SG_ALERT, "based on elev " << elev
396 myQNH = FGAtmo().QNH(_geod.getElevationM(), press);
398 if(ident.substr(0,2) == "EG" && fgGetBool("/sim/atc/use-millibars")) {
399 // Convert to millibars for the UK!
401 if (myQNH > 1000) myQNH -= 1000; // drop high digit
402 snprintf(buf, bs, "%03.0f", myQNH);
405 myQNH *= 100.; // shift two decimal places
406 snprintf(buf, bs, "%04.0f", myQNH);
408 transmission += ConvertNumToSpokenDigits(buf) + BRK;
410 if (_type == ATIS /* as opposed to AWOS */) {
411 const FGAirport* apt = fgFindAirportID(ident);
413 string rwy_no = apt->getActiveRunwayForUsage()->ident();
415 transmission += "Landing_and_departing_runway ";
416 transmission += ConvertRwyNumToSpokenString(rwy_no) + BRK;
419 //cout << "In atis.cxx, r.rwy_no: " << rwy_no
420 // << " wind_dir: " << wind_dir << endl;
423 transmission += "On_initial_contact_advise_you_have_information ";
424 transmission += phonetic_seq_string;
425 transmission += "... " + BRK;
428 cout << "**** ATIS active on:";
430 for (map<string,int>::iterator act = active_on.begin(); act != active_on.end(); act++){
431 string prop = "/instrumentation/" + act->first + "/atis";
432 globals->get_props()->setStringValue(prop.c_str(),
433 ("<pre>\n" + transmission + "</pre>\n").c_str());
439 cout << " ****" << endl;
440 cout << transmission << endl;
441 // Note that even if we aren't outputting the transmission
442 // on stdout, you can still see it by pointing a web browser
443 // at the property tree. The second comm radio is:
444 // http://localhost:5400/instrumentation/comm[1]
447 // Take the previous English-looking string and munge it to
448 // be relatively-more acceptable to the primitive tts system.
449 // Note that : ; and . are among the token-delimeters recognized
450 // by the tts system.
451 for (size_t where;;) {
452 where = transmission.find_first_of(":.");
453 if (where == string::npos) break;
454 transmission.replace(where, 1, " /_ ");