1 /*
2 Encode an ATIS into spoken words
3 Copyright (C) 2014 Torsten Dreyer
4 
5 This program is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License
7 as published by the Free Software Foundation; either version 2
8 of the License, or (at your option) any later version.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14 
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18 */
19 
20 #include "ATISEncoder.hxx"
21 #include <Airports/dynamics.hxx>
22 #include <Main/globals.hxx>
23 #include <Main/locale.hxx>
24 #include <simgear/structure/exception.hxx>
25 #include <simgear/props/props_io.hxx>
26 
27 #include <map>
28 #include <vector>
29 
30 using std::string;
31 using std::vector;
32 using simgear::PropertyList;
33 
34 static string NO_ATIS("nil");
35 static string EMPTY("");
36 #define SPACE append(1,' ')
37 
getSpokenDigit(int i)38 std::string ATCSpeech::getSpokenDigit( int i )
39 {
40   string key = "n" + std::to_string(i);
41   return globals->get_locale()->getLocalizedString(key.c_str(), "atc", "" );
42 }
43 
getSpokenNumber(string number)44 string ATCSpeech::getSpokenNumber( string number )
45 {
46   string result;
47   for( string::iterator it = number.begin(); it != number.end(); ++it ) {
48     if( false == result.empty() )
49       result.SPACE;
50     result.append( getSpokenDigit( (*it) - '0' ));
51   }
52   return result;
53 }
54 
getSpokenNumber(int number,bool leadingZero,int digits)55 string ATCSpeech::getSpokenNumber( int number, bool leadingZero, int digits )
56 {
57   string_list spokenDigits;
58   bool negative = false;
59   if( number < 0 ) {
60     negative = true;
61     number = -number;
62   }
63 
64   int n = 0;
65   while( number > 0 ) {
66     spokenDigits.push_back( getSpokenDigit(number%10) );
67     number /= 10;
68     n++;
69   }
70 
71   if( digits > 0 ) {
72     while( n++ < digits ) {
73       spokenDigits.push_back( getSpokenDigit(0) );
74     }
75   }
76 
77   string result;
78   if( negative ) {
79     result.append( globals->get_locale()->getLocalizedString("minus", "atc", "minus" ) );
80   }
81 
82   while( false == spokenDigits.empty() ) {
83     if( false == result.empty() )
84       result.SPACE;
85 
86     result.append( spokenDigits.back() );
87     spokenDigits.pop_back();
88   }
89 
90   return result;
91 }
92 
getSpokenAltitude(int altitude)93 string ATCSpeech::getSpokenAltitude( int altitude )
94 {
95   string result;
96   int thousands = altitude / 1000;
97   int hundrets = (altitude % 1000) / 100;
98 
99   if( thousands > 0 ) {
100     result.append( getSpokenNumber(thousands) );
101     result.SPACE;
102     result.append( getSpokenDigit(1000) );
103     result.SPACE;
104   }
105   if( hundrets > 0 )
106       result.append( getSpokenNumber(hundrets) )
107             .SPACE
108             .append( getSpokenDigit(100) );
109 
110   return result;
111 }
112 
ATISEncoder()113 ATISEncoder::ATISEncoder()
114 {
115   handlerMap.insert( std::make_pair( "text", &ATISEncoder::processTextToken ));
116   handlerMap.insert( std::make_pair( "token", &ATISEncoder::processTokenToken ));
117   handlerMap.insert( std::make_pair( "if", &ATISEncoder::processIfToken ));
118   handlerMap.insert( std::make_pair( "section", &ATISEncoder::processTokens ));
119 
120   handlerMap.insert( std::make_pair( "id", &ATISEncoder::getAtisId ));
121   handlerMap.insert( std::make_pair( "airport-name", &ATISEncoder::getAirportName ));
122   handlerMap.insert( std::make_pair( "time", &ATISEncoder::getTime ));
123   handlerMap.insert( std::make_pair( "approach-type", &ATISEncoder::getApproachType ));
124   handlerMap.insert( std::make_pair( "rwy-land", &ATISEncoder::getLandingRunway ));
125   handlerMap.insert( std::make_pair( "rwy-to", &ATISEncoder::getTakeoffRunway ));
126   handlerMap.insert( std::make_pair( "transition-level", &ATISEncoder::getTransitionLevel ));
127   handlerMap.insert( std::make_pair( "wind-dir", &ATISEncoder::getWindDirection ));
128   handlerMap.insert( std::make_pair( "wind-from", &ATISEncoder::getWindMinDirection ));
129   handlerMap.insert( std::make_pair( "wind-to", &ATISEncoder::getWindMaxDirection ));
130   handlerMap.insert( std::make_pair( "wind-speed-kn", &ATISEncoder::getWindspeedKnots ));
131   handlerMap.insert( std::make_pair( "gusts", &ATISEncoder::getGustsKnots ));
132   handlerMap.insert( std::make_pair( "visibility-metric", &ATISEncoder::getVisibilityMetric ));
133   handlerMap.insert( std::make_pair( "visibility-miles", &ATISEncoder::getVisibilityMiles ));
134   handlerMap.insert( std::make_pair( "phenomena", &ATISEncoder::getPhenomena ));
135   handlerMap.insert( std::make_pair( "clouds", &ATISEncoder::getClouds ));
136   handlerMap.insert( std::make_pair( "clouds-brief", &ATISEncoder::getCloudsBrief ));
137   handlerMap.insert( std::make_pair( "cavok", &ATISEncoder::getCavok ));
138   handlerMap.insert( std::make_pair( "temperature-deg", &ATISEncoder::getTemperatureDeg ));
139   handlerMap.insert( std::make_pair( "dewpoint-deg", &ATISEncoder::getDewpointDeg ));
140   handlerMap.insert( std::make_pair( "qnh", &ATISEncoder::getQnh ));
141   handlerMap.insert( std::make_pair( "inhg", &ATISEncoder::getInhg ));
142   handlerMap.insert( std::make_pair( "inhg-integer", &ATISEncoder::getInhgInteger ));
143   handlerMap.insert( std::make_pair( "inhg-fraction", &ATISEncoder::getInhgFraction ));
144   handlerMap.insert( std::make_pair( "trend", &ATISEncoder::getTrend ));
145 }
146 
~ATISEncoder()147 ATISEncoder::~ATISEncoder()
148 {
149 }
150 
findAtisTemplate(const std::string & stationId,SGPropertyNode_ptr atisSchemaNode)151 SGPropertyNode_ptr findAtisTemplate( const std::string & stationId, SGPropertyNode_ptr atisSchemaNode )
152 {
153   using simgear::strutils::starts_with;
154   SGPropertyNode_ptr atisTemplate;
155 
156   PropertyList schemaNodes = atisSchemaNode->getChildren("atis-schema");
157   for( PropertyList::iterator asit = schemaNodes.begin(); asit != schemaNodes.end(); ++asit ) {
158     SGPropertyNode_ptr ppp = (*asit)->getNode("station-starts-with", false );
159     atisTemplate = (*asit)->getNode("atis", false );
160     if( false == atisTemplate.valid() ) continue; // no <atis> node - ignore entry
161 
162     PropertyList startsWithNodes = (*asit)->getChildren("station-starts-with");
163     for( PropertyList::iterator swit = startsWithNodes.begin(); swit != startsWithNodes.end(); ++swit ) {
164 
165       if( starts_with( stationId,  (*swit)->getStringValue() ) ) {
166         return atisTemplate;
167       }
168     }
169 
170   }
171 
172   return atisTemplate;
173 }
174 
encodeATIS(ATISInformationProvider * atisInformation)175 string ATISEncoder::encodeATIS( ATISInformationProvider * atisInformation )
176 {
177   using simgear::strutils::lowercase;
178 
179   if( false == atisInformation->isValid() ) return NO_ATIS;
180 
181   airport = FGAirport::getByIdent( atisInformation->airportId() );
182   if( false == airport.valid() ) {
183     SG_LOG( SG_ATC, SG_WARN, "ATISEncoder: unknown airport id " << atisInformation->airportId() );
184     return NO_ATIS;
185   }
186 
187   _atis = atisInformation;
188 
189   // lazily load the schema file on the first call
190   if( false == atisSchemaNode.valid() ) {
191     atisSchemaNode = new SGPropertyNode();
192     try
193     {
194       SGPath path = globals->resolve_maybe_aircraft_path("ATC/atis.xml");
195       readProperties( path, atisSchemaNode );
196     }
197     catch (const sg_exception& e)
198     {
199       SG_LOG( SG_ATC, SG_ALERT, "ATISEncoder: Failed to load atis schema definition: " << e.getMessage());
200       return NO_ATIS;
201     }
202   }
203 
204   string stationId = lowercase( airport->ident() );
205 
206   SGPropertyNode_ptr atisTemplate = findAtisTemplate( stationId, atisSchemaNode );;
207   if( false == atisTemplate.valid() ) {
208     SG_LOG(SG_ATC, SG_WARN, "no matching atis template for station " << stationId  );
209     return NO_ATIS; // no template for this station!?
210   }
211 
212   return processTokens( atisTemplate );
213 }
214 
processTokens(SGPropertyNode_ptr node)215 string ATISEncoder::processTokens( SGPropertyNode_ptr node )
216 {
217   string result;
218   if( node.valid() ) {
219     for( int i = 0; i < node->nChildren(); i++ ) {
220       result.append(processToken( node->getChild(i) ));
221     }
222   }
223   return result;
224 }
225 
processToken(SGPropertyNode_ptr token)226 string ATISEncoder::processToken( SGPropertyNode_ptr token )
227 {
228   HandlerMap::iterator it = handlerMap.find( token->getName());
229   if( it == handlerMap.end() ) {
230     SG_LOG(SG_ATC, SG_WARN, "ATISEncoder: unknown token: " << token->getName() );
231     return EMPTY;
232   }
233   handler_t h = it->second;
234   return (this->*h)( token );
235 }
236 
processTextToken(SGPropertyNode_ptr token)237 string ATISEncoder::processTextToken( SGPropertyNode_ptr token )
238 {
239   return token->getStringValue();
240 }
241 
processTokenToken(SGPropertyNode_ptr token)242 string ATISEncoder::processTokenToken( SGPropertyNode_ptr token )
243 {
244   HandlerMap::iterator it = handlerMap.find( token->getStringValue());
245   if( it == handlerMap.end() ) {
246     SG_LOG(SG_ATC, SG_WARN, "ATISEncoder: unknown token: " << token->getStringValue() );
247     return EMPTY;
248   }
249   handler_t h = it->second;
250   return (this->*h)( token );
251 
252   token->getStringValue();
253 }
254 
processIfToken(SGPropertyNode_ptr token)255 string ATISEncoder::processIfToken( SGPropertyNode_ptr token )
256 {
257   using namespace simgear::strutils;
258 
259   SGPropertyNode_ptr n;
260 
261   if( (n = token->getChild("empty", false )).valid() ) {
262     return checkEmptyCondition( n, true) ?
263            processTokens(token->getChild("then",false)) :
264            processTokens(token->getChild("else",false));
265   }
266 
267   if( (n = token->getChild("not-empty", false )).valid() ) {
268     return checkEmptyCondition( n, false) ?
269            processTokens(token->getChild("then",false)) :
270            processTokens(token->getChild("else",false));
271   }
272 
273   if( (n = token->getChild("contains", false )).valid() ) {
274     return checkCondition( n, true, &contains, "contains") ?
275            processTokens(token->getChild("then",false)) :
276            processTokens(token->getChild("else",false));
277   }
278 
279   if( (n = token->getChild("not-contains", false )).valid() ) {
280     return checkCondition( n, false, &contains, "not-contains") ?
281            processTokens(token->getChild("then",false)) :
282            processTokens(token->getChild("else",false));
283   }
284 
285   if( (n = token->getChild("ends-with", false )).valid() ) {
286     return checkCondition( n, true, &ends_with, "ends-with") ?
287            processTokens(token->getChild("then",false)) :
288            processTokens(token->getChild("else",false));
289   }
290 
291   if( (n = token->getChild("not-ends-with", false )).valid() ) {
292     return checkCondition( n, false, &ends_with, "not-ends-with") ?
293            processTokens(token->getChild("then",false)) :
294            processTokens(token->getChild("else",false));
295   }
296 
297   if( (n = token->getChild("equals", false )).valid() ) {
298     return checkCondition( n, true, &equals, "equals") ?
299            processTokens(token->getChild("then",false)) :
300            processTokens(token->getChild("else",false));
301   }
302 
303   if( (n = token->getChild("not-equals", false )).valid() ) {
304     return checkCondition( n, false, &equals, "not-equals") ?
305            processTokens(token->getChild("then",false)) :
306            processTokens(token->getChild("else",false));
307   }
308 
309   if( (n = token->getChild("starts-with", false )).valid() ) {
310     return checkCondition( n, true, &starts_with, "starts-with") ?
311            processTokens(token->getChild("then",false)) :
312            processTokens(token->getChild("else",false));
313   }
314 
315   if( (n = token->getChild("not-starts-with", false )).valid() ) {
316     return checkCondition( n, false, &starts_with, "not-starts-with") ?
317            processTokens(token->getChild("then",false)) :
318            processTokens(token->getChild("else",false));
319   }
320 
321   SG_LOG(SG_ATC, SG_WARN, "ATISEncoder: no valid token found for <if> element" );
322 
323   return EMPTY;
324 }
325 
checkEmptyCondition(SGPropertyNode_ptr node,bool isEmpty)326 bool ATISEncoder::checkEmptyCondition( SGPropertyNode_ptr node, bool isEmpty )
327 {
328   SGPropertyNode_ptr n1 = node->getNode( "token", false );
329   if( false == n1.valid() ) {
330       SG_LOG(SG_ATC, SG_WARN, "missing <token> node for (not)-empty"  );
331       return false;
332   }
333   return processToken( n1 ).empty() == isEmpty;
334 }
335 
checkCondition(SGPropertyNode_ptr node,bool notInverted,bool (* fp)(const string &,const string &),const string & name)336 bool ATISEncoder::checkCondition( SGPropertyNode_ptr node, bool notInverted,
337     bool (*fp)(const string &, const string &), const string &name )
338 {
339   using namespace simgear::strutils;
340 
341   SGPropertyNode_ptr n1 = node->getNode( "token", 0, false );
342   SGPropertyNode_ptr n2 = node->getNode( "token", 1, false );
343 
344   if( n1.valid() && n2.valid() ) {
345     bool comp = fp( processToken( n1 ), processToken( n2 ) );
346     return comp == notInverted;
347   }
348 
349   if( n1.valid() && !n2.valid() ) {
350     SGPropertyNode_ptr t1 = node->getNode( "text", 0, false );
351     if( t1.valid() ) {
352       string n1s = lowercase( strip( processToken( n1 ) ) );
353       string t1s = lowercase( strip( processTextToken( t1 ) ) );
354       return fp( n1s, t1s ) == notInverted;
355     }
356     SG_LOG(SG_ATC, SG_WARN, "missing <token> or <text> node for " << name);
357     return false;
358   }
359 
360   SG_LOG(SG_ATC, SG_WARN, "missing <token> node for " << name);
361   return false;
362 }
363 
getAtisId(SGPropertyNode_ptr)364 string ATISEncoder::getAtisId( SGPropertyNode_ptr )
365 {
366   FGAirportDynamics * dynamics = airport->getDynamics();
367   if( NULL != dynamics ) {
368     dynamics->updateAtisSequence( 30*60, false );
369     return dynamics->getAtisSequence();
370   }
371   return EMPTY;
372 }
373 
getAirportName(SGPropertyNode_ptr)374 string ATISEncoder::getAirportName( SGPropertyNode_ptr )
375 {
376   return airport->getName();
377 }
378 
getTime(SGPropertyNode_ptr)379 string ATISEncoder::getTime( SGPropertyNode_ptr )
380 {
381   return getSpokenNumber( _atis->getTime() % (100*100), true, 4 );
382 }
383 
findBestRunwayForWind(FGAirportRef airport,int windDeg,int windKt)384 static inline FGRunwayRef findBestRunwayForWind( FGAirportRef airport, int windDeg, int windKt )
385 {
386   struct FGAirport::FindBestRunwayForHeadingParams p;
387   //TODO: ramp down the heading weight with wind speed
388   p.ilsWeight = 4;
389   return airport->findBestRunwayForHeading( windDeg, &p );
390 }
391 
getApproachType(SGPropertyNode_ptr)392 string ATISEncoder::getApproachType( SGPropertyNode_ptr )
393 {
394   FGRunwayRef runway = findBestRunwayForWind( airport, _atis->getWindDeg(), _atis->getWindSpeedKt() );
395   if( runway.valid() ) {
396     if( NULL != runway->ILS() ) return globals->get_locale()->getLocalizedString("ils", "atc", "ils" );
397     //TODO: any chance to find other approach types? localizer-dme, vor-dme, vor, ndb?
398   }
399 
400   return globals->get_locale()->getLocalizedString("visual", "atc", "visual" );
401 }
402 
getLandingRunway(SGPropertyNode_ptr)403 string ATISEncoder::getLandingRunway( SGPropertyNode_ptr )
404 {
405   FGRunwayRef runway = findBestRunwayForWind( airport, _atis->getWindDeg(), _atis->getWindSpeedKt() );
406   if( runway.valid() ) {
407     string runwayIdent = runway->ident();
408     if(runwayIdent != "NN") {
409       return getSpokenNumber(runwayIdent);
410     }
411   }
412   return EMPTY;
413 }
414 
getTakeoffRunway(SGPropertyNode_ptr p)415 string ATISEncoder::getTakeoffRunway( SGPropertyNode_ptr p )
416 {
417   //TODO: if the airport has more than one runway, probably pick another one?
418   return getLandingRunway( p );
419 }
420 
getTransitionLevel(SGPropertyNode_ptr)421 string ATISEncoder::getTransitionLevel( SGPropertyNode_ptr )
422 {
423   double hPa = _atis->getQnh();
424 
425   /* Transition level is the flight level above which aircraft must use standard pressure and below
426    * which airport pressure settings must be used.
427    * Following definitions are taken from German ATIS:
428    *      QNH <=  977 hPa: TRL 80
429    *      QNH <= 1013 hPa: TRL 70
430    *      QNH >  1013 hPa: TRL 60
431    * (maybe differs slightly for other countries...)
432    */
433   int tl;
434   if (hPa <= 978) {
435     tl = 80;
436   } else if( hPa > 978 && hPa <= 1013 ) {
437     tl = 70;
438   } else if( hPa > 1013 && hPa <= 1046 ) {
439     tl = 60;
440   } else {
441     tl = 50;
442   }
443 
444   // add an offset to the transition level for high altitude airports (just guessing here,
445   // seems reasonable)
446   int e = int(airport->getElevation() / 1000.0);
447   if (e >= 3) {
448     // TL steps in 10(00)ft
449     tl += (e-2)*10;
450   }
451 
452   return getSpokenNumber(tl);
453 }
454 
getWindDirection(SGPropertyNode_ptr)455 string ATISEncoder::getWindDirection( SGPropertyNode_ptr )
456 {
457   string variable = globals->get_locale()->getLocalizedString("variable", "atc", "variable" );
458 
459   bool vrb = _atis->getWindMinDeg() == 0 && _atis->getWindMaxDeg() == 359;
460   return vrb ? variable : getSpokenNumber( _atis->getWindDeg(), true, 3 );
461 }
462 
getWindMinDirection(SGPropertyNode_ptr)463 string ATISEncoder::getWindMinDirection( SGPropertyNode_ptr )
464 {
465   return getSpokenNumber( _atis->getWindMinDeg(), true, 3 );
466 }
467 
getWindMaxDirection(SGPropertyNode_ptr)468 string ATISEncoder::getWindMaxDirection( SGPropertyNode_ptr )
469 {
470   return getSpokenNumber( _atis->getWindMaxDeg(), true, 3 );
471 }
472 
getWindspeedKnots(SGPropertyNode_ptr)473 string ATISEncoder::getWindspeedKnots( SGPropertyNode_ptr )
474 {
475   return getSpokenNumber( _atis->getWindSpeedKt() );
476 }
477 
getGustsKnots(SGPropertyNode_ptr)478 string ATISEncoder::getGustsKnots( SGPropertyNode_ptr )
479 {
480   int g = _atis->getGustsKt();
481   return g > 0 ? getSpokenNumber( g ) : EMPTY;
482 }
483 
getCavok(SGPropertyNode_ptr)484 string ATISEncoder::getCavok( SGPropertyNode_ptr )
485 {
486   string CAVOK = globals->get_locale()->getLocalizedString("cavok", "atc", "cavok" );
487 
488   return _atis->isCavok() ? CAVOK : EMPTY;
489 }
490 
getVisibilityMetric(SGPropertyNode_ptr)491 string ATISEncoder::getVisibilityMetric( SGPropertyNode_ptr )
492 {
493   string m = globals->get_locale()->getLocalizedString("meters", "atc", "meters" );
494   string km = globals->get_locale()->getLocalizedString("kilometers", "atc", "kilometers" );
495   string or_more = globals->get_locale()->getLocalizedString("ormore", "atc", "or more" );
496 
497   int v = _atis->getVisibilityMeters();
498   string reply;
499   if( v < 5000 ) return reply.append( getSpokenAltitude( v ) ).SPACE.append( m );
500   if( v >= 9999 ) return reply.append( getSpokenNumber(10) ).SPACE.append( km ).SPACE.append(or_more);
501   return reply.append( getSpokenNumber( v/1000 ).SPACE.append( km ) );
502 }
503 
getVisibilityMiles(SGPropertyNode_ptr)504 string ATISEncoder::getVisibilityMiles( SGPropertyNode_ptr )
505 {
506   string feet = globals->get_locale()->getLocalizedString("feet", "atc", "feet" );
507 
508   int v = _atis->getVisibilityMeters();
509   int vft = int( v * SG_METER_TO_FEET / 100 + 0.5 ) * 100; // Rounded to 100 ft
510   int vsm = int( v * SG_METER_TO_SM + 0.5 );
511 
512   string reply;
513   if( vsm < 1 ) return reply.append( getSpokenAltitude( vft ) ).SPACE.append( feet );
514   if( vsm >= 10 ) return reply.append( getSpokenNumber(10) );
515   return reply.append( getSpokenNumber( vsm ) );
516 }
517 
getPhenomena(SGPropertyNode_ptr)518 string ATISEncoder::getPhenomena( SGPropertyNode_ptr )
519 {
520   return _atis->getPhenomena();
521 }
522 
getClouds(SGPropertyNode_ptr)523 string ATISEncoder::getClouds( SGPropertyNode_ptr )
524 {
525   string FEET =  globals->get_locale()->getLocalizedString("feet", "atc", "feet" );
526   string reply;
527 
528   ATISInformationProvider::CloudEntries cloudEntries = _atis->getClouds();
529 
530   for( ATISInformationProvider::CloudEntries::iterator it = cloudEntries.begin(); it != cloudEntries.end(); it++ ) {
531     if( false == reply.empty() ) reply.SPACE;
532     reply.append( it->second ).SPACE.append( getSpokenAltitude(it->first).SPACE.append( FEET ) );
533   }
534   return reply;
535 }
536 
getCloudsBrief(SGPropertyNode_ptr)537 string ATISEncoder::getCloudsBrief( SGPropertyNode_ptr )
538 {
539   string reply;
540 
541   ATISInformationProvider::CloudEntries cloudEntries = _atis->getClouds();
542 
543   for( ATISInformationProvider::CloudEntries::iterator it = cloudEntries.begin(); it != cloudEntries.end(); it++ ) {
544     if( false == reply.empty() ) reply.append(",").SPACE;
545     reply.append( it->second ).SPACE.append( getSpokenAltitude(it->first) );
546   }
547   return reply;
548 }
549 
getTemperatureDeg(SGPropertyNode_ptr)550 string ATISEncoder::getTemperatureDeg( SGPropertyNode_ptr )
551 {
552   return getSpokenNumber( _atis->getTemperatureDeg() );
553 }
554 
getDewpointDeg(SGPropertyNode_ptr)555 string ATISEncoder::getDewpointDeg( SGPropertyNode_ptr )
556 {
557   return getSpokenNumber( _atis->getDewpointDeg() );
558 }
559 
getQnh(SGPropertyNode_ptr)560 string ATISEncoder::getQnh( SGPropertyNode_ptr )
561 {
562   return getSpokenNumber( _atis->getQnh() );
563 }
564 
getInhgInteger(SGPropertyNode_ptr)565 string ATISEncoder::getInhgInteger( SGPropertyNode_ptr )
566 {
567   double qnh = _atis->getQnhInHg();
568   return getSpokenNumber( (int)qnh, true, 2 );
569 }
570 
getInhgFraction(SGPropertyNode_ptr)571 string ATISEncoder::getInhgFraction( SGPropertyNode_ptr )
572 {
573   double qnh = _atis->getQnhInHg();
574   int f = int(100 * (qnh - int(qnh)) + 0.5);
575   return getSpokenNumber( f, true, 2 );
576 }
577 
getInhg(SGPropertyNode_ptr node)578 string ATISEncoder::getInhg( SGPropertyNode_ptr node)
579 {
580   string DECIMAL = globals->get_locale()->getLocalizedString("dp", "atc", "decimal" );
581   return getInhgInteger(node)
582     .SPACE.append(DECIMAL).SPACE
583     .append(getInhgFraction(node));
584 }
585 
getTrend(SGPropertyNode_ptr)586 string ATISEncoder::getTrend( SGPropertyNode_ptr )
587 {
588   return _atis->getTrend();
589 }
590 
591