FlightGear next
newnavradio.cxx
Go to the documentation of this file.
1// navradio.cxx -- class to manage a nav radio instance
2//
3// Written by Curtis Olson, started April 2000.
4// Rewritten by Torsten Dreyer, August 2011
5//
6// Copyright (C) 2000 - 2011 Curtis L. Olson - http://www.flightgear.org/~curt
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#ifdef HAVE_CONFIG_H
24# include <config.h>
25#endif
26
27#include "newnavradio.hxx"
28
29#include <assert.h>
30
31#include <simgear/math/interpolater.hxx>
32#include <simgear/sg_inlines.h>
33#include <simgear/props/propertyObject.hxx>
34#include <simgear/misc/strutils.hxx>
35#include <simgear/sound/sample_group.hxx>
36
37#include <Main/fg_props.hxx>
38#include <Navaids/navlist.hxx>
39#include <Sound/audioident.hxx>
40
41#include "navradio.hxx"
43
44namespace Instrumentation {
45
46using namespace std::string_literals;
47using simgear::PropertyObject;
48
49/* --------------The Navigation Indicator ----------------------------- */
50
52public:
53 NavIndicator( SGPropertyNode * rootNode ) :
54 _cdi( rootNode->getNode("heading-needle-deflection", true ) ),
55 _cdiNorm( rootNode->getNode("heading-needle-deflection-norm", true ) ),
56 _course( rootNode->getNode("radials/selected-deg", true ) ),
57 _toFlag( rootNode->getNode("to-flag", true ) ),
58 _fromFlag( rootNode->getNode("from-flag", true ) ),
59 _signalQuality( rootNode->getNode("signal-quality-norm", true ) ),
60 _hasGS( rootNode->getNode("has-gs", true ) ),
61 _gsDeflection(rootNode->getNode("gs-needle-deflection", true )),
62 _gsDeflectionDeg(rootNode->getNode("gs-needle-deflection-deg", true )),
63 _gsDeflectionNorm(rootNode->getNode("gs-needle-deflection-norm", true ))
64 {
65 }
66
67 virtual ~NavIndicator() {}
68
73 void setCDI( double norm )
74 {
75 _cdi = norm * 10.0;
76 _cdiNorm = norm;
77 }
78
83 void setGS( double norm )
84 {
85 _gsDeflectionNorm = norm;
86 _gsDeflectionDeg = norm * 0.7;
87 _gsDeflection = norm * 3.5;
88 }
89
90 void setGS( bool enabled )
91 {
92 _hasGS = enabled;
93 if( !enabled ) {
94 setGS( 0.0 );
95 }
96 }
97
98 void showFrom( bool on )
99 {
100 _fromFlag = on;
101 }
102
103 void showTo( bool on )
104 {
105 _toFlag = on;
106 }
107
108 void setSelectedCourse( double course )
109 {
110 _course = course;
111 }
112
113 double getSelectedCourse() const
114 {
115 return SGMiscd::normalizePeriodic(0.0, 360.0, _course );
116 }
117
118 void setSignalQuality( double signalQuality )
119 {
120 _signalQuality = signalQuality;
121 }
122
123private:
124 PropertyObject<double> _cdi;
125 PropertyObject<double> _cdiNorm;
126 PropertyObject<double> _course;
127 PropertyObject<double> _toFlag;
128 PropertyObject<double> _fromFlag;
129 PropertyObject<double> _signalQuality;
130 PropertyObject<double> _hasGS;
131 PropertyObject<double> _gsDeflection;
132 PropertyObject<double> _gsDeflectionDeg;
133 PropertyObject<double> _gsDeflectionNorm;
134};
135
136/* ---------------------------------------------------------------- */
137
139public:
140 NavRadioComponent( const std::string & name, SGPropertyNode_ptr rootNode );
141 virtual ~NavRadioComponent();
142
143 virtual void update( double dt, const SGGeod & aircraftPosition );
144 virtual void search( double frequency, const SGGeod & aircraftPosition );
145 virtual double getRange_nm( const SGGeod & aircraftPosition );
146 virtual void display( NavIndicator & navIndicator ) = 0;
147 virtual bool valid() const { return NULL != _navRecord && _serviceable; }
148 virtual const std::string getIdent() const { return _ident; }
149
150protected:
151 virtual double computeSignalQuality_norm( const SGGeod & aircraftPosition );
153
154 // General-purpose sawtooth function. Graph looks like this:
155 // /\ .
156 // \/
157 // Odd symmetry, inversion symmetry about the origin.
158 // Unit slope at the origin.
159 // Max 1, min -1, period 4.
160 // Two zero-crossings per period, one with + slope, one with - slope.
161 // Useful for false localizer courses.
162 static double sawtooth(double xx)
163 {
164 return 4.0 * fabs(xx/4.0 + 0.25 - floor(xx/4.0 + 0.75)) - 1.0;
165 }
166
167 SGPropertyNode_ptr _rootNode;
168 const std::string _name;
170 PropertyObject<bool> _serviceable;
171 PropertyObject<double> _signalQuality_norm;
172 PropertyObject<double> _trueBearingTo_deg;
173 PropertyObject<double> _trueBearingFrom_deg;
174 PropertyObject<double> _trackDistance_m;
175 PropertyObject<double> _slantDistance_m;
176 PropertyObject<double> _heightAboveStation_ft;
177 PropertyObject<std::string> _ident;
178 PropertyObject<bool> _inRange;
179 PropertyObject<double> _range_nm;
180};
181
183public:
184 NavRadioComponentWithIdent( const std::string & name, SGPropertyNode_ptr rootNode, AudioIdent * audioIdent );
186 void update( double dt, const SGGeod & aircraftPosition );
187protected:
188 static std::string getIdentString( const std::string & name, int index );
189private:
190 AudioIdent * _audioIdent;
191 PropertyObject<double> _identVolume;
192 PropertyObject<bool> _identEnabled;
193};
194
195std::string NavRadioComponentWithIdent::getIdentString( const std::string & name, int index )
196{
197 std::ostringstream temp;
198 temp << name << "-ident-" << index;
199 return temp.str();
200}
201
202NavRadioComponentWithIdent::NavRadioComponentWithIdent( const std::string & name, SGPropertyNode_ptr rootNode, AudioIdent * audioIdent ) :
203 NavRadioComponent( name, rootNode ),
204 _audioIdent( audioIdent ),
205 _identVolume( rootNode->getNode(name,true)->getNode("ident-volume",true) ),
206 _identEnabled( rootNode->getNode(name,true)->getNode("ident-enabled",true) )
207{
208 _audioIdent->init();
209
210}
215
216void NavRadioComponentWithIdent::update( double dt, const SGGeod & aircraftPosition )
217{
218 NavRadioComponent::update( dt, aircraftPosition );
219 _audioIdent->update( dt );
220
221 if( !( valid() && _identEnabled && _signalQuality_norm > 0.1 ) ) {
222 _audioIdent->setIdent("", 0.0 );
223 return;
224 }
225 _audioIdent->setIdent( _ident, SGMiscd::clip(_identVolume, 0.0, 1.0) );
226}
227
228NavRadioComponent::NavRadioComponent( const std::string & name, SGPropertyNode_ptr rootNode ) :
229 _rootNode(rootNode),
230 _name(name),
231 _navRecord(NULL),
232 _serviceable( rootNode->getNode(name,true)->getNode("serviceable",true) ),
233 _signalQuality_norm( rootNode->getNode(name,true)->getNode("signal-quality-norm",true) ),
234 _trueBearingTo_deg( rootNode->getNode(name,true)->getNode("true-bearing-to-deg",true) ),
235 _trueBearingFrom_deg( rootNode->getNode(name,true)->getNode("true-bearing-from-deg",true) ),
236 _trackDistance_m( rootNode->getNode(name,true)->getNode("track-distance-m",true) ),
237 _slantDistance_m( rootNode->getNode(name,true)->getNode("slant-distance-m",true) ),
238 _heightAboveStation_ft( rootNode->getNode(name,true)->getNode("height-above-station-ft",true) ),
239 _ident( rootNode->getNode(name,true)->getNode("ident",true) ),
240 _inRange( rootNode->getNode(name,true)->getNode("in-range",true) ),
241 _range_nm( rootNode->getNode(_name,true)->getNode("range-nm",true) )
242{
243 simgear::props::Type typ = _serviceable.node()->getType();
244 if ((typ == simgear::props::NONE) || (typ == simgear::props::UNSPECIFIED))
245 _serviceable = true;
246}
247
251
252double NavRadioComponent::getRange_nm( const SGGeod & aircraftPosition )
253{
254 if( _navRecord == NULL ) return 0.0; // no station: no range
255 double d = _navRecord->get_range();
256 if( d <= SGLimitsd::min() ) return 25.0; // no configured range: arbitrary number
257 return d; // configured range
258}
259
260void NavRadioComponent::search( double frequency, const SGGeod & aircraftPosition )
261{
262 _navRecord = FGNavList::findByFreq(frequency, aircraftPosition, getNavaidFilter() );
263 if( NULL == _navRecord ) {
264 SG_LOG(SG_INSTR,SG_DEBUG, "No " << _name << " available at " << frequency );
265 _ident = "";
266 return;
267 }
268 SG_LOG(SG_INSTR, SG_INFO,
269 "Using " << _name << " '" << _navRecord->get_ident() << "' for " <<
270 frequency);
271 _ident = _navRecord->ident();
272}
273
274double NavRadioComponent::computeSignalQuality_norm( const SGGeod & aircraftPosition )
275{
276 if( !valid() ) return 0.0;
277
278 double distance_nm = _slantDistance_m * SG_METER_TO_NM;
279 double range_nm = _range_nm;
280
281 // assume signal quality is 100% up to the published range and
282 // decay with the distance squared further out
283 if ( distance_nm <= range_nm ) return 1.0;
284 return range_nm*range_nm/(distance_nm*distance_nm);
285}
286
287void NavRadioComponent::update( double dt, const SGGeod & aircraftPosition )
288{
289 if( !valid() ) {
291 _trueBearingTo_deg = 0.0;
293 _trackDistance_m = 0.0;
294 _slantDistance_m = 0.0;
295 return;
296 }
297
298 _slantDistance_m = dist(_navRecord->cart(), SGVec3d::fromGeod(aircraftPosition));
299
300 double az1 = 0.0, az2 = 0.0, dist = 0.0;
301 SGGeodesy::inverse(aircraftPosition, _navRecord->geod(), az1, az2, dist );
303 _heightAboveStation_ft = SGMiscd::max(0.0, aircraftPosition.getElevationFt() - _navRecord->get_elev_ft());
304
305 _range_nm = getRange_nm(aircraftPosition);
308}
309
310/* ---------------------------------------------------------------- */
311
312static SGPath VORTablePath( const char * name )
313{
314 SGPath path( globals->get_fg_root() );
315 path.append( "Navaids" );
316 path.append(name);
317 return path;
318}
319
321public:
322 VOR( SGPropertyNode_ptr rootNode);
323 virtual ~VOR();
324 virtual void update( double dt, const SGGeod & aircraftPosition );
325 virtual void display( NavIndicator & navIndicator );
326 virtual double getRange_nm(const SGGeod & aircraftPosition);
327protected:
328 virtual double computeSignalQuality_norm( const SGGeod & aircraftPosition );
330
331private:
332 double _totalTime;
333 class ServiceVolume {
334 public:
335 ServiceVolume() :
336 term_tbl(VORTablePath("range.term")),
337 low_tbl(VORTablePath("range.low")),
338 high_tbl(VORTablePath("range.high")) {
339 }
340 double adjustRange( double height_ft, double nominalRange_nm );
341
342 private:
343 SGInterpTable term_tbl;
344 SGInterpTable low_tbl;
345 SGInterpTable high_tbl;
346 } _serviceVolume;
347
348 PropertyObject<double> _radial;
349 PropertyObject<double> _radialInbound;
350};
351
352// model standard VOR/DME/TACAN service volumes as per AIM 1-1-8
353double VOR::ServiceVolume::adjustRange( double height_ft, double nominalRange_nm )
354{
355 if (nominalRange_nm < SGLimitsd::min() )
356 nominalRange_nm = FG_NAV_DEFAULT_RANGE;
357
358 // extend out actual usable range to be 1.3x the published safe range
359 const double usability_factor = 1.3;
360
361 // assumptions we model the standard service volume, plus
362 // ... rather than specifying a cylinder, we model a cone that
363 // contains the cylinder. Then we put an upside down cone on top
364 // to model diminishing returns at too-high altitudes.
365
366 if ( nominalRange_nm < 25.0 + SG_EPSILON ) {
367 // Standard Terminal Service Volume
368 return term_tbl.interpolate( height_ft ) * usability_factor;
369 } else if ( nominalRange_nm < 50.0 + SG_EPSILON ) {
370 // Standard Low Altitude Service Volume
371 // table is based on range of 40, scale to actual range
372 return low_tbl.interpolate( height_ft ) * nominalRange_nm / 40.0
373 * usability_factor;
374 } else {
375 // Standard High Altitude Service Volume
376 // table is based on range of 130, scale to actual range
377 return high_tbl.interpolate( height_ft ) * nominalRange_nm / 130.0
378 * usability_factor;
379 }
380}
381
382VOR::VOR( SGPropertyNode_ptr rootNode) :
383 NavRadioComponentWithIdent("vor", rootNode,
384 new VORAudioIdent(getIdentString("vor"s,
385 rootNode->getIndex()))),
386 _totalTime(0.0),
387 _radial( rootNode->getNode(_name,true)->getNode("radial",true) ),
388 _radialInbound( rootNode->getNode(_name,true)->getNode("radial-inbound",true) )
389{
390}
391
393{
394}
395
396double VOR::getRange_nm( const SGGeod & aircraftPosition )
397{
398 return _serviceVolume.adjustRange( _heightAboveStation_ft, _navRecord->get_range() );
399}
400
402{
404 return &filter;
405}
406
407double VOR::computeSignalQuality_norm( const SGGeod & aircraftPosition )
408{
409 // apply cone of confusion. Some sources say it's opening angle is 53deg, others estimate
410 // a diameter of 1NM per 6000ft (approx. 45deg). ICAO Annex 10 says minimum 40deg.
411 // We use 1NM@6000ft and a distance-squared
412 // function to make signal-quality=100% 0.5NM@6000ft from the center and zero overhead
413 double cone_of_confusion_width = 0.5 * _heightAboveStation_ft / 6000.0 * SG_NM_TO_METER;
414 if( _trackDistance_m < cone_of_confusion_width ) {
415 double d = cone_of_confusion_width <= SGLimitsd::min() ? 1 :
416 (1 - _trackDistance_m/cone_of_confusion_width);
417 return 1-d*d;
418 }
419
420 // use default decay function outside the cone of confusion
422}
423
424void VOR::update( double dt, const SGGeod & aircraftPosition )
425{
426 _totalTime += dt;
427 NavRadioComponentWithIdent::update( dt, aircraftPosition );
428
429 if( !valid() ) {
430 _radial = 0.0;
431 return;
432 }
433
434 // an arbitrary error function
435 double error = 0.5*(sin(_totalTime/11.0) + sin(_totalTime/23.0));
436
437 // add 1% error at 100% signal-quality
438 // add 50% error at 0% signal-quality
439 // of full deflection (+/-10deg)
440 double e = 10.0 * ( 0.01 + (1-_signalQuality_norm) * 0.49 ) * error;
441
442 // compute magnetic bearing from the station (aka current radial)
443 double r = SGMiscd::normalizePeriodic(0.0, 360.0, _trueBearingFrom_deg - _navRecord->get_multiuse() + e );
444
445 _radial = r;
446 _radialInbound = SGMiscd::normalizePeriodic(0.0,360.0, 180.0 + _radial);
447}
448
449void VOR::display( NavIndicator & navIndicator )
450{
451 if( !valid() ) return;
452
453 double offset = SGMiscd::normalizePeriodic(-180.0,180.0,_radial - navIndicator.getSelectedCourse());
454 bool to = fabs(offset) >= 90.0;
455
456 if( to ) offset = -offset + copysign(180.0,offset);
457
458 navIndicator.showTo( to );
459 navIndicator.showFrom( !to );
460 // normalize to +/- 1.0 for +/- 10deg, decrease deflection with decreasing signal
461 navIndicator.setCDI( SGMiscd::clip( -offset/10.0, -1.0, 1.0 ) * _signalQuality_norm );
463}
464
465/* ---------------------------------------------------------------- */
467public:
468 LOC( SGPropertyNode_ptr rootNode );
469 virtual ~LOC();
470 virtual void update( double dt, const SGGeod & aircraftPosition );
471 virtual void search( double frequency, const SGGeod & aircraftPosition );
472 virtual void display( NavIndicator & navIndicator );
473 virtual double getRange_nm(const SGGeod & aircraftPosition);
474
475protected:
476 virtual double computeSignalQuality_norm( const SGGeod & aircraftPosition );
478
479private:
480 class ServiceVolume {
481 public:
482 ServiceVolume();
483 double adjustRange( double azimuthAngle_deg, double elevationAngle_deg );
484 private:
485 SGInterpTable _azimuthTable;
486 SGInterpTable _elevationTable;
487 } _serviceVolume;
488 PropertyObject<double> _localizerOffset_norm;
489 PropertyObject<double> _localizerOffset_m;
490 PropertyObject<double> _localizerWidth_deg;
491};
492
493LOC::ServiceVolume::ServiceVolume()
494{
495// maybe this: http://www.tpub.com/content/aviation2/P-1244/P-12440125.htm
496 // ICAO Annex 10 - 3.1.3.2.2: The emission from the localizer
497 // shall be horizontally polarized
498 // very rough abstraction of a 5-element yagi antenna's
499 // E-plane radiation diagram
500 _azimuthTable.addEntry( 0.0, 1.0 );
501 _azimuthTable.addEntry( 10.0, 1.0 );
502 _azimuthTable.addEntry( 30.0, 0.75 );
503 _azimuthTable.addEntry( 40.0, 0.50 );
504 _azimuthTable.addEntry( 50.0, 0.20 );
505 _azimuthTable.addEntry( 60.0, 0.10 );
506 _azimuthTable.addEntry( 70.0, 0.20 );
507 _azimuthTable.addEntry( 80.0, 0.10 );
508 _azimuthTable.addEntry( 90.0, 0.05 );
509 _azimuthTable.addEntry( 105.0, 0.10 );
510 _azimuthTable.addEntry( 130.0, 0.05 );
511 _azimuthTable.addEntry( 150.0, 0.30 );
512 _azimuthTable.addEntry( 160.0, 0.40 );
513 _azimuthTable.addEntry( 170.0, 0.50 );
514 _azimuthTable.addEntry( 180.0, 0.50 );
515
516 _elevationTable.addEntry( 0.0, 0.1 );
517 _elevationTable.addEntry( 1.05, 1.0 );
518 _elevationTable.addEntry( 7.00, 1.0 );
519 _elevationTable.addEntry( 45.0, 0.3 );
520 _elevationTable.addEntry( 90.0, 0.1 );
521 _elevationTable.addEntry( 180.0, 0.01 );
522}
523
524double LOC::ServiceVolume::adjustRange( double azimuthAngle_deg, double elevationAngle_deg )
525{
526 return _azimuthTable.interpolate( fabs(azimuthAngle_deg) ) *
527 _elevationTable.interpolate( fabs(elevationAngle_deg) );
528}
529
530LOC::LOC( SGPropertyNode_ptr rootNode) :
531 NavRadioComponentWithIdent("loc", rootNode, new LOCAudioIdent(getIdentString("loc"s,
532 rootNode->getIndex()))),
533 _serviceVolume(),
534 _localizerOffset_norm( rootNode->getNode(_name,true)->getNode("offset-norm",true) ),
535 _localizerOffset_m( rootNode->getNode(_name,true)->getNode("offset-m",true) ),
536 _localizerWidth_deg( rootNode->getNode(_name,true)->getNode("width-deg",true) )
537{
538}
539
541{
542}
543
548
549void LOC::search( double frequency, const SGGeod & aircraftPosition )
550{
551 NavRadioComponentWithIdent::search( frequency, aircraftPosition );
552 if( !valid() ) {
553 _localizerWidth_deg = 0.0;
554 return;
555 }
556
557 // cache slightly expensive value,
558 // sanitized in FGNavRecord::localizerWidth() to never become zero
559 _localizerWidth_deg = _navRecord->localizerWidth();
560}
561
562/* Localizer coverage (ICAO Annex 10 Volume I 3.1.3.3
563 25NM within +/-10 deg from the front course line
564 17NM between 10 and 35deg from the front course line
565 10NM outside of +/- 35deg if coverage is provided
566 at and above a height of 2000ft above threshold or
567 1000ft above the highest point within intermediate
568 and final approach areas. Upper limit is a surface
569 extending outward from the localizer and inclined at
570 7 degrees above the horizontal
571 */
572double LOC::getRange_nm(const SGGeod & aircraftPosition)
573{
574 double elevationAngle = ::atan2(_heightAboveStation_ft*SG_FEET_TO_METER, _trackDistance_m)*SG_RADIANS_TO_DEGREES;
575 double azimuthAngle = SGMiscd::normalizePeriodic( -180.0, 180.0, _trueBearingFrom_deg + 180.0 - _navRecord->get_multiuse() );
576
577 // looks like our navrecord declared range is based on 10NM?
578 return _navRecord->get_range() * _serviceVolume.adjustRange( azimuthAngle, elevationAngle );
579}
580
581double LOC::computeSignalQuality_norm( const SGGeod & aircraftPosition )
582{
584}
585
586void LOC::update( double dt, const SGGeod & aircraftPosition )
587{
588 NavRadioComponentWithIdent::update( dt, aircraftPosition );
589
590 if( !valid() ) {
591 _localizerOffset_norm = 0.0;
592 _localizerOffset_m = 0.0;
593 return;
594 }
595
596 double offsetDeg = SGMiscd::normalizePeriodic( -180.0, 180.0, _trueBearingFrom_deg + 180.0 - _navRecord->get_multiuse() );
597
598 // cross-track error (in meters)
599 _localizerOffset_m = _trackDistance_m * sin(offsetDeg * SGD_DEGREES_TO_RADIANS);
600
601 // The factor of 30.0 gives a period of 120 which gives us 3 cycles and six
602 // zeros i.e. six courses: one front course, one back course, and four
603 // false courses. Three of the six are reverse sensing.
604 offsetDeg = 30.0 * sawtooth(offsetDeg / 30.0);
605
606 // normalize offsetDeg to the localizer width, scale and clip to [-1..1]
607 offsetDeg = SGMiscd::clip( 2.0 * offsetDeg / _localizerWidth_deg, -1.0, 1.0 );
608
609 _localizerOffset_norm = offsetDeg;
610}
611
612void LOC::display( NavIndicator & navIndicator )
613{
614 if( !valid() )
615 return;
616
617 navIndicator.showTo( true );
618 navIndicator.showFrom( false );
619
620 navIndicator.setCDI( _localizerOffset_norm * _signalQuality_norm );
622}
623
624class GS : public NavRadioComponent {
625public:
626 GS( SGPropertyNode_ptr rootNode);
627 virtual ~GS();
628 virtual void update( double dt, const SGGeod & aircraftPosition );
629 virtual void search( double frequency, const SGGeod & aircraftPosition );
630 virtual void display( NavIndicator & navIndicator );
631
632 virtual double getRange_nm(const SGGeod & aircraftPosition);
633protected:
635private:
636 class ServiceVolume {
637 public:
638 ServiceVolume();
639 double adjustRange( double azimuthAngle_deg, double elevationAngle_deg );
640 private:
641 SGInterpTable _azimuthTable;
642 SGInterpTable _elevationTable;
643 } _serviceVolume;
644 static SGVec3d tangentVector(const SGGeod& midpoint, const double heading);
645
646 PropertyObject<double> _targetGlideslope_deg;
647 PropertyObject<double> _glideslopeOffset_norm;
648 SGVec3d _gsAxis;
649 SGVec3d _gsVertical;
650};
651
652GS::ServiceVolume::ServiceVolume()
653{
654// maybe this: http://www.tpub.com/content/aviation2/P-1244/P-12440125.htm
655 // ICAO Annex 10 - 3.1.5.2.2: The emission from the glide path equipment
656 // shall be horizontally polarized
657 // very rough abstraction of a 5-element yagi antenna's
658 // E-plane radiation diagram
659 _azimuthTable.addEntry( 0.0, 1.0 );
660 _azimuthTable.addEntry( 10.0, 1.0 );
661 _azimuthTable.addEntry( 30.0, 0.75 );
662 _azimuthTable.addEntry( 40.0, 0.50 );
663 _azimuthTable.addEntry( 50.0, 0.20 );
664 _azimuthTable.addEntry( 60.0, 0.10 );
665 _azimuthTable.addEntry( 70.0, 0.20 );
666 _azimuthTable.addEntry( 80.0, 0.10 );
667 _azimuthTable.addEntry( 90.0, 0.05 );
668 _azimuthTable.addEntry( 105.0, 0.10 );
669 _azimuthTable.addEntry( 130.0, 0.05 );
670 _azimuthTable.addEntry( 150.0, 0.30 );
671 _azimuthTable.addEntry( 160.0, 0.40 );
672 _azimuthTable.addEntry( 170.0, 0.50 );
673 _azimuthTable.addEntry( 180.0, 0.50 );
674
675 _elevationTable.addEntry( 0.0, 0.1 );
676 _elevationTable.addEntry( 1.05, 1.0 );
677 _elevationTable.addEntry( 7.00, 1.0 );
678 _elevationTable.addEntry( 45.0, 0.3 );
679 _elevationTable.addEntry( 90.0, 0.1 );
680 _elevationTable.addEntry( 180.0, 0.01 );
681}
682
683double GS::ServiceVolume::adjustRange( double azimuthAngle_deg, double elevationAngle_deg )
684{
685 return _azimuthTable.interpolate( fabs(azimuthAngle_deg) ) *
686 _elevationTable.interpolate( fabs(elevationAngle_deg) );
687}
688
689GS::GS( SGPropertyNode_ptr rootNode) :
690 NavRadioComponent("gs", rootNode ),
691 _targetGlideslope_deg( rootNode->getNode(_name,true)->getNode("slope",true) ),
692 _glideslopeOffset_norm( rootNode->getNode(_name,true)->getNode("offset-norm",true) ),
693 _gsAxis(SGVec3d::zeros()),
694 _gsVertical(SGVec3d::zeros())
695{
696}
697
699{
700}
701
703{
705 return &filter;
706}
707
708double GS::getRange_nm(const SGGeod & aircraftPosition)
709{
710 double elevationAngle = ::atan2(_heightAboveStation_ft*SG_FEET_TO_METER, _trackDistance_m)*SG_RADIANS_TO_DEGREES;
711 double azimuthAngle = SGMiscd::normalizePeriodic( -180.0, 180.0, _trueBearingFrom_deg + 180.0 - fmod(_navRecord->get_multiuse(), 1000.0) );
712 return _navRecord->get_range() * _serviceVolume.adjustRange( azimuthAngle, elevationAngle );
713}
714
715// Calculate a Cartesian unit vector in the
716// local horizontal plane, i.e. tangent to the
717// surface of the earth at the local ground zero.
718// The tangent vector passes through the given <midpoint>
719// and points forward along the given <heading>.
720// The <heading> is given in degrees.
721SGVec3d GS::tangentVector(const SGGeod& midpoint, const double heading)
722{
723 // move 100m away from the midpoint - arbitrary number
724 const double delta(100.0);
725 SGGeod head, tail;
726 double az2; // ignored
727 SGGeodesy::direct(midpoint, heading, delta, head, az2);
728 SGGeodesy::direct(midpoint, 180+heading, delta, tail, az2);
729 head.setElevationM(midpoint.getElevationM());
730 tail.setElevationM(midpoint.getElevationM());
731 SGVec3d head_xyz = SGVec3d::fromGeod(head);
732 SGVec3d tail_xyz = SGVec3d::fromGeod(tail);
733// Awkward formula here, needed because vector-by-scalar
734// multiplication is defined, but not vector-by-scalar division.
735 return (head_xyz - tail_xyz) * (0.5/delta);
736}
737
738void GS::search( double frequency, const SGGeod & aircraftPosition )
739{
740 NavRadioComponent::search( frequency, aircraftPosition );
741 if( !valid() ) {
742 _gsAxis = SGVec3d::zeros();
743 _gsVertical = SGVec3d::zeros();
744 _targetGlideslope_deg = 3.0;
745 return;
746 }
747
748 double gs_radial = SGMiscd::normalizePeriodic(0.0, 360.0, fmod(_navRecord->get_multiuse(), 1000.0) );
749
750 _gsAxis = tangentVector(_navRecord->geod(), gs_radial);
751 SGVec3d gsBaseline = tangentVector(_navRecord->geod(), gs_radial + 90.0);
752 _gsVertical = cross(gsBaseline, _gsAxis);
753
754 int tmp = (int)(_navRecord->get_multiuse() / 1000.0);
755 // catch unconfigured glideslopes here, they will cause nan later
756 _targetGlideslope_deg = SGMiscd::max( 1.0, (double)tmp / 100.0 );
757}
758
759void GS::update( double dt, const SGGeod & aircraftPosition )
760{
761 NavRadioComponent::update( dt, aircraftPosition );
762 if( !valid() ) {
763 _glideslopeOffset_norm = 0.0;
764 return;
765 }
766
767 SGVec3d pos = SGVec3d::fromGeod(aircraftPosition) - _navRecord->cart(); // relative vector from gs antenna to aircraft
768 // The positive GS axis points along the runway in the landing direction,
769 // toward the far end, not toward the approach area, so we need a - sign here:
770 double comp_h = -dot(pos, _gsAxis); // component in horiz direction
771 double comp_v = dot(pos, _gsVertical); // component in vertical direction
772 //double comp_b = dot(pos, _gsBaseline); // component in baseline direction
773 //if (comp_b) {} // ... (useful for debugging)
774
775// _gsDirect represents the angle of elevation of the aircraft
776// as seen by the GS transmitter.
777 double gsDirect = atan2(comp_v, comp_h) * SGD_RADIANS_TO_DEGREES;
778// At this point, if the aircraft is centered on the glide slope,
779// _gsDirect will be a small positive number, e.g. 3.0 degrees
780
781// Aim the branch cut straight down
782// into the ground below the GS transmitter:
783 if (gsDirect < -90.0) gsDirect += 360.0;
784
785 double offset = _targetGlideslope_deg - gsDirect;
786 if( offset < 0.0 )
787 offset = _targetGlideslope_deg/2 * sawtooth(2.0*offset/_targetGlideslope_deg);
788 assert( !SGMisc<double>::isNaN(offset) );
789// GS is documented to be 1.4 degrees thick,
790// i.e. plus or minus 0.7 degrees from the midline:
791 _glideslopeOffset_norm = SGMiscd::clip(offset/0.7, -1.0, 1.0);
792}
793
794void GS::display( NavIndicator & navIndicator )
795{
796 if( !valid() ) {
797 navIndicator.setGS( false );
798 return;
799 }
800 navIndicator.setGS( true );
801 navIndicator.setGS( _glideslopeOffset_norm );
802}
803
804/* ------------- The NavRadio implementation ---------------------- */
805
806class NavRadioImpl : public NavRadio
807{
808public:
809 NavRadioImpl( SGPropertyNode_ptr node );
810 virtual ~NavRadioImpl();
811
812 // Subsystem API.
813 void init() override;
814 void update(double dt) override;
815
816private:
817 void search();
818
819 class Legacy {
820 public:
821 Legacy( NavRadioImpl * navRadioImpl ) : _navRadioImpl( navRadioImpl ) {}
822
823 void init();
824 void update( double dt );
825
826 private:
827 NavRadioImpl * _navRadioImpl;
828 SGPropertyNode_ptr is_valid_node;
829 SGPropertyNode_ptr nav_serviceable_node;
830 SGPropertyNode_ptr nav_id_node;
831 SGPropertyNode_ptr id_c1_node;
832 SGPropertyNode_ptr id_c2_node;
833 SGPropertyNode_ptr id_c3_node;
834 SGPropertyNode_ptr id_c4_node;
835 } _legacy;
836
837 const static int VOR_COMPONENT = 0;
838 const static int LOC_COMPONENT = 1;
839 const static int GS_COMPONENT = 2;
840
841 std::string _name;
842 int _num;
843 SGPropertyNode_ptr _rootNode;
844 FrequencyFormatter _useFrequencyFormatter;
845 FrequencyFormatter _stbyFrequencyFormatter;
846 std::vector<NavRadioComponent*> _components;
847 NavIndicator _navIndicator;
848 double _stationTTL;
849 double _frequency;
850 PropertyObject<bool> _cdiDisconnected;
851 PropertyObject<std::string> _navType;
852};
853
854NavRadioImpl::NavRadioImpl( SGPropertyNode_ptr node ) :
855 _legacy( this ),
856 _name(node->getStringValue("name", "nav")),
857 _num(node->getIntValue("number", 0)),
858 _rootNode(fgGetNode( "/instrumentation/"s + _name, _num, true)),
859 _useFrequencyFormatter( _rootNode->getNode("frequencies/selected-mhz",true), _rootNode->getNode("frequencies/selected-mhz-fmt",true), 0.05, 108.0, 118.0 ),
860 _stbyFrequencyFormatter( _rootNode->getNode("frequencies/standby-mhz",true), _rootNode->getNode("frequencies/standby-mhz-fmt",true), 0.05, 108.0, 118.0 ),
861 _navIndicator(_rootNode),
862 _stationTTL(0.0),
863 _frequency(-1.0),
864 _cdiDisconnected(_rootNode->getNode("cdi-disconnected",true)),
865 _navType(_rootNode->getNode("nav-type",true))
866{
867}
868
870{
871 for( auto p : _components ) {
872 delete p;
873 }
874}
875
877{
878 if( ! _components.empty() )
879 return;
880
881 _components.push_back( new VOR(_rootNode) );
882 _components.push_back( new LOC(_rootNode) );
883 _components.push_back( new GS(_rootNode) );
884
885 _legacy.init();
886}
887
888void NavRadioImpl::search()
889{
890}
891
892void NavRadioImpl::update( double dt )
893{
894 if( dt < SGLimitsd::min() ) return;
895
896 SGGeod position;
897
898 try {
899 position = globals->get_aircraft_position();
900 }
901 catch( std::exception & ) {
902 return;
903 }
904
905 _stationTTL -= dt;
906 if( _frequency != _useFrequencyFormatter.getFrequency() ) {
907 _frequency = _useFrequencyFormatter.getFrequency();
908 _stationTTL = 0.0;
909 }
910
911 for( auto p : _components ) {
912 if( _stationTTL <= 0.0 )
913 p->search( _frequency, position );
914 p->update( dt, position );
915
916 if( !_cdiDisconnected )
917 p->display( _navIndicator );
918 }
919
920 if( _stationTTL <= 0.0 )
921 _stationTTL = 30.0;
922
923 if( _components[VOR_COMPONENT]->valid() ) {
924 _navType = "vor";
925 } else if( _components[LOC_COMPONENT]->valid() ) {
926 _navType = "loc";
927 } else {
928 _navType = "";
929 }
930
931 _legacy.update( dt );
932}
933
934void NavRadioImpl::Legacy::init()
935{
936 is_valid_node = _navRadioImpl->_rootNode->getChild("data-is-valid", 0, true);
937 nav_serviceable_node = _navRadioImpl->_rootNode->getChild("serviceable", 0, true);
938
939 nav_id_node = _navRadioImpl->_rootNode->getChild("nav-id", 0, true );
940 id_c1_node = _navRadioImpl->_rootNode->getChild("nav-id_asc1", 0, true );
941 id_c2_node = _navRadioImpl->_rootNode->getChild("nav-id_asc2", 0, true );
942 id_c3_node = _navRadioImpl->_rootNode->getChild("nav-id_asc3", 0, true );
943 id_c4_node = _navRadioImpl->_rootNode->getChild("nav-id_asc4", 0, true );
944
945}
946
947void NavRadioImpl::Legacy::update( double dt )
948{
949 is_valid_node->setBoolValue(
950 _navRadioImpl->_components[VOR_COMPONENT]->valid() || _navRadioImpl->_components[LOC_COMPONENT]->valid()
951 );
952
953 std::string ident = _navRadioImpl->_components[VOR_COMPONENT]->getIdent();
954 if( ident.empty() )
955 ident = _navRadioImpl->_components[LOC_COMPONENT]->getIdent();
956
957 nav_id_node->setStringValue( ident );
958
959 ident = simgear::strutils::rpad( ident, 4, ' ' );
960 id_c1_node->setIntValue( (int)ident[0] );
961 id_c2_node->setIntValue( (int)ident[1] );
962 id_c3_node->setIntValue( (int)ident[2] );
963 id_c4_node->setIntValue( (int)ident[3] );
964}
965
966
967SGSubsystem * NavRadio::createInstance( SGPropertyNode_ptr rootNode )
968{
969 // use old navradio code by default
970 if( fgGetBool( "/instrumentation/use-new-navradio", false ) )
971 return new NavRadioImpl( rootNode );
972
973 return new FGNavRadio( rootNode );
974}
975
976
977// Register the subsystem.
978#if 0
979SGSubsystemMgr::InstancedRegistrant<NavRadio> registrantNavRadio(
980 SGSubsystemMgr::FDM,
981 {{"instrumentation", SGSubsystemMgr::Dependency::HARD}});
982#endif
983
984} // namespace Instrumentation
985
#define p(x)
static TypeFilter * locFilter()
filter matching ILS/LOC transmitter
Definition navlist.cxx:157
static FGNavRecordRef findByFreq(double freq, const SGGeod &position, TypeFilter *filter=nullptr)
Query the database for the specified station.
Definition navlist.cxx:187
GS(SGPropertyNode_ptr rootNode)
virtual double getRange_nm(const SGGeod &aircraftPosition)
virtual void display(NavIndicator &navIndicator)
virtual void search(double frequency, const SGGeod &aircraftPosition)
virtual FGNavList::TypeFilter * getNavaidFilter()
virtual void update(double dt, const SGGeod &aircraftPosition)
virtual double computeSignalQuality_norm(const SGGeod &aircraftPosition)
virtual double getRange_nm(const SGGeod &aircraftPosition)
virtual void update(double dt, const SGGeod &aircraftPosition)
virtual void search(double frequency, const SGGeod &aircraftPosition)
virtual FGNavList::TypeFilter * getNavaidFilter()
LOC(SGPropertyNode_ptr rootNode)
virtual void display(NavIndicator &navIndicator)
void setSignalQuality(double signalQuality)
void setCDI(double norm)
set the normalized CDI deflection
void setGS(double norm)
set the normalized GS deflection
void setSelectedCourse(double course)
NavIndicator(SGPropertyNode *rootNode)
NavRadioComponentWithIdent(const std::string &name, SGPropertyNode_ptr rootNode, AudioIdent *audioIdent)
static std::string getIdentString(const std::string &name, int index)
void update(double dt, const SGGeod &aircraftPosition)
PropertyObject< double > _heightAboveStation_ft
PropertyObject< double > _trueBearingFrom_deg
PropertyObject< double > _trueBearingTo_deg
PropertyObject< double > _range_nm
PropertyObject< std::string > _ident
virtual double getRange_nm(const SGGeod &aircraftPosition)
PropertyObject< double > _signalQuality_norm
NavRadioComponent(const std::string &name, SGPropertyNode_ptr rootNode)
static double sawtooth(double xx)
PropertyObject< double > _slantDistance_m
virtual void display(NavIndicator &navIndicator)=0
virtual double computeSignalQuality_norm(const SGGeod &aircraftPosition)
PropertyObject< bool > _inRange
virtual void update(double dt, const SGGeod &aircraftPosition)
virtual void search(double frequency, const SGGeod &aircraftPosition)
virtual const std::string getIdent() const
PropertyObject< double > _trackDistance_m
virtual FGNavList::TypeFilter * getNavaidFilter()=0
PropertyObject< bool > _serviceable
NavRadioImpl(SGPropertyNode_ptr node)
void update(double dt) override
static SGSubsystem * createInstance(SGPropertyNode_ptr rootNode)
virtual double computeSignalQuality_norm(const SGGeod &aircraftPosition)
virtual FGNavList::TypeFilter * getNavaidFilter()
virtual double getRange_nm(const SGGeod &aircraftPosition)
VOR(SGPropertyNode_ptr rootNode)
virtual void update(double dt, const SGGeod &aircraftPosition)
virtual void display(NavIndicator &navIndicator)
const char * name
FGGlobals * globals
Definition globals.cxx:142
static SGPath VORTablePath(const char *name)
const double FG_NAV_DEFAULT_RANGE
Definition navrecord.hxx:36
bool fgGetBool(char const *name, bool def)
Get a bool value for a property.
Definition proptest.cpp:25
SGPropertyNode * fgGetNode(const char *path, bool create)
Get a property node.
Definition proptest.cpp:27