]> git.mxchange.org Git - flightgear.git/blob - src/Instrumentation/navradio.cxx
Add normalised heading deflection property to navradio.
[flightgear.git] / src / Instrumentation / navradio.cxx
1 // navradio.cxx -- class to manage a nav radio instance
2 //
3 // Written by Curtis Olson, started April 2000.
4 //
5 // Copyright (C) 2000 - 2002  Curtis L. Olson - http://www.flightgear.org/~curt
6 //
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License as
9 // published by the Free Software Foundation; either version 2 of the
10 // License, or (at your option) any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 // General Public License for more details.
16 //
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20 //
21 // $Id$
22
23
24 #ifdef HAVE_CONFIG_H
25 #  include <config.h>
26 #endif
27
28 #include <sstream>
29
30 #include <simgear/sg_inlines.h>
31 #include <simgear/timing/sg_time.hxx>
32 #include <simgear/math/vector.hxx>
33 #include <simgear/math/sg_random.h>
34 #include <simgear/misc/sg_path.hxx>
35 #include <simgear/math/sg_geodesy.hxx>
36 #include <simgear/structure/exception.hxx>
37 #include <simgear/math/interpolater.hxx>
38
39 #include "Navaids/navrecord.hxx"
40 #include <Navaids/navlist.hxx>
41 #include <Main/util.hxx>
42 #include "navradio.hxx"
43
44 using std::string;
45
46 // Constructor
47 FGNavRadio::FGNavRadio(SGPropertyNode *node) :
48     lon_node(fgGetNode("/position/longitude-deg", true)),
49     lat_node(fgGetNode("/position/latitude-deg", true)),
50     alt_node(fgGetNode("/position/altitude-ft", true)),
51     is_valid_node(NULL),
52     power_btn_node(NULL),
53     freq_node(NULL),
54     alt_freq_node(NULL),
55     sel_radial_node(NULL),
56     vol_btn_node(NULL),
57     ident_btn_node(NULL),
58     audio_btn_node(NULL),
59     backcourse_node(NULL),
60     nav_serviceable_node(NULL),
61     cdi_serviceable_node(NULL),
62     gs_serviceable_node(NULL),
63     tofrom_serviceable_node(NULL),
64     fmt_freq_node(NULL),
65     fmt_alt_freq_node(NULL),
66     heading_node(NULL),
67     radial_node(NULL),
68     recip_radial_node(NULL),
69     target_radial_true_node(NULL),
70     target_auto_hdg_node(NULL),
71     time_to_intercept(NULL),
72     to_flag_node(NULL),
73     from_flag_node(NULL),
74     inrange_node(NULL),
75     signal_quality_norm_node(NULL),
76     cdi_deflection_node(NULL),
77     cdi_deflection_norm_node(NULL),
78     cdi_xtrack_error_node(NULL),
79     cdi_xtrack_hdg_err_node(NULL),
80     has_gs_node(NULL),
81     loc_node(NULL),
82     loc_dist_node(NULL),
83     gs_deflection_node(NULL),
84     gs_deflection_norm_node(NULL),
85     gs_rate_of_climb_node(NULL),
86     gs_dist_node(NULL),
87     nav_id_node(NULL),
88     id_c1_node(NULL),
89     id_c2_node(NULL),
90     id_c3_node(NULL),
91     id_c4_node(NULL),
92     nav_slaved_to_gps_node(NULL),
93     gps_cdi_deflection_node(NULL),
94     gps_to_flag_node(NULL),
95     gps_from_flag_node(NULL),
96     gps_has_gs_node(NULL),
97     play_count(0),
98     last_time(0),
99     target_radial(0.0),
100     horiz_vel(0.0),
101     last_x(0.0),
102     last_loc_dist(0.0),
103     last_xtrack_error(0.0),
104     _name(node->getStringValue("name", "nav")),
105     _num(node->getIntValue("number", 0)),
106     _time_before_search_sec(-1.0)
107 {
108     SGPath path( globals->get_fg_root() );
109     SGPath term = path;
110     term.append( "Navaids/range.term" );
111     SGPath low = path;
112     low.append( "Navaids/range.low" );
113     SGPath high = path;
114     high.append( "Navaids/range.high" );
115
116     term_tbl = new SGInterpTable( term.str() );
117     low_tbl = new SGInterpTable( low.str() );
118     high_tbl = new SGInterpTable( high.str() );
119 }
120
121
122 // Destructor
123 FGNavRadio::~FGNavRadio() 
124 {
125     delete term_tbl;
126     delete low_tbl;
127     delete high_tbl;
128 }
129
130
131 void
132 FGNavRadio::init ()
133 {
134     morse.init();
135
136     string branch;
137     branch = "/instrumentation/" + _name;
138
139     SGPropertyNode *node = fgGetNode(branch.c_str(), _num, true );
140
141     bus_power_node = 
142         fgGetNode(("/systems/electrical/outputs/" + _name).c_str(), true);
143
144     // inputs
145     is_valid_node = node->getChild("data-is-valid", 0, true);
146     power_btn_node = node->getChild("power-btn", 0, true);
147     power_btn_node->setBoolValue( true );
148     vol_btn_node = node->getChild("volume", 0, true);
149     ident_btn_node = node->getChild("ident", 0, true);
150     ident_btn_node->setBoolValue( true );
151     audio_btn_node = node->getChild("audio-btn", 0, true);
152     audio_btn_node->setBoolValue( true );
153     backcourse_node = node->getChild("back-course-btn", 0, true);
154     backcourse_node->setBoolValue( false );
155     nav_serviceable_node = node->getChild("serviceable", 0, true);
156     cdi_serviceable_node = (node->getChild("cdi", 0, true))
157         ->getChild("serviceable", 0, true);
158     gs_serviceable_node = (node->getChild("gs", 0, true))
159         ->getChild("serviceable");
160     tofrom_serviceable_node = (node->getChild("to-from", 0, true))
161         ->getChild("serviceable", 0, true);
162
163     // frequencies
164     SGPropertyNode *subnode = node->getChild("frequencies", 0, true);
165     freq_node = subnode->getChild("selected-mhz", 0, true);
166     alt_freq_node = subnode->getChild("standby-mhz", 0, true);
167     fmt_freq_node = subnode->getChild("selected-mhz-fmt", 0, true);
168     fmt_alt_freq_node = subnode->getChild("standby-mhz-fmt", 0, true);
169
170     // radials
171     subnode = node->getChild("radials", 0, true);
172     sel_radial_node = subnode->getChild("selected-deg", 0, true);
173     radial_node = subnode->getChild("actual-deg", 0, true);
174     recip_radial_node = subnode->getChild("reciprocal-radial-deg", 0, true);
175     target_radial_true_node = subnode->getChild("target-radial-deg", 0, true);
176     target_auto_hdg_node = subnode->getChild("target-auto-hdg-deg", 0, true);
177
178     // outputs
179     heading_node = node->getChild("heading-deg", 0, true);
180     time_to_intercept = node->getChild("time-to-intercept-sec", 0, true);
181     to_flag_node = node->getChild("to-flag", 0, true);
182     from_flag_node = node->getChild("from-flag", 0, true);
183     inrange_node = node->getChild("in-range", 0, true);
184     signal_quality_norm_node = node->getChild("signal-quality-norm", 0, true);
185     cdi_deflection_node = node->getChild("heading-needle-deflection", 0, true);
186     cdi_deflection_norm_node = node->getChild("heading-needle-deflection-norm", 0, true);
187     cdi_xtrack_error_node = node->getChild("crosstrack-error-m", 0, true);
188     cdi_xtrack_hdg_err_node
189         = node->getChild("crosstrack-heading-error-deg", 0, true);
190     has_gs_node = node->getChild("has-gs", 0, true);
191     loc_node = node->getChild("nav-loc", 0, true);
192     loc_dist_node = node->getChild("nav-distance", 0, true);
193     gs_deflection_node = node->getChild("gs-needle-deflection", 0, true);
194     gs_deflection_norm_node = node->getChild("gs-needle-deflection-norm", 0, true);
195     gs_rate_of_climb_node = node->getChild("gs-rate-of-climb", 0, true);
196     gs_dist_node = node->getChild("gs-distance", 0, true);
197     nav_id_node = node->getChild("nav-id", 0, true);
198     id_c1_node = node->getChild("nav-id_asc1", 0, true);
199     id_c2_node = node->getChild("nav-id_asc2", 0, true);
200     id_c3_node = node->getChild("nav-id_asc3", 0, true);
201     id_c4_node = node->getChild("nav-id_asc4", 0, true);
202
203     // gps slaving support
204     nav_slaved_to_gps_node = node->getChild("slaved-to-gps", 0, true);
205     gps_cdi_deflection_node = fgGetNode("/instrumentation/gps/cdi-deflection", true);
206     gps_to_flag_node = fgGetNode("/instrumentation/gps/to-flag", true);
207     gps_from_flag_node = fgGetNode("/instrumentation/gps/from-flag", true);
208     gps_has_gs_node = fgGetNode("/instrumentation/gps/has-gs", true);
209     
210     std::ostringstream temp;
211     temp << _name << "nav-ident" << _num;
212     nav_fx_name = temp.str();
213     temp << _name << "dme-ident" << _num;
214     dme_fx_name = temp.str();
215 }
216
217 void
218 FGNavRadio::bind ()
219 {
220     std::ostringstream temp;
221     string branch;
222     temp << _num;
223     branch = "/instrumentation/" + _name + "[" + temp.str() + "]";
224 }
225
226
227 void
228 FGNavRadio::unbind ()
229 {
230     std::ostringstream temp;
231     string branch;
232     temp << _num;
233     branch = "/instrumentation/" + _name + "[" + temp.str() + "]";
234 }
235
236
237 // model standard VOR/DME/TACAN service volumes as per AIM 1-1-8
238 double FGNavRadio::adjustNavRange( double stationElev, double aircraftElev,
239                                  double nominalRange )
240 {
241     // extend out actual usable range to be 1.3x the published safe range
242     const double usability_factor = 1.3;
243
244     // assumptions we model the standard service volume, plus
245     // ... rather than specifying a cylinder, we model a cone that
246     // contains the cylinder.  Then we put an upside down cone on top
247     // to model diminishing returns at too-high altitudes.
248
249     // altitude difference
250     double alt = ( aircraftElev * SG_METER_TO_FEET - stationElev );
251     // cout << "aircraft elev = " << aircraftElev * SG_METER_TO_FEET
252     //      << " station elev = " << stationElev << endl;
253
254     if ( nominalRange < 25.0 + SG_EPSILON ) {
255         // Standard Terminal Service Volume
256         return term_tbl->interpolate( alt ) * usability_factor;
257     } else if ( nominalRange < 50.0 + SG_EPSILON ) {
258         // Standard Low Altitude Service Volume
259         // table is based on range of 40, scale to actual range
260         return low_tbl->interpolate( alt ) * nominalRange / 40.0
261             * usability_factor;
262     } else {
263         // Standard High Altitude Service Volume
264         // table is based on range of 130, scale to actual range
265         return high_tbl->interpolate( alt ) * nominalRange / 130.0
266             * usability_factor;
267     }
268 }
269
270
271 // model standard ILS service volumes as per AIM 1-1-9
272 double FGNavRadio::adjustILSRange( double stationElev, double aircraftElev,
273                                  double offsetDegrees, double distance )
274 {
275     // assumptions we model the standard service volume, plus
276
277     // altitude difference
278     // double alt = ( aircraftElev * SG_METER_TO_FEET - stationElev );
279 //     double offset = fabs( offsetDegrees );
280
281 //     if ( offset < 10 ) {
282 //      return FG_ILS_DEFAULT_RANGE;
283 //     } else if ( offset < 35 ) {
284 //      return 10 + (35 - offset) * (FG_ILS_DEFAULT_RANGE - 10) / 25;
285 //     } else if ( offset < 45 ) {
286 //      return (45 - offset);
287 //     } else if ( offset > 170 ) {
288 //         return FG_ILS_DEFAULT_RANGE;
289 //     } else if ( offset > 145 ) {
290 //      return 10 + (offset - 145) * (FG_ILS_DEFAULT_RANGE - 10) / 25;
291 //     } else if ( offset > 135 ) {
292 //         return (offset - 135);
293 //     } else {
294 //      return 0;
295 //     }
296     return FG_LOC_DEFAULT_RANGE;
297 }
298
299
300 //////////////////////////////////////////////////////////////////////////
301 // Update the various nav values based on position and valid tuned in navs
302 //////////////////////////////////////////////////////////////////////////
303 void 
304 FGNavRadio::update(double dt) 
305 {
306   if (dt <= 0.0) {
307     return; // paused
308   }
309     
310   // Create "formatted" versions of the nav frequencies for
311   // instrument displays.
312   char tmp[16];
313   sprintf( tmp, "%.2f", freq_node->getDoubleValue() );
314   fmt_freq_node->setStringValue(tmp);
315   sprintf( tmp, "%.2f", alt_freq_node->getDoubleValue() );
316   fmt_alt_freq_node->setStringValue(tmp);
317
318   if (power_btn_node->getBoolValue() 
319       && (bus_power_node->getDoubleValue() > 1.0)
320       && nav_serviceable_node->getBoolValue() )
321   {   
322     if (nav_slaved_to_gps_node->getBoolValue()) {
323       updateGPSSlaved();
324     } else {
325       updateReceiver(dt);
326     }
327     
328     updateCDI(dt);
329   } else {
330     clearOutputs();
331   }
332   
333   updateAudio();
334 }
335
336 void FGNavRadio::clearOutputs()
337 {
338   inrange_node->setBoolValue( false );
339   cdi_deflection_node->setDoubleValue( 0.0 );
340   cdi_deflection_norm_node->setDoubleValue( 0.0 );
341   cdi_xtrack_error_node->setDoubleValue( 0.0 );
342   cdi_xtrack_hdg_err_node->setDoubleValue( 0.0 );
343   time_to_intercept->setDoubleValue( 0.0 );
344   gs_deflection_node->setDoubleValue( 0.0 );
345   gs_deflection_norm_node->setDoubleValue(0.0);
346   
347   to_flag_node->setBoolValue( false );
348   from_flag_node->setBoolValue( false );
349 }
350
351 void FGNavRadio::updateReceiver(double dt)
352 {
353   // Do a nav station search only once a second to reduce
354   // unnecessary work. (Also, make sure to do this before caching
355   // any values!)
356   _time_before_search_sec -= dt;
357   if ( _time_before_search_sec < 0 ) {
358    search();
359   }
360
361   if (!_navaid) {
362     _cdiDeflection = 0.0;
363     _cdiCrossTrackErrorM = 0.0;
364     _toFlag = _fromFlag = false;
365     _gsNeedleDeflection = 0.0;
366     _gsNeedleDeflectionNorm = 0.0;
367     inrange_node->setBoolValue(false);
368     return;
369   }
370
371   SGGeod pos = SGGeod::fromDegFt(lon_node->getDoubleValue(),
372                                lat_node->getDoubleValue(),
373                                alt_node->getDoubleValue());
374                                
375   double nav_elev = _navaid->get_elev_ft();
376   SGVec3d aircraft = SGVec3d::fromGeod(pos);
377   double loc_dist = dist(aircraft, _navaid->cart());
378   loc_dist_node->setDoubleValue( loc_dist );
379   bool is_loc = loc_node->getBoolValue();
380   double signal_quality_norm = signal_quality_norm_node->getDoubleValue();
381   
382   double az2, s;
383   //////////////////////////////////////////////////////////
384         // compute forward and reverse wgs84 headings to localizer
385   //////////////////////////////////////////////////////////
386   double hdg;
387   SGGeodesy::inverse(pos, _navaid->geod(), hdg, az2, s);
388   heading_node->setDoubleValue(hdg);
389   double radial = az2 - twist;
390   double recip = radial + 180.0;
391   SG_NORMALIZE_RANGE(recip, 0.0, 360.0);
392   radial_node->setDoubleValue( radial );
393   recip_radial_node->setDoubleValue( recip );
394   
395   //////////////////////////////////////////////////////////
396   // compute the target/selected radial in "true" heading
397   //////////////////////////////////////////////////////////
398   if (!is_loc) {
399     target_radial = sel_radial_node->getDoubleValue();
400   }
401   
402   // VORs need twist (mag-var) added; ILS/LOCs don't but we set twist to 0.0
403   double trtrue = target_radial + twist;
404   SG_NORMALIZE_RANGE(trtrue, 0.0, 360.0);
405   target_radial_true_node->setDoubleValue( trtrue );
406
407   //////////////////////////////////////////////////////////
408   // adjust reception range for altitude
409   // FIXME: make sure we are using the navdata range now that
410   //        it is valid in the data file
411   //////////////////////////////////////////////////////////
412         if ( is_loc ) {
413             double offset = radial - target_radial;
414       SG_NORMALIZE_RANGE(offset, -180.0, 180.0);
415             effective_range
416                 = adjustILSRange( nav_elev, pos.getElevationM(), offset,
417                                   loc_dist * SG_METER_TO_NM );
418         } else {
419             effective_range
420                 = adjustNavRange( nav_elev, pos.getElevationM(), _navaid->get_range() );
421         }
422
423   double effective_range_m = effective_range * SG_NM_TO_METER;
424
425   //////////////////////////////////////////////////////////
426   // compute signal quality
427   // 100% within effective_range
428   // decreases 1/x^2 further out
429   //////////////////////////////////////////////////////////  
430   double last_signal_quality_norm = signal_quality_norm;
431
432   if ( loc_dist < effective_range_m ) {
433     signal_quality_norm = 1.0;
434   } else {
435     double range_exceed_norm = loc_dist/effective_range_m;
436     signal_quality_norm = 1/(range_exceed_norm*range_exceed_norm);
437   }
438
439   signal_quality_norm = fgGetLowPass( last_signal_quality_norm, 
440            signal_quality_norm, dt );
441   
442   signal_quality_norm_node->setDoubleValue( signal_quality_norm );
443   bool inrange = signal_quality_norm > 0.2;
444   inrange_node->setBoolValue( inrange );
445   
446   //////////////////////////////////////////////////////////
447   // compute to/from flag status
448   //////////////////////////////////////////////////////////
449   if (inrange) {
450     if (is_loc) {
451       _toFlag = true;
452     } else {
453       double offset = fabs(radial - target_radial);
454       _toFlag = (offset > 90.0 && offset < 270.0);
455     }
456     _fromFlag = !_toFlag;
457   } else {
458     _toFlag = _fromFlag = false;
459   }
460   
461   // CDI deflection
462   double r = radial - target_radial;
463   SG_NORMALIZE_RANGE(r, -180.0, 180.0);
464   if ( fabs(r) > 90.0 ) {
465     r = ( r<0.0 ? -r-180.0 : -r+180.0 );
466   }
467   
468   r = -r; // reverse, since radial is outbound
469   _cdiDeflection = r;
470   if ( is_loc ) {
471     // According to Robin Peel, the ILS is 4x more
472     // sensitive than a vor
473     // http://www.allstar.fiu.edu/aero/ILS.htm confirms both the 4x sensitvity
474     // increase, and also the 'full-deflection is 10-degrees for a VOR' clamp
475     _cdiDeflection *= 4.0;
476   }
477   SG_CLAMP_RANGE(_cdiDeflection, -10.0, 10.0 );
478   _cdiDeflection *= signal_quality_norm;
479   
480   // cross-track error (in metres)
481   _cdiCrossTrackErrorM = loc_dist * sin(r * SGD_DEGREES_TO_RADIANS);
482   
483   updateGlideSlope(dt, aircraft, signal_quality_norm);
484   
485   last_loc_dist = loc_dist;
486 }
487
488 void FGNavRadio::updateGlideSlope(double dt, const SGVec3d& aircraft, double signal_quality_norm)
489 {
490   if (!_gs || !inrange_node->getBoolValue()) {
491     gs_dist_node->setDoubleValue( 0.0 );
492     return;
493   }
494   
495   // find closest distance to the gs base line
496   double dist = sgdClosestPointToLineDistSquared(aircraft.data(), _gs->cart().data(),
497                                                  gs_base_vec.data());
498   dist = sqrt(dist);
499   gs_dist_node->setDoubleValue(dist);
500   double heightAboveStationM = 
501     (alt_node->getDoubleValue() - _gs->elevation()) * SG_FEET_TO_METER;
502   
503   //////////////////////////////////////////////////////////
504   // compute the amount of glide slope needle deflection
505   // (.i.e. the number of degrees we are off the glide slope * 5.0
506   //
507   // CLO - 13 Mar 2006: The glide slope needle should peg at
508   // +/-0.7 degrees off the ideal glideslope.  I'm not sure why
509   // we compute the factor the way we do (5*gs_error), but we
510   // need to compensate for our 'odd' number in the glideslope
511   // needle animation.  This means that the needle should peg
512   // when this values is +/-3.5.
513   //////////////////////////////////////////////////////////
514   double angle = atan2(heightAboveStationM, dist) * SGD_RADIANS_TO_DEGREES;
515   double deflectionAngle = target_gs - angle;
516   //SG_CLAMP_RANGE(deflectionAngle, -0.7, 0.7);
517   _gsNeedleDeflection = deflectionAngle * 5.0;
518   _gsNeedleDeflection *= signal_quality_norm;
519   _gsNeedleDeflectionNorm = (deflectionAngle / 0.7) * signal_quality_norm;
520   SG_CLAMP_RANGE(_gsNeedleDeflectionNorm, -1.0, 1.0);
521   
522   //////////////////////////////////////////////////////////
523   // Calculate desired rate of climb for intercepting the GS
524   //////////////////////////////////////////////////////////
525   double gs_diff = target_gs - angle;
526   // convert desired vertical path angle into a climb rate
527   double des_angle = angle - 10 * gs_diff;
528
529   // estimate horizontal speed towards ILS in meters per minute
530   double elapsedDistance = last_x - dist;
531   last_x = dist;
532       
533   double new_vel = ( elapsedDistance / dt );
534   horiz_vel = 0.75 * horiz_vel + 0.25 * new_vel;
535
536   gs_rate_of_climb_node
537       ->setDoubleValue( -sin( des_angle * SGD_DEGREES_TO_RADIANS )
538                         * horiz_vel * SG_METER_TO_FEET );
539 }
540
541 void FGNavRadio::updateGPSSlaved()
542 {
543   has_gs_node->setBoolValue(gps_has_gs_node->getBoolValue());
544  
545   _toFlag = gps_to_flag_node->getBoolValue();
546   _fromFlag = gps_from_flag_node->getBoolValue();
547
548   inrange_node->setBoolValue(_toFlag | _fromFlag);
549   
550   _cdiDeflection =  gps_cdi_deflection_node->getDoubleValue();
551   // clmap to some range (+/- 10 degrees) as the regular deflection
552   SG_CLAMP_RANGE(_cdiDeflection, -10.0, 10.0 );
553   
554   _cdiCrossTrackErrorM = 0.0; // FIXME, supply this
555   _gsNeedleDeflection = 0.0; // FIXME, supply this
556 }
557
558 void FGNavRadio::updateCDI(double dt)
559 {
560   bool cdi_serviceable = cdi_serviceable_node->getBoolValue();
561   bool inrange = inrange_node->getBoolValue();
562                                
563   if (tofrom_serviceable_node->getBoolValue()) {
564     to_flag_node->setBoolValue(_toFlag);
565     from_flag_node->setBoolValue(_fromFlag);
566   } else {
567     to_flag_node->setBoolValue(false);
568     from_flag_node->setBoolValue(false);
569   }
570   
571   if (!cdi_serviceable) {
572     _cdiDeflection = 0.0;
573     _cdiCrossTrackErrorM = 0.0;
574   }
575   
576   cdi_deflection_node->setDoubleValue(_cdiDeflection);
577   cdi_deflection_norm_node->setDoubleValue(_cdiDeflection * 0.1);
578   cdi_xtrack_error_node->setDoubleValue(_cdiCrossTrackErrorM);
579
580   //////////////////////////////////////////////////////////
581   // compute an approximate ground track heading error
582   //////////////////////////////////////////////////////////
583   double hdg_error = 0.0;
584   if ( inrange && cdi_serviceable ) {
585     double vn = fgGetDouble( "/velocities/speed-north-fps" );
586     double ve = fgGetDouble( "/velocities/speed-east-fps" );
587     double gnd_trk_true = atan2( ve, vn ) * SGD_RADIANS_TO_DEGREES;
588     if ( gnd_trk_true < 0.0 ) { gnd_trk_true += 360.0; }
589
590     SGPropertyNode *true_hdg
591         = fgGetNode("/orientation/heading-deg", true);
592     hdg_error = gnd_trk_true - true_hdg->getDoubleValue();
593
594     // cout << "ground track = " << gnd_trk_true
595     //      << " orientation = " << true_hdg->getDoubleValue() << endl;
596   }
597   cdi_xtrack_hdg_err_node->setDoubleValue( hdg_error );
598
599   //////////////////////////////////////////////////////////
600   // Calculate a suggested target heading to smoothly intercept
601   // a nav/ils radial.
602   //////////////////////////////////////////////////////////
603
604   // Now that we have cross track heading adjustment built in,
605   // we shouldn't need to overdrive the heading angle within 8km
606   // of the station.
607   //
608   // The cdi deflection should be +/-10 for a full range of deflection
609   // so multiplying this by 3 gives us +/- 30 degrees heading
610   // compensation.
611   double adjustment = _cdiDeflection * 3.0;
612   SG_CLAMP_RANGE( adjustment, -30.0, 30.0 );
613
614   // determine the target heading to fly to intercept the
615   // tgt_radial = target radial (true) + cdi offset adjustmest -
616   // xtrack heading error adjustment
617   double nta_hdg;
618   double trtrue = target_radial_true_node->getDoubleValue();
619   if ( loc_node->getBoolValue() && backcourse_node->getBoolValue() ) {
620       // tuned to a localizer and backcourse mode activated
621       trtrue += 180.0;   // reverse the target localizer heading
622       SG_NORMALIZE_RANGE(trtrue, 0.0, 360.0);
623       nta_hdg = trtrue - adjustment - hdg_error;
624   } else {
625       nta_hdg = trtrue + adjustment - hdg_error;
626   }
627
628   SG_NORMALIZE_RANGE(nta_hdg, 0.0, 360.0);
629   target_auto_hdg_node->setDoubleValue( nta_hdg );
630
631   //////////////////////////////////////////////////////////
632   // compute the time to intercept selected radial (based on
633   // current and last cross track errors and dt
634   //////////////////////////////////////////////////////////
635   double t = 0.0;
636   if ( inrange && cdi_serviceable ) {
637     double xrate_ms = (last_xtrack_error - _cdiCrossTrackErrorM) / dt;
638     if ( fabs(xrate_ms) > 0.00001 ) {
639         t = _cdiCrossTrackErrorM / xrate_ms;
640     } else {
641         t = 9999.9;
642     }
643   }
644   time_to_intercept->setDoubleValue( t );
645
646   if (!gs_serviceable_node->getBoolValue() ) {
647     _gsNeedleDeflection = 0.0;
648     _gsNeedleDeflectionNorm = 0.0;
649   }
650   gs_deflection_node->setDoubleValue(_gsNeedleDeflection);
651   gs_deflection_norm_node->setDoubleValue(_gsNeedleDeflectionNorm);
652   
653   last_xtrack_error = _cdiCrossTrackErrorM;
654 }
655
656 void FGNavRadio::updateAudio()
657 {
658   if (!_navaid || !inrange_node->getBoolValue() || !nav_serviceable_node->getBoolValue()) {
659     return;
660   }
661   
662         // play station ident via audio system if on + ident,
663         // otherwise turn it off
664         if (!power_btn_node->getBoolValue()
665       || !(bus_power_node->getDoubleValue() > 1.0)
666       || !ident_btn_node->getBoolValue()
667       || !audio_btn_node->getBoolValue() ) {
668     globals->get_soundmgr()->stop( nav_fx_name );
669     globals->get_soundmgr()->stop( dme_fx_name );
670     return;
671   }
672
673   SGSoundSample *sound = globals->get_soundmgr()->find( nav_fx_name );
674   double vol = vol_btn_node->getDoubleValue();
675   SG_CLAMP_RANGE(vol, 0.0, 1.0);
676   
677   if ( sound != NULL ) {
678     sound->set_volume( vol );
679   } else {
680     SG_LOG( SG_COCKPIT, SG_ALERT, "Can't find nav-vor-ident sound" );
681   }
682   
683   sound = globals->get_soundmgr()->find( dme_fx_name );
684   if ( sound != NULL ) {
685     sound->set_volume( vol );
686   } else {
687     SG_LOG( SG_COCKPIT, SG_ALERT, "Can't find nav-dme-ident sound" );
688   }
689
690   if ( last_time < globals->get_time_params()->get_cur_time() - 30 ) {
691                 last_time = globals->get_time_params()->get_cur_time();
692                 play_count = 0;
693   }
694   
695   if ( play_count < 4 ) {
696                 // play VOR ident
697                 if ( !globals->get_soundmgr()->is_playing(nav_fx_name) ) {
698                     globals->get_soundmgr()->play_once( nav_fx_name );
699                     ++play_count;
700     }
701   } else if ( play_count < 5 &&  has_dme) {
702                 // play DME ident
703                 if ( !globals->get_soundmgr()->is_playing(nav_fx_name) &&
704                      !globals->get_soundmgr()->is_playing(dme_fx_name) ) {
705                     globals->get_soundmgr()->play_once( dme_fx_name );
706                     ++play_count;
707                 }
708   }
709 }
710
711 FGNavRecord* FGNavRadio::findPrimaryNavaid(const SGGeod& aPos, double aFreqMHz)
712 {
713   FGNavRecord* nav = globals->get_navlist()->findByFreq(aFreqMHz, aPos);
714   if (nav) {
715     return nav;
716   }
717   
718   return globals->get_loclist()->findByFreq(aFreqMHz, aPos);
719 }
720
721 // Update current nav/adf radio stations based on current postition
722 void FGNavRadio::search() 
723 {
724   _time_before_search_sec = 1.0;
725   SGGeod pos = SGGeod::fromDegFt(lon_node->getDoubleValue(),
726     lat_node->getDoubleValue(), alt_node->getDoubleValue());
727   double freq = freq_node->getDoubleValue();
728   
729   FGNavRecord* nav = findPrimaryNavaid(pos, freq);
730   if (nav == _navaid) {
731     return; // found the same as last search, we're done
732   }
733   
734   _navaid = nav;
735   char identBuffer[5] = "    ";
736   if (nav) {
737     FGNavRecord* dme = globals->get_dmelist()->findByFreq(freq, pos);
738     has_dme = (dme != NULL);
739     
740     nav_id_node->setStringValue(nav->get_ident());
741     strncpy(identBuffer, nav->ident().c_str(), 5);
742     
743     effective_range = adjustNavRange(nav->get_elev_ft(), pos.getElevationM(), nav->get_range());
744     loc_node->setBoolValue(nav->type() != FGPositioned::VOR);
745     twist = nav->get_multiuse();
746
747     if (nav->type() == FGPositioned::VOR) {
748       target_radial = sel_radial_node->getDoubleValue();
749       _gs = NULL;
750     } else { // ILS or LOC
751       _gs = globals->get_gslist()->findByFreq(freq, pos);
752       has_gs_node->setBoolValue(_gs != NULL);
753       twist = 0.0;
754             effective_range = FG_LOC_DEFAULT_RANGE;
755       
756       target_radial = nav->get_multiuse();
757       SG_NORMALIZE_RANGE(target_radial, 0.0, 360.0);
758       
759       if (_gs) {
760         int tmp = (int)(_gs->get_multiuse() / 1000.0);
761         target_gs = (double)tmp / 100.0;
762         
763         SGGeod baseLine; 
764         double dummy;
765         SGGeodesy::direct(_gs->geod(), target_radial + 90.0, 100.0, baseLine, dummy);
766         gs_base_vec = SGVec3d::fromGeod(baseLine) - _gs->cart();
767       } // of have glideslope
768     } // of found LOC or ILS
769     
770     audioNavidChanged();
771   } else { // found nothing
772     _gs = NULL;
773     nav_id_node->setStringValue("");
774     has_dme = false;
775     globals->get_soundmgr()->remove( nav_fx_name );
776     globals->get_soundmgr()->remove( dme_fx_name );
777   }
778
779   is_valid_node->setBoolValue(nav != NULL);
780   id_c1_node->setIntValue( (int)identBuffer[0] );
781   id_c2_node->setIntValue( (int)identBuffer[1] );
782   id_c3_node->setIntValue( (int)identBuffer[2] );
783   id_c4_node->setIntValue( (int)identBuffer[3] );
784 }
785
786 void FGNavRadio::audioNavidChanged()
787 {
788   if ( globals->get_soundmgr()->exists(nav_fx_name)) {
789                 globals->get_soundmgr()->remove(nav_fx_name);
790   }
791   
792   try {
793     string trans_ident(_navaid->get_trans_ident());
794     SGSoundSample* sound = morse.make_ident(trans_ident, LO_FREQUENCY);
795     sound->set_volume( 0.3 );
796     if (!globals->get_soundmgr()->add( sound, nav_fx_name )) {
797       SG_LOG(SG_COCKPIT, SG_WARN, "Failed to add v1-vor-ident sound");
798     }
799
800           if ( globals->get_soundmgr()->exists( dme_fx_name ) ) {
801       globals->get_soundmgr()->remove( dme_fx_name );
802     }
803      
804     sound = morse.make_ident( trans_ident, HI_FREQUENCY );
805     sound->set_volume( 0.3 );
806     globals->get_soundmgr()->add( sound, dme_fx_name );
807
808           int offset = (int)(sg_random() * 30.0);
809           play_count = offset / 4;
810     last_time = globals->get_time_params()->get_cur_time() - offset;
811   } catch (sg_io_exception& e) {
812     SG_LOG(SG_GENERAL, SG_ALERT, e.getFormattedMessage());
813   }
814 }