2 Encode an ATIS into spoken words
3 Copyright (C) 2014 Torsten Dreyer
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.
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.
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.
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 #include <boost/lexical_cast.hpp>
33 using simgear::PropertyList;
35 static string NO_ATIS("nil");
36 static string EMPTY("");
37 #define SPACE append(1,' ')
39 const char * ATCSpeech::getSpokenDigit( int i )
41 string key = "n" + boost::lexical_cast<std::string>( i );
42 return globals->get_locale()->getLocalizedString(key.c_str(), "atc", "" );
45 string ATCSpeech::getSpokenNumber( string number )
48 for( string::iterator it = number.begin(); it != number.end(); ++it ) {
49 result.append( getSpokenDigit( (*it) - '0' )).SPACE;
54 string ATCSpeech::getSpokenNumber( int number, bool leadingZero, int digits )
56 vector<const char *> spokenDigits;
59 spokenDigits.push_back( getSpokenDigit(number%10) );
65 while( n++ < digits ) {
66 spokenDigits.push_back( getSpokenDigit(0) );
71 while( false == spokenDigits.empty() ) {
72 if( false == spokenDigits.empty() )
75 result.append( spokenDigits.back() );
76 spokenDigits.pop_back();
82 string ATCSpeech::getSpokenAltitude( int altitude )
85 int thousands = altitude / 1000;
86 int hundrets = (altitude % 1000) / 100;
89 result.append( getSpokenNumber(thousands) );
91 result.append( getSpokenDigit(1000) );
95 result.append( getSpokenNumber(hundrets) )
97 .append( getSpokenDigit(100) );
102 ATISEncoder::ATISEncoder()
104 handlerMap.insert( std::make_pair( "text", &ATISEncoder::processTextToken ));
105 handlerMap.insert( std::make_pair( "token", &ATISEncoder::processTokenToken ));
106 handlerMap.insert( std::make_pair( "if", &ATISEncoder::processIfToken ));
108 handlerMap.insert( std::make_pair( "id", &ATISEncoder::getAtisId ));
109 handlerMap.insert( std::make_pair( "airport-name", &ATISEncoder::getAirportName ));
110 handlerMap.insert( std::make_pair( "time", &ATISEncoder::getTime ));
111 handlerMap.insert( std::make_pair( "approach-type", &ATISEncoder::getApproachType ));
112 handlerMap.insert( std::make_pair( "rwy-land", &ATISEncoder::getLandingRunway ));
113 handlerMap.insert( std::make_pair( "rwy-to", &ATISEncoder::getTakeoffRunway ));
114 handlerMap.insert( std::make_pair( "transition-level", &ATISEncoder::getTransitionLevel ));
115 handlerMap.insert( std::make_pair( "wind-dir", &ATISEncoder::getWindDirection ));
116 handlerMap.insert( std::make_pair( "wind-speed-kn", &ATISEncoder::getWindspeedKnots ));
117 handlerMap.insert( std::make_pair( "gusts", &ATISEncoder::getGustsKnots ));
118 handlerMap.insert( std::make_pair( "visibility-metric", &ATISEncoder::getVisibilityMetric ));
119 handlerMap.insert( std::make_pair( "phenomena", &ATISEncoder::getPhenomena ));
120 handlerMap.insert( std::make_pair( "clouds", &ATISEncoder::getClouds ));
121 handlerMap.insert( std::make_pair( "cavok", &ATISEncoder::getCavok ));
122 handlerMap.insert( std::make_pair( "temperature-deg", &ATISEncoder::getTemperatureDeg ));
123 handlerMap.insert( std::make_pair( "dewpoint-deg", &ATISEncoder::getDewpointDeg ));
124 handlerMap.insert( std::make_pair( "qnh", &ATISEncoder::getQnh ));
125 handlerMap.insert( std::make_pair( "inhg", &ATISEncoder::getInhg ));
126 handlerMap.insert( std::make_pair( "trend", &ATISEncoder::getTrend ));
129 ATISEncoder::~ATISEncoder()
133 SGPropertyNode_ptr findAtisTemplate( const std::string & stationId, SGPropertyNode_ptr atisSchemaNode )
135 using simgear::strutils::starts_with;
136 SGPropertyNode_ptr atisTemplate;
138 PropertyList schemaNodes = atisSchemaNode->getChildren("atis-schema");
139 for( PropertyList::iterator asit = schemaNodes.begin(); asit != schemaNodes.end(); ++asit ) {
140 SGPropertyNode_ptr ppp = (*asit)->getNode("station-starts-with", false );
141 atisTemplate = (*asit)->getNode("atis", false );
142 if( false == atisTemplate.valid() ) continue; // no <atis> node - ignore entry
144 PropertyList startsWithNodes = (*asit)->getChildren("station-starts-with");
145 for( PropertyList::iterator swit = startsWithNodes.begin(); swit != startsWithNodes.end(); ++swit ) {
147 if( starts_with( stationId, (*swit)->getStringValue() ) ) {
157 string ATISEncoder::encodeATIS( ATISInformationProvider * atisInformation )
159 using simgear::strutils::lowercase;
161 if( false == atisInformation->isValid() ) return NO_ATIS;
163 airport = FGAirport::getByIdent( atisInformation->airportId() );
164 if( false == airport.valid() ) {
165 SG_LOG( SG_ATC, SG_WARN, "ATISEncoder: unknown airport id " << atisInformation->airportId() );
169 _atis = atisInformation;
171 // lazily load the schema file on the first call
172 if( false == atisSchemaNode.valid() ) {
173 atisSchemaNode = new SGPropertyNode();
176 SGPath path = globals->resolve_maybe_aircraft_path("ATC/atis.xml");
177 readProperties( path.str(), atisSchemaNode );
179 catch (const sg_exception& e)
181 SG_LOG( SG_ATC, SG_ALERT, "ATISEncoder: Failed to load atis schema definition: " << e.getMessage());
186 string stationId = lowercase( airport->ident() );
188 SGPropertyNode_ptr atisTemplate = findAtisTemplate( stationId, atisSchemaNode );;
189 if( false == atisTemplate.valid() ) {
190 SG_LOG(SG_ATC, SG_WARN, "no matching atis template for station " << stationId );
191 return NO_ATIS; // no template for this station!?
194 return processTokens( atisTemplate );
197 string ATISEncoder::processTokens( SGPropertyNode_ptr node )
201 for( int i = 0; i < node->nChildren(); i++ ) {
202 result.append(processToken( node->getChild(i) ));
208 string ATISEncoder::processToken( SGPropertyNode_ptr token )
210 HandlerMap::iterator it = handlerMap.find( token->getName());
211 if( it == handlerMap.end() ) {
212 SG_LOG(SG_ATC, SG_WARN, "ATISEncoder: unknown token: " << token->getName() );
215 handler_t h = it->second;
216 return (this->*h)( token );
219 string ATISEncoder::processTextToken( SGPropertyNode_ptr token )
221 return token->getStringValue();
224 string ATISEncoder::processTokenToken( SGPropertyNode_ptr token )
226 HandlerMap::iterator it = handlerMap.find( token->getStringValue());
227 if( it == handlerMap.end() ) {
228 SG_LOG(SG_ATC, SG_WARN, "ATISEncoder: unknown token: " << token->getStringValue() );
231 handler_t h = it->second;
232 return (this->*h)( token );
234 token->getStringValue();
237 string ATISEncoder::processIfToken( SGPropertyNode_ptr token )
239 SGPropertyNode_ptr n;
241 if( (n = token->getChild("empty", false )).valid() ) {
242 return checkEmptyCondition( n, true) ?
243 processTokens(token->getChild("then",false)) :
244 processTokens(token->getChild("else",false));
247 if( (n = token->getChild("not-empty", false )).valid() ) {
248 return checkEmptyCondition( n, false) ?
249 processTokens(token->getChild("then",false)) :
250 processTokens(token->getChild("else",false));
253 if( (n = token->getChild("equals", false )).valid() ) {
254 return checkEqualsCondition( n, true) ?
255 processTokens(token->getChild("then",false)) :
256 processTokens(token->getChild("else",false));
259 if( (n = token->getChild("not-equals", false )).valid() ) {
260 return checkEqualsCondition( n, false) ?
261 processTokens(token->getChild("then",false)) :
262 processTokens(token->getChild("else",false));
265 SG_LOG(SG_ATC, SG_WARN, "ATISEncoder: no valid token found for <if> element" );
270 bool ATISEncoder::checkEmptyCondition( SGPropertyNode_ptr node, bool isEmpty )
272 SGPropertyNode_ptr n1 = node->getNode( "token", false );
273 if( false == n1.valid() ) {
274 SG_LOG(SG_ATC, SG_WARN, "missing <token> node for (not)-empty" );
277 return processToken( n1 ).empty() == isEmpty;
280 bool ATISEncoder::checkEqualsCondition( SGPropertyNode_ptr node, bool isEqual )
282 SGPropertyNode_ptr n1 = node->getNode( "token", 0, false );
283 SGPropertyNode_ptr n2 = node->getNode( "token", 1, false );
284 if( false == n1.valid() || false == n2.valid()) {
285 SG_LOG(SG_ATC, SG_WARN, "missing <token> node for (not)-equals" );
289 bool comp = processToken( n1 ).compare( processToken( n2 ) ) == 0;
290 return comp == isEqual;
293 string ATISEncoder::getAtisId( SGPropertyNode_ptr )
295 FGAirportDynamics * dynamics = airport->getDynamics();
296 if( NULL != dynamics ) {
297 dynamics->updateAtisSequence( 30*60, false );
298 return dynamics->getAtisSequence();
303 string ATISEncoder::getAirportName( SGPropertyNode_ptr )
305 return airport->getName();
308 string ATISEncoder::getTime( SGPropertyNode_ptr )
310 return getSpokenNumber( _atis->getTime() % (100*100), true, 4 );
313 static inline FGRunwayRef findBestRunwayForWind( FGAirportRef airport, int windDeg, int windKt )
315 struct FGAirport::FindBestRunwayForHeadingParams p;
316 //TODO: ramp down the heading weight with wind speed
318 return airport->findBestRunwayForHeading( windDeg, &p );
321 string ATISEncoder::getApproachType( SGPropertyNode_ptr )
323 FGRunwayRef runway = findBestRunwayForWind( airport, _atis->getWindDeg(), _atis->getWindSpeedKt() );
324 if( runway.valid() ) {
325 if( NULL != runway->ILS() ) return globals->get_locale()->getLocalizedString("ils", "atc", "ils" );
326 //TODO: any chance to find other approach types? localizer-dme, vor-dme, vor, ndb?
329 return globals->get_locale()->getLocalizedString("visual", "atc", "visual" );
332 string ATISEncoder::getLandingRunway( SGPropertyNode_ptr )
334 FGRunwayRef runway = findBestRunwayForWind( airport, _atis->getWindDeg(), _atis->getWindSpeedKt() );
335 if( runway.valid() ) {
336 string runwayIdent = runway->ident();
337 if(runwayIdent != "NN") {
338 return getSpokenNumber(runwayIdent);
344 string ATISEncoder::getTakeoffRunway( SGPropertyNode_ptr p )
346 //TODO: if the airport has more than one runway, probably pick another one?
347 return getLandingRunway( p );
350 string ATISEncoder::getTransitionLevel( SGPropertyNode_ptr )
352 double hPa = _atis->getQnh();
354 /* Transition level is the flight level above which aircraft must use standard pressure and below
355 * which airport pressure settings must be used.
356 * Following definitions are taken from German ATIS:
357 * QNH <= 977 hPa: TRL 80
358 * QNH <= 1013 hPa: TRL 70
359 * QNH > 1013 hPa: TRL 60
360 * (maybe differs slightly for other countries...)
365 } else if( hPa > 978 && hPa <= 1013 ) {
367 } else if( hPa > 1013 && hPa <= 1046 ) {
373 // add an offset to the transition level for high altitude airports (just guessing here,
375 int e = int(airport->getElevation() / 1000.0);
377 // TL steps in 10(00)ft
381 return getSpokenNumber(tl);
384 string ATISEncoder::getWindDirection( SGPropertyNode_ptr )
386 return getSpokenNumber( _atis->getWindDeg() );
389 string ATISEncoder::getWindspeedKnots( SGPropertyNode_ptr )
391 return getSpokenNumber( _atis->getWindSpeedKt() );
394 string ATISEncoder::getGustsKnots( SGPropertyNode_ptr )
396 int g = _atis->getGustsKt();
397 return g > 0 ? getSpokenNumber( g ) : EMPTY;
400 string ATISEncoder::getCavok( SGPropertyNode_ptr )
402 string CAVOK = globals->get_locale()->getLocalizedString("cavok", "atc", "cavok" );
404 return _atis->isCavok() ? CAVOK : EMPTY;
407 string ATISEncoder::getVisibilityMetric( SGPropertyNode_ptr )
409 string m = globals->get_locale()->getLocalizedString("meters", "atc", "meters" );
410 string km = globals->get_locale()->getLocalizedString("kilometersmeters", "atc", "kilometersmeters" );
411 string or_more = globals->get_locale()->getLocalizedString("ormore", "atc", "or more" );
413 int v = _atis->getVisibilityMeters();
415 if( v < 5000 ) return reply.append( getSpokenAltitude( v ) ).SPACE.append( m );
416 if( v >= 10000 ) return reply.append( getSpokenNumber(10) ).SPACE.append( km ).SPACE.append(or_more);
417 return reply.append( getSpokenNumber( v/1000 ).append( km ) );
420 string ATISEncoder::getPhenomena( SGPropertyNode_ptr )
422 return _atis->getPhenomena();
425 string ATISEncoder::getClouds( SGPropertyNode_ptr )
427 string FEET = globals->get_locale()->getLocalizedString("feet", "atc", "feet" );
430 ATISInformationProvider::CloudEntries cloudEntries = _atis->getClouds();
432 for( ATISInformationProvider::CloudEntries::iterator it = cloudEntries.begin(); it != cloudEntries.end(); it++ ) {
433 if( false == reply.empty() ) reply.SPACE;
434 reply.append( it->second ).SPACE.append( getSpokenAltitude(it->first).SPACE.append( FEET ) );
439 string ATISEncoder::getTemperatureDeg( SGPropertyNode_ptr )
441 return getSpokenNumber( _atis->getTemperatureDeg() );
444 string ATISEncoder::getDewpointDeg( SGPropertyNode_ptr )
446 return getSpokenNumber( _atis->getDewpointDeg() );
449 string ATISEncoder::getQnh( SGPropertyNode_ptr )
451 return getSpokenNumber( _atis->getQnh() );
454 string ATISEncoder::getInhg( SGPropertyNode_ptr )
456 string DECIMAL = globals->get_locale()->getLocalizedString("dp", "atc", "decimal" );
458 int fractpart = 1000 * ::modf( _atis->getQnh() * 100.0 / SG_INHG_TO_PA, &intpart );
463 reply.append( getSpokenNumber( (int)intpart ) )
464 .append( DECIMAL ).SPACE
465 .append( getSpokenNumber( fractpart ) );
469 string ATISEncoder::getTrend( SGPropertyNode_ptr )
471 return _atis->getTrend();