]> git.mxchange.org Git - flightgear.git/blob - src/ATCDCL/atis.cxx
Split ATIS generator into smaller, more readable methods.
[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 #include <ATC/CommStation.hxx>
59
60 #include "ATCutils.hxx"
61 #include "ATISmgr.hxx"
62
63 using std::string;
64 using std::map;
65 using std::cout;
66 using std::cout;
67 using boost::ref;
68 using boost::tie;
69 using flightgear::CommStation;
70
71 FGATIS::FGATIS(const std::string& name, int num) :
72   _name(name),
73   _num(num),
74   _cb_attention(this, &FGATIS::attend, fgGetNode("/environment/attention", true)),
75   transmission(""),
76   trans_ident(""),
77   old_volume(0),
78   atis_failed(false),
79   msg_time(0),
80   cur_time(0),
81   msg_OK(0),
82   _attention(false),
83   _prev_display(0),
84   _time_before_search_sec(0),
85   _last_frequency(0)
86 {
87   _root         = fgGetNode("/instrumentation", true)->getNode(_name, num, true);
88   _volume       = _root->getNode("volume",true);
89   _serviceable  = _root->getNode("serviceable",true);
90
91   if (name != "nav")
92   {
93       // only drive "operable" for non-nav instruments (nav radio drives this separately)
94       _operable = _root->getNode("operable",true);
95       _operable->setBoolValue(false);
96   }
97
98   _electrical   = fgGetNode("/systems/electrical/outputs",true)->getNode(_name,num, true);
99   _atis         = _root->getNode("atis",true);
100   _freq         = _root->getNode("frequencies/selected-mhz",true);
101
102   // current position
103   _lon_node  = fgGetNode("/position/longitude-deg", true);
104   _lat_node  = fgGetNode("/position/latitude-deg",  true);
105   _elev_node = fgGetNode("/position/altitude-ft",   true);
106
107   // backward compatibility: some properties may not exist (but default to "ON")
108   if (!_serviceable->hasValue())
109       _serviceable->setBoolValue(true);
110   if (!_electrical->hasValue())
111       _electrical->setDoubleValue(24.0);
112
113 ///////////////
114 // FIXME:  This would be more flexible and more extensible
115 // if the mappings were taken from an XML file, not hard-coded ...
116 // ... although having it in a .hxx file is better than nothing.
117 //
118 // Load the remap list from the .hxx file:
119   using namespace lex;
120 # define NIL ""
121 # define REMAP(from,to) _remap[#from] = to;
122 # include "atis_remap.hxx"
123 # undef REMAP
124 # undef NIL
125
126 #ifdef ATIS_TEST
127   SG_LOG(SG_ATC, SG_ALERT, "ATIS initialized");
128 #endif
129 }
130
131 // Hint:
132 // http://localhost:5400/environment/attention?value=1&submit=update
133
134 FGATCVoice* FGATIS::GetVoicePointer()
135 {
136     FGATISMgr* pAtisMgr = globals->get_ATIS_mgr();
137     if (!pAtisMgr)
138     {
139         SG_LOG(SG_ATC, SG_ALERT, "ERROR! No ATIS manager! Oops...");
140         return NULL;
141     }
142
143     return pAtisMgr->GetVoicePointer(ATIS);
144 }
145
146 void FGATIS::init() {
147 // Nothing to see here.  Move along.
148 }
149
150 void
151 FGATIS::attend(SGPropertyNode* node)
152 {
153   _attention = node->getBoolValue();
154 #ifdef ATMO_TEST
155   int flag = fgGetInt("/sim/logging/atmo");
156   if (flag) {
157     FGAltimeter().check_model();
158         FGAltimeter().dump_stack();
159   }
160 #endif
161 }
162
163
164 // Main update function - checks whether we are displaying or not the correct message.
165 void FGATIS::update(double dt) {
166   cur_time = globals->get_time_params()->get_cur_time();
167   msg_OK = (msg_time < cur_time);
168
169 #ifdef ATIS_TEST
170   if (msg_OK || _display != _prev_display) {
171     cout << "ATIS Update: " << _display << "  " << _prev_display
172       << "  len: " << transmission.length()
173       << "  oldvol: " << old_volume
174       << "  dt: " << dt << endl;
175     msg_time = cur_time;
176   }
177 #endif
178
179   double volume = 0;
180   if ((_electrical->getDoubleValue() > 8) && _serviceable->getBoolValue())
181   {
182       _time_before_search_sec -= dt;
183       // radio is switched on and OK
184       if (_operable.valid())
185           _operable->setBoolValue(true);
186
187       // Search the tuned frequencies
188       search();
189
190       if (_display)
191       {
192           volume = _volume->getDoubleValue();
193       }
194   }
195   else
196   {
197       // radio is OFF
198       if (_operable.valid())
199           _operable->setBoolValue(false);
200       _time_before_search_sec = 0;
201   }
202
203   if (volume > 0.05)
204   {
205     // Check if we need to update the message
206     // - basically every hour and if the weather changes significantly at the station
207     // If !_prev_display, the radio had been detuned for a while and our
208     // "transmission" variable was lost when we were de-instantiated.
209     int changed = genTransmission(!_prev_display, _attention);
210
211     // update output property
212     TreeOut(msg_OK);
213
214     if (changed || volume != old_volume) {
215       // audio output enabled
216       Render(transmission, volume, _name, true);
217       old_volume = volume;
218     }
219     _prev_display = _display;
220   } else {
221     // silence
222     NoRender(_name);
223     _prev_display = false;
224   }
225   _attention = false;
226
227   FGATC::update(dt);
228 }
229
230 string uppercase(const string &s) {
231   string rslt(s);
232   for(string::iterator p = rslt.begin(); p != rslt.end(); p++){
233     *p = toupper(*p);
234   }
235   return rslt;
236 }
237
238 // Replace all occurrences of a given word.
239 // Words in the original string must be separated by hyphens (not spaces).
240 // We check for the word as given, and for the all-caps version thereof.
241 string replace_word(const string _orig, const string _www, const string _nnn){
242 // The following are so we can match words at the beginning
243 // and end of the string.
244   string orig = "-" + _orig + "-";
245   string www = "-" + _www + "-";
246   string nnn = "-" + _nnn + "-";
247
248   size_t where(0);
249   for ( ; (where = orig.find(www, where)) != string::npos ; ) {
250     orig.replace(where, www.length(), nnn);
251     where += nnn.length();
252   }
253   
254   www = uppercase(www);
255   for ( ; (where = orig.find(www, where)) != string::npos ; ) {
256     orig.replace(where, www.length(), nnn);
257     where += nnn.length();
258   }
259   where = orig.length();
260   return orig.substr(1, where-2);
261 }
262
263 // Normally the interval is 1 hour, 
264 // but you can shorten it for testing.
265 const int minute(60);           // measured in seconds
266 #ifdef ATIS_TEST
267   const int ATIS_interval(2*minute);
268 #else
269   const int ATIS_interval(60*minute);
270 #endif
271
272 // FIXME:  This is heuristic.  It gets the right answer for
273 // more than 90% of the world's airports, which is a lot
274 // better than nothing ... but it's not 100%.
275 // We know "most" of the world uses millibars,
276 // but the US, Canada and *some* other places use inches of mercury,
277 // but (a) we have not implemented a reliable method of
278 // ascertaining which airports are in the US, let alone
279 // (b) ascertaining which other places use inches.
280 //
281 bool Apt_US_CA(const string id) {
282 // Assume all IDs have length 3 or 4.
283 // No counterexamples have been seen.
284   if (id.length() == 4) {
285     if (id.substr(0,1) == "K") return true;
286     if (id.substr(0,2) == "CY") return true;
287   }
288   for (string::const_iterator ptr = id.begin(); ptr != id.end();  ptr++) {
289     if (isdigit(*ptr)) return true;
290   }
291   return false;
292 }
293
294 static string BRK = ".\n";
295 static string PAUSE = " / ";
296
297 /** Generate the actual broadcast ATIS transmission.
298 *   'regen' triggers a regeneration of the /current/ transmission.
299 *   'special' generates a new transmission, with a new sequence.
300 *   Returns 1 if we actually generated something.
301 */
302 int FGATIS::genTransmission(const int regen, const bool special) {
303   using namespace atmodel;
304   using namespace lex;
305
306   // ATIS updated hourly, AWOS updated more frequently
307   int interval = _type == ATIS ? ATIS_interval : 2*minute;
308
309   FGAirport* apt = FGAirport::findByIdent(ident);
310   int sequence = apt->getDynamics()->updateAtisSequence(interval, special);
311   if (!regen && sequence > LTRS) {
312     //xx      if (msg_OK) cout << "ATIS:  no change: " << sequence << endl;
313     //xx    msg_time = cur_time;
314     return 0;   // no change since last time
315   }
316
317   transmission = "";
318
319   bool US_CA = Apt_US_CA(ident);
320   if (!US_CA) {
321       // UK CAA radiotelephony manual indicates ATIS transmissions start
322       // with "This is ...", while US just starts with airport name.
323       transmission += This_is + " ";
324   }
325
326   // add facility name
327   genFacilityInfo();
328
329   if (_type == ATIS) {
330       // ATIS phraseology starts with "... airport information"
331       transmission += airport_information + " ";
332   } else {
333       // AWOS
334       transmission += Automated_weather_observation + " ";
335   }
336
337   string phonetic_seq_string = GetPhoneticLetter(sequence);  // Add the sequence letter
338   transmission += phonetic_seq_string + BRK;
339
340   genTimeInfo();
341
342   genWindInfo();
343
344   // Sounds better with a pause in there:
345   transmission += PAUSE;
346
347   genCloudInfo();
348
349   double Tsl;
350   genTemperatureInfo(Tsl, US_CA);
351
352   genVisibilityInfo();
353
354   genPressureInfo(US_CA, Tsl);
355
356   if (_type == ATIS /* as opposed to AWOS */) {
357       genRunwayInfo(phonetic_seq_string);
358   }
359
360   // Pause in between two messages must be 3-5 seconds
361   transmission += PAUSE + PAUSE + PAUSE + PAUSE;
362
363   /////////////////////////////////////////////////////////
364   // postprocessing
365   /////////////////////////////////////////////////////////
366   transmission_readable = transmission;
367
368   // Take the previous readable string and munge it to
369   // be relatively-more acceptable to the primitive tts system.
370   // Note that : ; and . are among the token-delimiters recognized
371   // by the tts system.
372   for (size_t where;;) {
373     where = transmission.find_first_of(":.");
374     if (where == string::npos) break;
375     transmission.replace(where, 1, PAUSE);
376   }
377
378   return 1;
379 }
380
381 void FGATIS::genTimeInfo(void)
382 {
383     using namespace atmodel;
384     using namespace lex;
385
386     string hours, mins;
387     string time_str = fgGetString("sim/time/gmt-string");
388
389     // Warning - this is fragile if the time string format changes
390     hours = time_str.substr(0,2).c_str();
391     mins  = time_str.substr(3,2).c_str();
392
393     // speak each digit separately:
394     transmission += ConvertNumToSpokenDigits(hours + mins);
395     transmission += " " + zulu + " " + weather + BRK;
396 }
397
398 void FGATIS::genVisibilityInfo(void)
399 {
400     using namespace atmodel;
401     using namespace lex;
402     const int bs(100);
403     char buf[bs];
404
405     transmission += Visibility + ": ";
406     double visibility = fgGetDouble("/environment/config/boundary/entry[0]/visibility-m");
407     visibility /= atmodel::sm;    // convert to statute miles
408     if (visibility < 0.25) {
409       transmission += less_than_one_quarter;
410     } else if (visibility < 0.5) {
411       transmission += one_quarter;
412     } else if (visibility < 0.75) {
413       transmission += one_half;
414     } else if (visibility < 1.0) {
415       transmission += three_quarters;
416     } else if (visibility >= 1.5 && visibility < 2.0) {
417       transmission += one_and_one_half;
418     } else {
419       // integer miles
420       if (visibility > 10) visibility = 10;
421       sprintf(buf, "%i", int(.5 + visibility));
422       transmission += ConvertNumToSpokenDigits(buf);
423     }
424     transmission += BRK;
425 }
426
427 void FGATIS::genTemperatureInfo(double& Tsl, bool US_CA)
428 {
429     using namespace atmodel;
430     using namespace lex;
431     const int bs(100);
432     char buf[bs];
433
434     transmission += Temperature + ": ";
435     Tsl = fgGetDouble("/environment/temperature-sea-level-degc");
436     int temp = int(SGMiscd::round(FGAtmo().fake_T_vs_a_us(_geod.getElevationFt(), Tsl)));
437     if(temp < 0) {
438         transmission += lex::minus + " ";
439     }
440     snprintf(buf, bs, "%i", abs(temp));
441     transmission += ConvertNumToSpokenDigits(buf);
442     if (US_CA) transmission += " " + Celsius;
443     transmission += " " + dewpoint + " ";
444     double dpsl = fgGetDouble("/environment/dewpoint-sea-level-degc");
445     temp = int(SGMiscd::round(FGAtmo().fake_dp_vs_a_us(dpsl, _geod.getElevationFt())));
446     if(temp < 0) {
447         transmission += lex::minus + " ";
448     }
449     snprintf(buf, bs, "%i", abs(temp));
450     transmission += ConvertNumToSpokenDigits(buf);
451     if (US_CA) transmission += " " + Celsius;
452     transmission += BRK;
453 }
454
455 void FGATIS::genCloudInfo(void)
456 {
457     using namespace atmodel;
458     using namespace lex;
459
460     const int bs(100);
461     char buf[bs];
462
463     bool did_some = false;
464     bool did_ceiling = false;
465
466     for (int layer = 0; layer <= 4; layer++) {
467       snprintf(buf, bs, "/environment/clouds/layer[%i]/coverage", layer);
468       string coverage = fgGetString(buf);
469       if (coverage == clear) continue;
470       snprintf(buf, bs, "/environment/clouds/layer[%i]/thickness-ft", layer);
471       if (fgGetDouble(buf) == 0) continue;
472       snprintf(buf, bs, "/environment/clouds/layer[%i]/elevation-ft", layer);
473       double ceiling = int(fgGetDouble(buf) - _geod.getElevationFt());
474       if (ceiling > 12000) continue;
475
476   // BEWARE:  At the present time, the environment system has no
477   // way (so far as I know) to represent a "thin broken" or
478   // "thin overcast" layer.  If/when such things are implemented
479   // in the environment system, code will have to be written here
480   // to handle them.
481
482   // First, do the prefix if any:
483       if (coverage == scattered || coverage == few) {
484         if (!did_some) {
485           transmission += "   " + Sky_condition + ": ";
486           did_some++;
487         }
488       } else /* must be a ceiling */  if (!did_ceiling) {
489         transmission += "   " + Ceiling + ": ";
490         did_ceiling++;
491         did_some++;
492       } else {
493         transmission += "   ";    // no prefix required
494       }
495       int cig00  = int(SGMiscd::round(ceiling/100));  // hundreds of feet
496       if (cig00) {
497         int cig000 = cig00/10;
498         cig00 -= cig000*10;       // just the hundreds digit
499         if (cig000) {
500           snprintf(buf, bs, "%i", cig000);
501           transmission += ConvertNumToSpokenDigits(buf);
502           transmission += " " + thousand + " ";
503         }
504         if (cig00) {
505           snprintf(buf, bs, "%i", cig00);
506           transmission += ConvertNumToSpokenDigits(buf);
507           transmission += " " + hundred + " ";
508         }
509       } else {
510         // Should this be "sky obscured?"
511         transmission += " " + zero + " ";     // not "zero hundred"
512       }
513       transmission += coverage + BRK;
514     }
515     if (!did_some) transmission += "   " + Sky + " " + clear + BRK;
516 }
517
518 void FGATIS::genFacilityInfo(void)
519 {
520     // SG_LOG(SG_ATC, SG_ALERT, "ATIS: facility name: " << name);
521
522     // Note that at this point, multi-word facility names
523     // will sometimes contain hyphens, not spaces.
524     vector<string> name_words;
525     boost::split(name_words, name, boost::is_any_of(" -"));
526
527     for (vector<string>::const_iterator wordp = name_words.begin();
528                   wordp != name_words.end(); wordp++) {
529       string word(*wordp);
530     // Remap some abbreviations that occur in apt.dat, to
531     // make things nicer for the text-to-speech system:
532       for (MSS::const_iterator replace = _remap.begin();
533             replace != _remap.end(); replace++) {
534         // Due to inconsistent capitalisation in the apt.dat file, we need
535         // to do a case-insensitive comparison here.
536         string tmp1 = word, tmp2 = replace->first;
537         boost::algorithm::to_lower(tmp1);
538         boost::algorithm::to_lower(tmp2);
539         if (tmp1 == tmp2) {
540           word = replace->second;
541           break;
542         }
543       }
544       transmission += word + " ";
545     }
546 }
547
548 void FGATIS::genWindInfo(void)
549 {
550     using namespace atmodel;
551     using namespace lex;
552
553     transmission += wind + ": ";
554
555     double wind_speed = fgGetDouble("/environment/config/boundary/entry[0]/wind-speed-kt");
556     double wind_dir = fgGetDouble("/environment/config/boundary/entry[0]/wind-from-heading-deg");
557     while (wind_dir <= 0) wind_dir += 360;
558     // The following isn't as bad a kludge as it might seem.
559     // It combines the magvar at the /aircraft/ location with
560     // the wind direction in the environment/config array.
561     // But if the aircraft is close enough to the station to
562     // be receiving the ATIS signal, this should be a good-enough
563     // approximation.  For more-distant aircraft, the wind_dir
564     // shouldn't be corrected anyway.
565     // The less-kludgy approach would be to use the magvar associated
566     // with the station, but that is not tabulated in the stationweather
567     // structure as it stands, and computing it would be expensive.
568     // Also note that as it stands, there is only one environment in
569     // the entire FG universe, so the aircraft environment is the same
570     // as the station environment anyway.
571     wind_dir -= fgGetDouble("/environment/magnetic-variation-deg");       // wind_dir now magnetic
572     if (wind_speed == 0) {
573     // Force west-facing rwys to be used in no-wind situations
574     // which is consistent with Flightgear's initial setup:
575         wind_dir = 270;
576         transmission += " " + light_and_variable;
577     } else {
578         const int bs(100);
579         char buf[bs];
580
581         // FIXME: get gust factor in somehow
582         snprintf(buf, bs, "%03.0f", 5*SGMiscd::round(wind_dir/5));
583         transmission += ConvertNumToSpokenDigits(buf);
584
585         snprintf(buf, bs, "%1.0f", wind_speed);
586         transmission += " " + at + " " + ConvertNumToSpokenDigits(buf) + BRK;
587     }
588 }
589
590 void FGATIS::genPressureInfo(bool US_CA, double Tsl)
591 {
592     using namespace atmodel;
593     using namespace lex;
594     double myQNH;
595     double Psl = fgGetDouble("/environment/pressure-sea-level-inhg");
596     const int bs(100);
597     char buf[bs];
598
599     {
600       double press, temp;
601
602       tie(press, temp) = PT_vs_hpt(_geod.getElevationM(), Psl*inHg, Tsl + freezing);
603   #if 0
604       SG_LOG(SG_ATC, SG_ALERT, "Field P: " << press << "  T: " << temp);
605       SG_LOG(SG_ATC, SG_ALERT, "based on elev " << elev
606                                   << "  Psl: " << Psl
607                                   << "  Tsl: " << Tsl);
608   #endif
609       myQNH = FGAtmo().QNH(_geod.getElevationM(), press);
610     }
611
612   // Convert to millibars for most of the world (not US, not CA)
613     if((!US_CA) && fgGetBool("/sim/atc/use-millibars")) {
614       transmission += QNH + ": ";
615       myQNH /= mbar;
616       snprintf(buf, bs, "%03.0f", myQNH);
617       transmission += ConvertNumToSpokenDigits(buf);
618       // TODO Extend voice samples so we can replace "millibars" with "hectopascal" (new ATIS standard since 2011)
619       if  (myQNH < 1000)
620           transmission += " " + millibars; // "hectopascal" (millibars) spoken for values below 1000 only (to avoid confusion with inHg)
621       transmission += BRK;
622     } else {
623       transmission += Altimeter + ": ";
624       double asetting = myQNH / inHg;         // use inches of mercury
625       asetting *= 100.;                       // shift two decimal places
626       snprintf(buf, bs, "%04.0f", asetting);
627       transmission += ConvertNumToSpokenDigits(buf) + BRK;
628     }
629 }
630
631 void FGATIS::genRunwayInfo(const string& phonetic_seq_string)
632 {
633     using namespace atmodel;
634     using namespace lex;
635
636     const FGAirport* apt = fgFindAirportID(ident);
637     if (apt) {
638       FGRunway* rwy = apt->getActiveRunwayForUsage();
639       if (rwy)
640       {
641           string rwy_no = rwy->ident();
642           if(rwy_no != "NN") {
643             transmission += Landing_and_departing_runway + " ";
644             transmission += ConvertRwyNumToSpokenString(rwy_no) + BRK;
645     #ifdef ATIS_TEST
646             if (msg_OK) {
647               msg_time = cur_time;
648               cout << "In atis.cxx, r.rwy_no: " << rwy_no
649                  << " wind_dir: " << wind_dir << endl;
650             }
651     #endif
652           }
653       }
654     }
655     transmission += On_initial_contact_advise_you_have_information + " ";
656     transmission += phonetic_seq_string;
657     transmission += "... " + BRK;
658 }
659
660 // Put the transmission into the property tree.
661 // You can see it by pointing a web browser
662 // at the property tree.  The second comm radio is:
663 // http://localhost:5400/instrumentation/comm[1]
664 //
665 // (Also, if in debug mode, dump it to the console.)
666 void FGATIS::TreeOut(int msg_OK)
667 {
668     _atis->setStringValue("<pre>\n" + transmission_readable + "</pre>\n");
669     SG_LOG(SG_ATC, SG_DEBUG, "**** ATIS active on: " << _name <<
670            "transmission: " << transmission_readable);
671 }
672
673
674
675 class RangeFilter : public CommStation::Filter
676 {
677 public:
678     RangeFilter( const SGGeod & pos ) :
679       CommStation::Filter(),
680       _cart(SGVec3d::fromGeod(pos)),
681       _pos(pos)
682     {
683     }
684
685     virtual bool pass(FGPositioned* aPos) const
686     {
687         flightgear::CommStation * stn = dynamic_cast<flightgear::CommStation*>(aPos);
688         if( NULL == stn )
689             return false;
690
691         // do the range check in cartesian space, since the distances are potentially
692         // large enough that the geodetic functions become unstable
693         // (eg, station on opposite side of the planet)
694         double rangeM = SGMiscd::max( stn->rangeNm(), 10.0 ) * SG_NM_TO_METER;
695         double d2 = distSqr( aPos->cart(), _cart);
696
697         return d2 <= (rangeM * rangeM);
698     }
699
700     virtual CommStation::Type minType() const
701     {
702       return CommStation::FREQ_ATIS;
703     }
704
705     virtual CommStation::Type maxType() const
706     {
707       return CommStation::FREQ_AWOS;
708     }
709
710 private:
711     SGVec3d _cart;
712     SGGeod _pos;
713 };
714
715 // Search for ATC stations by frequency
716 void FGATIS::search(void)
717 {
718     double frequency = _freq->getDoubleValue();
719
720     // Note:  122.375 must be rounded DOWN to 122370
721     // in order to be consistent with apt.dat et cetera.
722     int freqKhz = 10 * static_cast<int>(frequency * 100 + 0.25);
723
724     // throttle frequency searches
725     if ((freqKhz == _last_frequency)&&(_time_before_search_sec > 0))
726         return;
727
728     _last_frequency = freqKhz;
729     _time_before_search_sec = 4.0;
730
731     // Position of the Users Aircraft
732     SGGeod aircraftPos = SGGeod::fromDegFt(_lon_node->getDoubleValue(),
733                                            _lat_node->getDoubleValue(),
734                                            _elev_node->getDoubleValue());
735
736     RangeFilter rangeFilter(aircraftPos );
737     CommStation* sta = CommStation::findByFreq(freqKhz, aircraftPos, &rangeFilter );
738     SetStation(sta);
739     if (sta && sta->airport())
740     {
741         SG_LOG(SG_ATC, SG_DEBUG, "FGATIS " << _name << ": " << sta->airport()->name());
742     }
743     else
744     {
745         SG_LOG(SG_ATC, SG_DEBUG, "FGATIS " << _name << ": no station.");
746     }
747 }