d98fb88994f5dea14913bc890f17e70b4a8e6f3a
[fg:fgdata.git] / Nasal / canvas / map / navdisplay.mfd
1 # ==============================================================================
2 # Boeing Navigation Display by Gijs de Rooy
3 # See: http://wiki.flightgear.org/Canvas_ND_Framework
4 # ==============================================================================
5
6
7
8 ##
9 # this file contains a hash that declares features in a generic fashion
10 # we want to get rid of the bloated update() method sooner than later
11 # PLEASE DO NOT ADD any code to update() !! 
12 # Instead, help clean up the file and move things over to the navdisplay.styles file
13
14 # This is the only sane way to keep on generalizing the framework, so that we can
15 # also support different makes/models of NDs in the future
16 #
17 # a huge bloated update() method is going to make that basically IMPOSSIBLE
18 #
19 io.include("Nasal/canvas/map/navdisplay.styles");
20
21 ##
22 # encapsulate hdg/lat/lon source, so that the ND may also display AI/MP aircraft in a pilot-view at some point (aka stress-testing)
23 # TODO: this predates aircraftpos.controller (MapStructure) should probably be unified to some degree ...
24
25 var NDSourceDriver = {};
26 NDSourceDriver.new = func {
27         var m = {parents:[NDSourceDriver]};
28         m.get_hdg_mag= func getprop("/orientation/heading-magnetic-deg");
29         m.get_hdg_tru= func getprop("/orientation/heading-deg");
30         m.get_hgg = func getprop("instrumentation/afds/settings/heading");
31         m.get_trk_mag= func
32         {
33                 if(getprop("/velocities/groundspeed-kt") > 80)
34                         getprop("/orientation/track-magnetic-deg");
35                 else
36                         getprop("/orientation/heading-magnetic-deg");
37         };
38         m.get_trk_tru = func
39         {
40                 if(getprop("/velocities/groundspeed-kt") > 80)
41                         getprop("/orientation/track-deg");
42                 else
43                         getprop("/orientation/heading-deg");
44         };
45         m.get_lat= func getprop("/position/latitude-deg");
46         m.get_lon= func getprop("/position/longitude-deg");
47         m.get_spd= func getprop("/instrumentation/airspeed-indicator/true-speed-kt");
48         m.get_gnd_spd= func getprop("/velocities/groundspeed-kt");
49         m.get_vspd= func getprop("/velocities/vertical-speed-fps");
50         return m;
51 }
52
53 ##
54 # configure aircraft specific cockpit switches here
55 # these are some defaults, can be overridden when calling NavDisplay.new() -
56 # see the 744 ND.nas file the backend code should never deal directly with
57 # aircraft specific properties using getprop.
58 # To get started implementing your own ND, just copy the switches hash to your
59 # ND.nas file and map the keys to your cockpit properties - and things will just work.
60
61 # TODO: switches are ND specific, so move to the NDStyle hash!
62
63 var default_switches = {
64         'toggle_range':        {path: '/inputs/range-nm', value:40, type:'INT'},
65         'toggle_weather':      {path: '/inputs/wxr', value:0, type:'BOOL'},
66         'toggle_airports':     {path: '/inputs/arpt', value:0, type:'BOOL'},
67         'toggle_stations':     {path: '/inputs/sta', value:0, type:'BOOL'},
68         'toggle_waypoints':    {path: '/inputs/wpt', value:0, type:'BOOL'},
69         'toggle_position':     {path: '/inputs/pos', value:0, type:'BOOL'},
70         'toggle_data':         {path: '/inputs/data',value:0, type:'BOOL'},
71         'toggle_terrain':      {path: '/inputs/terr',value:0, type:'BOOL'},
72         'toggle_traffic':      {path: '/inputs/tfc',value:0, type:'BOOL'},
73         'toggle_centered':     {path: '/inputs/nd-centered',value:0, type:'BOOL'},
74         'toggle_lh_vor_adf':   {path: '/inputs/lh-vor-adf',value:0, type:'INT'},
75         'toggle_rh_vor_adf':   {path: '/inputs/rh-vor-adf',value:0, type:'INT'},
76         'toggle_display_mode': {path: '/mfd/display-mode', value:'MAP', type:'STRING'}, # valid values are: APP, MAP, PLAN or VOR
77         'toggle_display_type': {path: '/mfd/display-type', value:'CRT', type:'STRING'}, # valid values are: CRT or LCD
78         'toggle_true_north':   {path: '/mfd/true-north', value:0, type:'BOOL'},
79         'toggle_rangearc':     {path: '/mfd/rangearc', value:0, type:'BOOL'},
80         'toggle_track_heading':{path: '/trk-selected', value:0, type:'BOOL'},
81 };
82
83 ##
84 # TODO:
85 # - introduce a MFD class (use it also for PFD/EICAS)
86 # - introduce a SGSubsystem class and use it  here
87 # - introduce a Boeing NavDisplay class
88 var NavDisplay = {
89         # static
90         id:0,
91
92         del: func {
93                 print("Cleaning up NavDisplay");
94                 # shut down all timers and other loops here
95                 me.update_timer.stop();
96                 foreach(var t; me.timers)
97                         t.stop();
98                 foreach(var l; me.listeners)
99                         removelistener(l);
100                 # clean up MapStructure
101                 me.map.del();
102                 # destroy the canvas
103                 if (me.canvas_handle != nil)
104                         me.canvas_handle.del();
105                 me.inited = 0;
106                 NavDisplay.id -= 1;
107         },
108
109         addtimer: func(interval, cb) {
110                 append(me.timers, var job=maketimer(interval, cb));
111                 return job; # so that we can directly work with the timer (start/stop)
112         },
113
114         listen: func(p,c) {
115                 append(me.listeners, setlistener(p,c));
116         },
117
118         # listeners for cockpit switches
119         listen_switch: func(s,c) {
120                 # print("event setup for: ", id(c));
121                 me.listen( me.get_full_switch_path(s), func {
122                         # print("listen_switch triggered:", s, " callback id:", id(c) );
123                         c();
124                 });
125         },
126
127         # get the full property path for a given switch
128         get_full_switch_path: func (s) {
129                 # debug.dump( me.efis_switches[s] );
130                 return me.efis_path ~ me.efis_switches[s].path; # FIXME: should be using props.nas instead of ~
131         },
132
133         # helper method for getting configurable cockpit switches (which are usually different in each aircraft)
134         get_switch: func(s) {
135                 var switch = me.efis_switches[s];
136                 var path = me.efis_path ~ switch.path ;
137                 #print(s,":Getting switch prop:", path);
138
139                 return getprop( path );
140         },
141
142         # for creating NDs that are driven by AI traffic instead of the main aircraft (generalization rocks!)
143         connectAI: func(source=nil) {
144                 me.aircraft_source = {
145                         get_hdg_mag: func source.getNode('orientation/heading-magnetic-deg').getValue(),
146                         get_trk_mag: func source.getNode('orientation/track-magnetic-deg').getValue(),
147                         get_lat: func source.getNode('position/latitude-deg').getValue(),
148                         get_lon: func source.getNode('position/longitude-deg').getValue(),
149                         get_spd: func source.getNode('velocities/true-airspeed-kt').getValue(),
150                         get_gnd_spd: func source.getNode('velocities/groundspeed-kt').getValue(),
151                 };
152         }, # of connectAI
153
154         setTimerInterval: func(update_time=0.05) me.update_timer.restart(update_time),
155
156         # TODO: the ctor should allow customization, for different aircraft
157         # especially properties and SVG files/handles (747, 757, 777 etc)
158         new : func(prop1, switches=default_switches, style='Boeing') {
159                 NavDisplay.id +=1;
160                 var m = { parents : [NavDisplay]};
161
162                 m.inited = 0;
163
164                 m.timers=[]; 
165                 m.listeners=[]; # for cleanup handling
166                 m.aircraft_source = NDSourceDriver.new(); # uses the main aircraft as the driver/source (speeds, position, heading)
167
168                 m.nd_style = NDStyles[style]; # look up ND specific stuff (file names etc)
169
170                 m.radio_list=["instrumentation/comm/frequencies","instrumentation/comm[1]/frequencies",
171                               "instrumentation/nav/frequencies", "instrumentation/nav[1]/frequencies"];
172                 m.mfd_mode_list=["APP","VOR","MAP","PLAN"];
173
174                 m.efis_path = prop1;
175                 m.efis_switches = switches;
176
177                 # just an alias, to avoid having to rewrite the old code for now
178                 m.rangeNm = func m.get_switch('toggle_range');
179
180                 m.efis = props.globals.initNode(prop1);
181                 m.mfd = m.efis.initNode("mfd");
182
183                 # TODO: unify this with switch handling
184                 m.mfd_mode_num     = m.mfd .initNode("mode-num",2,"INT");
185                 m.std_mode         = m.efis.initNode("inputs/setting-std",0,"BOOL");
186                 m.previous_set     = m.efis.initNode("inhg-previous",29.92);
187                 m.kpa_mode         = m.efis.initNode("inputs/kpa-mode",0,"BOOL");
188                 m.kpa_output       = m.efis.initNode("inhg-kpa",29.92);
189                 m.kpa_prevoutput   = m.efis.initNode("inhg-kpa-previous",29.92);
190                 m.temp             = m.efis.initNode("fixed-temp",0);
191                 m.alt_meters       = m.efis.initNode("inputs/alt-meters",0,"BOOL");
192                 m.fpv              = m.efis.initNode("inputs/fpv",0,"BOOL");
193
194                 m.mins_mode     = m.efis.initNode("inputs/minimums-mode",0,"BOOL");
195                 m.mins_mode_txt = m.efis.initNode("minimums-mode-text","RADIO","STRING");
196                 m.minimums      = m.efis.initNode("minimums",250,"INT");
197                 m.mk_minimums   = props.globals.getNode("instrumentation/mk-viii/inputs/arinc429/decision-height");
198
199                 # TODO: these are switches, can be unified with switch handling hash above (eventually):
200                 m.nd_plan_wpt = m.efis.initNode("inputs/plan-wpt-index", 0, "INT"); # not yet in switches hash
201
202                 ###
203                 # initialize all switches based on the defaults specified in the switch hash
204                 #
205                 foreach(var switch; keys( m.efis_switches ) )
206                         props.globals.initNode
207                                 (       m.get_full_switch_path (switch),
208                                         m.efis_switches[switch].value,
209                                         m.efis_switches[switch].type
210                                 );
211
212
213                 return m;
214         },
215         newMFD: func(canvas_group, parent=nil, options=nil, update_time=0.05)
216         {
217                 if (me.inited) die("MFD already was added to scene");
218                 me.inited = 1;
219                 me.update_timer = maketimer(update_time, func me.update() );
220                 me.nd = canvas_group;
221                 me.canvas_handle = parent;
222
223                 # load the specified SVG file into the me.nd group and populate all sub groups
224
225                 canvas.parsesvg(me.nd, me.nd_style.svg_filename, {'font-mapper': me.nd_style.font_mapper});
226                 me.symbols = {}; # storage for SVG elements, to avoid namespace pollution (all SVG elements end up  here)
227
228                 foreach(var feature; me.nd_style.features ) {
229                         me.symbols[feature.id] = me.nd.getElementById(feature.id).updateCenter();
230                         if(contains(feature.impl,'init')) feature.impl.init(me.nd, feature); # call The element's init code (i.e. updateCenter)
231                 }
232
233                 ### this is the "old" method that's less flexible, we want to use the style hash instead (see above)
234                 # because things are much better configurable that way
235                 # now look up all required SVG elements and initialize member fields using the same name  to have a convenient handle
236                 foreach(var element; ["dmeLDist","dmeRDist","dmeL","dmeR","vorL","vorR","vorLId","vorRId",
237                                       "status.wxr","status.wpt","status.sta","status.arpt"])
238                         me.symbols[element] = me.nd.getElementById(element);
239
240                 # load elements from vector image, and create instance variables using identical names, and call updateCenter() on each
241                 # anything that needs updatecenter called, should be added to the vector here
242                 #
243                 foreach(var element; ["staArrowL2","staArrowR2","staFromL2","staToL2","staFromR2","staToR2",
244                                       "hdgTrk","trkInd","hdgBug","HdgBugCRT","TrkBugLCD","HdgBugLCD","curHdgPtr",
245                                       "HdgBugCRT2","TrkBugLCD2","HdgBugLCD2","hdgBug2","selHdgLine","selHdgLine2","curHdgPtr2",
246                                       "staArrowL","staArrowR","staToL","staFromL","staToR","staFromR"] )
247                         me.symbols[element] = me.nd.getElementById(element).updateCenter();
248
249                 me.map = me.nd.createChild("map","map")
250                         .set("clip", "rect(124, 1024, 1024, 0)")
251                         .set("screen-range", 700);
252
253                 me.update_sub(); # init some map properties based on switches
254
255                 # predicate for the draw controller
256                 var is_tuned = func(freq) {
257                         var nav1=getprop("instrumentation/nav[0]/frequencies/selected-mhz");
258                         var nav2=getprop("instrumentation/nav[1]/frequencies/selected-mhz");
259                         if (freq == nav1 or freq == nav2) return 1;
260                         return 0;
261                 }
262
263                 # another predicate for the draw controller
264                 var get_course_by_freq = func(freq) {
265                         if (freq == getprop("instrumentation/nav[0]/frequencies/selected-mhz"))
266                                 return getprop("instrumentation/nav[0]/radials/selected-deg");
267                         else
268                                 return getprop("instrumentation/nav[1]/radials/selected-deg");
269                 }
270
271                 var get_current_position = func {
272                         delete(caller(0)[0], "me"); # remove local me, inherit outer one
273                         return [
274                                 me.aircraft_source.get_lat(), me.aircraft_source.get_lon()
275                         ];
276                 }
277
278                 # a hash with controller callbacks, will be passed onto draw routines to customize behavior/appearance
279                 # the point being that draw routines don't know anything about their frontends (instrument or GUI dialog)
280                 # so we need some simple way to communicate between frontend<->backend until we have real controllers
281                 # for now, a single controller hash is shared by most layers - unsupported callbacks are simply ignored by the draw files
282                 #
283
284                 var controller = {
285                         parents: [canvas.Map.Controller],
286                         _pos: nil, _time: nil,
287                         is_tuned:is_tuned,
288                         get_tuned_course:get_course_by_freq,
289                         get_position: get_current_position,
290                         new: func(map) return { parents:[controller], map:map },
291                         should_update_all: func {
292                                 # TODO: this is just copied from aircraftpos.controller,
293                                 # it really should be moved to somewhere common and reused
294                                 # and extended to fully differentiate between "static"
295                                 # and "volatile" layers.
296                                 var pos = me.map.getPosCoord();
297                                 if (pos == nil) return 0;
298                                 var time = systime();
299                                 if (me._pos == nil)
300                                         me._pos = geo.Coord.new(pos);
301                                 else {
302                                         var dist_m = me._pos.direct_distance_to(pos);
303                                         # 2 NM until we update again
304                                         if (dist_m < 2 * NM2M) return 0;
305                                         # Update at most every 4 seconds to avoid excessive stutter:
306                                         elsif (time - me._time < 4) return 0;
307                                 }
308                                 #print("update aircraft position");
309                                 var (x,y,z) = pos.xyz();
310                                 me._pos.set_xyz(x,y,z);
311                                 me._time = time;
312                                 return 1;
313                         },
314                 };
315                 me.map.setController(controller);
316
317                 ###
318                 # set up various layers, controlled via callbacks in the controller hash
319                 # revisit this code once Philosopher's "Smart MVC Symbols/Layers" work is committed and integrated
320
321                 # helper / closure generator
322                 var make_event_handler = func(predicate, layer) func predicate(me, layer);
323
324                 me.layers={}; # storage container for all ND specific layers
325                 # look up all required layers as specified per the NDStyle hash and do the initial setup for event handling
326                 foreach(var layer; me.nd_style.layers) {
327                         if(layer['disabled']) continue; # skip this layer
328                         #print("newMFD(): Setting up ND layer:", layer.name);
329
330                         var the_layer = nil;
331                         if(!layer['isMapStructure']) # set up an old INEFFICIENT and SLOW layer
332                                 the_layer = me.layers[layer.name] = canvas.MAP_LAYERS[layer.name].new( me.map, layer.name, controller );
333                         else {
334                                 printlog(_MP_dbg_lvl, "Setting up MapStructure-based layer for ND, name:", layer.name);
335                                 var opt = options != nil and options[layer.name] != nil ? options[layer.name] :nil;
336                                 # print("Options is: ", opt!=nil?"enabled":"disabled");
337                                 me.map.addLayer(
338                                         factory: canvas.SymbolLayer,
339                                         type_arg: layer.name,
340                                         options:opt,
341                                         visible:0,
342                                         priority: layer['z-index']
343                                 );
344                                 the_layer = me.layers[layer.name] = me.map.getLayer(layer.name);
345                                 if (1) (func {
346                                         var l = layer;
347                                         var _predicate = l.predicate;
348                                         l.predicate = func {
349                                                 var t = systime();
350                                                 call(_predicate, arg, me);
351                                                 printlog(_MP_dbg_lvl, "Took "~((systime()-t)*1000)~"ms to update layer "~l.name);
352                                         }
353                                 })();
354                         }
355
356                         # now register all layer specific notification listeners and their corresponding update predicate/callback
357                         # pass the ND instance and the layer handle to the predicate when it is called
358                         # so that it can directly access the ND instance and its own layer (without having to know the layer's name)
359                         var event_handler = make_event_handler(layer.predicate, the_layer);
360                         foreach(var event; layer.update_on) {
361                                 # this handles timers
362                                 if (typeof(event)=='hash' and contains(event, 'rate_hz')) {
363                                         #print("FIXME: navdisplay.mfd timer handling is broken ATM");
364                                         var job=me.addtimer(1/event.rate_hz, event_handler);    
365                                         job.start();
366                                 }
367                                 # and this listeners    
368                                 else
369                                 # print("Setting up subscription:", event, " for ", layer.name, " handler id:", id(event_handler) );
370                                 me.listen_switch(event, event_handler);
371                         } # foreach event subscription
372                         # and now update/init each layer once by calling its update predicate for initialization
373                         event_handler();
374                 } # foreach layer
375
376                 #print("navdisplay.mfd:ND layer setup completed");
377
378                 # start the update timer, which makes sure that the update() will be called
379                 me.update_timer.start();
380
381                 # TODO: move this to RTE.lcontroller ?
382                 me.listen("/autopilot/route-manager/current-wp", func(activeWp) {
383                         canvas.updatewp( activeWp.getValue() );
384                 });
385
386         },
387
388         in_mode:func(switch, modes)
389         {
390                 foreach(var m; modes) if(me.get_switch(switch)==m) return 1;
391                 return 0;
392         },
393         # Helper function for below (update()) and above (newMFD())
394         # to ensure position etc. are correct.
395         update_sub: func()
396         {
397                 # Variables:
398                 var userLat = me.aircraft_source.get_lat();
399                 var userLon = me.aircraft_source.get_lon();
400                 var userGndSpd = me.aircraft_source.get_gnd_spd();
401                 var userVSpd = me.aircraft_source.get_vspd();
402                 var dispLCD = me.get_switch('toggle_display_type') == "LCD";
403                 # Heading update
404                 var userHdgMag = me.aircraft_source.get_hdg_mag();
405                 var userHdgTru = me.aircraft_source.get_hdg_tru();
406                 var userTrkMag = me.aircraft_source.get_trk_mag();
407                 var userTrkTru = me.aircraft_source.get_trk_tru();
408                 
409                 if(me.get_switch('toggle_true_north')) {
410                         var userHdg=userHdgTru;
411                         me.userHdg=userHdgTru;
412                         var userTrk=userTrkTru;
413                         me.userTrk=userTrkTru;
414                 } else {
415                         var userHdg=userHdgMag;
416                         me.userHdg=userHdgMag;
417                         var userTrk=userTrkMag;
418                         me.userTrk=userTrkMag;
419                 }
420                 # this should only ever happen when testing the experimental AI/MP ND driver hash (not critical)
421                 # or when an error occurs (critical)
422                 if (!userHdg or !userTrk or !userLat or !userLon) {
423                         print("aircraft source invalid, returning !");
424                         return;
425                 }
426                 if (me.aircraft_source.get_gnd_spd() < 80) {
427                         userTrk = userHdg;
428                         me.userTrk=userHdg;
429                 }
430                 
431                 if((me.in_mode('toggle_display_mode', ['MAP']) and me.get_switch('toggle_display_type') == "CRT")
432                     or (me.get_switch('toggle_track_heading') and me.get_switch('toggle_display_type') == "LCD"))
433                 {
434                         userHdgTrk = userTrk;
435                         me.userHdgTrk = userTrk;
436                         userHdgTrkTru = userTrkTru;
437                         me.symbols.hdgTrk.setText("TRK");
438                 } else {
439                         userHdgTrk = userHdg;
440                         me.userHdgTrk = userHdg;
441                         userHdgTrkTru = userHdgTru;
442                         me.symbols.hdgTrk.setText("HDG");
443                 }
444
445                 # First, update the display position of the map
446                 var pos = {
447                         lat: nil, lon: nil,
448                         alt: nil, hdg: nil,
449                         range: nil,
450                 };
451                 # reposition the map, change heading & range:
452                 if(me.in_mode('toggle_display_mode', ['PLAN']) and getprop(me.efis_path ~ "/inputs/plan-wpt-index") >= 0) {
453                         pos.lat = getprop("/autopilot/route-manager/route/wp["~getprop(me.efis_path ~ "/inputs/plan-wpt-index")~"]/latitude-deg");
454                         pos.lon = getprop("/autopilot/route-manager/route/wp["~getprop(me.efis_path ~ "/inputs/plan-wpt-index")~"]/longitude-deg");
455                 } else {
456                         pos.lat = userLat;
457                         pos.lon = userLon;
458                 }
459                 if(me.in_mode('toggle_display_mode', ['PLAN'])) {
460                         pos.hdg = 0;
461                         pos.range = me.rangeNm()*2
462                 } else {
463                         pos.range = me.rangeNm(); # avoid this  here, use a listener instead
464                         pos.hdg = userHdgTrkTru;
465                 }
466                 call(me.map.setPos, [pos.lat, pos.lon], me.map, pos);
467         },
468         # each model should keep track of when it last got updated, using current lat/lon
469         # in update(), we can then check if the aircraft has traveled more than 0.5-1 nm (depending on selected range)
470         # and update each model accordingly
471         # TODO: Hooray is still waiting for a really rainy weekend to clean up all the mess here... so plz don't add to it!
472         update: func() # FIXME: This stuff is still too aircraft specific, cannot easily be reused by other aircraft
473         {
474                 var _time = systime();
475
476                 call(me.update_sub, nil, nil, caller(0)[0]); # call this in the same namespace to "steal" its variables
477
478                 # MapStructure update!
479                 if (me.map.controller.should_update_all()) {
480                         me.map.update();
481                 } else {
482                         # TODO: ugly list here
483                         # FIXME: use a VOLATILE layer helper here that handles TFC, APS, WXR etc ?
484                         me.map.update(func(layer) (var n=layer.type) == "TFC" or n == "APS");
485                 }
486
487                 # Other symbol update
488                 # TODO: should be refactored!
489                 if(me.in_mode('toggle_display_mode', ['PLAN']))
490                         me.map.setTranslation(512,512);
491                 elsif(me.get_switch('toggle_centered'))
492                         me.map.setTranslation(512,565);
493                 else
494                         me.map.setTranslation(512,824);
495
496                 if(me.get_switch('toggle_rh_vor_adf') == 1) {
497                         me.symbols.vorR.setText("VOR R");
498                         me.symbols.vorR.setColor(0.195,0.96,0.097);
499                         me.symbols.dmeR.setText("DME");
500                         me.symbols.dmeR.setColor(0.195,0.96,0.097);
501                         if(getprop("instrumentation/nav[1]/in-range"))
502                                 me.symbols.vorRId.setText(getprop("instrumentation/nav[1]/nav-id"));
503                         else
504                                 me.symbols.vorRId.setText(getprop("instrumentation/nav[1]/frequencies/selected-mhz-fmt"));
505                         me.symbols.vorRId.setColor(0.195,0.96,0.097);
506                         if(getprop("instrumentation/dme[1]/in-range"))
507                                 me.symbols.dmeRDist.setText(sprintf("%3.1f",getprop("instrumentation/dme[1]/indicated-distance-nm")));
508                         else me.symbols.dmeRDist.setText(" ---");
509                         me.symbols.dmeRDist.setColor(0.195,0.96,0.097);
510                 } elsif(me.get_switch('toggle_rh_vor_adf') == -1) {
511                         me.symbols.vorR.setText("ADF R");
512                         me.symbols.vorR.setColor(0,0.6,0.85);
513                         me.symbols.dmeR.setText("");
514                         me.symbols.dmeR.setColor(0,0.6,0.85);
515                         if((var navident=getprop("instrumentation/adf[1]/ident")) != "")
516                                 me.symbols.vorRId.setText(navident);
517                         else me.symbols.vorRId.setText(sprintf("%3d",getprop("instrumentation/adf[1]/frequencies/selected-khz")));
518                         me.symbols.vorRId.setColor(0,0.6,0.85);
519                         me.symbols.dmeRDist.setText("");
520                         me.symbols.dmeRDist.setColor(0,0.6,0.85);
521                 } else {
522                         me.symbols.vorR.setText("");
523                         me.symbols.dmeR.setText("");
524                         me.symbols.vorRId.setText("");
525                         me.symbols.dmeRDist.setText("");
526                 }
527
528                 # Hide heading bug 10 secs after change
529                 var vhdg_bug = getprop("autopilot/settings/heading-bug-deg") or 0;
530                 var hdg_bug_active = getprop("autopilot/settings/heading-bug-active");
531                 if (hdg_bug_active == nil)
532                         hdg_bug_active = 1;
533                                                 
534                 if((me.in_mode('toggle_display_mode', ['MAP']) and me.get_switch('toggle_display_type') == "CRT")
535                         or (me.get_switch('toggle_track_heading') and me.get_switch('toggle_display_type') == "LCD"))
536                 {
537                         me.symbols.trkInd.setRotation(0);
538                         me.symbols.curHdgPtr.setRotation((userHdg-userTrk)*D2R);
539                         me.symbols.curHdgPtr2.setRotation((userHdg-userTrk)*D2R);
540                 }
541                 else
542                 {
543                         me.symbols.trkInd.setRotation((userTrk-userHdg)*D2R);
544                         me.symbols.curHdgPtr.setRotation(0);
545                         me.symbols.curHdgPtr2.setRotation(0);
546                 }
547                 if(!me.in_mode('toggle_display_mode', ['PLAN']))
548                 {
549                         var hdgBugRot = (vhdg_bug-userHdgTrk)*D2R;
550                         me.symbols.selHdgLine.setRotation(hdgBugRot);
551                         me.symbols.hdgBug.setRotation(hdgBugRot);
552                         me.symbols.hdgBug2.setRotation(hdgBugRot);
553                         me.symbols.selHdgLine2.setRotation(hdgBugRot);
554                 }
555
556                 var staPtrVis = !me.in_mode('toggle_display_mode', ['PLAN']);
557                 if((me.in_mode('toggle_display_mode', ['MAP']) and me.get_switch('toggle_display_type') == "CRT")
558                     or (me.get_switch('toggle_track_heading') and me.get_switch('toggle_display_type') == "LCD"))
559                 {
560                         var vorheading = userTrkTru;
561                         var adfheading = userTrkMag;
562                 }
563                 else
564                 {
565                         var vorheading = userHdgTru;
566                         var adfheading = userHdgMag;
567                 }
568                 if(getprop("instrumentation/nav/heading-deg") != nil)
569                         var nav0hdg=getprop("instrumentation/nav/heading-deg") - vorheading;
570                 if(getprop("instrumentation/nav[1]/heading-deg") != nil)
571                         var nav1hdg=getprop("instrumentation/nav[1]/heading-deg") - vorheading;
572                 var adf0hdg=getprop("instrumentation/adf/indicated-bearing-deg");
573                 var adf1hdg=getprop("instrumentation/adf[1]/indicated-bearing-deg");
574                 if(!me.get_switch('toggle_centered'))
575                 {
576                         if(me.in_mode('toggle_display_mode', ['PLAN']))
577                                 me.symbols.trkInd.hide();
578                         else
579                                 me.symbols.trkInd.show();
580                         if((getprop("instrumentation/nav/in-range") and me.get_switch('toggle_lh_vor_adf') == 1)) {
581                                 me.symbols.staArrowL.setVisible(staPtrVis);
582                                 me.symbols.staToL.setColor(0.195,0.96,0.097);
583                                 me.symbols.staFromL.setColor(0.195,0.96,0.097);
584                                 me.symbols.staArrowL.setRotation(nav0hdg*D2R);
585                         }
586                         elsif(getprop("instrumentation/adf/in-range") and (me.get_switch('toggle_lh_vor_adf') == -1)) {
587                                 me.symbols.staArrowL.setVisible(staPtrVis);
588                                 me.symbols.staToL.setColor(0,0.6,0.85);
589                                 me.symbols.staFromL.setColor(0,0.6,0.85);
590                                 me.symbols.staArrowL.setRotation(adf0hdg*D2R);
591                         } else {
592                                 me.symbols.staArrowL.hide();
593                         }
594                         if((getprop("instrumentation/nav[1]/in-range") and me.get_switch('toggle_rh_vor_adf') == 1)) {
595                                 me.symbols.staArrowR.setVisible(staPtrVis);
596                                 me.symbols.staToR.setColor(0.195,0.96,0.097);
597                                 me.symbols.staFromR.setColor(0.195,0.96,0.097);
598                                 me.symbols.staArrowR.setRotation(nav1hdg*D2R);
599                         } elsif(getprop("instrumentation/adf[1]/in-range") and (me.get_switch('toggle_rh_vor_adf') == -1)) {
600                                 me.symbols.staArrowR.setVisible(staPtrVis);
601                                 me.symbols.staToR.setColor(0,0.6,0.85);
602                                 me.symbols.staFromR.setColor(0,0.6,0.85);
603                                 me.symbols.staArrowR.setRotation(adf1hdg*D2R);
604                         } else {
605                                 me.symbols.staArrowR.hide();
606                         }
607                         me.symbols.staArrowL2.hide();
608                         me.symbols.staArrowR2.hide();
609                         me.symbols.curHdgPtr2.hide();
610                         me.symbols.HdgBugCRT2.hide();
611                         me.symbols.TrkBugLCD2.hide();
612                         me.symbols.HdgBugLCD2.hide();
613                         me.symbols.selHdgLine2.hide();
614                         me.symbols.curHdgPtr.setVisible(staPtrVis);
615                         me.symbols.HdgBugCRT.setVisible(staPtrVis and !dispLCD);
616                         if(me.get_switch('toggle_track_heading'))
617                         {
618                                 me.symbols.HdgBugLCD.hide();
619                                 me.symbols.TrkBugLCD.setVisible(staPtrVis and dispLCD);
620                         }
621                         else
622                         {
623                                 me.symbols.TrkBugLCD.hide();
624                                 me.symbols.HdgBugLCD.setVisible(staPtrVis and dispLCD);
625                         }
626                         me.symbols.selHdgLine.setVisible(staPtrVis and hdg_bug_active);
627                 } else {
628                         me.symbols.trkInd.hide();
629                         if((getprop("instrumentation/nav/in-range")     and me.get_switch('toggle_lh_vor_adf') == 1)) {
630                                 me.symbols.staArrowL2.setVisible(staPtrVis);
631                                 me.symbols.staFromL2.setColor(0.195,0.96,0.097);
632                                 me.symbols.staToL2.setColor(0.195,0.96,0.097);
633                                 me.symbols.staArrowL2.setRotation(nav0hdg*D2R);
634                         } elsif(getprop("instrumentation/adf/in-range") and (me.get_switch('toggle_lh_vor_adf') == -1)) {
635                                 me.symbols.staArrowL2.setVisible(staPtrVis);
636                                 me.symbols.staFromL2.setColor(0,0.6,0.85);
637                                 me.symbols.staToL2.setColor(0,0.6,0.85);
638                                 me.symbols.staArrowL2.setRotation(adf0hdg*D2R);
639                         } else {
640                                 me.symbols.staArrowL2.hide();
641                         }
642                         if((getprop("instrumentation/nav[1]/in-range") and me.get_switch('toggle_rh_vor_adf') == 1)) {
643                                 me.symbols.staArrowR2.setVisible(staPtrVis);
644                                 me.symbols.staFromR2.setColor(0.195,0.96,0.097);
645                                 me.symbols.staToR2.setColor(0.195,0.96,0.097);
646                                 me.symbols.staArrowR2.setRotation(nav1hdg*D2R);
647                         } elsif(getprop("instrumentation/adf[1]/in-range") and (me.get_switch('toggle_rh_vor_adf') == -1)) {
648                                 me.symbols.staArrowR2.setVisible(staPtrVis);
649                                 me.symbols.staFromR2.setColor(0,0.6,0.85);
650                                 me.symbols.staToR2.setColor(0,0.6,0.85);
651                                 me.symbols.staArrowR2.setRotation(adf1hdg*D2R);
652                         } else {
653                                 me.symbols.staArrowR2.hide();
654                         }
655                         me.symbols.staArrowL.hide();
656                         me.symbols.staArrowR.hide();
657                         me.symbols.curHdgPtr.hide();
658                         me.symbols.HdgBugCRT.hide();
659                         me.symbols.TrkBugLCD.hide();
660                         me.symbols.HdgBugLCD.hide();
661                         me.symbols.selHdgLine.hide();
662                         me.symbols.curHdgPtr2.setVisible(staPtrVis);
663                         me.symbols.HdgBugCRT2.setVisible(staPtrVis and !dispLCD);
664                         if(me.get_switch('toggle_track_heading'))
665                         {
666                                 me.symbols.HdgBugLCD2.hide();
667                                 me.symbols.TrkBugLCD2.setVisible(staPtrVis and dispLCD);
668                         }
669                         else
670                         {
671                                 me.symbols.TrkBugLCD2.hide();
672                                 me.symbols.HdgBugLCD2.setVisible(staPtrVis and dispLCD);
673                         }
674                         me.symbols.selHdgLine2.setVisible(staPtrVis and hdg_bug_active);
675                 }
676
677                 ## run all predicates in the NDStyle hash and evaluate their true/false behavior callbacks
678                 ## this is in line with the original design, but normally we don't need to getprop/poll here,
679                 ## using listeners or timers would be more canvas-friendly whenever possible
680                 ## because running setprop() on any group/canvas element at framerate means that the canvas
681                 ## will be updated at frame rate too - wasteful ... (check the performance monitor!)
682
683                 foreach(var feature; me.nd_style.features ) {
684                         # for stuff that always needs to be updated
685                         if (contains(feature.impl, 'common')) feature.impl.common(me);
686                         # conditional stuff
687                         if(!contains(feature.impl, 'predicate')) continue; # no conditional stuff
688                         if ( var result=feature.impl.predicate(me) )
689                                 feature.impl.is_true(me, result); # pass the result to the predicate
690                         else
691                                 feature.impl.is_false( me, result ); # pass the result to the predicate
692                 }
693
694                 ## update the status flags shown on the ND (wxr, wpt, arpt, sta)
695                 # this could/should be using listeners instead ...
696                 me.symbols['status.wxr'].setVisible( me.get_switch('toggle_weather') and me.in_mode('toggle_display_mode', ['MAP']));
697                 me.symbols['status.wpt'].setVisible( me.get_switch('toggle_waypoints') and me.in_mode('toggle_display_mode', ['MAP']));
698                 me.symbols['status.arpt'].setVisible( me.get_switch('toggle_airports') and me.in_mode('toggle_display_mode', ['MAP']));
699                 me.symbols['status.sta'].setVisible( me.get_switch('toggle_stations') and  me.in_mode('toggle_display_mode', ['MAP']));
700                 # Okay, _how_ do we hook this up with FGPlot?
701                 printlog(_MP_dbg_lvl, "Total ND update took "~((systime()-_time)*100)~"ms");
702                 setprop("/instrumentation/navdisplay["~ NavDisplay.id ~"]/update-ms", systime() - _time);
703         } # of update() method (50% of our file ...seriously?)
704 };