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