NavDisplay bug fixes:
[fg:toms-fgdata.git] / Nasal / canvas / map / navdisplay.mfd
1 # ==============================================================================
2 # Boeing Navigation Display by Gijs de Rooy
3 # ==============================================================================
4
5 ##
6 # do we really need to keep track of each drawable here ??
7 var i = 0;
8
9 ##
10 # pseudo DSL-ish: use these as placeholders in the config hash  below
11 var ALWAYS = func 1;
12 var NOTHING = func nil;
13
14 ##
15 # so that we only need to update a single line ...
16 #
17 var trigger_update = func(layer)  layer._model.init();
18
19 ##
20 # TODO: move ND-specific implementation details into this lookup hash
21 # so that other aircraft and ND types can be more easily supported
22 #
23 # any aircraft-specific ND behavior should be wrapped here,
24 # to isolate/decouple things in the generic NavDisplay class
25 #
26 # TODO: move this to an XML config file
27 #
28 var NDStyles = {
29         ##
30         # this configures the 744 ND to help generalize the NavDisplay class itself
31         'Boeing': {
32                 font_mapper: func(family, weight) {
33                         if( family == "Liberation Sans" and weight == "normal" )
34                                 return "LiberationFonts/LiberationSans-Regular.ttf";
35                 },
36
37                 # where all the symbols are stored
38                 # TODO: SVG elements should be renamed to use boeing/airbus prefix
39                 # aircraft developers should all be editing the same ND.svg image
40                 # the code can deal with the differences now
41                 svg_filename: "Nasal/canvas/map/boeingND.svg",
42                 ##
43                 ## this loads and configures existing layers (currently, *.layer files in Nasal/canvas/map)
44                 ##
45
46                 layers: [
47                         { name:'fixes', disabled:1, update_on:['toggle_range','toggle_waypoints'],
48                                 predicate: func(nd, layer) {
49                                         # print("Running fixes predicate");
50                                         var visible=nd.get_switch('toggle_waypoints') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
51                                         if (visible) {
52                                                 # print("fixes update requested!");
53                                                 trigger_update( layer );
54                                         }
55                                         layer._view.setVisible(visible);
56                                 }, # end of layer update predicate
57                         }, # end of fixes layer
58                         { name:'FIX', isMapStructure:1, update_on:['toggle_range','toggle_waypoints'],
59                                 # FIXME: this is a really ugly place for controller code
60                                 predicate: func(nd, layer) {
61                                         # print("Running vor layer predicate");
62                                         # toggle visibility here
63                                         var visible=nd.get_switch('toggle_waypoints') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
64                                         layer.group.setVisible( nd.get_switch('toggle_waypoints') );
65                                         if (visible) {
66                                                 #print("Updating MapStructure ND layer: FIX");
67                                                 # (Hopefully) smart update
68                                                 layer.update();
69                                         }
70                                 }, # end of layer update predicate
71                         }, # end of FIX layer
72                         # Should redraw every 10 seconds
73                         { name:'storms', update_on:['toggle_range','toggle_weather','toggle_display_mode'],
74                                 predicate: func(nd, layer) {
75                                         # print("Running fixes predicate");
76                                         var visible=nd.get_switch('toggle_weather') and nd.get_switch('toggle_display_mode') != "PLAN";
77                                         if (visible) {
78                                                 #print("storms update requested!");
79                                                 trigger_update( layer );
80                                         }
81                                         layer._view.setVisible(visible);
82                                 }, # end of layer update predicate
83                         }, # end of storms layer
84
85                         { name:'airports-nd', update_on:['toggle_range','toggle_airports','toggle_display_mode'],
86                                 predicate: func(nd, layer) {
87                                         # print("Running airports-nd predicate");
88                                         var visible = nd.get_switch('toggle_airports') and nd.in_mode('toggle_display_mode', ['MAP']);
89                                         if (visible) {
90                                                 trigger_update( layer ); # clear & redraw
91                                         }
92                                         layer._view.setVisible( visible );
93                                 }, # end of layer update predicate
94                         }, # end of airports layer
95
96                         # Should distinct between low and high altitude navaids. Hiding above 40 NM for now, to prevent clutter/lag.
97                         { name:'vor', disabled:1, update_on:['toggle_range','toggle_stations','toggle_display_mode'],
98                                 predicate: func(nd, layer) {
99                                         # print("Running vor layer predicate");
100                                         var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
101                                         if(visible) {
102                                                 trigger_update( layer ); # clear & redraw
103                                         }
104                                         layer._view.setVisible( nd.get_switch('toggle_stations') );
105                                 }, # end of layer update predicate
106                         }, # end of VOR layer
107                         { name:'VOR', isMapStructure:1, update_on:['toggle_range','toggle_stations','toggle_display_mode'],
108                                 # FIXME: this is a really ugly place for controller code
109                                 predicate: func(nd, layer) {
110                                         # print("Running vor layer predicate");
111                                         # toggle visibility here
112                                         var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
113                                         layer.group.setVisible( visible );
114                                         if (visible) {
115                                                 #print("Updating MapStructure ND layer: VOR");
116                                                 # (Hopefully) smart update
117                                                 layer.update();
118                                         }
119                                 }, # end of layer update predicate
120                         }, # end of VOR layer
121
122                         # Should distinct between low and high altitude navaids. Hiding above 40 NM for now, to prevent clutter/lag.
123                         { name:'dme', disabled:1, update_on:['toggle_range','toggle_stations'],
124                                 predicate: func(nd, layer) {
125                                         var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
126                                         if(visible) {
127                                                 trigger_update( layer ); # clear & redraw
128                                         }
129                                         layer._view.setVisible( nd.get_switch('toggle_stations') );
130                                 }, # end of layer update predicate
131                         }, # end of DME layers
132                         { name:'DME', isMapStructure:1, update_on:['toggle_range','toggle_stations'],
133                                 # FIXME: this is a really ugly place for controller code
134                                 predicate: func(nd, layer) {
135                                         var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
136                                         # print("Running vor layer predicate");
137                                         # toggle visibility here
138                                         layer.group.setVisible( visible );
139                                         if (visible) {
140                                                 #print("Updating MapStructure ND layer: DME");
141                                                 # (Hopefully) smart update
142                                                 layer.update();
143                                         }
144                                 }, # end of layer update predicate
145                         }, # end of DME layer
146
147                         { name:'mp-traffic', disabled:1, update_on:['toggle_range','toggle_traffic'],
148                                 predicate: func(nd, layer) {
149                                         var visible = nd.get_switch('toggle_traffic');
150                                         layer._view.setVisible( visible );
151                                         if (visible) {
152                                                 trigger_update( layer ); # clear & redraw
153                                         }
154                                 }, # end of layer update predicate
155                         }, # end of traffic  layer
156                         { name:'TFC', isMapStructure:1, update_on:['toggle_range','toggle_traffic'],
157                                 predicate: func(nd, layer) {
158                                         var visible = nd.get_switch('toggle_traffic');
159                                         layer.group.setVisible( visible );
160                                         if (visible) {
161                                                 #print("Updating MapStructure ND layer: TFC");
162                                                 layer.update();
163                                         }
164                                 }, # end of layer update predicate
165                         }, # end of traffic  layer
166
167                         { name:'runway-nd', update_on:['toggle_range','toggle_display_mode'],
168                                 predicate: func(nd, layer) {
169                                         var visible = (nd.rangeNm() <= 40) and getprop("autopilot/route-manager/active") and nd.in_mode('toggle_display_mode', ['MAP','PLAN']) ;
170                                         if (visible)
171                                                 trigger_update( layer ); # clear & redraw
172                                         layer._view.setVisible( visible );
173                                 }, # end of layer update predicate
174                         }, # end of airports-nd layer
175
176                         { name:'route', update_on:['toggle_range','toggle_display_mode'],
177                                 predicate: func(nd, layer) {
178                                         var visible= (nd.in_mode('toggle_display_mode', ['MAP','PLAN']));
179                                         if (visible)
180                                                 trigger_update( layer ); # clear & redraw
181                                         layer._view.setVisible( visible );
182                                 }, # end of layer update predicate
183                         }, # end of route layer
184
185                         ## add other layers here, layer names must match the registered names as used in *.layer files for now
186                         ## this will all change once we're using Philosopher's MapStructure framework
187
188                 ], # end of vector with configured layers
189
190                 # This is where SVG elements are configured by providing "behavior" hashes, i.e. for animations
191
192                 # to animate each SVG symbol, specify behavior via callbacks (predicate, and true/false implementation)
193                 # SVG identifier, callback  etc
194                 # TODO: update_on([]), update_mode (update() vs. timers/listeners)
195                 # TODO: support putting symbols on specific layers
196                 features: [
197                         {
198                                 # TODO: taOnly doesn't need to use getprop polling in update(), use a listener instead!
199                                 id: 'taOnly', # the SVG ID
200                                 impl: { # implementation hash
201                                         init: func(nd, symbol), # for updateCenter stuff, called during initialization in the ctor
202                                         predicate: func(nd) getprop("instrumentation/tcas/inputs/mode") == 2, # the condition
203                                         is_true:   func(nd) nd.symbols.taOnly.show(),                   # if true, run this
204                                         is_false:  func(nd) nd.symbols.taOnly.hide(),                   # if false, run this
205                                 }, # end of taOnly  behavior/callbacks
206                         }, # end of taOnly
207                         {
208                                 id: 'tas',
209                                 impl: {
210                                         init: func(nd,symbol),
211                                         predicate: func(nd) nd.aircraft_source.get_spd() > 100,
212                                         is_true: func(nd) {
213                                                 nd.symbols.tas.setText(sprintf("%3.0f",getprop("/velocities/airspeed-kt") ));
214                                                 nd.symbols.tas.show();
215                                         },
216                                         is_false: func(nd) nd.symbols.tas.hide(),
217                                 },
218                         },
219                         {
220                                 id: 'tasLbl',
221                                 impl: {
222                                         init: func(nd,symbol),
223                                         predicate: func(nd) nd.aircraft_source.get_spd() > 100,
224                                         is_true: func(nd) nd.symbols.tasLbl.show(),
225                                         is_false: func(nd) nd.symbols.tasLbl.hide(),
226                                 },
227                         },
228                         {
229                                 id: 'wpActiveId',
230                                 impl: {
231                                         init: func(nd,symbol),
232                                         predicate: func(nd) getprop("/autopilot/route-manager/wp/id") != nil and getprop("autopilot/route-manager/active"),
233                                         is_true: func(nd) {
234                                                 nd.symbols.wpActiveId.setText(getprop("/autopilot/route-manager/wp/id"));
235                                                 nd.symbols.wpActiveId.show();
236                                         },
237                                         is_false: func(nd) nd.symbols.wpActiveId.hide(),
238                                 }, # of wpActiveId.impl
239                         }, # of wpActiveId
240                         {
241                                 id: 'wpActiveDist',
242                                 impl: {
243                                         init: func(nd,symbol),
244                                         predicate: func(nd) getprop("/autopilot/route-manager/wp/dist") != nil and getprop("autopilot/route-manager/active"),
245                                         is_true: func(nd) {
246                                                 nd.symbols.wpActiveDist.setText(sprintf("%3.01f",getprop("/autopilot/route-manager/wp/dist")));
247                                                 nd.symbols.wpActiveDist.show();
248                                         },
249                                         is_false: func(nd) nd.symbols.wpActiveDist.hide(),
250                                 },
251                         },
252                         {
253                                 id: 'wpActiveDistLbl',
254                                 impl: {
255                                         init: func(nd,symbol),
256                                         predicate: func(nd) getprop("/autopilot/route-manager/wp/dist") != nil and getprop("autopilot/route-manager/active"),
257                                         is_true: func(nd) nd.symbols.wpActiveDistLbl.show(),
258                                         is_false: func(nd) nd.symbols.wpActiveDistLbl.hide(),
259                                 },
260                         },
261                         {
262                                 id: 'eta',
263                                 impl: {
264                                         init: func(nd,symbol),
265                                         predicate: func(nd) getprop("autopilot/route-manager/wp/eta") != nil and getprop("autopilot/route-manager/active"),
266                                         is_true: func(nd) {
267                                                 var etaSec = getprop("/sim/time/utc/day-seconds")+getprop("autopilot/route-manager/wp/eta-seconds");
268                                                 var h = math.floor(etaSec/3600);
269                                                 if (h>24) h=h-24;
270                                                 etaSec=etaSec-3600*h;
271                                                 var m = math.floor(etaSec/60);
272                                                 etaSec=etaSec-60*m;
273                                                 var s = etaSec/10;
274                                                 nd.symbols.eta.setText(sprintf("%02.0f%02.0f.%01.0fz",h,m,s));
275                                                 nd.symbols.eta.show();
276                                         },
277                                         is_false: func(nd) nd.symbols.eta.hide(),
278                                 },  # of eta.impl
279                         }, # of eta
280                         {
281                                 id:'hdg',
282                                 impl: {
283                                         init: func(nd,symbol),
284                                         predicate: ALWAYS, # always true
285                                         is_true: func(nd) nd.symbols.hdg.setText(sprintf("%03.0f", nd.aircraft_source.get_hdg_mag() )),
286                                         is_false: NOTHING,
287                                 }, # of hdg.impl
288                         }, # of hdg
289                         {
290                                 id:'gs',
291                                 impl: {
292                                         init: func(nd,symbol),
293                                         common: func(nd) nd.symbols.gs.setText(sprintf("%3.0f",nd.aircraft_source.get_gnd_spd() )),
294                                         predicate: func(nd) nd.aircraft_source.get_gnd_spd() >= 30,
295                                         is_true: func(nd) {
296                                                 nd.symbols.gs.setFontSize(36);
297                                         },
298                                         is_false: func(nd) nd.symbols.gs.setFontSize(52),
299                                 },
300                         },
301                         {
302                                 id:'rangeArcs',
303                                 impl: {
304                                         init: func(nd,symbol),
305                                         predicate: func(nd) ((((nd.get_switch('toggle_display_mode') == "APP" or nd.get_switch('toggle_display_mode') == "VOR") and nd.get_switch('toggle_weather')) or nd.get_switch('toggle_display_mode') == "MAP") and (!nd.get_switch('toggle_centered'))),
306                                         is_true: func(nd) nd.symbols.rangeArcs.show(),
307                                         is_false: func(nd) nd.symbols.rangeArcs.hide(),
308                                 }, # of rangeArcs.impl
309                         }, # of rangeArcs
310                         {
311                                 id:'rangePln1',
312                                 impl: {
313                                         init: func(nd,symbol),
314                                         predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
315                                         is_true: func(nd) { 
316                                                 nd.symbols.rangePln1.show();
317                                                 nd.symbols.rangePln1.setText(sprintf("%3.0f",nd.rangeNm()));
318                                         },
319                                         is_false: func(nd) nd.symbols.rangePln1.hide(),
320                                 },
321                         },
322                         {
323                                 id:'rangePln2',
324                                 impl: {
325                                         init: func(nd,symbol),
326                                         predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
327                                         is_true: func(nd) { 
328                                                 nd.symbols.rangePln2.show();
329                                                 nd.symbols.rangePln2.setText(sprintf("%3.0f",nd.rangeNm()/2));
330                                         },
331                                         is_false: func(nd) nd.symbols.rangePln2.hide(),
332                                 },
333                         },
334                         {
335                                 id:'rangePln3',
336                                 impl: {
337                                         init: func(nd,symbol),
338                                         predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
339                                         is_true: func(nd) { 
340                                                 nd.symbols.rangePln3.show();
341                                                 nd.symbols.rangePln3.setText(sprintf("%3.0f",nd.rangeNm()/2));
342                                         },
343                                         is_false: func(nd) nd.symbols.rangePln3.hide(),
344                                 },
345                         },
346                         {
347                                 id:'rangePln4',
348                                 impl: {
349                                         init: func(nd,symbol),
350                                         predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
351                                         is_true: func(nd) { 
352                                                 nd.symbols.rangePln4.show();
353                                                 nd.symbols.rangePln4.setText(sprintf("%3.0f",nd.rangeNm()));
354                                         },
355                                         is_false: func(nd) nd.symbols.rangePln4.hide(),
356                                 },
357                         },
358                         {
359                                 id:'windArrow',
360                                 impl: {
361                                         init: func(nd,symbol),
362                                         predicate: func(nd) (!(nd.in_mode('toggle_display_mode', ['PLAN']) and (nd.get_switch('toggle_display_type') == "LCD")) and nd.aircraft_source.get_spd() > 100),
363                                         is_true: func(nd) {
364                                                 nd.symbols.windArrow.show();
365                                                 var windArrowRot = (nd.aircraft_source.get_hdg_tru()-nd.aircraft_source.get_trk_tru())*D2R;
366                                                 if(nd.in_mode('toggle_display_mode', ['MAP','PLAN']))
367                                                         windArrowRot = 2*windArrowRot;
368                                                 nd.symbols.windArrow.setRotation(windArrowRot);
369                                         },
370                                         is_false: func(nd) nd.symbols.windArrow.hide(),
371                                 },
372                         },
373
374                 ], # end of vector with features
375
376
377         }, # end of Boeing style
378 #####
379 ##
380 ## add support for other aircraft/ND types and styles here (Airbus etc)
381 ##
382 ##
383
384 }; # end of NDStyles
385
386 ##
387 # 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)
388 #
389
390 var NDSourceDriver = {};
391 NDSourceDriver.new = func {
392         var m = {parents:[NDSourceDriver]};
393         m.get_hdg_mag= func getprop("/orientation/heading-magnetic-deg");
394         m.get_hdg_tru= func getprop("/orientation/heading-deg");
395         m.get_hgg = func getprop("instrumentation/afds/settings/heading");
396         m.get_trk_mag= func
397         {
398                 if(getprop("/velocities/groundspeed-kt") > 80)
399                 {
400                         getprop("/orientation/track-magnetic-deg");
401                 }
402                 else
403                 {
404                         getprop("/orientation/heading-magnetic-deg");
405                 }
406         };
407         m.get_trk_tru = func
408         {
409                 if(getprop("/velocities/groundspeed-kt") > 80)
410                 {
411                         getprop("/orientation/track-deg");
412                 }
413                 else
414                 {
415                         getprop("/orientation/heading-deg");
416                 }
417         };
418         m.get_lat= func getprop("/position/latitude-deg");
419         m.get_lon= func getprop("/position/longitude-deg");
420         m.get_spd= func getprop("/velocities/airspeed-kt");
421         m.get_gnd_spd= func getprop("/velocities/groundspeed-kt");
422         m.get_vspd= func getprop("/velocities/vertical-speed-fps");
423         return m;
424 }
425
426 ##
427 # configure aircraft specific cockpit switches here
428 # these are some defaults, can be overridden when calling NavDisplay.new() -
429 # see the 744 ND.nas file the backend code should never deal directly with
430 # aircraft specific properties using getprop.
431 # To get started implementing your own ND, just copy the switches hash to your
432 # ND.nas file and map the keys to your cockpit properties - and things will just work.
433
434 # TODO: switches are ND specific, so move to the NDStyle hash!
435
436 var default_switches = {
437         'toggle_range':        {path: '/inputs/range-nm', value:40, type:'INT'},
438         'toggle_weather':      {path: '/inputs/wxr', value:0, type:'BOOL'},
439         'toggle_airports':     {path: '/inputs/arpt', value:0, type:'BOOL'},
440         'toggle_stations':     {path: '/inputs/sta', value:0, type:'BOOL'},
441         'toggle_waypoints':    {path: '/inputs/wpt', value:0, type:'BOOL'},
442         'toggle_position':     {path: '/inputs/pos', value:0, type:'BOOL'},
443         'toggle_data':         {path: '/inputs/data',value:0, type:'BOOL'},
444         'toggle_terrain':      {path: '/inputs/terr',value:0, type:'BOOL'},
445         'toggle_traffic':      {path: '/inputs/tfc',value:0, type:'BOOL'},
446         'toggle_centered':     {path: '/inputs/nd-centered',value:0, type:'BOOL'},
447         'toggle_lh_vor_adf':   {path: '/inputs/lh-vor-adf',value:0, type:'INT'},
448         'toggle_rh_vor_adf':   {path: '/inputs/rh-vor-adf',value:0, type:'INT'},
449         'toggle_display_mode': {path: '/mfd/display-mode', value:'MAP', type:'STRING'}, # valid values are: APP, MAP, PLAN or VOR
450         'toggle_display_type': {path: '/mfd/display-type', value:'CRT', type:'STRING'}, # valid values are: CRT or LCD
451         'toggle_true_north':   {path: '/mfd/true-north', value:0, type:'BOOL'},
452 };
453
454 # Hack to update weather radar once every 10 seconds
455 var update_weather = func {
456         if (getprop("/instrumentation/efis/inputs/wxr") != nil)
457                 setprop("/instrumentation/efis/inputs/wxr",getprop("/instrumentation/efis/inputs/wxr"));
458         settimer(update_weather, 10);
459 }
460 update_weather();
461
462 # Hack to update airplane symbol location on PLAN mode every 5 seconds
463 var update_apl_sym = func {
464         if (getprop("/instrumentation/efis/mfd/display-mode") == "PLAN")
465                 setprop("/instrumentation/efis/mfd/display-mode","PLAN");
466         settimer(update_apl_sym, 5);
467 }
468 update_apl_sym();
469
470 ##
471 # TODO:
472 # - introduce a MFD class (use it also for PFD/EICAS)
473 # - introduce a SGSubsystem class and use it  here
474 # - introduce a Boeing NavDisplay class
475 var NavDisplay = {
476
477         # reset handler
478         del: func {
479                 print("Cleaning up NavDisplay");
480                 # shut down all timers and other loops here
481                 me.update_timer.stop();
482                 foreach(var l; me.listeners)
483                         removelistener(l);
484                 # clean up MapStructure
485                 me.map.del();
486                 # destroy the canvas
487                 if (me.canvas_handle != nil)
488                         me.canvas_handle.del();
489                 me.inited = 0;
490         },
491
492         listen: func(p,c) {
493                 append(me.listeners, setlistener(p,c));
494         },
495
496         # listeners for cockpit switches
497         listen_switch: func(s,c) {
498                 # print("event setup for: ", id(c));
499                 me.listen( me.get_full_switch_path(s), func {
500                         # print("listen_switch triggered:", s, " callback id:", id(c) );
501                         c();
502                 });
503         },
504
505         # get the full property path for a given switch
506         get_full_switch_path: func (s) {
507                 # debug.dump( me.efis_switches[s] );
508                 return me.efis_path ~ me.efis_switches[s].path; # FIXME: should be using props.nas instead of ~
509         },
510
511         # helper method for getting configurable cockpit switches (which are usually different in each aircraft)
512         get_switch: func(s) {
513                 var switch = me.efis_switches[s];
514                 var path = me.efis_path ~ switch.path ;
515                 #print(s,":Getting switch prop:", path);
516
517                 return getprop( path );
518         },
519
520         # for creating NDs that are driven by AI traffic instead of the main aircraft (generalization rocks!)
521         connectAI: func(source=nil) {
522                 me.aircraft_source = {
523                         get_hdg_mag: func source.getNode('orientation/heading-magnetic-deg').getValue(),
524                         get_trk_mag: func source.getNode('orientation/track-magnetic-deg').getValue(),
525                         get_lat: func source.getNode('position/latitude-deg').getValue(),
526                         get_lon: func source.getNode('position/longitude-deg').getValue(),
527                         get_spd: func source.getNode('velocities/true-airspeed-kt').getValue(),
528                         get_gnd_spd: func source.getNode('velocities/groundspeed-kt').getValue(),
529                 };
530         }, # of connectAI
531
532         # TODO: the ctor should allow customization, for different aircraft
533         # especially properties and SVG files/handles (747, 757, 777 etc)
534         new : func(prop1, switches=default_switches, style='Boeing') {
535                 var m = { parents : [NavDisplay]};
536
537                 m.inited = 0;
538
539                 m.listeners=[]; # for cleanup handling
540                 m.aircraft_source = NDSourceDriver.new(); # uses the main aircraft as the driver/source (speeds, position, heading)
541
542                 m.nd_style = NDStyles[style]; # look up ND specific stuff (file names etc)
543
544                 m.radio_list=["instrumentation/comm/frequencies","instrumentation/comm[1]/frequencies",
545                               "instrumentation/nav/frequencies", "instrumentation/nav[1]/frequencies"];
546                 m.mfd_mode_list=["APP","VOR","MAP","PLAN"];
547
548                 m.efis_path = prop1;
549                 m.efis_switches = switches;
550
551                 # just an alias, to avoid having to rewrite the old code for now
552                 m.rangeNm = func m.get_switch('toggle_range');
553
554                 m.efis = props.globals.initNode(prop1);
555                 m.mfd = m.efis.initNode("mfd");
556
557                 # TODO: unify this with switch handling
558                 m.mfd_mode_num     = m.mfd .initNode("mode-num",2,"INT");
559                 m.std_mode         = m.efis.initNode("inputs/setting-std",0,"BOOL");
560                 m.previous_set     = m.efis.initNode("inhg-previous",29.92);
561                 m.kpa_mode         = m.efis.initNode("inputs/kpa-mode",0,"BOOL");
562                 m.kpa_output       = m.efis.initNode("inhg-kpa",29.92);
563                 m.kpa_prevoutput   = m.efis.initNode("inhg-kpa-previous",29.92);
564                 m.temp             = m.efis.initNode("fixed-temp",0);
565                 m.alt_meters       = m.efis.initNode("inputs/alt-meters",0,"BOOL");
566                 m.fpv              = m.efis.initNode("inputs/fpv",0,"BOOL");
567
568                 m.mins_mode     = m.efis.initNode("inputs/minimums-mode",0,"BOOL");
569                 m.mins_mode_txt = m.efis.initNode("minimums-mode-text","RADIO","STRING");
570                 m.minimums      = m.efis.initNode("minimums",250,"INT");
571                 m.mk_minimums   = props.globals.getNode("instrumentation/mk-viii/inputs/arinc429/decision-height");
572
573                 # TODO: these are switches, can be unified with switch handling hash above (eventually):
574                 m.nd_plan_wpt = m.efis.initNode("inputs/plan-wpt-index", 0, "INT"); # not yet in switches hash
575
576                 ###
577                 # initialize all switches based on the defaults specified in the switch hash
578                 #
579                 foreach(var switch; keys( m.efis_switches ) )
580                         props.globals.initNode
581                                 (       m.get_full_switch_path (switch),
582                                         m.efis_switches[switch].value,
583                                         m.efis_switches[switch].type
584                                 );
585
586
587                 return m;
588         },
589         newMFD: func(canvas_group, parent=nil)
590         {
591                 if (me.inited) die("MFD already was added to scene");
592                 me.inited = 1;
593                 #me.listen("/sim/signals/reinit", func(n) me.del() );
594                 me.update_timer = maketimer(0.05, func me.update() ); # TODO: make interval configurable via ctor
595                 me.nd = canvas_group;
596                 me.canvas_handle = parent;
597
598                 # load the specified SVG file into the me.nd group and populate all sub groups
599
600                 canvas.parsesvg(me.nd, me.nd_style.svg_filename, {'font-mapper': me.nd_style.font_mapper});
601                 me.symbols = {}; # storage for SVG elements, to avoid namespace pollution (all SVG elements end up  here)
602
603                 foreach(var feature; me.nd_style.features ) {
604                         # print("Setting up SVG feature:", feature.id);
605                         me.symbols[feature.id] = me.nd.getElementById(feature.id).updateCenter();
606                         if(contains(feature.impl,'init')) feature.impl.init(me.nd, feature); # call The element's init code (i.e. updateCenter)
607                 }
608
609                 ### this is the "old" method that's less flexible, we want to use the style hash instead (see above)
610                 # because things are much better configurable that way
611                 # now look up all required SVG elements and initialize member fields using the same name  to have a convenient handle
612                 foreach(var element; ["wind","dmeLDist","dmeRDist","dmeL","dmeR","vorL","vorR","vorLId","vorRId",
613                                       "range","status.wxr","status.wpt","hdgGroup","status.sta","status.arpt"])
614                         me.symbols[element] = me.nd.getElementById(element);
615
616                 # load elements from vector image, and create instance variables using identical names, and call updateCenter() on each
617                 # anything that needs updatecenter called, should be added to the vector here
618                 #
619                 foreach(var element; ["compassApp","northUp","aplSymMap","aplSymMapCtr","aplSymVor",
620                                       "staFromL2","staToL2","staFromR2","staToR2",
621                                       "locPtr","hdgTrk","truMag","altArc","planArcs",
622                                       "trkInd","compass","HdgBugCRT","TrkBugLCD","HdgBugLCD","selHdgLine","curHdgPtr",
623                                       "staFromL","staToL","staFromR","staToR"] )
624                         me.symbols[element] = me.nd.getElementById(element).updateCenter();
625
626                 foreach(var element; ["HdgBugCRT2","TrkBugLCD2","HdgBugLCD2","selHdgLine2","curHdgPtr2","vorCrsPtr2"] )
627                         me.symbols[element] = me.nd.getElementById(element).setCenter(512,565);
628
629                 # this should probably be using Philosopher's new SymbolLayer ?
630                 me.map = me.nd.createChild("map","map")
631                         .set("clip", "rect(124, 1024, 1024, 0)")
632                         .set("screen-range", "700");
633
634                 # this callback will be passed onto the model via the controller hash, and used for the positioned queries, to specify max query range:
635                 var get_range = func me.get_switch('toggle_range');
636
637                 # predicate for the draw controller
638                 var is_tuned = func(freq) {
639                         var nav1=getprop("instrumentation/nav[0]/frequencies/selected-mhz");
640                         var nav2=getprop("instrumentation/nav[1]/frequencies/selected-mhz");
641                         if (freq == nav1 or freq == nav2) return 1;
642                         return 0;
643                 }
644
645                 # another predicate for the draw controller
646                 var get_course_by_freq = func(freq) {
647                         if (freq == getprop("instrumentation/nav[0]/frequencies/selected-mhz"))
648                                 return getprop("instrumentation/nav[0]/radials/selected-deg");
649                         else
650                                 return getprop("instrumentation/nav[1]/radials/selected-deg");
651                 }
652
653                 var get_current_position = func {
654                         delete(caller(0)[0], "me"); # remove local me, inherit outer one
655                         return [
656                                 me.aircraft_source.get_lat(), me.aircraft_source.get_lon()
657                         ];
658                 }
659
660                 # a hash with controller callbacks, will be passed onto draw routines to customize behavior/appearance
661                 # the point being that draw routines don't know anything about their frontends (instrument or GUI dialog)
662                 # so we need some simple way to communicate between frontend<->backend until we have real controllers
663                 # for now, a single controller hash is shared by most layers - unsupported callbacks are simply ignored by the draw files
664                 #
665                 var controller = {
666                         query_range: func get_range(),
667                         is_tuned:is_tuned,
668                         get_tuned_course:get_course_by_freq,
669                         get_position: get_current_position,
670                 };
671
672                 # FIXME: MapStructure: big hack
673                 canvas.Symbol.Controller.get("VOR").query_range = controller.query_range;
674                 canvas.Symbol.Controller.get("VOR").get_tuned_course = controller.get_tuned_course;
675                 canvas.Symbol.Controller.get("DME").is_tuned = controller.is_tuned;
676                 canvas.SymbolLayer.Controller.get("TFC").query_range = controller.query_range;
677                 canvas.SymbolLayer.Controller.get("TFC").get_position = controller.get_position;
678
679                 ###
680                 # set up various layers, controlled via callbacks in the controller hash
681                 # revisit this code once Philosopher's "Smart MVC Symbols/Layers" work is committed and integrated
682
683                 # helper / closure generator
684                 var make_event_handler = func(predicate, layer) func predicate(me, layer);
685
686                 me.layers={}; # storage container for all ND specific layers
687                 # look up all required layers as specified per the NDStyle hash and do the initial setup for event handling
688                 foreach(var layer; me.nd_style.layers) {
689                         if(layer['disabled']) continue; # skip this layer
690                         #print("newMFD(): Setting up ND layer:", layer.name);
691                         # huge hack for the alt-arc, which is not rendered as a map group, but directly as part of the toplevel ND group
692                         var render_target = (!contains(layer,'not_a_map') or !layer.not_a_map) ? me.map : me.nd;
693
694                         var the_layer = nil;
695                         if(!layer['isMapStructure'])
696                                 the_layer = me.layers[layer.name] = canvas.MAP_LAYERS[layer.name].new( render_target, layer.name, controller );
697                         else {
698                                 printlog(_MP_dbg_lvl, "Setting up MapStructure-based layer for ND, name:", layer.name);
699                                 render_target.addLayer(factory: canvas.SymbolLayer, type_arg: layer.name);
700                                 the_layer = me.layers[layer.name] = render_target.getLayer(layer.name);
701                         }
702
703                         # now register all layer specific notification listeners and their corresponding update predicate/callback
704                         # pass the ND instance and the layer handle to the predicate when it is called
705                         # so that it can directly access the ND instance and its own layer (without having to know the layer's name)
706                         var event_handler = make_event_handler(layer.predicate, the_layer);
707                         foreach(var event; layer.update_on) {
708                                 # print("Setting up subscription:", event, " for ", layer.name, " handler id:", id(event_handler) );
709                                 me.listen_switch(event, event_handler);
710                         } # foreach event subscription
711                         # and now update/init each layer once by calling its update predicate for initialization
712                         event_handler();
713                 } # foreach layer
714
715                 #print("navdisplay.mfd:ND layer setup completed");
716
717                 # start the update timer, which makes sure that the update() will be called
718                 me.update_timer.start();
719
720
721                 # next, radio & autopilot & listeners
722                 # TODO: move this to .init field in layers hash or to model files
723                 foreach(var n; var radios = [
724                               "instrumentation/nav/frequencies/selected-mhz",
725                               "instrumentation/nav[1]/frequencies/selected-mhz"])
726                         me.listen(n, func() {
727                         #       me.drawvor();
728                         #       me.drawdme();
729                         });
730                 # TODO: move this to the route.model
731                 me.listen("/autopilot/route-manager/current-wp", func(activeWp) {
732                         canvas.updatewp( activeWp.getValue() );
733                 });
734
735         },
736
737         in_mode:func(switch, modes)
738         {
739                 foreach(var m; modes) if(me.get_switch(switch)==m) return 1;
740                 return 0;
741         },
742         # each model should keep track of when it last got updated, using current lat/lon
743         # in update(), we can then check if the aircraft has traveled more than 0.5-1 nm (depending on selected range)
744         # and update each model accordingly
745         update: func() # FIXME: This stuff is still too aircraft specific, cannot easily be reused by other aircraft
746         {
747                 ##
748                 # important constants
749                 var m1 = 111132.92;
750                 var m2 = -559.82;
751                 var m3 = 1.175;
752                 var m4 = -0.0023;
753                 var p1 = 111412.84;
754                 var p2 = -93.5;
755                 var p3 = 0.118;
756                 var latNm = 60;
757                 var lonNm = 60;
758
759                 # fgcommand('profiler-start');
760                 # Heading update
761                 var userHdgMag = me.aircraft_source.get_hdg_mag();
762                 var userHdgTru = me.aircraft_source.get_hdg_tru();
763                 var userTrkMag = me.aircraft_source.get_trk_mag();
764                 var userTrkTru = me.aircraft_source.get_trk_tru();
765                 if(me.get_switch('toggle_true_north')) {
766                         me.symbols.truMag.setText("TRU");
767                         var userHdg=userHdgTru;
768                         var userTrk=userTrkTru;
769                 } else {
770                         me.symbols.truMag.setText("MAG");
771                         var userHdg=userHdgMag;
772                         var userTrk=userTrkMag;
773                 }
774                 if (me.aircraft_source.get_gnd_spd() < 80)
775                         userTrk = userHdg;
776                 var userLat = me.aircraft_source.get_lat();
777                 var userLon = me.aircraft_source.get_lon();
778                 var userGndSpd = me.aircraft_source.get_gnd_spd();
779                 var userVSpd = me.aircraft_source.get_vspd();
780                 var dispLCD = me.get_switch('toggle_display_type') == "LCD";
781
782                 # this should only ever happen when testing the experimental AI/MP ND driver hash (not critical)
783                 if (!userHdg or !userTrk or !userLat or !userLon) {
784                         print("aircraft source invalid, returning !");
785                         return;
786                 }
787
788                 if(me.in_mode('toggle_display_mode', ['PLAN']))
789                         me.map.setTranslation(512,512);
790                 elsif(me.get_switch('toggle_centered'))
791                         me.map.setTranslation(512,565);
792                 else
793                         me.map.setTranslation(512,824);
794                 # Calculate length in NM of one degree at current location TODO: expose as methods, for external callbacks
795                 var userLatR = userLat*D2R;
796                 var userLonR = userLon*D2R;
797                 var latlen = m1 + (m2 * math.cos(2 * userLatR)) + (m3 * math.cos(4 * userLatR)) + (m4 * math.cos(6 * userLatR));
798                 var lonlen = (p1 * math.cos(userLatR)) + (p2 * math.cos(3 * userLatR)) + (p3 * math.cos(5 * userLatR));
799                 latNm = latlen*M2NM; #60 at equator
800                 lonNm = lonlen*M2NM; #60 at equator
801
802                 me.symbols.wind.setText(sprintf("%3.0f / %2.0f",getprop("/environment/wind-from-heading-deg"),getprop("/environment/wind-speed-kt")));
803
804                 if(me.get_switch('toggle_lh_vor_adf') == 1)
805                 {
806                         me.symbols.vorL.setText("VOR L");
807                         me.symbols.vorL.setColor(0.195,0.96,0.097);
808                         me.symbols.dmeL.setText("DME");
809                         me.symbols.dmeL.setColor(0.195,0.96,0.097);
810                         if(getprop("instrumentation/nav/in-range"))
811                                 me.symbols.vorLId.setText(getprop("instrumentation/nav/nav-id"));
812                         else
813                                 me.symbols.vorLId.setText(getprop("instrumentation/nav/frequencies/selected-mhz-fmt"));
814                         me.symbols.vorLId.setColor(0.195,0.96,0.097);
815                         if(getprop("instrumentation/nav/dme-in-range"))
816                                 me.symbols.dmeLDist.setText(sprintf("%3.1f",getprop("instrumentation/nav/nav-distance")*0.000539));
817                         else me.symbols.dmeLDist.setText(" ---");
818                         me.symbols.dmeLDist.setColor(0.195,0.96,0.097);
819                 } elsif(me.get_switch('toggle_lh_vor_adf') == -1) {
820                         me.symbols.vorL.setText("ADF L");
821                         me.symbols.vorL.setColor(0,0.6,0.85);
822                         me.symbols.dmeL.setText("");
823                         me.symbols.dmeL.setColor(0,0.6,0.85);
824                         if((var navident=getprop("instrumentation/adf/ident")) != "")
825                                 me.symbols.vorLId.setText(navident);
826                         else me.symbols.vorLId.setText(sprintf("%3d",getprop("instrumentation/adf/frequencies/selected-khz")));
827                         me.symbols.vorLId.setColor(0,0.6,0.85);
828                         me.symbols.dmeLDist.setText("");
829                         me.symbols.dmeLDist.setColor(0,0.6,0.85);
830                 } else {
831                         me.symbols.vorL.setText("");
832                         me.symbols.dmeL.setText("");
833                         me.symbols.vorLId.setText("");
834                         me.symbols.dmeLDist.setText("");
835                 }
836                 if(me.get_switch('toggle_rh_vor_adf') == 1) {
837                         me.symbols.vorR.setText("VOR R");
838                         me.symbols.vorR.setColor(0.195,0.96,0.097);
839                         me.symbols.dmeR.setText("DME");
840                         me.symbols.dmeR.setColor(0.195,0.96,0.097);
841                         if(getprop("instrumentation/nav[1]/in-range"))
842                                 me.symbols.vorRId.setText(getprop("instrumentation/nav[1]/nav-id"));
843                         else
844                                 me.symbols.vorRId.setText(getprop("instrumentation/nav[1]/frequencies/selected-mhz-fmt"));
845                         me.symbols.vorRId.setColor(0.195,0.96,0.097);
846                         if(getprop("instrumentation/nav[1]/dme-in-range"))
847                                 me.symbols.dmeRDist.setText(sprintf("%3.1f",getprop("instrumentation/nav[1]/nav-distance")*0.000539));
848                         else me.symbols.dmeRDist.setText(" ---");
849                         me.symbols.dmeRDist.setColor(0.195,0.96,0.097);
850                 } elsif(me.get_switch('toggle_rh_vor_adf') == -1) {
851                         me.symbols.vorR.setText("ADF R");
852                         me.symbols.vorR.setColor(0,0.6,0.85);
853                         me.symbols.dmeR.setText("");
854                         me.symbols.dmeR.setColor(0,0.6,0.85);
855                         if((var navident=getprop("instrumentation/adf[1]/ident")) != "")
856                                 me.symbols.vorRId.setText(navident);
857                         else me.symbols.vorRId.setText(sprintf("%3d",getprop("instrumentation/adf[1]/frequencies/selected-khz")));
858                         me.symbols.vorRId.setColor(0,0.6,0.85);
859                         me.symbols.dmeRDist.setText("");
860                         me.symbols.dmeRDist.setColor(0,0.6,0.85);
861                 } else {
862                         me.symbols.vorR.setText("");
863                         me.symbols.dmeR.setText("");
864                         me.symbols.vorRId.setText("");
865                         me.symbols.dmeRDist.setText("");
866                 }
867
868                 me.symbols.range.setText(sprintf("%3.0f",me.rangeNm()/2));
869
870                 # reposition the map, change heading & range:
871                 if(me.in_mode('toggle_display_mode', ['PLAN'])) {
872                         me.map._node.getNode("hdg",1).setDoubleValue(0);
873                         if (getprop(me.efis_path ~ "/inputs/plan-wpt-index") >= 0) {
874                                 me.map._node.getNode("ref-lat",1).setDoubleValue(getprop("/autopilot/route-manager/route/wp["~getprop(me.efis_path ~ "/inputs/plan-wpt-index")~"]/latitude-deg"));
875                                 me.map._node.getNode("ref-lon",1).setDoubleValue(getprop("/autopilot/route-manager/route/wp["~getprop(me.efis_path ~ "/inputs/plan-wpt-index")~"]/longitude-deg"));
876                         }
877                 } else {
878                         me.map._node.getNode("ref-lat",1).setDoubleValue(userLat);
879                         me.map._node.getNode("ref-lon",1).setDoubleValue(userLon);
880                 }
881                 # The set range of the map does not correspond to what we see in-sim!!
882                 me.map._node.getNode("range",1).setDoubleValue(me.rangeNm()); # avoid this  here, use a listener instead
883
884                 # Hide heading bug 10 secs after change
885                 var vhdg_bug = getprop("autopilot/settings/heading-bug-deg");
886                 var hdg_bug_active = getprop("autopilot/settings/heading-bug-active");
887                 if (hdg_bug_active == nil)
888                         hdg_bug_active = 1;
889                                                 
890                 if(me.in_mode('toggle_display_mode', ['MAP'])) {
891                         me.symbols.HdgBugCRT.setRotation((vhdg_bug-userTrk)*D2R);
892                         me.symbols.HdgBugLCD.setRotation((vhdg_bug-userTrk)*D2R);
893                         me.symbols.TrkBugLCD.setRotation((vhdg_bug-userTrk)*D2R);
894                         me.symbols.selHdgLine.setRotation((vhdg_bug-userTrk)*D2R);
895                         me.symbols.HdgBugCRT2.setRotation((vhdg_bug-userTrk)*D2R);
896                         me.symbols.TrkBugLCD2.setRotation((vhdg_bug-userTrk)*D2R);
897                         me.symbols.selHdgLine2.setRotation((vhdg_bug-userTrk)*D2R);
898                         me.symbols.trkInd.setRotation(0);
899                         me.symbols.curHdgPtr.setRotation((userHdg-userTrk)*D2R);
900                         me.symbols.curHdgPtr2.setRotation((userHdg-userTrk)*D2R);
901                         me.map._node.getNode("hdg",1).setDoubleValue(userTrkTru);
902                         me.symbols.compass.setRotation(-userTrk*D2R);
903                         me.symbols.compassApp.setRotation(-userTrk*D2R);
904                         me.symbols.hdgTrk.setText("TRK");
905                 }
906                 if(me.in_mode('toggle_display_mode', ['APP','VOR'])) {
907                         me.symbols.HdgBugCRT.setRotation((vhdg_bug-userHdg)*D2R);
908                         me.symbols.HdgBugLCD.setRotation((vhdg_bug-userHdg)*D2R);
909                         me.symbols.selHdgLine.setRotation((vhdg_bug-userHdg)*D2R);
910                         me.symbols.HdgBugCRT2.setRotation((vhdg_bug-userHdg)*D2R);
911                         me.symbols.HdgBugLCD2.setRotation((vhdg_bug-userHdg)*D2R);
912                         me.symbols.selHdgLine2.setRotation((vhdg_bug-userHdg)*D2R);
913                         me.symbols.trkInd.setRotation((userTrk-userHdg)*D2R);
914                         me.symbols.curHdgPtr.setRotation(0);
915                         me.symbols.curHdgPtr2.setRotation(0);
916                         me.map._node.getNode("hdg",1).setDoubleValue(userHdgTru);
917                         me.symbols.compass.setRotation(-userHdg*D2R);
918                         me.symbols.compassApp.setRotation(-userHdg*D2R);
919                         me.symbols.hdgTrk.setText("HDG");
920                 }
921                 if(me.get_switch('toggle_centered')) {
922                         if (me.in_mode('toggle_display_mode', ['APP','VOR'])) {
923                                 me.symbols.vorCrsPtr2.show();
924                                 me.symbols.compassApp.show();
925                                 if(getprop("instrumentation/nav/in-range")) {
926                                         var deflection = getprop("instrumentation/nav/heading-needle-deflection-norm");
927                                         me.symbols.locPtr.show();
928                                         me.symbols.locPtr.setTranslation(deflection*150,0);
929                                         if(abs(deflection < 0.99))
930                                                 me.symbols.locPtr.setColorFill(1,0,1,1);
931                                         else
932                                                 me.symbols.locPtr.setColorFill(1,0,1,0);
933                                 } else {
934                                         me.symbols.locPtr.hide();
935                                 }
936                                 me.symbols.vorCrsPtr2.setRotation((getprop("instrumentation/nav/radials/selected-deg")-userHdg)*D2R);
937                                 me.symbols.hdgGroup.setTranslation(0,100);
938                         } else {
939                                 me.symbols.vorCrsPtr2.hide();
940                                 me.symbols.hdgGroup.setTranslation(0,100*me.in_mode('toggle_display_mode', ['MAP']));
941                                 me.symbols.compassApp.setVisible(me.in_mode('toggle_display_mode', ['MAP']));
942                         }
943                 } else {
944                         me.symbols.vorCrsPtr2.hide();
945                         me.symbols.hdgGroup.setTranslation(0,0);
946                         me.symbols.compassApp.hide();
947                 }
948
949                 if ((me.get_switch('toggle_centered') and !me.in_mode('toggle_display_mode', ['PLAN'])) or me.in_mode('toggle_display_mode', ['PLAN'])) {
950                         me.symbols.compass.hide();
951                 } else {
952                         me.symbols.compass.show();
953                 }
954
955                 var staPtrVis = !me.in_mode('toggle_display_mode', ['APP','PLAN']);
956                 var magVar = getprop("environment/magnetic-variation-deg");
957                 if(me.in_mode('toggle_display_mode', ['APP','MAP','VOR','PLAN']))
958                 {
959                         if(getprop("instrumentation/nav/heading-deg") != nil)
960                                 var nav0hdg=getprop("instrumentation/nav/heading-deg") - userHdg - magVar;
961                         if(getprop("instrumentation/nav[1]/heading-deg") != nil)
962                                 var nav1hdg=getprop("instrumentation/nav[1]/heading-deg") - userHdg - magVar;
963                         var adf0hdg=getprop("instrumentation/adf/indicated-bearing-deg");
964                         var adf1hdg=getprop("instrumentation/adf[1]/indicated-bearing-deg");
965                         if(!me.get_switch('toggle_centered'))
966                         {
967                                 if(me.in_mode('toggle_display_mode', ['PLAN']))
968                                         me.symbols.trkInd.hide();
969                                 else
970                                         me.symbols.trkInd.show();
971                                 if((getprop("instrumentation/nav/in-range") and me.get_switch('toggle_lh_vor_adf') == 1)) {
972                                         me.symbols.staFromL.setVisible(staPtrVis);
973                                         me.symbols.staToL.setVisible(staPtrVis);
974                                         me.symbols.staFromL.setColor(0.195,0.96,0.097);
975                                         me.symbols.staToL.setColor(0.195,0.96,0.097);
976                                         me.symbols.staFromL.setRotation((nav0hdg+180)*D2R);
977                                         me.symbols.staToL.setRotation(nav0hdg*D2R);
978                                 }
979                                 elsif(getprop("instrumentation/adf/in-range") and (me.get_switch('toggle_lh_vor_adf') == -1)) {
980                                         me.symbols.staFromL.setVisible(staPtrVis);
981                                         me.symbols.staToL.setVisible(staPtrVis);
982                                         me.symbols.staFromL.setColor(0,0.6,0.85);
983                                         me.symbols.staToL.setColor(0,0.6,0.85);
984                                         me.symbols.staFromL.setRotation((adf0hdg+180)*D2R);
985                                         me.symbols.staToL.setRotation(adf0hdg*D2R);
986                                 } else {
987                                         me.symbols.staFromL.hide();
988                                         me.symbols.staToL.hide();
989                                 }
990                                 if((getprop("instrumentation/nav[1]/in-range") and me.get_switch('toggle_rh_vor_adf') == 1)) {
991                                         me.symbols.staFromR.setVisible(staPtrVis);
992                                         me.symbols.staToR.setVisible(staPtrVis);
993                                         me.symbols.staFromR.setColor(0.195,0.96,0.097);
994                                         me.symbols.staToR.setColor(0.195,0.96,0.097);
995                                         me.symbols.staFromR.setRotation((nav1hdg+180)*D2R);
996                                         me.symbols.staToR.setRotation(nav1hdg*D2R);
997                                 } elsif(getprop("instrumentation/adf[1]/in-range") and (me.get_switch('toggle_rh_vor_adf') == -1)) {
998                                         me.symbols.staFromR.setVisible(staPtrVis);
999                                         me.symbols.staToR.setVisible(staPtrVis);
1000                                         me.symbols.staFromR.setColor(0,0.6,0.85);
1001                                         me.symbols.staToR.setColor(0,0.6,0.85);
1002                                         me.symbols.staFromR.setRotation((adf1hdg+180)*D2R);
1003                                         me.symbols.staToR.setRotation(adf1hdg*D2R);
1004                                 } else {
1005                                         me.symbols.staFromR.hide();
1006                                         me.symbols.staToR.hide();
1007                                 }
1008                                 me.symbols.staFromL2.hide();
1009                                 me.symbols.staToL2.hide();
1010                                 me.symbols.staFromR2.hide();
1011                                 me.symbols.staToR2.hide();
1012                                 me.symbols.curHdgPtr2.hide();
1013                                 me.symbols.HdgBugCRT2.hide();
1014                                 me.symbols.TrkBugLCD2.hide();
1015                                 me.symbols.HdgBugLCD2.hide();
1016                                 me.symbols.selHdgLine2.hide();
1017                                 me.symbols.curHdgPtr.setVisible(staPtrVis);
1018                                 me.symbols.TrkBugLCD.hide();
1019                                 me.symbols.HdgBugCRT.setVisible(staPtrVis and !dispLCD);
1020                                 me.symbols.HdgBugLCD.setVisible(staPtrVis and dispLCD);
1021                                 me.symbols.selHdgLine.setVisible(staPtrVis and hdg_bug_active);
1022                         } else {
1023                                 me.symbols.trkInd.hide();
1024                                 if((getprop("instrumentation/nav/in-range")     and me.get_switch('toggle_lh_vor_adf') == 1)) {
1025                                         me.symbols.staFromL2.setVisible(staPtrVis);
1026                                         me.symbols.staToL2.setVisible(staPtrVis);
1027                                         me.symbols.staFromL2.setColor(0.195,0.96,0.097);
1028                                         me.symbols.staToL2.setColor(0.195,0.96,0.097);
1029                                         me.symbols.staFromL2.setRotation((nav0hdg+180)*D2R);
1030                                         me.symbols.staToL2.setRotation(nav0hdg*D2R);
1031                                 } elsif(getprop("instrumentation/adf/in-range") and (me.get_switch('toggle_lh_vor_adf') == -1)) {
1032                                         me.symbols.staFromL2.setVisible(staPtrVis);
1033                                         me.symbols.staToL2.setVisible(staPtrVis);
1034                                         me.symbols.staFromL2.setColor(0,0.6,0.85);
1035                                         me.symbols.staToL2.setColor(0,0.6,0.85);
1036                                         me.symbols.staFromL2.setRotation((adf0hdg+180)*D2R);
1037                                         me.symbols.staToL2.setRotation(adf0hdg*D2R);
1038                                 } else {
1039                                         me.symbols.staFromL2.hide();
1040                                         me.symbols.staToL2.hide();
1041                                 }
1042                                 if((getprop("instrumentation/nav[1]/in-range") and me.get_switch('toggle_rh_vor_adf') == 1)) {
1043                                         me.symbols.staFromR2.setVisible(staPtrVis);
1044                                         me.symbols.staToR2.setVisible(staPtrVis);
1045                                         me.symbols.staFromR2.setColor(0.195,0.96,0.097);
1046                                         me.symbols.staToR2.setColor(0.195,0.96,0.097);
1047                                         me.symbols.staFromR2.setRotation((nav1hdg+180)*D2R);
1048                                         me.symbols.staToR2.setRotation(nav1hdg*D2R);
1049                                 } elsif(getprop("instrumentation/adf[1]/in-range") and (me.get_switch('toggle_rh_vor_adf') == -1)) {
1050                                         me.symbols.staFromR2.setVisible(staPtrVis);
1051                                         me.symbols.staToR2.setVisible(staPtrVis);
1052                                         me.symbols.staFromR2.setColor(0,0.6,0.85);
1053                                         me.symbols.staToR2.setColor(0,0.6,0.85);
1054                                         me.symbols.staFromR2.setRotation((adf1hdg+180)*D2R);
1055                                         me.symbols.staToR2.setRotation(adf1hdg*D2R);
1056                                 } else {
1057                                         me.symbols.staFromR2.hide();
1058                                         me.symbols.staToR2.hide();
1059                                 }
1060                                 me.symbols.staFromL.hide();
1061                                 me.symbols.staToL.hide();
1062                                 me.symbols.staFromR.hide();
1063                                 me.symbols.staToR.hide();
1064                                 me.symbols.curHdgPtr.hide();
1065                                 me.symbols.HdgBugCRT.hide();
1066                                 me.symbols.TrkBugLCD.hide();
1067                                 me.symbols.HdgBugLCD.hide();
1068                                 me.symbols.selHdgLine.hide();
1069                                 me.symbols.curHdgPtr2.setVisible(staPtrVis);
1070                                 me.symbols.TrkBugLCD2.hide();
1071                                 me.symbols.HdgBugCRT2.setVisible(staPtrVis and !dispLCD);
1072                                 me.symbols.HdgBugLCD2.setVisible(staPtrVis and dispLCD);
1073                                 me.symbols.selHdgLine2.setVisible(staPtrVis and hdg_bug_active);
1074                         }
1075                 }
1076
1077                 me.symbols.hdgGroup.setVisible(!me.in_mode('toggle_display_mode', ['PLAN']));
1078                 me.symbols.northUp.setVisible(me.in_mode('toggle_display_mode', ['PLAN']));
1079                 me.symbols.aplSymMap.setVisible(me.in_mode('toggle_display_mode', ['APP','MAP','VOR']) and !me.get_switch('toggle_centered'));
1080                 me.symbols.aplSymMapCtr.setVisible(me.in_mode('toggle_display_mode', ['MAP']) and me.get_switch('toggle_centered'));
1081                 me.symbols.aplSymVor.setVisible(me.in_mode('toggle_display_mode', ['APP','VOR']) and me.get_switch('toggle_centered'));
1082                 me.symbols.planArcs.setVisible(me.in_mode('toggle_display_mode', ['PLAN']));
1083
1084                 if (abs(userVSpd) > 5) {
1085                         var altDiff = (getprop("autopilot/settings/target-altitude-ft") or 0)-(getprop("instrumentation/altimeter/indicated-altitude-ft") or 0);
1086                         if (abs(altDiff) > 50 and altDiff/userVSpd > 0) {
1087                                 var altRangeNm = altDiff/userVSpd*userGndSpd*KT2MPS*M2NM;
1088                                 if(altRangeNm > 1) {
1089                                         var altRangePx = (350/me.rangeNm())*altRangeNm;
1090                                         if (altRangePx > 700)
1091                                                 altRangePx = 700;
1092                                         me.symbols.altArc.setTranslation(0,-altRangePx);
1093                                 }
1094                                 me.symbols.altArc.show();
1095                         } else
1096                                 me.symbols.altArc.hide();
1097                 } else {
1098                         me.symbols.altArc.hide();
1099                 }
1100
1101                 ## run all predicates in the NDStyle hash and evaluate their true/false behavior callbacks
1102                 ## this is in line with the original design, but normally we don't need to getprop/poll here,
1103                 ## using listeners or timers would be more canvas-friendly whenever possible
1104                 ## because running setprop() on any group/canvas element at framerate means that the canvas
1105                 ## will be updated at frame rate too - wasteful ... (check the performance monitor!)
1106
1107                 foreach(var feature; me.nd_style.features ) {
1108                         # for stuff that always needs to be updated
1109                         if (contains(feature.impl, 'common')) feature.impl.common(me);
1110                         # conditional stuff
1111                         if(!contains(feature.impl, 'predicate')) continue; # no conditional stuff
1112                         if ( var result=feature.impl.predicate(me) )
1113                                 feature.impl.is_true(me, result); # pass the result to the predicate
1114                         else
1115                                 feature.impl.is_false( me, result ); # pass the result to the predicate
1116                 }
1117
1118                 ## update the status flags shown on the ND (wxr, wpt, arpt, sta)
1119                 # this could/should be using listeners instead ...
1120                 me.symbols['status.wxr'].setVisible( me.get_switch('toggle_weather') and me.in_mode('toggle_display_mode', ['MAP']));
1121                 me.symbols['status.wpt'].setVisible( me.get_switch('toggle_waypoints') and me.in_mode('toggle_display_mode', ['MAP']));
1122                 me.symbols['status.arpt'].setVisible( me.get_switch('toggle_airports') and me.in_mode('toggle_display_mode', ['MAP']));
1123                 me.symbols['status.sta'].setVisible( me.get_switch('toggle_stations') and  me.in_mode('toggle_display_mode', ['MAP']));
1124         }
1125 };